├── src └── main │ ├── resources │ ├── static │ │ ├── img │ │ │ ├── blank.gif │ │ │ ├── danbo.png │ │ │ ├── donateqr.png │ │ │ ├── fadegif.gif │ │ │ ├── osclogo.png │ │ │ ├── discord_qr.png │ │ │ ├── badges │ │ │ │ ├── nunchuk.gif │ │ │ │ ├── wii_remote.gif │ │ │ │ ├── wii_zapper.gif │ │ │ │ ├── usb_keyboard.gif │ │ │ │ ├── classic_controller.gif │ │ │ │ ├── gamecube_controller.gif │ │ │ │ └── wiimote.svg │ │ │ ├── link-hover.svg │ │ │ ├── osclogoanimated.gif │ │ │ ├── link-pressed.svg │ │ │ ├── title_placeholder.png │ │ │ ├── screenshot_placeholder.png │ │ │ ├── buttons │ │ │ │ ├── option.svg │ │ │ │ ├── option-hover.svg │ │ │ │ ├── button-hl.svg │ │ │ │ ├── button.svg │ │ │ │ ├── button-hl-hover.svg │ │ │ │ ├── button-hover.svg │ │ │ │ ├── button-dl.svg │ │ │ │ └── button-dl-hover.svg │ │ │ ├── icons │ │ │ │ ├── right-arrow.svg │ │ │ │ ├── top.svg │ │ │ │ ├── left-arrow.svg │ │ │ │ ├── help.svg │ │ │ │ ├── controller.svg │ │ │ │ ├── utilities.svg │ │ │ │ ├── media.svg │ │ │ │ └── channels.svg │ │ │ ├── update.svg │ │ │ ├── 1px-blue-rounded-border.svg │ │ │ └── confirmation-card.svg │ │ ├── js │ │ │ ├── buttons.js │ │ │ ├── download.js │ │ │ ├── ec.js │ │ │ ├── ec-watchdog.js │ │ │ ├── common.js │ │ │ └── skeleton.js │ │ └── css │ │ │ └── common.css │ ├── application.yml │ ├── data │ │ └── recommended.json │ └── templates │ │ ├── error │ │ ├── 404.ftl │ │ ├── ecerror.ftl │ │ └── 500.ftl │ │ ├── includes │ │ ├── macros.ftl │ │ ├── base-layout.ftl │ │ └── header.ftl │ │ ├── initial.ftl │ │ ├── debug.ftl │ │ ├── title │ │ ├── controllers.ftl │ │ ├── details.ftl │ │ ├── download.ftl │ │ ├── title.ftl │ │ └── prepare-download.ftl │ │ ├── home.ftl │ │ ├── publishers.ftl │ │ ├── browse.ftl │ │ ├── catalog.ftl │ │ └── landing.ftl │ └── java │ └── org │ └── oscwii │ └── shop │ ├── config │ ├── ContentConfig.java │ ├── ShopServerConfig.java │ ├── SchedulerConfig.java │ ├── AppConfig.java │ └── WebConfig.java │ ├── model │ └── RTitlesPage.java │ ├── utils │ ├── AssetUtil.java │ ├── Paginator.java │ ├── DownloadUtil.java │ └── FormatUtil.java │ ├── controllers │ ├── BaseController.java │ ├── StatController.java │ ├── TitleController.java │ └── PageController.java │ ├── ShopServer.java │ └── services │ ├── CatalogService.java │ ├── DownloadService.java │ └── RTitlesService.java ├── translations ├── de │ └── LC_MESSAGES │ │ ├── messages.mo │ │ └── messages.po ├── es │ └── LC_MESSAGES │ │ ├── messages.mo │ │ └── messages.po ├── fr │ └── LC_MESSAGES │ │ ├── messages.mo │ │ └── messages.po ├── it │ └── LC_MESSAGES │ │ ├── messages.mo │ │ └── messages.po ├── ja │ └── LC_MESSAGES │ │ ├── messages.mo │ │ └── messages.po ├── nl │ └── LC_MESSAGES │ │ ├── messages.mo │ │ └── messages.po └── readme.md ├── .editorconfig ├── crowdin.yml ├── data ├── motd.txt ├── messages.txt └── errors.json ├── .gitignore ├── README.md ├── messages.pot └── pom.xml /src/main/resources/static/img/blank.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenShopChannel/WSC-Web/HEAD/src/main/resources/static/img/blank.gif -------------------------------------------------------------------------------- /src/main/resources/static/img/danbo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenShopChannel/WSC-Web/HEAD/src/main/resources/static/img/danbo.png -------------------------------------------------------------------------------- /translations/de/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenShopChannel/WSC-Web/HEAD/translations/de/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /translations/es/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenShopChannel/WSC-Web/HEAD/translations/es/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /translations/fr/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenShopChannel/WSC-Web/HEAD/translations/fr/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /translations/it/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenShopChannel/WSC-Web/HEAD/translations/it/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /translations/ja/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenShopChannel/WSC-Web/HEAD/translations/ja/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /translations/nl/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenShopChannel/WSC-Web/HEAD/translations/nl/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 4 5 | indent_style = space 6 | 7 | [*.{js,css,html,svg,ftl}] 8 | indent_style = tab -------------------------------------------------------------------------------- /src/main/resources/static/img/donateqr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenShopChannel/WSC-Web/HEAD/src/main/resources/static/img/donateqr.png -------------------------------------------------------------------------------- /src/main/resources/static/img/fadegif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenShopChannel/WSC-Web/HEAD/src/main/resources/static/img/fadegif.gif -------------------------------------------------------------------------------- /src/main/resources/static/img/osclogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenShopChannel/WSC-Web/HEAD/src/main/resources/static/img/osclogo.png -------------------------------------------------------------------------------- /src/main/resources/static/img/discord_qr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenShopChannel/WSC-Web/HEAD/src/main/resources/static/img/discord_qr.png -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | bundles: 2 | - 2 3 | files: 4 | - source: messages.pot 5 | translation: /translations/%two_letters_code%/LC_MESSAGES/messages.po 6 | -------------------------------------------------------------------------------- /src/main/resources/static/img/badges/nunchuk.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenShopChannel/WSC-Web/HEAD/src/main/resources/static/img/badges/nunchuk.gif -------------------------------------------------------------------------------- /src/main/resources/static/img/link-hover.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/static/img/osclogoanimated.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenShopChannel/WSC-Web/HEAD/src/main/resources/static/img/osclogoanimated.gif -------------------------------------------------------------------------------- /src/main/resources/static/img/badges/wii_remote.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenShopChannel/WSC-Web/HEAD/src/main/resources/static/img/badges/wii_remote.gif -------------------------------------------------------------------------------- /src/main/resources/static/img/badges/wii_zapper.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenShopChannel/WSC-Web/HEAD/src/main/resources/static/img/badges/wii_zapper.gif -------------------------------------------------------------------------------- /src/main/resources/static/img/link-pressed.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/static/img/title_placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenShopChannel/WSC-Web/HEAD/src/main/resources/static/img/title_placeholder.png -------------------------------------------------------------------------------- /src/main/resources/static/img/badges/usb_keyboard.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenShopChannel/WSC-Web/HEAD/src/main/resources/static/img/badges/usb_keyboard.gif -------------------------------------------------------------------------------- /src/main/resources/static/img/screenshot_placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenShopChannel/WSC-Web/HEAD/src/main/resources/static/img/screenshot_placeholder.png -------------------------------------------------------------------------------- /src/main/resources/static/img/badges/classic_controller.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenShopChannel/WSC-Web/HEAD/src/main/resources/static/img/badges/classic_controller.gif -------------------------------------------------------------------------------- /src/main/resources/static/img/buttons/option.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/main/resources/static/img/badges/gamecube_controller.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenShopChannel/WSC-Web/HEAD/src/main/resources/static/img/badges/gamecube_controller.gif -------------------------------------------------------------------------------- /src/main/resources/static/img/buttons/option-hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /data/motd.txt: -------------------------------------------------------------------------------- 1 | Open the shop, or so we said. 2 | Have you, in fact, got any cheese here at all? 3 | There's a spill in the utilities aisle. 4 | If the shop is closed, try the back entrance. 5 | Curbside delivery not available. -------------------------------------------------------------------------------- /src/main/resources/static/img/icons/right-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/main/resources/static/img/icons/top.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/main/java/org/oscwii/shop/config/ContentConfig.java: -------------------------------------------------------------------------------- 1 | package org.oscwii.shop.config; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | 5 | import java.nio.file.Path; 6 | 7 | @ConfigurationProperties(prefix = "shop-server.contents") 8 | public record ContentConfig(Path nandLoaderPath, Path appInstallerPath) 9 | { 10 | } 11 | -------------------------------------------------------------------------------- /src/main/resources/static/img/icons/left-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/main/java/org/oscwii/shop/model/RTitlesPage.java: -------------------------------------------------------------------------------- 1 | package org.oscwii.shop.model; 2 | 3 | public record RTitlesPage(String id, String title, String subtitle, boolean dynamic, String[] apps) 4 | { 5 | public RTitlesPage(RTitlesPage oldPage, String[] apps) 6 | { 7 | this(oldPage.id(), oldPage.title(), oldPage.subtitle(), oldPage.dynamic(), apps); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/resources/static/img/update.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/resources/static/img/1px-blue-rounded-border.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /translations/readme.md: -------------------------------------------------------------------------------- 1 | How to use these translations? Glad you asked. 2 | 3 | ### Compile Translations 4 | `pybabel compile -d translations` 5 | 6 | ### Extract new strings 7 | `pybabel extract -F babel.cfg -o messages.pot .` 8 | 9 | ### Initialize a new translation for a language 10 | `pybabel init -i messages.pot -d translations -l de` 11 | 12 | ### Update existing translation files with new strings 13 | `pybabel update -i messages.pot -d translations` 14 | -------------------------------------------------------------------------------- /src/main/java/org/oscwii/shop/utils/AssetUtil.java: -------------------------------------------------------------------------------- 1 | package org.oscwii.shop.utils; 2 | 3 | import org.oscwii.api.Package; 4 | import org.oscwii.api.Package.Asset; 5 | 6 | @SuppressWarnings("unused") 7 | public class AssetUtil 8 | { 9 | public static String getWSCIconUrl(Package app) 10 | { 11 | // WSC won't load content over HTTPS for other (sub)domains 12 | return app.assets().get(Asset.Type.ICON).url().replace("https", "http"); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/org/oscwii/shop/config/ShopServerConfig.java: -------------------------------------------------------------------------------- 1 | package org.oscwii.shop.config; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | import org.springframework.boot.context.properties.NestedConfigurationProperty; 5 | 6 | @ConfigurationProperties(prefix = "shop-server") 7 | public record ShopServerConfig(String apiHost, @NestedConfigurationProperty ContentConfig contentConfig, 8 | boolean development, boolean handleEc, String repoManAccessToken) 9 | { 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | 25 | # idea 26 | *.iml 27 | .idea/ 28 | 29 | application.yml 30 | application-staging.yml 31 | application-dev*.yml 32 | contents/ 33 | /data/ 34 | target/ -------------------------------------------------------------------------------- /src/main/resources/static/img/icons/help.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8080 3 | 4 | sentry: 5 | dsn: "" 6 | environment: "production" 7 | # It is recommended to lower this value from the default of 1.0 on production 8 | traces-sample-rate: 1.0 9 | 10 | spring: 11 | application: 12 | name: "WSC-Web" 13 | 14 | shop-server: 15 | api-host: "https://hbb1.oscwii.org" 16 | contents: 17 | nandloader-path: "contents/00000001" 18 | appinstaller-path: "contents/00000002" 19 | development: true 20 | handle-ec: true 21 | repoman-access-token: "ChangeMe" 22 | 23 | logging: 24 | level: 25 | root: INFO 26 | org.springframework: INFO 27 | # org.springframework.beans: TRACE -------------------------------------------------------------------------------- /src/main/resources/static/js/buttons.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Setup and listen to specific events on buttons, as to play the hover and selection sounds. 3 | */ 4 | function setupButtons() { 5 | $(".btn").each(function(i) { 6 | $(this).mouseenter(function(e) { 7 | // too bad optional chaining is too new 8 | if (sound) 9 | sound.playSE(SoundType.HOVER); 10 | 11 | // ditto 12 | if (isWiiShop) 13 | redrawElement(e.currentTarget, 100); 14 | }); 15 | 16 | $(this).mousedown(function(e) { 17 | if (sound) { 18 | if ($(this).hasClass("btn-cancel")) 19 | sound.playSE(SoundType.CANCEL); 20 | else 21 | sound.playSE(SoundType.SELECT); 22 | } 23 | }); 24 | }); 25 | } -------------------------------------------------------------------------------- /src/main/java/org/oscwii/shop/config/SchedulerConfig.java: -------------------------------------------------------------------------------- 1 | package org.oscwii.shop.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.scheduling.TaskScheduler; 6 | import org.springframework.scheduling.annotation.EnableScheduling; 7 | import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; 8 | 9 | @Configuration 10 | @EnableScheduling 11 | public class SchedulerConfig 12 | { 13 | @Bean 14 | public TaskScheduler taskScheduler() 15 | { 16 | var scheduler = new ThreadPoolTaskScheduler(); 17 | scheduler.setPoolSize(3); 18 | scheduler.setThreadNamePrefix("Shopkeeper"); 19 | return scheduler; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/resources/static/img/confirmation-card.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/main/resources/data/recommended.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "recommendedTitles-1", 4 | "title": "Recommended Titles", 5 | "subtitle": "", 6 | "apps": [ 7 | "danbo", "danbo2", "danbo3", "danbo4" 8 | ] 9 | }, 10 | { 11 | "id": "recommendedTitles-2", 12 | "title": "Recommended Titles 2", 13 | "subtitle": "", 14 | "apps": [ 15 | "danbo5", "danbo6", "danbo7", "danbo8" 16 | ] 17 | }, 18 | { 19 | "id": "recommendedTitles-3", 20 | "title": "Highlights", 21 | "subtitle": "", 22 | "dynamic": true, 23 | "apps": [ 24 | "osc:featuredapp", "osc:latest", "osc:latest:games", "osc:latest:utilities" 25 | ] 26 | } 27 | ] -------------------------------------------------------------------------------- /src/main/resources/static/img/badges/wiimote.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/main/java/org/oscwii/shop/controllers/BaseController.java: -------------------------------------------------------------------------------- 1 | package org.oscwii.shop.controllers; 2 | 3 | import org.oscwii.shop.config.ShopServerConfig; 4 | import org.oscwii.shop.services.CatalogService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.web.bind.annotation.ModelAttribute; 7 | 8 | public abstract class BaseController 9 | { 10 | @Autowired 11 | protected ShopServerConfig config; 12 | @Autowired 13 | protected CatalogService catalog; 14 | 15 | @ModelAttribute("isDevelopment") 16 | protected boolean isDevelopment() 17 | { 18 | return config.development(); 19 | } 20 | 21 | @ModelAttribute("handleEc") 22 | protected boolean handleEc() 23 | { 24 | return config.handleEc(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/resources/templates/error/404.ftl: -------------------------------------------------------------------------------- 1 | <#import "../includes/base-layout.ftl" as layout> 2 | <@layout.header.header "404 - Not Found"> 3 | 8 | 9 | 10 | <@layout.navigation/> 11 | 12 | <@layout.page> 13 | <#-- "200" will be our prefix for Shop HTTP errors--> 14 |

Error Code: 200404

15 |

Not Found

16 |
17 |

The resource you were looking for is not here.

18 |
19 | <@layout.osc.btn body="Return to the Shop" href="/" w="200px" h="50px" style="margin: 59px auto 0px auto"/> 20 | 21 | 22 | <@layout.footer/> -------------------------------------------------------------------------------- /src/main/resources/static/img/icons/controller.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main/resources/templates/error/ecerror.ftl: -------------------------------------------------------------------------------- 1 | <#import "../includes/base-layout.ftl" as layout> 2 | <@layout.header.header "Shop Error"> 3 | 8 | 9 | 10 | <@layout.navigation/> 11 | 12 | <@layout.page> 13 | <#-- "219" will be our prefix for OSC/EC errors--> 14 |

Error Code: 219${code}

15 | <#-- TODO error desc --> 16 |

ERROR_DESC

17 |
18 |

A critical error has occurred.
19 | Please try again later, if this continues please contact the Open Shop Channel. 20 |

21 |
22 | <@layout.osc.btn body="Wii Menu" href="javascript:shop.returnToMenu()" w="200px" h="50px" style="margin: 17px auto 0px auto"/> 23 | 24 | 25 | <@layout.footer/> -------------------------------------------------------------------------------- /src/main/resources/templates/includes/macros.ftl: -------------------------------------------------------------------------------- 1 | <#macro btn body="" class="" id="" href="" w="" h="" img="" img_w="" img_h="" style=""> 2 | id="${id}"<#if href?has_content> href="${href?html}"<#if w?has_content || h?has_content || style?has_content> style="<#if w?has_content> width: ${w};<#if h?has_content> height: ${h};<#if style?has_content> ${style}"> 3 | <#-- XHTML doesn't permit us to put block elements like div in an inline element like anchor, so we use span as it's the closest to a generic inline container --> 4 | <#if img?has_content> 5 | width="${img_w}"<#if img_h?has_content> height="${img_h}"/> 6 | 7 | <#if body?has_content> 8 | ${body} 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/resources/templates/error/500.ftl: -------------------------------------------------------------------------------- 1 | <#import "../includes/base-layout.ftl" as layout> 2 | <@layout.header.header "500 - Internal Server Error"> 3 | 8 | 9 | 10 | <@layout.navigation/> 11 | 12 | <@layout.page> 13 | <#-- "200" will be our prefix for Shop HTTP errors--> 14 |

Error Code: 200500

15 |

Internal Server Error

16 |
17 |

A critical server-side error has occurred.
18 | Please try again later, if this continues please contact the Open Shop Channel. 19 |

20 |
21 | <@layout.osc.btn body="Wii Menu" href="javascript:shop.returnToMenu()" w="200px" h="50px" style="margin: 17px auto 0px auto"/> 22 | 23 | 24 | <@layout.footer/> -------------------------------------------------------------------------------- /src/main/java/org/oscwii/shop/utils/Paginator.java: -------------------------------------------------------------------------------- 1 | package org.oscwii.shop.utils; 2 | 3 | import java.util.List; 4 | 5 | public class Paginator 6 | { 7 | private final List items; 8 | private final int itemsPerPage; 9 | private final int pages; 10 | 11 | public Paginator(List items) 12 | { 13 | this(items, 10); 14 | } 15 | 16 | public Paginator(List items, int itemsPerPage) 17 | { 18 | this.items = items; 19 | this.itemsPerPage = itemsPerPage; 20 | this.pages = (int) Math.ceil((double) items.size() / itemsPerPage); 21 | } 22 | 23 | public int getNumberOfPages() 24 | { 25 | return pages; 26 | } 27 | 28 | public List paginate(int page) 29 | { 30 | page = Math.min(Math.max(page, 1), pages); 31 | return getPage(page); 32 | } 33 | 34 | private List getPage(int pageNum) 35 | { 36 | int start = (pageNum - 1) * itemsPerPage; 37 | int end = Math.min(pageNum * itemsPerPage, items.size()); 38 | return start < 0 || end <= 0 ? List.of() : items.subList(start, end); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/resources/static/img/icons/utilities.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/static/img/buttons/button-hl.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/main/resources/static/img/buttons/button.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/main/resources/static/img/buttons/button-hl-hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/main/resources/static/img/buttons/button-hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/main/resources/static/img/icons/media.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/main/resources/templates/initial.ftl: -------------------------------------------------------------------------------- 1 | <#import "includes/base-layout.ftl" as layout> 2 | <@layout.header.header "Connecting"> 3 | 33 | 34 | 40 | 41 | 42 | <@layout.navigation/> 43 | 44 | <@layout.page> 45 |

Connecting. Please wait...

46 | 47 | 48 | <@layout.footer/> -------------------------------------------------------------------------------- /src/main/java/org/oscwii/shop/controllers/StatController.java: -------------------------------------------------------------------------------- 1 | package org.oscwii.shop.controllers; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import org.oscwii.shop.services.DownloadService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.stereotype.Controller; 8 | import org.springframework.web.bind.annotation.PathVariable; 9 | import org.springframework.web.bind.annotation.PostMapping; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | import org.springframework.web.bind.annotation.RequestParam; 12 | 13 | @Controller 14 | @RequestMapping("/stat") 15 | public class StatController 16 | { 17 | private final DownloadService downloadService; 18 | 19 | @Autowired 20 | public StatController(DownloadService downloadService) 21 | { 22 | this.downloadService = downloadService; 23 | } 24 | 25 | @PostMapping("/download/{slug}") 26 | public ResponseEntity notifyDownload(@PathVariable String slug, @RequestParam String token, HttpServletRequest request) 27 | { 28 | if(token == null || token.isBlank()) 29 | return ResponseEntity.badRequest().body("No token provided"); 30 | if(!downloadService.isValidToken(token)) 31 | return ResponseEntity.badRequest().body("Invalid token"); 32 | 33 | downloadService.notifyDownload(slug, request); 34 | return ResponseEntity.noContent().build(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/resources/templates/debug.ftl: -------------------------------------------------------------------------------- 1 | <#import "includes/base-layout.ftl" as layout> 2 | <@layout.header.header "Debug"> 3 | 16 | 17 | 32 | 33 | 34 | <@layout.navigation dots=true showTitle=false/> 35 | 36 | <@layout.page> 37 | Go back 38 | 39 |

Current language: ${lang}

40 |

Force language:

41 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/main/java/org/oscwii/shop/config/AppConfig.java: -------------------------------------------------------------------------------- 1 | package org.oscwii.shop.config; 2 | 3 | import com.google.gson.Gson; 4 | import io.sentry.okhttp.SentryOkHttpEventListener; 5 | import io.sentry.okhttp.SentryOkHttpInterceptor; 6 | import okhttp3.OkHttpClient; 7 | import org.oscwii.api.OSCAPI; 8 | import org.oscwii.api.impl.APIBackend; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.boot.info.GitProperties; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.context.annotation.Configuration; 13 | 14 | import java.util.Optional; 15 | 16 | @Configuration 17 | public class AppConfig 18 | { 19 | @Bean 20 | public OkHttpClient httpClient() 21 | { 22 | return new OkHttpClient.Builder() 23 | .addInterceptor(new SentryOkHttpInterceptor()) 24 | .eventListener(new SentryOkHttpEventListener()) 25 | .build(); 26 | } 27 | 28 | @Bean 29 | public OSCAPI shopApi(Gson gson, OkHttpClient httpClient, ShopServerConfig config) 30 | { 31 | return new APIBackend(gson, httpClient, config.apiHost(), "OSC WSC Server"); 32 | } 33 | 34 | @Autowired 35 | public void setupSentry(Optional gitProperties) 36 | { 37 | String rel = gitProperties.isEmpty() ? "DEV" : gitProperties.get().getCommitId(); 38 | System.setProperty("shopserver.release", rel); 39 | System.setProperty("sentry.release", rel); 40 | System.setProperty("sentry.stacktrace.app.packages", "org.oscwii"); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /data/messages.txt: -------------------------------------------------------------------------------- 1 | _("The forwarder channel was unable to open boot.dol in /openshopchannel/downloader/. Have you installed the downloader app?") 2 | _("The forwarder channel was unable to access the SD card. Is your SD card inserted?") 3 | _("The forwarder channel was unable to load boot.dol in /openshopchannel/downloader/ into memory. Have you installed the downloader app?") 4 | _("The downloader app was unable to find the IP address of the Open Shop Channel server. Please contact the Open Shop Channel developers at https://oscwii.org.") 5 | _("The downloader app was unable to establish a connection to the Open Shop Channel server. The Open Shop Channel's content delivery network may be down.") 6 | _("The downloader app was able to connect to the Open Shop Channel server, but could not communicate with it.") 7 | _("The downloader app was able to connect to the Open Shop Channel server, but it did not send a response that the downloader app could understand.") 8 | _("The downloader app was able to download the app from the Open Shop Channel server, but could not save it to your SD card. Your SD card is likely out of space, or has the write-protect switch enabled.") 9 | _("The downloader app was unable to read from the internal system memory.") 10 | _("The downloader app could not discover what app it needed to download. If you are running the downloader app independently, note that it will not function without being fed the appropriate data; please refer to the documentation on GitHub.") 11 | _("The downloader app could not set-up the Wii networking subsystem.") 12 | _("You shouldn't be able to see this.") 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WSC-Web 2 | This is the frontend for the Open Shop Channel, loaded via the embedded Opera browser in the Wii Shop Channel. 3 | It acts as a catalog for OSC's database, querying its API and providing Wii-specific components where necessary. 4 | 5 | ## Setup 6 | WSC-Web is intended to be a standard Flask application. 7 | 8 | By default, the frontend will be exposed over TLS. In order to test the default configuration, you will need to provide a `cert.pem` and `key.pem`. It is possible to generate certificates, alongside patching them into the Wii Shop Channel, via [WSC-Patcher](https://github.com/OpenShopChannel/WSC-Patcher). 9 | 10 | You can typically start the frontend with the following steps: 11 | 1. Configure a virtual environment. For example, `python3 -m venv venv && source venv/bin/activate` 12 | 2. Install the requirements. `pip3 install -r requirements.txt` 13 | 3. Copy `config.py.example` to `config.py`, and edit accordingly. 14 | 4. Run via `app.py`. `python3 app.py` 15 | 16 | By default, EC-related requests (the backing API to download titles) will be disabled when run individually. 17 | Consider starting this server via `flask run` for development, or `gunicorn` if in production. 18 | 19 | ## Contributing 20 | Ensure you have tested all changes via the Wii Shop Channel, either on Dolphin or a physical Wii/vWii. Please do not make changes to translations directly - they will be periodically synced from translations elsewhere. 21 | 22 | ## Resources 23 | - https://caniuse.com is an excellent resource to see whether a property is available for usage with Opera 8. 24 | - Consult the [WSC documentation](https://docs.oscwii.org) for information about Nintendo's exposed JavaScript APIs. -------------------------------------------------------------------------------- /src/main/resources/static/img/buttons/button-dl.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/main/resources/templates/includes/base-layout.ftl: -------------------------------------------------------------------------------- 1 | <#import "header.ftl" as header> 2 | <#import "macros.ftl" as osc> 3 | <#global FormatUtil=statics['org.oscwii.shop.utils.FormatUtil']> 4 | 5 | <#macro navigation dots=true headerBtns=false headerTitleBlue=false headerTitle="" showTitle=true> 6 | 7 |
8 |
9 | <#if showTitle> 10 |

<#if headerTitle?has_content>${headerTitle}<#else>${FormatUtil.openify("Open")} Shop Channel

11 | 12 | <#nested> 13 | <#if headerBtns> 14 |
15 |
<@osc.btn class="btn-cancel" id="top-btn" href="/home" w="52px" h="55px" img="/static/img/icons/top.svg" img_w="34" img_h="34"/>
16 |
<@osc.btn id="help-btn" w="52px" h="55px" img="/static/img/icons/help.svg" img_w="23" img_h="35"/>
17 |
18 | 19 | <#if isDevelopment> 20 | Debug 21 | 22 |
23 | <#if dots>
・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・
24 |
25 | 26 | 27 | <#macro page> 28 |
29 | <#nested> 30 |
31 | 32 | 33 | <#macro footer dots=true> 34 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/main/java/org/oscwii/shop/utils/DownloadUtil.java: -------------------------------------------------------------------------------- 1 | package org.oscwii.shop.utils; 2 | 3 | import org.oscwii.api.Package; 4 | import org.oscwii.api.Package.ShopTitle; 5 | import org.oscwii.shop.config.ContentConfig; 6 | 7 | import java.io.IOException; 8 | import java.nio.file.Files; 9 | 10 | public class DownloadUtil 11 | { 12 | /** 13 | * The size of the app: TMD (Banner and Ticket) + Contents (Launcher and the actual app) 14 | * 15 | * @param app the app 16 | * @return the app size, in bytes 17 | */ 18 | public static long getTotalAppSize(Package app) 19 | { 20 | ShopTitle titleInfo = app.titleInfo(); 21 | return titleInfo.tmdSize() + calculateContentsSize(titleInfo); 22 | } 23 | 24 | public static long getTotalBlockSize(Package app) 25 | { 26 | return getBlockSize(getTotalAppSize(app)); 27 | } 28 | 29 | public static long getBlockSize(double size) 30 | { 31 | double megaBytes = size / 1024 / 1024; 32 | return (long) Math.ceil(megaBytes * 8); 33 | } 34 | 35 | private static long calculateContentsSize(ShopTitle titleInfo) 36 | { 37 | long size = titleInfo.contentsSize(); 38 | 39 | try 40 | { 41 | // TODO this probably should be calculated one-time 42 | // but then what if the file changes? 43 | size += Files.size(CONFIG.nandLoaderPath()); 44 | size += Files.size(CONFIG.appInstallerPath()); 45 | } 46 | catch(IOException e) 47 | { 48 | throw new RuntimeException("Failed to calculate NAND Loader/App Installer content size:", e); 49 | } 50 | 51 | return size; 52 | } 53 | 54 | private static ContentConfig CONFIG; 55 | public static void setConfig(ContentConfig config) 56 | { 57 | DownloadUtil.CONFIG = config; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/resources/static/js/download.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Whether an existing download operation is ongoing. 3 | */ 4 | var startingDownload = false; 5 | 6 | /** 7 | * Current title ID for download 8 | */ 9 | var _titleId = ""; 10 | 11 | function preDownload(titleId, callback) { 12 | _titleId = titleId; 13 | // TODO: Allow installation to the SD Card. 14 | /* 15 | ec.setParameter("SPACE_CHECK_POLICY", "SPACE_CHECK_ENTIRE_FS"); 16 | sdCard.isJournaling(); 17 | const journalRet = sdCard.setJournalFlag(titleId); 18 | if (journalRet !== 0) { 19 | error("setJournalFlag failed with " + journalRet); 20 | } 21 | 22 | pollSDProgress(noop, installChannel); 23 | */ 24 | installChannel(callback); 25 | } 26 | 27 | /** 28 | * Installs a channel for the given title ID. 29 | */ 30 | function installChannel(callback) { 31 | if (!startingDownload) { 32 | startingDownload = true; 33 | 34 | // We do not actually support paying for titles. 35 | const price = new ECPrice("0.00", "POINTS"); 36 | const method = new ECAccountPayment(); 37 | 38 | // Apply empty limits. 39 | const limits = new ECTitleLimits(); 40 | 41 | const result = ec.purchaseTitle(_titleId, '0', price, method, limits, true); 42 | completeOp(result, callback); 43 | // TODO ranks 44 | shop.setSCARank(1); 45 | } 46 | } 47 | 48 | function notifyDownload(title, token) { 49 | $.ajax({ 50 | type: "POST", 51 | url: "/stat/download/" + title + "?token=" + token, 52 | success: function () { 53 | trace("Successfully notified download to API for title: " + title); 54 | }, 55 | error: function (req) { 56 | trace("Failed to notify download to API for title: \"" + title + "\" with status: " + req.status); 57 | if(isDevelopment) { 58 | error(ErrorCodes.API_DOWNLOAD_NOTIFICATION_FAILED, "Failed to notify download to API for title: \"" + 59 | title + "\" (" + req.status + ")"); 60 | } 61 | } 62 | }); 63 | } -------------------------------------------------------------------------------- /src/main/resources/static/img/buttons/button-dl-hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/main/java/org/oscwii/shop/ShopServer.java: -------------------------------------------------------------------------------- 1 | package org.oscwii.shop; 2 | 3 | import org.apache.logging.log4j.LogManager; 4 | import org.apache.logging.log4j.Logger; 5 | import org.oscwii.shop.config.ShopServerConfig; 6 | import org.oscwii.shop.services.CatalogService; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.SpringApplication; 9 | import org.springframework.boot.autoconfigure.SpringBootApplication; 10 | import org.springframework.boot.context.properties.ConfigurationPropertiesScan; 11 | import org.springframework.scheduling.annotation.Scheduled; 12 | import org.springframework.stereotype.Controller; 13 | import org.springframework.ui.Model; 14 | import org.springframework.web.bind.annotation.GetMapping; 15 | 16 | import java.util.concurrent.TimeUnit; 17 | 18 | @SpringBootApplication 19 | @Controller 20 | @ConfigurationPropertiesScan(value = "org.oscwii.shop.config") 21 | public class ShopServer 22 | { 23 | private final CatalogService catalog; 24 | private final Logger logger; 25 | private final ShopServerConfig config; 26 | 27 | @Autowired 28 | public ShopServer(CatalogService catalog, ShopServerConfig config) 29 | { 30 | this.catalog = catalog; 31 | this.logger = LogManager.getLogger(ShopServer.class); 32 | this.config = config; 33 | 34 | if(config.development()) 35 | logger.info("Server is running on development mode, debug will be enabled"); 36 | } 37 | 38 | @GetMapping 39 | public String initial(Model model) 40 | { 41 | model.addAttribute("handleEc", config.handleEc()) 42 | .addAttribute("isDevelopment", config.development()); 43 | return "initial"; 44 | } 45 | 46 | @Scheduled(fixedDelay = 1, timeUnit = TimeUnit.HOURS) 47 | public void refreshCatalog() 48 | { 49 | catalog.refresh(); 50 | logger.info("Fetched {} packages from the catalog", catalog.getPackages().size()); 51 | } 52 | 53 | public static void main(String[] args) 54 | { 55 | SpringApplication.run(ShopServer.class, args); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/resources/templates/title/controllers.ftl: -------------------------------------------------------------------------------- 1 | <#import "../includes/base-layout.ftl" as layout> 2 | <@layout.header.header "Controller Support"> 3 | 10 | 11 | 37 | 38 | 39 | <@layout.navigation headerTitle="Controller Support" headerBtns=true/> 40 | 41 | <@layout.page> 42 | <#if package??> 43 |
44 |

${package.name()}

45 |
46 |
47 |

Please note the following when downloading this software:

48 |
49 | <#list controllers as controller> 50 | <#if !(controller == "sdhc" || controller == "unknown")> 51 | 52 | 53 | 54 |
55 |

${FormatUtil.peripheralsDescription(package)}.

56 |
57 | <#else> 58 |

The title cannot be found.

59 | 60 | 61 | 62 | <@layout.footer> 63 | 69 | -------------------------------------------------------------------------------- /src/main/java/org/oscwii/shop/config/WebConfig.java: -------------------------------------------------------------------------------- 1 | package org.oscwii.shop.config; 2 | 3 | import freemarker.template.TemplateException; 4 | import no.api.freemarker.java8.Java8ObjectWrapper; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.web.servlet.config.annotation.EnableWebMvc; 8 | import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; 9 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 10 | import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer; 11 | import org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver; 12 | 13 | import java.io.IOException; 14 | import java.nio.charset.StandardCharsets; 15 | 16 | import static freemarker.template.Configuration.VERSION_2_3_32; 17 | 18 | @Configuration 19 | @EnableWebMvc 20 | public class WebConfig implements WebMvcConfigurer 21 | { 22 | @Bean 23 | public FreeMarkerConfigurer freemarkerConfig() throws TemplateException, IOException 24 | { 25 | FreeMarkerConfigurer freeMarkerConfigurer = new FreeMarkerConfigurer(); 26 | freeMarkerConfigurer.setTemplateLoaderPath("classpath:templates/"); 27 | 28 | Java8ObjectWrapper objectWrapper = new Java8ObjectWrapper(VERSION_2_3_32); 29 | objectWrapper.setExposeFields(true); 30 | 31 | freeMarkerConfigurer.afterPropertiesSet(); 32 | freeMarkerConfigurer.getConfiguration().setBooleanFormat("c"); 33 | freeMarkerConfigurer.getConfiguration().setURLEscapingCharset(StandardCharsets.US_ASCII.name()); 34 | freeMarkerConfigurer.getConfiguration().setObjectWrapper(objectWrapper); 35 | freeMarkerConfigurer.getConfiguration().setSharedVariable("statics", objectWrapper.getStaticModels()); 36 | return freeMarkerConfigurer; 37 | } 38 | 39 | @Bean 40 | public FreeMarkerViewResolver freeMarkerViewResolver() 41 | { 42 | FreeMarkerViewResolver viewResolver = new FreeMarkerViewResolver(); 43 | viewResolver.setCache(true); 44 | viewResolver.setSuffix(".ftl"); 45 | viewResolver.setContentType("text/html; charset=UTF-8"); 46 | return viewResolver; 47 | } 48 | 49 | @Override 50 | public void addResourceHandlers(ResourceHandlerRegistry registry) 51 | { 52 | registry.addResourceHandler("/static/**") 53 | .addResourceLocations("classpath:/static/"); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/resources/templates/home.ftl: -------------------------------------------------------------------------------- 1 | <#import "includes/base-layout.ftl" as layout> 2 | <@layout.header.header "Home"> 3 | 12 | 13 | 28 | 29 | 30 | <@layout.navigation showTitle=false> 31 |

${FormatUtil.openify("Open")} Shop Channel

32 | 33 | 34 | <@layout.page> 35 |
36 |
<@layout.osc.btn body="Games" href="/browse?category=games" w="173px" h="131px" img="static/img/icons/controller.svg" img_w="92"/>
37 |
<@layout.osc.btn body="Media" href="/browse?category=media" w="167px" h="131px" img="static/img/icons/media.svg" img_w="79"/>
38 |
<@layout.osc.btn body="Utilities" href="/browse?category=utilities" w="173px" h="131px" img="static/img/icons/utilities.svg" img_w="50"/>
39 |
40 | 41 | <#-- {{ osc.btn(_("View More"), href="", w="292px", h="69px", style="margin: 12px auto;") }} --> 42 |
43 |
<@layout.osc.btn body="Emulators" href="/browse?category=emulators" w="268px" h="69px"/>
44 |
<@layout.osc.btn body="Demos" href="/browse?category=demos" w="268px" h="69px"/>
45 |
46 | 47 |
48 |
<@layout.osc.btn body="Option 1" class="btn-alt" href="" w="182px" h="56px"/>
49 |
<@layout.osc.btn body="Titles You've Downloaded" class="btn-alt" href="" w="182px" h="56px"/>
50 |
<@layout.osc.btn body="Settings and Features" class="btn-alt" href="" w="182px" h="56px"/>
51 |
52 | 53 | 54 | <@layout.footer> 55 | 59 | -------------------------------------------------------------------------------- /src/main/resources/static/img/icons/channels.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /translations/ja/LC_MESSAGES/messages.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "MIME-Version: 1.0\n" 4 | "Content-Type: text/plain; charset=UTF-8\n" 5 | "Content-Transfer-Encoding: 8bit\n" 6 | "X-Generator: POEditor.com\n" 7 | "Project-Id-Version: Open Shop Channel Shop Client\n" 8 | "Language: ja\n" 9 | 10 | #: templates/app.html:4 11 | msgid "Application" 12 | msgstr "アプリ" 13 | 14 | #: templates/app.html:152 templates/browse.html:180 templates/category.html:91 15 | #: templates/donate.html:56 templates/error.html:79 templates/keyword.html:85 16 | #: templates/search.html:152 17 | msgid "Back" 18 | msgstr "戻る" 19 | 20 | #: templates/app.html:155 21 | msgid "Download" 22 | msgstr "ダウンロード" 23 | 24 | #: templates/browse.html:152 templates/keyword.html:4 templates/keyword.html:78 25 | msgid "Search by Keyword" 26 | msgstr "キーワードで検索" 27 | 28 | #: templates/browse.html:159 templates/category.html:4 29 | #: templates/category.html:78 30 | msgid "Search by Category" 31 | msgstr "カテゴリでで検索" 32 | 33 | #: templates/browse.html:166 34 | msgid "Featured Application" 35 | msgstr "おすすめアプリ" 36 | 37 | #: templates/browse.html:173 38 | msgid "Random Application" 39 | msgstr "ランダムアプリ" 40 | 41 | #: templates/category.html:81 42 | msgid "Demos" 43 | msgstr "デモ" 44 | 45 | #: templates/category.html:82 46 | msgid "Emulators" 47 | msgstr "エミュレータ" 48 | 49 | #: templates/category.html:83 50 | msgid "Games" 51 | msgstr "ゲーム" 52 | 53 | #: templates/category.html:84 54 | msgid "Media" 55 | msgstr "メディア" 56 | 57 | #: templates/category.html:85 58 | msgid "Utilities" 59 | msgstr "ユーティリティー" 60 | 61 | #: templates/category.html:94 templates/keyword.html:88 62 | msgid "Search" 63 | msgstr "検索" 64 | 65 | #: templates/donate.html:4 66 | msgid "Donate" 67 | msgstr "寄贈" 68 | 69 | #: templates/error.html:4 70 | msgid "Error" 71 | msgstr "エラー" 72 | 73 | #: templates/landing.html:99 74 | msgid "Start Browsing" 75 | msgstr "スタート" 76 | 77 | #: templates/landing.html:102 78 | msgid "App of the Day" 79 | msgstr "今日のアプリ" 80 | 81 | #: templates/search.html:4 templates/search.html:132 82 | msgid "Search Results" 83 | msgstr "検索結果" 84 | 85 | #: templates/search.html:157 86 | msgid "Previous" 87 | msgstr "前へ" 88 | 89 | #: templates/search.html:163 90 | msgid "Next" 91 | msgstr "次へ" 92 | 93 | #: templates/startdownload.html:4 94 | msgid "Start Download" 95 | msgstr "ダウンロード開始" 96 | 97 | #: templates/landing.html:4 98 | msgid "Welcome" 99 | msgstr "ようこそ!" 100 | 101 | #: app.py:162 102 | msgid "The requested page could not be found." 103 | msgstr "このページは見つかりませんでした。" 104 | 105 | #: app.py:169 106 | msgid "The server has encountered an error. Try again later." 107 | msgstr "サーバーにエラーが発生しました。後でもう一度やり直してください。" 108 | 109 | -------------------------------------------------------------------------------- /src/main/java/org/oscwii/shop/services/CatalogService.java: -------------------------------------------------------------------------------- 1 | package org.oscwii.shop.services; 2 | 3 | import org.oscwii.api.Category; 4 | import org.oscwii.api.OSCAPI; 5 | import org.oscwii.api.Package; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.stereotype.Service; 8 | 9 | import java.util.Collections; 10 | import java.util.HashMap; 11 | import java.util.List; 12 | import java.util.Map; 13 | 14 | @Service 15 | public class CatalogService 16 | { 17 | private final OSCAPI api; 18 | 19 | private Map newestApps; 20 | 21 | @Autowired 22 | public CatalogService(OSCAPI api) 23 | { 24 | this.api = api; 25 | this.newestApps = Collections.emptyMap(); 26 | } 27 | 28 | public void refresh() 29 | { 30 | api.fetchRepositoryInformation(); 31 | api.fetchPackages(); 32 | api.fetchFeaturedApp(); 33 | this.newestApps = calculateNewestPackages(); 34 | } 35 | 36 | public Package getBySlug(String slug) 37 | { 38 | return api.getBySlug(slug); 39 | } 40 | 41 | public Package getFeaturedApp() 42 | { 43 | return api.getFeaturedApp(); 44 | } 45 | 46 | public Map getNewestPackages() 47 | { 48 | Map packages = new HashMap<>(); 49 | for(Map.Entry entry : newestApps.entrySet()) 50 | packages.put(entry.getKey(), api.getBySlug(entry.getValue())); 51 | return packages; 52 | } 53 | 54 | public List getPackages() 55 | { 56 | return api.getPackages(); 57 | } 58 | 59 | public List filterPackages(String category, String query) 60 | { 61 | return api.filterPackages(category, query); 62 | } 63 | 64 | private Map calculateNewestPackages() 65 | { 66 | Map packages = new HashMap<>(); 67 | packages.put("newest", getNewest(getPackages())); 68 | 69 | for(Category category : api.getCategories()) 70 | { 71 | String newest = getNewest(filterPackages(category.name(), null)); 72 | if(newest != null) 73 | packages.put(category.name(), newest); 74 | } 75 | 76 | return packages; 77 | } 78 | 79 | private String getNewest(List selection) 80 | { 81 | long date = 0; 82 | Package selected = null; 83 | 84 | for(Package app : selection) 85 | { 86 | if(date < app.releaseDate()) 87 | { 88 | date = app.releaseDate(); 89 | selected = app; 90 | } 91 | } 92 | 93 | return selected != null ? selected.slug() : null; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /translations/nl/LC_MESSAGES/messages.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "MIME-Version: 1.0\n" 4 | "Content-Type: text/plain; charset=UTF-8\n" 5 | "Content-Transfer-Encoding: 8bit\n" 6 | "X-Generator: POEditor.com\n" 7 | "Project-Id-Version: Open Shop Channel Shop Client\n" 8 | "Language: nl\n" 9 | 10 | #: templates/app.html:4 11 | msgid "Application" 12 | msgstr "Willekeurige Applicatie" 13 | 14 | #: templates/app.html:152 templates/browse.html:180 templates/category.html:91 15 | #: templates/donate.html:56 templates/error.html:79 templates/keyword.html:85 16 | #: templates/search.html:152 17 | msgid "Back" 18 | msgstr "Terug" 19 | 20 | #: templates/app.html:155 21 | msgid "Download" 22 | msgstr "Download" 23 | 24 | #: templates/browse.html:152 templates/keyword.html:4 templates/keyword.html:78 25 | msgid "Search by Keyword" 26 | msgstr "Zoeken op Trefwoord" 27 | 28 | #: templates/browse.html:159 templates/category.html:4 29 | #: templates/category.html:78 30 | msgid "Search by Category" 31 | msgstr "Zoeken op Categorie" 32 | 33 | #: templates/browse.html:166 34 | msgid "Featured Application" 35 | msgstr "Uitgelichte Applicaties\n" 36 | "" 37 | 38 | #: templates/browse.html:173 39 | msgid "Random Application" 40 | msgstr "Willekeurige Applicatie" 41 | 42 | #: templates/category.html:81 43 | msgid "Demos" 44 | msgstr "Demo's" 45 | 46 | #: templates/category.html:82 47 | msgid "Emulators" 48 | msgstr "Emulators" 49 | 50 | #: templates/category.html:83 51 | msgid "Games" 52 | msgstr "Games" 53 | 54 | #: templates/category.html:84 55 | msgid "Media" 56 | msgstr "Media" 57 | 58 | #: templates/category.html:85 59 | msgid "Utilities" 60 | msgstr "Hulpprogramma's" 61 | 62 | #: templates/category.html:94 templates/keyword.html:88 63 | msgid "Search" 64 | msgstr "Zoeken" 65 | 66 | #: templates/donate.html:4 67 | msgid "Donate" 68 | msgstr "" 69 | 70 | #: templates/error.html:4 71 | msgid "Error" 72 | msgstr "Error" 73 | 74 | #: templates/landing.html:99 75 | msgid "Start Browsing" 76 | msgstr "Begin met browsen" 77 | 78 | #: templates/landing.html:102 79 | msgid "App of the Day" 80 | msgstr "App van de dag" 81 | 82 | #: templates/search.html:4 templates/search.html:132 83 | msgid "Search Results" 84 | msgstr "Zoek Resultaten" 85 | 86 | #: templates/search.html:157 87 | msgid "Previous" 88 | msgstr "Vorige" 89 | 90 | #: templates/search.html:163 91 | msgid "Next" 92 | msgstr "Volgende" 93 | 94 | #: templates/startdownload.html:4 95 | #, fuzzy 96 | msgid "Start Download" 97 | msgstr "Download" 98 | 99 | #: templates/landing.html:4 100 | msgid "Welcome" 101 | msgstr "" 102 | 103 | #: app.py:162 104 | msgid "The requested page could not be found." 105 | msgstr "" 106 | 107 | #: app.py:169 108 | msgid "The server has encountered an error. Try again later." 109 | msgstr "" 110 | 111 | -------------------------------------------------------------------------------- /translations/fr/LC_MESSAGES/messages.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "MIME-Version: 1.0\n" 4 | "Content-Type: text/plain; charset=UTF-8\n" 5 | "Content-Transfer-Encoding: 8bit\n" 6 | "X-Generator: POEditor.com\n" 7 | "Project-Id-Version: Open Shop Channel Shop Client\n" 8 | "Language: fr\n" 9 | 10 | #: templates/app.html:4 11 | msgid "Application" 12 | msgstr "Application aléatoire" 13 | 14 | #: templates/app.html:152 templates/browse.html:180 templates/category.html:91 15 | #: templates/donate.html:56 templates/error.html:79 templates/keyword.html:85 16 | #: templates/search.html:152 17 | msgid "Back" 18 | msgstr "Retour" 19 | 20 | #: templates/app.html:155 21 | msgid "Download" 22 | msgstr "Télécharger" 23 | 24 | #: templates/browse.html:152 templates/keyword.html:4 templates/keyword.html:78 25 | msgid "Search by Keyword" 26 | msgstr "Recherche par mot" 27 | 28 | #: templates/browse.html:159 templates/category.html:4 29 | #: templates/category.html:78 30 | msgid "Search by Category" 31 | msgstr "Recherche par catégorie" 32 | 33 | #: templates/browse.html:166 34 | msgid "Featured Application" 35 | msgstr "Application en vedette" 36 | 37 | #: templates/browse.html:173 38 | msgid "Random Application" 39 | msgstr "Application aléatoire" 40 | 41 | #: templates/category.html:81 42 | msgid "Demos" 43 | msgstr "Démos" 44 | 45 | #: templates/category.html:82 46 | msgid "Emulators" 47 | msgstr "Emulateurs" 48 | 49 | #: templates/category.html:83 50 | msgid "Games" 51 | msgstr "Jeux" 52 | 53 | #: templates/category.html:84 54 | msgid "Media" 55 | msgstr "Médias" 56 | 57 | #: templates/category.html:85 58 | msgid "Utilities" 59 | msgstr "Outils" 60 | 61 | #: templates/category.html:94 templates/keyword.html:88 62 | msgid "Search" 63 | msgstr "Rechercher" 64 | 65 | #: templates/donate.html:4 66 | msgid "Donate" 67 | msgstr "Donner" 68 | 69 | #: templates/error.html:4 70 | msgid "Error" 71 | msgstr "Erreur" 72 | 73 | #: templates/landing.html:99 74 | msgid "Start Browsing" 75 | msgstr "Démarrer" 76 | 77 | #: templates/landing.html:102 78 | msgid "App of the Day" 79 | msgstr "Application du jour" 80 | 81 | #: templates/search.html:4 templates/search.html:132 82 | msgid "Search Results" 83 | msgstr "Résultats de la recherche" 84 | 85 | #: templates/search.html:157 86 | msgid "Previous" 87 | msgstr "Précédent" 88 | 89 | #: templates/search.html:163 90 | msgid "Next" 91 | msgstr "Suivant" 92 | 93 | #: templates/startdownload.html:4 94 | #, fuzzy 95 | msgid "Start Download" 96 | msgstr "Télécharger" 97 | 98 | #: templates/landing.html:4 99 | msgid "Welcome" 100 | msgstr "" 101 | 102 | #: app.py:162 103 | msgid "The requested page could not be found." 104 | msgstr "" 105 | 106 | #: app.py:169 107 | msgid "The server has encountered an error. Try again later." 108 | msgstr "" 109 | 110 | -------------------------------------------------------------------------------- /translations/it/LC_MESSAGES/messages.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "MIME-Version: 1.0\n" 4 | "Content-Type: text/plain; charset=UTF-8\n" 5 | "Content-Transfer-Encoding: 8bit\n" 6 | "X-Generator: POEditor.com\n" 7 | "Project-Id-Version: Open Shop Channel Shop Client\n" 8 | "Language: it\n" 9 | 10 | #: templates/app.html:4 11 | msgid "Application" 12 | msgstr "Applicazione" 13 | 14 | #: templates/app.html:152 templates/browse.html:180 templates/category.html:91 15 | #: templates/donate.html:56 templates/error.html:79 templates/keyword.html:85 16 | #: templates/search.html:152 17 | msgid "Back" 18 | msgstr "Indietro" 19 | 20 | #: templates/app.html:155 21 | msgid "Download" 22 | msgstr "Scarica" 23 | 24 | #: templates/browse.html:152 templates/keyword.html:4 templates/keyword.html:78 25 | msgid "Search by Keyword" 26 | msgstr "Cerca per parola chiave" 27 | 28 | #: templates/browse.html:159 templates/category.html:4 29 | #: templates/category.html:78 30 | msgid "Search by Category" 31 | msgstr "Cerca per categoria" 32 | 33 | #: templates/browse.html:166 34 | msgid "Featured Application" 35 | msgstr "Applicazione consigliata" 36 | 37 | #: templates/browse.html:173 38 | msgid "Random Application" 39 | msgstr "Applicazione casuale" 40 | 41 | #: templates/category.html:81 42 | msgid "Demos" 43 | msgstr "Demo" 44 | 45 | #: templates/category.html:82 46 | msgid "Emulators" 47 | msgstr "Emulatori" 48 | 49 | #: templates/category.html:83 50 | msgid "Games" 51 | msgstr "Giochi" 52 | 53 | #: templates/category.html:84 54 | msgid "Media" 55 | msgstr "Media" 56 | 57 | #: templates/category.html:85 58 | msgid "Utilities" 59 | msgstr "Strumenti" 60 | 61 | #: templates/category.html:94 templates/keyword.html:88 62 | msgid "Search" 63 | msgstr "Cerca" 64 | 65 | #: templates/donate.html:4 66 | msgid "Donate" 67 | msgstr "Dona" 68 | 69 | #: templates/error.html:4 70 | msgid "Error" 71 | msgstr "Errore" 72 | 73 | #: templates/landing.html:99 74 | msgid "Start Browsing" 75 | msgstr "Naviga" 76 | 77 | #: templates/landing.html:102 78 | msgid "App of the Day" 79 | msgstr "Applicazione del giorno" 80 | 81 | #: templates/search.html:4 templates/search.html:132 82 | msgid "Search Results" 83 | msgstr "Risultati ricerca" 84 | 85 | #: templates/search.html:157 86 | msgid "Previous" 87 | msgstr "Precedente" 88 | 89 | #: templates/search.html:163 90 | msgid "Next" 91 | msgstr "Successivo" 92 | 93 | #: templates/startdownload.html:4 94 | msgid "Start Download" 95 | msgstr "Inizia a scaricare" 96 | 97 | #: templates/landing.html:4 98 | msgid "Welcome" 99 | msgstr "Benvenuto" 100 | 101 | #: app.py:162 102 | msgid "The requested page could not be found." 103 | msgstr "La pagina richiesta non è stata trovata." 104 | 105 | #: app.py:169 106 | msgid "The server has encountered an error. Try again later." 107 | msgstr "Il server ha riscontrato un problema. Riprova più tardi." 108 | 109 | -------------------------------------------------------------------------------- /translations/es/LC_MESSAGES/messages.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "MIME-Version: 1.0\n" 4 | "Content-Type: text/plain; charset=UTF-8\n" 5 | "Content-Transfer-Encoding: 8bit\n" 6 | "X-Generator: POEditor.com\n" 7 | "Project-Id-Version: Open Shop Channel Shop Client\n" 8 | "Language: es\n" 9 | 10 | #: templates/app.html:4 11 | msgid "Application" 12 | msgstr "Aplicación" 13 | 14 | #: templates/app.html:152 templates/browse.html:180 templates/category.html:91 15 | #: templates/donate.html:56 templates/error.html:79 templates/keyword.html:85 16 | #: templates/search.html:152 17 | msgid "Back" 18 | msgstr "Atrás" 19 | 20 | #: templates/app.html:155 21 | msgid "Download" 22 | msgstr "Descargar" 23 | 24 | #: templates/browse.html:152 templates/keyword.html:4 templates/keyword.html:78 25 | msgid "Search by Keyword" 26 | msgstr "Buscar por término" 27 | 28 | #: templates/browse.html:159 templates/category.html:4 29 | #: templates/category.html:78 30 | msgid "Search by Category" 31 | msgstr "Buscar por categoría" 32 | 33 | #: templates/browse.html:166 34 | msgid "Featured Application" 35 | msgstr "Aplicación destacada" 36 | 37 | #: templates/browse.html:173 38 | msgid "Random Application" 39 | msgstr "Aplicación al azar" 40 | 41 | #: templates/category.html:81 42 | msgid "Demos" 43 | msgstr "Demos" 44 | 45 | #: templates/category.html:82 46 | msgid "Emulators" 47 | msgstr "Emuladores" 48 | 49 | #: templates/category.html:83 50 | msgid "Games" 51 | msgstr "Juegos" 52 | 53 | #: templates/category.html:84 54 | msgid "Media" 55 | msgstr "Multimedia" 56 | 57 | #: templates/category.html:85 58 | msgid "Utilities" 59 | msgstr "Herramientas" 60 | 61 | #: templates/category.html:94 templates/keyword.html:88 62 | msgid "Search" 63 | msgstr "Buscar" 64 | 65 | #: templates/donate.html:4 66 | msgid "Donate" 67 | msgstr "Donar" 68 | 69 | #: templates/error.html:4 70 | msgid "Error" 71 | msgstr "Error" 72 | 73 | #: templates/landing.html:99 74 | msgid "Start Browsing" 75 | msgstr "Comenzar" 76 | 77 | #: templates/landing.html:102 78 | msgid "App of the Day" 79 | msgstr "Aplicación del día" 80 | 81 | #: templates/search.html:4 templates/search.html:132 82 | msgid "Search Results" 83 | msgstr "Resultados de búsqueda" 84 | 85 | #: templates/search.html:157 86 | msgid "Previous" 87 | msgstr "Anterior" 88 | 89 | #: templates/search.html:163 90 | msgid "Next" 91 | msgstr "Siguiente" 92 | 93 | #: templates/startdownload.html:4 94 | #, fuzzy 95 | msgid "Start Download" 96 | msgstr "Descargar" 97 | 98 | #: templates/landing.html:4 99 | msgid "Welcome" 100 | msgstr "Bienvenido" 101 | 102 | #: app.py:162 103 | msgid "The requested page could not be found." 104 | msgstr "No se ha podido encontrar la página solicitada." 105 | 106 | #: app.py:169 107 | msgid "The server has encountered an error. Try again later." 108 | msgstr "El servidor ha encontrado un error. Vuelve a intentarlo más tarde." 109 | 110 | -------------------------------------------------------------------------------- /translations/de/LC_MESSAGES/messages.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "MIME-Version: 1.0\n" 4 | "Content-Type: text/plain; charset=UTF-8\n" 5 | "Content-Transfer-Encoding: 8bit\n" 6 | "X-Generator: POEditor.com\n" 7 | "Project-Id-Version: Open Shop Channel Shop Client\n" 8 | "Language: de\n" 9 | 10 | #: templates/app.html:4 11 | #, fuzzy 12 | msgid "Application" 13 | msgstr "Zufällige Anwendung" 14 | 15 | #: templates/app.html:152 templates/browse.html:180 templates/category.html:91 16 | #: templates/donate.html:56 templates/error.html:79 templates/keyword.html:85 17 | #: templates/search.html:152 18 | msgid "Back" 19 | msgstr "Zurück" 20 | 21 | #: templates/app.html:155 22 | msgid "Download" 23 | msgstr "Herunterladen" 24 | 25 | #: templates/browse.html:152 templates/keyword.html:4 templates/keyword.html:78 26 | msgid "Search by Keyword" 27 | msgstr "Suche nach Wörtern" 28 | 29 | #: templates/browse.html:159 templates/category.html:4 30 | #: templates/category.html:78 31 | msgid "Search by Category" 32 | msgstr "Suche nach Kategorien" 33 | 34 | #: templates/browse.html:166 35 | msgid "Featured Application" 36 | msgstr "Ausgewählte Anwendung" 37 | 38 | #: templates/browse.html:173 39 | msgid "Random Application" 40 | msgstr "Zufällige Anwendung" 41 | 42 | #: templates/category.html:81 43 | msgid "Demos" 44 | msgstr "Demos" 45 | 46 | #: templates/category.html:82 47 | msgid "Emulators" 48 | msgstr "Emulatoren" 49 | 50 | #: templates/category.html:83 51 | msgid "Games" 52 | msgstr "Spiele" 53 | 54 | #: templates/category.html:84 55 | msgid "Media" 56 | msgstr "Medien" 57 | 58 | #: templates/category.html:85 59 | msgid "Utilities" 60 | msgstr "Werkzeuge" 61 | 62 | #: templates/category.html:94 templates/keyword.html:88 63 | msgid "Search" 64 | msgstr "Suche" 65 | 66 | #: templates/donate.html:4 67 | msgid "Donate" 68 | msgstr "Spenden" 69 | 70 | #: templates/error.html:4 71 | msgid "Error" 72 | msgstr "Fehler" 73 | 74 | #: templates/landing.html:99 75 | msgid "Start Browsing" 76 | msgstr "Start" 77 | 78 | #: templates/landing.html:102 79 | msgid "App of the Day" 80 | msgstr "Anwendung des Tages" 81 | 82 | #: templates/search.html:4 templates/search.html:132 83 | msgid "Search Results" 84 | msgstr "Suchergebnisse" 85 | 86 | #: templates/search.html:157 87 | msgid "Previous" 88 | msgstr "Vorherige" 89 | 90 | #: templates/search.html:163 91 | msgid "Next" 92 | msgstr "Nächste" 93 | 94 | #: templates/startdownload.html:4 95 | #, fuzzy 96 | msgid "Start Download" 97 | msgstr "Herunterladen" 98 | 99 | #: templates/landing.html:4 100 | msgid "Welcome" 101 | msgstr "Willkommen!" 102 | 103 | #: app.py:162 104 | msgid "The requested page could not be found." 105 | msgstr "Die angeforderte Seite konnte nicht gefunden werden." 106 | 107 | #: app.py:169 108 | msgid "The server has encountered an error. Try again later." 109 | msgstr "Auf dem Server ist ein Fehler aufgetreten. Versuche es später noch einmal." 110 | 111 | -------------------------------------------------------------------------------- /src/main/java/org/oscwii/shop/utils/FormatUtil.java: -------------------------------------------------------------------------------- 1 | package org.oscwii.shop.utils; 2 | 3 | import org.oscwii.api.Package; 4 | 5 | import java.text.DateFormat; 6 | import java.text.SimpleDateFormat; 7 | import java.time.Instant; 8 | import java.util.Date; 9 | import java.util.LinkedList; 10 | import java.util.List; 11 | import java.util.Locale; 12 | 13 | @SuppressWarnings("unused") 14 | public class FormatUtil 15 | { 16 | public static String date(long seconds) 17 | { 18 | Date date = Date.from(Instant.ofEpochSecond(seconds)); 19 | return DATE_FORMAT.format(date); 20 | } 21 | 22 | public static String openify(String text) 23 | { 24 | String result = ""; 25 | char[] array = text.toCharArray(); 26 | for(int i = 0; i < array.length; i++) 27 | { 28 | char oscChar = OPEN[i % 4]; 29 | //noinspection StringConcatenationInLoop 30 | result += "%c".formatted(oscChar, array[i]); 31 | } 32 | 33 | return result; 34 | } 35 | 36 | // TODO localization 37 | public static String peripheralsDescription(Package app) 38 | { 39 | List peripherals = app.peripherals(); 40 | List toJoin = new LinkedList<>(); 41 | StringBuilder desc = new StringBuilder("You can use this software with "); 42 | 43 | // Check Wii Remotes 44 | if(peripherals.contains("wii_remote")) 45 | { 46 | long wiiRemotes = calculateNumberOfRemotes(peripherals); 47 | String str = wiiRemotes == 1 ? "a Wii Remote" : "up to %d Wii Remotes".formatted(wiiRemotes); 48 | // Check Nunchuk 49 | if(peripherals.contains("nunchuk")) 50 | str += " and Nunchuk"; 51 | // Check Wii Zapper 52 | if(peripherals.contains("wii_zapper")) 53 | str += " with the Wii Zapper"; 54 | toJoin.add(str); 55 | } 56 | // Check Classic Controller 57 | if(peripherals.contains("classic_controller")) 58 | toJoin.add("the Classic Controller"); 59 | // Check GameCube Controller 60 | if(peripherals.contains("gamecube_controller")) 61 | toJoin.add("the GameCube Controller"); 62 | // Check Keyboard 63 | if(peripherals.contains("usb_keyboard")) 64 | toJoin.add("a USB Keyboard"); 65 | 66 | desc.append(String.join(", ", toJoin)); 67 | if(desc.lastIndexOf(",") != -1) 68 | desc.replace(desc.lastIndexOf(","), desc.lastIndexOf(",") + 1, " and"); 69 | return desc.toString(); 70 | } 71 | 72 | private static long calculateNumberOfRemotes(List peripherals) 73 | { 74 | return peripherals.stream() 75 | .filter(peripheral -> peripheral.equals("wii_remote")) 76 | .count(); 77 | } 78 | 79 | private static final char[] OPEN = {'o', 'p', 'e', 'n'}; 80 | private static final DateFormat DATE_FORMAT = new SimpleDateFormat("MMMM d, y", Locale.ENGLISH); 81 | } 82 | -------------------------------------------------------------------------------- /src/main/resources/templates/publishers.ftl: -------------------------------------------------------------------------------- 1 | <#import "includes/base-layout.ftl" as layout> 2 | <#-- Rename this to a more appropriate filename later... This will be reused for subcategories once they are implemented (e.g., having a list of game genres) --> 3 | <@layout.header.header "Search by Publisher"> 4 | 11 | 12 | 70 | 71 | 72 | <@layout.navigation headerTitle="Search by Publisher" headerBtns=true/> 73 | 74 | <#macro btnItem name titles> 75 | 76 | ${name} 77 | 78 | ${titles} 79 | 80 | 81 | 82 | <@layout.page> 83 |
84 | <#list publishers as publisher, titles> 85 |
<@btnItem publisher "Titles: ${titles}"/>
86 | 87 |
88 | 89 | 90 | <@layout.footer> 91 | 94 | -------------------------------------------------------------------------------- /src/main/resources/templates/title/details.ftl: -------------------------------------------------------------------------------- 1 | <#import "../includes/base-layout.ftl" as layout> 2 | <@layout.header.header "More Details"> 3 | 11 | 12 | 83 | 84 | 85 | <@layout.navigation headerTitle="More Details" headerBtns=true/> 86 | 87 | <@layout.page> 88 | <#if package??> 89 |
${package.category()?capitalize}
90 |
91 |
92 |

${package.name()}

93 |
94 |
95 |
96 |
97 |
98 | 99 | 100 |
101 |
102 |
${package.description().longDesc()}
103 |
104 |
105 |
106 |
107 |
108 | <#else> 109 |

The title cannot be found.

110 | 111 | 112 | 113 | <@layout.footer> 114 | 117 | -------------------------------------------------------------------------------- /src/main/resources/templates/includes/header.ftl: -------------------------------------------------------------------------------- 1 | <#macro header title=""> 2 | 3 | 4 | 5 | 6 | 7 | ${title}<#if title?has_content> - </#if>Open Shop Channel 8 | 9 | <#-- gah, have to do this inline so I can url_for --> 10 | 59 | <#-- JS wasn't sane enough until the 2010s, so we use jQuery so DOM stuff is less of a hassle (we can't even select an array of elements by class!) --> 60 | 61 | 62 | 63 | 64 | 65 | 66 | 74 | 75 | <#nested> 76 | 77 | -------------------------------------------------------------------------------- /src/main/resources/templates/browse.ftl: -------------------------------------------------------------------------------- 1 | <#import "includes/base-layout.ftl" as layout> 2 | <#assign categoryName=category?capitalize/> 3 | <@layout.header.header categoryName> 4 | 29 | 30 | 81 | 82 | 83 | <@layout.navigation headerTitle=categoryName headerBtns=true/> 84 | 85 | <@layout.page> 86 |

Unknown

87 | <#-- could maybe use CSS table as that could semantically be more correct, but that would require more code (and doesn't matter for this) --> 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | <#-- percent width... bad! won't work later --> 96 | 97 | 98 | 99 | 100 | 106 | 107 |
<@layout.osc.btn body="Popular Titles" href="/search?category=" + category + "&type=popular" w="240px" h="65px"/><@layout.osc.btn body="Newest Additions" href="/search?category=" + category + "&type=newest" w="240px" h="65px"/>
<@layout.osc.btn body="Search by Publisher" href="/search?category=" + category + "&type=publishers" w="100%" h="65px"/>
101 |
102 | 103 | <@layout.osc.btn body="Search by Title" w="100%" h="65px"/> 104 |
105 |
108 | 109 | 110 | <@layout.footer> 111 | 114 | -------------------------------------------------------------------------------- /src/main/java/org/oscwii/shop/controllers/TitleController.java: -------------------------------------------------------------------------------- 1 | package org.oscwii.shop.controllers; 2 | 3 | import org.oscwii.api.Package; 4 | import org.oscwii.shop.config.ContentConfig; 5 | import org.oscwii.shop.services.DownloadService; 6 | import org.oscwii.shop.utils.DownloadUtil; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.stereotype.Controller; 9 | import org.springframework.ui.Model; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.ModelAttribute; 12 | import org.springframework.web.bind.annotation.PathVariable; 13 | import org.springframework.web.bind.annotation.RequestMapping; 14 | import org.springframework.web.bind.annotation.RequestParam; 15 | 16 | import java.util.LinkedHashSet; 17 | import java.util.Optional; 18 | import java.util.Set; 19 | 20 | @Controller 21 | @RequestMapping("/title/{slug}/") 22 | public class TitleController extends BaseController 23 | { 24 | private final DownloadService downloadService; 25 | 26 | @Autowired 27 | public TitleController(ContentConfig contentConfig, DownloadService downloadService) 28 | { 29 | this.downloadService = downloadService; 30 | DownloadUtil.setConfig(contentConfig); 31 | } 32 | 33 | @GetMapping 34 | public String details(Package app, Model model) 35 | { 36 | model.addAttribute("blocks", app == null ? 0 : DownloadUtil.getTotalBlockSize(app)); 37 | return "title/title"; 38 | } 39 | 40 | @GetMapping("controllers") 41 | public String controllers(Model model, @RequestParam Optional download, 42 | @RequestParam Optional location, @RequestParam Optional nextPage) 43 | { 44 | Package app = (Package) model.getAttribute("package"); 45 | // Doing this to remove the duplicates 46 | Set controllers = app == null ? Set.of() : new LinkedHashSet<>(app.peripherals()); 47 | model.addAttribute("controllers", controllers) 48 | .addAttribute("isDownload", download.orElse(false)) 49 | .addAttribute("location", location.orElse("")) 50 | .addAttribute("nextPage", nextPage.orElse("")); 51 | return "title/controllers"; 52 | } 53 | 54 | @GetMapping("details") 55 | public String moreDetails() 56 | { 57 | return "title/details"; 58 | } 59 | 60 | @GetMapping("prepare-download") 61 | public String prepareDownload(Package app, Model model) 62 | { 63 | long appSize = app == null ? 0 : app.titleInfo().tmdSize() + app.titleInfo().contentsSize(); 64 | long totalSize = app == null ? 0 : DownloadUtil.getTotalAppSize(app); 65 | long installerSize = totalSize - appSize; 66 | 67 | model.addAttribute("appBlocks", DownloadUtil.getBlockSize(appSize)) 68 | .addAttribute("installerBlocks", DownloadUtil.getBlockSize(installerSize)) 69 | .addAttribute("totalBlocks", DownloadUtil.getBlockSize(totalSize)); 70 | return "title/prepare-download"; 71 | } 72 | 73 | @GetMapping("download") 74 | public String download(Package app, @RequestParam @ModelAttribute("location") String location, Model model) 75 | { 76 | if(app != null) 77 | { 78 | String token = downloadService.generateToken(app.slug()); 79 | model.addAttribute("token", token); 80 | } 81 | 82 | return "title/download"; 83 | } 84 | 85 | @ModelAttribute("package") 86 | private Package getPackage(@PathVariable String slug) 87 | { 88 | return catalog.getBySlug(slug); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/org/oscwii/shop/services/DownloadService.java: -------------------------------------------------------------------------------- 1 | package org.oscwii.shop.services; 2 | 3 | import com.google.common.cache.Cache; 4 | import com.google.common.cache.CacheBuilder; 5 | import com.google.common.io.ByteArrayDataOutput; 6 | import com.google.common.io.ByteStreams; 7 | import jakarta.servlet.http.HttpServletRequest; 8 | import okhttp3.Call; 9 | import okhttp3.Callback; 10 | import okhttp3.MediaType; 11 | import okhttp3.OkHttpClient; 12 | import okhttp3.Request; 13 | import okhttp3.RequestBody; 14 | import okhttp3.Response; 15 | import org.apache.tomcat.util.codec.binary.Base64; 16 | import org.oscwii.shop.config.ShopServerConfig; 17 | import org.slf4j.Logger; 18 | import org.slf4j.LoggerFactory; 19 | import org.springframework.stereotype.Service; 20 | 21 | import java.io.IOException; 22 | import java.util.UUID; 23 | import java.util.concurrent.TimeUnit; 24 | 25 | import static com.google.common.net.MediaType.PLAIN_TEXT_UTF_8; 26 | import static java.nio.charset.StandardCharsets.UTF_8; 27 | 28 | @Service 29 | public class DownloadService 30 | { 31 | private final Cache tokens; 32 | private final Logger logger; 33 | private final OkHttpClient httpClient; 34 | private final ShopServerConfig config; 35 | 36 | public DownloadService(OkHttpClient httpClient, ShopServerConfig config) 37 | { 38 | this.tokens = CacheBuilder.newBuilder() 39 | .expireAfterWrite(5, TimeUnit.MINUTES) 40 | .build(); 41 | this.logger = LoggerFactory.getLogger(DownloadService.class); 42 | this.httpClient = httpClient; 43 | this.config = config; 44 | } 45 | 46 | public boolean isValidToken(String token) 47 | { 48 | if(tokens.getIfPresent(token) != null) 49 | { 50 | tokens.invalidate(token); 51 | return true; 52 | } 53 | 54 | return false; 55 | } 56 | 57 | public String generateToken(String app) 58 | { 59 | String secRand = UUID.randomUUID().toString(); 60 | String token = UUID.nameUUIDFromBytes((app + secRand).getBytes(UTF_8)).toString(); 61 | token = token.replace("-", ""); 62 | tokens.put(token, true); 63 | return token; 64 | } 65 | 66 | public void notifyDownload(String slug, HttpServletRequest request) 67 | { 68 | Request req = new Request.Builder() 69 | .url(config.apiHost() + "/shop/download/" + slug) 70 | .header("Authorization", config.repoManAccessToken()) 71 | .method("POST", RequestBody.create(getDownloadKey(request, slug), MediaType.get(PLAIN_TEXT_UTF_8.toString()))) 72 | .build(); 73 | 74 | httpClient.newCall(req).enqueue(new Callback() 75 | { 76 | @Override 77 | public void onFailure(Call call, IOException e) 78 | { 79 | // Fail silently to the user 80 | logger.error("Failed to notify download for {}", slug, e); 81 | } 82 | 83 | @Override 84 | public void onResponse(Call call, Response response) 85 | { 86 | if(!response.isSuccessful()) 87 | { 88 | logger.error("Failed to notify download for {}: Status Code {}: {}", slug, response.code(), 89 | response.message()); 90 | } 91 | } 92 | }); 93 | } 94 | 95 | private String getDownloadKey(HttpServletRequest req, String slug) 96 | { 97 | ByteArrayDataOutput encode = ByteStreams.newDataOutput(); 98 | encode.writeUTF(req.getRemoteAddr()); 99 | encode.writeUTF(req.getHeader("User-Agent")); 100 | encode.writeUTF(slug); 101 | return Base64.encodeBase64String(encode.toByteArray()); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main/resources/static/css/common.css: -------------------------------------------------------------------------------- 1 | html { 2 | /* Makes visualising the boundaries of the page easier */ 3 | background-color: gray; 4 | } 5 | 6 | * { 7 | margin: 0; 8 | padding: 0; 9 | box-sizing: border-box; 10 | } 11 | 12 | body { 13 | width: 608px; 14 | height: 456px; 15 | 16 | position: absolute; 17 | 18 | background-color: white; 19 | color: #323232; 20 | 21 | font-family: "Wii NTLG PGothic Regular", sans-serif; 22 | } 23 | 24 | #main-header { 25 | width: 100%; 26 | height: 82px; 27 | position: relative; 28 | } 29 | 30 | #main-content { 31 | height: 281px; /* 456 - (82 + 93) = 281px (i hate this) */ 32 | padding: 0px 22px 0px 25px; 33 | overflow: auto; 34 | } 35 | 36 | #main-footer { 37 | width: 100%; 38 | height: 93px; 39 | position: relative; 40 | } 41 | 42 | #main-header-contents { 43 | width: 100%; 44 | padding: 28px 19px 0px 29px; 45 | z-index: 1; 46 | } 47 | 48 | #main-footer-contents { 49 | width: 100%; 50 | /* left margin on reference screenshot of error page is actually about 25px. much consistency, thanks nintendo */ 51 | padding: 0px 22px 23px 25px; 52 | z-index: 1; 53 | } 54 | 55 | #main-heading { 56 | /* line-height: 1; */ 57 | } 58 | 59 | .dots { 60 | width: 100%; 61 | line-height: 23px; 62 | text-align: center; 63 | color: #8c8c8c; 64 | } 65 | 66 | 67 | /* :( */ 68 | #main-header > *, #main-footer > * { 69 | position: absolute; 70 | } 71 | #main-header > #main-header-contents { top: 0; } 72 | #main-header > .dots { bottom: 0; } 73 | #main-footer > .dots { top: 0; } 74 | #main-footer > #main-footer-contents { bottom: 0; } 75 | 76 | #main-header-btns { 77 | position: absolute; 78 | /* ideally doing `right: 0` would go to the padding of the parent, but it doesn't */ 79 | top: 23px; /* 28 - 5 = 23px (ARRRGHHH) */ 80 | right: 19px; 81 | background-color: white; 82 | } 83 | 84 | #main-header-btns > div { 85 | float: left; 86 | margin-right: 8px; 87 | } 88 | 89 | /* arrrrgh, no :last-* support */ 90 | #main-header-btns > div.last { 91 | margin-right: 0; 92 | } 93 | 94 | /* ugh */ 95 | #main-header-btns .btn { 96 | padding: 0; 97 | } 98 | 99 | /* TODO: move button dimensions outside CSS in preparation for using images */ 100 | #main-footer-btns .btn { 101 | width: 187px; 102 | height: 55px; 103 | } 104 | 105 | .heading { 106 | font-size: 28px; 107 | font-weight: bold; 108 | } 109 | 110 | .heading.link > a { 111 | width: 100%; 112 | display: inline-block; 113 | text-decoration: none; 114 | color: inherit; 115 | } 116 | 117 | .btn { 118 | display: table; 119 | color: inherit; 120 | font-size: 18px; 121 | text-decoration: none; 122 | cursor: pointer; 123 | padding: 14px; 124 | } 125 | 126 | .btn > * { 127 | display: table-row; 128 | } 129 | 130 | .btn > * > * { 131 | display: table-cell; 132 | vertical-align: middle; 133 | text-align: center; 134 | } 135 | 136 | .btn-vert-img-c { 137 | height: 100%; 138 | } 139 | 140 | .btn-alt { 141 | border-radius: 28px; 142 | box-shadow: 0px 1px 2px 0px gray; 143 | padding: 0; 144 | } 145 | 146 | /* Using this seems to *really* put noticeable lag on buttons */ 147 | .justify-hr { 148 | text-align: justify; 149 | } 150 | 151 | .justify-hr > * { 152 | display: inline-block; 153 | vertical-align: top; 154 | } 155 | 156 | .justify-hr::after { 157 | width: 100%; 158 | display: inline-block; 159 | content: ""; 160 | } 161 | 162 | .osc-o { color: #34beed; } 163 | .osc-p { color: #34ed90; } 164 | .osc-e { color: #edd134; } 165 | .osc-n { color: #ed349f; } 166 | 167 | .blue { color: #34beed; } 168 | .red { color: #ff0000; } 169 | 170 | .bold { font-weight: bold; } 171 | 172 | .text-center { text-align: center; } 173 | 174 | /* mmh */ 175 | .font-14px { font-size: 14px; } 176 | .font-16px { font-size: 16px; } 177 | .font-18px { font-size: 18px; } 178 | .font-20px { font-size: 20px; } 179 | 180 | .d-none { display: none; } -------------------------------------------------------------------------------- /src/main/resources/static/js/ec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Enum describing possible error codes via ECProgress. 3 | * @readonly 4 | * @enum {number} 5 | */ 6 | const ECReturnCodes = { 7 | COMPLETE: 0, 8 | NO_OPERATION: -4007, 9 | ONGOING: -4009, 10 | TITLE_NOT_INSTALLED: -4050 11 | }; 12 | 13 | /** 14 | * Enum describing possible registration states for this console. 15 | * @readonly 16 | * @enum {string} 17 | */ 18 | const ECRegistrationStates = { 19 | REGISTERED: 'R', 20 | UNREGISTERED: 'U' 21 | }; 22 | 23 | /** 24 | * Returns a usable subdomain for the given service type. 25 | * This is useful for getting domains such as ias, ecs and cas. 26 | * 27 | * @param serviceType Name of service to return 28 | * @return {string} Domain name usable for other usage 29 | */ 30 | function getSubdomain(serviceType) { 31 | // Get the base domain. 32 | const currentDomain = window.location.hostname; 33 | 34 | // Strip the first subdomain. 35 | // We could expect this to be "oss-auth", but it can be anything. 36 | const baseDomain = currentDomain.substring(currentDomain.indexOf('.')); 37 | 38 | return "https://" + serviceType + baseDomain; 39 | } 40 | 41 | /** 42 | * Returns a usable URL for the ECommerce Services SOAP endpoint. 43 | * 44 | * @return {string} 45 | */ 46 | function getECS() { 47 | return getSubdomain("ecs") + "/ecs/services/ECommerceSOAP"; 48 | } 49 | 50 | /** 51 | * Returns a usable URL for the Identity/Authentication SOAP endpoint. 52 | * 53 | * @returns {string} 54 | */ 55 | function getIAS() { 56 | return getSubdomain("ias") + "/ias/services/IdentityAuthenticationSOAP"; 57 | } 58 | 59 | /** 60 | * Returns a usable URL for the Cataloguing SOAP endpoint. 61 | * 62 | * @returns {string} 63 | */ 64 | function getCAS() { 65 | return getSubdomain("cas") + "/cas/services/CatalogingSOAP"; 66 | } 67 | 68 | /** 69 | * Returns a usable URL for the cached content domain, used 70 | * to download titles and their contents over http. 71 | * 72 | * @returns {string} 73 | */ 74 | function getCCS() { 75 | return getSubdomain("ccs"); 76 | } 77 | 78 | /** 79 | * Returns a usable URL for the uncached content domain, used 80 | * to download titles and their contents over https. 81 | * 82 | * @returns {string} 83 | */ 84 | function getUCS() { 85 | return getSubdomain("ucs"); 86 | } 87 | 88 | /** 89 | * Initially populates EC URLs. 90 | */ 91 | function initializeEC() { 92 | ec.setWebSvcUrls(getECS(), getIAS(), getCAS()); 93 | ec.setContentUrls(getCCS(), getUCS()); 94 | } 95 | 96 | /** 97 | * Registers the console to WiiSOAP, if necessary. 98 | * 99 | * @param {function} callback 100 | */ 101 | function doRegistrationDosido(callback) { 102 | trace("Registration status: " + info.registrationStatus); 103 | completeOp(ec.checkRegistration(), function() { 104 | // We need to call ec.getDeviceInfo() once more to ensure 105 | // we have the latest registration status. 106 | var status = ec.getDeviceInfo().registrationStatus; 107 | if (status === ECRegistrationStates.UNREGISTERED) { 108 | trace("Console is unregistered, registering now..."); 109 | // We need to register this console. 110 | // This challenge - "NintyWhyPls" - is hardcoded 111 | // within WiiSOAP, as requesting a challenge from the server 112 | // and immediately sending it back is useless. 113 | completeOp(ec.register("NintyWhyPls"), function() { 114 | // We're done here! 115 | callback(); 116 | }); 117 | } else if (status === ECRegistrationStates.REGISTERED) { 118 | trace("Console is already registered, syncing registration..."); 119 | // syncRegistration ensures that the client has the latest token from the server. 120 | // We could potentially allow token updating at a point, but do not currently. 121 | // TODO(spotlightishere): Determine the best approach 122 | completeOp(ec.syncRegistration("NintyWhyPls"), function() { 123 | // Finished at last. 124 | callback(); 125 | }); 126 | } else { 127 | trace("Unknown registration state: " + status + ". Cannot continue."); 128 | error(ErrorCodes.EC_FAILED_REGISTRATION, "Unknown EC registration state passed."); 129 | } 130 | }); 131 | } -------------------------------------------------------------------------------- /data/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "DOL_OPEN_FAILED": [ 3 | { 4 | "desc": "The forwarder channel was unable to open boot.dol in /openshopchannel/downloader/. Have you installed the downloader app?" 5 | } 6 | ], 7 | "FAT_INIT_FAILED": [ 8 | { 9 | "desc": "The forwarder channel was unable to access the SD card. Is your SD card inserted?" 10 | } 11 | ], 12 | "DOL_BUFF_FAILED": [ 13 | { 14 | "desc": "The forwarder channel was unable to load boot.dol in /openshopchannel/downloader/ into memory. Have you installed the downloader app?" 15 | } 16 | ], 17 | "DNS_FAILED": [ 18 | { 19 | "desc": "The downloader app was unable to find the IP address of the Open Shop Channel server. Please contact the Open Shop Channel developers at https://oscwii.org." 20 | } 21 | ], 22 | "SOCK_CREATE_FAILED": [ 23 | { 24 | "desc": "The downloader app was unable to establish a connection to the Open Shop Channel server. The Open Shop Channel's content delivery network may be down." 25 | } 26 | ], 27 | "SOCK_CONNECT_FAILED": [ 28 | { 29 | "desc": "The downloader app was unable to establish a connection to the Open Shop Channel server. The Open Shop Channel's content delivery network may be down." 30 | } 31 | ], 32 | "CONLEN_SOCK_WRITE_FAILED": [ 33 | { 34 | "desc": "The downloader app was able to connect to the Open Shop Channel server, but could not communicate with it." 35 | } 36 | ], 37 | "CONLEN_SOCK_READ_FAILED": [ 38 | { 39 | "desc": "The downloader app was able to connect to the Open Shop Channel server, but could not communicate with it." 40 | } 41 | ], 42 | "CONLEN_HEADER_STRSTR_FAILED": [ 43 | { 44 | "desc": "The downloader app was able to connect to the Open Shop Channel server, but it did not send a response that the downloader app could understand." 45 | } 46 | ], 47 | "CONLEN_HEADER_STRSTR_END_FAILED": [ 48 | { 49 | "desc": "The downloader app was able to connect to the Open Shop Channel server, but it did not send a response that the downloader app could understand." 50 | } 51 | ], 52 | "CHUNK_SOCK_READ_FAILED": [ 53 | { 54 | "desc": "The downloader app was able to connect to the Open Shop Channel server, but could not communicate with it." 55 | } 56 | ], 57 | "CHUNK_FWRITE_FAILED": [ 58 | { 59 | "desc": "The downloader app was able to download the app from the Open Shop Channel server, but could not save it to your SD card. Your SD card is likely out of space, or has the write-protect switch enabled." 60 | } 61 | ], 62 | "ISFS_OPEN_FAILED": [ 63 | { 64 | "desc": "The downloader app was unable to read from the internal system memory." 65 | } 66 | ], 67 | "NO_URL_IN_BIN": [ 68 | { 69 | "desc": "The downloader app could not discover what app it needed to download. If you are running the downloader app independently, note that it will not function without being fed the appropriate data; please refer to the documentation on GitHub." 70 | } 71 | ], 72 | "NET_INIT_FAILED": [ 73 | { 74 | "desc": "The downloader app could not set-up the Wii networking subsystem." 75 | } 76 | ], 77 | "NAND_INIT_FAILED": [ 78 | { 79 | "desc": "The downloader app was unable to read from the internal system memory." 80 | } 81 | ], 82 | "ZIP_OPEN_FAILED": [ 83 | { 84 | "desc": "The downloader app was able to download the app from the Open Shop Channel server, but could not save it to your SD card. Your SD card is likely out of space, or has the write-protect switch enabled." 85 | } 86 | ], 87 | "ZIP_EXTRACT_FAILED": [ 88 | { 89 | "desc": "The downloader app was able to download the app from the Open Shop Channel server, but could not save it to your SD card. Your SD card is likely out of space, or has the write-protect switch enabled." 90 | } 91 | ], 92 | "SUCCESS": [ 93 | { 94 | "desc": "You shouldn't be able to see this." 95 | } 96 | ] 97 | 98 | } 99 | -------------------------------------------------------------------------------- /src/main/resources/static/js/ec-watchdog.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Completes an operation with helpful debugging output regarding state. 3 | * 4 | * @param {ECProgress} progress 5 | * @param {function} callback 6 | */ 7 | function completeOp(progress, callback) { 8 | trace("hi! " + logProgress(progress)); 9 | 10 | const watchdog = new ECWatchdog(3000); 11 | watchdog.complete(callback); 12 | } 13 | 14 | /** 15 | * ECWatchdog is designed to monitor the gradual progression of an asynchronous EC operation. 16 | * It operates by comparing state across poll intervals. 17 | * If it has not made progress for an amount of time, it errors. 18 | * 19 | * @param timeout {number} The amount of milliseconds to wait until calling it quits 20 | * @constructor 21 | */ 22 | function ECWatchdog(timeout) { 23 | /** 24 | * The amount of milliseconds this function should poll throughout until timeout. 25 | */ 26 | this.pollInterval = 1000; 27 | 28 | // However, we can't listen beneath the minimal poll interval. 29 | if (this.pollInterval > timeout) { 30 | error(ErrorCodes.GENERIC_ERROR, "You can't watch beneath the minimal poll interval, every 1 second."); 31 | } 32 | 33 | /** 34 | * Amount of time to sleep in total. 35 | */ 36 | this.timeout = timeout; 37 | 38 | /** 39 | * Current amount of time spent without progress. 40 | */ 41 | var elapsedLackOfProgress = 0; 42 | 43 | /** 44 | * Cached state of operation. 45 | */ 46 | var cachedPhase = 0; 47 | 48 | /** 49 | * Cached download amount. 50 | */ 51 | var cachedDownloadSize = 0; 52 | 53 | /** 54 | * Run a Watchdog lifecycle. 55 | * 56 | * @param callback {function} What to call on success 57 | */ 58 | this.complete = function(callback) { 59 | // Get the foremost operation's progress. 60 | const progress = ec.getProgress(); 61 | 62 | trace("Milliseconds since last accident: " + elapsedLackOfProgress + "\n" + 63 | "Current status: " + progress.status); 64 | 65 | // See if this already has a state in some way that isn't STILL_WORKING. 66 | const status = progress.status; 67 | if (status < 0 && status !== ECReturnCodes.ONGOING) { 68 | // Uh oh... 69 | trace("Operation failed - " + logProgress(progress)); 70 | error(ErrorCodes.EC_ERROR, "requested operation to complete came in with status " + status); 71 | return; 72 | } else if (status === 0) { 73 | // We're free! 74 | trace("Operation completed - running callback."); 75 | callback(); 76 | return; 77 | } 78 | // status should never be above zero. 79 | // Here's hoping no bits flip... 80 | 81 | var didProgress; 82 | // Progress means one of two states: 83 | if (progress.totalSize !== 0) { 84 | // ...one, that we are a download operation with a total size, 85 | // and our attributed downloaded size is different from before. 86 | didProgress = (progress.downloadedSize !== cachedDownloadSize); 87 | trace("Did progress: " + didProgress + ", Downloaded size: " + progress.downloadedSize + ", Cached downloaded size: " + cachedDownloadSize); 88 | } else { 89 | // ...two, that we are waiting for the proper response of an action 90 | // and our phase has changed. 91 | didProgress = (progress.phase !== cachedPhase); 92 | trace("Did progress: " + didProgress + ", New Phase: " + progress.phase + ", Old Phase: " + cachedPhase); 93 | } 94 | 95 | // If we've progressed, we need to update our cache and reset our failure timer. 96 | if (didProgress) { 97 | cachedDownloadSize = progress.downloadedSize; 98 | cachedPhase = progress.phase; 99 | elapsedLackOfProgress = 0; 100 | trace("Resetting lack of progress timer."); 101 | } else { 102 | // If we didn't, we need to add this poll to our failure timer. 103 | elapsedLackOfProgress += this.pollInterval; 104 | trace("Adding " + this.pollInterval + "ms to lack of progress timer."); 105 | } 106 | 107 | // Check if we've gone over our allocated time. 108 | if (elapsedLackOfProgress >= this.timeout) { 109 | error(ErrorCodes.EC_TIMEOUT, "running operation timed out after " + elapsedLackOfProgress + "ms"); 110 | return; 111 | } 112 | 113 | // Continue the cycle as we're not completed. 114 | var currentObject = this; 115 | setTimeout(function() { 116 | currentObject.complete(callback); 117 | }, this.pollInterval); 118 | }; 119 | } 120 | 121 | function logProgress(progress) { 122 | return "progress: status: " + progress.status + ", operation: " + progress.operation + 123 | ", description: " + progress.description + ", phase: " + progress.phase + 124 | ", isCancelRequested: " + progress.isCancelRequested + ", downloadedSize: " + progress.downloadedSize + 125 | ", totalSize: " + progress.totalSize + ", errCode: " + progress.errCode + ", errInfo: " + progress.errInfo; 126 | } -------------------------------------------------------------------------------- /src/main/java/org/oscwii/shop/services/RTitlesService.java: -------------------------------------------------------------------------------- 1 | package org.oscwii.shop.services; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.JsonArray; 5 | import com.google.gson.JsonElement; 6 | import com.google.gson.JsonObject; 7 | import org.oscwii.api.Package; 8 | import org.oscwii.shop.ShopServer; 9 | import org.oscwii.shop.model.RTitlesPage; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.stereotype.Service; 12 | 13 | import java.io.IOException; 14 | import java.io.InputStream; 15 | import java.io.Reader; 16 | import java.nio.file.Files; 17 | import java.nio.file.Path; 18 | import java.util.Arrays; 19 | import java.util.LinkedList; 20 | import java.util.List; 21 | 22 | @Service 23 | public class RTitlesService 24 | { 25 | private final CatalogService catalog; 26 | private final Gson gson; 27 | private final List pages; 28 | 29 | @Autowired 30 | public RTitlesService(CatalogService catalog, Gson gson) throws IOException 31 | { 32 | this.catalog = catalog; 33 | this.gson = gson; 34 | this.pages = loadTitles(); 35 | } 36 | 37 | public List getPages() 38 | { 39 | List pages = new LinkedList<>(); 40 | for(RTitlesPage page : this.pages) 41 | { 42 | if(!page.dynamic()) 43 | { 44 | pages.add(new RTitlesPage(page, 45 | Arrays.stream(page.apps()) 46 | .map(slug -> 47 | { 48 | Package pkg = catalog.getBySlug(slug); 49 | return pkg == null ? "unknown" : pkg.name(); 50 | }) 51 | .toArray(String[]::new))); 52 | continue; 53 | } 54 | 55 | var newPage = new RTitlesPage(page, getDynamicTitles(page.apps())); 56 | pages.add(newPage); 57 | } 58 | 59 | return pages; 60 | } 61 | 62 | private String[] getDynamicTitles(String[] apps) 63 | { 64 | String[] titles = new String[apps.length]; 65 | 66 | for(int i = 0; i < apps.length; i++) 67 | { 68 | String app = apps[i]; 69 | if(!app.startsWith("osc:")) 70 | { 71 | titles[i] = app; 72 | continue; 73 | } 74 | 75 | String[] parts = app.split(":", 3); 76 | Package pkg; 77 | switch(parts[1]) 78 | { 79 | case "featuredapp": 80 | pkg = catalog.getFeaturedApp(); 81 | titles[i] = pkg == null ? "unknown" : pkg.slug(); 82 | break; 83 | case "newest": 84 | if(parts.length > 2) 85 | { 86 | pkg = getNewestPackage(parts[2]); 87 | titles[i] = pkg == null ? "unknown" : pkg.slug(); 88 | } 89 | else 90 | { 91 | pkg = catalog.getNewestPackages().get("newest"); 92 | titles[i] = pkg == null ? "unknown" : pkg.slug(); 93 | } 94 | break; 95 | } 96 | } 97 | 98 | return titles; 99 | } 100 | 101 | private Package getNewestPackage(String category) 102 | { 103 | return catalog.getNewestPackages().get(category); 104 | } 105 | 106 | private List loadTitles() throws IOException 107 | { 108 | List pages = new LinkedList<>(); 109 | 110 | Path file = Path.of("data", "recommended.json"); 111 | if(Files.notExists(file)) 112 | { 113 | try(InputStream res = ShopServer.class.getResourceAsStream("data/recommended.json")) 114 | { 115 | if(res == null) 116 | throw new IllegalStateException("Could not find recommended.json in resources!"); 117 | Files.copy(res, file); 118 | } 119 | } 120 | 121 | try(Reader reader = Files.newBufferedReader(file)) 122 | { 123 | JsonArray root = gson.fromJson(reader, JsonArray.class); 124 | for(JsonElement element : root.asList()) 125 | { 126 | JsonObject obj = element.getAsJsonObject(); 127 | var page = new RTitlesPage(obj.get("id").getAsString(), 128 | obj.get("title").getAsString(), 129 | obj.get("subtitle").getAsString(), 130 | obj.has("dynamic") && obj.get("dynamic").getAsBoolean(), 131 | gson.fromJson(obj.getAsJsonArray("apps"), String[].class)); 132 | pages.add(page); 133 | } 134 | } 135 | 136 | return pages; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/main/resources/templates/title/download.ftl: -------------------------------------------------------------------------------- 1 | <#import "../includes/base-layout.ftl" as layout> 2 | <@layout.header.header "Download Software"> 3 | 4 | 5 | 41 | 42 | 101 | 102 | <#-- TODO DEV:disable buttons --> 103 | <@layout.navigation headerTitle="Download Software" headerBtns=true/> 104 | 105 | <@layout.page> 106 | <#if package??> 107 |
108 |

You are downloading

109 |

${package.name()}

110 |
111 |
112 |
113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 |
NAND Blocks after Download:0Blocks
NAND Blocks after Installation:0Blocks
SD Card Blocks after Installation:0Blocks
130 |
131 |
132 |

Download successful!

133 | <@layout.osc.btn body="OK" id="download-result-btn" href="/home" w="187px" h="55px"/> 134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 | Error code: 000000 142 |
143 |
144 |

Sorry! An error occurred with the download and could not be completed.

145 |
146 |
147 | Discord Support QR 148 |

Please try again, if the error persists, please report this in our Discord server.

149 |
150 |
151 |
152 | <#else> 153 |

The title cannot be found.

154 | 155 | 156 | 157 | <@layout.footer> 158 | 165 | -------------------------------------------------------------------------------- /src/main/resources/templates/catalog.ftl: -------------------------------------------------------------------------------- 1 | <#import "includes/base-layout.ftl" as layout> 2 | <#assign AssetUtil=statics['org.oscwii.shop.utils.AssetUtil']> 3 | <@layout.header.header "Catalog"> 4 | 44 | 45 | 128 | 129 | 130 | <@layout.navigation headerTitle="Catalog" headerBtns=true/> 131 | 132 | <#macro catalogItem slug title author titleId banner="static/img/title_placeholder.png"> 133 | 146 | 147 | 148 | <@layout.page> 149 | <#list packages as package> 150 | <@catalogItem package.slug() package.name() package.author() package.titleInfo().titleId() AssetUtil.getWSCIconUrl(package)/> 151 | 152 | 153 | 154 | <@layout.footer> 155 | 169 | -------------------------------------------------------------------------------- /src/main/resources/templates/title/title.ftl: -------------------------------------------------------------------------------- 1 | <#import "../includes/base-layout.ftl" as layout> 2 | <#assign AssetUtil=statics['org.oscwii.shop.utils.AssetUtil']> 3 | <@layout.header.header (package.name())!""> 4 | 21 | 22 | 109 | 110 | 111 | <@layout.navigation headerTitle="Details" headerBtns=true/> 112 | 113 | <#macro btnDownload label blocks href style=""> 114 | 115 | ${label} 116 | 117 | ${blocks} ${(blocks == 1)?then("Block", "Blocks")} 118 | 119 | 120 | 121 | <@layout.page> 122 | <#if package??> 123 |
${package.category()?capitalize}
124 |
125 |
126 |
127 |
128 | 129 | <@layout.osc.btn body="View compatible controllers" id="btn-controllers" href="controllers" w="67px" h="55px"/> 130 |
131 |
132 |

Version: ${package.version()}

133 |

Released: ${FormatUtil.date(package.releaseDate())}

134 |

Author: ${package.author()}

135 | <#if package.authors()?size gt 0> 136 |

Developers: ${package.authors()?join(", ")}

137 | 138 |

Downloads: ${package.downloads()}

139 | <#-- would just be hidden if there are no subcategories --> 140 |
141 |
142 |
143 |
144 |

${package.name()}

145 |
146 | <@btnDownload "Download" blocks "controllers?download=true" "margin: auto"/> 147 | <#else> 148 |

The title cannot be found.

149 | 150 | 151 | 152 | <@layout.footer> 153 | 159 | -------------------------------------------------------------------------------- /messages.pot: -------------------------------------------------------------------------------- 1 | # Translations template for PROJECT. 2 | # Copyright (C) 2021 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2021. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PROJECT VERSION\n" 10 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 11 | "POT-Creation-Date: 2021-10-15 19:57+0300\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Generated-By: Babel 2.9.1\n" 19 | 20 | #: app.py:24 templates/category.html:81 21 | msgid "Demos" 22 | msgstr "" 23 | 24 | #: app.py:26 templates/category.html:85 25 | msgid "Utilities" 26 | msgstr "" 27 | 28 | #: app.py:28 templates/category.html:82 29 | msgid "Emulators" 30 | msgstr "" 31 | 32 | #: app.py:30 templates/category.html:83 33 | msgid "Games" 34 | msgstr "" 35 | 36 | #: app.py:32 templates/category.html:84 37 | msgid "Media" 38 | msgstr "" 39 | 40 | #: app.py:178 41 | msgid "The requested page could not be found." 42 | msgstr "" 43 | 44 | #: app.py:185 45 | msgid "The server has encountered an error. Try again later." 46 | msgstr "" 47 | 48 | #: data/messages.txt:1 49 | msgid "" 50 | "The forwarder channel was unable to open boot.dol in " 51 | "/openshopchannel/downloader/. Have you installed the downloader app?" 52 | msgstr "" 53 | 54 | #: data/messages.txt:2 55 | msgid "" 56 | "The forwarder channel was unable to access the SD card. Is your SD card " 57 | "inserted?" 58 | msgstr "" 59 | 60 | #: data/messages.txt:3 61 | msgid "" 62 | "The forwarder channel was unable to load boot.dol in " 63 | "/openshopchannel/downloader/ into memory. Have you installed the " 64 | "downloader app?" 65 | msgstr "" 66 | 67 | #: data/messages.txt:4 68 | msgid "" 69 | "The downloader app was unable to find the IP address of the Open Shop " 70 | "Channel server. Please contact the Open Shop Channel developers at " 71 | "https://oscwii.org." 72 | msgstr "" 73 | 74 | #: data/messages.txt:5 75 | msgid "" 76 | "The downloader app was unable to establish a connection to the Open Shop " 77 | "Channel server. The Open Shop Channel's content delivery network may be " 78 | "down." 79 | msgstr "" 80 | 81 | #: data/messages.txt:6 82 | msgid "" 83 | "The downloader app was able to connect to the Open Shop Channel server, " 84 | "but could not communicate with it." 85 | msgstr "" 86 | 87 | #: data/messages.txt:7 88 | msgid "" 89 | "The downloader app was able to connect to the Open Shop Channel server, " 90 | "but it did not send a response that the downloader app could understand." 91 | msgstr "" 92 | 93 | #: data/messages.txt:8 94 | msgid "" 95 | "The downloader app was able to download the app from the Open Shop " 96 | "Channel server, but could not save it to your SD card. Your SD card is " 97 | "likely out of space, or has the write-protect switch enabled." 98 | msgstr "" 99 | 100 | #: data/messages.txt:9 101 | msgid "The downloader app was unable to read from the internal system memory." 102 | msgstr "" 103 | 104 | #: data/messages.txt:10 105 | msgid "" 106 | "The downloader app could not discover what app it needed to download. If " 107 | "you are running the downloader app independently, note that it will not " 108 | "function without being fed the appropriate data; please refer to the " 109 | "documentation on GitHub." 110 | msgstr "" 111 | 112 | #: data/messages.txt:11 113 | msgid "The downloader app could not set-up the Wii networking subsystem." 114 | msgstr "" 115 | 116 | #: data/messages.txt:12 117 | msgid "You shouldn't be able to see this." 118 | msgstr "" 119 | 120 | #: templates/app.html:4 121 | msgid "Application" 122 | msgstr "" 123 | 124 | #: templates/app.html:152 templates/browse.html:180 templates/category.html:91 125 | #: templates/donate.html:56 templates/error.html:79 templates/keyword.html:85 126 | #: templates/search.html:152 127 | msgid "Back" 128 | msgstr "" 129 | 130 | #: templates/app.html:155 131 | msgid "Download" 132 | msgstr "" 133 | 134 | #: templates/browse.html:152 templates/keyword.html:4 templates/keyword.html:78 135 | msgid "Search by Keyword" 136 | msgstr "" 137 | 138 | #: templates/browse.html:159 templates/category.html:4 139 | #: templates/category.html:78 140 | msgid "Search by Category" 141 | msgstr "" 142 | 143 | #: templates/browse.html:166 144 | msgid "Featured Application" 145 | msgstr "" 146 | 147 | #: templates/browse.html:173 148 | msgid "Random Application" 149 | msgstr "" 150 | 151 | #: templates/category.html:94 templates/keyword.html:88 152 | msgid "Search" 153 | msgstr "" 154 | 155 | #: templates/donate.html:4 156 | msgid "Donate" 157 | msgstr "" 158 | 159 | #: templates/error.html:4 160 | msgid "Error" 161 | msgstr "" 162 | 163 | #: templates/landing.html:4 164 | msgid "Welcome" 165 | msgstr "" 166 | 167 | #: templates/landing.html:99 168 | msgid "Start Browsing" 169 | msgstr "" 170 | 171 | #: templates/landing.html:102 172 | msgid "App of the Day" 173 | msgstr "" 174 | 175 | #: templates/search.html:4 templates/search.html:132 176 | msgid "Search Results" 177 | msgstr "" 178 | 179 | #: templates/search.html:157 180 | msgid "Previous" 181 | msgstr "" 182 | 183 | #: templates/search.html:163 184 | msgid "Next" 185 | msgstr "" 186 | 187 | #: templates/startdownload.html:4 188 | msgid "Start Download" 189 | msgstr "" 190 | 191 | -------------------------------------------------------------------------------- /src/main/java/org/oscwii/shop/controllers/PageController.java: -------------------------------------------------------------------------------- 1 | package org.oscwii.shop.controllers; 2 | 3 | import jakarta.servlet.RequestDispatcher; 4 | import jakarta.servlet.http.HttpServletRequest; 5 | import org.oscwii.api.Package; 6 | import org.oscwii.shop.utils.Paginator; 7 | import org.springframework.boot.web.servlet.error.ErrorController; 8 | import org.springframework.stereotype.Controller; 9 | import org.springframework.ui.Model; 10 | import org.springframework.web.bind.annotation.CookieValue; 11 | import org.springframework.web.bind.annotation.GetMapping; 12 | import org.springframework.web.bind.annotation.RequestParam; 13 | 14 | import java.util.Collections; 15 | import java.util.Comparator; 16 | import java.util.List; 17 | import java.util.Map; 18 | import java.util.Optional; 19 | import java.util.TreeMap; 20 | import java.util.function.Function; 21 | import java.util.stream.Collectors; 22 | 23 | import static java.lang.String.CASE_INSENSITIVE_ORDER; 24 | 25 | @Controller 26 | public class PageController extends BaseController implements ErrorController 27 | { 28 | /*private final RTitlesService recommendedTitles; 29 | 30 | @Autowired 31 | public PageController(RTitlesService recommendedTitles) 32 | { 33 | this.recommendedTitles = recommendedTitles; 34 | }*/ 35 | 36 | @GetMapping("/debug") 37 | public String debug(@CookieValue String language, Model model) 38 | { 39 | if(!isDevelopment()) 40 | return "redirect:/"; 41 | model.addAttribute("lang", language); 42 | return "debug"; 43 | } 44 | 45 | @GetMapping("/error") 46 | public String handleError(HttpServletRequest request, Model model, @RequestParam(value = "code") Optional ecError) 47 | { 48 | Object code = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE); 49 | if(code != null) 50 | if((int) code == 404) 51 | return "error/404"; 52 | 53 | if(ecError.isPresent()) 54 | { 55 | model.addAttribute("code", ecError.get()); 56 | return "error/ecerror"; 57 | } 58 | 59 | return "error/500"; 60 | } 61 | 62 | @GetMapping("/landing") 63 | public String landing(Model model) 64 | { 65 | model.addAttribute("catalog", catalog) 66 | .addAttribute("featuredPackage", catalog.getFeaturedApp()) 67 | .addAttribute("newestPackages", catalog.getNewestPackages()); 68 | //.addAttribute("rTitles", recommendedTitles.getPages()) 69 | return "landing"; 70 | } 71 | 72 | @GetMapping("/home") 73 | public String home() 74 | { 75 | return "home"; 76 | } 77 | 78 | @GetMapping("/browse") 79 | public String browse(@RequestParam(required = false, defaultValue = "games") String category, Model model) 80 | { 81 | model.addAttribute("category", category); 82 | return "browse"; 83 | } 84 | 85 | @GetMapping("/search") 86 | public String search(@RequestParam(required = false) String query, @RequestParam String type, 87 | @RequestParam String category, 88 | @RequestParam(defaultValue = "1") int page, Model model) 89 | { 90 | model.addAttribute("category", category); 91 | Comparator sortingCriteria = Comparator.comparing(Package::name); 92 | 93 | switch(type) 94 | { 95 | case "popular": 96 | sortingCriteria = Comparator.comparing(Package::downloads).reversed(); 97 | break; 98 | case "newest": 99 | sortingCriteria = Comparator.comparing(Package::releaseDate).reversed(); 100 | break; 101 | case "publishers": 102 | if(query == null) 103 | return listPublishers(model, category); 104 | else 105 | return publisherSearch(page, model, category, query); 106 | } 107 | 108 | List packages = catalog.filterPackages(category, query) 109 | .stream() 110 | .sorted(sortingCriteria) 111 | .toList(); 112 | Paginator paginator = new Paginator<>(packages); 113 | model.addAttribute("packages", paginator.paginate(page)) 114 | .addAttribute("currentPage", page) 115 | .addAttribute("pages", paginator.getNumberOfPages()); 116 | return "catalog"; 117 | } 118 | 119 | private String listPublishers(Model model, String category) 120 | { 121 | Map publishers = new TreeMap<>(CASE_INSENSITIVE_ORDER); 122 | List packages = catalog.filterPackages(category, null); 123 | 124 | List authors = packages.stream() 125 | .map(Package::author) 126 | .toList(); 127 | publishers.putAll(authors.stream() 128 | .distinct() 129 | .collect(Collectors.toMap( 130 | Function.identity(), 131 | publisher -> Collections.frequency(authors, publisher) 132 | ))); 133 | model.addAttribute("publishers", publishers); 134 | return "publishers"; 135 | } 136 | 137 | private String publisherSearch(int page, Model model, String category, String query) 138 | { 139 | List packages = catalog.filterPackages(category, null) 140 | .stream() 141 | .filter(pkg -> pkg.author().equalsIgnoreCase(query)) 142 | .sorted(Comparator.comparing(Package::author)) 143 | .toList(); 144 | Paginator paginator = new Paginator<>(packages); 145 | model.addAttribute("packages", paginator.paginate(page)) 146 | .addAttribute("currentPage", page) 147 | .addAttribute("pages", paginator.getNumberOfPages()); 148 | return "catalog"; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/main/resources/templates/landing.ftl: -------------------------------------------------------------------------------- 1 | <#import "includes/base-layout.ftl" as layout> 2 | <#assign AssetUtil=statics['org.oscwii.shop.utils.AssetUtil']> 3 | <@layout.header.header "Landing"> 4 | 15 | 16 | 113 | 114 | 115 | <@layout.navigation dots=false> 116 | <#--Recommended Titles-->Highlights 117 | 118 | 119 | <#macro recommendedTitle slug name author category categoryId banner="static/img/title_placeholder.png"> 120 | 137 | 138 | 139 | <@layout.page> 140 |
141 | <#if featuredPackage??> 142 | <@recommendedTitle featuredPackage.slug() featuredPackage.name() 143 | featuredPackage.author() "App of the Day" "aod" AssetUtil.getWSCIconUrl(featuredPackage) 144 | /> 145 | 146 | <#if newestPackages??> 147 | <#if newestPackages["games"]??> 148 | <@recommendedTitle newestPackages["games"].slug() newestPackages["games"].name() 149 | newestPackages["games"].author() "Latest update in ${newestPackages['games'].category()?capitalize}" 150 | newestPackages["games"].category() AssetUtil.getWSCIconUrl(newestPackages["games"]) 151 | /> 152 | <#else> 153 | <@recommendedTitle "Unknown" "Unknown" "Unknown" "Unknown" ""/> 154 | 155 | <#if newestPackages["utilities"]??> 156 | <@recommendedTitle newestPackages["utilities"].slug() newestPackages["utilities"].name() 157 | newestPackages["utilities"].author() "Latest update in ${newestPackages['utilities'].category()?capitalize}" 158 | newestPackages["utilities"].category() AssetUtil.getWSCIconUrl(newestPackages["utilities"]) 159 | /> 160 | <#else> 161 | <@recommendedTitle "Unknown" "Unknown" "Unknown" "Unknown" ""/> 162 | 163 | <#if newestPackages["emulators"]??> 164 | <@recommendedTitle newestPackages["emulators"].slug() newestPackages["emulators"].name() 165 | newestPackages["emulators"].author() "Latest update in ${newestPackages['emulators'].category()?capitalize}" 166 | newestPackages["emulators"].category() AssetUtil.getWSCIconUrl(newestPackages["emulators"]) 167 | /> 168 | <#else> 169 | <@recommendedTitle "Unknown" "Unknown" "Unknown" "Unknown" ""/> 170 | 171 | 172 |
173 | <#--<#assign first=true> 174 | <#list rTitles as page> 175 |
176 | <#assign first=false> 177 | <#list page.apps() as title> 178 | <#if title == "unknown"> 179 | <@recommendedTitle title "Unknown" "Unknown"/> 180 | <#else> 181 | <#assign app=catalog.getBySlug(title)> 182 | <@recommendedTitle app.slug() app.name() app.author()/> 183 | 184 | 185 |
186 | --> 187 | 188 | 189 | <@layout.footer dots=false> 190 | <@layout.osc.btn body="Start Shopping" id="start-shopping-btn" href="/home" w="353px" h="56px"/> 191 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | org.springframework.boot 9 | spring-boot-starter-parent 10 | 3.2.4 11 | 12 | 13 | 14 | org.oscwii.shop 15 | shop-frontend 16 | 1.0-SNAPSHOT 17 | 18 | 19 | 21 20 | 2.23.1 21 | 7.8.0 22 | 23 | UTF-8 24 | 25 | 26 | 27 | 28 | osc 29 | https://repo.craftium.net/repository/maven-snapshots/ 30 | 31 | 32 | 33 | 34 | 35 | 36 | io.sentry 37 | sentry-bom 38 | ${sentry.version} 39 | pom 40 | import 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | org.oscwii 49 | openshop-api 50 | v4-SNAPSHOT 51 | 52 | 53 | 54 | 55 | org.springframework.boot 56 | spring-boot-starter-web 57 | 58 | 59 | 60 | org.springframework.boot 61 | spring-boot-starter-logging 62 | 63 | 64 | 65 | 66 | 67 | org.springframework.boot 68 | spring-boot-starter-log4j2 69 | 70 | 71 | 72 | org.springframework.boot 73 | spring-boot-starter-freemarker 74 | 75 | 76 | 77 | org.springframework 78 | spring-context-support 79 | 80 | 81 | 82 | 83 | org.freemarker 84 | freemarker 85 | 2.3.32 86 | 87 | 88 | 89 | no.api.freemarker 90 | freemarker-java8 91 | 2.1.0 92 | 93 | 94 | 95 | 96 | com.squareup.okhttp3 97 | okhttp 98 | 4.12.0 99 | 100 | 101 | 102 | com.google.code.gson 103 | gson 104 | 105 | 106 | 107 | 108 | org.apache.logging.log4j 109 | log4j-core 110 | ${log4j2.version} 111 | 112 | 113 | 114 | org.apache.logging.log4j 115 | log4j-slf4j2-impl 116 | ${log4j2.version} 117 | 118 | 119 | 120 | org.apache.logging.log4j 121 | log4j-jul 122 | ${log4j2.version} 123 | 124 | 125 | 126 | 127 | io.sentry 128 | sentry-log4j2 129 | 130 | 131 | 132 | io.sentry 133 | sentry-spring-boot-starter-jakarta 134 | 135 | 136 | 137 | io.sentry 138 | sentry-okhttp 139 | 140 | 141 | 142 | 143 | com.google.guava 144 | guava 145 | 33.3.0-jre 146 | 147 | 148 | 149 | 150 | 151 | 152 | org.apache.maven.plugins 153 | maven-compiler-plugin 154 | 3.12.1 155 | 156 | 157 | ${java.version} 158 | ${java.version} 159 | 160 | 161 | 162 | 163 | org.springframework.boot 164 | spring-boot-maven-plugin 165 | 166 | 167 | 168 | io.github.git-commit-id 169 | git-commit-id-maven-plugin 170 | 171 | 172 | 173 | generate-resources 174 | 175 | revision 176 | 177 | 178 | 179 | 180 | 181 | false 182 | yyyy-MM-dd-HH:mm:ss 183 | true 184 | 185 | 186 | 187 | 188 | -------------------------------------------------------------------------------- /src/main/resources/static/js/common.js: -------------------------------------------------------------------------------- 1 | /** @type {boolean} */ 2 | const isWiiShop = navigator.userAgent.indexOf("Wii Shop") !== -1; 3 | 4 | /** 5 | * Channel interfaces 6 | */ 7 | var shop = new wiiShop(); 8 | var ec = new ECommerceInterface(); 9 | var info = ec.getDeviceInfo(); 10 | var NWC24 = new wiiNwc24(); 11 | var sound = new wiiSound(); 12 | var keyboard = new wiiKeyboard(); 13 | var sdCard = new wiiSDCard(); 14 | 15 | /** 16 | * Enum describing the available wallpapers that show in 16:9. 17 | * @readonly 18 | * @enum {number} 19 | */ 20 | const WallpaperType = { 21 | DOTTED_HORIZONTAL_LINES: 0, 22 | BLACK: 1, 23 | WHITE: 2, 24 | BLUE_VERTICAL_LINES: 3 25 | }; 26 | 27 | /** 28 | * Enum describing sounds available to play. 29 | * @readonly 30 | * @enum {number} 31 | */ 32 | const SoundType = { 33 | PUSH: 1, 34 | HOVER: 2, 35 | SELECT: 3, 36 | CANCEL: 4, 37 | CHOICE_CHANGE: 5, 38 | ERROR: 6, 39 | ADD_POINT: 7, 40 | DOWNLOAD_COMPLETE: 8, 41 | SMALL_MARIO_JUMP: 9, 42 | LARGE_MARIO_JUMP: 10, 43 | FIRE_BALL: 11, 44 | COIN: 12, 45 | HIT_BLOCK: 13, 46 | COPYING: 14, 47 | LOADING: 15 48 | }; 49 | 50 | /** 51 | * Enum describing types of available keyboards. 52 | * @readonly 53 | * @enum {number} 54 | */ 55 | const KeyboardType = { 56 | // The "default" keyboard. 57 | DEFAULT: 0, 58 | // Also the "default" keyboard. 59 | // This may differ across locales, but it does not appear to. 60 | DEFAULT_TWO: 1, 61 | // Provides a number entry keyboard. 62 | NUMBER_PAD: 2, 63 | // The "default" keyboard, but without word completion or a return key. 64 | DEFAULT_NO_COMPLETION: 3, 65 | // Text entered is present within a large font. 66 | LARGE_FONT: 4, 67 | // The "default" keyboard, but without word completion, return key, 68 | // or the switcher to a number pad. 69 | DEFAULT_NO_COMPLETION_PAD: 5, 70 | // The large font keyboard, but without word completion, return key, 71 | // or the switcher to a number pad. 72 | LARGE_FONT_NO_COMPLETION_PAD: 6, 73 | // The number pad keyboard, but with a decimal option. 74 | NUMBER_PAD_DECIMAL: 7, 75 | // Also the number pad keyboard with decimal option. 76 | // This may differ across locales, but it does not appear to. 77 | NUMBER_PAD_DECIMAL_TWO: 8, 78 | // The keyboard used for friend code entry, dividing every 4 numbers into groups. 79 | FRIEND_CODE_ENTRY: 9, 80 | // The "default" keyboard, but without a return key. 81 | DEFAULT_NO_RETURN: 10 82 | }; 83 | 84 | /** 85 | * Hastily displays an error message within logging. 86 | * Please rewrite this function later. 87 | * 88 | * @param {number} code The error code. 89 | * @param {string} message The message to display. 90 | */ 91 | function error(code, message) { 92 | // TODO: should this become an enum of errors for easier localization? 93 | trace("An error occurred: " + message + "(" + code + ")"); 94 | // If debug is enabled, go to console, else show error page 95 | if (isDevelopment) { 96 | window.location.href = "/debug"; 97 | } else { 98 | window.location.href = "/error?code=" + code; 99 | } 100 | } 101 | 102 | /** 103 | * Gets the browser to redraw the page. 104 | * A required hack due to a Wii Shop Channel bug with SVGs. 105 | * 106 | * @param {HTMLElement} el Element to perform redraw on. 107 | * @param {number} ms Time in milliseconds to delay redraw by. 108 | */ 109 | function redrawElement(el, ms) { 110 | setTimeout(function() { 111 | var disp = el.style.display; 112 | el.style.display = "none"; 113 | el.style.display = disp; 114 | }, ms); 115 | } 116 | 117 | /** 118 | * Apply miscellaneous page fixes for both the Wii Shop and other browsers (for testing). 119 | */ 120 | function pageFixes() { 121 | if (isWiiShop) { 122 | // Work around WSC bug where SVGs aren't shown after they're loaded unless page is redrawn 123 | redrawElement(document.body, $(".btn").length * 100); 124 | } 125 | } 126 | 127 | /** 128 | * Element to scroll when buttons are pressed. 129 | * @type {HTMLElement} 130 | */ 131 | var scrollTarget; 132 | 133 | /** 134 | * Amount of pixels to scroll by. 135 | * @type {number} 136 | */ 137 | var scrollStep = 30; 138 | 139 | /** 140 | * Setup and listen to specific events on the page, as to scroll when the D-pad buttons are pressed, 141 | * and to set the scroll target to the most recently hovered element that's vertically scrollable. 142 | */ 143 | function setupScrolling() { 144 | // This isn't great as this doesn't work if you hover over a child of a scrollable element. 145 | $(document).mouseover(function(e) { 146 | var el = e.target; 147 | if (el.scrollHeight > el.clientWidth) 148 | scrollTarget = el; 149 | }); 150 | 151 | $(document).keypress(function(e) { 152 | if (!scrollTarget) 153 | return; 154 | 155 | /* The Wii Shop seems to have weird behaviour regarding scrolling with controller buttons. 156 | * Left and right always work fine and fire once when they're pressed as usual, but with up 157 | * and down they seem to be on a timer and refire. This is a weird move by Nintendo, as 158 | * utilising the keydown and keyup events would be much more appropriate than modifying the 159 | * browser behaviour for keypress. 160 | * 161 | * Another thing we have to do is preventDefault the event for keyCode 37 (left arrow), as 162 | * for some reason that's sent in tandem with the events for the up and down buttons. 163 | * Not preventDefaulting it will result in the events for up and down only firing once and 164 | * never again unless you focus the element again by pressing A on it, as it seems to 165 | * unfocus it. This is odd because this issue is not present on Nintendo's pages. 166 | */ 167 | switch (e.keyCode) { 168 | case 175: 169 | scrollTarget.scrollTop -= scrollStep; break; 170 | case 176: 171 | scrollTarget.scrollTop += scrollStep; break; 172 | case 178: 173 | scrollTarget.scrollLeft -= scrollStep; break; 174 | case 177: 175 | scrollTarget.scrollLeft += scrollStep; break; 176 | case 37: 177 | e.preventDefault(); break; 178 | } 179 | }); 180 | } 181 | 182 | /** 183 | * Returns an object representing the query string parameters. 184 | * @returns {Object} Query string split by keys and values. Value will be null if none specified. 185 | */ 186 | function getSplitQueryString() { 187 | var split = location.search.substring(1).split('&'); 188 | var params = {}; 189 | 190 | for (var i = 0; i < split.length; i++) { 191 | var equalsIndex = split[i].indexOf('='); 192 | 193 | if (equalsIndex == -1) { 194 | params[split[i]] = null; 195 | continue; 196 | } else if (equalsIndex == split[i].length - 1) { 197 | params[split[i].substring(0, split[i].length - 1)] = null; 198 | continue; 199 | } 200 | 201 | params[split[i].substring(0, equalsIndex)] = decodeURIComponent(split[i].substring(equalsIndex + 1)); 202 | } 203 | 204 | return params; 205 | } 206 | 207 | /** 208 | * Performs common page load tasks. 209 | */ 210 | function onLoadCommon() { 211 | if (isWiiShop) 212 | initializeEC(); 213 | 214 | setupButtons(); 215 | 216 | setupScrolling(); 217 | scrollTarget = $("#main-content")[0]; // Reasonable assumption to make. 218 | 219 | pageFixes(); 220 | } 221 | 222 | /** 223 | * Utility function to retrieve a session value from EC and then remove it. 224 | * @param {string} name 225 | * @returns {string} 226 | */ 227 | function getAndClearSessionValue(name) { 228 | const value = ec.getSessionValue(name); 229 | ec.setSessionValue(name, ""); 230 | return value; 231 | } 232 | 233 | /** 234 | * Enum describing error codes. 235 | * @readonly 236 | * @enum {number} 237 | */ 238 | const ErrorCodes = { 239 | GENERIC_ERROR: 100, 240 | EC_ERROR: 200, 241 | EC_TIMEOUT: 201, 242 | EC_FAILED_REGISTRATION: 202, 243 | API_ERROR: 300, 244 | API_DOWNLOAD_NOTIFICATION_FAILED: 301 245 | } -------------------------------------------------------------------------------- /src/main/resources/templates/title/prepare-download.ftl: -------------------------------------------------------------------------------- 1 | <#import "../includes/base-layout.ftl" as layout> 2 | <@layout.header.header "Download"> 3 | 109 | 110 | 227 | 228 | 229 | <@layout.navigation headerTitle="Download" headerBtns=true/> 230 | 231 | <@layout.page> 232 | <#if package??> 233 |
234 | <#-- Download location selection - start --> 235 |
236 | <#-- sometimes you ain't got no choice --> 237 | 238 | 239 | 240 | 241 | 242 | <#-- These are encoded automatically by the button --> 243 | 244 | 245 | 246 |
Please choose a location to download the data to.
<@layout.osc.btn body="Wii System Memory" id="nand-btn" href="javascript:setDownloadLocation(\"nand\")" w="228px" h="176px" img="/static/img/blank.gif"/><@layout.osc.btn body="SD Card" id="sd-card-btn" href="javascript:setDownloadLocation(\"sd\")" w="228px" h="176px" img="/static/img/blank.gif"/>
247 |
248 | <#-- Download location selection - end --> 249 | <#-- Download summary - start --> 250 |
251 |
252 |
253 |

${package.name()}

254 |
255 |
256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 |
NAND Open Blocks:4096Blocks
NAND Blocks to Download:0Blocks
NAND Blocks after Download:0Blocks
NAND Blocks after Installation:0Blocks
SD Card Open Blocks:0Blocks
SD Card Blocks to Download:0Blocks
SD Card Blocks after Download:0Blocks
294 |
295 | 299 |
300 |

Download this software to Wii
system memory now?

301 |
302 | <#-- Download summary - end --> 303 |
304 | <#else> 305 |

The title cannot be found.

306 | 307 | 308 | 309 | <@layout.footer> 310 | 322 | -------------------------------------------------------------------------------- /src/main/resources/static/js/skeleton.js: -------------------------------------------------------------------------------- 1 | // !!! Do not include this file on the actual Wii Shop Channel !!! 2 | // This only exists to assist with code completion for common functions. 3 | 4 | /** 5 | * Outputs a trace-level message via the EC logger. 6 | * @param {string} message Message to trace 7 | */ 8 | function trace(message) {} 9 | 10 | /** 11 | * Represents the wiiShop object type inserted into the engine on any Wii. 12 | * This object allows various interaction with the channel's runtime. 13 | * You should instantiate it without any parameters. 14 | * 15 | * @constructor 16 | */ 17 | function wiiShop() { 18 | /** 19 | * Shows the main loading spinner. 20 | */ 21 | this.beginWaiting = function() {} 22 | 23 | /** 24 | * Hides the main loading spinner. 25 | */ 26 | this.endWaiting = function() {} 27 | 28 | /** 29 | * Resets the channel. 30 | **/ 31 | this.retry = function() {} 32 | 33 | /** 34 | * Disables the HOME menu. 35 | */ 36 | this.disableHRP = function() {} 37 | 38 | /** 39 | * Enables the HOME menu. 40 | */ 41 | this.enableHRP = function() {} 42 | 43 | /** 44 | * Returns the localized string for the shop connecting. 45 | * For the English locale, this is "Connecting. Please wait..." 46 | * It is mandatory to access this, such as setting its value to an unused variable. 47 | * This disables an internal timer that causes the error 209601. 48 | **/ 49 | this.connecting = ""; 50 | 51 | /** 52 | * Throws an error, going to a separate page. 53 | * 54 | * @param {number} number Number of the error code to display. 55 | * @param {number} type Type of error message to display. 56 | */ 57 | this.error = function(number, type) {} 58 | 59 | /** 60 | * Sets the image wrapping the 61 | * 62 | * @param {number} wallpaper The type of wallpaper to set. 63 | * 64 | * See https://docs.oscwii.org/wii-shop-channel/js/shop 65 | */ 66 | this.setWallpaper = function(wallpaper) {} 67 | 68 | /** 69 | * Changes the appearance of Mario. 70 | * 0 is small Mario 71 | * 1 is star Mario 72 | * 2 is Mario running against Bullet Bill 73 | * 3 is Fire Mario 74 | * @param {number} value 75 | */ 76 | this.setSCARank = function (value) {} 77 | } 78 | 79 | /** 80 | * Represents the wiiKeyboard object type inserted into the engine on any Wii. 81 | * This object allows using a native keyboard. 82 | * You should instantiate it without any parameters. 83 | * 84 | * @constructor 85 | */ 86 | function wiiKeyboard() { 87 | /** 88 | * Brings up a keyboard. 89 | * @param {number} type Type of keyboard. See also: https://docs.oscwii.org/wii-shop-channel/js/keyboard 90 | * @param {number} rowLimit Number of rows the user should be able to type. 91 | * @param {boolean} isPasswordField Whether to treat text entered as a password or not. 92 | * @param {string} title Content to show as a hint while typing. 93 | */ 94 | this.call = function(type, rowLimit, isPasswordField, title) {} 95 | } 96 | 97 | /** 98 | * Represents the wiiSound object type inserted into the engine on any Wii. 99 | * You should instantiate it without any parameters. 100 | * 101 | * @constructor 102 | */ 103 | function wiiSound() { 104 | /** 105 | * Plays the Wii Shop Channel theme. 106 | */ 107 | this.playBGM = function() {} 108 | 109 | /** 110 | * Plays the given sound. 111 | * 112 | * @param {number} sound The sound to play. 113 | */ 114 | this.playSE = function(sound) {} 115 | } 116 | 117 | /** 118 | * Represents the ECTitleInfo object type inserted into the engine on any Wii. 119 | * While this can be instantiated alone, it's not recommended. 120 | * 121 | * @see ECommerceInterface.getTitleInfo 122 | * @constructor 123 | */ 124 | function ECTitleInfo() { 125 | /** 126 | * The current title version. 127 | * @type {number} 128 | */ 129 | this.version = 0 130 | 131 | /** 132 | * Whether the title is installed onto the device. 133 | * @type {boolean} 134 | */ 135 | this.isOnDevice = false 136 | } 137 | 138 | /** 139 | * Represents the ECPrice object type inserted into the engine on any Wii. 140 | * This object represents the price a client wishes to pay when 141 | * sending to the server. 142 | * While this can be instantiated alone, it's not recommended. 143 | * 144 | * @param {string} amount The amount to pay, such as '4.99'. 145 | * @param {string} currency The currency desired, such as 'POINTS', or 'USD'. 146 | * @constructor 147 | */ 148 | function ECPrice(amount, currency) { 149 | 150 | } 151 | 152 | /** 153 | * A native object representing a payment with the device's shop account. 154 | * 155 | * @constructor 156 | */ 157 | function ECAccountPayment() { 158 | 159 | } 160 | 161 | /** 162 | * A native object representing an array of title limits. 163 | * 164 | * @constructor 165 | */ 166 | function ECTitleLimits() { 167 | 168 | } 169 | 170 | /** 171 | * Represents the ECommerceInterface object type inserted into the engine on any Wii. 172 | * This high-level object allows a great amount of interaction with the underlying EC library. 173 | * You should instantiate it without any parameters, and only have one per page. 174 | * 175 | * @constructor 176 | */ 177 | function ECommerceInterface() { 178 | /** 179 | * Sets internal engine endpoints. 180 | * 181 | * @param {string} ecsUrl The endpoint used for ECS-related requests. 182 | */ 183 | this.setWebSvcUrls = function(ecsUrl) {} 184 | 185 | /** 186 | * Sets internal engine endpoints. 187 | * 188 | * @param {string} ecsUrl The endpoint used for ECS-related requests. 189 | * @param {string} iasUrl The endpoint used for IAS-related requests. 190 | */ 191 | this.setWebSvcUrls = function(ecsUrl, iasUrl) {} 192 | 193 | /** 194 | * Sets internal engine endpoints. 195 | * 196 | * @param {string} ecsUrl The endpoint used for ECS-related requests. 197 | * @param {string} iasUrl The endpoint used for IAS-related requests. 198 | * @param {string} casUrl The endpoint used for CAS-related requests. Unused within the Wii Shop Channel. 199 | */ 200 | this.setWebSvcUrls = function(ecsUrl, iasUrl, casUrl) {} 201 | 202 | /** 203 | * Sets content URLs. 204 | * 205 | * @param ccsUrl The URL to use for cached content downloads over HTTP 206 | */ 207 | this.setContentUrls = function(ccsUrl) {} 208 | 209 | /** 210 | * Sets content URLs. 211 | * 212 | * @param ccsUrl The URL to use for cached content downloads over HTTP 213 | * @param ucsUrl The URL to use for uncached content downloads over HTTPS 214 | */ 215 | this.setContentUrls = function(ccsUrl, ucsUrl) {} 216 | 217 | /** 218 | * Retrieves the current log. 219 | * 220 | * @returns {string} 221 | */ 222 | this.getLog = function() {} 223 | 224 | /** 225 | * Requests for a challenge from the server. 226 | * 227 | * Note that with WiiSOAP as the backend, 228 | * the challenge is hardcoded to "NintyWhyPls". 229 | * 230 | * @see getChallengeResp 231 | * @returns {ECProgress} 232 | */ 233 | this.sendChallengeReq = function() {} 234 | 235 | /** 236 | * Returns the challenge as requested from the server. 237 | * 238 | * @returns {string} 239 | */ 240 | this.getChallengeResp = function() {} 241 | 242 | /** 243 | * Returns progress for the foremost operation. 244 | * 245 | * @returns {ECProgress} 246 | */ 247 | this.getProgress = function() {} 248 | 249 | /** 250 | * Synchronizes identifiers such as ticket sync times 251 | * and the device account's balance. 252 | * 253 | * @returns {ECProgress} 254 | */ 255 | this.checkDeviceStatus = function() {} 256 | 257 | /** 258 | * Returns information about this device's identifiers, 259 | * storage, and service status. 260 | * 261 | * @returns {ECDeviceInfo} 262 | */ 263 | this.getDeviceInfo = function() {} 264 | 265 | /** 266 | * Syncronizes the device's registration status. 267 | * 268 | * @see ECDeviceInfo.registrationStatus 269 | * @returns {ECProgress} 270 | */ 271 | this.checkRegistration = function() {} 272 | 273 | /** 274 | * Requests for this console to be registered. 275 | * 276 | * @param {string} challenge The challenge returned from the server. 277 | * @returns {ECProgress} 278 | */ 279 | this.register = function(challenge) {} 280 | 281 | /** 282 | * Syncs the device's tokens from the server. 283 | * 284 | * @param {string} challenge The challenge returned from the server. 285 | * @returns {ECProgress} 286 | */ 287 | this.syncRegistration = function(challenge) {} 288 | 289 | /** 290 | * Sets a persistent value within the device's EC configuration. 291 | * 292 | * By default, this is the configuration file present within 293 | * "/title/00010002/48414241/data/ec.cfg". 294 | * OSC patches rename it to "osc.cfg" to avoid conflict. 295 | * 296 | * @param {string} key Name of the config key to set. 297 | * @param {string} value Contents of the value to set. 298 | */ 299 | this.setPersistentValue = function(key, value) {} 300 | 301 | /** 302 | * Launches the given channel by title ID and ticket ID. 303 | * 304 | * @param {string} titleId Title ID of the channel. 305 | * @param {string} ticketId Ticket ID of the channel. 306 | */ 307 | this.launchTitle = function(titleId, ticketId) {} 308 | 309 | /** 310 | * Returns title metadata for the given title ID. 311 | * 312 | * @param {string} titleId The title ID to retrieve metadata for. 313 | * @returns {ECTitleInfo} Title metadata. 314 | */ 315 | this.getTitleInfo = function(titleId) {} 316 | 317 | /** 318 | * Purchases a title. 319 | * 320 | * @param {string} titleId The title ID to purchase. 321 | * @param {string} itemId Unknown. 322 | * @param {ECPrice} price The price the client wishes to use. 323 | * @param {ECAccountPayment} payment The payment method to use. Note that ECard/CreditCard payment types are also available. 324 | * @param {ECTitleLimits} limits An array of limits to apply with this title. 325 | * @param {boolean} downloadContent Whether to download title upon purchase. 326 | * @param {string} [taxes] 327 | * @param {string} [purchaseInfo] 328 | * @param {string} [discount] 329 | * @returns {ECProgress} 330 | */ 331 | this.purchaseTitle = function(titleId, itemId, price, payment, limits, downloadContent, taxes, purchaseInfo, discount) {} 332 | 333 | /** 334 | * Quite possibly downloads a title. 335 | * 336 | * @param {string} titleId 337 | */ 338 | this.downloadTitle = function (titleId) {} 339 | 340 | /** 341 | * For the current EC object, get a value for the given name from an internal store. 342 | * 343 | * This key will persist within the internal store for the duration of the channel's runtime. 344 | * 345 | * @param {string} name 346 | */ 347 | this.getSessionValue = function(name) {} 348 | 349 | /** 350 | * Sets the key for `name` to `value` within the internal store. 351 | * 352 | * @param {string} name 353 | * @param {string} value 354 | */ 355 | this.setSessionValue = function(name, value) {} 356 | } 357 | 358 | /** 359 | * SD Card innit 360 | * 361 | * @constructor 362 | */ 363 | function wiiSDCard() { 364 | /** 365 | * 366 | * @param {string} titleId 367 | */ 368 | this.backupToSDCard = function (titleId) {} 369 | 370 | /** 371 | * 372 | * @param {string} titleId 373 | */ 374 | this.setJournalFlag = function (titleId) {} 375 | 376 | /** 377 | * 378 | * @returns Number 379 | */ 380 | this.hasProgressFinished = function () {} 381 | 382 | /** 383 | * 384 | * @returns Number 385 | */ 386 | this.getFreeKBytes = function () {} 387 | } 388 | 389 | /** 390 | * Represents the ECDeviceInfo object type inserted into the engine on any Wii. 391 | * This high-level object exposes many device identifiers. 392 | * 393 | * @constructor 394 | */ 395 | function ECDeviceInfo() { 396 | /** 397 | * The amount of used blocks in the device. 398 | * @type {number} 399 | */ 400 | this.usedBlocks = 0; 401 | 402 | /** 403 | * The total amount of blocks on the device 404 | * @type {number} 405 | */ 406 | this.totalBlocks = 0; 407 | 408 | /** 409 | * The state of the console's registration. 410 | * To populate, please call ec.checkDeviceStatus(); 411 | * @returns {ECRegistrationStates} 412 | */ 413 | this.registrationStatus = "" 414 | } 415 | 416 | /** 417 | * Represents the progress of an asynchronous operation performed by EC. 418 | * This should not be instantiated by itself. 419 | * 420 | * @constructor 421 | */ 422 | function ECProgress() { 423 | /** 424 | * Status returned by EC internally. 425 | * @returns 426 | */ 427 | this.status = 0 428 | /** 429 | * Operation title. 430 | */ 431 | this.operation = "" 432 | /** 433 | * Operation description. 434 | */ 435 | this.description = "" 436 | /** 437 | * Unknown. 438 | */ 439 | this.phase = 0 440 | /** 441 | * State of cancellation. 442 | */ 443 | this.isCancelRequested = false 444 | /** 445 | * Size currently downloaded. Most useful for a title contents-related operation, 0 otherwise. 446 | */ 447 | this.downloadedSize = 0 448 | /** 449 | * Size of the finished contents. Most useful for a title contents-related operation, 0 otherwise. 450 | */ 451 | this.totalSize = 0 452 | /** 453 | * Error code returned from operation. 454 | */ 455 | this.errCode = 0 456 | /** 457 | * Information about the error. TODO: find how this is set 458 | */ 459 | this.errInfo = "" 460 | } 461 | 462 | /** 463 | * Represents the wiiDlTask object type inserted into the engine on any Wii. 464 | * This high-level object allows management of download operations via the 465 | * WiiConnect24 scheduler. Typically, files such as banners are downloaded. 466 | * 467 | * @constructor 468 | */ 469 | function wiiDlTask() { 470 | /** 471 | * Adds a task to be downloaded. 472 | * 473 | * @param {string} url The URL to register for download. 474 | * @param {number} interval The interval of minutes to download this file over. 475 | */ 476 | this.addDownloadTask = function(url, interval) {} 477 | 478 | /** 479 | * Deletes one registered download task. 480 | */ 481 | this.deleteDownloadTask = function() {} 482 | 483 | /** 484 | * Returns whether the amount of registered tasks is 0. 485 | * 486 | * @returns {boolean} 487 | */ 488 | this.hasDeletedDLTask = function() {} 489 | } --------------------------------------------------------------------------------