├── .gitignore ├── HOWTO-SETUP.rst ├── README.rst ├── bin ├── build-html.php ├── clean-generated-and-qr.sh ├── clean-generated-keep-qr.sh ├── create-qr.sh ├── filters.php ├── functions.php └── import-game-data.php ├── config.php.dist ├── data └── templates │ ├── allgames.tpl.php │ ├── discover.tpl.php │ └── game.tpl.php ├── src └── push-to-my-ouya-helpers.php └── www ├── .htaccess ├── agreements └── marketplace.html ├── api ├── firmware_builds ├── razer │ ├── session │ └── session.php └── v1 │ ├── console_configuration │ ├── crash_report │ ├── details-dummy │ ├── .htaccess │ └── error-unknown-app-details.json │ ├── events │ ├── gamers │ ├── me.json │ ├── me.php │ ├── me │ │ ├── agreements │ │ ├── apps │ │ │ └── developed │ │ ├── consoles │ │ └── user_messages │ └── register-error.json │ ├── games │ ├── purchase.php │ └── purchases-empty.json │ ├── partner_builds.json │ ├── partner_builds.razer-forge-tv.json │ ├── partner_builds │ └── release_notes │ ├── premium_purchases │ ├── queued_downloads │ ├── queued_downloads.php │ ├── queued_downloads_delete.php │ ├── ratings │ ├── recommendations │ ├── sessions │ ├── sessions.php │ ├── status │ ├── themes │ └── wallet ├── bg.jpg ├── bg_details.jpg ├── datatables ├── datatables.css ├── datatables.js ├── datatables.min.css ├── datatables.min.js ├── jquery-3.5.1.min.js ├── jquery.dataTables.yadcf.css └── jquery.dataTables.yadcf.js ├── empty-json.php ├── favicon.ico ├── ouya-allgames.css ├── ouya-discover.css ├── ouya-game.css ├── ouya-logo.grey.svg ├── ouya-logo.svg ├── ouya-square.png ├── ouya-square.svg ├── push-to-my-ouya.php ├── push-to-my-ouya.png └── updates-ouya_1_1.json /.gitignore: -------------------------------------------------------------------------------- 1 | /config.php 2 | /data/push-to-my-ouya.sqlite3 3 | /data/usernames.sqlite3 4 | /README.html 5 | www/api/v1/apps/ 6 | www/api/v1/details-data/ 7 | www/api/v1/developers/ 8 | www/api/v1/discover-data/ 9 | www/api/v1/discover.json 10 | www/api/v1/games/*/ 11 | www/api/v1/search-data/ 12 | www/discover/ 13 | www/game/ 14 | www/gen-qr/ 15 | www/game-data-version 16 | -------------------------------------------------------------------------------- /HOWTO-SETUP.rst: -------------------------------------------------------------------------------- 1 | ====================================== 2 | Building and enabling a local stouyapi 3 | ====================================== 4 | 5 | The whole procedure was done on Linux, using Pop-OS, release Pop!_OS 22.04 LTS. 6 | The configuration will be the same for any Ubuntu/Debian based distro. 7 | 8 | NOTE: Commands with the $ symbol mean they should be run with your default user. 9 | Commands with # means they must be run as root. 10 | 11 | 12 | 1 - Installing the dependencies: 13 | ================================ 14 | 15 | To build the stouyapi API and HTML files on Pop-OS you need to install the 16 | following packages: 17 | 18 | - imagemagick 19 | - exiftool 20 | - qrencode 21 | - ttf-mscorefonts-installer (*) 22 | 23 | (*) Pop-OS complained about missing helvetica font, so this package is needed. 24 | I haven't tested it with another Helvetica compatible font package. This package 25 | just installs the actual font installer. A screen will appear asking you to 26 | accept the fonts EULA and continue with the installation. 27 | 28 | To run the server, you need to install the following packages: 29 | 30 | - apache 31 | - php (*) 32 | - php-sqlite 33 | 34 | (*) When installed, it already activates the necessary module in apache. 35 | 36 | To install the packages on Pop-OS, just use the following command:: 37 | 38 | # apt install imagemagick exiftool qrencode ttf-mscorefonts-installer apache2 php php-sqlite3 39 | 40 | **ATTENTION: The above listing is not definitive and may vary if you use another 41 | distro such as Fedora, CentOS, etc. Make sure you have the package installed on 42 | your distribution.** 43 | 44 | 45 | 2 - Building the API and HTML files: 46 | ==================================== 47 | 48 | First download the stouyapi code and files to your computer. 49 | 50 | In a terminal, type:: 51 | 52 | $ git clone https://github.com/cweiske/stouyapi.git 53 | 54 | This will create the stouyapi directory. 55 | 56 | Now enter in the stouyapi directory and download the ouya-game-data code and files:: 57 | 58 | $ cd stouyapi 59 | $ git clone https://github.com/ouya-saviors/ouya-game-data.git 60 | 61 | **ATTENTION: If you want to add a game/program to your local store, now is the time: 62 | Just include the json file of the game/program in the "ouya-game-data/new" folder, 63 | before importing the data.** 64 | 65 | Now, before creating the API files and HTML files, you must rename and, if you wish, 66 | edit the config.php.dist file. 67 | 68 | This file: 69 | 70 | - Changes all links pointing to the archive.org site to point to static.ouya.world; 71 | - Configures the list of indicated games that appears on the OUYA home screen (where we have the options PLAY, DISCOVER, etc.); 72 | - Configures a list of suggested games, which appears within DISCOVER, below the list of new games. 73 | 74 | Rename the config.php.dist file to config.php:: 75 | 76 | $ cp config.php.dist config.php 77 | 78 | If you want to edit it, open it with nano or any text editor of your choice:: 79 | 80 | $ nano config.php 81 | 82 | You should only change the following two sections: 83 | 84 | The first section is personal game recommendations within DISCOVER, below the new games listing:: 85 | 86 | $GLOBALS['packagelists']["cweiske's picks"] = [ 87 | 'de.eiswuxe.blookid2', 88 | 'com.cosmos.babylonantwins', 89 | 'com.inverseblue.skyriders', 90 | ]; 91 | 92 | If you want: 93 | 94 | - Change the title, cweiske's picks, keeping the double quotes, 95 | - Change/include a game by informing the name of the game's json file between single quotes and a comma at the end, following the same formatting as above. If you want to Delete a game, just delete the line. 96 | 97 | The session below are indications of games that appear on the OUYA home screen:: 98 | 99 | $GLOBALS['home']['2020 Winter GameJam'] = [ 100 | 'com.DESKINK.ToneTests', 101 | 'com.Eightbbgames.yahayor', 102 | 'com.FuzzyPopcorn', 103 | 'com.NYYLE.NYCTO', 104 | 'com.NoelRojasOliveras.PaintKiller', 105 | 'com.StrawHat.Fall', 106 | 'com.oliverstogden.trf', 107 | 'com.samuelsousa.shootdestroy', 108 | 'com.scorpion.shootout', 109 | 'com.sd_game_dev.aliens_taste_my_sword', 110 | 'com.sumotown.sirtet', 111 | 'de.x2041.games.gyrogun', 112 | 'ht.sr.git.arcticGrind.embed', 113 | 'tv.ouya.demo.DarkSpacePioneer', 114 | 'tv.ouya.win.unity.brokenbeauty', 115 | ]; 116 | 117 | Edit in the same way, but note that on the home screen the title of the recommendations, 118 | 2020 Winter GameJam, is enclosed in single quotes. 119 | 120 | Do not change any other field in the file and after making changes, save it. 121 | 122 | Now generate the API files:: 123 | 124 | $ ./bin/import-game-data.php ouya-game-data/folders 125 | 126 | Creating the files takes a while. Wait to finish. 127 | 128 | When finished, create the HTML files:: 129 | 130 | $ ./bin/build-html.php 131 | 132 | 133 | 3 - Setting up the site 134 | ======================== 135 | 136 | So far, apache is already running. If you type in the browser http://localhost the default 137 | apache website will appear. Now let's create the settings for the STOUYAPI. 138 | 139 | In the terminal, type:: 140 | 141 | $ cd /etc/apache2/sites-available/ 142 | 143 | Now, copy the apache default site file and rename it however you want but keep the ".conf" 144 | extension. I left it with the name of stouyapi:: 145 | 146 | # cp 000-default.conf stouyapi.conf 147 | 148 | The file we copied is a file with minimal apache default settings for virtual hosts. 149 | 150 | Now let's edit it with nano:: 151 | 152 | # nano stouyapi.conf 153 | 154 | Now, look for the line that looks like below:: 155 | 156 | #ServerName www.example.com 157 | 158 | It tells apache the address of the site. Uncomment it (remove the #) and change the address 159 | to whatever you like. Here I left it like this:: 160 | 161 | ServerName stouyapi.local 162 | 163 | Now find a line that looks like below:: 164 | 165 | DocumentRoot /var/www/html 166 | 167 | That line basically tells apache where the site's files are. 168 | I chose to leave my files in the following path:: 169 | 170 | DocumentRoot /srv/stouyapi/www 171 | 172 | **ATTENTION: You can use any directory name you want, but 173 | remember that the path you enter must be complete until the 174 | folder that contains the files and folders on the server. 175 | They are all those that are inside the www directory, inside 176 | the stouyapi folder where we generate the API files and HTML files.** 177 | 178 | Now let's go to the end of the file, and before the line below:: 179 | 180 | 181 | 182 | Include the following lines:: 183 | 184 | Script PUT /empty-json.php 185 | Script DELETE /api/v1/queued_downloads_delete.php 186 | 187 | 188 | AllowOverride All 189 | Require all granted 190 | 191 | 192 | **ATTENTION: Pay attention that the path in "DocumentRoot" and "" should be the same.** 193 | 194 | In the end, disregarding all the comment lines that the file has, it will look like this:: 195 | 196 | 197 | 198 | ServerName stouyapi.local 199 | 200 | ServerAdmin webmaster@localhost 201 | DocumentRoot /srv/stouyapi/www 202 | 203 | ErrorLog ${APACHE_LOG_DIR}/error.log 204 | CustomLog ${APACHE_LOG_DIR}/access.log combined 205 | 206 | Script PUT /empty-json.php 207 | Script DELETE /api/v1/queued_downloads_delete.php 208 | 209 | 210 | AllowOverride All 211 | Require all granted 212 | 213 | 214 | 215 | 216 | Save the file and close. 217 | 218 | Now let's move the site files to the location indicated in the configuration file. 219 | 220 | Do:: 221 | 222 | # mkdir /srv/stouyapi 223 | 224 | Then go inside the stouyapi folder where we created the API and HTML files and do:: 225 | 226 | # cp -R www /srv/stouyapi 227 | 228 | This will copy the www folder to /srv/stouyapi. 229 | 230 | You can check with the following command:: 231 | 232 | $ ls /srv/stouyapi 233 | 234 | Which will return the www folder. 235 | 236 | 237 | 4 - Activating the apache modules and the website. 238 | ================================================== 239 | 240 | With the configuration file created and the site files in place, let's activate the modules and the site. 241 | 242 | First the modules, enter the following command:: 243 | 244 | # a2enmod actions expires php8.1 rewrite 245 | 246 | This will activate the necessary modules. Don't worry if any of them are already active 247 | (php8.1 will be), as apache just tells you that it's already configured. 248 | 249 | It will ask to restart apache, showing the command to run which is:: 250 | 251 | # systemctl restart apache2 252 | 253 | Finally, to activate the site, type:: 254 | 255 | # a2ensite stouyapi 256 | 257 | If you used another name for the site configuration file, change the name in the above command. 258 | If you just type a2ensite and press enter it will show you all the sites available to activate 259 | and you just type the name of the site and press enter. 260 | 261 | Finally, it will ask to reload apache, which we will do with the command:: 262 | 263 | # systemctl reload apache2 264 | 265 | With that we finish the settings and the site is already running. 266 | 267 | To check if everything is ok, in the terminal:: 268 | 269 | ##To check if normal API routes work, type: 270 | $ curl -I http://stouyapi.cwboo/api/firmware_builds 271 | 272 | ##To check if rewritten API routes work, type: 273 | $ curl -I http://stouyapi.cwboo/api/v1/discover/discover 274 | 275 | ##To check if PHP routes work, type: 276 | $ curl -I http://stouyapi.cwboo/api/v1/gamers/me 277 | 278 | All curl commands above should return ``HTTP/1.1 200 OK`` with some other information. 279 | 280 | 281 | 5 - Configuring the files in the OUYA 282 | ===================================== 283 | 284 | We must access the OUYA through adb, either in the case of an installation after a factory reset 285 | or to use the local stouyapi, and edit the hosts file located in /etc (/etc/hosts) and include a 286 | line with the format below:: 287 | 288 | IP-APACHE-SERVER STOUYAPI-SITE-NAME 289 | 290 | It will look like this:: 291 | 292 | 127.0.0.1 localhost 293 | 192.168.0.5 stouyapi.local 294 | 295 | ATTENTION: The hosts file already has a line that refers to localhost and it should not be deleted. 296 | Also, you must leave a blank line after your stouyapi address. 297 | 298 | And the ouya_config.properties file, which is in /sdcard, will look like this:: 299 | 300 | OUYA_SERVER_URL=http://stouyapi.local 301 | OUYA_STATUS_SERVER_URL=http://stouyapi.local/api/v1/status 302 | 303 | ATTENTION: the site to be used, which in the above case is stouyapi.local, is the one that we inform 304 | in the apache configuration file, in the line that starts with "ServerName". 305 | 306 | With this, the OUYA will use the local stouyapi immediately. 307 | If it do not, reboot the OUYA once. 308 | 309 | 310 | 6 - OUYA setup 311 | ============== 312 | 313 | 1. User registration: "Existing account" 314 | 2. Enter any username, leave password empty. Continue. 315 | 3. Skip credit card registration 316 | 317 | The username will appear on your ouya main screen. 318 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ************************** 2 | stouyapi - Static OUYA API 3 | ************************** 4 | 5 | A static API for the OUYA gaming console that still lets you sign in 6 | and install games, despite the OUYA server shutdown in 2019. 7 | 8 | 9 | ===== 10 | Setup 11 | ===== 12 | 13 | OUYA config change 14 | ================== 15 | - Mount via USB (Micro USB cable) 16 | - Create file ``ouya_config.properties`` 17 | - Add:: 18 | 19 | OUYA_SERVER_URL=http://stouyapi.example.org 20 | OUYA_STATUS_SERVER_URL=http://stouyapi.example.org/api/v1/status 21 | 22 | The changes should take effect immediately. 23 | If they do not, reboot the OUYA once. 24 | 25 | 26 | OUYA setup 27 | ========== 28 | 29 | 1. User registration: "Existing account" 30 | 2. Enter any username, leave password empty. Continue. 31 | 3. Skip credit card registration 32 | 33 | The username will appear on your ouya main screen. 34 | 35 | 36 | Apache setup 37 | ============ 38 | 39 | .. note:: Step-by-step setup instructions can be found in 40 | `HOWTO-SETUP.rst `__. 41 | 42 | 43 | Virtual host configuration:: 44 | 45 | 46 | ServerName stouyapi.example.org 47 | DocumentRoot /path/to/stouyapi/www 48 | 49 | CustomLog /var/log/apache2/stouyapi-access.log combined 50 | ErrorLog /var/log/apache2/stouyapi-error.log 51 | 52 | Script PUT /empty-json.php 53 | Script DELETE /api/v1/queued_downloads_delete.php 54 | 55 | 56 | AllowOverride All 57 | Require all granted 58 | 59 | 60 | 61 | The following modules need to be enabled in Apache 2.4 62 | (with e.g. ``a2enmod``): 63 | 64 | - ``actions`` 65 | - ``expires`` 66 | - ``php`` (or php-fpm via fastcgi) 67 | - ``rewrite`` 68 | 69 | The virtual host's document root needs to point to the ``www`` folder. 70 | 71 | 72 | Test your Apache setup 73 | ---------------------- 74 | :: 75 | 76 | # check if normal API routes work 77 | $ curl -I http://stouyapi.example.org/api/firmware_builds 78 | HTTP/1.1 200 OK 79 | [...] 80 | 81 | # check if rewritten API routes work 82 | $ curl -I http://stouyapi.example.org/api/v1/discover/discover 83 | HTTP/1.1 200 OK 84 | [...] 85 | 86 | # check if PHP routes work 87 | curl -I http://stouyapi.example.org/api/v1/gamers/me 88 | HTTP/1.1 200 OK 89 | [...] 90 | 91 | 92 | Building API data 93 | ================= 94 | Download the OUYA game data repository from 95 | https://github.com/ouya-saviors/ouya-game-data 96 | and then generate the API files with it:: 97 | 98 | $ git clone https://github.com/ouya-saviors/ouya-game-data.git 99 | $ ./bin/import-game-data.php ouya-game-data/folders 100 | 101 | 102 | Building the web discover store 103 | =============================== 104 | After building the API files, generate the HTML:: 105 | 106 | $ ./bin/build-html.php 107 | 108 | 109 | =============== 110 | Push to my OUYA 111 | =============== 112 | stouyapi's HTML game detail page have a "Push to my OUYA" button that 113 | allows anyone to tell his own OUYA to install that game. 114 | It works without any user accounts, and is only based on IP addresses. 115 | 116 | If your PC that you click the Push button on and your OUYA have the same 117 | public IP address (IPv4 NAT), or the same IPv6 64bit prefix, then 118 | the OUYA will install the game within 5 minutes. 119 | 120 | It will also work if you run stouyapi inside your local network, because 121 | all private IP addresses are mapped to a special "local" address. 122 | 123 | You can inspect your own download queue by simply opening 124 | ``/api/v1/queued_downloads`` in your browser. 125 | 126 | 127 | ======== 128 | See also 129 | ======== 130 | 131 | - https://gitlab.com/devirich/BrewyaOnOuya - alternative storefront 132 | - https://archive.org/details/ouyalibrary - Archived OUYA games 133 | - https://github.com/ouya-saviors/ouya-game-data/ - OUYA game data repository 134 | 135 | 136 | =========== 137 | Discoveries 138 | =========== 139 | 140 | - data/data/tv.ouya/cache/ion/ 141 | 142 | - image cache for main menu image 143 | 144 | - Don't put a trailing slash into ``OUYA_SERVER_URL`` - it will make double slashes 145 | -------------------------------------------------------------------------------- /bin/build-html.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 7 | */ 8 | require_once __DIR__ . '/functions.php'; 9 | 10 | //default configuration values 11 | $GLOBALS['pushToMyOuyaUrl'] = '../push-to-my-ouya.php'; 12 | $cfgFile = __DIR__ . '/../config.php'; 13 | if (file_exists($cfgFile)) { 14 | include $cfgFile; 15 | } 16 | 17 | $wwwDir = __DIR__ . '/../www/'; 18 | $discoverDir = __DIR__ . '/../www/api/v1/discover-data/'; 19 | $wwwDiscoverDir = $wwwDir . 'discover/'; 20 | $gameDetailsDir = __DIR__ . '/../www/api/v1/details-data/'; 21 | $wwwGameDir = $wwwDir . 'game/'; 22 | 23 | if (!is_dir($wwwDiscoverDir)) { 24 | mkdir($wwwDiscoverDir, 0755); 25 | } 26 | if (!is_dir($wwwGameDir)) { 27 | mkdir($wwwGameDir, 0755); 28 | } 29 | 30 | foreach (glob($gameDetailsDir . '*.json') as $gameDataFile) { 31 | $htmlFile = basename($gameDataFile, '.json') . '.htm'; 32 | file_put_contents( 33 | $wwwGameDir . $htmlFile, 34 | renderGameFile($gameDataFile, 'game/' . $htmlFile) 35 | ); 36 | } 37 | 38 | foreach (glob($discoverDir . '*.json') as $discoverFile) { 39 | $htmlFile = basename($discoverFile, '.json') . '.htm'; 40 | if ($htmlFile == 'discover.htm') { 41 | $htmlFile = 'index.htm'; 42 | } 43 | file_put_contents( 44 | $wwwDiscoverDir . $htmlFile, 45 | renderDiscoverFile($discoverFile, 'discover/' . $htmlFile) 46 | ); 47 | } 48 | 49 | file_put_contents( 50 | $wwwDiscoverDir . 'allgames.htm', 51 | renderAllGamesList(glob($gameDetailsDir . '*.json'), 'discover/allgames.htm') 52 | ); 53 | 54 | 55 | function renderAllGamesList($detailsFiles, $path) 56 | { 57 | $games = []; 58 | foreach ($detailsFiles as $gameDataFile) { 59 | $json = json_decode(file_get_contents($gameDataFile)); 60 | $games[] = (object) [ 61 | 'packageName' => basename($gameDataFile, '.json'), 62 | 'title' => $json->title, 63 | 'genres' => $json->genres, 64 | 'developer' => $json->developer->name, 65 | 'developerUrl' => $json->stouyapi->{'developer-url'} ?? null, 66 | 'suggestedAge' => $json->suggestedAge, 67 | 'apkVersion' => $json->version->number, 68 | 'apkTimestamp' => $json->version->publishedAt, 69 | 'players' => $json->gamerNumbers, 70 | 'detailUrl' => '../game/' . str_replace( 71 | 'ouya://launcher/details?app=', 72 | '', 73 | basename($gameDataFile, '.json') 74 | ) . '.htm', 75 | ]; 76 | } 77 | $navLinks = [ 78 | './' => 'back', 79 | ]; 80 | $canonicalUrl = $GLOBALS['baseUrl'] . $path; 81 | 82 | $allGamesTemplate = __DIR__ . '/../data/templates/allgames.tpl.php'; 83 | ob_start(); 84 | include $allGamesTemplate; 85 | $html = ob_get_contents(); 86 | ob_end_clean(); 87 | 88 | return $html; 89 | } 90 | 91 | function renderDiscoverFile($discoverFile, $path) 92 | { 93 | $json = json_decode(file_get_contents($discoverFile)); 94 | 95 | $title = $json->title . ' OUYA games'; 96 | $subtitle = $json->stouyapi->subtitle ?? null; 97 | 98 | $sections = []; 99 | foreach ($json->rows as $row) { 100 | $section = (object) [ 101 | 'title' => $row->title, 102 | 'tiles' => [], 103 | ]; 104 | foreach ($row->tiles as $tileId) { 105 | $tileData = $json->tiles[$tileId]; 106 | if ($tileData->type == 'app') { 107 | $section->tiles[] = (object) [ 108 | 'type' => $tileData->type,//app 109 | 'thumb' => $tileData->image, 110 | 'title' => $tileData->title, 111 | 'rating' => $tileData->rating->average, 112 | 'ratingCount' => $tileData->rating->count, 113 | 'detailUrl' => '../game/' . str_replace( 114 | 'ouya://launcher/details?app=', 115 | '', 116 | $tileData->url 117 | ) . '.htm', 118 | ]; 119 | } else { 120 | $section->tiles[] = (object) [ 121 | 'type' => $tileData->type,//discover 122 | 'thumb' => $tileData->image, 123 | 'title' => $tileData->title, 124 | 'detailUrl' => str_replace( 125 | 'ouya://launcher/discover/', 126 | '', 127 | $tileData->url 128 | ) . '.htm', 129 | ]; 130 | } 131 | } 132 | $sections[] = $section; 133 | } 134 | 135 | $navLinks = []; 136 | if ($json->title == 'DISCOVER') { 137 | $navLinks['../'] = 'back'; 138 | $navLinks['allgames.htm'] = 'all games'; 139 | $title = 'OUYA games list'; 140 | } else { 141 | $navLinks['./'] = 'discover'; 142 | } 143 | 144 | if ($path === 'discover/index.htm') { 145 | $path = 'discover/'; 146 | } 147 | $canonicalUrl = $GLOBALS['baseUrl'] . $path; 148 | 149 | $discoverTemplate = __DIR__ . '/../data/templates/discover.tpl.php'; 150 | ob_start(); 151 | include $discoverTemplate; 152 | $html = ob_get_contents(); 153 | ob_end_clean(); 154 | 155 | return $html; 156 | } 157 | 158 | function renderGameFile($gameDataFile, $path) 159 | { 160 | $json = json_decode(file_get_contents($gameDataFile)); 161 | 162 | $appsDir = dirname($gameDataFile, 2) . '/apps/'; 163 | $appsFile = $appsDir . $json->version->uuid . '.json'; 164 | $appsJson = json_decode(file_get_contents($appsFile)); 165 | 166 | $downloadJson = json_decode( 167 | file_get_contents( 168 | $appsDir . $json->version->uuid . '-download.json' 169 | ) 170 | ); 171 | 172 | $apkDownloadUrl = $downloadJson->app->downloadLink; 173 | /* 174 | if (isset($json->premium) && $json->premium) { 175 | $apkDownloadUrl = null; 176 | } 177 | */ 178 | $developerDetailsUrl = null; 179 | if (isset($json->developer->url) && $json->developer->url) { 180 | $developerDetailsUrl = '../discover/' . str_replace( 181 | 'ouya://launcher/discover/', 182 | '', 183 | $json->developer->url 184 | ) . '.htm'; 185 | } 186 | 187 | $internetArchiveUrl = $json->stouyapi->{'internet-archive'} ?? null; 188 | $developerUrl = $json->stouyapi->{'developer-url'} ?? null; 189 | $blockedInWebText = $json->stouyapi->blockedInWebText ?? null; 190 | $blockedInWeb = ($json->stouyapi->blockedInWebText ?? null) !== null; 191 | $canonicalUrl = $GLOBALS['baseUrl'] . $path; 192 | 193 | $pushUrl = $GLOBALS['pushToMyOuyaUrl'] 194 | . '?game=' . urlencode($json->apk->package); 195 | 196 | $navLinks = []; 197 | foreach ($json->genres as $genreTitle) { 198 | $url = '../discover/' . categoryPath($genreTitle) . '.htm'; 199 | $navLinks[$url] = $genreTitle; 200 | } 201 | 202 | $gameTemplate = __DIR__ . '/../data/templates/game.tpl.php'; 203 | ob_start(); 204 | include $gameTemplate; 205 | $html = ob_get_contents(); 206 | ob_end_clean(); 207 | 208 | return $html; 209 | } 210 | ?> 211 | -------------------------------------------------------------------------------- /bin/clean-generated-and-qr.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # remove all autogenerated files, including qr 3 | set -e 4 | ./bin/clean-generated-keep-qr.sh 5 | test -d www/gen-qr/ && rm -f -r www/gen-qr/ 6 | -------------------------------------------------------------------------------- /bin/clean-generated-keep-qr.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # remove all autogenerated files except qr codes 3 | # QR codes are expensive to generate 4 | set -e 5 | rm -f www/api/v1/apps/* 6 | rm -f www/api/v1/details-data/* 7 | rm -f -r www/api/v1/developers/* 8 | rm -f www/api/v1/discover-data/* 9 | rm -f www/api/v1/games/*/* 10 | rmdir www/api/v1/games/*/ || true 11 | rm -f www/api/v1/search-data/* 12 | rm -f www/discover/* 13 | rm -f www/game/* 14 | rm -f www/game-data-version 15 | -------------------------------------------------------------------------------- /bin/create-qr.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Create an image that contains a QR code for the given website 3 | # together with the URL as readable text 4 | set -e 5 | 6 | if [ "$#" -lt 2 ]; then 7 | echo "Usage: create-qr.sh https://example.org outfile.png" > /dev/stderr 8 | exit 1 9 | fi 10 | 11 | if ! command -v convert > /dev/null; then 12 | echo "convert (imagemagick) is not installed" > /dev/stderr 13 | exit 2 14 | fi 15 | 16 | if ! command -v exiftool > /dev/null; then 17 | echo "exiftool is not installed" > /dev/stderr 18 | exit 2 19 | fi 20 | 21 | if ! command -v qrencode > /dev/null; then 22 | echo "qrencode is not installed" > /dev/stderr 23 | exit 2 24 | fi 25 | 26 | url="$1" 27 | filename="$2" 28 | 29 | qrencode -s 20 -o tmp-qr.png "$url" 30 | 31 | convert\ 32 | -filter point -resize 1260x580\ 33 | -background '#1a1a1a'\ 34 | tmp-qr.png\ 35 | -size 1260x120\ 36 | -fill '#dad9d9'\ 37 | -gravity south\ 38 | label:"$url"\ 39 | -append\ 40 | -bordercolor '#1a1a1a'\ 41 | -border 10\ 42 | "$filename" 43 | 44 | rm tmp-qr.png 45 | 46 | exiftool\ 47 | -quiet\ 48 | -ignoreMinorErrors\ 49 | -overwrite_original\ 50 | -PNG:Software=stouyapi\ 51 | -PNG:Title="$url"\ 52 | "$filename" 53 | -------------------------------------------------------------------------------- /bin/filters.php: -------------------------------------------------------------------------------- 1 | contentRating) { 7 | $filtered[] = $game; 8 | } 9 | } 10 | return $filtered; 11 | } 12 | 13 | function filterByGenre($origGames, $genre, $remove = false) 14 | { 15 | $filtered = []; 16 | foreach ($origGames as $game) { 17 | if ($remove) { 18 | if (array_search($genre, $game->genres) === false) { 19 | $filtered[] = $game; 20 | } 21 | } else { 22 | if (array_search($genre, $game->genres) !== false) { 23 | $filtered[] = $game; 24 | } 25 | } 26 | } 27 | return $filtered; 28 | } 29 | 30 | function filterByLetter($origGames, $letter) 31 | { 32 | $filtered = []; 33 | foreach ($origGames as $game) { 34 | $gameLetter = strtoupper($game->title[0]); 35 | if (!preg_match('#^[A-Z]$#', $gameLetter)) { 36 | $gameLetter = 'Other'; 37 | } 38 | if ($letter == $gameLetter) { 39 | $filtered[] = $game; 40 | } 41 | } 42 | return $filtered; 43 | } 44 | 45 | function filterByPackageNames($origGames, $packageNames) 46 | { 47 | $names = array_flip($packageNames); 48 | $filtered = []; 49 | foreach ($origGames as $game) { 50 | if (isset($names[$game->packageName])) { 51 | $filtered[$names[$game->packageName]] = $game; 52 | } 53 | } 54 | //keep original order 55 | ksort($filtered, SORT_NUMERIC); 56 | return $filtered; 57 | } 58 | 59 | function filterByPlayers($origGames, $numOfPlayers) 60 | { 61 | $filtered = []; 62 | foreach ($origGames as $game) { 63 | if (array_search($numOfPlayers, $game->players) !== false) { 64 | $filtered[] = $game; 65 | } 66 | } 67 | return $filtered; 68 | } 69 | 70 | function filterBySearchWord($origGames, $searchWord) 71 | { 72 | $filtered = []; 73 | foreach ($origGames as $game) { 74 | if (stripos($game->title, $searchWord) !== false) { 75 | $filtered[] = $game; 76 | } 77 | } 78 | return $filtered; 79 | } 80 | 81 | function filterLastAdded($origGames, $limit) 82 | { 83 | $games = array_values($origGames); 84 | usort( 85 | $games, 86 | function ($gameA, $gameB) { 87 | return strtotime($gameB->firstRelease->date) - strtotime($gameA->firstRelease->date); 88 | } 89 | ); 90 | 91 | return array_slice($games, 0, $limit); 92 | } 93 | 94 | function filterLastUpdated($origGames, $limit) 95 | { 96 | $games = array_values($origGames); 97 | usort( 98 | $games, 99 | function ($gameA, $gameB) { 100 | return strtotime($gameB->latestRelease->date) - strtotime($gameA->latestRelease->date); 101 | } 102 | ); 103 | 104 | return array_slice($games, 0, $limit); 105 | } 106 | 107 | function filterBestRated($origGames, $limit) 108 | { 109 | $games = array_values($origGames); 110 | usort( 111 | $games, 112 | function ($gameA, $gameB) { 113 | return ($gameB->rating->rank - $gameA->rating->rank) * 100; 114 | } 115 | ); 116 | 117 | return array_slice($games, 0, $limit); 118 | } 119 | 120 | function filterBestRatedGames($origGames, $limit) 121 | { 122 | $noApps = filterByGenre($origGames, 'App', true); 123 | $noAppsNoEmus = filterByGenre($noApps, 'Emulator', true); 124 | 125 | return filterBestRated($noAppsNoEmus, $limit); 126 | } 127 | 128 | function filterMostDownloaded($origGames, $limit) 129 | { 130 | $games = array_values($origGames); 131 | usort( 132 | $games, 133 | function ($gameA, $gameB) { 134 | return $gameB->rating->count - $gameA->rating->count; 135 | } 136 | ); 137 | 138 | return array_slice($games, 0, $limit); 139 | } 140 | 141 | function filterRandom($origGames, $limit) 142 | { 143 | $randKeys = array_rand($origGames, min(count($origGames), $limit)); 144 | $games = []; 145 | foreach ($randKeys as $key) { 146 | $games[] = $origGames[$key]; 147 | } 148 | return $games; 149 | } 150 | 151 | function sortByTitle($games) 152 | { 153 | usort( 154 | $games, 155 | function ($gameA, $gameB) { 156 | return strcasecmp($gameA->title, $gameB->title); 157 | } 158 | ); 159 | return $games; 160 | } 161 | ?> 162 | -------------------------------------------------------------------------------- /bin/functions.php: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /bin/import-game-data.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 8 | */ 9 | ini_set('xdebug.halt_level', E_WARNING|E_NOTICE|E_USER_WARNING|E_USER_NOTICE); 10 | require_once __DIR__ . '/functions.php'; 11 | require_once __DIR__ . '/filters.php'; 12 | 13 | //command line option parsing 14 | $optind = null; 15 | $opts = getopt('h', ['help', 'mini', 'noqr'], $optind); 16 | $args = array_slice($argv, $optind); 17 | 18 | if (isset($opts['help']) || isset($opts['h'])) { 19 | echo "Import games from a OUYA game data repository\n"; 20 | echo "\n"; 21 | echo "Usage: import-game-data.php [--mini] [--noqr] [--help|-h]\n"; 22 | echo " --mini Generate small but ugly JSON files\n"; 23 | echo " --noqr Do not generate and link QR code images\n"; 24 | exit(0); 25 | } 26 | 27 | if (!isset($args[0])) { 28 | error('Pass the path to a "folders" file with game data json files folder names'); 29 | } 30 | $foldersFile = $args[0]; 31 | if (!is_file($foldersFile)) { 32 | error('Given path is not a file: ' . $foldersFile); 33 | } 34 | 35 | $cfgMini = isset($opts['mini']); 36 | $cfgEnableQr = !isset($opts['noqr']); 37 | 38 | 39 | //default configuration values 40 | $GLOBALS['baseUrl'] = 'http://ouya.cweiske.de/'; 41 | $GLOBALS['categorySubtitles'] = []; 42 | $GLOBALS['packagelists'] = []; 43 | $GLOBALS['urlRewrites'] = []; 44 | $cfgFile = __DIR__ . '/../config.php'; 45 | if (file_exists($cfgFile)) { 46 | include $cfgFile; 47 | } 48 | 49 | $wwwDir = __DIR__ . '/../www/'; 50 | 51 | if ($cfgEnableQr) { 52 | $qrDir = $wwwDir . 'gen-qr/'; 53 | if (!is_dir($qrDir)) { 54 | mkdir($qrDir, 0775); 55 | } 56 | } 57 | 58 | $baseDir = dirname($foldersFile); 59 | $gameFiles = []; 60 | foreach (file($foldersFile) as $line) { 61 | $line = trim($line); 62 | if (strlen($line)) { 63 | if (strpos($line, '..') !== false) { 64 | error('Path attack in ' . $folder); 65 | } 66 | $folder = $baseDir . '/' . $line; 67 | if (!is_dir($folder)) { 68 | error('Folder does not exist: ' . $folder); 69 | } 70 | $gameFiles = array_merge($gameFiles, glob($folder . '/*.json')); 71 | } 72 | } 73 | 74 | //store git repository version of last folder 75 | $workdir = getcwd(); 76 | chdir($folder); 77 | $gitDate = `git log --max-count=1 --format="%h %cI"`; 78 | chdir($workdir); 79 | file_put_contents($wwwDir . '/game-data-version', $gitDate); 80 | 81 | $games = []; 82 | $count = 0; 83 | $developers = []; 84 | 85 | //load game data. doing early to collect a developer's games 86 | foreach ($gameFiles as $gameFile) { 87 | $game = json_decode(file_get_contents($gameFile)); 88 | if ($game === null) { 89 | error('JSON invalid at ' . $gameFile); 90 | } 91 | addMissingGameProperties($game); 92 | $games[$game->packageName] = $game; 93 | 94 | if (!isset($developers[$game->developer->uuid])) { 95 | $developers[$game->developer->uuid] = [ 96 | 'info' => $game->developer, 97 | 'products' => [], 98 | 'gameNames' => [], 99 | ]; 100 | } 101 | $developers[$game->developer->uuid]['gameNames'][] = $game->packageName; 102 | } 103 | 104 | //write json api files 105 | foreach ($games as $game) { 106 | $products = $game->products ?? []; 107 | foreach ($products as $product) { 108 | writeJson( 109 | 'api/v1/developers/' . $game->developer->uuid 110 | . '/products/' . $product->identifier . '.json', 111 | buildDeveloperProductOnly($product, $game->developer) 112 | ); 113 | $developers[$game->developer->uuid]['products'][] = $product; 114 | } 115 | 116 | writeJson( 117 | 'api/v1/details-data/' . $game->packageName . '.json', 118 | buildDetails( 119 | $game, 120 | count($developers[$game->developer->uuid]['gameNames']) > 1 121 | ) 122 | ); 123 | 124 | writeJson( 125 | 'api/v1/games/' . $game->packageName . '/purchases', 126 | buildPurchases($game) 127 | ); 128 | 129 | writeJson( 130 | 'api/v1/apps/' . $game->packageName . '.json', 131 | buildApps($game) 132 | ); 133 | $latestRelease = $game->latestRelease; 134 | writeJson( 135 | 'api/v1/apps/' . $latestRelease->uuid . '.json', 136 | buildApps($game) 137 | ); 138 | 139 | writeJson( 140 | 'api/v1/apps/' . $latestRelease->uuid . '-download.json', 141 | buildAppDownload($game, $latestRelease) 142 | ); 143 | 144 | if ($count++ > 20) { 145 | //break; 146 | } 147 | } 148 | 149 | calculateRank($games); 150 | 151 | foreach ($developers as $developer) { 152 | writeJson( 153 | //index.htm does not need a rewrite rule 154 | 'api/v1/developers/' . $developer['info']->uuid 155 | . '/products/index.htm', 156 | buildDeveloperProducts($developer['products'], $developer['info']) 157 | ); 158 | writeJson( 159 | 'api/v1/developers/' . $developer['info']->uuid 160 | . '/current_gamer', 161 | buildDeveloperCurrentGamer() 162 | ); 163 | 164 | if (count($developer['gameNames']) > 1) { 165 | writeJson( 166 | 'api/v1/discover-data/dev--' . $developer['info']->uuid . '.json', 167 | buildSpecialCategory( 168 | 'Developer: ' . $developer['info']->name, 169 | filterByPackageNames($games, $developer['gameNames']) 170 | ) 171 | ); 172 | } 173 | } 174 | 175 | $data = buildDiscover($games); 176 | writeJson('api/v1/discover-data/discover.json', $data); 177 | writeJson('api/v1/discover-data/discover.forge.json', convertCategoryToForge($data)); 178 | writeJson('api/v1/discover-data/home.json', buildDiscoverHome($games)); 179 | 180 | //make 181 | writeJson( 182 | 'api/v1/discover-data/tutorials.json', 183 | buildMakeCategory('Tutorials', filterByGenre($games, 'Tutorials')) 184 | ); 185 | 186 | $searchLetters = 'abcdefghijklmnopqrstuvwxyz0123456789., '; 187 | foreach (str_split($searchLetters) as $letter) { 188 | $letterGames = filterBySearchWord($games, $letter); 189 | writeJson( 190 | 'api/v1/search-data/' . $letter . '.json', 191 | buildSearch($letterGames) 192 | ); 193 | } 194 | 195 | 196 | function buildDiscover(array $games) 197 | { 198 | $games = removeMakeGames($games); 199 | $data = [ 200 | 'title' => 'DISCOVER', 201 | 'rows' => [], 202 | 'tiles' => [], 203 | ]; 204 | 205 | addDiscoverRow( 206 | $data, 'New games', 207 | filterLastAdded($games, 10) 208 | ); 209 | addDiscoverRow( 210 | $data, 'Best rated games', 211 | filterBestRatedGames($games, 10), 212 | true 213 | ); 214 | 215 | foreach ($GLOBALS['packagelists'] as $listTitle => $listPackageNames) { 216 | addDiscoverRow( 217 | $data, $listTitle, 218 | filterByPackageNames($games, $listPackageNames) 219 | ); 220 | } 221 | 222 | addDiscoverRow( 223 | $data, 'Special', 224 | [ 225 | 'Best rated', 226 | 'Best rated games', 227 | 'Most rated', 228 | 'Random', 229 | 'Last updated', 230 | ] 231 | ); 232 | writeCategoryJson( 233 | 'api/v1/discover-data/' . categoryPath('Best rated') . '.json', 234 | buildSpecialCategory('Best rated', filterBestRated($games, 99)) 235 | ); 236 | writeCategoryJson( 237 | 'api/v1/discover-data/' . categoryPath('Best rated games') . '.json', 238 | buildSpecialCategory('Best rated games', filterBestRatedGames($games, 99)) 239 | ); 240 | writeCategoryJson( 241 | 'api/v1/discover-data/' . categoryPath('Most rated') . '.json', 242 | buildSpecialCategory('Most rated', filterMostDownloaded($games, 99)) 243 | ); 244 | writeCategoryJson( 245 | 'api/v1/discover-data/' . categoryPath('Random') . '.json', 246 | buildSpecialCategory( 247 | 'Random ' . date('Y-m-d H:i'), 248 | filterRandom($games, 99) 249 | ) 250 | ); 251 | writeCategoryJson( 252 | 'api/v1/discover-data/' . categoryPath('Last updated') . '.json', 253 | buildSpecialCategory('Last updated', filterLastUpdated($games, 99)) 254 | ); 255 | 256 | $players = [ 257 | //1 => '1 player', 258 | 2 => '2 players', 259 | 3 => '3 players', 260 | 4 => '4 players', 261 | ]; 262 | addDiscoverRow($data, 'Multiplayer', $players); 263 | foreach ($players as $num => $title) { 264 | writeCategoryJson( 265 | 'api/v1/discover-data/' . categoryPath($title) . '.json', 266 | buildDiscoverCategory( 267 | $title, 268 | //I do not want emulators here, 269 | // and neither Streaming apps 270 | filterByGenre( 271 | filterByGenre( 272 | filterByPlayers($games, $num), 273 | 'Emulator', true 274 | ), 275 | 'App', true 276 | ) 277 | ) 278 | ); 279 | } 280 | 281 | $ages = getAllAges($games); 282 | natsort($ages); 283 | addDiscoverRow($data, 'Content rating', $ages); 284 | foreach ($ages as $num => $title) { 285 | writeCategoryJson( 286 | 'api/v1/discover-data/' . categoryPath($title) . '.json', 287 | buildDiscoverCategory($title, filterByAge($games, $title)) 288 | ); 289 | } 290 | 291 | $genres = removeMakeGenres(getAllGenres($games)); 292 | sort($genres); 293 | addChunkedDiscoverRows($data, $genres, 'Genres'); 294 | 295 | foreach ($genres as $genre) { 296 | writeCategoryJson( 297 | 'api/v1/discover-data/' . categoryPath($genre) . '.json', 298 | buildDiscoverCategory($genre, filterByGenre($games, $genre)) 299 | ); 300 | } 301 | 302 | $abc = array_merge(range('A', 'Z'), ['Other']); 303 | addChunkedDiscoverRows($data, $abc, 'Alphabetical'); 304 | foreach ($abc as $letter) { 305 | writeCategoryJson( 306 | 'api/v1/discover-data/' . categoryPath($letter) . '.json', 307 | buildDiscoverCategory($letter, filterByLetter($games, $letter)) 308 | ); 309 | } 310 | 311 | return $data; 312 | } 313 | 314 | /** 315 | * A genre category page 316 | */ 317 | function buildDiscoverCategory($name, $games) 318 | { 319 | $data = [ 320 | 'title' => $name, 321 | 'rows' => [], 322 | 'tiles' => [], 323 | ]; 324 | if (isset($GLOBALS['categorySubtitles'][$name])) { 325 | $data['stouyapi']['subtitle'] = $GLOBALS['categorySubtitles'][$name]; 326 | } 327 | 328 | if (count($games) >= 20) { 329 | addDiscoverRow( 330 | $data, 'Last updated', 331 | filterLastUpdated($games, 10) 332 | ); 333 | addDiscoverRow( 334 | $data, 'Best rated', 335 | filterBestRated($games, 10), 336 | true 337 | ); 338 | } 339 | 340 | $games = sortByTitle($games); 341 | $chunks = array_chunk($games, 4); 342 | $title = 'All'; 343 | foreach ($chunks as $chunkGames) { 344 | addDiscoverRow($data, $title, $chunkGames); 345 | $title = ''; 346 | } 347 | 348 | return $data; 349 | } 350 | 351 | /** 352 | * Modify a category to make it suitable for the Razer Forge TV 353 | * 354 | * - Fold rows without title into the previous row 355 | * - Remove automatically generated categories ("Last updated", "Best rated") 356 | * 357 | * @see buildDiscoverCategory() 358 | */ 359 | function convertCategoryToForge($data, $removeAutoCategories = false) 360 | { 361 | //merge tiles from rows without title into the previous row 362 | $lastTitleRowId = null; 363 | foreach ($data['rows'] as $rowId => $row) { 364 | if ($row['title'] !== '') { 365 | $lastTitleRowId = $rowId; 366 | } else if ($lastTitleRowId !== null) { 367 | $data['rows'][$lastTitleRowId]['tiles'] = array_merge( 368 | $data['rows'][$lastTitleRowId]['tiles'], 369 | $row['tiles'] 370 | ); 371 | unset($data['rows'][$rowId]); 372 | } 373 | } 374 | 375 | if ($removeAutoCategories) { 376 | foreach ($data['rows'] as $rowId => $row) { 377 | if ($row['title'] === 'Last updated' 378 | || $row['title'] === 'Best rated' 379 | ) { 380 | unset($data['rows'][$rowId]); 381 | } 382 | } 383 | } 384 | 385 | $data['rows'] = array_values($data['rows']); 386 | 387 | return $data; 388 | } 389 | 390 | function buildMakeCategory($name, $games) 391 | { 392 | $data = [ 393 | 'title' => $name, 394 | 'rows' => [], 395 | 'tiles' => [], 396 | ]; 397 | 398 | $games = sortByTitle($games); 399 | addDiscoverRow($data, '', $games); 400 | 401 | return $data; 402 | } 403 | 404 | /** 405 | * Category without the "Last updated" or "Best rated" top rows 406 | * 407 | * Used for "Best rated", "Most rated", "Random" 408 | */ 409 | function buildSpecialCategory($name, $games) 410 | { 411 | $data = [ 412 | 'title' => $name, 413 | 'rows' => [], 414 | 'tiles' => [], 415 | ]; 416 | 417 | $first3 = array_slice($games, 0, 3); 418 | $chunks = array_chunk(array_slice($games, 3), 4); 419 | array_unshift($chunks, $first3); 420 | 421 | foreach ($chunks as $chunkGames) { 422 | addDiscoverRow($data, '', $chunkGames); 423 | } 424 | 425 | return $data; 426 | } 427 | 428 | function buildDiscoverHome(array $games) 429 | { 430 | $data = [ 431 | 'title' => 'home', 432 | 'rows' => [ 433 | ], 434 | 'tiles' => [], 435 | ]; 436 | 437 | if (isset($GLOBALS['home'])) { 438 | reset($GLOBALS['home']); 439 | $title = key($GLOBALS['home']); 440 | addDiscoverRow( 441 | $data, $title, 442 | filterByPackageNames($games, $GLOBALS['home'][$title]) 443 | ); 444 | } else { 445 | $data['rows'][] = [ 446 | 'title' => 'FEATURED', 447 | 'showPrice' => false, 448 | 'ranked' => false, 449 | 'tiles' => [], 450 | ]; 451 | } 452 | 453 | return $data; 454 | } 455 | 456 | /** 457 | * Build api/v1/apps/$packageName 458 | */ 459 | function buildApps($game) 460 | { 461 | $latestRelease = $game->latestRelease; 462 | 463 | $product = null; 464 | $gamePromoted = getPromotedProduct($game); 465 | if ($gamePromoted) { 466 | $product = buildProduct($gamePromoted); 467 | } 468 | 469 | // http://cweiske.de/ouya-store-api-docs.htm#get-https-devs-ouya-tv-api-v1-apps-xxx 470 | return [ 471 | 'app' => [ 472 | 'uuid' => $latestRelease->uuid, 473 | 'title' => $game->title, 474 | 'overview' => $game->overview, 475 | 'description' => $game->description, 476 | 'gamerNumbers' => $game->players, 477 | 'genres' => $game->genres, 478 | 479 | 'website' => $game->website, 480 | 'contentRating' => $game->contentRating, 481 | 'premium' => $game->premium, 482 | 'firstPublishedAt' => $game->firstPublishedAt, 483 | 484 | 'likeCount' => $game->rating->likeCount, 485 | 'ratingAverage' => $game->rating->average, 486 | 'ratingCount' => $game->rating->count, 487 | 488 | 'versionNumber' => $latestRelease->name, 489 | 'latestVersion' => $latestRelease->uuid, 490 | 'md5sum' => $latestRelease->md5sum, 491 | 'apkFileSize' => $latestRelease->size, 492 | 'publishedAt' => $latestRelease->date, 493 | 'publicSize' => $latestRelease->publicSize, 494 | 'nativeSize' => $latestRelease->nativeSize, 495 | 496 | 'mainImageFullUrl' => $game->discover, 497 | 'videoUrl' => getFirstVideoUrl($game->media), 498 | 'filepickerScreenshots' => getAllImageUrls($game->media), 499 | 'mobileAppIcon' => null, 500 | 501 | 'developer' => $game->developer->name, 502 | 'supportEmailAddress' => $game->developer->supportEmail, 503 | 'supportPhone' => $game->developer->supportPhone, 504 | 'founder' => $game->developer->founder, 505 | 506 | 'promotedProduct' => $product, 507 | ], 508 | ]; 509 | } 510 | 511 | function buildAppDownload($game, $release) 512 | { 513 | return [ 514 | 'app' => [ 515 | 'fileSize' => $release->size, 516 | 'version' => $release->uuid, 517 | 'contentRating' => $game->contentRating, 518 | 'downloadLink' => $release->url, 519 | ] 520 | ]; 521 | } 522 | 523 | function buildProduct($product) 524 | { 525 | if ($product === null) { 526 | return null; 527 | } 528 | return [ 529 | 'type' => $product->type ?? 'entitlement', 530 | 'identifier' => $product->identifier, 531 | 'name' => $product->name, 532 | 'description' => $product->description ?? '', 533 | 'localPrice' => $product->localPrice, 534 | 'originalPrice' => $product->originalPrice, 535 | 'priceInCents' => $product->originalPrice * 100, 536 | 'percentOff' => 0, 537 | 'currency' => $product->currency, 538 | ]; 539 | } 540 | 541 | /** 542 | * Build /app/v1/details?app=org.example.game 543 | */ 544 | function buildDetails($game, $linkDeveloperPage = false) 545 | { 546 | $latestRelease = $game->latestRelease; 547 | 548 | $mediaTiles = []; 549 | if ($game->discover) { 550 | $mediaTiles[] = [ 551 | 'type' => 'image', 552 | 'urls' => [ 553 | 'thumbnail' => $game->discover, 554 | 'full' => $game->discover, 555 | ], 556 | ]; 557 | } 558 | foreach ($game->media as $medium) { 559 | if ($medium->type == 'image') { 560 | $mediaTiles[] = [ 561 | 'type' => 'image', 562 | 'urls' => [ 563 | 'thumbnail' => $medium->thumb ?? $medium->url, 564 | 'full' => $medium->url, 565 | ], 566 | ]; 567 | } else { 568 | if (!isUnsupportedVideoUrl($medium->url)) { 569 | $mediaTiles[] = [ 570 | 'type' => 'video', 571 | 'url' => $medium->url, 572 | ]; 573 | } 574 | } 575 | } 576 | 577 | $buttons = []; 578 | if (isset($game->links->unlocked)) { 579 | $buttons[] = [ 580 | 'text' => 'Show unlocked', 581 | 'url' => 'ouya://launcher/details?app=' . $game->links->unlocked, 582 | 'bold' => true, 583 | ]; 584 | } 585 | 586 | $product = null; 587 | $gamePromoted = getPromotedProduct($game); 588 | if ($gamePromoted) { 589 | $product = buildProduct($gamePromoted); 590 | } 591 | 592 | $iaUrl = null; 593 | if (isset($game->latestRelease->url) 594 | && substr($game->latestRelease->url, 0, 29) == 'https://archive.org/download/' 595 | ) { 596 | $iaUrl = dirname($game->latestRelease->url) . '/'; 597 | } 598 | 599 | $description = $game->description; 600 | if (isset($game->notes) && trim($game->notes)) { 601 | $description = "Technical notes:\r\n" . $game->notes 602 | . "\r\n----\r\n" 603 | . $description; 604 | } 605 | 606 | // http://cweiske.de/ouya-store-api-docs.htm#get-https-devs-ouya-tv-api-v1-details 607 | $data = [ 608 | 'type' => 'Game', 609 | 'title' => $game->title, 610 | 'description' => $description, 611 | 'gamerNumbers' => $game->players, 612 | 'genres' => $game->genres, 613 | 614 | 'suggestedAge' => $game->contentRating, 615 | 'premium' => $game->premium, 616 | 'inAppPurchases' => $game->inAppPurchases, 617 | 'firstPublishedAt' => strtotime($game->firstPublishedAt), 618 | 'ccUrl' => null, 619 | 620 | 'rating' => [ 621 | 'count' => $game->rating->count, 622 | 'average' => $game->rating->average, 623 | ], 624 | 625 | 'apk' => [ 626 | 'fileSize' => $latestRelease->size, 627 | 'nativeSize' => $latestRelease->nativeSize, 628 | 'publicSize' => $latestRelease->publicSize, 629 | 'md5sum' => $latestRelease->md5sum, 630 | 'filename' => 'FIXME', 631 | 'errors' => '', 632 | 'package' => $game->packageName, 633 | 'versionCode' => $latestRelease->versionCode, 634 | 'state' => 'complete', 635 | ], 636 | 637 | 'version' => [ 638 | 'number' => $latestRelease->name, 639 | 'publishedAt' => strtotime($latestRelease->date), 640 | 'uuid' => $latestRelease->uuid, 641 | ], 642 | 643 | 'developer' => [ 644 | 'name' => $game->developer->name, 645 | 'founder' => $game->developer->founder, 646 | ], 647 | 648 | 'metaData' => [ 649 | 'key:rating.average', 650 | 'key:developer.name', 651 | 'key:suggestedAge', 652 | number_format($latestRelease->size / 1024 / 1024, 2, '.', '') . ' MiB', 653 | ], 654 | 655 | 'tileImage' => $game->discover, 656 | 'mediaTiles' => $mediaTiles, 657 | 'mobileAppIcon' => null, 658 | 'heroImage' => [ 659 | 'url' => null, 660 | ], 661 | 662 | 'promotedProduct' => $product, 663 | 'buttons' => $buttons, 664 | 665 | 'stouyapi' => [ 666 | 'internet-archive' => $iaUrl, 667 | 'developer-url' => $game->developer->website ?? null, 668 | 'blockedInWebText' => $game->blockedInWebText ?? null, 669 | ] 670 | ]; 671 | 672 | if ($linkDeveloperPage) { 673 | $data['developer']['url'] = 'ouya://launcher/discover/dev--' 674 | . categoryPath($game->developer->uuid); 675 | } 676 | 677 | return $data; 678 | } 679 | 680 | function buildDeveloperCurrentGamer() 681 | { 682 | return [ 683 | 'gamer' => [ 684 | 'uuid' => '00702342-0000-1111-2222-c3e1500cafe2', 685 | 'username' => 'stouyapi', 686 | ], 687 | ]; 688 | } 689 | 690 | /** 691 | * For /api/v1/developers/xxx/products/?only=yyy 692 | */ 693 | function buildDeveloperProductOnly($product, $developer) 694 | { 695 | return [ 696 | 'developerName' => $developer->name, 697 | 'currency' => $product->currency, 698 | 'products' => [ 699 | buildProduct($product), 700 | ], 701 | ]; 702 | } 703 | 704 | /** 705 | * For /api/v1/developers/xxx/products/ 706 | */ 707 | function buildDeveloperProducts($products, $developer) 708 | { 709 | //remove duplicates 710 | $products = array_values(array_column($products, null, 'identifier')); 711 | 712 | $jsonProducts = []; 713 | foreach ($products as $product) { 714 | $jsonProducts[] = buildProduct($product); 715 | } 716 | return [ 717 | 'developerName' => $developer->name, 718 | 'currency' => $products[0]->currency ?? 'EUR', 719 | 'products' => $jsonProducts, 720 | ]; 721 | } 722 | 723 | function buildPurchases($game) 724 | { 725 | $purchasesData = [ 726 | 'purchases' => [], 727 | ]; 728 | $promotedProduct = getPromotedProduct($game); 729 | if ($promotedProduct) { 730 | $purchasesData['purchases'][] = [ 731 | 'purchaseDate' => time() * 1000, 732 | 'generateDate' => time() * 1000, 733 | 'identifier' => $promotedProduct->identifier, 734 | 'gamer' => '00702342-0000-1111-2222-c3e1500cafe2',//gamer uuid 735 | 'uuid' => '00702342-0000-1111-2222-c3e1500beef3',//transaction ID 736 | 'priceInCents' => $promotedProduct->originalPrice * 100, 737 | 'localPrice' => $promotedProduct->localPrice, 738 | 'currency' => $promotedProduct->currency, 739 | ]; 740 | } 741 | 742 | $encryptedOnce = dummyEncrypt($purchasesData); 743 | $encryptedTwice = dummyEncrypt($encryptedOnce); 744 | return $encryptedTwice; 745 | } 746 | 747 | function buildSearch($games) 748 | { 749 | $games = sortByTitle($games); 750 | $results = []; 751 | foreach ($games as $game) { 752 | $results[] = [ 753 | 'title' => $game->title, 754 | 'url' => 'ouya://launcher/details?app=' . $game->packageName, 755 | 'contentRating' => $game->contentRating, 756 | ]; 757 | } 758 | return [ 759 | 'count' => count($results), 760 | 'results' => $results, 761 | ]; 762 | } 763 | 764 | function dummyEncrypt($data) 765 | { 766 | return [ 767 | 'key' => base64_encode('0123456789abcdef'), 768 | 'iv' => 't3jir1LHpICunvhlM76edQ==',//random bytes 769 | 'blob' => base64_encode( 770 | json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) 771 | ), 772 | ]; 773 | } 774 | 775 | function addChunkedDiscoverRows(&$data, $games, $title) 776 | { 777 | $chunks = array_chunk($games, 4); 778 | $first = true; 779 | foreach ($chunks as $chunk) { 780 | addDiscoverRow( 781 | $data, $first ? $title : '', 782 | $chunk 783 | ); 784 | $first = false; 785 | } 786 | } 787 | 788 | function addDiscoverRow(&$data, $title, $games, $ranked = false) 789 | { 790 | $row = [ 791 | 'title' => $title, 792 | 'showPrice' => true, 793 | 'ranked' => $ranked, 794 | 'tiles' => [], 795 | ]; 796 | foreach ($games as $game) { 797 | if (is_string($game)) { 798 | //category link 799 | $tilePos = count($data['tiles']); 800 | $data['tiles'][$tilePos] = buildDiscoverCategoryTile($game); 801 | 802 | } else { 803 | //game 804 | if (isset($game->links->original)) { 805 | //do not link unlocked games. 806 | // people an access them via the original games 807 | continue; 808 | } 809 | $tilePos = findTile($data['tiles'], $game->packageName); 810 | if ($tilePos === null) { 811 | $tilePos = count($data['tiles']); 812 | $data['tiles'][$tilePos] = buildDiscoverGameTile($game); 813 | } 814 | } 815 | $row['tiles'][] = $tilePos; 816 | } 817 | $data['rows'][] = $row; 818 | } 819 | 820 | function findTile($tiles, $packageName) 821 | { 822 | foreach ($tiles as $pos => $tile) { 823 | if ($tile['package'] == $packageName) { 824 | return $pos; 825 | } 826 | } 827 | return null; 828 | } 829 | 830 | function buildDiscoverCategoryTile($title) 831 | { 832 | return [ 833 | 'url' => 'ouya://launcher/discover/' . categoryPath($title), 834 | 'image' => '', 835 | 'title' => $title, 836 | 'type' => 'discover' 837 | ]; 838 | } 839 | 840 | function buildDiscoverGameTile($game) 841 | { 842 | $latestRelease = $game->latestRelease; 843 | return [ 844 | 'gamerNumbers' => $game->players, 845 | 'genres' => $game->genres, 846 | 'url' => 'ouya://launcher/details?app=' . $game->packageName, 847 | 'latestVersion' => [ 848 | 'apk' => [ 849 | 'md5sum' => $latestRelease->md5sum, 850 | ], 851 | 'versionNumber' => $latestRelease->name, 852 | 'uuid' => $latestRelease->uuid, 853 | ], 854 | 'inAppPurchases' => $game->inAppPurchases, 855 | 'promotedProduct' => null, 856 | 'premium' => $game->premium, 857 | 'type' => 'app', 858 | 'package' => $game->packageName, 859 | 'updated_at' => strtotime($latestRelease->date), 860 | 'updatedAt' => $latestRelease->date, 861 | 'title' => $game->title, 862 | 'image' => $game->discover, 863 | 'contentRating' => $game->contentRating, 864 | 'rating' => [ 865 | 'count' => $game->rating->count, 866 | 'average' => $game->rating->average, 867 | ], 868 | 'promotedProduct' => buildProduct(getPromotedProduct($game)), 869 | ]; 870 | } 871 | 872 | function getAllAges($games) 873 | { 874 | $ages = []; 875 | foreach ($games as $game) { 876 | $ages[] = $game->contentRating; 877 | } 878 | return array_unique($ages); 879 | } 880 | 881 | function getAllGenres($games) 882 | { 883 | $genres = []; 884 | foreach ($games as $game) { 885 | $genres = array_merge($genres, $game->genres); 886 | } 887 | return array_unique($genres); 888 | } 889 | 890 | function addMissingGameProperties($game) 891 | { 892 | global $cfgEnableQr; 893 | 894 | if (!isset($game->overview)) { 895 | $game->overview = null; 896 | } 897 | if (!isset($game->description)) { 898 | $game->description = ''; 899 | } 900 | if (!isset($game->players)) { 901 | $game->players = [1]; 902 | } 903 | if (!isset($game->genres)) { 904 | $game->genres = ['Unsorted']; 905 | } 906 | if (!isset($game->website)) { 907 | $game->website = null; 908 | } 909 | if (!isset($game->contentRating)) { 910 | $game->contentRating = 'Everyone'; 911 | } 912 | if (!isset($game->premium)) { 913 | $game->premium = false; 914 | } 915 | if (!isset($game->firstPublishedAt)) { 916 | $game->firstPublishedAt = gmdate('c'); 917 | } 918 | 919 | if (!isset($game->rating)) { 920 | $game->rating = new stdClass(); 921 | } 922 | if (!isset($game->rating->likeCount)) { 923 | $game->rating->likeCount = 0; 924 | } 925 | if (!isset($game->rating->average)) { 926 | $game->rating->average = 0; 927 | } 928 | if (!isset($game->rating->count)) { 929 | $game->rating->count = 0; 930 | } 931 | 932 | $game->firstRelease = null; 933 | $game->latestRelease = null; 934 | $firstReleaseTimestamp = null; 935 | $latestReleaseTimestamp = 0; 936 | foreach ($game->releases as $release) { 937 | if (isset($release->broken) && $release->broken) { 938 | continue; 939 | } 940 | if (!isset($release->publicSize)) { 941 | $release->publicSize = 0; 942 | } 943 | if (!isset($release->nativeSize)) { 944 | $release->nativeSize = 0; 945 | } 946 | 947 | $releaseTimestamp = strtotime($release->date); 948 | if ($releaseTimestamp > $latestReleaseTimestamp) { 949 | $game->latestRelease = $release; 950 | $latestReleaseTimestamp = $releaseTimestamp; 951 | } 952 | if ($firstReleaseTimestamp === null 953 | || $releaseTimestamp < $firstReleaseTimestamp 954 | ) { 955 | $game->firstRelease = $release; 956 | $firstReleaseTimestamp = $releaseTimestamp; 957 | } 958 | } 959 | if ($game->firstRelease === null) { 960 | error('No first release for ' . $game->packageName); 961 | } 962 | if ($game->latestRelease === null) { 963 | error('No latest release for ' . $game->packageName); 964 | } 965 | 966 | if (!isset($game->media)) { 967 | $game->media = []; 968 | } 969 | 970 | if (!isset($game->developer->uuid)) { 971 | $game->developer->uuid = null; 972 | } 973 | if (!isset($game->developer->name)) { 974 | $game->developer->name = 'unknown'; 975 | } 976 | if (!isset($game->developer->supportEmail)) { 977 | $game->developer->supportEmail = null; 978 | } 979 | if (!isset($game->developer->supportPhone)) { 980 | $game->developer->supportPhone = null; 981 | } 982 | if (!isset($game->developer->founder)) { 983 | $game->developer->founder = false; 984 | } 985 | 986 | if ($cfgEnableQr && $game->website) { 987 | $qrfileName = preg_replace('#[^\\w\\d._-]#', '_', $game->website) . '.png'; 988 | $qrfilePath = $GLOBALS['qrDir'] . $qrfileName; 989 | if (!file_exists($qrfilePath)) { 990 | $cmd = __DIR__ . '/create-qr.sh' 991 | . ' ' . escapeshellarg($game->website) 992 | . ' ' . escapeshellarg($qrfilePath); 993 | passthru($cmd, $retval); 994 | if ($retval != 0) { 995 | exit(20); 996 | } 997 | } 998 | $qrUrlPath = $GLOBALS['baseUrl'] . 'gen-qr/' . $qrfileName; 999 | $game->media[] = (object) [ 1000 | 'type' => 'image', 1001 | 'url' => $qrUrlPath, 1002 | ]; 1003 | } 1004 | 1005 | //rewrite urls from Internet Archive to our servers 1006 | $game->discover = rewriteUrl($game->discover); 1007 | foreach ($game->media as $medium) { 1008 | $medium->url = rewriteUrl($medium->url); 1009 | } 1010 | foreach ($game->releases as $release) { 1011 | $release->url = rewriteUrl($release->url); 1012 | } 1013 | } 1014 | 1015 | /** 1016 | * Implements a sensible ranking system described in 1017 | * https://stackoverflow.com/a/1411268/2826013 1018 | */ 1019 | function calculateRank(array $games) 1020 | { 1021 | $averageRatings = array_map( 1022 | function ($game) { 1023 | return $game->rating->average; 1024 | }, 1025 | $games 1026 | ); 1027 | $average = array_sum($averageRatings) / count($averageRatings); 1028 | $C = $average; 1029 | $m = 500; 1030 | 1031 | foreach ($games as $game) { 1032 | $R = $game->rating->average; 1033 | $v = $game->rating->count; 1034 | $game->rating->rank = ($R * $v + $C * $m) / ($v + $m); 1035 | } 1036 | } 1037 | 1038 | function getFirstVideoUrl($media) 1039 | { 1040 | foreach ($media as $medium) { 1041 | if ($medium->type == 'video') { 1042 | return $medium->url; 1043 | } 1044 | } 1045 | return null; 1046 | } 1047 | 1048 | function getAllImageUrls($media) 1049 | { 1050 | $imageUrls = []; 1051 | foreach ($media as $medium) { 1052 | if ($medium->type == 'image') { 1053 | $imageUrls[] = $medium->url; 1054 | } 1055 | } 1056 | return $imageUrls; 1057 | } 1058 | 1059 | function getPromotedProduct($game) 1060 | { 1061 | if (!isset($game->products) || !count($game->products)) { 1062 | return null; 1063 | } 1064 | foreach ($game->products as $gameProd) { 1065 | if ($gameProd->promoted) { 1066 | return $gameProd; 1067 | } 1068 | } 1069 | return null; 1070 | } 1071 | 1072 | /** 1073 | * vimeo only work with HTTPS now, 1074 | * and the OUYA does not support SNI. 1075 | * We get SSL errors and no video for them :/ 1076 | */ 1077 | function isUnsupportedVideoUrl($url) 1078 | { 1079 | return strpos($url, '://vimeo.com/') !== false; 1080 | } 1081 | 1082 | function removeMakeGames(array $games) 1083 | { 1084 | return filterByGenre($games, 'Tutorials', true); 1085 | } 1086 | 1087 | function removeMakeGenres($genres) 1088 | { 1089 | $filtered = []; 1090 | foreach ($genres as $genre) { 1091 | if ($genre != 'Tutorials' && $genre != 'Builds') { 1092 | $filtered[] = $genre; 1093 | } 1094 | } 1095 | return $filtered; 1096 | } 1097 | 1098 | function rewriteUrl($url) 1099 | { 1100 | foreach ($GLOBALS['urlRewrites'] as $pattern => $replacement) { 1101 | $url = preg_replace($pattern, $replacement, $url); 1102 | } 1103 | return $url; 1104 | } 1105 | 1106 | function writeJson($path, $data) 1107 | { 1108 | global $cfgMini, $wwwDir; 1109 | $fullPath = $wwwDir . $path; 1110 | $dir = dirname($fullPath); 1111 | if (!is_dir($dir)) { 1112 | mkdir($dir, 0777, true); 1113 | } 1114 | $opts = JSON_UNESCAPED_SLASHES; 1115 | if (!$cfgMini) { 1116 | $opts |= JSON_PRETTY_PRINT; 1117 | } 1118 | file_put_contents( 1119 | $fullPath, 1120 | json_encode($data, $opts) . "\n" 1121 | ); 1122 | } 1123 | 1124 | function writeCategoryJson($path, $data) 1125 | { 1126 | writeJson($path, $data); 1127 | 1128 | $forgePath = str_replace('.json', '.forge.json', $path); 1129 | $forgeData = convertCategoryToForge($data, true); 1130 | writeJson($forgePath, $forgeData); 1131 | } 1132 | 1133 | function error($msg) 1134 | { 1135 | fwrite(STDERR, $msg . "\n"); 1136 | exit(1); 1137 | } 1138 | ?> 1139 | -------------------------------------------------------------------------------- /config.php.dist: -------------------------------------------------------------------------------- 1 | 'http://statics.ouya.world/ia/', 10 | ]; 11 | $GLOBALS['packagelists']["cweiske's picks"] = [ 12 | 'de.eiswuxe.blookid2', 13 | 'com.cosmos.babyloniantwins', 14 | 'com.inverseblue.skyriders', 15 | ]; 16 | $GLOBALS['home']['2020 Winter GameJam'] = [ 17 | 'com.DESKINK.ToneTests', 18 | 'com.Eightbbgames.yahayor', 19 | 'com.FuzzyPopcorn', 20 | 'com.NYYLE.NYCTO', 21 | 'com.NoelRojasOliveras.PaintKiller', 22 | 'com.StrawHat.Fall', 23 | 'com.oliverstogden.trf', 24 | 'com.samuelsousa.shootdestroy', 25 | 'com.scorpion.shootout', 26 | 'com.sd_game_dev.aliens_taste_my_sword', 27 | 'com.sumotown.sirtet', 28 | 'de.x2041.games.gyrogun', 29 | 'ht.sr.git.arcticGrind.embed', 30 | 'tv.ouya.demo.DarkSpacePioneer', 31 | 'tv.ouya.win.unity.brokenbeauty', 32 | ]; 33 | $GLOBALS['pushToMyOuyaUrl'] = '../push-to-my-ouya.php'; 34 | 35 | $GLOBALS['categorySubtitles'] = [ 36 | 'Exclusive' => 'Only available on the OUYA', 37 | 'Unlocked' => 'Official OUYA shutdown releases', 38 | 'VIP Room' => 'Once exclusive on the OUYA', 39 | ]; 40 | -------------------------------------------------------------------------------- /data/templates/allgames.tpl.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | List of all OUYA games 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |

List of all OUYA games

16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 38 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |
Game titleDeveloperAgePlayersGenresRelease
34 | 35 | title) ?> 36 | 37 | 39 | developerUrl): ?> 40 | developer) ?> 41 | 42 | developer) ?> 43 | 44 | suggestedAge) ?>players)) ?>genres)) ?>apkTimestamp)) ?>
53 | 54 | 59 | 60 | 61 | 62 | 63 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /data/templates/discover.tpl.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <?= htmlspecialchars($title); ?> 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |

title); ?>

14 | 15 |
16 | 17 | 18 |
19 | title): ?> 20 |

title) ?>

21 | 22 | 23 |
24 | tiles as $tile): ?> 25 |
26 |

title) ?>

27 | thumb != ''): ?> 28 | Screenshot of <?= htmlspecialchars($tile->title) ?> 29 | 30 | type == 'app' && $tile->ratingCount > 0): ?> 31 |

32 | rating ?> 33 | (ratingCount ?>) 34 |

35 | 36 |
37 | 38 |
39 | 40 |
41 | 42 | 43 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /data/templates/game.tpl.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <?= htmlspecialchars($json->title); ?> - OUYA game 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 |
22 |
23 |

title); ?>

24 |
25 |
Rating
26 |
27 | rating->average ?> 28 | (rating->count ?>) 29 |
30 | 31 |
Developer
32 |
33 | 34 | developer->name) ?> 35 | 36 | developer->name) ?> 37 | 38 |
39 | 40 |
Suggested age
41 |
42 | suggestedAge) ?> 43 |
44 | 45 |
Number of players
46 |
47 | gamerNumbers)) ?> 48 |
49 | 50 |
Download size
51 |
52 | apk->fileSize / 1024 / 1024, 2) ?> MiB 53 |
54 |
55 | 56 |

57 | inAppPurchases): ?> 58 | * Includes in-app purchases

59 | 60 | 61 | 62 | 63 | description)) ?> 64 | 65 |

66 |
67 | 68 | 69 |
70 |

Screenshots

71 |
72 | mediaTiles as $tile): ?> 73 | type == 'image'): ?> 74 | Screenshot of <?= htmlspecialchars($json->title); ?> 75 | type == 'video'): ?> 76 | 79 | 80 | 81 |
82 |
83 | 84 | 85 |
86 |

Links

87 | 88 |
89 | Download .apk 90 |

91 | Version version->number ?>, published 92 | version->publishedAt) ?> 93 |

94 |
95 | 96 | 97 |
98 | Internet Archive 99 |
100 | 101 | 102 |
103 | Developer page 104 |
105 | 106 | app->website): ?> 107 |
108 | Game website 109 |
110 | 111 |
112 |
113 | 118 |
119 |
120 |
121 | 122 | 127 | 128 | 133 | 138 | 139 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /src/push-to-my-ouya-helpers.php: -------------------------------------------------------------------------------- 1 | 13 | ExpiresActive On 14 | ExpiresDefault "access plus 1 day" 15 | 16 | 17 | #rewrite details GET parameter 18 | RewriteCond %{QUERY_STRING} ^app=([^&]+) 19 | RewriteCond %{DOCUMENT_ROOT}/api/v1/details-data/%1.json -f 20 | RewriteRule ^api/v1/details /api/v1/details-data/%1.json? [END] 21 | 22 | # details for unknown apps. Also see .htaccess in the dummy dir 23 | RewriteRule ^api/v1/details /api/v1/details-dummy/404? [END] 24 | 25 | RewriteRule ^api/v1/apps/(.*)/download$ /api/v1/apps/$1-download.json? [END] 26 | RewriteRule ^api/v1/apps/(.*)$ /api/v1/apps/$1.json? [END] 27 | 28 | #rewrite developer products "only" GET parameter 29 | RewriteCond %{QUERY_STRING} &only=([^&]+) 30 | RewriteCond %{DOCUMENT_ROOT}/api/v1/developers/$1/products/%1.json -f 31 | RewriteRule ^api/v1/developers/(.+)/products/ /api/v1/developers/$1/products/%1.json? [END] 32 | 33 | #discover homepage 34 | RewriteCond %{HTTP_USER_AGENT} "Forge" 35 | RewriteRule ^api/v1/discover/?$ /api/v1/discover-data/discover.forge.json [END] 36 | 37 | RewriteRule ^api/v1/discover/?$ /api/v1/discover-data/discover.json [END] 38 | 39 | #discover category 40 | RewriteCond %{HTTP_USER_AGENT} "Forge" 41 | RewriteCond %{REQUEST_URI} ^/api/v1/discover/(.+)$ 42 | RewriteCond %{DOCUMENT_ROOT}/api/v1/discover-data/$1.forge.json -f 43 | RewriteRule ^api/v1/discover/(.+)$ /api/v1/discover-data/$1.forge.json [END] 44 | 45 | RewriteRule ^api/v1/discover/(.+)$ /api/v1/discover-data/$1.json [END] 46 | 47 | #if directoryslash is on, gamers gets redirected to gamers/ (dir index) 48 | # the ouya registration does not support redirects there 49 | #TODO: Use that only for the api/v1/gamers path, not for all 50 | ErrorDocument 400 /api/v1/gamers/register-error.json 51 | RewriteRule ^api/v1/gamers$ /api/v1/gamers/register-error.json [R=400,END] 52 | RewriteRule ^api/razer/gamer$ /api/v1/gamers/register-error.json [R=400,END] 53 | 54 | #prevent redirect from gamers/me to gamers/me/ 55 | 56 | DirectorySlash Off 57 | 58 | 59 | #Disable the next three lines to have static usernames only 60 | RewriteRule ^api/razer/session$ /api/razer/session.php [END] 61 | RewriteRule ^api/v1/gamers/me$ /api/v1/gamers/me.php [END] 62 | RewriteRule ^api/v1/sessions$ /api/v1/sessions.php [END] 63 | 64 | RewriteRule ^api/v1/gamers/me$ /api/v1/gamers/me.json [END] 65 | 66 | #partner builds 67 | RewriteCond %{HTTP_USER_AGENT} "razer/pearlyn/pearlyn" 68 | RewriteRule ^api/v1/partner_builds$ /api/v1/partner_builds.razer-forge-tv.json [END] 69 | 70 | RewriteRule ^api/v1/partner_builds$ /api/v1/partner_builds.json [END] 71 | 72 | #purchased games/products 73 | # active buy requests 74 | RewriteCond %{REQUEST_METHOD} POST 75 | RewriteRule ^api/v1/games/(.+)/purchases?$ /api/v1/games/purchase.php [END] 76 | 77 | RewriteCond %{REQUEST_FILENAME} !-f 78 | RewriteRule ^api/v1/games/(.+)/purchases?$ /api/v1/games/purchases-empty.json [END] 79 | 80 | #search 81 | # q is first parameter 82 | RewriteCond %{QUERY_STRING} ^q=([^&]+) 83 | RewriteCond %{DOCUMENT_ROOT}/api/v1/search-data/%1.json -f 84 | RewriteRule ^api/v1/search /api/v1/search-data/%1.json? [END] 85 | # q is not the first parameter 86 | RewriteCond %{QUERY_STRING} &q=([^&]+) 87 | RewriteCond %{DOCUMENT_ROOT}/api/v1/search-data/%1.json -f 88 | RewriteRule ^api/v1/search /api/v1/search-data/%1.json? [END] 89 | 90 | # for http://clients3.google.com/generate_204 91 | RewriteRule ^generate_204$ /api/v1/status 92 | 93 | #this one wants a 204 status code 94 | RewriteRule ^api/v1/status$ - [R=204,L] 95 | 96 | #push-to-my-ouya needs php scripting support 97 | RewriteRule ^api/v1/queued_downloads?$ /api/v1/queued_downloads.php [END] 98 | 99 | RewriteCond %{REQUEST_METHOD} DELETE 100 | RewriteRule ^api/v1/queued_downloads/(.+)?$ /api/v1/queued_downloads_delete.php?game=$1 [END] 101 | -------------------------------------------------------------------------------- /www/agreements/marketplace.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | stouyapi marketplace agreements 4 | 5 | 6 |

stouyapi - OUYA API server

7 |

8 | We store no data, so you do not need to agree to anything :) 9 |

10 |

11 | Registration of new accounts does not work here! 12 | Simply go back and choose "existing account", 13 | type any username, leave the password empty and sign in. 14 |

15 | 16 | 17 | -------------------------------------------------------------------------------- /www/api/firmware_builds: -------------------------------------------------------------------------------- 1 | { 2 | "result": [ 3 | { 4 | "incremental": false, 5 | "filename": "OUYA-1.2.1427-r1.zip", 6 | "timestamp": "1418407476", 7 | "md5sum": "3a95be82b4c33bd68353dc314346f39f", 8 | "channel": "stable", 9 | "url": "http://ouya.cweiske.de/firmware/RC-OUYA-1.2.1427-r1_ota.zip", 10 | "requiredUpto": "1.2.995", 11 | "changes": null, 12 | "changesLocalized": { 13 | "de": "v1.2.1427 \"Chickcharney Hotfix 2\" wurde am 2014-12-15 veröffentlich und ist die letzte offiziell erschienene Firmware.", 14 | "it": "no info", 15 | "fr": "no info", 16 | "es": "no info", 17 | "en": "v1.2.1427 \"Chickcharney Hotfix 2\" was released on 2014-12-15 and is the last official firmware that was ever published." 18 | } 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /www/api/razer/session: -------------------------------------------------------------------------------- 1 | { 2 | "token": "00702342-0000-1111-2222-c3e1500cafe1" 3 | } 4 | -------------------------------------------------------------------------------- /www/api/razer/session.php: -------------------------------------------------------------------------------- 1 | 8 | * @see api/razer/session 9 | * @see api/v1/gamers/me 10 | */ 11 | 12 | if (!isset($_POST['email'])) { 13 | header('HTTP/1.0 400 Bad Request'); 14 | header('Content-type: application/json'); 15 | echo '{"error":{"message":"E-Mail missing","code": 2001}}' . "\n"; 16 | exit(1); 17 | } 18 | $email = $_POST['email']; 19 | 20 | //we use the ouya username storage code here 21 | // and simply use the part before the @ in the e-mail as username. 22 | list($_POST['username']) = explode('@', $email); 23 | require __DIR__ . '/../v1/sessions.php'; 24 | -------------------------------------------------------------------------------- /www/api/v1/console_configuration: -------------------------------------------------------------------------------- 1 | { 2 | "BTC_LAUNCHER_PACKAGES":"tv.ouya.xbmc,tunein.player,tv.twitch.android.viewer,com.plexapp.android,tv.ouya.bbciplayer,tv.ouya.hulu,tv.ouya.minecrafttv,tv.ouya.ouyabrowser,tv.ouya.pandora,tv.ouya.visiomgo,tv.ouya.youtube,tv.ouya.tubitv", 3 | "RATING_PROMPT_FREQ": 0 4 | } 5 | -------------------------------------------------------------------------------- /www/api/v1/crash_report: -------------------------------------------------------------------------------- 1 | { 2 | "success": true 3 | } 4 | -------------------------------------------------------------------------------- /www/api/v1/details-dummy/.htaccess: -------------------------------------------------------------------------------- 1 | DirectoryIndex index.html 2 | ErrorDocument 404 /api/v1/details-dummy/error-unknown-app-details.json 3 | -------------------------------------------------------------------------------- /www/api/v1/details-dummy/error-unknown-app-details.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": { 3 | "message": "Unknown application", 4 | "code": 2005 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /www/api/v1/events: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /www/api/v1/gamers/me.json: -------------------------------------------------------------------------------- 1 | { 2 | "gamer": { 3 | "uuid": "00702342-0000-1111-2222-c3e1500cafe2", 4 | "settings": {}, 5 | "founder": false, 6 | "email": "stouyapi@example.org", 7 | "username": "stouyapi", 8 | "nickname": "stouyapi", 9 | "avatar": null 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /www/api/v1/gamers/me.php: -------------------------------------------------------------------------------- 1 | 6 | * @see api/v1/sessions 7 | */ 8 | $dbFile = __DIR__ . '/../../../../data/usernames.sqlite3'; 9 | $cfgFile = __DIR__ . '/../../../../config.php'; 10 | if (file_exists($cfgFile)) { 11 | include $cfgFile; 12 | } 13 | 14 | 15 | $ip = $_SERVER['REMOTE_ADDR']; 16 | if ($ip == '') { 17 | //empty ip 18 | header('X-Fail-Reason: empty ip address'); 19 | header('Content-type: application/json'); 20 | echo file_get_contents('me.json'); 21 | exit(1); 22 | } 23 | 24 | try { 25 | $db = new SQLite3($dbFile, SQLITE3_OPEN_READONLY); 26 | } catch (Exception $e) { 27 | //db file not found 28 | header('X-Fail-Reason: database file not found'); 29 | header('Content-type: application/json'); 30 | echo file_get_contents('me.json'); 31 | exit(1); 32 | } 33 | 34 | $stmt = $db->prepare('SELECT * FROM usernames WHERE ip = :ip'); 35 | $stmt->bindValue(':ip', $ip); 36 | $res = $stmt->execute(); 37 | $row = $res->fetchArray(SQLITE3_ASSOC); 38 | $res->finalize(); 39 | 40 | if ($row === false) { 41 | header('Content-type: application/json'); 42 | echo file_get_contents('me.json'); 43 | exit(); 44 | } 45 | 46 | $data = json_decode(file_get_contents('me.json')); 47 | $data->gamer->username = $row['username']; 48 | $data->gamer->nickname = $row['username']; 49 | 50 | switch (strtolower($row['username'])) { 51 | case 'cweiske': 52 | $data->gamer->founder = true; 53 | $data->gamer->avatar = $GLOBALS['baseUrl'] . 'avatars/cweiske.png'; 54 | break; 55 | case 'szeraax': 56 | $data->gamer->founder = true; 57 | break; 58 | } 59 | 60 | header('Content-type: application/json'); 61 | echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"; 62 | ?> 63 | -------------------------------------------------------------------------------- /www/api/v1/gamers/me/agreements: -------------------------------------------------------------------------------- 1 | { 2 | "marketplace_required_version": 2, 3 | "marketplace": 2, 4 | "privacy_required_version": 0, 5 | "privacy": 0 6 | } 7 | -------------------------------------------------------------------------------- /www/api/v1/gamers/me/apps/developed: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [ 3 | ] 4 | } 5 | -------------------------------------------------------------------------------- /www/api/v1/gamers/me/consoles: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /www/api/v1/gamers/me/user_messages: -------------------------------------------------------------------------------- 1 | { 2 | "messages": [ 3 | ] 4 | } -------------------------------------------------------------------------------- /www/api/v1/gamers/register-error.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": { 3 | "message": "Validation errors occurred", 4 | "code": 2006, 5 | "data": { 6 | "username": [ 7 | "Registration not available. Login instead." 8 | ], 9 | "email": [ 10 | "Registration not available - login instead. First part of login e-mail will be used as nickname." 11 | ] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /www/api/v1/games/purchase.php: -------------------------------------------------------------------------------- 1 | blob)); 14 | if ($buyRequest === null) { 15 | echo "Error decoding encrypted inner JSON data\n"; 16 | exit(1); 17 | } 18 | if (!isset($buyRequest->uuid)) { 19 | echo "uuid key missing in JSON data\n"; 20 | exit(1); 21 | } 22 | if (!isset($buyRequest->identifier)) { 23 | echo "identifier key missing in JSON data\n"; 24 | exit(1); 25 | } 26 | 27 | ini_set('html_errors', false); 28 | 29 | $productFiles = glob( 30 | __DIR__ . '/../developers/*/products/' 31 | . $buyRequest->identifier . '.json' 32 | ); 33 | if (!count($productFiles)) { 34 | echo "Cannot find product file for product identifier\n"; 35 | exit(1); 36 | } 37 | $product = json_decode(file_get_contents($productFiles[0]))->products[0]; 38 | if ($product === null) { 39 | echo "could not find product in purchases file\n"; 40 | exit(1); 41 | } 42 | 43 | $payload = $product; 44 | $payload->uuid = $buyRequest->uuid; 45 | 46 | //"god of blades" and "pinball arcade" want double-encrypted responses 47 | // muffin knights works with single encryption 48 | $enc1 = [ 49 | 'key' => base64_encode('0123456789abcdef'), 50 | 'iv' => 't3jir1LHpICunvhlM76edQ==',//random bytes 51 | 'blob' => base64_encode( 52 | json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) 53 | ), 54 | ]; 55 | 56 | $enc2 = [ 57 | 'key' => base64_encode('0123456789abcdef'), 58 | 'iv' => 't3jir1LHpICunvhlM76edQ==',//random bytes 59 | 'blob' => base64_encode( 60 | json_encode($enc1, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) 61 | ), 62 | ]; 63 | echo json_encode($enc2, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"; 64 | ?> 65 | -------------------------------------------------------------------------------- /www/api/v1/games/purchases-empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "encryptedReceipt": "{\"key\": \"MDEyMzQ1Njc4OWFiY2RlZg==\", \"iv\": \"t3jir1LHpICunvhlM76edQ==\n\", \"blob\": \"ewogICAgInB1cmNoYXNlcyI6IFsKICAgIF0KfQo=\n\"}", 3 | "key": "MDEyMzQ1Njc4OWFiY2RlZg==\n", 4 | "iv": "t3jir1LHpICunvhlM76edQ==\n", 5 | "blob": "ewogICAgImtleSI6ICJDcVJFbGxuaVIyQ1pzVGRxR2xTSHErYjFuL2x1ZERDUFY4QUFhUjNhRjR2\nYThsY2tJUk9Oc3ZRbW5kSlNcbmljY2IvVWVZSFhRT2YybzA2L29SOHJHSnZyQWY1OThLN2E3UVFT\nU1kxdTZCdGQ4b3Avbk96Q24rRjdRNVxuNHRxdTdxdmxWUnJJTURJUXRtcy9TZlFURTJYRXlLdkVR\nVm0yTi9QaGJkalhtWG1nSkhVPVxuIiwKICAgICJpdiI6ICJ0M2ppcjFMSHBJQ3VudmhsTTc2ZWRR\nPT1cbiIsCiAgICAiYmxvYiI6ICJld29nSUNBZ0luQjFjbU5vWVhObGN5STZJRnNLSUNBZ0lGMEtm\nUW89XG4iCn0K\n" 6 | } 7 | -------------------------------------------------------------------------------- /www/api/v1/partner_builds.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [ 3 | { 4 | "action": "installFile", 5 | "packageName": "tv.ouya.oobe", 6 | "friendlyName": "OUYA OOBE", 7 | "md5": "41d50891225591fdd32045ed0821a3bf", 8 | "filesize": 7725861, 9 | "downloadUrl": "http://ouya.cweiske.de/apks/ouya-everywhere/tv.ouya.oobe-1.2.897.apk", 10 | "versionCode": 10200897 11 | }, 12 | 13 | { 14 | "action": "installFile", 15 | "packageName": "tv.ouya", 16 | "friendlyName": "OUYA Framework", 17 | "md5": "95d987e7b100c2c72bdac37a8eda2647", 18 | "filesize": 3123639, 19 | "downloadUrl": "http://ouya.cweiske.de/apks/ouya-everywhere/tv.ouya-1.2.897.apk", 20 | "versionCode": 10200897 21 | }, 22 | 23 | { 24 | "action": "installFile", 25 | "packageName": "tv.ouya.console", 26 | "friendlyName": "OUYA launcher", 27 | "md5": "97da25989c64a90c60070978077fff55", 28 | "filesize": 18953944, 29 | "downloadUrl": "http://ouya.cweiske.de/apks/ouya-everywhere/tv.ouya.console-1.2.897.apk", 30 | "versionCode": 10200897 31 | }, 32 | 33 | { 34 | "action": "launch", 35 | "packageName": "tv.ouya.console" 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /www/api/v1/partner_builds.razer-forge-tv.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [ 3 | { 4 | "action": "installFile", 5 | "packageName": "tv.ouya", 6 | "friendlyName": "Cortex Framework", 7 | "md5": "697248296f967e428efc5c205c75d301", 8 | "filesize": 6702855, 9 | "downloadUrl": "http://ouya.cweiske.de/apks/razer-forge/CortexFramework-1.2.2320-tv.ouya.apk", 10 | "versionCode": 10202320 11 | }, 12 | 13 | { 14 | "action": "installFile", 15 | "packageName": "tv.ouya.console", 16 | "friendlyName": "Razer Cortex", 17 | "md5": "90637a8a5cbc937b3a11782838a3112d", 18 | "filesize": 24220488, 19 | "downloadUrl": "http://ouya.cweiske.de/apks/razer-forge/Razer_Cortex-1.2.2320-tv.ouya.console.apk", 20 | "versionCode": 10202320 21 | }, 22 | 23 | { 24 | "action": "launch", 25 | "packageName": "tv.ouya.console" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /www/api/v1/partner_builds/release_notes: -------------------------------------------------------------------------------- 1 | { 2 | "releaseNotes": "This is the latest firmware for the Razer Forge TV." 3 | } 4 | -------------------------------------------------------------------------------- /www/api/v1/premium_purchases: -------------------------------------------------------------------------------- 1 | { 2 | "games": [ 3 | ] 4 | } 5 | -------------------------------------------------------------------------------- /www/api/v1/queued_downloads: -------------------------------------------------------------------------------- 1 | { 2 | "queue": [] 3 | } 4 | -------------------------------------------------------------------------------- /www/api/v1/queued_downloads.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | $dbFile = __DIR__ . '/../../../data/push-to-my-ouya.sqlite3'; 10 | $apiGameDir = __DIR__ . '/details-data/'; 11 | 12 | require_once __DIR__ . '/../../../src/push-to-my-ouya-helpers.php'; 13 | 14 | $ip = $_SERVER['REMOTE_ADDR']; 15 | if ($ip == '') { 16 | //empty ip 17 | header('X-Fail-Reason: empty ip address'); 18 | header('Content-type: application/json'); 19 | echo file_get_contents('queued_downloads'); 20 | exit(1); 21 | } 22 | $ip = mapIp($ip); 23 | 24 | try { 25 | $db = new SQLite3($dbFile, SQLITE3_OPEN_READONLY); 26 | } catch (Exception $e) { 27 | //db file not found 28 | header('X-Fail-Reason: database file not found'); 29 | header('Content-type: application/json'); 30 | echo file_get_contents('queued_downloads'); 31 | exit(1); 32 | } 33 | 34 | $res = $db->query( 35 | 'SELECT * FROM pushes' 36 | . ' WHERE ip = \'' . SQLite3::escapeString($ip) . '\'' 37 | ); 38 | $queue = []; 39 | while ($row = $res->fetchArray(SQLITE3_ASSOC)) { 40 | $apiGameFile = $apiGameDir . $row['game'] . '.json'; 41 | if (!file_exists($apiGameFile)) { 42 | //game deleted? 43 | continue; 44 | } 45 | $json = json_decode(file_get_contents($apiGameFile)); 46 | $queue[] = [ 47 | 'versionUuid' => '', 48 | 'title' => $json->title, 49 | 'source' => 'gamer', 50 | 'uuid' => $row['game'], 51 | ]; 52 | } 53 | 54 | header('Content-type: application/json'); 55 | echo json_encode(['queue' => $queue]) . "\n"; 56 | ?> 57 | -------------------------------------------------------------------------------- /www/api/v1/queued_downloads_delete.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | $dbFile = __DIR__ . '/../../../data/push-to-my-ouya.sqlite3'; 10 | $apiGameDir = __DIR__ . '/details-data/'; 11 | 12 | require_once __DIR__ . '/../../../src/push-to-my-ouya-helpers.php'; 13 | 14 | $ip = $_SERVER['REMOTE_ADDR']; 15 | if ($ip == '') { 16 | //empty ip 17 | header('X-Fail-Reason: empty ip address'); 18 | header('HTTP/1.0 204 No Content'); 19 | exit(1); 20 | } 21 | $ip = mapIp($ip); 22 | 23 | if (!isset($_GET['game'])) { 24 | header('HTTP/1.0 400 Bad Request'); 25 | header('Content-type: text/plain'); 26 | echo 'Game parameter missing' . "\n"; 27 | exit(1); 28 | } 29 | 30 | $game = $_GET['game']; 31 | $cleanGame = preg_replace('#[^a-zA-Z0-9._]#', '', $game); 32 | if ($game != $cleanGame || $game == '') { 33 | header('HTTP/1.0 400 Bad Request'); 34 | header('Content-type: text/plain'); 35 | echo 'Invalid game' . "\n"; 36 | exit(1); 37 | } 38 | 39 | try { 40 | $db = new SQLite3($dbFile, SQLITE3_OPEN_READWRITE); 41 | } catch (Exception $e) { 42 | //db file not found 43 | header('X-Fail-Reason: database file not found'); 44 | header('HTTP/1.0 204 No Content'); 45 | exit(1); 46 | } 47 | 48 | $rowId = $db->querySingle( 49 | 'SELECT id FROM pushes' 50 | . ' WHERE ip = \'' . SQLite3::escapeString($ip) . '\'' 51 | . ' AND game =\'' . SQLite3::escapeString($game) . '\'' 52 | ); 53 | if ($rowId === null) { 54 | header('HTTP/1.0 404 Not Found'); 55 | header('Content-type: text/plain'); 56 | echo 'Game not queued' . "\n"; 57 | exit(1); 58 | } 59 | 60 | $db->exec('DELETE FROM pushes WHERE id = ' . intval($rowId)); 61 | header('HTTP/1.0 204 No Content'); 62 | ?> 63 | -------------------------------------------------------------------------------- /www/api/v1/ratings: -------------------------------------------------------------------------------- 1 | { 2 | "ratings": [] 3 | } 4 | -------------------------------------------------------------------------------- /www/api/v1/recommendations: -------------------------------------------------------------------------------- 1 | { 2 | "games": [] 3 | } 4 | -------------------------------------------------------------------------------- /www/api/v1/sessions: -------------------------------------------------------------------------------- 1 | { 2 | "token": "00702342-0000-1111-2222-c3e1500cafe1" 3 | } 4 | -------------------------------------------------------------------------------- /www/api/v1/sessions.php: -------------------------------------------------------------------------------- 1 | 8 | * @see api/v1/sessions 9 | * @see api/v1/gamers/me 10 | */ 11 | $dbFile = __DIR__ . '/../../../data/usernames.sqlite3'; 12 | 13 | if (!isset($_POST['username'])) { 14 | header('HTTP/1.0 400 Bad Request'); 15 | header('Content-type: application/json'); 16 | echo '{"error":{"message":"Username missing","code": 2001}}' . "\n"; 17 | exit(1); 18 | } 19 | $username = $_POST['username']; 20 | 21 | $ip = $_SERVER['REMOTE_ADDR']; 22 | if ($ip == '') { 23 | header('HTTP/1.0 400 Bad Request'); 24 | header('Content-type: application/json'); 25 | echo '{"error":{"message":"Cannot detect your IP address","code": 2002}}' 26 | . "\n"; 27 | exit(1); 28 | } 29 | 30 | try { 31 | $db = new SQLite3($dbFile, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE); 32 | } catch (Exception $e) { 33 | header('HTTP/1.0 500 Internal server error'); 34 | header('Content-type: application/json'); 35 | echo '{"error":{"message":"Cannot open username database","code": 2003}}' 36 | . "\n"; 37 | echo $e->getMessage() . "\n"; 38 | exit(2); 39 | } 40 | 41 | $res = $db->querySingle( 42 | 'SELECT name FROM sqlite_master WHERE type = "table" AND name = "usernames"' 43 | ); 44 | if ($res === null) { 45 | //table does not exist yet 46 | $db->exec( 47 | <<exec( 60 | 'DELETE FROM usernames' 61 | . ' WHERE created_at < \'' . gmdate('Y-m-d H:i:s', time() - 86400) . '\'' 62 | ); 63 | 64 | //clean up previous logins 65 | $stmt = $db->prepare('DELETE FROM usernames WHERE ip = :ip'); 66 | $stmt->bindValue(':ip', $ip, SQLITE3_TEXT); 67 | $stmt->execute()->finalize(); 68 | 69 | //store the username 70 | $stmt = $db->prepare('INSERT INTO usernames (ip, username) VALUES(:ip, :username)'); 71 | $stmt->bindValue(':ip', $ip); 72 | $stmt->bindValue(':username', $username); 73 | $res = $stmt->execute(); 74 | if ($res === false) { 75 | header('HTTP/1.0 500 Internal server error'); 76 | header('Content-type: application/json'); 77 | echo '{"error":{"message":"Cannot store username","code": 2004}}' 78 | . "\n"; 79 | exit(3); 80 | } 81 | $res->finalize(); 82 | 83 | header('HTTP/1.0 200 OK'); 84 | header('Content-type: application/json'); 85 | require __DIR__ . '/sessions'; 86 | ?> 87 | -------------------------------------------------------------------------------- /www/api/v1/status: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cweiske/stouyapi/8b9bcae7760ae891ea3c9a93bc936219bebe9c54/www/api/v1/status -------------------------------------------------------------------------------- /www/api/v1/themes: -------------------------------------------------------------------------------- 1 | { 2 | "background_style": "static", 3 | "background": "", 4 | "video": "" 5 | } 6 | -------------------------------------------------------------------------------- /www/api/v1/wallet: -------------------------------------------------------------------------------- 1 | { 2 | "requiresPaymentMethod": false, 3 | "balance": 42.23, 4 | "credit_card": null, 5 | "currency": "EUR" 6 | } 7 | -------------------------------------------------------------------------------- /www/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cweiske/stouyapi/8b9bcae7760ae891ea3c9a93bc936219bebe9c54/www/bg.jpg -------------------------------------------------------------------------------- /www/bg_details.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cweiske/stouyapi/8b9bcae7760ae891ea3c9a93bc936219bebe9c54/www/bg_details.jpg -------------------------------------------------------------------------------- /www/datatables/datatables.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This combined file was created by the DataTables downloader builder: 3 | * https://datatables.net/download 4 | * 5 | * To rebuild or modify this file with the latest versions of the included 6 | * software please visit: 7 | * https://datatables.net/download/#dt/dt-1.10.21/fh-3.1.7/sp-1.1.1/sl-1.3.1 8 | * 9 | * Included libraries: 10 | * DataTables 1.10.21, FixedHeader 3.1.7, SearchPanes 1.1.1, Select 1.3.1 11 | */ 12 | 13 | /* 14 | * Table styles 15 | */ 16 | table.dataTable { 17 | width: 100%; 18 | margin: 0 auto; 19 | clear: both; 20 | border-collapse: separate; 21 | border-spacing: 0; 22 | /* 23 | * Header and footer styles 24 | */ 25 | /* 26 | * Body styles 27 | */ 28 | } 29 | table.dataTable thead th, 30 | table.dataTable tfoot th { 31 | font-weight: bold; 32 | } 33 | table.dataTable thead th, 34 | table.dataTable thead td { 35 | padding: 10px 18px; 36 | border-bottom: 1px solid #111; 37 | } 38 | table.dataTable thead th:active, 39 | table.dataTable thead td:active { 40 | outline: none; 41 | } 42 | table.dataTable tfoot th, 43 | table.dataTable tfoot td { 44 | padding: 10px 18px 6px 18px; 45 | border-top: 1px solid #111; 46 | } 47 | table.dataTable thead .sorting, 48 | table.dataTable thead .sorting_asc, 49 | table.dataTable thead .sorting_desc, 50 | table.dataTable thead .sorting_asc_disabled, 51 | table.dataTable thead .sorting_desc_disabled { 52 | cursor: pointer; 53 | *cursor: hand; 54 | background-repeat: no-repeat; 55 | background-position: center right; 56 | } 57 | table.dataTable thead .sorting { 58 | background-image: url("DataTables-1.10.21/images/sort_both.png"); 59 | } 60 | table.dataTable thead .sorting_asc { 61 | background-image: url("DataTables-1.10.21/images/sort_asc.png"); 62 | } 63 | table.dataTable thead .sorting_desc { 64 | background-image: url("DataTables-1.10.21/images/sort_desc.png"); 65 | } 66 | table.dataTable thead .sorting_asc_disabled { 67 | background-image: url("DataTables-1.10.21/images/sort_asc_disabled.png"); 68 | } 69 | table.dataTable thead .sorting_desc_disabled { 70 | background-image: url("DataTables-1.10.21/images/sort_desc_disabled.png"); 71 | } 72 | table.dataTable tbody tr { 73 | background-color: #ffffff; 74 | } 75 | table.dataTable tbody tr.selected { 76 | background-color: #B0BED9; 77 | } 78 | table.dataTable tbody th, 79 | table.dataTable tbody td { 80 | padding: 8px 10px; 81 | } 82 | table.dataTable.row-border tbody th, table.dataTable.row-border tbody td, table.dataTable.display tbody th, table.dataTable.display tbody td { 83 | border-top: 1px solid #ddd; 84 | } 85 | table.dataTable.row-border tbody tr:first-child th, 86 | table.dataTable.row-border tbody tr:first-child td, table.dataTable.display tbody tr:first-child th, 87 | table.dataTable.display tbody tr:first-child td { 88 | border-top: none; 89 | } 90 | table.dataTable.cell-border tbody th, table.dataTable.cell-border tbody td { 91 | border-top: 1px solid #ddd; 92 | border-right: 1px solid #ddd; 93 | } 94 | table.dataTable.cell-border tbody tr th:first-child, 95 | table.dataTable.cell-border tbody tr td:first-child { 96 | border-left: 1px solid #ddd; 97 | } 98 | table.dataTable.cell-border tbody tr:first-child th, 99 | table.dataTable.cell-border tbody tr:first-child td { 100 | border-top: none; 101 | } 102 | table.dataTable.stripe tbody tr.odd, table.dataTable.display tbody tr.odd { 103 | background-color: #f9f9f9; 104 | } 105 | table.dataTable.stripe tbody tr.odd.selected, table.dataTable.display tbody tr.odd.selected { 106 | background-color: #acbad4; 107 | } 108 | table.dataTable.hover tbody tr:hover, table.dataTable.display tbody tr:hover { 109 | background-color: #f6f6f6; 110 | } 111 | table.dataTable.hover tbody tr:hover.selected, table.dataTable.display tbody tr:hover.selected { 112 | background-color: #aab7d1; 113 | } 114 | table.dataTable.order-column tbody tr > .sorting_1, 115 | table.dataTable.order-column tbody tr > .sorting_2, 116 | table.dataTable.order-column tbody tr > .sorting_3, table.dataTable.display tbody tr > .sorting_1, 117 | table.dataTable.display tbody tr > .sorting_2, 118 | table.dataTable.display tbody tr > .sorting_3 { 119 | background-color: #fafafa; 120 | } 121 | table.dataTable.order-column tbody tr.selected > .sorting_1, 122 | table.dataTable.order-column tbody tr.selected > .sorting_2, 123 | table.dataTable.order-column tbody tr.selected > .sorting_3, table.dataTable.display tbody tr.selected > .sorting_1, 124 | table.dataTable.display tbody tr.selected > .sorting_2, 125 | table.dataTable.display tbody tr.selected > .sorting_3 { 126 | background-color: #acbad5; 127 | } 128 | table.dataTable.display tbody tr.odd > .sorting_1, table.dataTable.order-column.stripe tbody tr.odd > .sorting_1 { 129 | background-color: #f1f1f1; 130 | } 131 | table.dataTable.display tbody tr.odd > .sorting_2, table.dataTable.order-column.stripe tbody tr.odd > .sorting_2 { 132 | background-color: #f3f3f3; 133 | } 134 | table.dataTable.display tbody tr.odd > .sorting_3, table.dataTable.order-column.stripe tbody tr.odd > .sorting_3 { 135 | background-color: whitesmoke; 136 | } 137 | table.dataTable.display tbody tr.odd.selected > .sorting_1, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_1 { 138 | background-color: #a6b4cd; 139 | } 140 | table.dataTable.display tbody tr.odd.selected > .sorting_2, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_2 { 141 | background-color: #a8b5cf; 142 | } 143 | table.dataTable.display tbody tr.odd.selected > .sorting_3, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_3 { 144 | background-color: #a9b7d1; 145 | } 146 | table.dataTable.display tbody tr.even > .sorting_1, table.dataTable.order-column.stripe tbody tr.even > .sorting_1 { 147 | background-color: #fafafa; 148 | } 149 | table.dataTable.display tbody tr.even > .sorting_2, table.dataTable.order-column.stripe tbody tr.even > .sorting_2 { 150 | background-color: #fcfcfc; 151 | } 152 | table.dataTable.display tbody tr.even > .sorting_3, table.dataTable.order-column.stripe tbody tr.even > .sorting_3 { 153 | background-color: #fefefe; 154 | } 155 | table.dataTable.display tbody tr.even.selected > .sorting_1, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_1 { 156 | background-color: #acbad5; 157 | } 158 | table.dataTable.display tbody tr.even.selected > .sorting_2, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_2 { 159 | background-color: #aebcd6; 160 | } 161 | table.dataTable.display tbody tr.even.selected > .sorting_3, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_3 { 162 | background-color: #afbdd8; 163 | } 164 | table.dataTable.display tbody tr:hover > .sorting_1, table.dataTable.order-column.hover tbody tr:hover > .sorting_1 { 165 | background-color: #eaeaea; 166 | } 167 | table.dataTable.display tbody tr:hover > .sorting_2, table.dataTable.order-column.hover tbody tr:hover > .sorting_2 { 168 | background-color: #ececec; 169 | } 170 | table.dataTable.display tbody tr:hover > .sorting_3, table.dataTable.order-column.hover tbody tr:hover > .sorting_3 { 171 | background-color: #efefef; 172 | } 173 | table.dataTable.display tbody tr:hover.selected > .sorting_1, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_1 { 174 | background-color: #a2aec7; 175 | } 176 | table.dataTable.display tbody tr:hover.selected > .sorting_2, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_2 { 177 | background-color: #a3b0c9; 178 | } 179 | table.dataTable.display tbody tr:hover.selected > .sorting_3, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_3 { 180 | background-color: #a5b2cb; 181 | } 182 | table.dataTable.no-footer { 183 | border-bottom: 1px solid #111; 184 | } 185 | table.dataTable.nowrap th, table.dataTable.nowrap td { 186 | white-space: nowrap; 187 | } 188 | table.dataTable.compact thead th, 189 | table.dataTable.compact thead td { 190 | padding: 4px 17px; 191 | } 192 | table.dataTable.compact tfoot th, 193 | table.dataTable.compact tfoot td { 194 | padding: 4px; 195 | } 196 | table.dataTable.compact tbody th, 197 | table.dataTable.compact tbody td { 198 | padding: 4px; 199 | } 200 | table.dataTable th.dt-left, 201 | table.dataTable td.dt-left { 202 | text-align: left; 203 | } 204 | table.dataTable th.dt-center, 205 | table.dataTable td.dt-center, 206 | table.dataTable td.dataTables_empty { 207 | text-align: center; 208 | } 209 | table.dataTable th.dt-right, 210 | table.dataTable td.dt-right { 211 | text-align: right; 212 | } 213 | table.dataTable th.dt-justify, 214 | table.dataTable td.dt-justify { 215 | text-align: justify; 216 | } 217 | table.dataTable th.dt-nowrap, 218 | table.dataTable td.dt-nowrap { 219 | white-space: nowrap; 220 | } 221 | table.dataTable thead th.dt-head-left, 222 | table.dataTable thead td.dt-head-left, 223 | table.dataTable tfoot th.dt-head-left, 224 | table.dataTable tfoot td.dt-head-left { 225 | text-align: left; 226 | } 227 | table.dataTable thead th.dt-head-center, 228 | table.dataTable thead td.dt-head-center, 229 | table.dataTable tfoot th.dt-head-center, 230 | table.dataTable tfoot td.dt-head-center { 231 | text-align: center; 232 | } 233 | table.dataTable thead th.dt-head-right, 234 | table.dataTable thead td.dt-head-right, 235 | table.dataTable tfoot th.dt-head-right, 236 | table.dataTable tfoot td.dt-head-right { 237 | text-align: right; 238 | } 239 | table.dataTable thead th.dt-head-justify, 240 | table.dataTable thead td.dt-head-justify, 241 | table.dataTable tfoot th.dt-head-justify, 242 | table.dataTable tfoot td.dt-head-justify { 243 | text-align: justify; 244 | } 245 | table.dataTable thead th.dt-head-nowrap, 246 | table.dataTable thead td.dt-head-nowrap, 247 | table.dataTable tfoot th.dt-head-nowrap, 248 | table.dataTable tfoot td.dt-head-nowrap { 249 | white-space: nowrap; 250 | } 251 | table.dataTable tbody th.dt-body-left, 252 | table.dataTable tbody td.dt-body-left { 253 | text-align: left; 254 | } 255 | table.dataTable tbody th.dt-body-center, 256 | table.dataTable tbody td.dt-body-center { 257 | text-align: center; 258 | } 259 | table.dataTable tbody th.dt-body-right, 260 | table.dataTable tbody td.dt-body-right { 261 | text-align: right; 262 | } 263 | table.dataTable tbody th.dt-body-justify, 264 | table.dataTable tbody td.dt-body-justify { 265 | text-align: justify; 266 | } 267 | table.dataTable tbody th.dt-body-nowrap, 268 | table.dataTable tbody td.dt-body-nowrap { 269 | white-space: nowrap; 270 | } 271 | 272 | table.dataTable, 273 | table.dataTable th, 274 | table.dataTable td { 275 | box-sizing: content-box; 276 | } 277 | 278 | /* 279 | * Control feature layout 280 | */ 281 | .dataTables_wrapper { 282 | position: relative; 283 | clear: both; 284 | *zoom: 1; 285 | zoom: 1; 286 | } 287 | .dataTables_wrapper .dataTables_length { 288 | float: left; 289 | } 290 | .dataTables_wrapper .dataTables_filter { 291 | float: right; 292 | text-align: right; 293 | } 294 | .dataTables_wrapper .dataTables_filter input { 295 | margin-left: 0.5em; 296 | } 297 | .dataTables_wrapper .dataTables_info { 298 | clear: both; 299 | float: left; 300 | padding-top: 0.755em; 301 | } 302 | .dataTables_wrapper .dataTables_paginate { 303 | float: right; 304 | text-align: right; 305 | padding-top: 0.25em; 306 | } 307 | .dataTables_wrapper .dataTables_paginate .paginate_button { 308 | box-sizing: border-box; 309 | display: inline-block; 310 | min-width: 1.5em; 311 | padding: 0.5em 1em; 312 | margin-left: 2px; 313 | text-align: center; 314 | text-decoration: none !important; 315 | cursor: pointer; 316 | *cursor: hand; 317 | color: #333 !important; 318 | border: 1px solid transparent; 319 | border-radius: 2px; 320 | } 321 | .dataTables_wrapper .dataTables_paginate .paginate_button.current, .dataTables_wrapper .dataTables_paginate .paginate_button.current:hover { 322 | color: #333 !important; 323 | border: 1px solid #979797; 324 | background-color: white; 325 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, white), color-stop(100%, #dcdcdc)); 326 | /* Chrome,Safari4+ */ 327 | background: -webkit-linear-gradient(top, white 0%, #dcdcdc 100%); 328 | /* Chrome10+,Safari5.1+ */ 329 | background: -moz-linear-gradient(top, white 0%, #dcdcdc 100%); 330 | /* FF3.6+ */ 331 | background: -ms-linear-gradient(top, white 0%, #dcdcdc 100%); 332 | /* IE10+ */ 333 | background: -o-linear-gradient(top, white 0%, #dcdcdc 100%); 334 | /* Opera 11.10+ */ 335 | background: linear-gradient(to bottom, white 0%, #dcdcdc 100%); 336 | /* W3C */ 337 | } 338 | .dataTables_wrapper .dataTables_paginate .paginate_button.disabled, .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:hover, .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:active { 339 | cursor: default; 340 | color: #666 !important; 341 | border: 1px solid transparent; 342 | background: transparent; 343 | box-shadow: none; 344 | } 345 | .dataTables_wrapper .dataTables_paginate .paginate_button:hover { 346 | color: white !important; 347 | border: 1px solid #111; 348 | background-color: #585858; 349 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #585858), color-stop(100%, #111)); 350 | /* Chrome,Safari4+ */ 351 | background: -webkit-linear-gradient(top, #585858 0%, #111 100%); 352 | /* Chrome10+,Safari5.1+ */ 353 | background: -moz-linear-gradient(top, #585858 0%, #111 100%); 354 | /* FF3.6+ */ 355 | background: -ms-linear-gradient(top, #585858 0%, #111 100%); 356 | /* IE10+ */ 357 | background: -o-linear-gradient(top, #585858 0%, #111 100%); 358 | /* Opera 11.10+ */ 359 | background: linear-gradient(to bottom, #585858 0%, #111 100%); 360 | /* W3C */ 361 | } 362 | .dataTables_wrapper .dataTables_paginate .paginate_button:active { 363 | outline: none; 364 | background-color: #2b2b2b; 365 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #2b2b2b), color-stop(100%, #0c0c0c)); 366 | /* Chrome,Safari4+ */ 367 | background: -webkit-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%); 368 | /* Chrome10+,Safari5.1+ */ 369 | background: -moz-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%); 370 | /* FF3.6+ */ 371 | background: -ms-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%); 372 | /* IE10+ */ 373 | background: -o-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%); 374 | /* Opera 11.10+ */ 375 | background: linear-gradient(to bottom, #2b2b2b 0%, #0c0c0c 100%); 376 | /* W3C */ 377 | box-shadow: inset 0 0 3px #111; 378 | } 379 | .dataTables_wrapper .dataTables_paginate .ellipsis { 380 | padding: 0 1em; 381 | } 382 | .dataTables_wrapper .dataTables_processing { 383 | position: absolute; 384 | top: 50%; 385 | left: 50%; 386 | width: 100%; 387 | height: 40px; 388 | margin-left: -50%; 389 | margin-top: -25px; 390 | padding-top: 20px; 391 | text-align: center; 392 | font-size: 1.2em; 393 | background-color: white; 394 | background: -webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255, 255, 255, 0)), color-stop(25%, rgba(255, 255, 255, 0.9)), color-stop(75%, rgba(255, 255, 255, 0.9)), color-stop(100%, rgba(255, 255, 255, 0))); 395 | background: -webkit-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%); 396 | background: -moz-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%); 397 | background: -ms-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%); 398 | background: -o-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%); 399 | background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%); 400 | } 401 | .dataTables_wrapper .dataTables_length, 402 | .dataTables_wrapper .dataTables_filter, 403 | .dataTables_wrapper .dataTables_info, 404 | .dataTables_wrapper .dataTables_processing, 405 | .dataTables_wrapper .dataTables_paginate { 406 | color: #333; 407 | } 408 | .dataTables_wrapper .dataTables_scroll { 409 | clear: both; 410 | } 411 | .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody { 412 | *margin-top: -1px; 413 | -webkit-overflow-scrolling: touch; 414 | } 415 | .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > th, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > td, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > th, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > td { 416 | vertical-align: middle; 417 | } 418 | .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > th > div.dataTables_sizing, 419 | .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > td > div.dataTables_sizing, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > th > div.dataTables_sizing, 420 | .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > td > div.dataTables_sizing { 421 | height: 0; 422 | overflow: hidden; 423 | margin: 0 !important; 424 | padding: 0 !important; 425 | } 426 | .dataTables_wrapper.no-footer .dataTables_scrollBody { 427 | border-bottom: 1px solid #111; 428 | } 429 | .dataTables_wrapper.no-footer div.dataTables_scrollHead table.dataTable, 430 | .dataTables_wrapper.no-footer div.dataTables_scrollBody > table { 431 | border-bottom: none; 432 | } 433 | .dataTables_wrapper:after { 434 | visibility: hidden; 435 | display: block; 436 | content: ""; 437 | clear: both; 438 | height: 0; 439 | } 440 | 441 | @media screen and (max-width: 767px) { 442 | .dataTables_wrapper .dataTables_info, 443 | .dataTables_wrapper .dataTables_paginate { 444 | float: none; 445 | text-align: center; 446 | } 447 | .dataTables_wrapper .dataTables_paginate { 448 | margin-top: 0.5em; 449 | } 450 | } 451 | @media screen and (max-width: 640px) { 452 | .dataTables_wrapper .dataTables_length, 453 | .dataTables_wrapper .dataTables_filter { 454 | float: none; 455 | text-align: center; 456 | } 457 | .dataTables_wrapper .dataTables_filter { 458 | margin-top: 0.5em; 459 | } 460 | } 461 | 462 | 463 | table.fixedHeader-floating { 464 | position: fixed !important; 465 | background-color: white; 466 | } 467 | 468 | table.fixedHeader-floating.no-footer { 469 | border-bottom-width: 0; 470 | } 471 | 472 | table.fixedHeader-locked { 473 | position: absolute !important; 474 | background-color: white; 475 | } 476 | 477 | @media print { 478 | table.fixedHeader-floating { 479 | display: none; 480 | } 481 | } 482 | 483 | 484 | div.dtsp-topRow { 485 | display: flex; 486 | flex-direction: row; 487 | flex-wrap: nowrap; 488 | justify-content: space-around; 489 | align-content: flex-start; 490 | align-items: flex-start; 491 | } 492 | div.dtsp-topRow input.dtsp-search { 493 | text-overflow: ellipsis; 494 | } 495 | div.dtsp-topRow div.dtsp-subRow1 { 496 | display: flex; 497 | flex-direction: row; 498 | flex-wrap: nowrap; 499 | flex-grow: 1; 500 | flex-shrink: 0; 501 | flex-basis: 0; 502 | } 503 | div.dtsp-topRow div.dtsp-searchCont { 504 | display: flex; 505 | flex-direction: row; 506 | flex-wrap: nowrap; 507 | flex-grow: 1; 508 | flex-shrink: 0; 509 | flex-basis: 0; 510 | } 511 | div.dtsp-topRow button.dtsp-nameButton { 512 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACMAAAAjCAYAAAAe2bNZAAABcGlDQ1BpY2MAACiRdZHNSwJBGMYftTDS8FCHkA57sOigIAXRMQzyYh3UIKvL7rirwe66zK6IdA26dBA6RF36OvQf1DXoWhAERRAR9B/0dQnZ3nEFJXSG2ffHs/O8zDwD+DM6M+yBJGCYDs+mU9JaYV0KviNMM4QoEjKzreXcUh59x88jfKI+JESv/vt6jlBRtRngGyKeYxZ3iBeIMzXHErxHPMbKcpH4hDjO6YDEt0JXPH4TXPL4SzDPZxcBv+gplbpY6WJW5gbxNHHM0KusfR5xk7BqruaoRmlNwEYWaaQgQUEVW9DhIEHVpMx6+5It3woq5GH0tVAHJ0cJZfLGSa1SV5WqRrpKU0dd5P4/T1ubnfG6h1PA4Kvrfk4CwX2g2XDd31PXbZ4BgRfg2uz4K5TT/DfpjY4WOwYiO8DlTUdTDoCrXWD82ZK53JICtPyaBnxcACMFYPQeGN7wsmr/x/kTkN+mJ7oDDo+AKdof2fwDCBRoDkL8UccAAAAJcEhZcwAAD2EAAA9hAag/p2kAAAK2SURBVFgJ7ZY9j41BFICvryCExrJBQ6HyEYVEIREaUZDQIRoR2ViJKCioxV+gkVXYTVZEQiEUhG2EQnxUCh0FKolY4ut5XnM2cyfva3Pt5m7EPcmzZ2bemTNnzjkzd1utnvQi0IvAfxiBy5z5FoxO89kPY+8mbMjtzs47RXs5/WVpbAG6bWExt5PuIibvhVkwmC+ck3eK9ln6/fAddFojYzBVuYSBpcnIEvRaqOw2RcaN18FPuJH0JvRUxbT3wWf4ltiKPgfVidWlbGZgPozDFfgAC+EA/K2EI4cwcAJ+gPaeQ+VQU2SOMMGcPgPl/m/V2p50rrbRsRgt9Iv5h6xtpP22Bz7Ce1C+gFFxfKzOmShcU+Qmyh2w3w8rIJfddHTck66EukL/xPhj+JM8rHNmFys0Pg4v0up3aFNlwR9NYyodd3OL/C64zpsymcTFcf6ElM4YzjAWKYrJkaq8kE/yUYNP4BoYvS1QRo+hNtF5xfkTUjoTheukSFFMjlTFm6PjceOca/SMpKfeCR1L6Uzk/y2WIkVhNFJlJAZhP+hYns7b9D3IPuhY5mYrIv8OrQJvR5NYyNaW4jsU8pSGNySiVx4o5tXq3JkoXE/mg5R/M8dGJCJpKhaDcjBRdbI/Rm8g69c122om33BHmj2CHoV5qa9jUXBraJ+G1fAVjIBO1klc87ro1K4JZ/K35SWW3TwcyDd6TecqnAEd8cGq2+w84xvBm1n3vS0izKkkwh5XNC/GmFPqqAtPF89AOScKuemaNzoTV1SD5dtSbmLf1/RV+tC0WTgcj6R7HEtrVGWaqu/lYDZ/2pvxQ/kIyw/gFByHC9AHw910hv1aUUumyd8yy0QfhmEkfiNod0Xusct68J1qc8Tdux0Z97Q+hsDb+AYGYEbF/4Guw2Q/qDPqZG/zXgT+3Qj8AtKnfWhFwmuAAAAAAElFTkSuQmCC"); 513 | background-repeat: no-repeat; 514 | background-position: center; 515 | background-size: 23px; 516 | vertical-align: bottom; 517 | } 518 | div.dtsp-topRow button.dtsp-countButton { 519 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABcGlDQ1BpY2MAACiRdZHNSwJBGMYftTDS8FCHkA57sOigIAXRMQzyYh3UIKvL7rirwe66zK6IdA26dBA6RF36OvQf1DXoWhAERRAR9B/0dQnZ3nEFJXSG2ffHs/O8zDwD+DM6M+yBJGCYDs+mU9JaYV0KviNMM4QoEjKzreXcUh59x88jfKI+JESv/vt6jlBRtRngGyKeYxZ3iBeIMzXHErxHPMbKcpH4hDjO6YDEt0JXPH4TXPL4SzDPZxcBv+gplbpY6WJW5gbxNHHM0KusfR5xk7BqruaoRmlNwEYWaaQgQUEVW9DhIEHVpMx6+5It3woq5GH0tVAHJ0cJZfLGSa1SV5WqRrpKU0dd5P4/T1ubnfG6h1PA4Kvrfk4CwX2g2XDd31PXbZ4BgRfg2uz4K5TT/DfpjY4WOwYiO8DlTUdTDoCrXWD82ZK53JICtPyaBnxcACMFYPQeGN7wsmr/x/kTkN+mJ7oDDo+AKdof2fwDCBRoDkL8UccAAAAJcEhZcwAAD2EAAA9hAag/p2kAAAG5SURBVEgN3VU9LwVBFF0fiYhofUSlEQkKhU7z/oBCQkIiGr9BgUbhVzy9BAnhFyjV/AYFiU5ICM7ZN+c5Zud5dm3lJmfmzrkz9+7cu3c3y/6jjOBSF8CxXS7FmTkbwqIJjDpJvTcmsJ4K3KPZUpyZsx0sxoB9J6mnAkyC7wGuuCFIipNtEcpcWExgXpOBc78vgj6N+QO4NVsjwdFM59tUIDxDrHMBOeIQ34C5ZDregXuAQm4YcI68nN9B3wr2PcwPAIPkN2EqtJH6b+QZm1ajjTx7BqwAr26Lb+C2Kvpbt0Mb2HAJ7NrGFGfmXO3DeA4UshDfQAVmH0gaUFg852TTTDvlxwBlCtxy9zXyBhQFaq0wMmIdRebrfgosA3zb2hKnqG0oqchp4QbuR8X0TjzABhbdOT8jnQ/atcgqpnfwOA7yqZyTU587ZkIGdesLTt2EkynOnbreMUUKMI/dA4B/QVOcO13CQh+5wWCgDwo/75u59odB/wjmfhbgvACcAOyZPHihMWAoIwxyCLgf1oxfgjzVbgBXSTzIN+f0pg6s5DkcesLMRpsBrgE2XO3CN64JFP7JtUeKHX4CKtRRXFZ+7dEAAAAASUVORK5CYII="); 520 | background-repeat: no-repeat; 521 | background-position: center; 522 | background-size: 18px; 523 | vertical-align: bottom; 524 | } 525 | div.dtsp-topRow button.dtsp-searchIcon { 526 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAYAAAA71pVKAAABcGlDQ1BpY2MAACiRdZHNSwJBGMYftTDS8FCHkA57sOigIAXRMQzyYh3UIKvL7rirwe66zK6IdA26dBA6RF36OvQf1DXoWhAERRAR9B/0dQnZ3nEFJXSG2ffHs/O8zDwD+DM6M+yBJGCYDs+mU9JaYV0KviNMM4QoEjKzreXcUh59x88jfKI+JESv/vt6jlBRtRngGyKeYxZ3iBeIMzXHErxHPMbKcpH4hDjO6YDEt0JXPH4TXPL4SzDPZxcBv+gplbpY6WJW5gbxNHHM0KusfR5xk7BqruaoRmlNwEYWaaQgQUEVW9DhIEHVpMx6+5It3woq5GH0tVAHJ0cJZfLGSa1SV5WqRrpKU0dd5P4/T1ubnfG6h1PA4Kvrfk4CwX2g2XDd31PXbZ4BgRfg2uz4K5TT/DfpjY4WOwYiO8DlTUdTDoCrXWD82ZK53JICtPyaBnxcACMFYPQeGN7wsmr/x/kTkN+mJ7oDDo+AKdof2fwDCBRoDkL8UccAAAAJcEhZcwAAD2EAAA9hAag/p2kAAAEnSURBVCgVpdG7SgNBFIDh1RhJsBBEsDIgIhaWFjZa2GtpKb6AnU0MprKOWEjK2IuFFxCxS2lhZyOWXh5AQVER/X+zuwwywoIHvp3dM3Nm55Ik/4i+P2or5FewiBIe0cEt8ogVz9LbhEVf+cgkcew1tvAZ5PPXGm9HOMEanMAYQhunaCAazuqA1UjvILl9HGPc/n4fabjPGbzjMM2FjfkDuPw5O8JilzgA9/OKWDynyWnbsPiF7yc4SRWxmEyTN7ZhsSd7gTLW8TuGSSzBcZd2hsV+n+MNC9jGCNzjPDwsz8XCO/x02Bqeptcxhg+4gjD8YxetLOkBGRbuwcIr+NdRLMPl3uMM2YHx2gsLd+D97qKEQuGe65jCAzbgVRWOCUZuovAfs5m/AdVxL0R1AIsLAAAAAElFTkSuQmCC"); 527 | background-repeat: no-repeat; 528 | background-position: center; 529 | background-size: 12px; 530 | } 531 | 532 | div.dt-button-collection { 533 | z-index: 2002; 534 | } 535 | 536 | div.dataTables_scrollBody { 537 | background: white !important; 538 | } 539 | 540 | div.dtsp-columns-1 { 541 | min-width: 98%; 542 | max-width: 98%; 543 | padding-left: 1%; 544 | padding-right: 1%; 545 | margin: 0px !important; 546 | } 547 | 548 | div.dtsp-columns-2 { 549 | min-width: 48%; 550 | max-width: 48%; 551 | padding-left: 1%; 552 | padding-right: 1%; 553 | margin: 0px !important; 554 | } 555 | 556 | div.dtsp-columns-3 { 557 | min-width: 30.333%; 558 | max-width: 30.333%; 559 | padding-left: 1%; 560 | padding-right: 1%; 561 | margin: 0px !important; 562 | } 563 | 564 | div.dtsp-columns-4 { 565 | min-width: 23%; 566 | max-width: 23%; 567 | padding-left: 1%; 568 | padding-right: 1%; 569 | margin: 0px !important; 570 | } 571 | 572 | div.dtsp-columns-5 { 573 | min-width: 18%; 574 | max-width: 18%; 575 | padding-left: 1%; 576 | padding-right: 1%; 577 | margin: 0px !important; 578 | } 579 | 580 | div.dtsp-columns-6 { 581 | min-width: 15.666%; 582 | max-width: 15.666%; 583 | padding-left: 0.5%; 584 | padding-right: 0.5%; 585 | margin: 0px !important; 586 | } 587 | 588 | div.dtsp-columns-7 { 589 | min-width: 13.28%; 590 | max-width: 13.28%; 591 | padding-left: 0.5%; 592 | padding-right: 0.5%; 593 | margin: 0px !important; 594 | } 595 | 596 | div.dtsp-columns-8 { 597 | min-width: 11.5%; 598 | max-width: 11.5%; 599 | padding-left: 0.5%; 600 | padding-right: 0.5%; 601 | margin: 0px !important; 602 | } 603 | 604 | div.dtsp-columns-9 { 605 | min-width: 11.111%; 606 | max-width: 11.111%; 607 | padding-left: 0.5%; 608 | padding-right: 0.5%; 609 | margin: 0px !important; 610 | } 611 | 612 | div.dt-button-collection { 613 | float: none; 614 | } 615 | 616 | div.dtsp-panesContainer { 617 | width: 100%; 618 | } 619 | 620 | div.dtsp-panesContainer { 621 | font-family: 'Roboto', sans-serif; 622 | padding: 5px; 623 | border: 1px solid #ccc; 624 | border-radius: 6px; 625 | margin: 5px 0; 626 | clear: both; 627 | text-align: center; 628 | } 629 | div.dtsp-panesContainer div.dtsp-searchPanes { 630 | display: flex; 631 | flex-direction: row; 632 | flex-wrap: wrap; 633 | justify-content: flex-start; 634 | align-content: flex-start; 635 | align-items: stretch; 636 | clear: both; 637 | text-align: start; 638 | } 639 | div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-hidden { 640 | display: none !important; 641 | } 642 | div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane { 643 | flex-direction: row; 644 | flex-wrap: nowrap; 645 | flex-grow: 1; 646 | flex-shrink: 0; 647 | flex-basis: 280px; 648 | justify-content: space-around; 649 | align-content: flex-start; 650 | align-items: stretch; 651 | padding-top: 0px; 652 | padding-bottom: 0px; 653 | margin: 5px; 654 | margin-top: 0px; 655 | margin-bottom: 0px; 656 | font-size: 0.9em; 657 | } 658 | div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dataTables_wrapper { 659 | flex: 1; 660 | margin: 1em 0.5%; 661 | margin-top: 0px; 662 | border-bottom: 2px solid #f0f0f0; 663 | border: 2px solid #f0f0f0; 664 | border-radius: 4px; 665 | } 666 | div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dataTables_wrapper:hover { 667 | border: 2px solid #cfcfcf; 668 | } 669 | div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dataTables_wrapper div.dataTables_filter { 670 | display: none; 671 | } 672 | div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dtsp-selected { 673 | border: 2px solid #3276b1; 674 | border-radius: 4px; 675 | } 676 | div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dtsp-selected:hover { 677 | border: 2px solid #286092; 678 | } 679 | div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dtsp-topRow { 680 | display: flex; 681 | flex-direction: row; 682 | flex-wrap: nowrap; 683 | justify-content: space-around; 684 | align-content: flex-start; 685 | align-items: flex-start; 686 | } 687 | div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dtsp-topRow div.dtsp-searchCont { 688 | display: flex; 689 | flex-direction: row; 690 | flex-wrap: nowrap; 691 | flex-grow: 1; 692 | flex-shrink: 0; 693 | flex-basis: 0; 694 | } 695 | div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dtsp-topRow div.dtsp-searchCont input.dtsp-search { 696 | flex-direction: row; 697 | flex-wrap: nowrap; 698 | flex-grow: 1; 699 | flex-shrink: 0; 700 | flex-basis: 90px; 701 | min-height: 20px; 702 | max-width: none; 703 | min-width: 50px; 704 | border: none; 705 | padding-left: 12px; 706 | } 707 | div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dtsp-topRow div.dtsp-searchCont input.dtsp-search::placeholder { 708 | color: black; 709 | font-size: 16px; 710 | } 711 | div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dtsp-topRow div.dtsp-searchCont div.dtsp-searchButtonCont { 712 | display: inline-block; 713 | flex-direction: row; 714 | flex-wrap: nowrap; 715 | flex-grow: 0; 716 | flex-shrink: 0; 717 | flex-basis: 0; 718 | } 719 | div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dtsp-topRow div.dtsp-searchCont div.dtsp-searchButtonCont .dtsp-searchIcon { 720 | position: relative; 721 | display: inline-block; 722 | margin: 4px; 723 | display: block; 724 | top: -2px; 725 | right: 0px; 726 | font-size: 16px; 727 | margin-bottom: 0px; 728 | } 729 | div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dtsp-topRow button.dtsp-dull { 730 | cursor: context-menu !important; 731 | color: #7c7c7c; 732 | } 733 | div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dtsp-topRow button.dtsp-dull:hover { 734 | background-color: transparent; 735 | } 736 | div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane input.dtsp-paneInputButton, div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane button.dtsp-paneButton { 737 | height: 35px; 738 | width: 35px; 739 | max-width: 35px; 740 | min-width: 0; 741 | display: inline-block; 742 | margin: 2px; 743 | border: 0px solid transparent; 744 | background-color: transparent; 745 | font-size: 16px; 746 | margin-bottom: 0px; 747 | flex-grow: 0; 748 | flex-shrink: 0; 749 | flex-basis: 35px; 750 | font-family: sans-serif; 751 | } 752 | div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane input.dtsp-paneInputButton:hover, div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane button.dtsp-paneButton:hover { 753 | background-color: #f0f0f0; 754 | border-radius: 2px; 755 | cursor: pointer; 756 | } 757 | div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane button.dtsp-paneButton { 758 | opacity: 0.6; 759 | } 760 | div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane input.dtsp-disabledButton { 761 | height: 35px; 762 | width: 35px; 763 | max-width: 35px; 764 | min-width: 0; 765 | display: inline-block; 766 | margin: 2px; 767 | border: 0px solid transparent; 768 | background-color: transparent; 769 | font-size: 16px; 770 | margin-bottom: 0px; 771 | flex-grow: 0; 772 | flex-shrink: 0; 773 | flex-basis: 35px; 774 | } 775 | div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dataTables_scrollHead { 776 | display: none !important; 777 | } 778 | div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dataTables_scrollBody { 779 | border-bottom: none; 780 | } 781 | div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dataTables_scrollBody td.dtsp-countColumn { 782 | text-align: right; 783 | } 784 | div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dataTables_scrollBody td.dtsp-countColumn div.dtsp-pill { 785 | background-color: #cfcfcf; 786 | text-align: center; 787 | border: 1px solid #cfcfcf; 788 | border-radius: 10px; 789 | width: auto; 790 | display: inline-block; 791 | min-width: 30px; 792 | color: black; 793 | font-size: 0.9em; 794 | padding: 0 4px; 795 | } 796 | div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dataTables_scrollBody span.dtsp-pill { 797 | float: right; 798 | background-color: #cfcfcf; 799 | text-align: center; 800 | border: 1px solid #cfcfcf; 801 | border-radius: 10px; 802 | width: auto; 803 | min-width: 30px; 804 | color: black; 805 | font-size: 0.9em; 806 | padding: 0 4px; 807 | } 808 | div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane tr > th, 809 | div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane tr > td { 810 | padding: 5px 10px; 811 | } 812 | div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane td.dtsp-countColumn { 813 | text-align: right; 814 | } 815 | div.dtsp-panesContainer div.dtsp-title { 816 | float: left; 817 | margin: 20px; 818 | margin-bottom: 0px; 819 | margin-top: 13px; 820 | } 821 | div.dtsp-panesContainer button.dtsp-clearAll { 822 | float: right; 823 | margin: 20px; 824 | border: 1px solid transparent; 825 | background-color: transparent; 826 | padding: 10px; 827 | margin-bottom: 0px; 828 | margin-top: 5px; 829 | } 830 | div.dtsp-panesContainer button.dtsp-clearAll:hover { 831 | background-color: #f0f0f0; 832 | border-radius: 2px; 833 | } 834 | 835 | div.dt-button-collection div.panes { 836 | padding: 0px; 837 | border: none; 838 | margin: 0px; 839 | } 840 | 841 | div.dtsp-hidden { 842 | display: none !important; 843 | } 844 | 845 | div.dtsp-narrow { 846 | flex-direction: column !important; 847 | } 848 | div.dtsp-narrow div.dtsp-subRows { 849 | width: 100%; 850 | text-align: right; 851 | } 852 | 853 | @media screen and (max-width: 767px) { 854 | div.dtsp-columns-4, 855 | div.dtsp-columns-5, 856 | div.dtsp-columns-6 { 857 | max-width: 31% !important; 858 | min-width: 31% !important; 859 | } 860 | } 861 | @media screen and (max-width: 640px) { 862 | div.dtsp-searchPanes { 863 | flex-direction: column !important; 864 | } 865 | 866 | div.dtsp-searchPane { 867 | max-width: 98% !important; 868 | min-width: 98% !important; 869 | } 870 | } 871 | 872 | 873 | table.dataTable tbody > tr.selected, 874 | table.dataTable tbody > tr > .selected { 875 | background-color: #B0BED9; 876 | } 877 | table.dataTable.stripe tbody > tr.odd.selected, 878 | table.dataTable.stripe tbody > tr.odd > .selected, table.dataTable.display tbody > tr.odd.selected, 879 | table.dataTable.display tbody > tr.odd > .selected { 880 | background-color: #acbad4; 881 | } 882 | table.dataTable.hover tbody > tr.selected:hover, 883 | table.dataTable.hover tbody > tr > .selected:hover, table.dataTable.display tbody > tr.selected:hover, 884 | table.dataTable.display tbody > tr > .selected:hover { 885 | background-color: #aab7d1; 886 | } 887 | table.dataTable.order-column tbody > tr.selected > .sorting_1, 888 | table.dataTable.order-column tbody > tr.selected > .sorting_2, 889 | table.dataTable.order-column tbody > tr.selected > .sorting_3, 890 | table.dataTable.order-column tbody > tr > .selected, table.dataTable.display tbody > tr.selected > .sorting_1, 891 | table.dataTable.display tbody > tr.selected > .sorting_2, 892 | table.dataTable.display tbody > tr.selected > .sorting_3, 893 | table.dataTable.display tbody > tr > .selected { 894 | background-color: #acbad5; 895 | } 896 | table.dataTable.display tbody > tr.odd.selected > .sorting_1, table.dataTable.order-column.stripe tbody > tr.odd.selected > .sorting_1 { 897 | background-color: #a6b4cd; 898 | } 899 | table.dataTable.display tbody > tr.odd.selected > .sorting_2, table.dataTable.order-column.stripe tbody > tr.odd.selected > .sorting_2 { 900 | background-color: #a8b5cf; 901 | } 902 | table.dataTable.display tbody > tr.odd.selected > .sorting_3, table.dataTable.order-column.stripe tbody > tr.odd.selected > .sorting_3 { 903 | background-color: #a9b7d1; 904 | } 905 | table.dataTable.display tbody > tr.even.selected > .sorting_1, table.dataTable.order-column.stripe tbody > tr.even.selected > .sorting_1 { 906 | background-color: #acbad5; 907 | } 908 | table.dataTable.display tbody > tr.even.selected > .sorting_2, table.dataTable.order-column.stripe tbody > tr.even.selected > .sorting_2 { 909 | background-color: #aebcd6; 910 | } 911 | table.dataTable.display tbody > tr.even.selected > .sorting_3, table.dataTable.order-column.stripe tbody > tr.even.selected > .sorting_3 { 912 | background-color: #afbdd8; 913 | } 914 | table.dataTable.display tbody > tr.odd > .selected, table.dataTable.order-column.stripe tbody > tr.odd > .selected { 915 | background-color: #a6b4cd; 916 | } 917 | table.dataTable.display tbody > tr.even > .selected, table.dataTable.order-column.stripe tbody > tr.even > .selected { 918 | background-color: #acbad5; 919 | } 920 | table.dataTable.display tbody > tr.selected:hover > .sorting_1, table.dataTable.order-column.hover tbody > tr.selected:hover > .sorting_1 { 921 | background-color: #a2aec7; 922 | } 923 | table.dataTable.display tbody > tr.selected:hover > .sorting_2, table.dataTable.order-column.hover tbody > tr.selected:hover > .sorting_2 { 924 | background-color: #a3b0c9; 925 | } 926 | table.dataTable.display tbody > tr.selected:hover > .sorting_3, table.dataTable.order-column.hover tbody > tr.selected:hover > .sorting_3 { 927 | background-color: #a5b2cb; 928 | } 929 | table.dataTable.display tbody > tr:hover > .selected, 930 | table.dataTable.display tbody > tr > .selected:hover, table.dataTable.order-column.hover tbody > tr:hover > .selected, 931 | table.dataTable.order-column.hover tbody > tr > .selected:hover { 932 | background-color: #a2aec7; 933 | } 934 | table.dataTable tbody td.select-checkbox, 935 | table.dataTable tbody th.select-checkbox { 936 | position: relative; 937 | } 938 | table.dataTable tbody td.select-checkbox:before, table.dataTable tbody td.select-checkbox:after, 939 | table.dataTable tbody th.select-checkbox:before, 940 | table.dataTable tbody th.select-checkbox:after { 941 | display: block; 942 | position: absolute; 943 | top: 1.2em; 944 | left: 50%; 945 | width: 12px; 946 | height: 12px; 947 | box-sizing: border-box; 948 | } 949 | table.dataTable tbody td.select-checkbox:before, 950 | table.dataTable tbody th.select-checkbox:before { 951 | content: ' '; 952 | margin-top: -6px; 953 | margin-left: -6px; 954 | border: 1px solid black; 955 | border-radius: 3px; 956 | } 957 | table.dataTable tr.selected td.select-checkbox:after, 958 | table.dataTable tr.selected th.select-checkbox:after { 959 | content: '\2714'; 960 | margin-top: -11px; 961 | margin-left: -4px; 962 | text-align: center; 963 | text-shadow: 1px 1px #B0BED9, -1px -1px #B0BED9, 1px -1px #B0BED9, -1px 1px #B0BED9; 964 | } 965 | 966 | div.dataTables_wrapper span.select-info, 967 | div.dataTables_wrapper span.select-item { 968 | margin-left: 0.5em; 969 | } 970 | 971 | @media screen and (max-width: 640px) { 972 | div.dataTables_wrapper span.select-info, 973 | div.dataTables_wrapper span.select-item { 974 | margin-left: 0; 975 | display: block; 976 | } 977 | } 978 | 979 | 980 | -------------------------------------------------------------------------------- /www/datatables/datatables.min.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This combined file was created by the DataTables downloader builder: 3 | * https://datatables.net/download 4 | * 5 | * To rebuild or modify this file with the latest versions of the included 6 | * software please visit: 7 | * https://datatables.net/download/#dt/dt-1.10.21/fh-3.1.7/sp-1.1.1/sl-1.3.1 8 | * 9 | * Included libraries: 10 | * DataTables 1.10.21, FixedHeader 3.1.7, SearchPanes 1.1.1, Select 1.3.1 11 | */ 12 | 13 | table.dataTable{width:100%;margin:0 auto;clear:both;border-collapse:separate;border-spacing:0}table.dataTable thead th,table.dataTable tfoot th{font-weight:bold}table.dataTable thead th,table.dataTable thead td{padding:10px 18px;border-bottom:1px solid #111}table.dataTable thead th:active,table.dataTable thead td:active{outline:none}table.dataTable tfoot th,table.dataTable tfoot td{padding:10px 18px 6px 18px;border-top:1px solid #111}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_desc,table.dataTable thead .sorting_asc_disabled,table.dataTable thead .sorting_desc_disabled{cursor:pointer;*cursor:hand;background-repeat:no-repeat;background-position:center right}table.dataTable thead .sorting{background-image:url("DataTables-1.10.21/images/sort_both.png")}table.dataTable thead .sorting_asc{background-image:url("DataTables-1.10.21/images/sort_asc.png")}table.dataTable thead .sorting_desc{background-image:url("DataTables-1.10.21/images/sort_desc.png")}table.dataTable thead .sorting_asc_disabled{background-image:url("DataTables-1.10.21/images/sort_asc_disabled.png")}table.dataTable thead .sorting_desc_disabled{background-image:url("DataTables-1.10.21/images/sort_desc_disabled.png")}table.dataTable tbody tr{background-color:#ffffff}table.dataTable tbody tr.selected{background-color:#B0BED9}table.dataTable tbody th,table.dataTable tbody td{padding:8px 10px}table.dataTable.row-border tbody th,table.dataTable.row-border tbody td,table.dataTable.display tbody th,table.dataTable.display tbody td{border-top:1px solid #ddd}table.dataTable.row-border tbody tr:first-child th,table.dataTable.row-border tbody tr:first-child td,table.dataTable.display tbody tr:first-child th,table.dataTable.display tbody tr:first-child td{border-top:none}table.dataTable.cell-border tbody th,table.dataTable.cell-border tbody td{border-top:1px solid #ddd;border-right:1px solid #ddd}table.dataTable.cell-border tbody tr th:first-child,table.dataTable.cell-border tbody tr td:first-child{border-left:1px solid #ddd}table.dataTable.cell-border tbody tr:first-child th,table.dataTable.cell-border tbody tr:first-child td{border-top:none}table.dataTable.stripe tbody tr.odd,table.dataTable.display tbody tr.odd{background-color:#f9f9f9}table.dataTable.stripe tbody tr.odd.selected,table.dataTable.display tbody tr.odd.selected{background-color:#acbad4}table.dataTable.hover tbody tr:hover,table.dataTable.display tbody tr:hover{background-color:#f6f6f6}table.dataTable.hover tbody tr:hover.selected,table.dataTable.display tbody tr:hover.selected{background-color:#aab7d1}table.dataTable.order-column tbody tr>.sorting_1,table.dataTable.order-column tbody tr>.sorting_2,table.dataTable.order-column tbody tr>.sorting_3,table.dataTable.display tbody tr>.sorting_1,table.dataTable.display tbody tr>.sorting_2,table.dataTable.display tbody tr>.sorting_3{background-color:#fafafa}table.dataTable.order-column tbody tr.selected>.sorting_1,table.dataTable.order-column tbody tr.selected>.sorting_2,table.dataTable.order-column tbody tr.selected>.sorting_3,table.dataTable.display tbody tr.selected>.sorting_1,table.dataTable.display tbody tr.selected>.sorting_2,table.dataTable.display tbody tr.selected>.sorting_3{background-color:#acbad5}table.dataTable.display tbody tr.odd>.sorting_1,table.dataTable.order-column.stripe tbody tr.odd>.sorting_1{background-color:#f1f1f1}table.dataTable.display tbody tr.odd>.sorting_2,table.dataTable.order-column.stripe tbody tr.odd>.sorting_2{background-color:#f3f3f3}table.dataTable.display tbody tr.odd>.sorting_3,table.dataTable.order-column.stripe tbody tr.odd>.sorting_3{background-color:whitesmoke}table.dataTable.display tbody tr.odd.selected>.sorting_1,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_1{background-color:#a6b4cd}table.dataTable.display tbody tr.odd.selected>.sorting_2,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_2{background-color:#a8b5cf}table.dataTable.display tbody tr.odd.selected>.sorting_3,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_3{background-color:#a9b7d1}table.dataTable.display tbody tr.even>.sorting_1,table.dataTable.order-column.stripe tbody tr.even>.sorting_1{background-color:#fafafa}table.dataTable.display tbody tr.even>.sorting_2,table.dataTable.order-column.stripe tbody tr.even>.sorting_2{background-color:#fcfcfc}table.dataTable.display tbody tr.even>.sorting_3,table.dataTable.order-column.stripe tbody tr.even>.sorting_3{background-color:#fefefe}table.dataTable.display tbody tr.even.selected>.sorting_1,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_1{background-color:#acbad5}table.dataTable.display tbody tr.even.selected>.sorting_2,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_2{background-color:#aebcd6}table.dataTable.display tbody tr.even.selected>.sorting_3,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_3{background-color:#afbdd8}table.dataTable.display tbody tr:hover>.sorting_1,table.dataTable.order-column.hover tbody tr:hover>.sorting_1{background-color:#eaeaea}table.dataTable.display tbody tr:hover>.sorting_2,table.dataTable.order-column.hover tbody tr:hover>.sorting_2{background-color:#ececec}table.dataTable.display tbody tr:hover>.sorting_3,table.dataTable.order-column.hover tbody tr:hover>.sorting_3{background-color:#efefef}table.dataTable.display tbody tr:hover.selected>.sorting_1,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_1{background-color:#a2aec7}table.dataTable.display tbody tr:hover.selected>.sorting_2,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_2{background-color:#a3b0c9}table.dataTable.display tbody tr:hover.selected>.sorting_3,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_3{background-color:#a5b2cb}table.dataTable.no-footer{border-bottom:1px solid #111}table.dataTable.nowrap th,table.dataTable.nowrap td{white-space:nowrap}table.dataTable.compact thead th,table.dataTable.compact thead td{padding:4px 17px}table.dataTable.compact tfoot th,table.dataTable.compact tfoot td{padding:4px}table.dataTable.compact tbody th,table.dataTable.compact tbody td{padding:4px}table.dataTable th.dt-left,table.dataTable td.dt-left{text-align:left}table.dataTable th.dt-center,table.dataTable td.dt-center,table.dataTable td.dataTables_empty{text-align:center}table.dataTable th.dt-right,table.dataTable td.dt-right{text-align:right}table.dataTable th.dt-justify,table.dataTable td.dt-justify{text-align:justify}table.dataTable th.dt-nowrap,table.dataTable td.dt-nowrap{white-space:nowrap}table.dataTable thead th.dt-head-left,table.dataTable thead td.dt-head-left,table.dataTable tfoot th.dt-head-left,table.dataTable tfoot td.dt-head-left{text-align:left}table.dataTable thead th.dt-head-center,table.dataTable thead td.dt-head-center,table.dataTable tfoot th.dt-head-center,table.dataTable tfoot td.dt-head-center{text-align:center}table.dataTable thead th.dt-head-right,table.dataTable thead td.dt-head-right,table.dataTable tfoot th.dt-head-right,table.dataTable tfoot td.dt-head-right{text-align:right}table.dataTable thead th.dt-head-justify,table.dataTable thead td.dt-head-justify,table.dataTable tfoot th.dt-head-justify,table.dataTable tfoot td.dt-head-justify{text-align:justify}table.dataTable thead th.dt-head-nowrap,table.dataTable thead td.dt-head-nowrap,table.dataTable tfoot th.dt-head-nowrap,table.dataTable tfoot td.dt-head-nowrap{white-space:nowrap}table.dataTable tbody th.dt-body-left,table.dataTable tbody td.dt-body-left{text-align:left}table.dataTable tbody th.dt-body-center,table.dataTable tbody td.dt-body-center{text-align:center}table.dataTable tbody th.dt-body-right,table.dataTable tbody td.dt-body-right{text-align:right}table.dataTable tbody th.dt-body-justify,table.dataTable tbody td.dt-body-justify{text-align:justify}table.dataTable tbody th.dt-body-nowrap,table.dataTable tbody td.dt-body-nowrap{white-space:nowrap}table.dataTable,table.dataTable th,table.dataTable td{box-sizing:content-box}.dataTables_wrapper{position:relative;clear:both;*zoom:1;zoom:1}.dataTables_wrapper .dataTables_length{float:left}.dataTables_wrapper .dataTables_filter{float:right;text-align:right}.dataTables_wrapper .dataTables_filter input{margin-left:0.5em}.dataTables_wrapper .dataTables_info{clear:both;float:left;padding-top:0.755em}.dataTables_wrapper .dataTables_paginate{float:right;text-align:right;padding-top:0.25em}.dataTables_wrapper .dataTables_paginate .paginate_button{box-sizing:border-box;display:inline-block;min-width:1.5em;padding:0.5em 1em;margin-left:2px;text-align:center;text-decoration:none !important;cursor:pointer;*cursor:hand;color:#333 !important;border:1px solid transparent;border-radius:2px}.dataTables_wrapper .dataTables_paginate .paginate_button.current,.dataTables_wrapper .dataTables_paginate .paginate_button.current:hover{color:#333 !important;border:1px solid #979797;background-color:white;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #fff), color-stop(100%, #dcdcdc));background:-webkit-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:-moz-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:-ms-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:-o-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:linear-gradient(to bottom, #fff 0%, #dcdcdc 100%)}.dataTables_wrapper .dataTables_paginate .paginate_button.disabled,.dataTables_wrapper .dataTables_paginate .paginate_button.disabled:hover,.dataTables_wrapper .dataTables_paginate .paginate_button.disabled:active{cursor:default;color:#666 !important;border:1px solid transparent;background:transparent;box-shadow:none}.dataTables_wrapper .dataTables_paginate .paginate_button:hover{color:white !important;border:1px solid #111;background-color:#585858;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #585858), color-stop(100%, #111));background:-webkit-linear-gradient(top, #585858 0%, #111 100%);background:-moz-linear-gradient(top, #585858 0%, #111 100%);background:-ms-linear-gradient(top, #585858 0%, #111 100%);background:-o-linear-gradient(top, #585858 0%, #111 100%);background:linear-gradient(to bottom, #585858 0%, #111 100%)}.dataTables_wrapper .dataTables_paginate .paginate_button:active{outline:none;background-color:#2b2b2b;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #2b2b2b), color-stop(100%, #0c0c0c));background:-webkit-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-moz-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-ms-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-o-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:linear-gradient(to bottom, #2b2b2b 0%, #0c0c0c 100%);box-shadow:inset 0 0 3px #111}.dataTables_wrapper .dataTables_paginate .ellipsis{padding:0 1em}.dataTables_wrapper .dataTables_processing{position:absolute;top:50%;left:50%;width:100%;height:40px;margin-left:-50%;margin-top:-25px;padding-top:20px;text-align:center;font-size:1.2em;background-color:white;background:-webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255,255,255,0)), color-stop(25%, rgba(255,255,255,0.9)), color-stop(75%, rgba(255,255,255,0.9)), color-stop(100%, rgba(255,255,255,0)));background:-webkit-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:-moz-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:-ms-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:-o-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:linear-gradient(to right, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%)}.dataTables_wrapper .dataTables_length,.dataTables_wrapper .dataTables_filter,.dataTables_wrapper .dataTables_info,.dataTables_wrapper .dataTables_processing,.dataTables_wrapper .dataTables_paginate{color:#333}.dataTables_wrapper .dataTables_scroll{clear:both}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody{*margin-top:-1px;-webkit-overflow-scrolling:touch}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>th,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>td,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>th,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>td{vertical-align:middle}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>th>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>td>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>th>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>td>div.dataTables_sizing{height:0;overflow:hidden;margin:0 !important;padding:0 !important}.dataTables_wrapper.no-footer .dataTables_scrollBody{border-bottom:1px solid #111}.dataTables_wrapper.no-footer div.dataTables_scrollHead table.dataTable,.dataTables_wrapper.no-footer div.dataTables_scrollBody>table{border-bottom:none}.dataTables_wrapper:after{visibility:hidden;display:block;content:"";clear:both;height:0}@media screen and (max-width: 767px){.dataTables_wrapper .dataTables_info,.dataTables_wrapper .dataTables_paginate{float:none;text-align:center}.dataTables_wrapper .dataTables_paginate{margin-top:0.5em}}@media screen and (max-width: 640px){.dataTables_wrapper .dataTables_length,.dataTables_wrapper .dataTables_filter{float:none;text-align:center}.dataTables_wrapper .dataTables_filter{margin-top:0.5em}} 14 | 15 | 16 | table.fixedHeader-floating{position:fixed !important;background-color:white}table.fixedHeader-floating.no-footer{border-bottom-width:0}table.fixedHeader-locked{position:absolute !important;background-color:white}@media print{table.fixedHeader-floating{display:none}} 17 | 18 | 19 | div.dtsp-topRow{display:flex;flex-direction:row;flex-wrap:nowrap;justify-content:space-around;align-content:flex-start;align-items:flex-start}div.dtsp-topRow input.dtsp-search{text-overflow:ellipsis}div.dtsp-topRow div.dtsp-subRow1{display:flex;flex-direction:row;flex-wrap:nowrap;flex-grow:1;flex-shrink:0;flex-basis:0}div.dtsp-topRow div.dtsp-searchCont{display:flex;flex-direction:row;flex-wrap:nowrap;flex-grow:1;flex-shrink:0;flex-basis:0}div.dtsp-topRow button.dtsp-nameButton{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACMAAAAjCAYAAAAe2bNZAAABcGlDQ1BpY2MAACiRdZHNSwJBGMYftTDS8FCHkA57sOigIAXRMQzyYh3UIKvL7rirwe66zK6IdA26dBA6RF36OvQf1DXoWhAERRAR9B/0dQnZ3nEFJXSG2ffHs/O8zDwD+DM6M+yBJGCYDs+mU9JaYV0KviNMM4QoEjKzreXcUh59x88jfKI+JESv/vt6jlBRtRngGyKeYxZ3iBeIMzXHErxHPMbKcpH4hDjO6YDEt0JXPH4TXPL4SzDPZxcBv+gplbpY6WJW5gbxNHHM0KusfR5xk7BqruaoRmlNwEYWaaQgQUEVW9DhIEHVpMx6+5It3woq5GH0tVAHJ0cJZfLGSa1SV5WqRrpKU0dd5P4/T1ubnfG6h1PA4Kvrfk4CwX2g2XDd31PXbZ4BgRfg2uz4K5TT/DfpjY4WOwYiO8DlTUdTDoCrXWD82ZK53JICtPyaBnxcACMFYPQeGN7wsmr/x/kTkN+mJ7oDDo+AKdof2fwDCBRoDkL8UccAAAAJcEhZcwAAD2EAAA9hAag/p2kAAAK2SURBVFgJ7ZY9j41BFICvryCExrJBQ6HyEYVEIREaUZDQIRoR2ViJKCioxV+gkVXYTVZEQiEUhG2EQnxUCh0FKolY4ut5XnM2cyfva3Pt5m7EPcmzZ2bemTNnzjkzd1utnvQi0IvAfxiBy5z5FoxO89kPY+8mbMjtzs47RXs5/WVpbAG6bWExt5PuIibvhVkwmC+ck3eK9ln6/fAddFojYzBVuYSBpcnIEvRaqOw2RcaN18FPuJH0JvRUxbT3wWf4ltiKPgfVidWlbGZgPozDFfgAC+EA/K2EI4cwcAJ+gPaeQ+VQU2SOMMGcPgPl/m/V2p50rrbRsRgt9Iv5h6xtpP22Bz7Ce1C+gFFxfKzOmShcU+Qmyh2w3w8rIJfddHTck66EukL/xPhj+JM8rHNmFys0Pg4v0up3aFNlwR9NYyodd3OL/C64zpsymcTFcf6ElM4YzjAWKYrJkaq8kE/yUYNP4BoYvS1QRo+hNtF5xfkTUjoTheukSFFMjlTFm6PjceOca/SMpKfeCR1L6Uzk/y2WIkVhNFJlJAZhP+hYns7b9D3IPuhY5mYrIv8OrQJvR5NYyNaW4jsU8pSGNySiVx4o5tXq3JkoXE/mg5R/M8dGJCJpKhaDcjBRdbI/Rm8g69c122om33BHmj2CHoV5qa9jUXBraJ+G1fAVjIBO1klc87ro1K4JZ/K35SWW3TwcyDd6TecqnAEd8cGq2+w84xvBm1n3vS0izKkkwh5XNC/GmFPqqAtPF89AOScKuemaNzoTV1SD5dtSbmLf1/RV+tC0WTgcj6R7HEtrVGWaqu/lYDZ/2pvxQ/kIyw/gFByHC9AHw910hv1aUUumyd8yy0QfhmEkfiNod0Xusct68J1qc8Tdux0Z97Q+hsDb+AYGYEbF/4Guw2Q/qDPqZG/zXgT+3Qj8AtKnfWhFwmuAAAAAAElFTkSuQmCC");background-repeat:no-repeat;background-position:center;background-size:23px;vertical-align:bottom}div.dtsp-topRow button.dtsp-countButton{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABcGlDQ1BpY2MAACiRdZHNSwJBGMYftTDS8FCHkA57sOigIAXRMQzyYh3UIKvL7rirwe66zK6IdA26dBA6RF36OvQf1DXoWhAERRAR9B/0dQnZ3nEFJXSG2ffHs/O8zDwD+DM6M+yBJGCYDs+mU9JaYV0KviNMM4QoEjKzreXcUh59x88jfKI+JESv/vt6jlBRtRngGyKeYxZ3iBeIMzXHErxHPMbKcpH4hDjO6YDEt0JXPH4TXPL4SzDPZxcBv+gplbpY6WJW5gbxNHHM0KusfR5xk7BqruaoRmlNwEYWaaQgQUEVW9DhIEHVpMx6+5It3woq5GH0tVAHJ0cJZfLGSa1SV5WqRrpKU0dd5P4/T1ubnfG6h1PA4Kvrfk4CwX2g2XDd31PXbZ4BgRfg2uz4K5TT/DfpjY4WOwYiO8DlTUdTDoCrXWD82ZK53JICtPyaBnxcACMFYPQeGN7wsmr/x/kTkN+mJ7oDDo+AKdof2fwDCBRoDkL8UccAAAAJcEhZcwAAD2EAAA9hAag/p2kAAAG5SURBVEgN3VU9LwVBFF0fiYhofUSlEQkKhU7z/oBCQkIiGr9BgUbhVzy9BAnhFyjV/AYFiU5ICM7ZN+c5Zud5dm3lJmfmzrkz9+7cu3c3y/6jjOBSF8CxXS7FmTkbwqIJjDpJvTcmsJ4K3KPZUpyZsx0sxoB9J6mnAkyC7wGuuCFIipNtEcpcWExgXpOBc78vgj6N+QO4NVsjwdFM59tUIDxDrHMBOeIQ34C5ZDregXuAQm4YcI68nN9B3wr2PcwPAIPkN2EqtJH6b+QZm1ajjTx7BqwAr26Lb+C2Kvpbt0Mb2HAJ7NrGFGfmXO3DeA4UshDfQAVmH0gaUFg852TTTDvlxwBlCtxy9zXyBhQFaq0wMmIdRebrfgosA3zb2hKnqG0oqchp4QbuR8X0TjzABhbdOT8jnQ/atcgqpnfwOA7yqZyTU587ZkIGdesLTt2EkynOnbreMUUKMI/dA4B/QVOcO13CQh+5wWCgDwo/75u59odB/wjmfhbgvACcAOyZPHihMWAoIwxyCLgf1oxfgjzVbgBXSTzIN+f0pg6s5DkcesLMRpsBrgE2XO3CN64JFP7JtUeKHX4CKtRRXFZ+7dEAAAAASUVORK5CYII=");background-repeat:no-repeat;background-position:center;background-size:18px;vertical-align:bottom}div.dtsp-topRow button.dtsp-searchIcon{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAYAAAA71pVKAAABcGlDQ1BpY2MAACiRdZHNSwJBGMYftTDS8FCHkA57sOigIAXRMQzyYh3UIKvL7rirwe66zK6IdA26dBA6RF36OvQf1DXoWhAERRAR9B/0dQnZ3nEFJXSG2ffHs/O8zDwD+DM6M+yBJGCYDs+mU9JaYV0KviNMM4QoEjKzreXcUh59x88jfKI+JESv/vt6jlBRtRngGyKeYxZ3iBeIMzXHErxHPMbKcpH4hDjO6YDEt0JXPH4TXPL4SzDPZxcBv+gplbpY6WJW5gbxNHHM0KusfR5xk7BqruaoRmlNwEYWaaQgQUEVW9DhIEHVpMx6+5It3woq5GH0tVAHJ0cJZfLGSa1SV5WqRrpKU0dd5P4/T1ubnfG6h1PA4Kvrfk4CwX2g2XDd31PXbZ4BgRfg2uz4K5TT/DfpjY4WOwYiO8DlTUdTDoCrXWD82ZK53JICtPyaBnxcACMFYPQeGN7wsmr/x/kTkN+mJ7oDDo+AKdof2fwDCBRoDkL8UccAAAAJcEhZcwAAD2EAAA9hAag/p2kAAAEnSURBVCgVpdG7SgNBFIDh1RhJsBBEsDIgIhaWFjZa2GtpKb6AnU0MprKOWEjK2IuFFxCxS2lhZyOWXh5AQVER/X+zuwwywoIHvp3dM3Nm55Ik/4i+P2or5FewiBIe0cEt8ogVz9LbhEVf+cgkcew1tvAZ5PPXGm9HOMEanMAYQhunaCAazuqA1UjvILl9HGPc/n4fabjPGbzjMM2FjfkDuPw5O8JilzgA9/OKWDynyWnbsPiF7yc4SRWxmEyTN7ZhsSd7gTLW8TuGSSzBcZd2hsV+n+MNC9jGCNzjPDwsz8XCO/x02Bqeptcxhg+4gjD8YxetLOkBGRbuwcIr+NdRLMPl3uMM2YHx2gsLd+D97qKEQuGe65jCAzbgVRWOCUZuovAfs5m/AdVxL0R1AIsLAAAAAElFTkSuQmCC");background-repeat:no-repeat;background-position:center;background-size:12px}div.dt-button-collection{z-index:2002}div.dataTables_scrollBody{background:white !important}div.dtsp-columns-1{min-width:98%;max-width:98%;padding-left:1%;padding-right:1%;margin:0px !important}div.dtsp-columns-2{min-width:48%;max-width:48%;padding-left:1%;padding-right:1%;margin:0px !important}div.dtsp-columns-3{min-width:30.333%;max-width:30.333%;padding-left:1%;padding-right:1%;margin:0px !important}div.dtsp-columns-4{min-width:23%;max-width:23%;padding-left:1%;padding-right:1%;margin:0px !important}div.dtsp-columns-5{min-width:18%;max-width:18%;padding-left:1%;padding-right:1%;margin:0px !important}div.dtsp-columns-6{min-width:15.666%;max-width:15.666%;padding-left:0.5%;padding-right:0.5%;margin:0px !important}div.dtsp-columns-7{min-width:13.28%;max-width:13.28%;padding-left:0.5%;padding-right:0.5%;margin:0px !important}div.dtsp-columns-8{min-width:11.5%;max-width:11.5%;padding-left:0.5%;padding-right:0.5%;margin:0px !important}div.dtsp-columns-9{min-width:11.111%;max-width:11.111%;padding-left:0.5%;padding-right:0.5%;margin:0px !important}div.dt-button-collection{float:none}div.dtsp-panesContainer{width:100%}div.dtsp-panesContainer{font-family:'Roboto', sans-serif;padding:5px;border:1px solid #ccc;border-radius:6px;margin:5px 0;clear:both;text-align:center}div.dtsp-panesContainer div.dtsp-searchPanes{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:flex-start;align-content:flex-start;align-items:stretch;clear:both;text-align:start}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-hidden{display:none !important}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane{flex-direction:row;flex-wrap:nowrap;flex-grow:1;flex-shrink:0;flex-basis:280px;justify-content:space-around;align-content:flex-start;align-items:stretch;padding-top:0px;padding-bottom:0px;margin:5px;margin-top:0px;margin-bottom:0px;font-size:0.9em}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dataTables_wrapper{flex:1;margin:1em 0.5%;margin-top:0px;border-bottom:2px solid #f0f0f0;border:2px solid #f0f0f0;border-radius:4px}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dataTables_wrapper:hover{border:2px solid #cfcfcf}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dataTables_wrapper div.dataTables_filter{display:none}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dtsp-selected{border:2px solid #3276b1;border-radius:4px}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dtsp-selected:hover{border:2px solid #286092}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dtsp-topRow{display:flex;flex-direction:row;flex-wrap:nowrap;justify-content:space-around;align-content:flex-start;align-items:flex-start}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dtsp-topRow div.dtsp-searchCont{display:flex;flex-direction:row;flex-wrap:nowrap;flex-grow:1;flex-shrink:0;flex-basis:0}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dtsp-topRow div.dtsp-searchCont input.dtsp-search{flex-direction:row;flex-wrap:nowrap;flex-grow:1;flex-shrink:0;flex-basis:90px;min-height:20px;max-width:none;min-width:50px;border:none;padding-left:12px}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dtsp-topRow div.dtsp-searchCont input.dtsp-search::placeholder{color:black;font-size:16px}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dtsp-topRow div.dtsp-searchCont div.dtsp-searchButtonCont{display:inline-block;flex-direction:row;flex-wrap:nowrap;flex-grow:0;flex-shrink:0;flex-basis:0}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dtsp-topRow div.dtsp-searchCont div.dtsp-searchButtonCont .dtsp-searchIcon{position:relative;display:inline-block;margin:4px;display:block;top:-2px;right:0px;font-size:16px;margin-bottom:0px}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dtsp-topRow button.dtsp-dull{cursor:context-menu !important;color:#7c7c7c}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dtsp-topRow button.dtsp-dull:hover{background-color:transparent}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane input.dtsp-paneInputButton,div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane button.dtsp-paneButton{height:35px;width:35px;max-width:35px;min-width:0;display:inline-block;margin:2px;border:0px solid transparent;background-color:transparent;font-size:16px;margin-bottom:0px;flex-grow:0;flex-shrink:0;flex-basis:35px;font-family:sans-serif}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane input.dtsp-paneInputButton:hover,div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane button.dtsp-paneButton:hover{background-color:#f0f0f0;border-radius:2px;cursor:pointer}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane button.dtsp-paneButton{opacity:0.6}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane input.dtsp-disabledButton{height:35px;width:35px;max-width:35px;min-width:0;display:inline-block;margin:2px;border:0px solid transparent;background-color:transparent;font-size:16px;margin-bottom:0px;flex-grow:0;flex-shrink:0;flex-basis:35px}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dataTables_scrollHead{display:none !important}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dataTables_scrollBody{border-bottom:none}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dataTables_scrollBody td.dtsp-countColumn{text-align:right}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dataTables_scrollBody td.dtsp-countColumn div.dtsp-pill{background-color:#cfcfcf;text-align:center;border:1px solid #cfcfcf;border-radius:10px;width:auto;display:inline-block;min-width:30px;color:black;font-size:0.9em;padding:0 4px}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane div.dataTables_scrollBody span.dtsp-pill{float:right;background-color:#cfcfcf;text-align:center;border:1px solid #cfcfcf;border-radius:10px;width:auto;min-width:30px;color:black;font-size:0.9em;padding:0 4px}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane tr>th,div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane tr>td{padding:5px 10px}div.dtsp-panesContainer div.dtsp-searchPanes div.dtsp-searchPane td.dtsp-countColumn{text-align:right}div.dtsp-panesContainer div.dtsp-title{float:left;margin:20px;margin-bottom:0px;margin-top:13px}div.dtsp-panesContainer button.dtsp-clearAll{float:right;margin:20px;border:1px solid transparent;background-color:transparent;padding:10px;margin-bottom:0px;margin-top:5px}div.dtsp-panesContainer button.dtsp-clearAll:hover{background-color:#f0f0f0;border-radius:2px}div.dt-button-collection div.panes{padding:0px;border:none;margin:0px}div.dtsp-hidden{display:none !important}div.dtsp-narrow{flex-direction:column !important}div.dtsp-narrow div.dtsp-subRows{width:100%;text-align:right}@media screen and (max-width: 767px){div.dtsp-columns-4,div.dtsp-columns-5,div.dtsp-columns-6{max-width:31% !important;min-width:31% !important}}@media screen and (max-width: 640px){div.dtsp-searchPanes{flex-direction:column !important}div.dtsp-searchPane{max-width:98% !important;min-width:98% !important}} 20 | 21 | 22 | table.dataTable tbody>tr.selected,table.dataTable tbody>tr>.selected{background-color:#B0BED9}table.dataTable.stripe tbody>tr.odd.selected,table.dataTable.stripe tbody>tr.odd>.selected,table.dataTable.display tbody>tr.odd.selected,table.dataTable.display tbody>tr.odd>.selected{background-color:#acbad4}table.dataTable.hover tbody>tr.selected:hover,table.dataTable.hover tbody>tr>.selected:hover,table.dataTable.display tbody>tr.selected:hover,table.dataTable.display tbody>tr>.selected:hover{background-color:#aab7d1}table.dataTable.order-column tbody>tr.selected>.sorting_1,table.dataTable.order-column tbody>tr.selected>.sorting_2,table.dataTable.order-column tbody>tr.selected>.sorting_3,table.dataTable.order-column tbody>tr>.selected,table.dataTable.display tbody>tr.selected>.sorting_1,table.dataTable.display tbody>tr.selected>.sorting_2,table.dataTable.display tbody>tr.selected>.sorting_3,table.dataTable.display tbody>tr>.selected{background-color:#acbad5}table.dataTable.display tbody>tr.odd.selected>.sorting_1,table.dataTable.order-column.stripe tbody>tr.odd.selected>.sorting_1{background-color:#a6b4cd}table.dataTable.display tbody>tr.odd.selected>.sorting_2,table.dataTable.order-column.stripe tbody>tr.odd.selected>.sorting_2{background-color:#a8b5cf}table.dataTable.display tbody>tr.odd.selected>.sorting_3,table.dataTable.order-column.stripe tbody>tr.odd.selected>.sorting_3{background-color:#a9b7d1}table.dataTable.display tbody>tr.even.selected>.sorting_1,table.dataTable.order-column.stripe tbody>tr.even.selected>.sorting_1{background-color:#acbad5}table.dataTable.display tbody>tr.even.selected>.sorting_2,table.dataTable.order-column.stripe tbody>tr.even.selected>.sorting_2{background-color:#aebcd6}table.dataTable.display tbody>tr.even.selected>.sorting_3,table.dataTable.order-column.stripe tbody>tr.even.selected>.sorting_3{background-color:#afbdd8}table.dataTable.display tbody>tr.odd>.selected,table.dataTable.order-column.stripe tbody>tr.odd>.selected{background-color:#a6b4cd}table.dataTable.display tbody>tr.even>.selected,table.dataTable.order-column.stripe tbody>tr.even>.selected{background-color:#acbad5}table.dataTable.display tbody>tr.selected:hover>.sorting_1,table.dataTable.order-column.hover tbody>tr.selected:hover>.sorting_1{background-color:#a2aec7}table.dataTable.display tbody>tr.selected:hover>.sorting_2,table.dataTable.order-column.hover tbody>tr.selected:hover>.sorting_2{background-color:#a3b0c9}table.dataTable.display tbody>tr.selected:hover>.sorting_3,table.dataTable.order-column.hover tbody>tr.selected:hover>.sorting_3{background-color:#a5b2cb}table.dataTable.display tbody>tr:hover>.selected,table.dataTable.display tbody>tr>.selected:hover,table.dataTable.order-column.hover tbody>tr:hover>.selected,table.dataTable.order-column.hover tbody>tr>.selected:hover{background-color:#a2aec7}table.dataTable tbody td.select-checkbox,table.dataTable tbody th.select-checkbox{position:relative}table.dataTable tbody td.select-checkbox:before,table.dataTable tbody td.select-checkbox:after,table.dataTable tbody th.select-checkbox:before,table.dataTable tbody th.select-checkbox:after{display:block;position:absolute;top:1.2em;left:50%;width:12px;height:12px;box-sizing:border-box}table.dataTable tbody td.select-checkbox:before,table.dataTable tbody th.select-checkbox:before{content:' ';margin-top:-6px;margin-left:-6px;border:1px solid black;border-radius:3px}table.dataTable tr.selected td.select-checkbox:after,table.dataTable tr.selected th.select-checkbox:after{content:'\2714';margin-top:-11px;margin-left:-4px;text-align:center;text-shadow:1px 1px #B0BED9, -1px -1px #B0BED9, 1px -1px #B0BED9, -1px 1px #B0BED9}div.dataTables_wrapper span.select-info,div.dataTables_wrapper span.select-item{margin-left:0.5em}@media screen and (max-width: 640px){div.dataTables_wrapper span.select-info,div.dataTables_wrapper span.select-item{margin-left:0;display:block}} 23 | 24 | 25 | -------------------------------------------------------------------------------- /www/datatables/jquery.dataTables.yadcf.css: -------------------------------------------------------------------------------- 1 | .hide { 2 | display: none; 3 | } 4 | 5 | .inuse, .ui-slider-range .inuse, .yadcf-filter-range-number-slider .inuse { 6 | background: #8BBEF0; 7 | } 8 | 9 | .yadcf-filter-reset-button { 10 | display: inline-block; 11 | } 12 | 13 | .yadcf-filter-reset-button.range-number-slider-reset-button{ 14 | position: relative; 15 | top: -6px; 16 | } 17 | 18 | .yadcf-filter { 19 | padding-right: 4px; 20 | padding-left: 4px; 21 | padding-bottom: 3px; 22 | padding-top: 3px; 23 | } 24 | 25 | .yadcf-filter > option{ 26 | background: white; 27 | } 28 | 29 | .ui-autocomplete .ui-menu-item { 30 | font-size:13px; 31 | } 32 | 33 | #ui-datepicker-div { 34 | font-size:13px; 35 | } 36 | .yadcf-filter-wrapper { 37 | display: inline-block; 38 | white-space: nowrap; 39 | margin-left: 2px; 40 | } 41 | 42 | .yadcf-filter-range-number { 43 | width: 40px; 44 | } 45 | 46 | .yadcf-filter-range-number-seperator { 47 | margin-left: 10px; 48 | margin-right: 10px; 49 | } 50 | 51 | .yadcf-filter-range-date { 52 | width: 80px; 53 | } 54 | 55 | .yadcf-filter-range-date-seperator { 56 | margin-left: 10px; 57 | margin-right: 10px; 58 | } 59 | 60 | .yadcf-filter-wrapper-inner { 61 | display: inline-block; 62 | border: 1px solid #ABADB3; 63 | } 64 | 65 | .yadcf-number-slider-filter-wrapper-inner { 66 | display: inline-block; 67 | width: 200px; 68 | margin-bottom: 7px; 69 | } 70 | 71 | .yadcf-filter-range-number-slider .ui-slider-handle { 72 | width: 10px; 73 | height: 10px; 74 | margin-top: 1px; 75 | } 76 | 77 | .yadcf-filter-range-number-slider .ui-slider-range { 78 | position: relative; 79 | height: 5px; 80 | } 81 | 82 | .yadcf-filter-range-number-slider { 83 | height: 5px; 84 | margin-left: 6px; 85 | margin-right: 6px; 86 | } 87 | 88 | .yadcf-filter-range-number-slider { 89 | overflow: visible; 90 | } 91 | 92 | .yadcf-number-slider-filter-wrapper-inner .yadcf-filter-range-number-slider-min-tip { 93 | font-size: 13px; 94 | font-weight: normal; 95 | position: absolute; 96 | outline-style: none; 97 | } 98 | 99 | .yadcf-number-slider-filter-wrapper-inner .yadcf-filter-range-number-slider-max-tip { 100 | font-size: 13px; 101 | font-weight: normal; 102 | position:absolute; 103 | outline-style: none; 104 | } 105 | 106 | .yadcf-number-slider-filter-wrapper-inner .yadcf-filter-range-number-slider-min-tip-inner { 107 | position:absolute; 108 | top: 11px; 109 | } 110 | 111 | .yadcf-number-slider-filter-wrapper-inner .yadcf-filter-range-number-slider-max-tip-inner { 112 | position:absolute; 113 | top: 11px; 114 | } 115 | 116 | .yadcf-exclude-wrapper { 117 | display: inline-block; 118 | vertical-align: middle; 119 | margin-right: 5px; 120 | } 121 | .yadcf-label.small { 122 | font-size: 10px; 123 | } 124 | -------------------------------------------------------------------------------- /www/empty-json.php: -------------------------------------------------------------------------------- 1 | 15 | {} 16 | -------------------------------------------------------------------------------- /www/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cweiske/stouyapi/8b9bcae7760ae891ea3c9a93bc936219bebe9c54/www/favicon.ico -------------------------------------------------------------------------------- /www/ouya-allgames.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | html { 5 | height: 100%; 6 | } 7 | body { 8 | min-height: 100%; 9 | background-color: #333; 10 | background-image: url("bg_details.jpg"); 11 | background-size: cover; 12 | background-attachment: fixed; 13 | color: #CCC; 14 | font-family: sans; 15 | margin: 0; 16 | padding: 0; 17 | padding-top: 10ex; 18 | padding-left: 0ex; 19 | } 20 | 21 | header { 22 | position: fixed; 23 | top: 0; 24 | left: 0; 25 | display: block; 26 | width: 100%; 27 | height: 6rem; 28 | padding-left: 3ex; 29 | background-color: #333; 30 | background-image: url("bg_details.jpg"); 31 | background-size: cover; 32 | background-attachment: fixed; 33 | } 34 | .ouyalogo { 35 | height: 3rem; 36 | width: auto; 37 | position: fixed; 38 | top: 2ex; 39 | right: 6vw; 40 | } 41 | 42 | a { 43 | color: #CCC; 44 | } 45 | 46 | nav { 47 | margin-top: 5ex; 48 | text-align: center; 49 | } 50 | nav a { 51 | color: white; 52 | margin-left: 1ex; 53 | margin-right: 1ex; 54 | } 55 | 56 | thead th { 57 | text-align: left; 58 | /*position: sticky;*/ 59 | top: 4rem; 60 | background-color: rgba(0, 0, 0, .6); 61 | padding: 0.5ex; 62 | vertical-align: top; 63 | } 64 | 65 | table.dataTable.display tbody tr, 66 | table.dataTable.display tbody tr.odd 67 | { 68 | background-color: transparent; 69 | } 70 | 71 | table.dataTable.display tbody tr > .sorting_1, 72 | table.dataTable.order-column.stripe tbody tr > .sorting_1, 73 | table.dataTable.display tbody tr.odd > .sorting_1, 74 | table.dataTable.order-column.stripe tbody tr.odd > .sorting_1, 75 | table.dataTable.display tbody tr.even > .sorting_1, 76 | table.dataTable.order-column.stripe tbody tr.even > .sorting_1 77 | { 78 | background-color: rgba(0, 0, 0, .4); 79 | } 80 | 81 | table.dataTable.display tbody tr:hover, 82 | table.dataTable.display tbody tr.odd:hover, 83 | table.dataTable.display tbody tr.even:hover 84 | { 85 | background-color: rgba(255, 255, 255, .2); 86 | } 87 | 88 | .dataTables_info, 89 | .dataTables_filter 90 | { 91 | color: inherit !important; 92 | } 93 | 94 | #allouyagames_filter { 95 | display: none; 96 | } 97 | 98 | .dataTable.fixedHeader-floating { 99 | background-color: #333; 100 | background-image: url("bg_details.jpg"); 101 | background-size: cover; 102 | background-attachment: fixed; 103 | } 104 | 105 | .yadcf-filter-wrapper { 106 | display: block; 107 | margin-top: 1ex; 108 | } 109 | .yadcf-filter, 110 | .yadcf-filter-reset-button 111 | { 112 | background-color: transparent; 113 | color: inherit; 114 | height: 4ex; 115 | } 116 | -------------------------------------------------------------------------------- /www/ouya-discover.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | } 4 | body { 5 | min-height: 100%; 6 | background-color: #333; 7 | background-image: url("bg.jpg"); 8 | background-size: cover; 9 | background-attachment: fixed; 10 | color: #CCC; 11 | font-family: sans; 12 | margin: 0; 13 | padding: 0; 14 | padding-top: 10ex; 15 | padding-left: 5ex; 16 | } 17 | 18 | header { 19 | position: fixed; 20 | top: 0; 21 | left: 0; 22 | background-color: black; 23 | opacity: 0.5; 24 | display: block; 25 | width: 100%; 26 | padding-left: 3ex; 27 | } 28 | .ouyalogo { 29 | height: 3rem; 30 | width: auto; 31 | position: fixed; 32 | top: 2ex; 33 | right: 6vw; 34 | } 35 | 36 | h2 { 37 | text-shadow: 2px 2px #555; 38 | text-transform: uppercase; 39 | margin-bottom: 0; 40 | padding-left: 1ex; 41 | } 42 | a { 43 | color: #CCC; 44 | } 45 | 46 | 47 | .tiles { 48 | display: flex; 49 | overflow-x: auto; 50 | column-gap: 1em; 51 | margin-bottom: 2ex; 52 | } 53 | 54 | .tile { 55 | border: 0.5ex solid transparent; 56 | width: 20vw; 57 | 58 | display: flex; 59 | flex-direction: column; 60 | } 61 | .tile:hover { 62 | border: 0.5ex solid orange; 63 | } 64 | 65 | .tile > a { 66 | order: -1; 67 | } 68 | .tile img { 69 | width: 20vw; 70 | height: auto; 71 | } 72 | h3 { 73 | margin: 0; 74 | font-weight: normal; 75 | } 76 | h3 a { 77 | padding-left: 1ex; 78 | padding-top: 1ex; 79 | padding-bottom: 0ex; 80 | display: block; 81 | width: 100%; 82 | } 83 | .tile p { 84 | margin: 0; 85 | padding-left: 2ex; 86 | font-size: 80%; 87 | } 88 | 89 | 90 | nav { 91 | text-align: center; 92 | margin-top: 6ex; 93 | margin-bottom: 2ex; 94 | } 95 | nav a { 96 | color: white; 97 | margin-left: 1ex; 98 | margin-right: 1ex; 99 | } 100 | 101 | 102 | /* rating stars */ 103 | .average { 104 | visibility: hidden; 105 | font-size: 0; 106 | } 107 | .average:before { 108 | visibility: visible; 109 | font-size: 1rem; 110 | color: orange; 111 | } 112 | .average:after { 113 | visibility: visible; 114 | font-size: 1rem; 115 | } 116 | .average-0:after { 117 | content: "★★★★★"; 118 | } 119 | .average-1:before { 120 | content: "★"; 121 | } 122 | .average-1:after { 123 | content: "★★★★"; 124 | } 125 | .average-2:before { 126 | content: "★★"; 127 | } 128 | .average-2:after { 129 | content: "★★★"; 130 | } 131 | .average-3:before { 132 | content: "★★★"; 133 | } 134 | .average-3:after { 135 | content: "★★"; 136 | } 137 | .average-4:before { 138 | content: "★★★★"; 139 | } 140 | .average-4:after { 141 | content: "★"; 142 | } 143 | .average-5:before { 144 | content: "★★★★★"; 145 | } 146 | 147 | .tile.noimg { 148 | } 149 | .tile.noimg h3 { 150 | background-color: black; 151 | background: linear-gradient(to top right, black, 70%, #4b6f67); 152 | } 153 | .tile.noimg h3 a { 154 | padding-left: 0; 155 | padding-top: 5ex; 156 | padding-bottom: 5ex; 157 | text-align: center; 158 | text-transform: uppercase; 159 | } 160 | -------------------------------------------------------------------------------- /www/ouya-game.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | } 4 | body { 5 | min-height: 100%; 6 | background-color: #333; 7 | background-image: url("bg_details.jpg"); 8 | background-size: cover; 9 | background-attachment: fixed; 10 | color: #CCC; 11 | font-family: sans; 12 | margin: 0; 13 | padding: 0; 14 | padding-top: 10ex; 15 | padding-left: 5ex; 16 | 17 | display: grid; 18 | grid-template-columns: 45vw 45vw; 19 | grid-gap: 1.5vw; 20 | } 21 | 22 | header { 23 | position: fixed; 24 | top: 0; 25 | left: 0; 26 | display: block; 27 | width: 100%; 28 | padding-left: 3ex; 29 | } 30 | .ouyalogo { 31 | height: 3rem; 32 | width: auto; 33 | position: fixed; 34 | top: 2ex; 35 | right: 6vw; 36 | } 37 | 38 | section.media { 39 | grid-column: 1; 40 | grid-row: 1; 41 | } 42 | section.text { 43 | grid-column: 2; 44 | grid-row: 1; 45 | } 46 | 47 | section.buttons { 48 | grid-column: 1 / 3; 49 | grid-row: 2; 50 | } 51 | nav { 52 | grid-column: 1 / 3; 53 | grid-row: 3; 54 | } 55 | 56 | .media img, .media video { 57 | max-width: 100%; 58 | flex-basis: 100%; 59 | flex-shrink: 0; 60 | } 61 | 62 | 63 | .media h2 { 64 | display: none; 65 | } 66 | .media .content { 67 | overflow-x: auto; 68 | display: flex; 69 | flex-direction: row; 70 | scroll-snap-type: x mandatory; 71 | } 72 | .media .content img, .media .content video { 73 | scroll-snap-align: start; 74 | } 75 | 76 | 77 | .text h1 { 78 | font-weight: normal; 79 | margin-top: 0; 80 | } 81 | .text dt { 82 | display: none; 83 | } 84 | .text dd { 85 | display: inline; 86 | margin: 0; 87 | font-weight: bold; 88 | } 89 | .text dd ~ dd:before { 90 | content: "|"; 91 | font-weight: normal; 92 | } 93 | .text a { 94 | color: #CCC; 95 | } 96 | 97 | .description { 98 | overflow-y: auto; 99 | max-height: 50vh; 100 | } 101 | .description .blocked { 102 | color: red; 103 | } 104 | 105 | .buttons h2 { 106 | display: none; 107 | } 108 | .buttons { 109 | display: flex; 110 | justify-content: space-between; 111 | } 112 | .buttons a { 113 | font-size: 1.5rem; 114 | color: #CCC; 115 | } 116 | button.push-to-my-ouya { 117 | cursor: pointer; 118 | border: none; 119 | padding: 0; 120 | background-color: transparent; 121 | } 122 | 123 | nav { 124 | text-align: center; 125 | } 126 | nav a { 127 | color: white; 128 | margin-left: 1ex; 129 | margin-right: 1ex; 130 | } 131 | 132 | 133 | /* rating stars */ 134 | .average { 135 | visibility: hidden; 136 | font-size: 0; 137 | } 138 | .average:before { 139 | visibility: visible; 140 | font-size: 1rem; 141 | color: orange; 142 | } 143 | .average:after { 144 | visibility: visible; 145 | font-size: 1rem; 146 | } 147 | .average-0:after { 148 | content: "★★★★★"; 149 | } 150 | .average-1:before { 151 | content: "★"; 152 | } 153 | .average-1:after { 154 | content: "★★★★"; 155 | } 156 | .average-2:before { 157 | content: "★★"; 158 | } 159 | .average-2:after { 160 | content: "★★★"; 161 | } 162 | .average-3:before { 163 | content: "★★★"; 164 | } 165 | .average-3:after { 166 | content: "★★"; 167 | } 168 | .average-4:before { 169 | content: "★★★★"; 170 | } 171 | .average-4:after { 172 | content: "★"; 173 | } 174 | .average-5:before { 175 | content: "★★★★★"; 176 | } 177 | 178 | 179 | .popup { 180 | position: fixed; 181 | top: 1.5rem; 182 | right: 1.5rem; 183 | width: 20rem; 184 | padding: 1rem; 185 | background-color: black; 186 | border: 1px solid #AAA; 187 | border-radius: 0.5rem; 188 | } 189 | .popup a.close { 190 | color: white; 191 | font-size: 2rem; 192 | text-decoration: none; 193 | position: absolute; 194 | top: 0; 195 | right: 0.5rem; 196 | } 197 | .popup a.close:hover { 198 | color: #fc4422; 199 | } 200 | .popup strong { 201 | display: block; 202 | color: #fc4422; 203 | margin-bottom: 0.5rem; 204 | } 205 | -------------------------------------------------------------------------------- /www/ouya-logo.grey.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 38 | 41 | 42 | 44 | 46 | 47 | 49 | image/svg+xml 50 | 52 | 53 | 54 | 55 | 56 | 59 | 62 | 68 | 73 | 78 | 83 | 88 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /www/ouya-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 38 | 41 | 42 | 44 | 46 | 47 | 49 | image/svg+xml 50 | 52 | 53 | 54 | 55 | 56 | 59 | 62 | 68 | 73 | 78 | 83 | 88 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /www/ouya-square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cweiske/stouyapi/8b9bcae7760ae891ea3c9a93bc936219bebe9c54/www/ouya-square.png -------------------------------------------------------------------------------- /www/ouya-square.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 47 | 52 | 56 | 60 | 61 | 63 | 65 | 66 | 68 | image/svg+xml 69 | 71 | 72 | 73 | 74 | 75 | 80 | 85 | 88 | 93 | 98 | 99 | 102 | 107 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /www/push-to-my-ouya.php: -------------------------------------------------------------------------------- 1 | 12 | * @see api/v1/queued_downloads.php 13 | * @see api/v1/queued_downloads_delete.php 14 | */ 15 | $dbFile = __DIR__ . '/../data/push-to-my-ouya.sqlite3'; 16 | $apiGameDir = __DIR__ . '/api/v1/details-data/'; 17 | 18 | require_once __DIR__ . '/../src/push-to-my-ouya-helpers.php'; 19 | 20 | //support different ipv4-only domain 21 | header('Access-Control-Allow-Origin: *'); 22 | 23 | if ($_SERVER['REQUEST_METHOD'] != 'POST') { 24 | header('HTTP/1.0 400 Bad Request'); 25 | header('Content-type: text/plain'); 26 | echo 'POST only, please' . "\n"; 27 | exit(1); 28 | } 29 | 30 | if (!isset($_GET['game'])) { 31 | header('HTTP/1.0 400 Bad Request'); 32 | header('Content-type: text/plain'); 33 | echo '"game" parameter missing' . "\n"; 34 | exit(1); 35 | } 36 | 37 | $game = $_GET['game']; 38 | $cleanGame = preg_replace('#[^a-zA-Z0-9._]#', '', $game); 39 | if ($game != $cleanGame) { 40 | header('HTTP/1.0 400 Bad Request'); 41 | header('Content-type: text/plain'); 42 | echo 'Invalid game' . "\n"; 43 | exit(1); 44 | } 45 | 46 | $apiGameFile = $apiGameDir . $game . '.json'; 47 | if (!file_exists($apiGameFile)) { 48 | header('HTTP/1.0 404 Not Found'); 49 | header('Content-type: text/plain'); 50 | echo 'Game does not exist' . "\n"; 51 | exit(1); 52 | } 53 | 54 | $ip = $_SERVER['REMOTE_ADDR']; 55 | if ($ip == '') { 56 | header('HTTP/1.0 400 Bad Request'); 57 | header('Content-type: text/plain'); 58 | echo 'Cannot detect your IP address' . "\n"; 59 | exit(1); 60 | } 61 | $ip = mapIp($ip); 62 | 63 | try { 64 | $db = new SQLite3($dbFile, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE); 65 | } catch (Exception $e) { 66 | header('HTTP/1.0 500 Internal server error'); 67 | header('Content-type: text/plain'); 68 | echo 'Cannot open database' . "\n"; 69 | echo $e->getMessage() . "\n"; 70 | exit(2); 71 | } 72 | 73 | $res = $db->querySingle( 74 | 'SELECT name FROM sqlite_master WHERE type = "table" AND name = "pushes"' 75 | ); 76 | if ($res === null) { 77 | //table does not exist yet 78 | $db->exec( 79 | <<exec( 92 | 'DELETE FROM pushes' 93 | . ' WHERE created_at < \'' . gmdate('Y-m-d H:i:s', time() - 86400) . '\'' 94 | ); 95 | 96 | //check if this IP already pushed this game 97 | $numThisGame = $db->querySingle( 98 | 'SELECT COUNT(*) FROM pushes' 99 | . ' WHERE ip = \'' . SQLite3::escapeString($ip) . '\'' 100 | . ' AND game = \'' . SQLite3::escapeString($game) . '\'' 101 | ); 102 | if ($numThisGame >= 1) { 103 | header('HTTP/1.0 400 Bad Request'); 104 | header('Content-type: text/plain'); 105 | echo 'Already pushed.' . "\n"; 106 | exit(1); 107 | } 108 | 109 | //check number of pushes for this IP 110 | $numPushes = $db->querySingle( 111 | 'SELECT COUNT(*) FROM pushes' 112 | . ' WHERE ip = \'' . SQLite3::escapeString($ip) . '\'' 113 | ); 114 | if ($numPushes >= 30) { 115 | header('HTTP/1.0 400 Bad Request'); 116 | header('Content-type: text/plain'); 117 | echo 'Too many pushes. Come back tomorrow.' . "\n"; 118 | exit(1); 119 | } 120 | 121 | //store the push 122 | $stmt = $db->prepare('INSERT INTO pushes (game, ip) VALUES(:game, :ip)'); 123 | $stmt->bindValue(':game', $game); 124 | $stmt->bindValue(':ip', $ip); 125 | $res = $stmt->execute(); 126 | if ($res === false) { 127 | header('HTTP/1.0 500 Internal server error'); 128 | header('Content-type: text/plain'); 129 | echo 'Cannot store push' . "\n"; 130 | exit(3); 131 | } 132 | $res->finalize(); 133 | 134 | header('HTTP/1.0 200 OK'); 135 | header('Content-type: text/plain'); 136 | echo 'Push accepted' . "\n"; 137 | exit(3); 138 | ?> 139 | -------------------------------------------------------------------------------- /www/push-to-my-ouya.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cweiske/stouyapi/8b9bcae7760ae891ea3c9a93bc936219bebe9c54/www/push-to-my-ouya.png --------------------------------------------------------------------------------