├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── app.js ├── config ├── default.json └── local.json.EXAMPLE ├── package-lock.json ├── package.json └── public ├── css ├── library.css ├── nowplaying.css └── site.css ├── favicons ├── android-icon-144x144.png ├── android-icon-192x192.png ├── android-icon-36x36.png ├── android-icon-48x48.png ├── android-icon-72x72.png ├── android-icon-96x96.png ├── apple-icon-114x114.png ├── apple-icon-120x120.png ├── apple-icon-144x144.png ├── apple-icon-152x152.png ├── apple-icon-180x180.png ├── apple-icon-57x57.png ├── apple-icon-60x60.png ├── apple-icon-72x72.png ├── apple-icon-76x76.png ├── apple-icon-precomposed.png ├── apple-icon.png ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon-96x96.png ├── favicon.ico ├── manifest.json ├── ms-icon-144x144.png ├── ms-icon-150x150.png ├── ms-icon-310x310.png └── ms-icon-70x70.png ├── fullscreen.html ├── img └── transparent.png ├── js ├── NoSleep.min.js ├── color-thief.js ├── fullscreen.js ├── jquery.simplemarquee.js ├── library.js └── nowplaying.js ├── library.html ├── nowplaying.html └── side-by-side.html /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directory 2 | node_modules 3 | 4 | # Roon Core configuration file 5 | config.json 6 | 7 | # Local server configuration file 8 | config/local.json 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## Version 1.2.13 4 | 5 | NOTE: this is the final release of the 1.x series. 6 | 7 | Security 8 | - updated dependencies for security issues. 9 | 10 | 11 | ### Upgrade Notes 12 | 13 | The package dependencies have been updated to the latest versions. It is recommended to perform an update. 14 | 15 | If you followed the [Diet Pi installation](https://github.com/pluggemi/roon-web-controller/wiki/Diet-Pi-Installation) guide, follow these [update instructions](https://github.com/pluggemi/roon-web-controller/wiki/Diet-Pi-Installation#updating-the-web-controller-software) 16 | 17 | Otherwise, on the Node.js server: 18 | 19 | 1. Stop the extension 20 | 1. Run `git pull` 21 | 1. Run `npm install` 22 | 1. Run `npm update` 23 | 1. Start the extension 24 | 25 | --- 26 | 27 | # Release History 28 | 29 | ## Version 1.2.12 30 | 31 | Accessibility 32 | 33 | - corrected name and aria label for one of the zone list buttons 34 | 35 | 36 | ## Version 1.2.11 37 | 38 | ### New 39 | 40 | Accessibility 41 | 42 | - added dynamic alt text for album cover 43 | - added names and aria labels to buttons 44 | - added aria disabled status to buttons 45 | 46 | ## Version 1.2.10 47 | 48 | ### New 49 | 50 | - Updated dependencies to address a potential security vulnerability 51 | 52 | ## Version 1.2.9 53 | 54 | ### New 55 | 56 | - Updated dependencies 57 | - Added check to show zone selection overlay if selected zone is no longer available. 58 | 59 | ## Version 1.2.8 60 | 61 | ### New 62 | 63 | - Maintenance Release 64 | - Addressed issue where the song seek time was not updating. 65 | 66 | ## Version 1.2.7 67 | 68 | ### New 69 | 70 | - Maintenance Release 71 | - Updated dependencies and updated code for compatibility with the Command Line Usage module. 72 | 73 | ## Version 1.2.6 74 | 75 | ### New 76 | 77 | - Added option to disable the screensaver or sleep mode when a song is playing. The screensaver or sleep mode is allowed when a track is paused or stopped. 78 | - Known limitation - the app will not wake up a screen that is already sleeping when playback is started from another app. 79 | 80 | ## Version 1.2.5 81 | 82 | No changes - version bump to allow registration on [npmjs.com](https://www.npmjs.com/package/roon-web-controller) 83 | 84 | ## Version 1.2.4 85 | 86 | ### New 87 | 88 | - Added option for 4K cover images on the Now Playing screen. (Settings -> Use 4k Images) 89 | 90 | ### Fixed bugs 91 | 92 | - Fixed compatibility issue with older versions of IOS Safari. 93 | 94 | ## Version 1.2.3 95 | 96 | ### New 97 | 98 | ### Fixed bugs 99 | 100 | - Corrected button flow in the non-settings related overlays 101 | - Added feedback to zonelist overlay in library to match nowplaying section 102 | - Fixed bug where title text was not centered in the library list 103 | - Fixed bug where very long text in library list would extend past the button size 104 | 105 | ## Version 1.2.2 106 | 107 | ### New 108 | 109 | - OS native song notifications thanks to initial code by [jcharr1](https://github.com/jcharr1) 110 | - Added option to use circle icons (for Play, Pause and Stop only) 111 | - Added feedback on various overlays to show currently selected option 112 | 113 | ## Version 1.2.1 114 | 115 | ### New 116 | 117 | - Volume up and volume down buttons for more granular control of the volume 118 | 119 | ### Misc bug fixes and behind the scenes 120 | 121 | - Extended cookie life 7 to 365 days 122 | - Corrected the click action for the stop button 123 | - Removed the unused "getIcon" web endpoint 124 | - Addressed bug where images in library did not scale correctly 125 | - Fixed button rendering issue with Firefox 126 | - Added browser specific CSS for opacity, blur, and drop shadow - needed for older Chromium builds 127 | - Added "engine" field to package.json to specify the minimum required Node.js version 128 | - Added ability to run the script from any location - better compatibility with Roon Extension Manager 129 | - Updated package dependencies 130 | - Miscellaneous UI tweaks 131 | 132 | ### Upgrade Notes 133 | 134 | The package dependencies have been updated to the latest versions. It is recommended to perform an update. 135 | 136 | On the Node.js server: 137 | 138 | 1. Stop the extension 139 | 1. Run `git pull` 140 | 1. Run `npm install` 141 | 1. Run `npm update` 142 | 1. Start the extension `node .` 143 | 144 | ## Version 1.2.0 145 | 146 | ### New 147 | 148 | - Library Browser 149 | - Search (Library -> Search) 150 | - Added theme button to "Now Playing" screen 151 | - Split "Now Playing" and "Library" into a standalone pages to allow for custom layouts (example side-by-side layout provided, http://localhost:8080/side-by-side.html) 152 | - Removed workaround now that API loop bug (https://github.com/RoonLabs/node-roon-api/issues/5) is resolved 153 | 154 | ### Upgrade Notes 155 | 156 | Due to the use of new Roon APIs, you must remove the old extension authorization and create a new one! 157 | 158 | On the Node.js server: 159 | 160 | 1. Stop the extension 161 | 162 | In an official Roon Client: 163 | 164 | 1. Go to Settings -> Extensions. 165 | 1. Click the "View" button. 166 | 1. Remove all previous instances of "Web Controller". 167 | 168 | On the Node.js server: 169 | 170 | 1. Run `git pull` 171 | 1. Run `npm install` 172 | 1. Start the extension `node .` 173 | 174 | In an official Roon Client: 175 | 176 | 1. Go to Settings -> Extensions. 177 | 1. Click the "Enable" button beside "Web Controller". 178 | 179 | ## Version 1.1.1 180 | 181 | ### Fixed Bugs 182 | 183 | - Addressed issue that caused icons to be very small on high DPI monitors and devices 184 | 185 | ## Version 1.1.0 186 | 187 | ### Install Notes 188 | 189 | See the [README.md](README.md) for installation instructions 190 | 191 | ### Upgrade notes 192 | 193 | Please run `npm install` after upgrading due to new dependencies. 194 | 195 | ### New 196 | 197 | - Complete rewrite of UI - now supports both Portrait and Landscape view making it more suitable for phones 198 | - Added new theme based on the dominant color of the cover art 199 | - Added volume controls 200 | - Added controls for Loop, Shuffle, and Auto Radio 201 | - Added a check to see if the extension is enabled 202 | - Added visual feedback when clicking overlay buttons 203 | - Switching zones is now much more responsive 204 | - Depreciated the "Light" and "Cover Light" themes 205 | 206 | ### Implemented Feature Requests 207 | 208 | - Added config file for server settings (config/local.json). The example file `config/local.conf.EXAMPLE` shows usage and is tracked by `git`. The `config/local.conf` is not tracked by `git` so that local settings will not be clobbered by `git pull`. 209 | - Added command line options for help and to set the server port (`node app.js -h` for usage). 210 | 211 | ### Fixed Bugs 212 | 213 | - After switching a zone, the zone_id did not update for the controls. This could lead to controlling the previous zone with the new zone's controls. 214 | - Tweaked jquery.simplemarquee.js to use "span" instead of "div". Caused problems with CSS layouts. 215 | - Rewrote zone message parser to handle multiple events per message. Caused problems with zone list improperly reflecting grouped and ungrouped zones. Likely root cause of previously reported problem with repeated listings in Zone Listings. 216 | 217 | ## Version 1.0.1 218 | 219 | - Resolved bug that could result in repeated listings in Zone Listings 220 | - Changed all icons to utilize SVGs from Material Design Icons (https://materialdesignicons.com/) 221 | - Dramactically reduced the number of times the icons were being called and redrawn 222 | - Changed default theme to "Cover Dark" 223 | - Set default icon theme to "Circles" 224 | - Added configuration option to select icons between icons with or without circles 225 | - Miscellaneous aesthetic changes 226 | 227 | ## Version 1.0.0 228 | 229 | - Initial Release 230 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | Copyright 2017 Mike Plugge 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Roon Web Controller 1.2.13 2 | 3 | This is an extension for the Roon music player that provides a web based remote. 4 | 5 | NOTE: this is the final release of the 1.x series. 6 | 7 | ## New features 8 | 9 | Security 10 | - updated dependencies for security issues. 11 | 12 | See the [CHANGELOG.md](CHANGELOG.md) for complete list of changes 13 | 14 | ### Upgrade notes 15 | 16 | The package dependencies have been updated to the latest versions. It is recommended to perform an update. 17 | 18 | If you followed the [Diet Pi installation](https://github.com/pluggemi/roon-web-controller/wiki/Diet-Pi-Installation) guide, follow these [update instructions](https://github.com/pluggemi/roon-web-controller/wiki/Diet-Pi-Installation#updating-the-web-controller-software) 19 | 20 | Otherwise, on the Node.js server: 21 | 22 | 1. Stop the extension 23 | 1. Run `git pull` 24 | 1. Run `npm install` 25 | 1. Run `npm update` 26 | 1. Start the extension 27 | 28 | ## Screenshots 29 | 30 | ### Dark Theme 31 | 32 | ![Dark Theme](https://raw.githubusercontent.com/pluggemi/project-screenshots/master/roon-web-controller/dark-Portrait.png) 33 | ![Dark Theme](https://raw.githubusercontent.com/pluggemi/project-screenshots/master/roon-web-controller/dark-Landscape.png) 34 | 35 | Album Credit: [Julia Kent, Asperities](http://music.juliakent.com/album/asperities) 36 | 37 | ### Cover Art Theme 38 | 39 | ![Cover Art Theme](https://raw.githubusercontent.com/pluggemi/project-screenshots/master/roon-web-controller/cover-Portrait.png) 40 | ![Cover Art Theme](https://raw.githubusercontent.com/pluggemi/project-screenshots/master/roon-web-controller/cover-Landscape.png) 41 | 42 | Album Credit: [Beats Antique, Blind Threshold](https://beatsantique.bandcamp.com/album/blind-threshold) 43 | 44 | ### Dominant Color Theme 45 | 46 | The icons and text in this theme automatically adjust to show light or dark depending on which would be more readable. 47 | ![Dominant Color Theme](https://raw.githubusercontent.com/pluggemi/project-screenshots/master/roon-web-controller/color-Portrait.png) 48 | ![Dominant Color Theme](https://raw.githubusercontent.com/pluggemi/project-screenshots/master/roon-web-controller/color-Landscape.png) 49 | 50 | Album Credit: [Carbon Based Lifeforms, Twentythree](https://carbonbasedlifeforms.bandcamp.com/album/twentythree) 51 | 52 | ### Library Browser - Home Screen 53 | 54 | ![Library - Home](https://raw.githubusercontent.com/pluggemi/project-screenshots/master/roon-web-controller/library-Portrait-home.png) 55 | ![Library - Home](https://raw.githubusercontent.com/pluggemi/project-screenshots/master/roon-web-controller/library-Landscape-home.png) 56 | 57 | ### Library Browser - Artist Screen 58 | 59 | ![Library - Artist](https://raw.githubusercontent.com/pluggemi/project-screenshots/master/roon-web-controller/library-Portrait-artist.png) 60 | ![Library - Artist](https://raw.githubusercontent.com/pluggemi/project-screenshots/master/roon-web-controller/library-Landscape-artist.png) 61 | 62 | ### Library Browser - Album Screen 63 | 64 | ![Library - Album](https://raw.githubusercontent.com/pluggemi/project-screenshots/master/roon-web-controller/library-Portrait-album.png) 65 | ![Library - Album](https://raw.githubusercontent.com/pluggemi/project-screenshots/master/roon-web-controller/library-Landscape-album.png) 66 | 67 | ## Installation 68 | 69 | Ensure that Node.js version 6.x or higher is installed. 70 | 71 | Grab the software: 72 | 73 | - Via git (preferred): `git clone https://github.com/pluggemi/roon-web-controller.git` 74 | - Or download and extract the zip file. 75 | 76 | Change directory into the software: 77 | `cd roon-web-controller` 78 | 79 | Install the Node.js modules 80 | `npm install` 81 | 82 | Run the application: 83 | `node app.js` 84 | 85 | On an existing Roon client, go to "Settings" then "Extensions". Click "Enable" beside "Web Controller". 86 | 87 | Open a web browser to your server at either "http://localhost:8080" or "http://(IP of Device):8080". 88 | 89 | ### Firewall ports 90 | 91 | Many operating systems now have a firewall enabled by default. Make sure that the port that this is running is open on the firewall. By default, this is TCP port 8080. But this can be changed with the configuration file or command line options below. 92 | 93 | ### (Optional) Local configuration file 94 | 95 | Simply copy `config/local.json.EXAMPLE` to `config/local.json` and edit `config/local.json` as needed. 96 | 97 | - `config/local.json` is not tracked by `git`, so it will not be clobbered with updates 98 | - `config/local.json.EXAMPLE` is tracked by `git` and will be updated in the future as new options are available 99 | 100 | Content of `config/local.json.EXAMPLE` 101 | 102 | ``` 103 | // Copy this file to "local.json" and change the port as desired. 104 | { 105 | "server": { 106 | "port": "1234" 107 | } 108 | } 109 | ``` 110 | 111 | ### (Optional) Command Line Options 112 | 113 | This is the output `node app.js -h` which shows usage of the command line options. 114 | 115 | ``` 116 | Roon Web Controller 117 | 118 | A web based controller for the Roon Media System. 119 | 120 | Usage: node app.js 121 | 122 | Options 123 | 124 | -h, --help Display this usage guide. 125 | -p, --port number Specify the port the server listens on. 126 | 127 | Project home: https://github.com/pluggemi/roon-web-controller 128 | ``` 129 | 130 | ### (Optional) Sample systemd unit file 131 | 132 | [systemd](https://www.freedesktop.org/wiki/Software/systemd/) is the init system used by modern Linux systems. Here is a sample systemd unit file which can be used to automatically start this application at Linux system boot time. 133 | 134 | ``` 135 | [Unit] 136 | Description=NodeJS app - Roon Web Controller 137 | After=network.target 138 | 139 | [Service] 140 | User=node 141 | WorkingDirectory=/srv/node/roon-web-controller 142 | ExecStart=/usr/bin/node app.js 143 | 144 | [Install] 145 | WantedBy=multi-user.target 146 | ``` 147 | 148 | #### Usage 149 | 150 | To use this unit file: 151 | 152 | - Save this template to a file called `roon-web-controller.service` 153 | - Edit the `user` field to be the user running the application 154 | - Edit the `WorkingDirectory` field to be the location where the application is installed (**NOTE**: the `user` must have read and write access to this location!) 155 | - Using either `sudo` or as `root`, copy the edited `roon-web-controller.service` file to `/usr/lib/systemd/system` 156 | - Reload systemd: `sudo systemctl daemon-reload` 157 | - Start the application: `sudo systemctl start roon-web-controller.service` 158 | - Enable the application at boot up: `sudo systemctl enable roon-web-controller.service` 159 | 160 | You can monitor the output of this application using `journalctl`. 161 | 162 | - To show the application output: `sudo journalctl -u roon-web-controller` 163 | - To follow the application output: `sudo journalctl -f -u roon-web-controller` 164 | 165 | **NOTE:** 166 | Some Linux distributions - including [DietPi](http://dietpi.com/) and [Software Collections](http://www.softwarecollections.org/) (addon repository for Red Hat, Centos, and Fedora) - install the Node.js binaries in a different location. You can find the executable by running `which node`. Adjust the `ExecStart` line accordingly. 167 | 168 | Here is a list of common locations: 169 | 170 | - `/usr/bin/node` 171 | - `/usr/local/bin/node` 172 | - `/opt/rh/rh-nodejs6/root/usr/bin/node` 173 | - `/opt/rh/rh-nodejs8/root/usr/bin/node` 174 | 175 | ## Credits 176 | 177 | In addition to those packages installed via npm, this project uses: 178 | 179 | - [jquery.simplemarquee.js](https://github.com/IndigoUnited/jquery.simplemarquee) to automatically scroll the long text 180 | - Color palette from the KDE Visual Design Group [Human Interface Guidelines](https://community.kde.org/KDE_Visual_Design_Group/HIG/Color) 181 | - Icons from [Material Design Icons](https://materialdesignicons.com/) 182 | - [Color Thief](https://github.com/lokesh/color-thief) to calculate the dominant color of the album art 183 | 184 | Thanks go to [st0g1e](https://github.com/st0g1e) for doing one of the first [web clients](https://github.com/st0g1e/roon-extension-ws-player) for the Roon API. 185 | 186 | Thanks go to [jcharr1](https://github.com/jcharr1) for suggesting and doing the initial implementation of OS native song notifications. 187 | 188 | And of course thanks go to [Roon Labs](https://roonlabs.com/) for making the music player and the [APIs](https://github.com/RoonLabs). 189 | 190 | ## License 191 | 192 | # The MIT License (MIT) 193 | 194 | Copyright (c) 2019 Mike Plugge 195 | 196 | Permission is hereby granted, free of charge, to any person obtaining a copy of 197 | this software and associated documentation files (the "Software"), to deal in 198 | the Software without restriction, including without limitation the rights to 199 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 200 | of the Software, and to permit persons to whom the Software is furnished to do 201 | so, subject to the following conditions: 202 | 203 | The above copyright notice and this permission notice shall be included in all 204 | copies or substantial portions of the Software. 205 | 206 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 207 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 208 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 209 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 210 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 211 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 212 | SOFTWARE. 213 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // Setup general variables 3 | var defaultListenPort = 8080; 4 | 5 | var core, transport; 6 | var pairStatus = 0; 7 | var zoneStatus = []; 8 | var zoneList = []; 9 | 10 | // Change to working directory 11 | try { 12 | process.chdir(__dirname); 13 | console.log(`Working directory: ${process.cwd()}`); 14 | } catch (err) { 15 | console.error(`chdir: ${err}`); 16 | } 17 | 18 | // Read command line options 19 | var commandLineArgs = require("command-line-args"); 20 | var getUsage = require("command-line-usage"); 21 | 22 | var optionDefinitions = [ 23 | { 24 | name: "help", 25 | alias: "h", 26 | description: "Display this usage guide.", 27 | type: Boolean 28 | }, 29 | { 30 | name: "port", 31 | alias: "p", 32 | description: "Specify the port the server listens on.", 33 | type: Number 34 | } 35 | ]; 36 | 37 | var options = commandLineArgs(optionDefinitions, { partial: true }); 38 | 39 | var usage = getUsage([ 40 | { 41 | header: "Roon Web Controller", 42 | content: 43 | "A web based controller for the Roon Music Player.\n\nUsage: {bold node app.js }" 44 | }, 45 | { 46 | header: "Options", 47 | optionList: optionDefinitions 48 | }, 49 | { 50 | content: 51 | "Project home: {underline https://github.com/pluggemi/roon-web-controller}" 52 | } 53 | ]); 54 | 55 | if (options.help) { 56 | console.log(usage); 57 | process.exit(); 58 | } 59 | 60 | // Read config file 61 | var config = require("config"); 62 | 63 | var configPort = config.get("server.port"); 64 | 65 | // Determine listen port 66 | if (options.port) { 67 | var listenPort = options.port; 68 | } else if (configPort) { 69 | var listenPort = configPort; 70 | } else { 71 | var listenPort = defaultListenPort; 72 | } 73 | // Setup Express 74 | var express = require("express"); 75 | var http = require("http"); 76 | var bodyParser = require("body-parser"); 77 | 78 | var app = express(); 79 | app.use(express.static("public")); 80 | app.use(bodyParser.json()); 81 | 82 | app.use(function(req, res, next) { 83 | res.header("Access-Control-Allow-Origin", "*"); 84 | res.header( 85 | "Access-Control-Allow-Headers", 86 | "Origin, X-Requested-With, Content-Type, Accept" 87 | ); 88 | next(); 89 | }); 90 | 91 | // Setup Socket IO 92 | var server = http.createServer(app); 93 | var io = require("socket.io").listen(server); 94 | 95 | server.listen(listenPort, function() { 96 | console.log("Listening on port " + listenPort); 97 | }); 98 | 99 | // Setup Roon 100 | var RoonApi = require("node-roon-api"); 101 | var RoonApiImage = require("node-roon-api-image"); 102 | var RoonApiStatus = require("node-roon-api-status"); 103 | var RoonApiTransport = require("node-roon-api-transport"); 104 | var RoonApiBrowse = require("node-roon-api-browse"); 105 | 106 | var roon = new RoonApi({ 107 | extension_id: "com.pluggemi.web.controller", 108 | display_name: "Web Controller", 109 | display_version: "1.2.13", 110 | publisher: "Mike Plugge", 111 | // log_level: "none", 112 | email: "masked", 113 | website: "https://github.com/pluggemi/roon-web-controller", 114 | 115 | core_paired: function(core_) { 116 | core = core_; 117 | 118 | pairStatus = true; 119 | io.emit("pairStatus", JSON.parse('{"pairEnabled": ' + pairStatus + "}")); 120 | 121 | transport = core_.services.RoonApiTransport; 122 | 123 | transport.subscribe_zones(function(response, data) { 124 | var i, x, y, zone_id, display_name; 125 | if (response == "Subscribed") { 126 | for (x in data.zones) { 127 | zone_id = data.zones[x].zone_id; 128 | display_name = data.zones[x].display_name; 129 | var item = {}; 130 | item.zone_id = zone_id; 131 | item.display_name = display_name; 132 | 133 | zoneList.push(item); 134 | zoneStatus.push(data.zones[x]); 135 | } 136 | 137 | removeDuplicateList(zoneList, "zone_id"); 138 | removeDuplicateStatus(zoneStatus, "zone_id"); 139 | } else if (response == "Changed") { 140 | for (i in data) { 141 | if (i == "zones_changed" || i == "zones_seek_changed") { 142 | for (x in data.zones_changed) { 143 | for (y in zoneStatus) { 144 | if (zoneStatus[y].zone_id == data.zones_changed[x].zone_id) { 145 | zoneStatus[y] = data.zones_changed[x]; 146 | } 147 | } 148 | } 149 | io.emit("zoneStatus", zoneStatus); 150 | } else if (i == "zones_added") { 151 | for (x in data.zones_added) { 152 | zone_id = data.zones_added[x].zone_id; 153 | display_name = data.zones_added[x].display_name; 154 | 155 | item = {}; 156 | item.zone_id = zone_id; 157 | item.display_name = display_name; 158 | 159 | zoneList.push(item); 160 | zoneStatus.push(data.zones_added[x]); 161 | } 162 | 163 | removeDuplicateList(zoneList, "zone_id"); 164 | removeDuplicateStatus(zoneStatus, "zone_id"); 165 | } else if (i == "zones_removed") { 166 | for (x in data.zones_removed) { 167 | zoneList = zoneList.filter(function(zone) { 168 | return zone.zone_id != data.zones_removed[x]; 169 | }); 170 | zoneStatus = zoneStatus.filter(function(zone) { 171 | return zone.zone_id != data.zones_removed[x]; 172 | }); 173 | } 174 | removeDuplicateList(zoneList, "zone_id"); 175 | removeDuplicateStatus(zoneStatus, "zone_id"); 176 | } 177 | } 178 | } 179 | }); 180 | }, 181 | 182 | core_unpaired: function(core_) { 183 | pairStatus = false; 184 | io.emit("pairStatus", JSON.parse('{"pairEnabled": ' + pairStatus + "}")); 185 | } 186 | }); 187 | 188 | var svc_status = new RoonApiStatus(roon); 189 | 190 | roon.init_services({ 191 | required_services: [RoonApiTransport, RoonApiImage, RoonApiBrowse], 192 | provided_services: [svc_status] 193 | }); 194 | 195 | svc_status.set_status("Extension enabled", false); 196 | 197 | roon.start_discovery(); 198 | 199 | // Remove duplicates from zoneList array 200 | function removeDuplicateList(array, property) { 201 | var x; 202 | var new_array = []; 203 | var lookup = {}; 204 | for (x in array) { 205 | lookup[array[x][property]] = array[x]; 206 | } 207 | 208 | for (x in lookup) { 209 | new_array.push(lookup[x]); 210 | } 211 | 212 | zoneList = new_array; 213 | io.emit("zoneList", zoneList); 214 | } 215 | 216 | // Remove duplicates from zoneStatus array 217 | function removeDuplicateStatus(array, property) { 218 | var x; 219 | var new_array = []; 220 | var lookup = {}; 221 | for (x in array) { 222 | lookup[array[x][property]] = array[x]; 223 | } 224 | 225 | for (x in lookup) { 226 | new_array.push(lookup[x]); 227 | } 228 | 229 | zoneStatus = new_array; 230 | io.emit("zoneStatus", zoneStatus); 231 | } 232 | 233 | function refresh_browse(zone_id, options, callback) { 234 | options = Object.assign( 235 | { 236 | hierarchy: "browse", 237 | zone_or_output_id: zone_id 238 | }, 239 | options 240 | ); 241 | 242 | core.services.RoonApiBrowse.browse(options, function(error, payload) { 243 | if (error) { 244 | console.log(error, payload); 245 | return; 246 | } 247 | 248 | if (payload.action == "list") { 249 | var items = []; 250 | if (payload.list.display_offset > 0) { 251 | var listoffset = payload.list.display_offset; 252 | } else { 253 | var listoffset = 0; 254 | } 255 | core.services.RoonApiBrowse.load( 256 | { 257 | hierarchy: "browse", 258 | offset: listoffset, 259 | set_display_offset: listoffset 260 | }, 261 | function(error, payload) { 262 | callback(payload); 263 | } 264 | ); 265 | } 266 | }); 267 | } 268 | 269 | function load_browse(listoffset, callback) { 270 | core.services.RoonApiBrowse.load( 271 | { 272 | hierarchy: "browse", 273 | offset: listoffset, 274 | set_display_offset: listoffset 275 | }, 276 | function(error, payload) { 277 | callback(payload); 278 | } 279 | ); 280 | } 281 | 282 | // ---------------------------- WEB SOCKET -------------- 283 | io.on("connection", function(socket) { 284 | io.emit("pairStatus", JSON.parse('{"pairEnabled": ' + pairStatus + "}")); 285 | io.emit("zoneList", zoneList); 286 | io.emit("zoneStatus", zoneStatus); 287 | 288 | socket.on("getZone", function() { 289 | io.emit("zoneStatus", zoneStatus); 290 | }); 291 | 292 | socket.on("changeVolume", function(msg) { 293 | transport.change_volume(msg.output_id, "absolute", msg.volume); 294 | }); 295 | 296 | socket.on("changeSetting", function(msg) { 297 | var settings = []; 298 | 299 | if (msg.setting == "shuffle") { 300 | settings.shuffle = msg.value; 301 | } else if (msg.setting == "auto_radio") { 302 | settings.auto_radio = msg.value; 303 | } else if (msg.setting == "loop") { 304 | settings.loop = msg.value; 305 | } 306 | 307 | transport.change_settings(msg.zone_id, settings, function(error) {}); 308 | }); 309 | 310 | socket.on("goPrev", function(msg) { 311 | transport.control(msg, "previous"); 312 | }); 313 | 314 | socket.on("goNext", function(msg) { 315 | transport.control(msg, "next"); 316 | }); 317 | 318 | socket.on("goPlayPause", function(msg) { 319 | transport.control(msg, "playpause"); 320 | }); 321 | 322 | socket.on("goPlay", function(msg) { 323 | transport.control(msg, "play"); 324 | }); 325 | 326 | socket.on("goPause", function(msg) { 327 | transport.control(msg, "pause"); 328 | }); 329 | 330 | socket.on("goStop", function(msg) { 331 | transport.control(msg, "stop"); 332 | }); 333 | }); 334 | 335 | // Web Routes 336 | app.get("/", function(req, res) { 337 | res.sendFile(__dirname + "/public/fullscreen.html"); 338 | }); 339 | 340 | app.get("/roonapi/getImage", function(req, res) { 341 | core.services.RoonApiImage.get_image( 342 | req.query.image_key, 343 | { scale: "fit", width: 1080, height: 1080, format: "image/jpeg" }, 344 | function(cb, contentType, body) { 345 | res.contentType = contentType; 346 | 347 | res.writeHead(200, { "Content-Type": "image/jpeg" }); 348 | res.end(body, "binary"); 349 | } 350 | ); 351 | }); 352 | 353 | app.get("/roonapi/getImage4k", function(req, res) { 354 | core.services.RoonApiImage.get_image( 355 | req.query.image_key, 356 | { scale: "fit", width: 2160, height: 2160, format: "image/jpeg" }, 357 | function(cb, contentType, body) { 358 | res.contentType = contentType; 359 | 360 | res.writeHead(200, { "Content-Type": "image/jpeg" }); 361 | res.end(body, "binary"); 362 | } 363 | ); 364 | }); 365 | 366 | app.post("/roonapi/goRefreshBrowse", function(req, res) { 367 | refresh_browse(req.body.zone_id, req.body.options, function(payload) { 368 | res.send({ data: payload }); 369 | }); 370 | }); 371 | 372 | app.post("/roonapi/goLoadBrowse", function(req, res) { 373 | load_browse(req.body.listoffset, function(payload) { 374 | res.send({ data: payload }); 375 | }); 376 | }); 377 | 378 | app.use( 379 | "/jquery/jquery.min.js", 380 | express.static(__dirname + "/node_modules/jquery/dist/jquery.min.js") 381 | ); 382 | 383 | app.use( 384 | "/js-cookie/js.cookie.js", 385 | express.static(__dirname + "/node_modules/js-cookie/src/js.cookie.js") 386 | ); 387 | -------------------------------------------------------------------------------- /config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": { 3 | "port": "8080" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /config/local.json.EXAMPLE: -------------------------------------------------------------------------------- 1 | // Copy this file to "local.json" and change the port as desired. 2 | 3 | { 4 | "server": { 5 | "port": "1234" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "roon-web-controller", 3 | "version": "1.2.12", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "accepts": { 8 | "version": "1.3.7", 9 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", 10 | "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", 11 | "requires": { 12 | "mime-types": "~2.1.24", 13 | "negotiator": "0.6.2" 14 | } 15 | }, 16 | "after": { 17 | "version": "0.8.2", 18 | "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", 19 | "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" 20 | }, 21 | "ansi-styles": { 22 | "version": "3.2.1", 23 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 24 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 25 | "requires": { 26 | "color-convert": "^1.9.0" 27 | } 28 | }, 29 | "array-back": { 30 | "version": "3.1.0", 31 | "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", 32 | "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==" 33 | }, 34 | "array-flatten": { 35 | "version": "1.1.1", 36 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 37 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 38 | }, 39 | "arraybuffer.slice": { 40 | "version": "0.0.7", 41 | "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", 42 | "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==" 43 | }, 44 | "async-limiter": { 45 | "version": "1.0.1", 46 | "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", 47 | "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" 48 | }, 49 | "backo2": { 50 | "version": "1.0.2", 51 | "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", 52 | "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" 53 | }, 54 | "base64-arraybuffer": { 55 | "version": "0.1.5", 56 | "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", 57 | "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=" 58 | }, 59 | "base64id": { 60 | "version": "2.0.0", 61 | "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", 62 | "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" 63 | }, 64 | "better-assert": { 65 | "version": "1.0.2", 66 | "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", 67 | "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", 68 | "requires": { 69 | "callsite": "1.0.0" 70 | } 71 | }, 72 | "blob": { 73 | "version": "0.0.5", 74 | "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", 75 | "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==" 76 | }, 77 | "body-parser": { 78 | "version": "1.19.0", 79 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", 80 | "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", 81 | "requires": { 82 | "bytes": "3.1.0", 83 | "content-type": "~1.0.4", 84 | "debug": "2.6.9", 85 | "depd": "~1.1.2", 86 | "http-errors": "1.7.2", 87 | "iconv-lite": "0.4.24", 88 | "on-finished": "~2.3.0", 89 | "qs": "6.7.0", 90 | "raw-body": "2.4.0", 91 | "type-is": "~1.6.17" 92 | } 93 | }, 94 | "bytes": { 95 | "version": "3.1.0", 96 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", 97 | "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" 98 | }, 99 | "callsite": { 100 | "version": "1.0.0", 101 | "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", 102 | "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" 103 | }, 104 | "chalk": { 105 | "version": "2.4.2", 106 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 107 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 108 | "requires": { 109 | "ansi-styles": "^3.2.1", 110 | "escape-string-regexp": "^1.0.5", 111 | "supports-color": "^5.3.0" 112 | } 113 | }, 114 | "color-convert": { 115 | "version": "1.9.3", 116 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 117 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 118 | "requires": { 119 | "color-name": "1.1.3" 120 | } 121 | }, 122 | "color-name": { 123 | "version": "1.1.3", 124 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 125 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" 126 | }, 127 | "command-line-args": { 128 | "version": "5.1.1", 129 | "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.1.1.tgz", 130 | "integrity": "sha512-hL/eG8lrll1Qy1ezvkant+trihbGnaKaeEjj6Scyr3DN+RC7iQ5Rz84IeLERfAWDGo0HBSNAakczwgCilDXnWg==", 131 | "requires": { 132 | "array-back": "^3.0.1", 133 | "find-replace": "^3.0.0", 134 | "lodash.camelcase": "^4.3.0", 135 | "typical": "^4.0.0" 136 | } 137 | }, 138 | "command-line-usage": { 139 | "version": "6.1.0", 140 | "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-6.1.0.tgz", 141 | "integrity": "sha512-Ew1clU4pkUeo6AFVDFxCbnN7GIZfXl48HIOQeFQnkO3oOqvpI7wdqtLRwv9iOCZ/7A+z4csVZeiDdEcj8g6Wiw==", 142 | "requires": { 143 | "array-back": "^4.0.0", 144 | "chalk": "^2.4.2", 145 | "table-layout": "^1.0.0", 146 | "typical": "^5.2.0" 147 | }, 148 | "dependencies": { 149 | "array-back": { 150 | "version": "4.0.1", 151 | "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.1.tgz", 152 | "integrity": "sha512-Z/JnaVEXv+A9xabHzN43FiiiWEE7gPCRXMrVmRm00tWbjZRul1iHm7ECzlyNq1p4a4ATXz+G9FJ3GqGOkOV3fg==" 153 | }, 154 | "typical": { 155 | "version": "5.2.0", 156 | "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", 157 | "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==" 158 | } 159 | } 160 | }, 161 | "component-bind": { 162 | "version": "1.0.0", 163 | "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", 164 | "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" 165 | }, 166 | "component-emitter": { 167 | "version": "1.2.1", 168 | "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", 169 | "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" 170 | }, 171 | "component-inherit": { 172 | "version": "0.0.3", 173 | "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", 174 | "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" 175 | }, 176 | "config": { 177 | "version": "3.3.1", 178 | "resolved": "https://registry.npmjs.org/config/-/config-3.3.1.tgz", 179 | "integrity": "sha512-+2/KaaaAzdwUBE3jgZON11L1ggLLhpf2FsGrfqYFHZW22ySGv/HqYIXrBwKKvn+XZh1UBUjHwAcrfsSkSygT+Q==", 180 | "requires": { 181 | "json5": "^2.1.1" 182 | } 183 | }, 184 | "content-disposition": { 185 | "version": "0.5.3", 186 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", 187 | "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", 188 | "requires": { 189 | "safe-buffer": "5.1.2" 190 | } 191 | }, 192 | "content-type": { 193 | "version": "1.0.4", 194 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 195 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 196 | }, 197 | "cookie": { 198 | "version": "0.4.0", 199 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", 200 | "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" 201 | }, 202 | "cookie-signature": { 203 | "version": "1.0.6", 204 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 205 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 206 | }, 207 | "debug": { 208 | "version": "2.6.9", 209 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 210 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 211 | "requires": { 212 | "ms": "2.0.0" 213 | } 214 | }, 215 | "deep-extend": { 216 | "version": "0.6.0", 217 | "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", 218 | "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" 219 | }, 220 | "depd": { 221 | "version": "1.1.2", 222 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 223 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 224 | }, 225 | "destroy": { 226 | "version": "1.0.4", 227 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 228 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 229 | }, 230 | "ee-first": { 231 | "version": "1.1.1", 232 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 233 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 234 | }, 235 | "encodeurl": { 236 | "version": "1.0.2", 237 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 238 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" 239 | }, 240 | "engine.io": { 241 | "version": "3.4.0", 242 | "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.4.0.tgz", 243 | "integrity": "sha512-XCyYVWzcHnK5cMz7G4VTu2W7zJS7SM1QkcelghyIk/FmobWBtXE7fwhBusEKvCSqc3bMh8fNFMlUkCKTFRxH2w==", 244 | "requires": { 245 | "accepts": "~1.3.4", 246 | "base64id": "2.0.0", 247 | "cookie": "0.3.1", 248 | "debug": "~4.1.0", 249 | "engine.io-parser": "~2.2.0", 250 | "ws": "^7.1.2" 251 | }, 252 | "dependencies": { 253 | "cookie": { 254 | "version": "0.3.1", 255 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", 256 | "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" 257 | }, 258 | "debug": { 259 | "version": "4.1.1", 260 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", 261 | "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", 262 | "requires": { 263 | "ms": "^2.1.1" 264 | } 265 | }, 266 | "ms": { 267 | "version": "2.1.2", 268 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 269 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 270 | } 271 | } 272 | }, 273 | "engine.io-client": { 274 | "version": "3.4.0", 275 | "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.4.0.tgz", 276 | "integrity": "sha512-a4J5QO2k99CM2a0b12IznnyQndoEvtA4UAldhGzKqnHf42I3Qs2W5SPnDvatZRcMaNZs4IevVicBPayxYt6FwA==", 277 | "requires": { 278 | "component-emitter": "1.2.1", 279 | "component-inherit": "0.0.3", 280 | "debug": "~4.1.0", 281 | "engine.io-parser": "~2.2.0", 282 | "has-cors": "1.1.0", 283 | "indexof": "0.0.1", 284 | "parseqs": "0.0.5", 285 | "parseuri": "0.0.5", 286 | "ws": "~6.1.0", 287 | "xmlhttprequest-ssl": "~1.5.4", 288 | "yeast": "0.1.2" 289 | }, 290 | "dependencies": { 291 | "debug": { 292 | "version": "4.1.1", 293 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", 294 | "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", 295 | "requires": { 296 | "ms": "^2.1.1" 297 | } 298 | }, 299 | "ms": { 300 | "version": "2.1.2", 301 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 302 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 303 | }, 304 | "ws": { 305 | "version": "6.1.4", 306 | "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz", 307 | "integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==", 308 | "requires": { 309 | "async-limiter": "~1.0.0" 310 | } 311 | } 312 | } 313 | }, 314 | "engine.io-parser": { 315 | "version": "2.2.0", 316 | "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.0.tgz", 317 | "integrity": "sha512-6I3qD9iUxotsC5HEMuuGsKA0cXerGz+4uGcXQEkfBidgKf0amsjrrtwcbwK/nzpZBxclXlV7gGl9dgWvu4LF6w==", 318 | "requires": { 319 | "after": "0.8.2", 320 | "arraybuffer.slice": "~0.0.7", 321 | "base64-arraybuffer": "0.1.5", 322 | "blob": "0.0.5", 323 | "has-binary2": "~1.0.2" 324 | } 325 | }, 326 | "escape-html": { 327 | "version": "1.0.3", 328 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 329 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 330 | }, 331 | "escape-string-regexp": { 332 | "version": "1.0.5", 333 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 334 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" 335 | }, 336 | "etag": { 337 | "version": "1.8.1", 338 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 339 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 340 | }, 341 | "express": { 342 | "version": "4.17.1", 343 | "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", 344 | "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", 345 | "requires": { 346 | "accepts": "~1.3.7", 347 | "array-flatten": "1.1.1", 348 | "body-parser": "1.19.0", 349 | "content-disposition": "0.5.3", 350 | "content-type": "~1.0.4", 351 | "cookie": "0.4.0", 352 | "cookie-signature": "1.0.6", 353 | "debug": "2.6.9", 354 | "depd": "~1.1.2", 355 | "encodeurl": "~1.0.2", 356 | "escape-html": "~1.0.3", 357 | "etag": "~1.8.1", 358 | "finalhandler": "~1.1.2", 359 | "fresh": "0.5.2", 360 | "merge-descriptors": "1.0.1", 361 | "methods": "~1.1.2", 362 | "on-finished": "~2.3.0", 363 | "parseurl": "~1.3.3", 364 | "path-to-regexp": "0.1.7", 365 | "proxy-addr": "~2.0.5", 366 | "qs": "6.7.0", 367 | "range-parser": "~1.2.1", 368 | "safe-buffer": "5.1.2", 369 | "send": "0.17.1", 370 | "serve-static": "1.14.1", 371 | "setprototypeof": "1.1.1", 372 | "statuses": "~1.5.0", 373 | "type-is": "~1.6.18", 374 | "utils-merge": "1.0.1", 375 | "vary": "~1.1.2" 376 | } 377 | }, 378 | "finalhandler": { 379 | "version": "1.1.2", 380 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", 381 | "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", 382 | "requires": { 383 | "debug": "2.6.9", 384 | "encodeurl": "~1.0.2", 385 | "escape-html": "~1.0.3", 386 | "on-finished": "~2.3.0", 387 | "parseurl": "~1.3.3", 388 | "statuses": "~1.5.0", 389 | "unpipe": "~1.0.0" 390 | } 391 | }, 392 | "find-replace": { 393 | "version": "3.0.0", 394 | "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", 395 | "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", 396 | "requires": { 397 | "array-back": "^3.0.1" 398 | } 399 | }, 400 | "forwarded": { 401 | "version": "0.1.2", 402 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 403 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 404 | }, 405 | "fresh": { 406 | "version": "0.5.2", 407 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 408 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 409 | }, 410 | "has-binary2": { 411 | "version": "1.0.3", 412 | "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", 413 | "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", 414 | "requires": { 415 | "isarray": "2.0.1" 416 | } 417 | }, 418 | "has-cors": { 419 | "version": "1.1.0", 420 | "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", 421 | "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" 422 | }, 423 | "has-flag": { 424 | "version": "3.0.0", 425 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 426 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" 427 | }, 428 | "http-errors": { 429 | "version": "1.7.2", 430 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", 431 | "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", 432 | "requires": { 433 | "depd": "~1.1.2", 434 | "inherits": "2.0.3", 435 | "setprototypeof": "1.1.1", 436 | "statuses": ">= 1.5.0 < 2", 437 | "toidentifier": "1.0.0" 438 | } 439 | }, 440 | "iconv-lite": { 441 | "version": "0.4.24", 442 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 443 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 444 | "requires": { 445 | "safer-buffer": ">= 2.1.2 < 3" 446 | } 447 | }, 448 | "indexof": { 449 | "version": "0.0.1", 450 | "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", 451 | "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" 452 | }, 453 | "inherits": { 454 | "version": "2.0.3", 455 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 456 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 457 | }, 458 | "ip": { 459 | "version": "1.1.5", 460 | "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", 461 | "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" 462 | }, 463 | "ipaddr.js": { 464 | "version": "1.9.0", 465 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", 466 | "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==" 467 | }, 468 | "isarray": { 469 | "version": "2.0.1", 470 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", 471 | "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" 472 | }, 473 | "jquery": { 474 | "version": "3.5.1", 475 | "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz", 476 | "integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg==" 477 | }, 478 | "js-cookie": { 479 | "version": "2.2.1", 480 | "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", 481 | "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==" 482 | }, 483 | "json5": { 484 | "version": "2.1.3", 485 | "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", 486 | "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", 487 | "requires": { 488 | "minimist": "^1.2.5" 489 | } 490 | }, 491 | "lodash.camelcase": { 492 | "version": "4.3.0", 493 | "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", 494 | "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" 495 | }, 496 | "media-typer": { 497 | "version": "0.3.0", 498 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 499 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 500 | }, 501 | "merge-descriptors": { 502 | "version": "1.0.1", 503 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 504 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 505 | }, 506 | "methods": { 507 | "version": "1.1.2", 508 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 509 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 510 | }, 511 | "mime": { 512 | "version": "1.6.0", 513 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 514 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" 515 | }, 516 | "mime-db": { 517 | "version": "1.40.0", 518 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", 519 | "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" 520 | }, 521 | "mime-types": { 522 | "version": "2.1.24", 523 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", 524 | "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", 525 | "requires": { 526 | "mime-db": "1.40.0" 527 | } 528 | }, 529 | "minimist": { 530 | "version": "1.2.5", 531 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", 532 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" 533 | }, 534 | "ms": { 535 | "version": "2.0.0", 536 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 537 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 538 | }, 539 | "negotiator": { 540 | "version": "0.6.2", 541 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", 542 | "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" 543 | }, 544 | "node-roon-api": { 545 | "version": "github:roonlabs/node-roon-api#b09c875738360a9413518a8a51ac70294745a926", 546 | "from": "github:roonlabs/node-roon-api", 547 | "requires": { 548 | "ip": "^1.1.3", 549 | "node-uuid": "^1.4.7", 550 | "ws": ">=3.3.1" 551 | } 552 | }, 553 | "node-roon-api-browse": { 554 | "version": "github:roonlabs/node-roon-api-browse#98adcbaba3a30fd6ce109c1f04cd361536143222", 555 | "from": "github:roonlabs/node-roon-api-browse" 556 | }, 557 | "node-roon-api-image": { 558 | "version": "github:roonlabs/node-roon-api-image#a5f0efaf2dfb5457e91abe326c3a40d865131d32", 559 | "from": "github:roonlabs/node-roon-api-image" 560 | }, 561 | "node-roon-api-status": { 562 | "version": "github:roonlabs/node-roon-api-status#504c918d6da267e03fbb4337befa71ca3d3c7526", 563 | "from": "github:roonlabs/node-roon-api-status" 564 | }, 565 | "node-roon-api-transport": { 566 | "version": "github:roonlabs/node-roon-api-transport#0e6189de2935eac81e1d9fcc2e8d12c654fcb87a", 567 | "from": "github:roonlabs/node-roon-api-transport" 568 | }, 569 | "node-uuid": { 570 | "version": "1.4.8", 571 | "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz", 572 | "integrity": "sha1-sEDrCSOWivq/jTL7HxfxFn/auQc=" 573 | }, 574 | "object-component": { 575 | "version": "0.0.3", 576 | "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", 577 | "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=" 578 | }, 579 | "on-finished": { 580 | "version": "2.3.0", 581 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 582 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 583 | "requires": { 584 | "ee-first": "1.1.1" 585 | } 586 | }, 587 | "parseqs": { 588 | "version": "0.0.5", 589 | "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", 590 | "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", 591 | "requires": { 592 | "better-assert": "~1.0.0" 593 | } 594 | }, 595 | "parseuri": { 596 | "version": "0.0.5", 597 | "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", 598 | "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", 599 | "requires": { 600 | "better-assert": "~1.0.0" 601 | } 602 | }, 603 | "parseurl": { 604 | "version": "1.3.3", 605 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 606 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" 607 | }, 608 | "path-to-regexp": { 609 | "version": "0.1.7", 610 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 611 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 612 | }, 613 | "proxy-addr": { 614 | "version": "2.0.5", 615 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", 616 | "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", 617 | "requires": { 618 | "forwarded": "~0.1.2", 619 | "ipaddr.js": "1.9.0" 620 | } 621 | }, 622 | "qs": { 623 | "version": "6.7.0", 624 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", 625 | "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" 626 | }, 627 | "range-parser": { 628 | "version": "1.2.1", 629 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 630 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" 631 | }, 632 | "raw-body": { 633 | "version": "2.4.0", 634 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", 635 | "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", 636 | "requires": { 637 | "bytes": "3.1.0", 638 | "http-errors": "1.7.2", 639 | "iconv-lite": "0.4.24", 640 | "unpipe": "1.0.0" 641 | } 642 | }, 643 | "reduce-flatten": { 644 | "version": "2.0.0", 645 | "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz", 646 | "integrity": "sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==" 647 | }, 648 | "safe-buffer": { 649 | "version": "5.1.2", 650 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 651 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 652 | }, 653 | "safer-buffer": { 654 | "version": "2.1.2", 655 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 656 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 657 | }, 658 | "send": { 659 | "version": "0.17.1", 660 | "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", 661 | "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", 662 | "requires": { 663 | "debug": "2.6.9", 664 | "depd": "~1.1.2", 665 | "destroy": "~1.0.4", 666 | "encodeurl": "~1.0.2", 667 | "escape-html": "~1.0.3", 668 | "etag": "~1.8.1", 669 | "fresh": "0.5.2", 670 | "http-errors": "~1.7.2", 671 | "mime": "1.6.0", 672 | "ms": "2.1.1", 673 | "on-finished": "~2.3.0", 674 | "range-parser": "~1.2.1", 675 | "statuses": "~1.5.0" 676 | }, 677 | "dependencies": { 678 | "ms": { 679 | "version": "2.1.1", 680 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 681 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" 682 | } 683 | } 684 | }, 685 | "serve-static": { 686 | "version": "1.14.1", 687 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", 688 | "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", 689 | "requires": { 690 | "encodeurl": "~1.0.2", 691 | "escape-html": "~1.0.3", 692 | "parseurl": "~1.3.3", 693 | "send": "0.17.1" 694 | } 695 | }, 696 | "setprototypeof": { 697 | "version": "1.1.1", 698 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", 699 | "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" 700 | }, 701 | "socket.io": { 702 | "version": "2.3.0", 703 | "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.3.0.tgz", 704 | "integrity": "sha512-2A892lrj0GcgR/9Qk81EaY2gYhCBxurV0PfmmESO6p27QPrUK1J3zdns+5QPqvUYK2q657nSj0guoIil9+7eFg==", 705 | "requires": { 706 | "debug": "~4.1.0", 707 | "engine.io": "~3.4.0", 708 | "has-binary2": "~1.0.2", 709 | "socket.io-adapter": "~1.1.0", 710 | "socket.io-client": "2.3.0", 711 | "socket.io-parser": "~3.4.0" 712 | }, 713 | "dependencies": { 714 | "debug": { 715 | "version": "4.1.1", 716 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", 717 | "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", 718 | "requires": { 719 | "ms": "^2.1.1" 720 | } 721 | }, 722 | "ms": { 723 | "version": "2.1.2", 724 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 725 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 726 | } 727 | } 728 | }, 729 | "socket.io-adapter": { 730 | "version": "1.1.1", 731 | "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz", 732 | "integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs=" 733 | }, 734 | "socket.io-client": { 735 | "version": "2.3.0", 736 | "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.3.0.tgz", 737 | "integrity": "sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA==", 738 | "requires": { 739 | "backo2": "1.0.2", 740 | "base64-arraybuffer": "0.1.5", 741 | "component-bind": "1.0.0", 742 | "component-emitter": "1.2.1", 743 | "debug": "~4.1.0", 744 | "engine.io-client": "~3.4.0", 745 | "has-binary2": "~1.0.2", 746 | "has-cors": "1.1.0", 747 | "indexof": "0.0.1", 748 | "object-component": "0.0.3", 749 | "parseqs": "0.0.5", 750 | "parseuri": "0.0.5", 751 | "socket.io-parser": "~3.3.0", 752 | "to-array": "0.1.4" 753 | }, 754 | "dependencies": { 755 | "debug": { 756 | "version": "4.1.1", 757 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", 758 | "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", 759 | "requires": { 760 | "ms": "^2.1.1" 761 | } 762 | }, 763 | "ms": { 764 | "version": "2.1.2", 765 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 766 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 767 | }, 768 | "socket.io-parser": { 769 | "version": "3.3.0", 770 | "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.0.tgz", 771 | "integrity": "sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng==", 772 | "requires": { 773 | "component-emitter": "1.2.1", 774 | "debug": "~3.1.0", 775 | "isarray": "2.0.1" 776 | }, 777 | "dependencies": { 778 | "debug": { 779 | "version": "3.1.0", 780 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 781 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 782 | "requires": { 783 | "ms": "2.0.0" 784 | } 785 | }, 786 | "ms": { 787 | "version": "2.0.0", 788 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 789 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 790 | } 791 | } 792 | } 793 | } 794 | }, 795 | "socket.io-parser": { 796 | "version": "3.4.0", 797 | "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.4.0.tgz", 798 | "integrity": "sha512-/G/VOI+3DBp0+DJKW4KesGnQkQPFmUCbA/oO2QGT6CWxU7hLGWqU3tyuzeSK/dqcyeHsQg1vTe9jiZI8GU9SCQ==", 799 | "requires": { 800 | "component-emitter": "1.2.1", 801 | "debug": "~4.1.0", 802 | "isarray": "2.0.1" 803 | }, 804 | "dependencies": { 805 | "debug": { 806 | "version": "4.1.1", 807 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", 808 | "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", 809 | "requires": { 810 | "ms": "^2.1.1" 811 | } 812 | }, 813 | "ms": { 814 | "version": "2.1.2", 815 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 816 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 817 | } 818 | } 819 | }, 820 | "statuses": { 821 | "version": "1.5.0", 822 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 823 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" 824 | }, 825 | "supports-color": { 826 | "version": "5.5.0", 827 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 828 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 829 | "requires": { 830 | "has-flag": "^3.0.0" 831 | } 832 | }, 833 | "table-layout": { 834 | "version": "1.0.1", 835 | "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-1.0.1.tgz", 836 | "integrity": "sha512-dEquqYNJiGwY7iPfZ3wbXDI944iqanTSchrACLL2nOB+1r+h1Nzu2eH+DuPPvWvm5Ry7iAPeFlgEtP5bIp5U7Q==", 837 | "requires": { 838 | "array-back": "^4.0.1", 839 | "deep-extend": "~0.6.0", 840 | "typical": "^5.2.0", 841 | "wordwrapjs": "^4.0.0" 842 | }, 843 | "dependencies": { 844 | "array-back": { 845 | "version": "4.0.1", 846 | "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.1.tgz", 847 | "integrity": "sha512-Z/JnaVEXv+A9xabHzN43FiiiWEE7gPCRXMrVmRm00tWbjZRul1iHm7ECzlyNq1p4a4ATXz+G9FJ3GqGOkOV3fg==" 848 | }, 849 | "typical": { 850 | "version": "5.2.0", 851 | "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", 852 | "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==" 853 | } 854 | } 855 | }, 856 | "to-array": { 857 | "version": "0.1.4", 858 | "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", 859 | "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=" 860 | }, 861 | "toidentifier": { 862 | "version": "1.0.0", 863 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", 864 | "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" 865 | }, 866 | "type-is": { 867 | "version": "1.6.18", 868 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 869 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 870 | "requires": { 871 | "media-typer": "0.3.0", 872 | "mime-types": "~2.1.24" 873 | } 874 | }, 875 | "typical": { 876 | "version": "4.0.0", 877 | "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", 878 | "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==" 879 | }, 880 | "unpipe": { 881 | "version": "1.0.0", 882 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 883 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 884 | }, 885 | "utils-merge": { 886 | "version": "1.0.1", 887 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 888 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 889 | }, 890 | "vary": { 891 | "version": "1.1.2", 892 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 893 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 894 | }, 895 | "wordwrapjs": { 896 | "version": "4.0.0", 897 | "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-4.0.0.tgz", 898 | "integrity": "sha512-Svqw723a3R34KvsMgpjFBYCgNOSdcW3mQFK4wIfhGQhtaFVOJmdYoXgi63ne3dTlWgatVcUc7t4HtQ/+bUVIzQ==", 899 | "requires": { 900 | "reduce-flatten": "^2.0.0", 901 | "typical": "^5.0.0" 902 | }, 903 | "dependencies": { 904 | "typical": { 905 | "version": "5.2.0", 906 | "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", 907 | "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==" 908 | } 909 | } 910 | }, 911 | "ws": { 912 | "version": "7.1.2", 913 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.1.2.tgz", 914 | "integrity": "sha512-gftXq3XI81cJCgkUiAVixA0raD9IVmXqsylCrjRygw4+UOOGzPoxnQ6r/CnVL9i+mDncJo94tSkyrtuuQVBmrg==", 915 | "requires": { 916 | "async-limiter": "^1.0.0" 917 | } 918 | }, 919 | "xmlhttprequest-ssl": { 920 | "version": "1.5.5", 921 | "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", 922 | "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=" 923 | }, 924 | "yeast": { 925 | "version": "0.1.2", 926 | "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", 927 | "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" 928 | } 929 | } 930 | } 931 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "roon-web-controller", 3 | "version": "1.2.13", 4 | "description": "Roon Extension to create a web controller.", 5 | "main": "app.js", 6 | "author": "Mike Plugge", 7 | "license": "MIT", 8 | "engines": { 9 | "node": ">=6.x" 10 | }, 11 | "scripts": { 12 | "start": "node app.js" 13 | }, 14 | "dependencies": { 15 | "body-parser": "^1.19.0", 16 | "command-line-args": "^5.1.1", 17 | "command-line-usage": "^6.1.0", 18 | "config": "^3.3.1", 19 | "express": "4.x", 20 | "jquery": "^3.5.1", 21 | "js-cookie": "^2.2.1", 22 | "node-roon-api": "github:roonlabs/node-roon-api", 23 | "node-roon-api-browse": "github:roonlabs/node-roon-api-browse", 24 | "node-roon-api-image": "github:roonlabs/node-roon-api-image", 25 | "node-roon-api-status": "github:roonlabs/node-roon-api-status", 26 | "node-roon-api-transport": "github:roonlabs/node-roon-api-transport", 27 | "socket.io": "^2.3.0" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/pluggemi/roon-web-controller.git" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /public/css/library.css: -------------------------------------------------------------------------------- 1 | .navGroup { 2 | height: 100%; 3 | display: flex; 4 | flex-direction: row; 5 | flex-wrap: wrap; 6 | align-items: center; 7 | vertical-align: middle; 8 | } 9 | 10 | .navLeft { 11 | justify-content: flex-start; 12 | } 13 | 14 | .navCenter { 15 | justify-content: center; 16 | } 17 | 18 | .navRight { 19 | justify-content: flex-end; 20 | } 21 | 22 | .navButton { 23 | height: 100%; 24 | background-color: rgba(0,0,0,0); 25 | fill-opacity: 1; 26 | color: #eff0f1; 27 | } 28 | 29 | .navButton:disabled { 30 | fill-opacity: 0.33; 31 | } 32 | 33 | .hidden { 34 | display: none; 35 | } 36 | 37 | .itemListItem { 38 | background-color: #4d4d4d; 39 | fill-opacity: 1; 40 | color: #eff0f1; 41 | min-height: 48px; 42 | width: 100%; 43 | -webkit-filter: drop-shadow(5px 5px 5px black); 44 | filter: drop-shadow(5px 5px 5px black); 45 | margin-bottom: 10px; 46 | font-weight: bold; 47 | text-align: left; 48 | } 49 | 50 | .itemListItem:active { 51 | transform: translateY(4px); 52 | filter:none; 53 | } 54 | 55 | .itemListButton { 56 | height: 100%; 57 | background-color: #4d4d4d; 58 | fill-opacity: 1; 59 | color: #eff0f1; 60 | } 61 | 62 | .itemListButton:active { 63 | transform: translateY(4px); 64 | filter:none; 65 | } 66 | 67 | .listInfoImage { 68 | max-height: 98%; 69 | max-width: 98%; 70 | border-radius: 10px; 71 | width: auto; 72 | height: auto; 73 | object-fit: contain; 74 | filter: drop-shadow(5px 5px 5px black); 75 | } 76 | 77 | .searchGroup { 78 | height: 28px; 79 | width: 100%; 80 | -webkit-filter: drop-shadow(5px 5px 5px black); 81 | filter: drop-shadow(5px 5px 5px black); 82 | margin-bottom: 10px; 83 | display: flex; 84 | flex-direction: row; 85 | flex-wrap: wrap; 86 | align-items: center; 87 | vertical-align: middle; 88 | justify-content: center; 89 | } 90 | 91 | .searchForm { 92 | height: 100%; 93 | width: 80%; 94 | border: 0; 95 | padding: 0; 96 | font-size: 24px; 97 | } 98 | 99 | #navLine1 { 100 | position: absolute; 101 | top: 0; 102 | left: 0; 103 | right: 0; 104 | height: 40px; 105 | display: flex; 106 | flex-direction: row; 107 | flex-wrap: wrap; 108 | justify-content: space-between; 109 | align-items: center; 110 | vertical-align: middle; 111 | } 112 | 113 | #content { 114 | position: absolute; 115 | top: 48px; 116 | left: 0; 117 | right: 0; 118 | bottom: 48px; 119 | overflow: auto; 120 | } 121 | #listContainer { 122 | display: flex; 123 | flex-direction: row; 124 | justify-content: space-around; 125 | max-height: 40%; 126 | margin-bottom: 10px; 127 | } 128 | 129 | #listImage { 130 | display: flex; 131 | justify-content: center; 132 | width: 49%; 133 | } 134 | 135 | #listInfo { 136 | display: flex; 137 | flex-direction: column; 138 | justify-content: center; 139 | align-items: center; 140 | text-align: center; 141 | width: 49%; 142 | } 143 | 144 | #navLine2 { 145 | position: absolute; 146 | left: 0; 147 | right: 0; 148 | bottom: 2px; 149 | height: 40px; 150 | } 151 | -------------------------------------------------------------------------------- /public/css/nowplaying.css: -------------------------------------------------------------------------------- 1 | .buttonZoneName { 2 | background-color: rgba(0,0,0,0); 3 | width: auto; 4 | 5 | } 6 | .buttonAvailable { 7 | fill-opacity: 1; 8 | } 9 | 10 | .buttonActive { 11 | color: #3daee9; 12 | fill-opacity: 1; 13 | } 14 | 15 | .buttonInactive { 16 | fill-opacity: 0.33; 17 | } 18 | 19 | .itemImage { 20 | border-radius: 10px; 21 | width: auto; 22 | height: auto; 23 | object-fit: contain; 24 | -webkit-filter: drop-shadow(5px 5px 5px black); 25 | filter: drop-shadow(5px 5px 5px black); 26 | } 27 | 28 | @media screen and (orientation:portrait) { 29 | #containerCoverImage { 30 | position: absolute; 31 | top: 0px; 32 | right: 0px; 33 | left: 0px; 34 | height: 59%; 35 | } 36 | 37 | #containerMusicInfo { 38 | position: absolute; 39 | bottom:0px; 40 | left: 0px; 41 | right: 0px; 42 | height: 40%; 43 | text-align: center; 44 | } 45 | 46 | .itemImage { 47 | border-radius: 0; 48 | max-width: 100%; 49 | max-height: 100%; 50 | } 51 | } 52 | 53 | @media screen and (orientation:landscape) { 54 | #containerCoverImage { 55 | position: absolute; 56 | top: 0px; 57 | bottom: 0px; 58 | left: 0px; 59 | width: 49%; 60 | } 61 | 62 | #containerMusicInfo { 63 | position: absolute; 64 | top: 54px; 65 | bottom: 0px; 66 | right: 0px; 67 | width: 49%; 68 | text-align: left; 69 | } 70 | 71 | .itemImage { 72 | border-radius: 10px; 73 | max-width: 98%; 74 | max-height: 98%; 75 | } 76 | } 77 | 78 | #containerCoverImage { 79 | display: flex; 80 | justify-content: center; 81 | align-items: center; 82 | } 83 | 84 | #colorBackground { 85 | position: absolute; 86 | top: 0px; 87 | left: 0px; 88 | right: 0px; 89 | bottom: 0px; 90 | display: none; 91 | } 92 | 93 | #notPlaying { 94 | position: absolute; 95 | top: 0px; 96 | left: 0px; 97 | right: 0px; 98 | bottom: 0px; 99 | display: flex; 100 | flex-wrap: wrap; 101 | justify-content: space-around; 102 | } 103 | 104 | #containerMusicInfo { 105 | display: flex; 106 | flex-direction: column; 107 | justify-content: center; 108 | } 109 | 110 | .lineMusicInfo { 111 | height: 10%; 112 | margin-bottom: 1%; 113 | } 114 | 115 | #controlsPlayer { 116 | height: 20%; 117 | width: 100%; 118 | max-height: 96px; 119 | margin-bottom: 5%; 120 | display:flex; 121 | justify-content: space-around; 122 | align-items: center; 123 | vertical-align: middle; 124 | } 125 | 126 | .playerButton { 127 | width: 33%; 128 | } 129 | 130 | #controlsSettings { 131 | height: 10%; 132 | width: 100%; 133 | max-height: 48px; 134 | margin-bottom: 5%; 135 | display: flex; 136 | justify-content: space-around; 137 | align-items: center; 138 | vertical-align: middle; 139 | } 140 | 141 | .settingsButton { 142 | width: 20%; 143 | } 144 | 145 | #containerTrackSeek { 146 | height: 5%; 147 | display:flex; 148 | justify-content: center; 149 | align-items: center; 150 | vertical-align: middle; 151 | } 152 | 153 | #containerZoneList { 154 | height: 10%; 155 | display:flex; 156 | justify-content: center; 157 | align-items: center; 158 | vertical-align: middle; 159 | } 160 | 161 | #trackSeek { 162 | width: 95%; 163 | height: 5px; 164 | border-radius: 10px; 165 | background-color: rgba(239, 240, 241, 0.33); 166 | position: relative; 167 | display: block; 168 | padding: 0; 169 | align-self: center; 170 | } 171 | 172 | #trackSeekValue { 173 | height: 5px; 174 | background-color: #3daee9; 175 | border-radius: 10px; 176 | padding: 0; 177 | width: 0%; 178 | } 179 | 180 | #trackSeekValue span { 181 | position: absolute; 182 | bottom: -15px; 183 | width: 50%; 184 | margin: 0; 185 | line-height: 10px; 186 | font-size: 10px; 187 | } 188 | 189 | #trackSeekValue span.right { 190 | text-align: right; 191 | right: 0; 192 | } 193 | 194 | #trackSeekValue span.left { 195 | text-align: left; 196 | left: 0; 197 | } 198 | 199 | #notPlaying { 200 | background-color: #232629; 201 | color: #eff0f1; 202 | } 203 | 204 | #volumeList { 205 | display: flex; 206 | flex-direction: column; 207 | align-items: center; 208 | } 209 | 210 | .volumeGroup { 211 | width: 100%; 212 | display: flex; 213 | flex-direction: row; 214 | flex-wrap: wrap; 215 | justify-content: space-around; 216 | align-items: center; 217 | vertical-align: middle; 218 | text-align: center; 219 | } 220 | 221 | .volumeButton { 222 | color: #eff0f1; 223 | width: 15%; 224 | } 225 | 226 | .volumeSlider { 227 | width: 70%; 228 | } 229 | 230 | hr { 231 | width: 85%; 232 | } 233 | 234 | input[type=range] { 235 | height: 25px; 236 | -webkit-appearance: none; 237 | margin: 10px 0; 238 | width: 100%; 239 | background-color: rgba(0,0,0,0); 240 | } 241 | input[type=range]:focus { 242 | outline: none; 243 | } 244 | input[type=range]::-webkit-slider-runnable-track { 245 | width: 100%; 246 | height: 5px; 247 | cursor: pointer; 248 | animate: 0.2s; 249 | box-shadow: 0px 0px 0px #000000; 250 | background: #3daee9; 251 | border-radius: 10px; 252 | border: 0px solid #000000; 253 | } 254 | input[type=range]::-webkit-slider-thumb { 255 | box-shadow: 0px 0px 0px #232629; 256 | border: 1px solid #232629; 257 | height: 18px; 258 | width: 18px; 259 | border-radius: 25px; 260 | background: #eff0f1; 261 | cursor: pointer; 262 | -webkit-appearance: none; 263 | margin-top: -7px; 264 | } 265 | input[type=range]:focus::-webkit-slider-runnable-track { 266 | background: #3daee9; 267 | } 268 | input[type=range]::-moz-range-track { 269 | width: 100%; 270 | height: 5px; 271 | cursor: pointer; 272 | animate: 0.2s; 273 | box-shadow: 0px 0px 0px #000000; 274 | background: #3daee9; 275 | border-radius: 10px; 276 | border: 0px solid #000000; 277 | } 278 | input[type=range]::-moz-range-thumb { 279 | box-shadow: 0px 0px 0px #232629; 280 | border: 1px solid #232629; 281 | height: 18px; 282 | width: 18px; 283 | border-radius: 25px; 284 | background: #eff0f1; 285 | cursor: pointer; 286 | } 287 | input[type=range]::-ms-track { 288 | width: 100%; 289 | height: 5px; 290 | cursor: pointer; 291 | animate: 0.2s; 292 | background: transparent; 293 | border-color: transparent; 294 | color: transparent; 295 | } 296 | input[type=range]::-ms-fill-lower { 297 | background: #3daee9; 298 | border: 0px solid #000000; 299 | border-radius: 2px; 300 | box-shadow: 0px 0px 0px #000000; 301 | } 302 | input[type=range]::-ms-fill-upper { 303 | background: #3daee9; 304 | border: 0px solid #000000; 305 | border-radius: 2px; 306 | box-shadow: 0px 0px 0px #000000; 307 | } 308 | input[type=range]::-ms-thumb { 309 | margin-top: 1px; 310 | box-shadow: 0px 0px 0px #232629; 311 | border: 1px solid #232629; 312 | height: 18px; 313 | width: 18px; 314 | border-radius: 25px; 315 | background: #eff0f1; 316 | cursor: pointer; 317 | } 318 | input[type=range]:focus::-ms-fill-lower { 319 | background: #3daee9; 320 | } 321 | input[type=range]:focus::-ms-fill-upper { 322 | background: #3daee9; 323 | } 324 | -------------------------------------------------------------------------------- /public/css/site.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: sans-serif; 3 | background-color: #232629; 4 | color: #eff0f1; 5 | } 6 | 7 | button { 8 | outline: none; 9 | border: none; 10 | } 11 | 12 | button:focus { 13 | outline: none; 14 | border: none; 15 | } 16 | 17 | button:active { 18 | outline: none; 19 | border: none; 20 | } 21 | 22 | svg { 23 | width: auto; 24 | height: 100%; 25 | fill: currentColor; 26 | } 27 | 28 | .buttonOverlay { 29 | width: 96px; 30 | height: 48px; 31 | font-weight: bold; 32 | border: none; 33 | border-radius: 10px; 34 | -webkit-filter: drop-shadow(5px 5px 5px black); 35 | filter: drop-shadow(5px 5px 5px black); 36 | margin: 24px; 37 | background-color: #eff0f1; 38 | color: #232629; 39 | } 40 | 41 | .buttonOverlay:active { 42 | transform: translateY(4px); 43 | filter:none; 44 | } 45 | 46 | .buttonSettingActive { 47 | background-color: #3daee9; 48 | filter:none; 49 | } 50 | 51 | .buttonFillHeight { 52 | background-color: rgba(0,0,0,0); 53 | height: 100%; 54 | } 55 | 56 | .buttonPartialHeight { 57 | background-color: rgba(0,0,0,0); 58 | height: 75%; 59 | } 60 | 61 | .fullscreenFlex { 62 | position: absolute; 63 | top: 0px; 64 | left: 0px; 65 | right: 0px; 66 | bottom: 0px; 67 | display: flex; 68 | flex-wrap: wrap; 69 | justify-content: space-around; 70 | } 71 | 72 | .messages { 73 | max-width: 90%; 74 | font-size:5vw; 75 | align-self: center; 76 | text-align:center; 77 | } 78 | 79 | .overlay { 80 | display:none; 81 | } 82 | 83 | .overlayBackground { 84 | position: absolute; 85 | top: 0px; 86 | left: 0px; 87 | right: 0px; 88 | bottom: 0px; 89 | background-color: rgba(0,0,0,0.75); 90 | z-index:200; 91 | } 92 | 93 | .overlayContent { 94 | position: absolute; 95 | top: 50%; 96 | left: 50%; 97 | transform: translate(-50%, -50%); 98 | background-color: #232629; 99 | color: #eff0f1; 100 | width: auto; 101 | height: auto; 102 | min-width: 50%; 103 | max-height: 95%; 104 | max-width: 95%; 105 | border-radius: 10px; 106 | padding: 5px; 107 | -webkit-filter: drop-shadow(10px 10px 10px black); 108 | filter: drop-shadow(10px 10px 10px black); 109 | z-index:300; 110 | } 111 | 112 | .overlayList { 113 | display: flex; 114 | flex-direction: row; 115 | flex-wrap: wrap; 116 | justify-content: space-around; 117 | align-items: center; 118 | } 119 | 120 | .overlayListRow { 121 | display: flex; 122 | flex-direction: row; 123 | justify-content: space-between; 124 | align-items: center; 125 | width: 85%; 126 | } 127 | 128 | .settingsList { 129 | flex-direction: column; 130 | flex-wrap: nowrap; 131 | justify-content: center; 132 | } 133 | 134 | .textBold { 135 | font-weight: bold; 136 | } 137 | 138 | .textCenter { 139 | text-align: center; 140 | } 141 | 142 | .textSmall { 143 | font-size: 75%; 144 | } 145 | 146 | #coverBackground { 147 | position: absolute; 148 | top: 0px; 149 | left: 0px; 150 | right: 0px; 151 | bottom: 0px; 152 | background-position: center; 153 | background-repeat: no-repeat; 154 | background-size: cover; 155 | -webkit-filter: opacity(33%) blur(20px); 156 | filter: opacity(33%) blur(20px); 157 | display: flex; 158 | text-align: center; 159 | justify-content: center; 160 | align-items: center; 161 | display: none; 162 | } 163 | @media screen and (orientation:portrait) { 164 | .overlayContent { 165 | width: 95%; 166 | } 167 | } 168 | 169 | @media screen and (orientation:landscape) { 170 | } 171 | 172 | .settingsSwitch { 173 | position: relative; 174 | display: inline-block; 175 | width: 60px; 176 | height: 34px; 177 | } 178 | 179 | .settingsSwitch input { 180 | display: none; 181 | } 182 | 183 | .settingsSlider { 184 | position: absolute; 185 | cursor: pointer; 186 | top: 0; 187 | left: 0; 188 | right: 0; 189 | bottom: 0; 190 | background-color: #4d4d4d; 191 | -webkit-transition: .4s; 192 | transition: .4s; 193 | border-radius: 34px; 194 | } 195 | 196 | .settingsSlider:before { 197 | position: absolute; 198 | content: ""; 199 | height: 26px; 200 | width: 26px; 201 | left: 4px; 202 | bottom: 4px; 203 | background-color: #fcfcfc; 204 | -webkit-transition: .4s; 205 | transition: .4s; 206 | border-radius: 50%; 207 | } 208 | 209 | .settingsSwitch input:checked + .settingsSlider { 210 | background-color: #1d99f3; 211 | } 212 | 213 | .settingsSwitch input:checked + .settingsSlider:before { 214 | -webkit-transform: translateX(26px); 215 | -ms-transform: translateX(26px); 216 | transform: translateX(26px); 217 | } 218 | -------------------------------------------------------------------------------- /public/favicons/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pluggemi/roon-web-controller/9e4f9faf439d63633f78df6d6c714d13f575ea31/public/favicons/android-icon-144x144.png -------------------------------------------------------------------------------- /public/favicons/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pluggemi/roon-web-controller/9e4f9faf439d63633f78df6d6c714d13f575ea31/public/favicons/android-icon-192x192.png -------------------------------------------------------------------------------- /public/favicons/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pluggemi/roon-web-controller/9e4f9faf439d63633f78df6d6c714d13f575ea31/public/favicons/android-icon-36x36.png -------------------------------------------------------------------------------- /public/favicons/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pluggemi/roon-web-controller/9e4f9faf439d63633f78df6d6c714d13f575ea31/public/favicons/android-icon-48x48.png -------------------------------------------------------------------------------- /public/favicons/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pluggemi/roon-web-controller/9e4f9faf439d63633f78df6d6c714d13f575ea31/public/favicons/android-icon-72x72.png -------------------------------------------------------------------------------- /public/favicons/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pluggemi/roon-web-controller/9e4f9faf439d63633f78df6d6c714d13f575ea31/public/favicons/android-icon-96x96.png -------------------------------------------------------------------------------- /public/favicons/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pluggemi/roon-web-controller/9e4f9faf439d63633f78df6d6c714d13f575ea31/public/favicons/apple-icon-114x114.png -------------------------------------------------------------------------------- /public/favicons/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pluggemi/roon-web-controller/9e4f9faf439d63633f78df6d6c714d13f575ea31/public/favicons/apple-icon-120x120.png -------------------------------------------------------------------------------- /public/favicons/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pluggemi/roon-web-controller/9e4f9faf439d63633f78df6d6c714d13f575ea31/public/favicons/apple-icon-144x144.png -------------------------------------------------------------------------------- /public/favicons/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pluggemi/roon-web-controller/9e4f9faf439d63633f78df6d6c714d13f575ea31/public/favicons/apple-icon-152x152.png -------------------------------------------------------------------------------- /public/favicons/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pluggemi/roon-web-controller/9e4f9faf439d63633f78df6d6c714d13f575ea31/public/favicons/apple-icon-180x180.png -------------------------------------------------------------------------------- /public/favicons/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pluggemi/roon-web-controller/9e4f9faf439d63633f78df6d6c714d13f575ea31/public/favicons/apple-icon-57x57.png -------------------------------------------------------------------------------- /public/favicons/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pluggemi/roon-web-controller/9e4f9faf439d63633f78df6d6c714d13f575ea31/public/favicons/apple-icon-60x60.png -------------------------------------------------------------------------------- /public/favicons/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pluggemi/roon-web-controller/9e4f9faf439d63633f78df6d6c714d13f575ea31/public/favicons/apple-icon-72x72.png -------------------------------------------------------------------------------- /public/favicons/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pluggemi/roon-web-controller/9e4f9faf439d63633f78df6d6c714d13f575ea31/public/favicons/apple-icon-76x76.png -------------------------------------------------------------------------------- /public/favicons/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pluggemi/roon-web-controller/9e4f9faf439d63633f78df6d6c714d13f575ea31/public/favicons/apple-icon-precomposed.png -------------------------------------------------------------------------------- /public/favicons/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pluggemi/roon-web-controller/9e4f9faf439d63633f78df6d6c714d13f575ea31/public/favicons/apple-icon.png -------------------------------------------------------------------------------- /public/favicons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff -------------------------------------------------------------------------------- /public/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pluggemi/roon-web-controller/9e4f9faf439d63633f78df6d6c714d13f575ea31/public/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pluggemi/roon-web-controller/9e4f9faf439d63633f78df6d6c714d13f575ea31/public/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pluggemi/roon-web-controller/9e4f9faf439d63633f78df6d6c714d13f575ea31/public/favicons/favicon-96x96.png -------------------------------------------------------------------------------- /public/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pluggemi/roon-web-controller/9e4f9faf439d63633f78df6d6c714d13f575ea31/public/favicons/favicon.ico -------------------------------------------------------------------------------- /public/favicons/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Roon Web", 3 | "name": "Roon Web Controller", 4 | "background_color": "#232629", 5 | "display": "standalone", 6 | "start_url": "/", 7 | "icons": [ 8 | { 9 | "src": "/favicons/android-icon-36x36.png", 10 | "sizes": "36x36", 11 | "type": "image/png", 12 | "density": "0.75" 13 | }, 14 | { 15 | "src": "/favicons/android-icon-48x48.png", 16 | "sizes": "48x48", 17 | "type": "image/png", 18 | "density": "1.0" 19 | }, 20 | { 21 | "src": "/favicons/android-icon-72x72.png", 22 | "sizes": "72x72", 23 | "type": "image/png", 24 | "density": "1.5" 25 | }, 26 | { 27 | "src": "/favicons/android-icon-96x96.png", 28 | "sizes": "96x96", 29 | "type": "image/png", 30 | "density": "2.0" 31 | }, 32 | { 33 | "src": "/favicons/android-icon-144x144.png", 34 | "sizes": "144x144", 35 | "type": "image/png", 36 | "density": "3.0" 37 | }, 38 | { 39 | "src": "/favicons/android-icon-192x192.png", 40 | "sizes": "192x192", 41 | "type": "image/png", 42 | "density": "4.0" 43 | } 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /public/favicons/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pluggemi/roon-web-controller/9e4f9faf439d63633f78df6d6c714d13f575ea31/public/favicons/ms-icon-144x144.png -------------------------------------------------------------------------------- /public/favicons/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pluggemi/roon-web-controller/9e4f9faf439d63633f78df6d6c714d13f575ea31/public/favicons/ms-icon-150x150.png -------------------------------------------------------------------------------- /public/favicons/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pluggemi/roon-web-controller/9e4f9faf439d63633f78df6d6c714d13f575ea31/public/favicons/ms-icon-310x310.png -------------------------------------------------------------------------------- /public/favicons/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pluggemi/roon-web-controller/9e4f9faf439d63633f78df6d6c714d13f575ea31/public/favicons/ms-icon-70x70.png -------------------------------------------------------------------------------- /public/fullscreen.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Roon Web Controller 5 | 6 | 7 | 8 | 9 | 10 | 30 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 71 | 72 | 73 |
74 | 75 |
76 | 77 |
78 | 79 |
80 | 81 |
82 |
83 | This extension is not enabled. Please use a Roon client to enable it. 84 |
85 |
86 | 87 |
88 |
92 |
93 |
94 | 103 | 112 |
113 |
114 |
115 | 116 | 124 | 125 |
 
126 | 127 | 128 | -------------------------------------------------------------------------------- /public/img/transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pluggemi/roon-web-controller/9e4f9faf439d63633f78df6d6c714d13f575ea31/public/img/transparent.png -------------------------------------------------------------------------------- /public/js/NoSleep.min.js: -------------------------------------------------------------------------------- 1 | /*! NoSleep.min.js v0.7.0 - git.io/vfn01 - Rich Tibbett - MIT license */ 2 | !function(A,B){"object"==typeof exports&&"object"==typeof module?module.exports=B():"function"==typeof define&&define.amd?define([],B):"object"==typeof exports?exports.NoSleep=B():A.NoSleep=B()}(this,function(){return function(A){function B(e){if(Q[e])return Q[e].exports;var o=Q[e]={i:e,l:!1,exports:{}};return A[e].call(o.exports,o,o.exports,B),o.l=!0,o.exports}var Q={};return B.m=A,B.c=Q,B.d=function(A,Q,e){B.o(A,Q)||Object.defineProperty(A,Q,{configurable:!1,enumerable:!0,get:e})},B.n=function(A){var Q=A&&A.__esModule?function(){return A.default}:function(){return A};return B.d(Q,"a",Q),Q},B.o=function(A,B){return Object.prototype.hasOwnProperty.call(A,B)},B.p="",B(B.s=0)}([function(A,B,Q){"use strict";function e(A,B){if(!(A instanceof B))throw new TypeError("Cannot call a class as a function")}var o=function(){function A(A,B){for(var Q=0;Q.5&&(this.noSleepVideo.currentTime=Math.random())}.bind(this)))}return o(A,[{key:"enable",value:function(){n?(this.disable(),this.noSleepTimer=window.setInterval(function(){window.location.href="/",window.setTimeout(window.stop,0)},15e3)):this.noSleepVideo.play()}},{key:"disable",value:function(){n?this.noSleepTimer&&(window.clearInterval(this.noSleepTimer),this.noSleepTimer=null):this.noSleepVideo.pause()}}]),A}();A.exports=E},function(A,B,Q){"use strict";A.exports="data:video/mp4;base64,AAAAIGZ0eXBtcDQyAAACAGlzb21pc28yYXZjMW1wNDEAAAAIZnJlZQAACKBtZGF0AAAC8wYF///v3EXpvebZSLeWLNgg2SPu73gyNjQgLSBjb3JlIDE0MiByMjQ3OSBkZDc5YTYxIC0gSC4yNjQvTVBFRy00IEFWQyBjb2RlYyAtIENvcHlsZWZ0IDIwMDMtMjAxNCAtIGh0dHA6Ly93d3cudmlkZW9sYW4ub3JnL3gyNjQuaHRtbCAtIG9wdGlvbnM6IGNhYmFjPTEgcmVmPTEgZGVibG9jaz0xOjA6MCBhbmFseXNlPTB4MToweDExMSBtZT1oZXggc3VibWU9MiBwc3k9MSBwc3lfcmQ9MS4wMDowLjAwIG1peGVkX3JlZj0wIG1lX3JhbmdlPTE2IGNocm9tYV9tZT0xIHRyZWxsaXM9MCA4eDhkY3Q9MCBjcW09MCBkZWFkem9uZT0yMSwxMSBmYXN0X3Bza2lwPTEgY2hyb21hX3FwX29mZnNldD0wIHRocmVhZHM9NiBsb29rYWhlYWRfdGhyZWFkcz0xIHNsaWNlZF90aHJlYWRzPTAgbnI9MCBkZWNpbWF0ZT0xIGludGVybGFjZWQ9MCBibHVyYXlfY29tcGF0PTAgY29uc3RyYWluZWRfaW50cmE9MCBiZnJhbWVzPTMgYl9weXJhbWlkPTIgYl9hZGFwdD0xIGJfYmlhcz0wIGRpcmVjdD0xIHdlaWdodGI9MSBvcGVuX2dvcD0wIHdlaWdodHA9MSBrZXlpbnQ9MzAwIGtleWludF9taW49MzAgc2NlbmVjdXQ9NDAgaW50cmFfcmVmcmVzaD0wIHJjX2xvb2thaGVhZD0xMCByYz1jcmYgbWJ0cmVlPTEgY3JmPTIwLjAgcWNvbXA9MC42MCBxcG1pbj0wIHFwbWF4PTY5IHFwc3RlcD00IHZidl9tYXhyYXRlPTIwMDAwIHZidl9idWZzaXplPTI1MDAwIGNyZl9tYXg9MC4wIG5hbF9ocmQ9bm9uZSBmaWxsZXI9MCBpcF9yYXRpbz0xLjQwIGFxPTE6MS4wMACAAAAAOWWIhAA3//p+C7v8tDDSTjf97w55i3SbRPO4ZY+hkjD5hbkAkL3zpJ6h/LR1CAABzgB1kqqzUorlhQAAAAxBmiQYhn/+qZYADLgAAAAJQZ5CQhX/AAj5IQADQGgcIQADQGgcAAAACQGeYUQn/wALKCEAA0BoHAAAAAkBnmNEJ/8ACykhAANAaBwhAANAaBwAAAANQZpoNExDP/6plgAMuSEAA0BoHAAAAAtBnoZFESwr/wAI+SEAA0BoHCEAA0BoHAAAAAkBnqVEJ/8ACykhAANAaBwAAAAJAZ6nRCf/AAsoIQADQGgcIQADQGgcAAAADUGarDRMQz/+qZYADLghAANAaBwAAAALQZ7KRRUsK/8ACPkhAANAaBwAAAAJAZ7pRCf/AAsoIQADQGgcIQADQGgcAAAACQGe60Qn/wALKCEAA0BoHAAAAA1BmvA0TEM//qmWAAy5IQADQGgcIQADQGgcAAAAC0GfDkUVLCv/AAj5IQADQGgcAAAACQGfLUQn/wALKSEAA0BoHCEAA0BoHAAAAAkBny9EJ/8ACyghAANAaBwAAAANQZs0NExDP/6plgAMuCEAA0BoHAAAAAtBn1JFFSwr/wAI+SEAA0BoHCEAA0BoHAAAAAkBn3FEJ/8ACyghAANAaBwAAAAJAZ9zRCf/AAsoIQADQGgcIQADQGgcAAAADUGbeDRMQz/+qZYADLkhAANAaBwAAAALQZ+WRRUsK/8ACPghAANAaBwhAANAaBwAAAAJAZ+1RCf/AAspIQADQGgcAAAACQGft0Qn/wALKSEAA0BoHCEAA0BoHAAAAA1Bm7w0TEM//qmWAAy4IQADQGgcAAAAC0Gf2kUVLCv/AAj5IQADQGgcAAAACQGf+UQn/wALKCEAA0BoHCEAA0BoHAAAAAkBn/tEJ/8ACykhAANAaBwAAAANQZvgNExDP/6plgAMuSEAA0BoHCEAA0BoHAAAAAtBnh5FFSwr/wAI+CEAA0BoHAAAAAkBnj1EJ/8ACyghAANAaBwhAANAaBwAAAAJAZ4/RCf/AAspIQADQGgcAAAADUGaJDRMQz/+qZYADLghAANAaBwAAAALQZ5CRRUsK/8ACPkhAANAaBwhAANAaBwAAAAJAZ5hRCf/AAsoIQADQGgcAAAACQGeY0Qn/wALKSEAA0BoHCEAA0BoHAAAAA1Bmmg0TEM//qmWAAy5IQADQGgcAAAAC0GehkUVLCv/AAj5IQADQGgcIQADQGgcAAAACQGepUQn/wALKSEAA0BoHAAAAAkBnqdEJ/8ACyghAANAaBwAAAANQZqsNExDP/6plgAMuCEAA0BoHCEAA0BoHAAAAAtBnspFFSwr/wAI+SEAA0BoHAAAAAkBnulEJ/8ACyghAANAaBwhAANAaBwAAAAJAZ7rRCf/AAsoIQADQGgcAAAADUGa8DRMQz/+qZYADLkhAANAaBwhAANAaBwAAAALQZ8ORRUsK/8ACPkhAANAaBwAAAAJAZ8tRCf/AAspIQADQGgcIQADQGgcAAAACQGfL0Qn/wALKCEAA0BoHAAAAA1BmzQ0TEM//qmWAAy4IQADQGgcAAAAC0GfUkUVLCv/AAj5IQADQGgcIQADQGgcAAAACQGfcUQn/wALKCEAA0BoHAAAAAkBn3NEJ/8ACyghAANAaBwhAANAaBwAAAANQZt4NExC//6plgAMuSEAA0BoHAAAAAtBn5ZFFSwr/wAI+CEAA0BoHCEAA0BoHAAAAAkBn7VEJ/8ACykhAANAaBwAAAAJAZ+3RCf/AAspIQADQGgcAAAADUGbuzRMQn/+nhAAYsAhAANAaBwhAANAaBwAAAAJQZ/aQhP/AAspIQADQGgcAAAACQGf+UQn/wALKCEAA0BoHCEAA0BoHCEAA0BoHCEAA0BoHCEAA0BoHCEAA0BoHAAACiFtb292AAAAbG12aGQAAAAA1YCCX9WAgl8AAAPoAAAH/AABAAABAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAGGlvZHMAAAAAEICAgAcAT////v7/AAAF+XRyYWsAAABcdGtoZAAAAAPVgIJf1YCCXwAAAAEAAAAAAAAH0AAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAEAAAAAAygAAAMoAAAAAACRlZHRzAAAAHGVsc3QAAAAAAAAAAQAAB9AAABdwAAEAAAAABXFtZGlhAAAAIG1kaGQAAAAA1YCCX9WAgl8AAV+QAAK/IFXEAAAAAAAtaGRscgAAAAAAAAAAdmlkZQAAAAAAAAAAAAAAAFZpZGVvSGFuZGxlcgAAAAUcbWluZgAAABR2bWhkAAAAAQAAAAAAAAAAAAAAJGRpbmYAAAAcZHJlZgAAAAAAAAABAAAADHVybCAAAAABAAAE3HN0YmwAAACYc3RzZAAAAAAAAAABAAAAiGF2YzEAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAygDKAEgAAABIAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY//8AAAAyYXZjQwFNQCj/4QAbZ01AKOyho3ySTUBAQFAAAAMAEAAr8gDxgxlgAQAEaO+G8gAAABhzdHRzAAAAAAAAAAEAAAA8AAALuAAAABRzdHNzAAAAAAAAAAEAAAABAAAB8GN0dHMAAAAAAAAAPAAAAAEAABdwAAAAAQAAOpgAAAABAAAXcAAAAAEAAAAAAAAAAQAAC7gAAAABAAA6mAAAAAEAABdwAAAAAQAAAAAAAAABAAALuAAAAAEAADqYAAAAAQAAF3AAAAABAAAAAAAAAAEAAAu4AAAAAQAAOpgAAAABAAAXcAAAAAEAAAAAAAAAAQAAC7gAAAABAAA6mAAAAAEAABdwAAAAAQAAAAAAAAABAAALuAAAAAEAADqYAAAAAQAAF3AAAAABAAAAAAAAAAEAAAu4AAAAAQAAOpgAAAABAAAXcAAAAAEAAAAAAAAAAQAAC7gAAAABAAA6mAAAAAEAABdwAAAAAQAAAAAAAAABAAALuAAAAAEAADqYAAAAAQAAF3AAAAABAAAAAAAAAAEAAAu4AAAAAQAAOpgAAAABAAAXcAAAAAEAAAAAAAAAAQAAC7gAAAABAAA6mAAAAAEAABdwAAAAAQAAAAAAAAABAAALuAAAAAEAADqYAAAAAQAAF3AAAAABAAAAAAAAAAEAAAu4AAAAAQAAOpgAAAABAAAXcAAAAAEAAAAAAAAAAQAAC7gAAAABAAA6mAAAAAEAABdwAAAAAQAAAAAAAAABAAALuAAAAAEAAC7gAAAAAQAAF3AAAAABAAAAAAAAABxzdHNjAAAAAAAAAAEAAAABAAAAAQAAAAEAAAEEc3RzegAAAAAAAAAAAAAAPAAAAzQAAAAQAAAADQAAAA0AAAANAAAAEQAAAA8AAAANAAAADQAAABEAAAAPAAAADQAAAA0AAAARAAAADwAAAA0AAAANAAAAEQAAAA8AAAANAAAADQAAABEAAAAPAAAADQAAAA0AAAARAAAADwAAAA0AAAANAAAAEQAAAA8AAAANAAAADQAAABEAAAAPAAAADQAAAA0AAAARAAAADwAAAA0AAAANAAAAEQAAAA8AAAANAAAADQAAABEAAAAPAAAADQAAAA0AAAARAAAADwAAAA0AAAANAAAAEQAAAA8AAAANAAAADQAAABEAAAANAAAADQAAAQBzdGNvAAAAAAAAADwAAAAwAAADZAAAA3QAAAONAAADoAAAA7kAAAPQAAAD6wAAA/4AAAQXAAAELgAABEMAAARcAAAEbwAABIwAAAShAAAEugAABM0AAATkAAAE/wAABRIAAAUrAAAFQgAABV0AAAVwAAAFiQAABaAAAAW1AAAFzgAABeEAAAX+AAAGEwAABiwAAAY/AAAGVgAABnEAAAaEAAAGnQAABrQAAAbPAAAG4gAABvUAAAcSAAAHJwAAB0AAAAdTAAAHcAAAB4UAAAeeAAAHsQAAB8gAAAfjAAAH9gAACA8AAAgmAAAIQQAACFQAAAhnAAAIhAAACJcAAAMsdHJhawAAAFx0a2hkAAAAA9WAgl/VgIJfAAAAAgAAAAAAAAf8AAAAAAAAAAAAAAABAQAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAACsm1kaWEAAAAgbWRoZAAAAADVgIJf1YCCXwAArEQAAWAAVcQAAAAAACdoZGxyAAAAAAAAAABzb3VuAAAAAAAAAAAAAAAAU3RlcmVvAAAAAmNtaW5mAAAAEHNtaGQAAAAAAAAAAAAAACRkaW5mAAAAHGRyZWYAAAAAAAAAAQAAAAx1cmwgAAAAAQAAAidzdGJsAAAAZ3N0c2QAAAAAAAAAAQAAAFdtcDRhAAAAAAAAAAEAAAAAAAAAAAACABAAAAAArEQAAAAAADNlc2RzAAAAAAOAgIAiAAIABICAgBRAFQAAAAADDUAAAAAABYCAgAISEAaAgIABAgAAABhzdHRzAAAAAAAAAAEAAABYAAAEAAAAABxzdHNjAAAAAAAAAAEAAAABAAAAAQAAAAEAAAAUc3RzegAAAAAAAAAGAAAAWAAAAXBzdGNvAAAAAAAAAFgAAAOBAAADhwAAA5oAAAOtAAADswAAA8oAAAPfAAAD5QAAA/gAAAQLAAAEEQAABCgAAAQ9AAAEUAAABFYAAARpAAAEgAAABIYAAASbAAAErgAABLQAAATHAAAE3gAABPMAAAT5AAAFDAAABR8AAAUlAAAFPAAABVEAAAVXAAAFagAABX0AAAWDAAAFmgAABa8AAAXCAAAFyAAABdsAAAXyAAAF+AAABg0AAAYgAAAGJgAABjkAAAZQAAAGZQAABmsAAAZ+AAAGkQAABpcAAAauAAAGwwAABskAAAbcAAAG7wAABwYAAAcMAAAHIQAABzQAAAc6AAAHTQAAB2QAAAdqAAAHfwAAB5IAAAeYAAAHqwAAB8IAAAfXAAAH3QAAB/AAAAgDAAAICQAACCAAAAg1AAAIOwAACE4AAAhhAAAIeAAACH4AAAiRAAAIpAAACKoAAAiwAAAItgAACLwAAAjCAAAAFnVkdGEAAAAObmFtZVN0ZXJlbwAAAHB1ZHRhAAAAaG1ldGEAAAAAAAAAIWhkbHIAAAAAAAAAAG1kaXJhcHBsAAAAAAAAAAAAAAAAO2lsc3QAAAAzqXRvbwAAACtkYXRhAAAAAQAAAABIYW5kQnJha2UgMC4xMC4yIDIwMTUwNjExMDA="}])}); -------------------------------------------------------------------------------- /public/js/color-thief.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Color Thief v2.0 3 | * by Lokesh Dhakar - http://www.lokeshdhakar.com 4 | * 5 | * Thanks 6 | * ------ 7 | * Nick Rabinowitz - For creating quantize.js. 8 | * John Schulz - For clean up and optimization. @JFSIII 9 | * Nathan Spady - For adding drag and drop support to the demo page. 10 | * 11 | * License 12 | * ------- 13 | * Copyright 2011, 2015 Lokesh Dhakar 14 | * Released under the MIT license 15 | * https://raw.githubusercontent.com/lokesh/color-thief/master/LICENSE 16 | * 17 | * @license 18 | */ 19 | 20 | 21 | /* 22 | CanvasImage Class 23 | Class that wraps the html image element and canvas. 24 | It also simplifies some of the canvas context manipulation 25 | with a set of helper functions. 26 | */ 27 | var CanvasImage = function (image) { 28 | this.canvas = document.createElement('canvas'); 29 | this.context = this.canvas.getContext('2d'); 30 | 31 | document.body.appendChild(this.canvas); 32 | 33 | this.width = this.canvas.width = image.width; 34 | this.height = this.canvas.height = image.height; 35 | 36 | this.context.drawImage(image, 0, 0, this.width, this.height); 37 | }; 38 | 39 | CanvasImage.prototype.clear = function () { 40 | this.context.clearRect(0, 0, this.width, this.height); 41 | }; 42 | 43 | CanvasImage.prototype.update = function (imageData) { 44 | this.context.putImageData(imageData, 0, 0); 45 | }; 46 | 47 | CanvasImage.prototype.getPixelCount = function () { 48 | return this.width * this.height; 49 | }; 50 | 51 | CanvasImage.prototype.getImageData = function () { 52 | return this.context.getImageData(0, 0, this.width, this.height); 53 | }; 54 | 55 | CanvasImage.prototype.removeCanvas = function () { 56 | this.canvas.parentNode.removeChild(this.canvas); 57 | }; 58 | 59 | 60 | var ColorThief = function () {}; 61 | 62 | /* 63 | * getColor(sourceImage[, quality]) 64 | * returns {r: num, g: num, b: num} 65 | * 66 | * Use the median cut algorithm provided by quantize.js to cluster similar 67 | * colors and return the base color from the largest cluster. 68 | * 69 | * Quality is an optional argument. It needs to be an integer. 1 is the highest quality settings. 70 | * 10 is the default. There is a trade-off between quality and speed. The bigger the number, the 71 | * faster a color will be returned but the greater the likelihood that it will not be the visually 72 | * most dominant color. 73 | * 74 | * */ 75 | ColorThief.prototype.getColor = function(sourceImage, quality) { 76 | var palette = this.getPalette(sourceImage, 5, quality); 77 | var dominantColor = palette[0]; 78 | return dominantColor; 79 | }; 80 | 81 | 82 | /* 83 | * getPalette(sourceImage[, colorCount, quality]) 84 | * returns array[ {r: num, g: num, b: num}, {r: num, g: num, b: num}, ...] 85 | * 86 | * Use the median cut algorithm provided by quantize.js to cluster similar colors. 87 | * 88 | * colorCount determines the size of the palette; the number of colors returned. If not set, it 89 | * defaults to 10. 90 | * 91 | * BUGGY: Function does not always return the requested amount of colors. It can be +/- 2. 92 | * 93 | * quality is an optional argument. It needs to be an integer. 1 is the highest quality settings. 94 | * 10 is the default. There is a trade-off between quality and speed. The bigger the number, the 95 | * faster the palette generation but the greater the likelihood that colors will be missed. 96 | * 97 | * 98 | */ 99 | ColorThief.prototype.getPalette = function(sourceImage, colorCount, quality) { 100 | 101 | if (typeof colorCount === 'undefined' || colorCount < 2 || colorCount > 256) { 102 | colorCount = 10; 103 | } 104 | if (typeof quality === 'undefined' || quality < 1) { 105 | quality = 10; 106 | } 107 | 108 | // Create custom CanvasImage object 109 | var image = new CanvasImage(sourceImage); 110 | var imageData = image.getImageData(); 111 | var pixels = imageData.data; 112 | var pixelCount = image.getPixelCount(); 113 | 114 | // Store the RGB values in an array format suitable for quantize function 115 | var pixelArray = []; 116 | for (var i = 0, offset, r, g, b, a; i < pixelCount; i = i + quality) { 117 | offset = i * 4; 118 | r = pixels[offset + 0]; 119 | g = pixels[offset + 1]; 120 | b = pixels[offset + 2]; 121 | a = pixels[offset + 3]; 122 | // If pixel is mostly opaque and not white 123 | if (a >= 125) { 124 | if (!(r > 250 && g > 250 && b > 250)) { 125 | pixelArray.push([r, g, b]); 126 | } 127 | } 128 | } 129 | 130 | // Send array to quantize function which clusters values 131 | // using median cut algorithm 132 | var cmap = MMCQ.quantize(pixelArray, colorCount); 133 | var palette = cmap? cmap.palette() : null; 134 | 135 | // Clean up 136 | image.removeCanvas(); 137 | 138 | return palette; 139 | }; 140 | 141 | ColorThief.prototype.getColorFromUrl = function(imageUrl, callback, quality) { 142 | sourceImage = document.createElement("img"); 143 | var thief = this; 144 | sourceImage.addEventListener('load' , function(){ 145 | var palette = thief.getPalette(sourceImage, 5, quality); 146 | var dominantColor = palette[0]; 147 | callback(dominantColor, imageUrl); 148 | }); 149 | sourceImage.src = imageUrl 150 | }; 151 | 152 | 153 | ColorThief.prototype.getImageData = function(imageUrl, callback) { 154 | xhr = new XMLHttpRequest(); 155 | xhr.open('GET', imageUrl, true); 156 | xhr.responseType = 'arraybuffer' 157 | xhr.onload = function(e) { 158 | if (this.status == 200) { 159 | uInt8Array = new Uint8Array(this.response) 160 | i = uInt8Array.length 161 | binaryString = new Array(i); 162 | for (var i = 0; i < uInt8Array.length; i++){ 163 | binaryString[i] = String.fromCharCode(uInt8Array[i]) 164 | } 165 | data = binaryString.join('') 166 | base64 = window.btoa(data) 167 | callback ("data:image/png;base64,"+base64) 168 | } 169 | } 170 | xhr.send(); 171 | }; 172 | 173 | ColorThief.prototype.getColorAsync = function(imageUrl, callback, quality) { 174 | var thief = this; 175 | this.getImageData(imageUrl, function(imageData){ 176 | sourceImage = document.createElement("img"); 177 | sourceImage.addEventListener('load' , function(){ 178 | var palette = thief.getPalette(sourceImage, 5, quality); 179 | var dominantColor = palette[0]; 180 | callback(dominantColor, this); 181 | }); 182 | sourceImage.src = imageData; 183 | }); 184 | }; 185 | 186 | 187 | 188 | /*! 189 | * quantize.js Copyright 2008 Nick Rabinowitz. 190 | * Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php 191 | * @license 192 | */ 193 | 194 | // fill out a couple protovis dependencies 195 | /*! 196 | * Block below copied from Protovis: http://mbostock.github.com/protovis/ 197 | * Copyright 2010 Stanford Visualization Group 198 | * Licensed under the BSD License: http://www.opensource.org/licenses/bsd-license.php 199 | * @license 200 | */ 201 | if (!pv) { 202 | var pv = { 203 | map: function(array, f) { 204 | var o = {}; 205 | return f ? array.map(function(d, i) { o.index = i; return f.call(o, d); }) : array.slice(); 206 | }, 207 | naturalOrder: function(a, b) { 208 | return (a < b) ? -1 : ((a > b) ? 1 : 0); 209 | }, 210 | sum: function(array, f) { 211 | var o = {}; 212 | return array.reduce(f ? function(p, d, i) { o.index = i; return p + f.call(o, d); } : function(p, d) { return p + d; }, 0); 213 | }, 214 | max: function(array, f) { 215 | return Math.max.apply(null, f ? pv.map(array, f) : array); 216 | } 217 | }; 218 | } 219 | 220 | 221 | 222 | /** 223 | * Basic Javascript port of the MMCQ (modified median cut quantization) 224 | * algorithm from the Leptonica library (http://www.leptonica.com/). 225 | * Returns a color map you can use to map original pixels to the reduced 226 | * palette. Still a work in progress. 227 | * 228 | * @author Nick Rabinowitz 229 | * @example 230 | 231 | // array of pixels as [R,G,B] arrays 232 | var myPixels = [[190,197,190], [202,204,200], [207,214,210], [211,214,211], [205,207,207] 233 | // etc 234 | ]; 235 | var maxColors = 4; 236 | 237 | var cmap = MMCQ.quantize(myPixels, maxColors); 238 | var newPalette = cmap.palette(); 239 | var newPixels = myPixels.map(function(p) { 240 | return cmap.map(p); 241 | }); 242 | 243 | */ 244 | var MMCQ = (function() { 245 | // private constants 246 | var sigbits = 5, 247 | rshift = 8 - sigbits, 248 | maxIterations = 1000, 249 | fractByPopulations = 0.75; 250 | 251 | // get reduced-space color index for a pixel 252 | function getColorIndex(r, g, b) { 253 | return (r << (2 * sigbits)) + (g << sigbits) + b; 254 | } 255 | 256 | // Simple priority queue 257 | function PQueue(comparator) { 258 | var contents = [], 259 | sorted = false; 260 | 261 | function sort() { 262 | contents.sort(comparator); 263 | sorted = true; 264 | } 265 | 266 | return { 267 | push: function(o) { 268 | contents.push(o); 269 | sorted = false; 270 | }, 271 | peek: function(index) { 272 | if (!sorted) sort(); 273 | if (index===undefined) index = contents.length - 1; 274 | return contents[index]; 275 | }, 276 | pop: function() { 277 | if (!sorted) sort(); 278 | return contents.pop(); 279 | }, 280 | size: function() { 281 | return contents.length; 282 | }, 283 | map: function(f) { 284 | return contents.map(f); 285 | }, 286 | debug: function() { 287 | if (!sorted) sort(); 288 | return contents; 289 | } 290 | }; 291 | } 292 | 293 | // 3d color space box 294 | function VBox(r1, r2, g1, g2, b1, b2, histo) { 295 | var vbox = this; 296 | vbox.r1 = r1; 297 | vbox.r2 = r2; 298 | vbox.g1 = g1; 299 | vbox.g2 = g2; 300 | vbox.b1 = b1; 301 | vbox.b2 = b2; 302 | vbox.histo = histo; 303 | } 304 | VBox.prototype = { 305 | volume: function(force) { 306 | var vbox = this; 307 | if (!vbox._volume || force) { 308 | vbox._volume = ((vbox.r2 - vbox.r1 + 1) * (vbox.g2 - vbox.g1 + 1) * (vbox.b2 - vbox.b1 + 1)); 309 | } 310 | return vbox._volume; 311 | }, 312 | count: function(force) { 313 | var vbox = this, 314 | histo = vbox.histo; 315 | if (!vbox._count_set || force) { 316 | var npix = 0, 317 | index, i, j, k; 318 | for (i = vbox.r1; i <= vbox.r2; i++) { 319 | for (j = vbox.g1; j <= vbox.g2; j++) { 320 | for (k = vbox.b1; k <= vbox.b2; k++) { 321 | index = getColorIndex(i,j,k); 322 | npix += (histo[index] || 0); 323 | } 324 | } 325 | } 326 | vbox._count = npix; 327 | vbox._count_set = true; 328 | } 329 | return vbox._count; 330 | }, 331 | copy: function() { 332 | var vbox = this; 333 | return new VBox(vbox.r1, vbox.r2, vbox.g1, vbox.g2, vbox.b1, vbox.b2, vbox.histo); 334 | }, 335 | avg: function(force) { 336 | var vbox = this, 337 | histo = vbox.histo; 338 | if (!vbox._avg || force) { 339 | var ntot = 0, 340 | mult = 1 << (8 - sigbits), 341 | rsum = 0, 342 | gsum = 0, 343 | bsum = 0, 344 | hval, 345 | i, j, k, histoindex; 346 | for (i = vbox.r1; i <= vbox.r2; i++) { 347 | for (j = vbox.g1; j <= vbox.g2; j++) { 348 | for (k = vbox.b1; k <= vbox.b2; k++) { 349 | histoindex = getColorIndex(i,j,k); 350 | hval = histo[histoindex] || 0; 351 | ntot += hval; 352 | rsum += (hval * (i + 0.5) * mult); 353 | gsum += (hval * (j + 0.5) * mult); 354 | bsum += (hval * (k + 0.5) * mult); 355 | } 356 | } 357 | } 358 | if (ntot) { 359 | vbox._avg = [~~(rsum/ntot), ~~(gsum/ntot), ~~(bsum/ntot)]; 360 | } else { 361 | // console.log('empty box'); 362 | vbox._avg = [ 363 | ~~(mult * (vbox.r1 + vbox.r2 + 1) / 2), 364 | ~~(mult * (vbox.g1 + vbox.g2 + 1) / 2), 365 | ~~(mult * (vbox.b1 + vbox.b2 + 1) / 2) 366 | ]; 367 | } 368 | } 369 | return vbox._avg; 370 | }, 371 | contains: function(pixel) { 372 | var vbox = this, 373 | rval = pixel[0] >> rshift; 374 | gval = pixel[1] >> rshift; 375 | bval = pixel[2] >> rshift; 376 | return (rval >= vbox.r1 && rval <= vbox.r2 && 377 | gval >= vbox.g1 && gval <= vbox.g2 && 378 | bval >= vbox.b1 && bval <= vbox.b2); 379 | } 380 | }; 381 | 382 | // Color map 383 | function CMap() { 384 | this.vboxes = new PQueue(function(a,b) { 385 | return pv.naturalOrder( 386 | a.vbox.count()*a.vbox.volume(), 387 | b.vbox.count()*b.vbox.volume() 388 | ); 389 | }); 390 | } 391 | CMap.prototype = { 392 | push: function(vbox) { 393 | this.vboxes.push({ 394 | vbox: vbox, 395 | color: vbox.avg() 396 | }); 397 | }, 398 | palette: function() { 399 | return this.vboxes.map(function(vb) { return vb.color; }); 400 | }, 401 | size: function() { 402 | return this.vboxes.size(); 403 | }, 404 | map: function(color) { 405 | var vboxes = this.vboxes; 406 | for (var i=0; i 251 440 | var idx = vboxes.length-1, 441 | highest = vboxes[idx].color; 442 | if (highest[0] > 251 && highest[1] > 251 && highest[2] > 251) 443 | vboxes[idx].color = [255,255,255]; 444 | } 445 | }; 446 | 447 | // histo (1-d array, giving the number of pixels in 448 | // each quantized region of color space), or null on error 449 | function getHisto(pixels) { 450 | var histosize = 1 << (3 * sigbits), 451 | histo = new Array(histosize), 452 | index, rval, gval, bval; 453 | pixels.forEach(function(pixel) { 454 | rval = pixel[0] >> rshift; 455 | gval = pixel[1] >> rshift; 456 | bval = pixel[2] >> rshift; 457 | index = getColorIndex(rval, gval, bval); 458 | histo[index] = (histo[index] || 0) + 1; 459 | }); 460 | return histo; 461 | } 462 | 463 | function vboxFromPixels(pixels, histo) { 464 | var rmin=1000000, rmax=0, 465 | gmin=1000000, gmax=0, 466 | bmin=1000000, bmax=0, 467 | rval, gval, bval; 468 | // find min/max 469 | pixels.forEach(function(pixel) { 470 | rval = pixel[0] >> rshift; 471 | gval = pixel[1] >> rshift; 472 | bval = pixel[2] >> rshift; 473 | if (rval < rmin) rmin = rval; 474 | else if (rval > rmax) rmax = rval; 475 | if (gval < gmin) gmin = gval; 476 | else if (gval > gmax) gmax = gval; 477 | if (bval < bmin) bmin = bval; 478 | else if (bval > bmax) bmax = bval; 479 | }); 480 | return new VBox(rmin, rmax, gmin, gmax, bmin, bmax, histo); 481 | } 482 | 483 | function medianCutApply(histo, vbox) { 484 | if (!vbox.count()) return; 485 | 486 | var rw = vbox.r2 - vbox.r1 + 1, 487 | gw = vbox.g2 - vbox.g1 + 1, 488 | bw = vbox.b2 - vbox.b1 + 1, 489 | maxw = pv.max([rw, gw, bw]); 490 | // only one pixel, no split 491 | if (vbox.count() == 1) { 492 | return [vbox.copy()]; 493 | } 494 | /* Find the partial sum arrays along the selected axis. */ 495 | var total = 0, 496 | partialsum = [], 497 | lookaheadsum = [], 498 | i, j, k, sum, index; 499 | if (maxw == rw) { 500 | for (i = vbox.r1; i <= vbox.r2; i++) { 501 | sum = 0; 502 | for (j = vbox.g1; j <= vbox.g2; j++) { 503 | for (k = vbox.b1; k <= vbox.b2; k++) { 504 | index = getColorIndex(i,j,k); 505 | sum += (histo[index] || 0); 506 | } 507 | } 508 | total += sum; 509 | partialsum[i] = total; 510 | } 511 | } 512 | else if (maxw == gw) { 513 | for (i = vbox.g1; i <= vbox.g2; i++) { 514 | sum = 0; 515 | for (j = vbox.r1; j <= vbox.r2; j++) { 516 | for (k = vbox.b1; k <= vbox.b2; k++) { 517 | index = getColorIndex(j,i,k); 518 | sum += (histo[index] || 0); 519 | } 520 | } 521 | total += sum; 522 | partialsum[i] = total; 523 | } 524 | } 525 | else { /* maxw == bw */ 526 | for (i = vbox.b1; i <= vbox.b2; i++) { 527 | sum = 0; 528 | for (j = vbox.r1; j <= vbox.r2; j++) { 529 | for (k = vbox.g1; k <= vbox.g2; k++) { 530 | index = getColorIndex(j,k,i); 531 | sum += (histo[index] || 0); 532 | } 533 | } 534 | total += sum; 535 | partialsum[i] = total; 536 | } 537 | } 538 | partialsum.forEach(function(d,i) { 539 | lookaheadsum[i] = total-d; 540 | }); 541 | function doCut(color) { 542 | var dim1 = color + '1', 543 | dim2 = color + '2', 544 | left, right, vbox1, vbox2, d2, count2=0; 545 | for (i = vbox[dim1]; i <= vbox[dim2]; i++) { 546 | if (partialsum[i] > total / 2) { 547 | vbox1 = vbox.copy(); 548 | vbox2 = vbox.copy(); 549 | left = i - vbox[dim1]; 550 | right = vbox[dim2] - i; 551 | if (left <= right) 552 | d2 = Math.min(vbox[dim2] - 1, ~~(i + right / 2)); 553 | else d2 = Math.max(vbox[dim1], ~~(i - 1 - left / 2)); 554 | // avoid 0-count boxes 555 | while (!partialsum[d2]) d2++; 556 | count2 = lookaheadsum[d2]; 557 | while (!count2 && partialsum[d2-1]) count2 = lookaheadsum[--d2]; 558 | // set dimensions 559 | vbox1[dim2] = d2; 560 | vbox2[dim1] = vbox1[dim2] + 1; 561 | // console.log('vbox counts:', vbox.count(), vbox1.count(), vbox2.count()); 562 | return [vbox1, vbox2]; 563 | } 564 | } 565 | 566 | } 567 | // determine the cut planes 568 | return maxw == rw ? doCut('r') : 569 | maxw == gw ? doCut('g') : 570 | doCut('b'); 571 | } 572 | 573 | function quantize(pixels, maxcolors) { 574 | // short-circuit 575 | if (!pixels.length || maxcolors < 2 || maxcolors > 256) { 576 | // console.log('wrong number of maxcolors'); 577 | return false; 578 | } 579 | 580 | // XXX: check color content and convert to grayscale if insufficient 581 | 582 | var histo = getHisto(pixels), 583 | histosize = 1 << (3 * sigbits); 584 | 585 | // check that we aren't below maxcolors already 586 | var nColors = 0; 587 | histo.forEach(function() { nColors++; }); 588 | if (nColors <= maxcolors) { 589 | // XXX: generate the new colors from the histo and return 590 | } 591 | 592 | // get the beginning vbox from the colors 593 | var vbox = vboxFromPixels(pixels, histo), 594 | pq = new PQueue(function(a,b) { return pv.naturalOrder(a.count(), b.count()); }); 595 | pq.push(vbox); 596 | 597 | // inner function to do the iteration 598 | function iter(lh, target) { 599 | var ncolors = 1, 600 | niters = 0, 601 | vbox; 602 | while (niters < maxIterations) { 603 | vbox = lh.pop(); 604 | if (!vbox.count()) { /* just put it back */ 605 | lh.push(vbox); 606 | niters++; 607 | continue; 608 | } 609 | // do the cut 610 | var vboxes = medianCutApply(histo, vbox), 611 | vbox1 = vboxes[0], 612 | vbox2 = vboxes[1]; 613 | 614 | if (!vbox1) { 615 | // console.log("vbox1 not defined; shouldn't happen!"); 616 | return; 617 | } 618 | lh.push(vbox1); 619 | if (vbox2) { /* vbox2 can be null */ 620 | lh.push(vbox2); 621 | ncolors++; 622 | } 623 | if (ncolors >= target) return; 624 | if (niters++ > maxIterations) { 625 | // console.log("infinite loop; perhaps too few pixels!"); 626 | return; 627 | } 628 | } 629 | } 630 | 631 | // first set of colors, sorted by population 632 | iter(pq, fractByPopulations * maxcolors); 633 | 634 | // Re-sort by the product of pixel occupancy times the size in color space. 635 | var pq2 = new PQueue(function(a,b) { 636 | return pv.naturalOrder(a.count()*a.volume(), b.count()*b.volume()); 637 | }); 638 | while (pq.size()) { 639 | pq2.push(pq.pop()); 640 | } 641 | 642 | // next set - generate the median cuts using the (npix * vol) sorting. 643 | iter(pq2, maxcolors - pq2.size()); 644 | 645 | // calculate the actual colors 646 | var cmap = new CMap(); 647 | while (pq2.size()) { 648 | cmap.push(pq2.pop()); 649 | } 650 | 651 | return cmap; 652 | } 653 | 654 | return { 655 | quantize: quantize 656 | }; 657 | })(); 658 | -------------------------------------------------------------------------------- /public/js/fullscreen.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var socket = io(); 3 | 4 | $(document).ready(function() { 5 | $("#buttonMenu").html(getSVG("menu")); 6 | 7 | socket.on("pairStatus", function(payload) { 8 | var pairEnabled = payload.pairEnabled; 9 | 10 | if (pairEnabled === true) { 11 | showSection("nowPlaying"); 12 | } else { 13 | showSection("pairDisabled"); 14 | } 15 | }); 16 | }); 17 | 18 | function showSection(sectionName) { 19 | switch (sectionName) { 20 | case "nowPlaying": 21 | $("#buttonMenu").show(); 22 | // Show Now Playing screen 23 | $("#nowPlaying").show(); 24 | // Hide inactive sections 25 | $("#pairDisabled").hide(); 26 | $("#libraryBrowser").hide(); 27 | $("#overlayMainMenu").hide(); 28 | break; 29 | case "libraryBrowser": 30 | $("#buttonMenu").show(); 31 | // Show libraryBrowser 32 | $("#libraryBrowser").show(); 33 | // Hide inactive sections 34 | $("#pairDisabled").hide(); 35 | $("#nowPlaying").hide(); 36 | $("#overlayMainMenu").hide(); 37 | break; 38 | case "pairDisabled": 39 | // Show pairDisabled section 40 | $("#pairDisabled").show(); 41 | // Hide everthing else 42 | $("#buttonMenu").hide(); 43 | $("#libraryBrowser").hide(); 44 | $("#nowPlaying").hide(); 45 | $("#pageLoading").hide(); 46 | break; 47 | default: 48 | break; 49 | } 50 | var t = setTimeout(function() { 51 | $("#pageLoading").hide(); 52 | }, 250); 53 | } 54 | 55 | function getSVG(cmd) { 56 | switch (cmd) { 57 | case "menu": 58 | return ''; 59 | default: 60 | break; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /public/js/jquery.simplemarquee.js: -------------------------------------------------------------------------------- 1 | !(function (root, factory) { 2 | 3 | 'use strict'; 4 | 5 | if (typeof define === 'function' && define.amd) { 6 | define(['jquery'], factory); 7 | } else if (typeof exports === 'object') { 8 | module.exports = factory(require('jquery')); 9 | } else { 10 | factory(root.jQuery); 11 | } 12 | } (this, function ($) { 13 | 14 | 'use strict'; 15 | 16 | var cssPrefixes = ['-webkit-', '-moz-', '-o-', ''], 17 | eventPrefixes = ['webkit', 'moz', 'MS', 'o', '']; 18 | 19 | function prefixedEvent(element, name, callback) { 20 | eventPrefixes.forEach(function (prefix) { 21 | if (!prefix) { 22 | name = name.toLowerCase(); 23 | } 24 | 25 | element.on(prefix + name, callback); 26 | }); 27 | } 28 | 29 | function SimpleMarquee(element, options) { 30 | this._element = $(element); 31 | this._options = $.extend({ 32 | speed: 30, 33 | direction: 'left', 34 | cycles: 1, 35 | space: 40, 36 | delayBetweenCycles: 2000, 37 | handleHover: true, 38 | handleResize: true, 39 | easing: 'linear' 40 | }, options); 41 | 42 | this._resizeDelay = parseInt(this._options.handleResize, 10) || 300; 43 | this._horizontal = this._options.direction === 'left' || this._options.direction === 'right'; 44 | this._animationName = 'simplemarquee-' + Math.round((Math.random() * 10000000000000)).toString(18); 45 | 46 | // Binds 47 | this._onResize = this._onResize.bind(this); 48 | this._onCycle = this._onCycle.bind(this); 49 | 50 | // Events 51 | this._options.handleResize && $(window).on('resize', this._onResize); 52 | this._options.handleHover && this._element.on({ 53 | 'mouseenter.simplemarquee': this._onMouseEnter.bind(this), 54 | 'mouseleave.simplemarquee': this._onMouseLeave.bind(this) 55 | }); 56 | 57 | // Destroy event, see: https://github.com/IndigoUnited/jquery.destroy-event 58 | this._element.on('destroy.simplemarquee', this.destroy.bind(this)); 59 | 60 | // Init! 61 | this.update(true); 62 | } 63 | 64 | // ---------------------------------- 65 | 66 | SimpleMarquee.prototype.update = function (restart) { 67 | this._reset(); 68 | this._setup(); 69 | 70 | // If no animation is needed, reset vars 71 | if (!this._needsAnimation) { 72 | this._paused = false; 73 | this._cycles = 0; 74 | // If asked to restart, start from the begining 75 | } else if (restart) { 76 | this._paused = false; 77 | this._cycles = -1; 78 | this._onCycle(); 79 | // Pause it if the animation was paused 80 | } else if (this._paused) { 81 | this._pause(); 82 | } 83 | 84 | return this; 85 | }; 86 | 87 | SimpleMarquee.prototype.pause = function () { 88 | if (this._needsAnimation) { 89 | this._resetCycle(); 90 | 91 | if (!this._paused) { 92 | this._pause(); 93 | this._element.triggerHandler('pause'); 94 | this._paused = true; 95 | } 96 | } 97 | 98 | return this; 99 | }; 100 | 101 | SimpleMarquee.prototype.resume = function () { 102 | if (this._needsAnimation) { 103 | this._resetCycle(); 104 | 105 | if (this._paused) { 106 | this._resume(); 107 | this._element.triggerHandler('resume'); 108 | this._paused = false; 109 | } 110 | } 111 | 112 | return this; 113 | }; 114 | 115 | SimpleMarquee.prototype.toggle = function () { 116 | this._paused ? this.resume() : this.pause(); 117 | 118 | return this; 119 | }; 120 | 121 | SimpleMarquee.prototype.destroy = function () { 122 | this._reset(); 123 | 124 | // Cancel timeouts 125 | this._resizeTimeout && clearTimeout(this._resizeTimeout); 126 | 127 | // Clear listeners 128 | $(window).off('resize', this._onResize); 129 | this._element.off('.simplemarquee'); 130 | 131 | this._element.removeData('_simplemarquee'); 132 | this._element = null; 133 | }; 134 | 135 | // -------------------- 136 | 137 | SimpleMarquee.prototype._reset = function () { 138 | // Reset styles 139 | this._element 140 | .removeClass('has-enough-space') 141 | .css({ 142 | 'word-wrap': '', // Deprecated in favor of overflow wrap 143 | 'overflow-wrap': '', 144 | 'white-space': '', 145 | 'overflow': '', 146 | }); 147 | 148 | // Remove created elements 149 | // Recover contents only if the contents are still there 150 | // This is necessary because the user might have called .html() and .simplemarquee('update') 151 | // In this situation, we should not restore the original contents 152 | if (this._wrappers) { 153 | this._contents.closest(this._element).length && this._element.append(this._contents); 154 | this._wrappers.remove(); 155 | this._element.children('style').remove(); 156 | } 157 | 158 | // Reset vars 159 | this._contents = this._wrappers = this._size = null; 160 | this._needsAnimation = false; 161 | 162 | // Reset cycle timer 163 | this._resetCycle(); 164 | }; 165 | 166 | SimpleMarquee.prototype._setup = function () { 167 | var wrapper; 168 | 169 | // Set necessary wrap styles and decide if we need the marquee 170 | if (this._horizontal) { 171 | this._element.css({ 172 | 'word-wrap': 'normal', // Deprecated in favor of overflow wrap 173 | 'overflow-wrap': 'normal', 174 | 'white-space': 'nowrap', 175 | 'overflow': 'hidden', 176 | }); 177 | 178 | this._needsAnimation = this._element[0].scrollWidth > Math.ceil(this._element.outerWidth()); 179 | } else { 180 | this._element.css({ 181 | 'word-wrap': 'break-word', // Deprecated in favor of overflow wrap 182 | 'overflow-wrap': 'break-word', 183 | 'white-space': 'normal', 184 | 'overflow': 'hidden', 185 | }); 186 | 187 | this._needsAnimation = this._element[0].scrollHeight > Math.ceil(this._element.outerHeight()); 188 | } 189 | 190 | this._element.toggleClass('has-enough-space', !this._needsAnimation); 191 | 192 | // If marquee is not necessary, skip the code bellow 193 | if (!this._needsAnimation) { 194 | return; 195 | } 196 | 197 | // Wrap contents 198 | this._contents = this._element.contents(); 199 | wrapper = $(''); 200 | wrapper.append(this._contents); 201 | this._element.append(wrapper); 202 | wrapper = $(''); 203 | wrapper.append(this._contents.clone()); 204 | this._element.append(wrapper); 205 | this._wrappers = this._element.children(); 206 | 207 | // Calculate the contents size and define the margin according 208 | // to the specified space option 209 | if (this._horizontal) { 210 | this._wrappers.css('display', 'inline-block'); // Use display inline block for the wrappers 211 | this._wrappers.eq(1).css('margin-left', this._options.space); 212 | this._size = this._wrappers.eq(0).outerWidth() + this._options.space; 213 | } else { 214 | this._wrappers.eq(1).css('margin-top', this._options.space); 215 | this._size = this._wrappers.eq(0).outerHeight() + this._options.space; 216 | } 217 | 218 | // Build the animation 219 | this._setupAnimation(); 220 | }; 221 | 222 | SimpleMarquee.prototype._setupAnimation = function () { 223 | var styleStr; 224 | 225 | // Add the style element 226 | styleStr = '\n'; 255 | 256 | // Append the style and associate the animation to the wrappers 257 | this._element.append(styleStr); 258 | this._wrappers.css('animation', this._animationName + ' ' + (this._size / this._options.speed) + 's ' + this._options.easing + ' infinite'); 259 | 260 | // Setup animation listeners 261 | prefixedEvent(this._wrappers.eq(0), 'AnimationIteration', this._onCycle); 262 | }; 263 | 264 | SimpleMarquee.prototype._pause = function () { 265 | this._wrappers.css('animation-play-state', 'paused'); 266 | }; 267 | 268 | SimpleMarquee.prototype._resume = function () { 269 | this._wrappers.css('animation-play-state', ''); 270 | }; 271 | 272 | SimpleMarquee.prototype._resetCycle = function () { 273 | if (this._cycleTimeout) { 274 | clearTimeout(this._cycleTimeout); 275 | this._cycleTimeout = null; 276 | } 277 | }; 278 | 279 | SimpleMarquee.prototype._onCycle = function () { 280 | this._resetCycle(); 281 | 282 | this._cycles += 1; 283 | 284 | // Pause if reached the end 285 | if (this._cycles >= this._options.cycles) { 286 | this.pause(); 287 | this._element.triggerHandler('finish'); 288 | // Otherwise pause it and schedule the resume 289 | } else { 290 | this._pause(); 291 | this._element.triggerHandler('cycle'); 292 | 293 | this._cycleTimeout = setTimeout(function () { 294 | this._cycleTimeout = null; 295 | this._resume(); 296 | }.bind(this), this._options.delayBetweenCycles); 297 | } 298 | }; 299 | 300 | SimpleMarquee.prototype._onMouseEnter = function () { 301 | // Restart if already finished 302 | if (this._paused) { 303 | this._cycles = 0; 304 | this.resume(); 305 | } else { 306 | this.pause(); 307 | } 308 | }; 309 | 310 | SimpleMarquee.prototype._onMouseLeave = function () { 311 | this.resume(); 312 | }; 313 | 314 | SimpleMarquee.prototype._onResize = function () { 315 | this._resizeTimeout && clearTimeout(this._resizeTimeout); 316 | this._resizeTimeout = setTimeout(function () { 317 | this._resizeTimeout = null; 318 | this.update(); 319 | }.bind(this), this._resizeDelay); 320 | }; 321 | 322 | // ----------------------------------------------- 323 | 324 | $.fn.simplemarquee = function (options) { 325 | this.each(function (index, el) { 326 | var instance; 327 | 328 | el = $(el); 329 | instance = el.data('_simplemarquee'); 330 | 331 | // .simplemarquee('method') 332 | if (typeof options === 'string') { 333 | if (!instance) { 334 | return; 335 | } 336 | 337 | instance[options](arguments[1]); 338 | // .simplemarquee({}) 339 | } else { 340 | if (!instance) { 341 | instance = new SimpleMarquee(el, options); 342 | el.data('_simplemarquee', instance); 343 | } else { 344 | instance.update(true); 345 | } 346 | } 347 | }); 348 | 349 | return this; 350 | }; 351 | 352 | $.fn.simplemarquee.Constructor = SimpleMarquee; 353 | 354 | return $; 355 | })); 356 | -------------------------------------------------------------------------------- /public/js/library.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var socket = io(); 3 | var settings = []; 4 | 5 | $(document).ready(function() { 6 | showPage(); 7 | }); 8 | 9 | function showPage() { 10 | // Read settings from cookie 11 | settings.zoneID = readCookie("settings['zoneID']"); 12 | settings.displayName = readCookie("settings['displayName']"); 13 | 14 | // Set page fields to settings 15 | if (settings.zoneID === null) { 16 | $("#overlayZoneList").show(); 17 | } 18 | 19 | if (settings.displayName !== null) { 20 | $(".buttonZoneName").html(settings.displayName); 21 | if (settings.zoneID !== null) { 22 | goHome(); 23 | } 24 | } 25 | 26 | enableSockets(); 27 | } 28 | 29 | function enableSockets() { 30 | socket.on("zoneList", function(payload) { 31 | $(".zoneList").html(""); 32 | 33 | if (payload !== undefined) { 34 | for (var x in payload) { 35 | $(".zoneList").append( 36 | '" 45 | ); 46 | } 47 | // Set zone button to active 48 | $(".buttonZoneId").removeClass("buttonSettingActive"); 49 | $("#button-" + settings.zoneID).addClass("buttonSettingActive"); 50 | } 51 | }); 52 | } 53 | 54 | function selectZone(zone_id, display_name) { 55 | settings.zoneID = zone_id; 56 | settings.displayName = display_name; 57 | $(".buttonZoneName").html(settings.displayName); 58 | 59 | // Set zone button to active 60 | $(".buttonZoneId").removeClass("buttonSettingActive"); 61 | $("#button-" + settings.zoneID).addClass("buttonSettingActive"); 62 | $("#overlayZoneList").hide(); 63 | goHome(settings.zoneID); 64 | } 65 | 66 | function goBack() { 67 | var data = {}; 68 | data.zone_id = settings.zoneID; 69 | data.options = { pop_levels: 1 }; 70 | 71 | $.ajax({ 72 | type: "POST", 73 | data: JSON.stringify(data), 74 | contentType: "application/json", 75 | url: "/roonapi/goRefreshBrowse", 76 | success: function(payload) { 77 | showData(payload, settings.zoneID, 1); 78 | } 79 | }); 80 | } 81 | 82 | function goHome() { 83 | var data = {}; 84 | data.zone_id = settings.zoneID; 85 | data.options = { pop_all: true }; 86 | 87 | $.ajax({ 88 | type: "POST", 89 | data: JSON.stringify(data), 90 | contentType: "application/json", 91 | url: "/roonapi/goRefreshBrowse", 92 | success: function(payload) { 93 | showData(payload, settings.zoneID); 94 | } 95 | }); 96 | } 97 | 98 | function goRefresh() { 99 | var data = {}; 100 | data.zone_id = settings.zoneID; 101 | data.options = { refresh_list: true }; 102 | 103 | $.ajax({ 104 | type: "POST", 105 | data: JSON.stringify(data), 106 | contentType: "application/json", 107 | url: "/roonapi/goRefreshBrowse", 108 | success: function(payload) { 109 | showData(payload, settings.zoneID); 110 | } 111 | }); 112 | } 113 | 114 | function goList(item_key, listoffset) { 115 | var data = {}; 116 | data.zone_id = settings.zoneID; 117 | data.options = { item_key: item_key }; 118 | 119 | if (listoffset === undefined) { 120 | data.listoffset = 0; 121 | } else { 122 | data.listoffset = listoffset; 123 | } 124 | 125 | $.ajax({ 126 | type: "POST", 127 | data: JSON.stringify(data), 128 | contentType: "application/json", 129 | url: "/roonapi/goRefreshBrowse", 130 | success: function(payload) { 131 | showData(payload, settings.zoneID); 132 | } 133 | }); 134 | } 135 | 136 | function goPage(listoffset) { 137 | var data = {}; 138 | if (listoffset === undefined) { 139 | data.listoffset = 0; 140 | } else { 141 | data.listoffset = listoffset; 142 | } 143 | 144 | $.ajax({ 145 | type: "POST", 146 | data: JSON.stringify(data), 147 | contentType: "application/json", 148 | url: "/roonapi/goLoadBrowse", 149 | success: function(payload) { 150 | showData(payload, settings.zoneID); 151 | } 152 | }); 153 | } 154 | 155 | function goSearch() { 156 | var data = {}; 157 | data.zone_id = settings.zoneID; 158 | data.options = {}; 159 | if ($("#searchText").val() === "" || $("#searchItemKey").val() === "") { 160 | return; 161 | } else { 162 | data.options.item_key = $("#searchItemKey").val(); 163 | data.options.input = $("#searchText").val(); 164 | } 165 | 166 | $.ajax({ 167 | type: "POST", 168 | data: JSON.stringify(data), 169 | contentType: "application/json", 170 | url: "/roonapi/goRefreshBrowse", 171 | success: function(payload) { 172 | showData(payload, settings.zoneID); 173 | } 174 | }); 175 | } 176 | 177 | function showData(payload, zone_id) { 178 | $("#buttonRefresh") 179 | .html(getSVG("refresh")) 180 | .attr("onclick", "goRefresh()"); 181 | 182 | var items = payload.data.items; 183 | var list = payload.data.list; 184 | 185 | $("#items").html(""); 186 | 187 | if (items !== null) { 188 | $("#listTitle").html(list.title); 189 | $("#listSubtitle").html(list.subtitle); 190 | 191 | if (list.image_key) { 192 | $("#listImage") 193 | .html( 194 | '' 197 | ) 198 | .show(); 199 | $("#coverBackground") 200 | .css( 201 | "background-image", 202 | 'url("/roonapi/getImage?image_key=' + list.image_key + '")' 203 | ) 204 | .show(); 205 | } else { 206 | $("#listImage") 207 | .html("") 208 | .hide(); 209 | $("#coverBackground").hide(); 210 | } 211 | 212 | for (var x in items) { 213 | var html = ""; 214 | if (items[x].input_prompt) { 215 | html += '
'; 216 | html += 217 | ''; 220 | html += 221 | '"; 224 | html += 225 | ''; 228 | html += "
"; 229 | 230 | $("#items").append(html); 231 | } else { 232 | html += 233 | '"; 243 | html += ""; 244 | 245 | $("#items").append(html); 246 | } 247 | } 248 | 249 | if (list.level == 0) { 250 | $("#buttonBack") 251 | .prop("disabled", true) 252 | .attr("aria-disabled", true) 253 | .html(getSVG("back")); 254 | $("#buttonHome") 255 | .prop("disabled", true) 256 | .attr("aria-disabled", true) 257 | .html(getSVG("home")); 258 | } else { 259 | $("#buttonBack") 260 | .attr("onclick", "goBack()") 261 | .attr("aria-disabled", false) 262 | .html(getSVG("back")) 263 | .prop("disabled", false); 264 | $("#buttonHome") 265 | .attr("onclick", "goHome()") 266 | .attr("aria-disabled", false) 267 | .html(getSVG("home")) 268 | .prop("disabled", false); 269 | } 270 | 271 | if (list.display_offset > 0) { 272 | $("#buttonPrev") 273 | .prop("disabled", false) 274 | .attr("onclick", "goPage('" + (list.display_offset - 100) + "')") 275 | .attr("aria-disabled", false) 276 | .html(getSVG("prev")); 277 | } else { 278 | $("#buttonPrev") 279 | .prop("disabled", true) 280 | .attr("aria-disabled", true) 281 | .html(getSVG("prev")); 282 | } 283 | 284 | if (list.display_offset + items.length < list.count) { 285 | $("#buttonNext") 286 | .prop("disabled", false) 287 | .attr("aria-disabled", false) 288 | .attr("onclick", "goPage('" + (list.display_offset + 100) + "')") 289 | .html(getSVG("next")); 290 | } else { 291 | $("#buttonNext") 292 | .prop("disabled", true) 293 | .attr("aria-disabled", true) 294 | .html(getSVG("next")); 295 | } 296 | 297 | $("#pageNumber").html( 298 | list.display_offset + 299 | 1 + 300 | "-" + 301 | (list.display_offset + items.length) + 302 | " of " + 303 | list.count 304 | ); 305 | 306 | if ( 307 | $("#buttonPrev").prop("disabled") === true && 308 | $("#buttonNext").prop("disabled") === true 309 | ) { 310 | $("#navLine2").hide(); 311 | $("#content").css("bottom", "0"); 312 | } else { 313 | $("#navLine2").show(); 314 | $("#content").css("bottom", "48px"); 315 | } 316 | } 317 | } 318 | 319 | function readCookie(name) { 320 | return Cookies.get(name); 321 | } 322 | 323 | function getSVG(cmd) { 324 | switch (cmd) { 325 | case "home": 326 | return ''; 327 | case "back": 328 | return ''; 329 | case "refresh": 330 | return ''; 331 | case "prev": 332 | return ''; 333 | case "next": 334 | return ''; 335 | case "backspace": 336 | return ''; 337 | case "search": 338 | return ''; 339 | case "music": 340 | return ''; 341 | default: 342 | break; 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /public/js/nowplaying.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var socket = io(); 3 | var noSleep = new NoSleep(); 4 | var curZone; 5 | var css = []; 6 | var settings = []; 7 | var state = []; 8 | var inVolumeSlider = false; 9 | 10 | $(document).ready(function() { 11 | showPage(); 12 | fixFontSize(); 13 | }); 14 | 15 | function toggleCircleIcon() { 16 | if ($("#circleIconsSwitch").is(":checked", false)) { 17 | // Triggered when the unchecked toggle has been checked 18 | $("#circleIconsSwitch").prop("checked", true); 19 | settings.useCircleIcons = true; 20 | state = []; 21 | } else { 22 | // Triggered when the checked toggle has been unchecked 23 | $("#circleIconsSwitch").prop("checked", false); 24 | settings.useCircleIcons = false; 25 | state = []; 26 | } 27 | setCookie("settings['useCircleIcons']", settings.useCircleIcons); 28 | } 29 | 30 | function toggle4kImages() { 31 | if ($("#4kImagesSwitch").is(":checked", false)) { 32 | // Triggered when the unchecked toggle has been checked 33 | $("#4kImagesSwitch").prop("checked", true); 34 | settings.use4kImages = true; 35 | state = []; 36 | } else { 37 | // Triggered when the checked toggle has been unchecked 38 | $("#4kImagesSwitch").prop("checked", false); 39 | settings.use4kImages = false; 40 | state = []; 41 | } 42 | setCookie("settings['use4kImages']", settings.use4kImages); 43 | } 44 | 45 | function toggleScreensaver() { 46 | if ($("#screensaverSwitch").is(":checked", false)) { 47 | // Triggered when the unchecked toggle has been checked 48 | $("#screensaverSwitch").prop("checked", true); 49 | settings.screensaverDisable = true; 50 | state = []; 51 | } else { 52 | // Triggered when the checked toggle has been unchecked 53 | $("#screensaverSwitch").prop("checked", false); 54 | settings.screensaverDisable = false; 55 | state = []; 56 | } 57 | setCookie("settings['screensaverDisable']", settings.screensaverDisable); 58 | } 59 | 60 | function toggleNotifications() { 61 | if ($("#notificationsSwitch").is(":checked", false)) { 62 | // Triggered when the unchecked toggle has been checked 63 | $("#notificationsSwitch").prop("checked", true); 64 | settings.showNotifications = true; 65 | settings.showNotificationsChanged = true; 66 | } else { 67 | // Triggered when the checked toggle has been unchecked 68 | $("#notificationsSwitch").prop("checked", false); 69 | settings.showNotifications = false; 70 | settings.showNotificationsChanged = true; 71 | } 72 | setCookie("settings['showNotifications']", settings.showNotifications); 73 | } 74 | 75 | function notifyMe(three_line) { 76 | // Let's check if the browser supports notifications 77 | if (!("Notification" in window)) { 78 | console.log("This browser does not support desktop notification"); 79 | } 80 | 81 | // Let's check whether notification permissions have already been granted 82 | else if (Notification.permission === "granted") { 83 | // If it's okay let's create a notification 84 | var options = { 85 | body: three_line.line2 + " - " + three_line.line3, 86 | icon: "/roonapi/getImage?image_key=" + curZone.now_playing.image_key 87 | }; 88 | var notification = new Notification(three_line.line1, options); 89 | } 90 | 91 | // Otherwise, we need to ask the user for permission 92 | else if (Notification.permission !== "denied") { 93 | Notification.requestPermission(function(permission) { 94 | // If the user accepts, let's create a notification 95 | if (permission === "granted") { 96 | var options = { 97 | body: three_line.line2 + " - " + three_line.line3, 98 | icon: "/roonapi/getImage?image_key=" + curZone.now_playing.image_key 99 | }; 100 | var notification = new Notification(three_line.line1, options); 101 | } 102 | }); 103 | } 104 | 105 | // At last, if the user has denied notifications, and you 106 | // want to be respectful there is no need to bother them any more. 107 | console.log(notification); 108 | } 109 | 110 | function fixFontSize() { 111 | $(".lineMusicInfo").css( 112 | "font-size", 113 | Math.round(parseInt($("#line1").height() * 0.75)) 114 | ); 115 | 116 | var zoneFontSize = Math.round(parseInt($("#containerZoneList").height() / 3)); 117 | if (zoneFontSize <= 20) { 118 | $("#nowplayingZoneList").css("font-size", 20); 119 | } else { 120 | $("#nowplayingZoneList").css("font-size", zoneFontSize); 121 | } 122 | } 123 | 124 | function showPage() { 125 | // Read settings from cookie 126 | settings.zoneID = readCookie("settings['zoneID']"); 127 | settings.displayName = readCookie("settings['displayName']"); 128 | settings.theme = readCookie("settings['theme']"); 129 | 130 | var showNotifications = readCookie("settings['showNotifications']"); 131 | if (showNotifications === "true") { 132 | settings.showNotifications = true; 133 | $("#notificationsSwitch").prop("checked", true); 134 | } else { 135 | settings.showNotifications = false; 136 | $("#notificationsSwitch").prop("checked", false); 137 | } 138 | 139 | var useCircleIcons = readCookie("settings['useCircleIcons']"); 140 | if (useCircleIcons === "true") { 141 | settings.useCircleIcons = true; 142 | $("#circleIconsSwitch").prop("checked", true); 143 | } else { 144 | settings.useCircleIcons = false; 145 | $("#circleIconsSwitch").prop("checked", false); 146 | } 147 | 148 | var use4kImages = readCookie("settings['use4kImages']"); 149 | if (use4kImages === "true") { 150 | settings.use4kImages = true; 151 | $("#4kImagesSwitch").prop("checked", true); 152 | } else { 153 | settings.use4kImages = false; 154 | $("#4kImagesSwitch").prop("checked", false); 155 | } 156 | 157 | var screensaverDisable = readCookie("settings['screensaverDisable']"); 158 | if (screensaverDisable === "true") { 159 | settings.screensaverDisable = true; 160 | $("#screensaverSwitch").prop("checked", true); 161 | } else { 162 | settings.screensaverDisable = false; 163 | $("#screensaverSwitch").prop("checked", false); 164 | } 165 | 166 | // Set page fields to settings 167 | if (settings.zoneID === undefined) { 168 | $("#overlayZoneList").show(); 169 | } 170 | 171 | if (settings.displayName !== undefined) { 172 | $(".buttonZoneName").html(settings.displayName); 173 | } 174 | 175 | if (settings.theme === undefined) { 176 | settings.theme = "dark"; 177 | setCookie("settings['theme']", settings.theme); 178 | setTheme(settings.theme); 179 | } else { 180 | setTheme(settings.theme); 181 | } 182 | 183 | // Get Buttons 184 | $("#buttonVolume").html(getSVG("volume")); 185 | $("#buttonSettings").html(getSVG("settings")); 186 | 187 | // Hide pages until player state is determined 188 | $("#notPlaying").hide(); 189 | $("#isPlaying").hide(); 190 | 191 | enableSockets(); 192 | } 193 | 194 | function enableSockets() { 195 | socket.on("zoneList", function(payload) { 196 | $(".zoneList").html(""); 197 | 198 | if (payload !== undefined) { 199 | var payloadids = []; 200 | for (var x in payload) { 201 | $(".zoneList").append( 202 | '" 211 | ); 212 | payloadids.push(payload[x].zone_id); 213 | } 214 | if (payloadids.includes(settings.zoneID) === false) { 215 | $("#overlayZoneList").show(); 216 | } 217 | } 218 | }); 219 | 220 | socket.on("zoneStatus", function(payload) { 221 | if (settings.zoneID !== undefined) { 222 | for (var x in payload) { 223 | if (payload[x].zone_id == settings.zoneID) { 224 | curZone = payload[x]; 225 | // Set zone button to active 226 | $(".buttonZoneId").removeClass("buttonSettingActive"); 227 | $("#button-" + settings.zoneID).addClass("buttonSettingActive"); 228 | 229 | updateZone(curZone); 230 | } else { 231 | curZone = undefined; 232 | } 233 | } 234 | } 235 | }); 236 | } 237 | 238 | function selectZone(zone_id, display_name) { 239 | settings.zoneID = zone_id; 240 | setCookie("settings['zoneID']", settings.zoneID); 241 | 242 | settings.displayName = display_name; 243 | setCookie("settings['displayName']", settings.displayName); 244 | $(".buttonZoneName").html(settings.displayName); 245 | 246 | // Set zone button to active 247 | $(".buttonZoneId").removeClass("buttonSettingActive"); 248 | $("#button-" + settings.zoneID).addClass("buttonSettingActive"); 249 | 250 | // Reset state on zone switch 251 | state = []; 252 | socket.emit("getZone", zone_id); 253 | 254 | $("#overlayZoneList").hide(); 255 | } 256 | 257 | function updateZone(curZone) { 258 | if (curZone.now_playing) { 259 | showIsPlaying(curZone); 260 | } else { 261 | showNotPlaying(); 262 | } 263 | } 264 | 265 | function showNotPlaying() { 266 | $("#notPlaying").show(); 267 | $("#isPlaying").hide(); 268 | 269 | // Reset icons 270 | $("#controlPrev") 271 | .html(getSVG("prev")) 272 | .removeClass("buttonAvailable") 273 | .addClass("buttonInactive"); 274 | $("#controlPlayPauseStop") 275 | .html(getSVG("play")) 276 | .removeClass("buttonAvailable") 277 | .addClass("buttonInactive"); 278 | $("#controlNext") 279 | .html(getSVG("next")) 280 | .removeClass("buttonAvailable") 281 | .addClass("buttonInactive"); 282 | $("#buttonLoop") 283 | .html(getSVG("loop")) 284 | .removeClass("buttonAvailable buttonActive") 285 | .addClass("buttonInactive"); 286 | $("#buttonShuffle") 287 | .html(getSVG("shuffle")) 288 | .removeClass("buttonAvailable buttonActive") 289 | .addClass("buttonInactive"); 290 | $("#buttonRadio") 291 | .html(getSVG("radio")) 292 | .removeClass("buttonAvailable buttonActive") 293 | .addClass("buttonInactive"); 294 | 295 | // Blank text fields 296 | $("#line1, #line2, #line3, #seekPosition, #seekLength").html(" "); 297 | $("#trackSeekValue").css("width", "0%"); 298 | 299 | // Reset pictures 300 | $("#containerCoverImage").html( 301 | '' 302 | ); 303 | $("#coverBackground").css("background-image", "url('/img/transparent.png')"); 304 | 305 | // Turn off screensaverDisable 306 | noSleep.disable(); 307 | 308 | // Reset state and browser title 309 | state = []; 310 | $(document).prop("title", "Roon Web Controller"); 311 | } 312 | 313 | function showIsPlaying(curZone) { 314 | $("#notPlaying").hide(); 315 | $("#isPlaying").show(); 316 | 317 | if (state.line1 != curZone.now_playing.three_line.line1) { 318 | state.line1 = curZone.now_playing.three_line.line1; 319 | fixFontSize(); 320 | $("#line1") 321 | .html(state.line1) 322 | .simplemarquee({ 323 | cycles: Infinity, 324 | delayBetweenCycles: 5000, 325 | handleHover: false 326 | }); 327 | } 328 | 329 | if (state.line2 != curZone.now_playing.three_line.line2) { 330 | state.line2 = curZone.now_playing.three_line.line2; 331 | $("#line2") 332 | .html(curZone.now_playing.three_line.line2) 333 | .simplemarquee({ 334 | cycles: Infinity, 335 | delayBetweenCycles: 5000, 336 | handleHover: false 337 | }); 338 | } 339 | 340 | if (state.line3 != curZone.now_playing.three_line.line3) { 341 | state.line3 = curZone.now_playing.three_line.line3; 342 | $("#line3") 343 | .html(curZone.now_playing.three_line.line3) 344 | .simplemarquee({ 345 | cycles: Infinity, 346 | delayBetweenCycles: 5000, 347 | handleHover: false 348 | }); 349 | } 350 | 351 | if (state.title != curZone.now_playing.one_line.line1) { 352 | state.title = curZone.now_playing.one_line.line1; 353 | $(document).prop("title", curZone.now_playing.one_line.line1); 354 | if (settings.showNotifications === true) { 355 | notifyMe(curZone.now_playing.three_line); 356 | } 357 | } 358 | 359 | if (settings.showNotificationsChanged === true) { 360 | if (settings.showNotifications === true) { 361 | notifyMe(curZone.now_playing.three_line); 362 | } 363 | settings.showNotificationsChanged = false; 364 | } 365 | 366 | if (curZone.is_seek_allowed === true) { 367 | $("#seekPosition").html(secondsConvert(curZone.now_playing.seek_position)); 368 | $("#seekLength").html(secondsConvert(curZone.now_playing.length)); 369 | $("#trackSeekValue").css( 370 | "width", 371 | Math.round( 372 | (curZone.now_playing.seek_position / curZone.now_playing.length) * 100 373 | ) + "%" 374 | ); 375 | } else { 376 | $("#seekPosition, #seekLength").html(" "); 377 | $("#trackSeekValue").css("width", "0%"); 378 | } 379 | 380 | if ( 381 | state.image_key != curZone.now_playing.image_key || 382 | state.image_key === undefined 383 | ) { 384 | state.image_key = curZone.now_playing.image_key; 385 | 386 | if (curZone.now_playing.image_key === undefined) { 387 | state.imgUrl = "/img/transparent.png"; 388 | } else { 389 | if (settings.use4kImages === true) { 390 | state.imgUrl = 391 | "/roonapi/getImage4k?image_key=" + curZone.now_playing.image_key; 392 | state.CTimgUrl = 393 | "/roonapi/getImage?image_key=" + curZone.now_playing.image_key; 394 | } else { 395 | state.imgUrl = 396 | "/roonapi/getImage?image_key=" + curZone.now_playing.image_key; 397 | state.CTimgUrl = 398 | "/roonapi/getImage?image_key=" + curZone.now_playing.image_key; 399 | } 400 | } 401 | $("#containerCoverImage").html( 402 | 'Cover art for ' +
405 |         state.title +
406 |         '' 407 | ); 408 | $("#coverBackground").css("background-image", "url(" + state.imgUrl + ")"); 409 | 410 | if (settings.theme == "color") { 411 | var colorThief = new ColorThief(); 412 | 413 | colorThief.getColorAsync(state.CTimgUrl, function(color) { 414 | var r = color[0]; 415 | var g = color[1]; 416 | var b = color[2]; 417 | css.colorBackground = "rgb(" + color + ")"; 418 | 419 | var yiq = (r * 299 + g * 587 + b * 114) / 1000; 420 | if (yiq >= 128) { 421 | css.backgroundColor = "#eff0f1"; 422 | css.foregroundColor = "#232629"; 423 | css.trackSeek = "rgba(35, 38, 41, 0.33)"; 424 | } else { 425 | css.backgroundColor = "#232629"; 426 | css.foregroundColor = "#eff0f1"; 427 | css.trackSeek = "rgba(239, 240, 241, 0.33)"; 428 | } 429 | $("#colorBackground").show(); 430 | showTheme("color"); 431 | }); 432 | } 433 | } 434 | 435 | if (state.Prev != curZone.is_previous_allowed || state.Prev === undefined) { 436 | state.Prev = curZone.is_previous_allowed; 437 | if (curZone.is_previous_allowed === true) { 438 | $("#controlPrev") 439 | .attr("onclick", "goCmd('prev', '" + curZone.zone_id + "')") 440 | .attr("aria-disabled", false) 441 | .html(getSVG("prev")) 442 | .addClass("buttonAvailable") 443 | .removeClass("buttonInactive"); 444 | } else { 445 | $("#controlPrev") 446 | .attr("onclick", "") 447 | .attr("aria-disabled", true) 448 | .html(getSVG("prev")) 449 | .addClass("buttonInactive") 450 | .removeClass("buttonAvailable"); 451 | } 452 | } 453 | 454 | if (state.Next != curZone.is_next_allowed || state.Next === undefined) { 455 | state.Next = curZone.is_next_allowed; 456 | if (curZone.is_next_allowed === true) { 457 | $("#controlNext") 458 | .attr("onclick", "goCmd('next', '" + curZone.zone_id + "')") 459 | .attr("aria-disabled", false) 460 | .html(getSVG("next")) 461 | .addClass("buttonAvailable") 462 | .removeClass("buttonInactive"); 463 | } else { 464 | $("#controlNext") 465 | .attr("onclick", "") 466 | .attr("aria-disabled", true) 467 | .html(getSVG("next")) 468 | .addClass("buttonInactive") 469 | .removeClass("buttonAvailable"); 470 | } 471 | } 472 | 473 | if (curZone.is_play_allowed === true) { 474 | state.PlayPauseStop = "showPlay"; 475 | noSleep.disable(); 476 | } else if (curZone.state == "playing" && curZone.is_play_allowed === false) { 477 | if (curZone.is_pause_allowed === true) { 478 | state.PlayPauseStop = "showPause"; 479 | if (settings.screensaverDisable === true) { 480 | noSleep.enable(); 481 | } else { 482 | noSleep.disable(); 483 | } 484 | } else { 485 | state.PlayPauseStop = "showStop"; 486 | if (settings.screensaverDisable === true) { 487 | noSleep.enable(); 488 | } else { 489 | noSleep.disable(); 490 | } 491 | } 492 | } else { 493 | state.PlayPauseStop = "showPlayDisabled"; 494 | noSleep.disable(); 495 | } 496 | 497 | if ( 498 | state.PlayPauseStopLast != state.PlayPauseStop || 499 | state.PlayPauseStop === undefined 500 | ) { 501 | state.PlayPauseStopLast = state.PlayPauseStop; 502 | if (state.PlayPauseStop == "showPlay") { 503 | $("#controlPlayPauseStop") 504 | .attr("onclick", "goCmd('play', '" + curZone.zone_id + "')") 505 | .attr("aria-disabled", false) 506 | .html(getSVG("play")) 507 | .addClass("buttonAvailable") 508 | .removeClass("buttonInactive"); 509 | } else if (state.PlayPauseStop == "showPause") { 510 | $("#controlPlayPauseStop") 511 | .attr("onclick", "goCmd('pause', '" + curZone.zone_id + "')") 512 | .attr("aria-disabled", false) 513 | .html(getSVG("pause")) 514 | .addClass("buttonAvailable") 515 | .removeClass("buttonInactive"); 516 | } else if (state.PlayPauseStop == "showStop") { 517 | $("#controlPlayPauseStop") 518 | .attr("onclick", "goCmd('stop', '" + curZone.zone_id + "')") 519 | .attr("aria-disabled", false) 520 | .html(getSVG("stop")) 521 | .addClass("buttonAvailable") 522 | .removeClass("buttonInactive"); 523 | } else if (state.PlayPauseStop == "showPlayDisabled") { 524 | $("#controlPlayPauseStop") 525 | .html(getSVG("play")) 526 | .attr("onclick", "") 527 | .attr("aria-disabled", true) 528 | .addClass("buttonInactive") 529 | .removeClass("buttonAvailable"); 530 | } 531 | } 532 | 533 | if (state.Loop != curZone.settings.loop || state.Loop === undefined) { 534 | state.Loop = curZone.settings.loop; 535 | if (state.Loop == "disabled") { 536 | $("#buttonLoop") 537 | .html(getSVG("loop")) 538 | .attr( 539 | "onclick", 540 | "changeZoneSetting('loop', 'loop', '" + curZone.zone_id + "')" 541 | ) 542 | .attr("name", "Loop off") 543 | .attr("aria-label", "Loop off") 544 | .attr("aria-disabled", false) 545 | .removeClass("buttonActive buttonInactive") 546 | .addClass("buttonAvailable") 547 | .css("color", css.foregroundColor); 548 | } else if (state.Loop == "loop_one") { 549 | $("#buttonLoop") 550 | .html(getSVG("loopOne")) 551 | .attr( 552 | "onclick", 553 | "changeZoneSetting('loop', 'disabled', '" + curZone.zone_id + "')" 554 | ) 555 | .attr("name", "Loop one") 556 | .attr("aria-label", "Loop one") 557 | .attr("aria-disabled", false) 558 | .removeClass("buttonAvailable buttonInactive") 559 | .addClass("buttonActive") 560 | .css("color", "#3daee9"); 561 | } else if (state.Loop == "loop") { 562 | $("#buttonLoop") 563 | .html(getSVG("loop")) 564 | .attr( 565 | "onclick", 566 | "changeZoneSetting('loop', 'loop_one', '" + curZone.zone_id + "')" 567 | ) 568 | .attr("name", "Loop all") 569 | .attr("aria-label", "Loop all") 570 | .attr("aria-disabled", false) 571 | .removeClass("buttonAvailable buttonInactive") 572 | .addClass("buttonActive") 573 | .css("color", "#3daee9"); 574 | } else { 575 | $("#buttonLoop") 576 | .html(getSVG("loop")) 577 | .attr("onclick", "") 578 | .attr("name", "Loop disabled") 579 | .attr("aria-label", "Loop disabled") 580 | .attr("aria-disabled", true) 581 | .removeClass("buttonAvailable buttonActive") 582 | .addClass("buttonInactive") 583 | .css("color", css.foregroundColor); 584 | } 585 | } 586 | 587 | if ( 588 | state.Shuffle != curZone.settings.shuffle || 589 | state.Shuffle === undefined 590 | ) { 591 | state.Shuffle = curZone.settings.shuffle; 592 | if (state.Shuffle === false) { 593 | $("#buttonShuffle") 594 | .html(getSVG("shuffle")) 595 | .attr( 596 | "onclick", 597 | "changeZoneSetting('shuffle', 'true', '" + curZone.zone_id + "')" 598 | ) 599 | .attr("name", "Shuffle off") 600 | .attr("aria-label", "Shuffle off") 601 | .attr("aria-disabled", false) 602 | .removeClass("buttonActive buttonInactive") 603 | .addClass("buttonAvailable") 604 | .css("color", css.foregroundColor); 605 | } else if (state.Shuffle === true) { 606 | $("#buttonShuffle") 607 | .html(getSVG("shuffle")) 608 | .attr( 609 | "onclick", 610 | "changeZoneSetting('shuffle', 'false', '" + curZone.zone_id + "')" 611 | ) 612 | .attr("name", "Shuffle on") 613 | .attr("aria-label", "Shuffle on") 614 | .attr("aria-disabled", false) 615 | .removeClass("buttonAvailable buttonInactive") 616 | .addClass("buttonActive") 617 | .css("color", "#3daee9"); 618 | } else { 619 | $("#buttonShuffle") 620 | .html(getSVG("shuffle")) 621 | .attr("onclick", "") 622 | .attr("name", "Shuffle disabled") 623 | .attr("aria-label", "Shuffle disabled") 624 | .attr("aria-disabled", true) 625 | .removeClass("buttonAvailable buttonActive") 626 | .addClass("buttonInactive") 627 | .css("color", css.foregroundColor); 628 | } 629 | } 630 | 631 | if (state.Radio != curZone.settings.auto_radio || state.Radio === undefined) { 632 | state.Radio = curZone.settings.auto_radio; 633 | if (state.Radio === false) { 634 | $("#buttonRadio") 635 | .html(getSVG("radio")) 636 | .attr( 637 | "onclick", 638 | "changeZoneSetting('auto_radio', 'true', '" + curZone.zone_id + "')" 639 | ) 640 | .attr("name", "Roon Radio off") 641 | .attr("aria-label", "Roon Radio off") 642 | .attr("aria-disabled", false) 643 | .removeClass("buttonActive buttonInactive") 644 | .addClass("buttonAvailable") 645 | .css("color", css.foregroundColor); 646 | } else if (state.Radio === true) { 647 | $("#buttonRadio") 648 | .html(getSVG("radio")) 649 | .attr( 650 | "onclick", 651 | "changeZoneSetting('auto_radio', 'false', '" + curZone.zone_id + "')" 652 | ) 653 | .attr("name", "Roon Radio on") 654 | .attr("aria-label", "Roon Radio on") 655 | .attr("aria-disabled", false) 656 | .removeClass("buttonAvailable buttonInactive") 657 | .addClass("buttonActive") 658 | .css("color", "#3daee9"); 659 | } else { 660 | $("#buttonRadio") 661 | .html(getSVG("radio")) 662 | .attr("onclick", "") 663 | .attr("name", "Roon Radio disabled") 664 | .attr("aria-label", "Roon Radio disabled") 665 | .attr("aria-disabled", true) 666 | .removeClass("buttonAvailable buttonActive") 667 | .addClass("buttonInactive") 668 | .css("color", css.foregroundColor); 669 | } 670 | } 671 | 672 | if (inVolumeSlider === false) { 673 | $("#volumeList").html(""); 674 | for (var x in curZone.outputs) { 675 | if (x >= 1) { 676 | $("#volumeList").append("
"); 677 | } 678 | if (curZone.outputs[x].volume) { 679 | var html = 680 | '
' + curZone.outputs[x].display_name + "
"; 681 | html += "
" + curZone.outputs[x].volume.value + "
"; 682 | 683 | html += '
'; 684 | html += '
"; 729 | 730 | $("#volumeList").append(html); 731 | } else { 732 | $("#volumeList") 733 | .append( 734 | '
' + 735 | curZone.outputs[x].display_name + 736 | "
" 737 | ) 738 | .append("
Fixed Volume
"); 739 | } 740 | } 741 | } 742 | 743 | if (state.themeShowing === undefined) { 744 | state.themeShowing = true; 745 | showTheme(settings.theme); 746 | } 747 | } 748 | 749 | function goCmd(cmd, zone_id) { 750 | if (cmd == "prev") { 751 | socket.emit("goPrev", zone_id); 752 | } else if (cmd == "next") { 753 | socket.emit("goNext", zone_id); 754 | } else if (cmd == "play") { 755 | socket.emit("goPlay", zone_id); 756 | } else if (cmd == "pause") { 757 | socket.emit("goPause", zone_id); 758 | } else if (cmd == "stop") { 759 | socket.emit("goStop", zone_id); 760 | } 761 | } 762 | 763 | function changeZoneSetting(zoneSetting, zoneSettingValue, zone_id) { 764 | // for (x in curZone.outputs){ 765 | var msg = JSON.parse( 766 | '{"zone_id": "' + 767 | zone_id + 768 | '", "setting": "' + 769 | zoneSetting + 770 | '", "value": "' + 771 | zoneSettingValue + 772 | '" }' 773 | ); 774 | socket.emit("changeSetting", msg); 775 | // } 776 | } 777 | 778 | function volumeButton(spanId, value, output_id) { 779 | $("#" + spanId + "").html(value); 780 | 781 | var msg = JSON.parse( 782 | '{"output_id": "' + output_id + '", "volume": "' + value + '" }' 783 | ); 784 | socket.emit("changeVolume", msg); 785 | } 786 | 787 | function volumeInput(spanId, value, output_id) { 788 | inVolumeSlider = true; 789 | $("#" + spanId + "").html(value); 790 | 791 | var msg = JSON.parse( 792 | '{"output_id": "' + output_id + '", "volume": "' + value + '" }' 793 | ); 794 | socket.emit("changeVolume", msg); 795 | } 796 | 797 | function volumeChange(id, value, output_id) { 798 | inVolumeSlider = false; 799 | } 800 | 801 | function setTheme(theme) { 802 | settings.theme = theme; 803 | state.themeShowing = undefined; 804 | setCookie("settings['theme']", theme); 805 | 806 | if (theme == "dark" || theme === undefined) { 807 | css.backgroundColor = "#232629"; 808 | css.foregroundColor = "#eff0f1"; 809 | css.trackSeek = "rgba(239, 240, 241, 0.33)"; 810 | 811 | $("#coverBackground").hide(); 812 | $("#colorBackground").hide(); 813 | $("#buttonThemeDark").addClass("buttonSettingActive"); 814 | $("#buttonThemeColor, #buttonThemeCover").removeClass( 815 | "buttonSettingActive" 816 | ); 817 | } else if (theme == "cover") { 818 | css.backgroundColor = "#232629"; 819 | css.foregroundColor = "#eff0f1"; 820 | css.trackSeek = "rgba(239, 240, 241, 0.33)"; 821 | 822 | $("#coverBackground").show(); 823 | $("#colorBackground").hide(); 824 | $("#buttonThemeCover").addClass("buttonSettingActive"); 825 | $("#buttonThemeColor, #buttonThemeDark").removeClass("buttonSettingActive"); 826 | } else if (theme == "color") { 827 | state.image_key = undefined; 828 | $("#coverBackground").hide(); 829 | $("#colorBackground").show(); 830 | $("#buttonThemeColor").addClass("buttonSettingActive"); 831 | $("#buttonThemeDark, #buttonThemeCover").removeClass("buttonSettingActive"); 832 | } else { 833 | settings.theme = undefined; 834 | setTheme(settings.theme); 835 | } 836 | state = []; 837 | socket.emit("getZone", true); 838 | } 839 | 840 | function showTheme(theme) { 841 | $("body") 842 | .css("background-color", css.backgroundColor) 843 | .css("color", css.foregroundColor); 844 | $(".colorChange").css("color", css.foregroundColor); 845 | $("#colorBackground").css("background-color", css.colorBackground); 846 | $(".buttonAvailable").css("color", css.foregroundColor); 847 | $(".buttonInactive").css("color", css.foregroundColor); 848 | $("#trackSeek").css("background-color", css.trackSeek); 849 | socket.emit("getZone", true); 850 | } 851 | 852 | function readCookie(name) { 853 | return Cookies.get(name); 854 | } 855 | 856 | function setCookie(name, value) { 857 | Cookies.set(name, value, { expires: 365 }); 858 | } 859 | 860 | function secondsConvert(seconds) { 861 | seconds = Number(seconds); 862 | var hour = Math.floor(seconds / 3600); 863 | var minute = Math.floor((seconds % 3600) / 60); 864 | var second = Math.floor((seconds % 3600) % 60); 865 | return ( 866 | (hour > 0 ? hour + ":" + (minute < 10 ? "0" : "") : "") + 867 | minute + 868 | ":" + 869 | (second < 10 ? "0" : "") + 870 | second 871 | ); 872 | } 873 | 874 | function getSVG(cmd) { 875 | if (settings.useCircleIcons === true) { 876 | switch (cmd) { 877 | case "play": 878 | return ''; 879 | case "pause": 880 | return ''; 881 | case "stop": 882 | return ''; 883 | default: 884 | break; 885 | } 886 | } else { 887 | switch (cmd) { 888 | case "play": 889 | return ''; 890 | case "pause": 891 | return ''; 892 | case "stop": 893 | return ''; 894 | default: 895 | break; 896 | } 897 | } 898 | 899 | switch (cmd) { 900 | case "loop": 901 | return ''; 902 | case "loopOne": 903 | return ''; 904 | case "shuffle": 905 | return ''; 906 | case "radio": 907 | return ''; 908 | case "prev": 909 | return ''; 910 | case "next": 911 | return ''; 912 | case "volume": 913 | return ''; 914 | case "volume-minus": 915 | return ''; 916 | case "volume-plus": 917 | return ''; 918 | case "settings": 919 | return ''; 920 | default: 921 | break; 922 | } 923 | } 924 | -------------------------------------------------------------------------------- /public/library.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Roon Web Controller - Library Browser 5 | 6 | 7 | 8 | 9 | 10 | 30 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 |
45 | 59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | 76 |
77 |
81 |
82 | 83 |
84 |
85 | 86 | 87 | -------------------------------------------------------------------------------- /public/nowplaying.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Roon Web Controller - Now Playing 5 | 6 | 7 | 8 | 9 | 10 | 30 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 54 | 55 | 56 |
57 |
58 |
59 |
60 |

Nothing playing on

61 | 70 |
71 |
72 |
73 |
74 |
75 |
 
76 |
 
77 |
 
78 |
79 | 86 | 93 | 100 |
101 |
102 |
103 |
104 |   105 |   106 |
107 |
108 |
109 |
110 | 120 |
121 |
122 | 127 | 132 | 137 | 145 | 153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
165 |
166 | 167 |
168 |
169 |
170 |
174 |
175 |
176 |
177 | 187 | 197 | 207 |
208 |
209 |
210 |
Notifications
211 |
212 | 222 |
223 |
224 |
225 |
226 |
Use Circle Icons
227 |
228 | 238 |
239 |
240 |
241 |
242 |
Use 4k Images
243 |
244 | 254 |
255 |
256 |
257 |
258 |
Disable screensaver while playing
259 |
260 | 270 |
271 |
272 |
273 |
274 |
275 |
276 | 277 | 278 | -------------------------------------------------------------------------------- /public/side-by-side.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Roon Web Controller - Side by Side Layout 5 | 6 | 7 | 8 | 9 | 10 | 30 | 35 | 36 | 60 | 61 | 62 |
63 |
64 | 65 | 66 | --------------------------------------------------------------------------------