diff --git a/CMakeLists.txt b/CMakeLists.txt
index 388aab0d..f55403b3 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -116,10 +116,10 @@ find_package(GitVersion)
configure_file("${PROJECT_SOURCE_DIR}/HyperionConfig.h.in" "${PROJECT_BINARY_DIR}/HyperionConfig.h")
include_directories("${PROJECT_BINARY_DIR}")
-if(ENABLE_QT5)
- ADD_DEFINITIONS ( -DENABLE_QT5 )
+if( NOT ENABLE_QT5)
+ #ADD_DEFINITIONS ( -DENABLE_QT5 )
#find_package(Qt5Widgets)
-else()
+#else()
# Add specific cmake modules to find qt4 (default version finds first available QT which might not be qt4)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake/qt4)
endif()
diff --git a/HyperionConfig.h.in b/HyperionConfig.h.in
index 3ce776b5..6f9abff6 100644
--- a/HyperionConfig.h.in
+++ b/HyperionConfig.h.in
@@ -32,6 +32,7 @@
// Define to enable profiler for development purpose
#cmakedefine ENABLE_PROFILER
+#cmakedefine ENABLE_QT5
// the hyperion build id string
#define HYPERION_VERSION_ID "${HYPERION_VERSION_ID}"
diff --git a/assets/webconfig/css/index.css b/assets/webconfig/css/index.css
new file mode 100644
index 00000000..89d347ee
--- /dev/null
+++ b/assets/webconfig/css/index.css
@@ -0,0 +1,609 @@
+@font-face {
+ font-family: 'fontello';
+ src: url('../res/fontello.ttf') format('truetype'), url('../res/fontello.woff') format('woff');
+ font-weight: normal;
+ font-style: normal;
+}
+
+html, body {
+ height: 100%;
+ width: 100%;
+ padding: 0;
+ margin: 0;
+ color: #A6B4B4;
+ background-color: #2C2C2C;
+}
+
+* {
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+ -webkit-touch-callout: none;
+ -webkit-text-size-adjust: none;
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+ font-family: 'Lucida Grande', Helvetica, Arial, Roboto, serif;
+}
+
+#app {
+ height: 100%;
+ width: 100%;
+ display: -webkit-flex;
+ -webkit-flex-direction: column;
+ -webkit-flex-wrap: nowrap;
+ display: flex;
+ flex-direction: column;
+ flex-wrap: nowrap;
+
+ background-color: #2C2C2C;
+ min-width: 320px;
+ min-height: 460px;
+}
+
+.work {
+ -webkit-flex: 1;
+ flex: 1;
+ position: relative;
+ overflow: hidden;
+}
+
+.footer {
+ border-top: 1px solid #919F9F;
+ display: -webkit-flex;
+ -webkit-justify-content: space-around;
+ display: flex;
+ justify-content: space-around;
+}
+
+.footer .button {
+ width: 100%;
+ text-align: center;
+ position: relative;
+ padding: 5px;
+}
+
+.touchrect {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ z-index: 10;
+}
+
+.icon {
+ font-family: "fontello";
+}
+
+.footer .button .icon {
+ font-size: 25px;
+ margin-top: 8px;
+}
+
+.footer .button .title {
+ font-size: 12px;
+ margin-top: 8px;
+ margin-bottom: 8px;
+}
+
+.footer .button:active,
+.footer .button.selected {
+ color: #FFFFFF;
+}
+
+.container {
+ position: absolute;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ width: 400%;
+ display: flex;
+ display: -webkit-flex;
+ -webkit-transition: left 0.5s ease-in-out;
+ transition: left 0.5s ease-in-out;
+}
+
+.contentarea {
+ width: 100%;
+ height: 100%;
+ overflow-y: auto;
+ -ms-overflow-y: auto;
+ -ms-overflow-style: -ms-autohiding-scrollbar;
+ position: relative;
+ -webkit-overflow-scrolling: touch;
+}
+
+#color {
+ display: -webkit-flex;
+ -webkit-flex-direction: column;
+ display: flex;
+ flex-direction: column;
+}
+
+#colorpicker {
+ position: relative;
+ -webkit-flex: 1;
+ flex: 1;
+}
+
+#colorwheelbg {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ margin: auto;
+}
+
+#pointer {
+ width: 30px;
+ height: 30px;
+ position: absolute;
+ border: 2px solid #FFFFFF;
+ border-radius: 15px;
+ left: calc(50% - 15px);
+ top: calc(50% - 15px);
+}
+
+ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+li {
+ display: block;
+}
+
+.horizontal {
+ display: -webkit-flex;
+ -webkit-flex: 1;
+ display: flex;
+ flex: 1;
+ position: relative;
+}
+
+li .delete_icon {
+ font-family: "fontello";
+ font-size: 20px;
+ color: red;
+ height: 40px;
+ width: 40px;
+ line-height: 40px;
+ text-align: center;
+}
+
+li .edit_icon {
+ font-family: "fontello";
+ font-size: 20px;
+ height: 40px;
+ width: 40px;
+ line-height: 40px;
+ text-align: center;
+}
+
+.locked .edit_icon,
+.locked .delete_icon {
+ display: none;
+}
+
+li .titlebox {
+ -webkit-flex: 1;
+ flex: 1;
+}
+
+.titlebox label {
+ display: block;
+ line-height: 20px;
+ height: 20px;
+}
+
+.titlebox label.title {
+ font-size: 16px;
+}
+
+.titlebox label.subtitle {
+ font-size: 10px;
+}
+
+/*
+li:not(:last-child) {
+ border-bottom: 1px solid #919F9F;
+}
+*/
+
+li {
+ border-bottom: 1px solid #919F9F;
+}
+
+li input[type=range] {
+ width: 100%;
+}
+
+#transform li {
+ padding: 0;
+}
+
+#transform .icon {
+ width: 40px;
+ text-align: center;
+}
+
+.group,
+.grouplist {
+ margin-top: 10px;
+}
+
+.grouplist .header {
+ display: flex;
+ justify-content: space-between;
+ border-bottom: 1px solid gray;
+ align-items: center;
+ padding-left: 15px;
+ font-size: 16px;
+ display: -webkit-flex;
+ -webkit-justify-content: space-between;
+ -webkit-align-items: center;
+}
+
+.group ul,
+.grouplist ul {
+ overflow-y: hidden;
+ -webkit-transition: all 0.5s ease-in-out;
+ transition: all 0.5s ease-in-out;
+}
+
+.group[collapsed=true] ul {
+ max-height: 0;
+}
+
+.group li label {
+ margin: 4px 0 0 50px;
+ display: block;
+ background: none;
+}
+
+.group > .header {
+ border-bottom: 1px solid gray;
+ padding: 3px 0 3px 10px;
+ position: relative;
+}
+
+.group > .header label {
+ display: block;
+ background: none;
+}
+
+.group > .header label:first-child {
+ font-size: 16px;
+}
+
+.group > .header label:last-child {
+ font-size: 12px;
+}
+
+.group > .header label:first-child:after {
+ font-family: "fontello";
+ content: '\e808';
+ font-size: 20px;
+ transition: all 0.5s ease-in-out;
+ -webkit-transform-origin: 50% 33%;
+ -webkit-transform: rotateZ(180deg);
+ transform-origin: 50% 33%;
+ transform: rotateZ(180deg);
+ -ms-transform-origin: 50% 33%;
+ -ms-transform: rotateZ(180deg);
+ position: absolute;
+ right: 24px;
+ top: 0;
+ bottom: 0;
+}
+
+.grouplist .header .callout {
+ font-family: "fontello";
+ font-size: 20px;
+ padding: 10px;
+}
+
+.grouplist .header .callout:active {
+ color: #FFFFFF;
+}
+
+.group[collapsed=true] > .header label:first-child:after {
+ -webkit-transform: rotateZ(0deg);
+ -ms-transform: rotateZ(0deg);
+ transform: rotateZ(0deg);
+}
+
+.wrapper {
+ display: flex;
+ align-items: center;
+ display: -webkit-flex;
+ -webkit-align-items: center;
+}
+
+.wrapper .value {
+ width: 40px;
+ background: transparent;
+ border: 0;
+ color: #FFFFFF;
+ margin: auto 5px;
+}
+
+#transform .group .slider {
+ -webkit-flex: 1;
+ flex: 1;
+ width: 100%;
+}
+
+.slider {
+ position: relative;
+ margin-left: 20px;
+ margin-right: 20px;
+}
+
+.slider .track {
+ border-radius: 4px;
+ height: 4px;
+ border: 1px solid #BDC3C7;
+ background-color: #FFFFFF;
+
+}
+
+.slider .thumb {
+ box-sizing: border-box;
+ border-radius: 10px;
+ height: 20px;
+ width: 20px;
+ border: 2px solid #BDC3C7;
+ background-color: #FFFFFF;
+ position: absolute;
+ top: -8px;
+ margin-left: -10px;
+}
+
+.wrapper .icon {
+ display: block;
+ font-size: 30px;
+}
+
+.red .icon {
+ color: #FF0000;
+}
+
+.green .icon {
+ color: #00FF00;
+}
+
+.blue .icon {
+ color: #0000FF;
+}
+
+.msg {
+ position: relative;
+ margin: auto;
+ padding: 5px 10px;
+ font-size: 12px;
+ font-weight: bold;
+}
+
+.error {
+ background-color: #D70000;
+ color: #FFFFFF;
+}
+
+.status {
+ background-color: #00A200;
+ color: #FFFFFF;
+}
+
+.wrapper_msg {
+ position: absolute;
+ bottom: 5px;
+ width: 100%;
+ display: -webkit-flex;
+ display: flex;
+ visibility: visible;
+ opacity: 1;
+ -webkit-transition: opacity 0.2s linear;
+ transition: opacity 0.2s linear;
+}
+
+.invisible {
+ opacity: 0;
+ visibility: hidden;
+}
+
+.hidden {
+ display: none !important;
+}
+
+.inputline {
+ display: -webkit-flex;
+ -webkit-justify-content: space-between;
+ -webkit-align-items: center;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-size: 14px;
+ padding: 6px 10px;
+}
+
+#settings .inputline input {
+ background: none;
+ border: 1px solid rgba(100, 100, 100, 0.4);
+ height: 100%;
+ color: white;
+ text-align: right;
+ font-size: inherit;
+ -webkit-user-select: text;
+ -moz-user-select: text;
+ -ms-user-select: text;
+ user-select: text;
+ outline: none;
+}
+
+#settings .inputline input:focus {
+ color: white;
+}
+
+.line {
+ width: 100%;
+ position: relative;
+ padding: 5px 0;
+}
+
+button {
+ margin: auto 10px;
+ border: 1px #A6B4B4 solid;
+ background: none;
+ color: #A6B4B4;
+ padding: 6px 20px;
+}
+
+button:active {
+ border: 1px #FFFFFF solid;
+ background-color: transparent;
+ color: #FFFFFF;
+ outline: none;
+}
+
+button:focus {
+ outline: none;
+}
+
+.spinner {
+ height: 20px;
+ width: 20px;
+ display: inline-flex;
+ display: -webkit-inline-flex;
+ -webkit-animation: rotation .8s infinite linear;
+ animation: rotation .8s infinite linear;
+ border: 6px inset #D7D7D7;
+ border-radius: 50%;
+ float: right;
+ margin-right: 15px;
+}
+
+@-webkit-keyframes rotation {
+ from {
+ -ms-transform: rotate(0deg);
+ }
+ to {
+ -ms-transform: rotate(359deg);
+ }
+}
+
+@-webkit-keyframes rotation {
+ from {
+ -webkit-transform: rotate(0deg);
+ }
+ to {
+ -webkit-transform: rotate(359deg);
+ }
+}
+
+@keyframes rotation {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(359deg);
+ }
+}
+
+li.selected {
+ background-color: rgba(100, 100, 100, 0.5);
+}
+
+#effects li {
+ height: 40px;
+ line-height: 40px;
+ padding-left: 20px;
+}
+
+.info {
+ font-size: 16px;
+ position: absolute;
+ top: 50%;
+ width: 100%;
+ text-align: center;
+}
+
+#color #buttonctrl {
+ display: -webkit-flex;
+ -webkit-justify-content: space-between;
+ -webkit-align-items: flex-end;
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-end;
+ margin: 10px auto 35px auto;
+}
+
+#color #buttonctrl > .icon {
+ font-size: 27px;
+ line-height: 100%;
+ padding: 0 20px;
+}
+
+#color #buttonctrl > .icon:active {
+ color: #FFFFFF;
+}
+
+#color .slider {
+ position: relative;
+ width: 70%;
+ margin: 10px auto;
+ height: 20px;
+}
+
+#color .slider .track {
+ width: 100%;
+ height: 100%;
+ background-image: -webkit-linear-gradient(left, #000000 0%, #FFFFFF 100%);
+ background-image: linear-gradient(to right, #000000 0%, #FFFFFF 100%);
+}
+
+#color .slider .thumb {
+ background: transparent;
+ border: 3px solid rgba(255, 255, 255, 1.0);
+ width: 14px;
+ border-radius: 4px;
+ height: 28px;
+ position: absolute;
+ top: -4px;
+ margin-left: -7px;
+}
+
+#color .value {
+ outline: none;
+ border: 1px solid white;
+ font-family: Monaco, monospace;
+ text-align: center;
+ background: transparent;
+ width: 100px;
+ color: white;
+ font-size: 16px;
+ display: block;
+ -webkit-user-select: text;
+ -moz-user-select: text;
+ -ms-user-select: text;
+ user-select: text;
+}
+
+.checkbox::before {
+ display: table-cell;
+ font-family: "fontello";
+ content: '\e803';
+ height: 40px;
+ width: 40px;
+ font-size: 20px;
+ vertical-align: middle;
+ text-align: center;
+}
+
+.selected .checkbox::before {
+ content: '\e802';
+}
\ No newline at end of file
diff --git a/assets/webconfig/index.html b/assets/webconfig/index.html
new file mode 100644
index 00000000..5f283710
--- /dev/null
+++ b/assets/webconfig/index.html
@@ -0,0 +1,69 @@
+
+
+
+ Hyperion remote control
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/assets/webconfig/js/app/api/ChromeLocalStorage.js b/assets/webconfig/js/app/api/ChromeLocalStorage.js
new file mode 100644
index 00000000..82467df8
--- /dev/null
+++ b/assets/webconfig/js/app/api/ChromeLocalStorage.js
@@ -0,0 +1,37 @@
+/*global define, chrome */
+define(['api/LocalStorage'], function (LocalStorage) {
+ 'use strict';
+ return LocalStorage.subclass(/** @lends ChromeLocalStorage.prototype */{
+
+ /**
+ * @class ChromeLocalStorage
+ * @classdesc Chrome's persistent storage
+ * @constructs
+ * @extends LocalStorage
+ */
+ constructor: function () {
+ },
+
+ get: function () {
+ chrome.storage.local.get('data', function (entry) {
+ if (chrome.runtime.lastError) {
+ this.emit('error', chrome.runtime.lastError.message);
+ } else {
+ this.emit('got', entry.data);
+ }
+ }.bind(this));
+ },
+
+ set: function (data) {
+ var entry = {};
+ entry.data = data;
+ chrome.storage.local.set(entry, function () {
+ if (chrome.runtime.lastError) {
+ this.emit('error', chrome.runtime.lastError.message);
+ } else {
+ this.emit('set');
+ }
+ }.bind(this));
+ }
+ });
+});
diff --git a/assets/webconfig/js/app/api/ChromeNetwork.js b/assets/webconfig/js/app/api/ChromeNetwork.js
new file mode 100644
index 00000000..99154112
--- /dev/null
+++ b/assets/webconfig/js/app/api/ChromeNetwork.js
@@ -0,0 +1,57 @@
+/*global chrome */
+define(['api/Network'], function (Network) {
+ 'use strict';
+
+ return Network.subclass(/** @lends ChromeNetwork.prototype */{
+
+ /**
+ * @class ChromeNetwork
+ * @extends Network
+ * @classdesc Network functions for chrome apps
+ * @constructs
+ */
+ constructor: function () {
+ },
+
+ /**
+ * @overrides
+ * @param onSuccess
+ * @param onError
+ */
+ getLocalInterfaces: function (onSuccess, onError) {
+ var ips = [];
+
+ chrome.system.network.getNetworkInterfaces(function (networkInterfaces) {
+ var i;
+
+ if (chrome.runtime.lastError) {
+ if (onError) {
+ onError('Could not get network interfaces');
+ }
+ return;
+ }
+
+ for (i = 0; i < networkInterfaces.length; i++) {
+ // check only ipv4
+ if (networkInterfaces[i].address.indexOf('.') === -1) {
+ continue;
+ }
+
+ ips.push(networkInterfaces[i].address);
+ }
+
+ if (onSuccess) {
+ onSuccess(ips);
+ }
+ });
+ },
+
+ /**
+ * @overrides
+ * @return {boolean}
+ */
+ canDetectLocalAddress: function () {
+ return true;
+ }
+ }, true);
+});
diff --git a/assets/webconfig/js/app/api/ChromeTcpSocket.js b/assets/webconfig/js/app/api/ChromeTcpSocket.js
new file mode 100644
index 00000000..f626b1d6
--- /dev/null
+++ b/assets/webconfig/js/app/api/ChromeTcpSocket.js
@@ -0,0 +1,307 @@
+/*global chrome */
+define(['lib/stapes', 'api/Socket', 'utils/Tools'], function (Stapes, Socket, tools) {
+ 'use strict';
+ return Socket.subclass(/** @lends ChromeTcpSocket.prototype */{
+ DEBUG: false,
+
+ /**
+ * @type {number}
+ */
+ handle: null,
+
+ /**
+ * @type {function}
+ */
+ currentResponseCallback: null,
+
+ /**
+ * @type {function}
+ */
+ currentErrorCallback: null,
+
+ /**
+ * Temporary buffer for incoming data
+ * @type {Uint8Array}
+ */
+ inputBuffer: null,
+ inputBufferIndex: 0,
+ readBufferTimerId: null,
+
+ /**
+ * @class ChromeTcpSocket
+ * @extends Socket
+ * @constructs
+ */
+ constructor: function () {
+ this.inputBuffer = new Uint8Array(4096);
+ this.inputBufferIndex = 0;
+
+ chrome.sockets.tcp.onReceive.addListener(this.onDataReceived.bind(this));
+ chrome.sockets.tcp.onReceiveError.addListener(this.onError.bind(this));
+ },
+
+ create: function (onSuccess, onError) {
+ if (this.DEBUG) {
+ console.log('[DEBUG] Creating socket...');
+ }
+ chrome.sockets.tcp.create({bufferSize: 4096}, function (createInfo) {
+ if (this.DEBUG) {
+ console.log('[DEBUG] Socket created: ' + createInfo.socketId);
+ }
+ this.handle = createInfo.socketId;
+ if (onSuccess) {
+ onSuccess();
+ }
+ }.bind(this));
+ },
+
+ isConnected: function (resultCallback) {
+ if (this.DEBUG) {
+ console.log('[DEBUG] Checking if socket is connected...');
+ }
+
+ if (!this.handle) {
+ if (this.DEBUG) {
+ console.log('[DEBUG] Socket not created');
+ }
+
+ if (resultCallback) {
+ resultCallback(false);
+ }
+ return;
+ }
+
+ chrome.sockets.tcp.getInfo(this.handle, function (socketInfo) {
+ if (this.DEBUG) {
+ console.log('[DEBUG] Socket connected: ' + socketInfo.connected);
+ }
+
+ if (socketInfo.connected) {
+ if (resultCallback) {
+ resultCallback(true);
+ }
+ } else {
+ if (resultCallback) {
+ resultCallback(false);
+ }
+ }
+ }.bind(this));
+ },
+
+ connect: function (server, onSuccess, onError) {
+ var timeoutHandle;
+
+ if (this.DEBUG) {
+ console.log('[DEBUG] Connecting to peer ' + server.address + ':' + server.port);
+ }
+
+ if (!this.handle) {
+ if (this.DEBUG) {
+ console.log('[DEBUG] Socket not created');
+ }
+
+ if (onError) {
+ onError('Socket handle is invalid');
+ }
+ return;
+ }
+
+ // FIXME for some reason chrome blocks if peer is not reachable
+ timeoutHandle = setTimeout(function () {
+ chrome.sockets.tcp.getInfo(this.handle, function (socketInfo) {
+ if (!socketInfo.connected) {
+ // let the consumer decide if to close or not?
+ // this.close();
+ onError('Could not connect to ' + server.address + ':' + server.port);
+ }
+ }.bind(this));
+ }.bind(this), 500);
+
+ chrome.sockets.tcp.connect(this.handle, server.address, server.port, function (result) {
+ if (this.DEBUG) {
+ console.log('[DEBUG] Connect result: ' + result);
+ }
+ clearTimeout(timeoutHandle);
+
+ if (chrome.runtime.lastError) {
+ if (onError) {
+ onError('Could not connect to ' + server.address + ':' + server.port);
+ }
+ return;
+ }
+
+ if (result !== 0) {
+ if (onError) {
+ onError('Could not connect to ' + server.address + ':' + server.port);
+ }
+ } else if (onSuccess) {
+ onSuccess();
+ }
+ }.bind(this));
+ },
+
+ close: function (onSuccess, onError) {
+ if (this.DEBUG) {
+ console.log('[DEBUG] Closing socket...');
+ }
+
+ if (this.handle) {
+ chrome.sockets.tcp.close(this.handle, function () {
+ this.handle = null;
+ if (onSuccess) {
+ onSuccess();
+ }
+ }.bind(this));
+ } else {
+ if (this.DEBUG) {
+ console.log('[DEBUG] Socket not created');
+ }
+
+ if (onError) {
+ onError('Socket handle is invalid');
+ }
+ }
+ },
+
+ write: function (data, onSuccess, onError) {
+ var dataToSend = null, dataType = typeof (data);
+
+ if (this.DEBUG) {
+ console.log('[DEBUG] writing to socket...');
+ }
+
+ if (!this.handle) {
+ if (this.DEBUG) {
+ console.log('[DEBUG] Socket not created');
+ }
+
+ if (onError) {
+ onError('Socket handle is invalid');
+ }
+ return;
+ }
+
+ this.isConnected(function (connected) {
+ if (connected) {
+ if (dataType === 'string') {
+ if (this.DEBUG) {
+ console.log('> ' + data);
+ }
+ dataToSend = tools.str2ab(data);
+ } else {
+ if (this.DEBUG) {
+ console.log('> ' + tools.ab2hexstr(data));
+ }
+ dataToSend = data;
+ }
+
+ chrome.sockets.tcp.send(this.handle, tools.a2ab(dataToSend), function (sendInfo) {
+ if (this.DEBUG) {
+ console.log('[DEBUG] Socket write result: ' + sendInfo.resultCode);
+ }
+
+ if (sendInfo.resultCode !== 0) {
+ onError('Socket write error: ' + sendInfo.resultCode);
+ } else if (onSuccess) {
+ onSuccess();
+ }
+ }.bind(this));
+ } else {
+ if (onError) {
+ onError('No connection to peer');
+ }
+ }
+ }.bind(this));
+
+ },
+
+ read: function (onSuccess, onError) {
+ if (this.DEBUG) {
+ console.log('[DEBUG] reading from socket...');
+ }
+
+ if (!this.handle) {
+ if (this.DEBUG) {
+ console.log('[DEBUG] socket not created');
+ }
+
+ if (onError) {
+ onError('Socket handle is invalid');
+ }
+ return;
+ }
+
+ this.isConnected(function (connected) {
+ if (!connected) {
+ this.currentResponseCallback = null;
+ this.currentErrorCallback = null;
+
+ if (onError) {
+ onError('No connection to peer');
+ }
+ }
+ }.bind(this));
+
+ if (onSuccess) {
+ this.currentResponseCallback = onSuccess;
+ }
+
+ if (onError) {
+ this.currentErrorCallback = onError;
+ }
+ },
+
+ /**
+ * Data receiption callback
+ * @private
+ * @param info
+ */
+ onDataReceived: function (info) {
+ if (this.DEBUG) {
+ console.log('[DEBUG] received data...');
+ }
+
+ if (info.socketId === this.handle && info.data) {
+ if (this.readBufferTimerId) {
+ clearTimeout(this.readBufferTimerId);
+ }
+ if (this.readTimeoutTimerId) {
+ clearTimeout(this.readTimeoutTimerId);
+ this.readTimeoutTimerId = null;
+ }
+ this.inputBuffer.set(new Uint8Array(info.data), this.inputBufferIndex);
+ this.inputBufferIndex += info.data.byteLength;
+
+ if (this.DEBUG) {
+ console.log('< ' + tools.ab2hexstr(info.data));
+ }
+
+ if (this.currentResponseCallback) {
+ this.readBufferTimerId = setTimeout(function () {
+ this.currentResponseCallback(this.inputBuffer.subarray(0, this.inputBufferIndex));
+ this.inputBufferIndex = 0;
+ this.currentResponseCallback = null;
+ }.bind(this), 200);
+ }
+ }
+ },
+
+ /**
+ * Error callback
+ * @private
+ * @param info
+ */
+ onError: function (info) {
+ if (this.DEBUG) {
+ console.log('[ERROR]: ' + info.resultCode);
+ }
+
+ if (info.socketId === this.handle) {
+ if (this.currentErrorCallback) {
+ this.currentErrorCallback(info.resultCode);
+ this.currentErrorCallback = null;
+ }
+ }
+ }
+ }, true);
+});
diff --git a/assets/webconfig/js/app/api/LocalStorage.js b/assets/webconfig/js/app/api/LocalStorage.js
new file mode 100644
index 00000000..1b74543e
--- /dev/null
+++ b/assets/webconfig/js/app/api/LocalStorage.js
@@ -0,0 +1,49 @@
+/*global define */
+define(['lib/stapes'], function (Stapes) {
+ 'use strict';
+ return Stapes.subclass(/** @lends LocalStorage.prototype */{
+
+ /**
+ * @class LocalStorage
+ * @classdesc LocalStorage handler using HTML5 localStorage
+ * @constructs
+ *
+ * @fires got
+ * @fires error
+ * @fires set
+ */
+ constructor: function () {
+ },
+
+ /**
+ * Gets stored data
+ */
+ get: function () {
+ var data;
+
+ if (!window.localStorage) {
+ this.emit('error', 'Local Storage not supported');
+ return;
+ }
+
+ if (localStorage.data) {
+ data = JSON.parse(localStorage.data);
+ this.emit('got', data);
+ }
+ },
+
+ /**
+ * Stores settings
+ * @param {object} data - Data object to store
+ */
+ set: function (data) {
+ if (!window.localStorage) {
+ this.emit('error', 'Local Storage not supported');
+ return;
+ }
+
+ localStorage.data = JSON.stringify(data);
+ this.emit('set');
+ }
+ });
+});
diff --git a/assets/webconfig/js/app/api/Network.js b/assets/webconfig/js/app/api/Network.js
new file mode 100644
index 00000000..2d93ca65
--- /dev/null
+++ b/assets/webconfig/js/app/api/Network.js
@@ -0,0 +1,57 @@
+/*global define */
+define(['lib/stapes'], function (Stapes) {
+ 'use strict';
+ return Stapes.subclass(/** @lends Network.prototype */{
+ detectTimerId: null,
+
+ /**
+ * @class Network
+ * @classdesc Empty network functions handler
+ * @constructs
+ */
+ constructor: function () {
+ },
+
+ /**
+ * Returns the list of known local interfaces (ipv4)
+ * @param {function(string[])} [onSuccess] - Callback to call on success
+ * @param {function(error:string)} [onError] - Callback to call on error
+ */
+ getLocalInterfaces: function (onSuccess, onError) {
+ var ips = [], RTCPeerConnection;
+
+ // https://developer.mozilla.org/de/docs/Web/API/RTCPeerConnection
+ RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection || window.msRTCPeerConnection;
+
+ var rtc = new RTCPeerConnection({iceServers: []});
+ rtc.onicecandidate = function (event) {
+ var parts;
+
+ if (this.detectTimerId) {
+ clearTimeout(this.detectTimerId);
+ }
+
+ if (event.candidate) {
+ parts = event.candidate.candidate.split(' ');
+ if (ips.indexOf(parts[4]) === -1) {
+ console.log(event.candidate);
+ ips.push(parts[4]);
+ }
+ }
+
+ this.detectTimerId = setTimeout(function () {
+ if (onSuccess) {
+ onSuccess(ips);
+ }
+ }, 200);
+ }.bind(this);
+
+ rtc.createDataChannel('');
+ rtc.createOffer(rtc.setLocalDescription.bind(rtc), onError);
+ },
+
+ canDetectLocalAddress: function () {
+ return window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection || window.msRTCPeerConnection;
+ }
+ }, true);
+});
diff --git a/assets/webconfig/js/app/api/Socket.js b/assets/webconfig/js/app/api/Socket.js
new file mode 100644
index 00000000..1d6e5871
--- /dev/null
+++ b/assets/webconfig/js/app/api/Socket.js
@@ -0,0 +1,68 @@
+/*global define */
+define(['lib/stapes'], function (Stapes) {
+ 'use strict';
+ return Stapes.subclass(/** @lends Socket.prototype */{
+
+ /**
+ * @class Socket
+ * @abstract
+ */
+ constructor: function () {
+ },
+
+ /**
+ * Create the socket
+ * @param onSuccess
+ * @param onError
+ * @abstract
+ */
+ create: function (onSuccess, onError) {
+ },
+
+ /**
+ * Check if a connection is opened.
+ * @abstract
+ */
+ isConnected: function () {
+ return false;
+ },
+
+ /**
+ * Connect to another peer
+ * @param {Object} peer Port object
+ * @param {function} [onSuccess] Callback to call on success
+ * @param {function(error:string)} [onError] Callback to call on error
+ * @abstract
+ */
+ connect: function (peer, onSuccess, onError) {
+ },
+
+ /**
+ * Close the current connection
+ * @param {function} [onSuccess] Callback to call on success
+ * @param {function(error:string)} [onError] Callback to call on error
+ * @abstract
+ */
+ close: function (onSuccess, onError) {
+ },
+
+ /**
+ * Read data from the socket
+ * @param {function} [onSuccess] Callback to call on success
+ * @param {function(error:string)} [onError] Callback to call on error
+ * @abstract
+ */
+ read: function (onSuccess, onError) {
+ },
+
+ /**
+ * Writes data to the socket
+ * @param {string | Array} data Data to send.
+ * @param {function} [onSuccess] Callback to call if data was sent successfully
+ * @param {function(error:string)} [onError] Callback to call on error
+ * @abstract
+ */
+ write: function (data, onSuccess, onError) {
+ }
+ }, true);
+});
diff --git a/assets/webconfig/js/app/api/WebSocket.js b/assets/webconfig/js/app/api/WebSocket.js
new file mode 100644
index 00000000..a231f07e
--- /dev/null
+++ b/assets/webconfig/js/app/api/WebSocket.js
@@ -0,0 +1,229 @@
+define(['lib/stapes', 'api/Socket', 'utils/Tools'], function (Stapes, Socket, tools) {
+ 'use strict';
+ return Socket.subclass(/** @lends WebSocket.prototype */{
+ DEBUG: false,
+
+ handle: null,
+
+ /**
+ * @type {function}
+ */
+ currentResponseCallback: null,
+
+ /**
+ * @type {function}
+ */
+ currentErrorCallback: null,
+
+ /**
+ * Temporary buffer for incoming data
+ * @type {Uint8Array}
+ */
+ inputBuffer: null,
+ inputBufferIndex: 0,
+ readBufferTimerId: null,
+
+ /**
+ * @class WebSocket
+ * @extends Socket
+ * @constructs
+ */
+ constructor: function () {
+ this.inputBuffer = new Uint8Array(4096);
+ this.inputBufferIndex = 0;
+ },
+
+ create: function (onSuccess, onError) {
+ if (this.DEBUG) {
+ console.log('[DEBUG] Creating socket...');
+ }
+
+ if (onSuccess) {
+ onSuccess();
+ }
+ },
+
+ isConnected: function (resultCallback) {
+ if (this.DEBUG) {
+ console.log('[DEBUG] Checking if socket is connected...');
+ }
+
+ if (!this.handle) {
+ if (this.DEBUG) {
+ console.log('[DEBUG] Socket not created');
+ }
+
+ if (resultCallback) {
+ resultCallback(false);
+ }
+ return;
+ }
+
+ if (resultCallback) {
+ if (this.handle.readyState === WebSocket.OPEN) {
+ resultCallback(true);
+ } else {
+ resultCallback(false);
+ }
+ }
+ },
+
+ connect: function (server, onSuccess, onError) {
+ if (this.DEBUG) {
+ console.log('[DEBUG] Connecting to peer ' + server.address + ':' + server.port);
+ }
+
+ this.currentErrorCallback = onError;
+
+ this.handle = new WebSocket('ws://' + server.address + ':' + server.port);
+ this.handle.onmessage = this.onDataReceived.bind(this);
+ this.handle.onclose = function () {
+ if (this.DEBUG) {
+ console.log('onClose');
+ }
+ }.bind(this);
+ this.handle.onerror = function () {
+ if (this.DEBUG) {
+ console.log('[ERROR]: ');
+ }
+
+ if (this.currentErrorCallback) {
+ this.currentErrorCallback('WebSocket error');
+ this.currentErrorCallback = null;
+ }
+ }.bind(this);
+ this.handle.onopen = function () {
+ if (onSuccess) {
+ onSuccess();
+ }
+ };
+ },
+
+ close: function (onSuccess, onError) {
+ if (this.DEBUG) {
+ console.log('[DEBUG] Closing socket...');
+ }
+
+ if (this.handle) {
+ this.handle.close();
+ if (onSuccess) {
+ onSuccess();
+ }
+ } else {
+ if (this.DEBUG) {
+ console.log('[DEBUG] Socket not created');
+ }
+
+ if (onError) {
+ onError('Socket handle is invalid');
+ }
+ }
+ },
+
+ write: function (data, onSuccess, onError) {
+ var dataToSend = null, dataType = typeof (data);
+
+ if (this.DEBUG) {
+ console.log('[DEBUG] writing to socket...');
+ }
+
+ if (!this.handle) {
+ if (this.DEBUG) {
+ console.log('[DEBUG] Socket not created');
+ }
+
+ if (onError) {
+ onError('Socket handle is invalid');
+ }
+ return;
+ }
+
+ this.isConnected(function (connected) {
+ if (connected) {
+ if (dataType === 'string') {
+ if (this.DEBUG) {
+ console.log('> ' + data);
+ }
+ //dataToSend = tools.str2ab(data);
+ dataToSend = data;
+ } else {
+ if (this.DEBUG) {
+ console.log('> ' + tools.ab2hexstr(data));
+ }
+ dataToSend = data;
+ }
+
+ this.currentErrorCallback = onError;
+ this.handle.send(dataToSend);
+
+ if (onSuccess) {
+ onSuccess();
+ }
+ } else {
+ if (onError) {
+ onError('No connection to peer');
+ }
+ }
+ }.bind(this));
+
+ },
+
+ read: function (onSuccess, onError) {
+ if (this.DEBUG) {
+ console.log('[DEBUG] reading from socket...');
+ }
+
+ if (!this.handle) {
+ if (this.DEBUG) {
+ console.log('[DEBUG] socket not created');
+ }
+
+ if (onError) {
+ onError('Socket handle is invalid');
+ }
+ return;
+ }
+
+ this.isConnected(function (connected) {
+ if (!connected) {
+ this.currentResponseCallback = null;
+ this.currentErrorCallback = null;
+
+ if (onError) {
+ onError('No connection to peer');
+ }
+ }
+ }.bind(this));
+
+ if (onSuccess) {
+ this.currentResponseCallback = onSuccess;
+ }
+
+ if (onError) {
+ this.currentErrorCallback = onError;
+ }
+ },
+
+ /**
+ * Data receiption callback
+ * @private
+ * @param event
+ */
+ onDataReceived: function (event) {
+ if (this.DEBUG) {
+ console.log('[DEBUG] received data...');
+ }
+
+ if (this.handle && event.data) {
+ if (this.DEBUG) {
+ console.log('< ' + event.data);
+ }
+
+ if (this.currentResponseCallback) {
+ this.currentResponseCallback(tools.str2ab(event.data));
+ this.currentResponseCallback = null;
+ }
+ }
+ }
+ }, true);
+});
diff --git a/assets/webconfig/js/app/controllers/AppController.js b/assets/webconfig/js/app/controllers/AppController.js
new file mode 100644
index 00000000..c50e4e6d
--- /dev/null
+++ b/assets/webconfig/js/app/controllers/AppController.js
@@ -0,0 +1,534 @@
+/*global define */
+define([
+ 'lib/stapes', 'views/MainView', 'models/Settings', 'views/SettingsView', 'views/EffectsView', 'views/TransformView', 'data/ServerControl', 'api/Socket', 'api/Network'
+], function (Stapes, MainView, Settings, SettingsView, EffectsView, TransformView, ServerControl, Socket, Network) {
+ 'use strict';
+ var network = new Network();
+
+ return Stapes.subclass(/** @lends AppController.prototype */{
+ /**
+ * @type MainView
+ */
+ mainView: null,
+ /**
+ * @type SettingsView
+ */
+ settingsView: null,
+ /**
+ * @type EffectsView
+ */
+ effectsView: null,
+ /**
+ * @type TransformView
+ */
+ transformView: null,
+ /**
+ * @type Settings
+ */
+ settings: null,
+ /**
+ * @type ServerControl
+ */
+ serverControl: null,
+ color: {
+ r: 25,
+ g: 25,
+ b: 25
+ },
+ effects: [],
+ transform: {},
+ selectedServer: null,
+
+ /**
+ * @class AppController
+ * @constructs
+ */
+ constructor: function () {
+ this.mainView = new MainView();
+ this.settingsView = new SettingsView();
+ this.effectsView = new EffectsView();
+ this.transformView = new TransformView();
+
+ this.settings = new Settings();
+
+ this.bindEventHandlers();
+ this.mainView.setColor(this.color);
+
+ if (!network.canDetectLocalAddress()) {
+ this.settingsView.enableDetectButton(false);
+ }
+ },
+
+ /**
+ * Do initialization
+ */
+ init: function () {
+ this.settings.load();
+ },
+
+ /**
+ * @private
+ */
+ bindEventHandlers: function () {
+ this.settings.on({
+ 'loaded': function () {
+ var i;
+
+ for (i = 0; i < this.settings.servers.length; i++) {
+ if (this.settings.servers[i].selected) {
+ this.selectedServer = this.settings.servers[i];
+ break;
+ }
+ }
+
+ this.settingsView.fillServerList(this.settings.servers);
+
+ if (!this.selectedServer) {
+ this.gotoArea('settings');
+ } else {
+ this.connectToServer(this.selectedServer);
+ }
+ },
+ 'error': function (message) {
+ this.showError(message);
+ },
+ 'serverAdded': function (server) {
+ var i;
+ for (i = 0; i < this.settings.servers.length; i++) {
+ if (this.settings.servers[i].selected) {
+ this.selectedServer = this.settings.servers[i];
+ this.connectToServer(server);
+ break;
+ }
+ }
+
+ this.settingsView.fillServerList(this.settings.servers);
+ },
+ 'serverChanged': function (server) {
+ var i;
+ for (i = 0; i < this.settings.servers.length; i++) {
+ if (this.settings.servers[i].selected) {
+ this.selectedServer = this.settings.servers[i];
+ this.connectToServer(server);
+ break;
+ }
+ }
+
+ this.settingsView.fillServerList(this.settings.servers);
+ this.connectToServer(server);
+ },
+ 'serverRemoved': function () {
+ var i, removedSelected = true;
+ this.settingsView.fillServerList(this.settings.servers);
+
+ for (i = 0; i < this.settings.servers.length; i++) {
+ if (this.settings.servers[i].selected) {
+ removedSelected = false;
+ break;
+ }
+ }
+
+ if (removedSelected) {
+ this.selectedServer = null;
+ if (this.serverControl) {
+ this.serverControl.disconnect();
+ }
+ this.effectsView.clear();
+ this.transformView.clear();
+ }
+ }
+ }, this);
+
+ this.mainView.on({
+ 'barClick': function (id) {
+ if (id !== 'settings') {
+ if (!this.selectedServer) {
+ this.showError('No server selected');
+ } else if (!this.serverControl) {
+ this.connectToServer(this.selectedServer);
+ }
+ }
+ this.gotoArea(id);
+ },
+ 'colorChange': function (color) {
+ this.color = color;
+
+ if (!this.selectedServer) {
+ this.showError('No server selected');
+ } else if (!this.serverControl) {
+ this.connectToServer(this.selectedServer, function () {
+ this.serverControl.setColor(color, this.selectedServer.duration);
+ }.bind(this));
+ } else {
+ this.serverControl.setColor(color, this.selectedServer.duration);
+ }
+ },
+ 'clear': function () {
+ if (!this.selectedServer) {
+ this.showError('No server selected');
+ } else if (!this.serverControl) {
+ this.connectToServer(this.selectedServer, function () {
+ this.serverControl.clear();
+ }.bind(this));
+ } else {
+ this.serverControl.clear();
+ }
+ },
+ 'clearall': function () {
+ if (!this.selectedServer) {
+ this.showError('No server selected');
+ } else if (!this.serverControl) {
+ this.connectToServer(this.selectedServer, function () {
+ this.serverControl.clearall();
+ this.mainView.setColor({r: 0, g: 0, b: 0});
+ }.bind(this));
+ } else {
+ this.serverControl.clearall();
+ this.mainView.setColor({r: 0, g: 0, b: 0});
+ }
+ }
+ }, this);
+
+ this.settingsView.on({
+ 'serverAdded': function (server) {
+ if (server.address && server.port) {
+ server.priority = server.priority || 50;
+ this.settings.addServer(server);
+ this.lockSettingsView(false);
+ } else {
+ this.showError('Invalid server data');
+ }
+ },
+ 'serverAddCanceled': function () {
+ this.lockSettingsView(false);
+ this.settingsView.fillServerList(this.settings.servers);
+ },
+ 'serverEditCanceled': function () {
+ this.lockSettingsView(false);
+ this.settingsView.fillServerList(this.settings.servers);
+ },
+ 'serverSelected': function (index) {
+ this.lockSettingsView(false);
+ this.settings.setSelectedServer(index);
+ },
+ 'serverRemoved': function (index) {
+ this.settings.removeServer(index);
+ },
+ 'serverChanged': function (data) {
+ if (data.server.address && data.server.port) {
+ data.server.priority = data.server.priority || 50;
+ this.settings.updateServer(data.index, data.server);
+ this.lockSettingsView(false);
+ } else {
+ this.showError('Invalid server data');
+ }
+ },
+ 'editServer': function (index) {
+ var server = this.settings.servers[index];
+ this.settingsView.editServer({index: index, server: server});
+ },
+ 'durationChanged': function (value) {
+ this.settings.duration = value;
+ this.settings.save();
+ },
+ 'detect': function () {
+ this.lockSettingsView(true);
+ this.settingsView.showWaiting(true);
+ this.searchForServer(function (server) {
+ this.settings.addServer(server);
+ }.bind(this), function () {
+ this.lockSettingsView(false);
+ this.settingsView.showWaiting(false);
+ }.bind(this));
+ }
+ }, this);
+
+ this.effectsView.on({
+ 'effectSelected': function (effectId) {
+ if (!this.serverControl) {
+ this.connectToServer(this.selectedServer, function () {
+ this.serverControl.runEffect(this.effects[parseInt(effectId)]);
+ }.bind(this));
+ } else {
+ this.serverControl.runEffect(this.effects[parseInt(effectId)]);
+ }
+ }
+ }, this);
+
+ this.transformView.on({
+ 'gamma': function (data) {
+ if (data.r) {
+ this.transform.gamma[0] = data.r;
+ } else if (data.g) {
+ this.transform.gamma[1] = data.g;
+ } else if (data.b) {
+ this.transform.gamma[2] = data.b;
+ }
+
+ if (this.serverControl) {
+ this.serverControl.setTransform(this.transform);
+ }
+ },
+ 'whitelevel': function (data) {
+ if (data.r) {
+ this.transform.whitelevel[0] = data.r;
+ } else if (data.g) {
+ this.transform.whitelevel[1] = data.g;
+ } else if (data.b) {
+ this.transform.whitelevel[2] = data.b;
+ }
+
+ if (this.serverControl) {
+ this.serverControl.setTransform(this.transform);
+ }
+ },
+ 'blacklevel': function (data) {
+ if (data.r) {
+ this.transform.blacklevel[0] = data.r;
+ } else if (data.g) {
+ this.transform.blacklevel[1] = data.g;
+ } else if (data.b) {
+ this.transform.blacklevel[2] = data.b;
+ }
+
+ if (this.serverControl) {
+ this.serverControl.setTransform(this.transform);
+ }
+ },
+ 'threshold': function (data) {
+ if (data.r) {
+ this.transform.threshold[0] = data.r;
+ } else if (data.g) {
+ this.transform.threshold[1] = data.g;
+ } else if (data.b) {
+ this.transform.threshold[2] = data.b;
+ }
+
+ if (this.serverControl) {
+ this.serverControl.setTransform(this.transform);
+ }
+ },
+ 'hsv': function (data) {
+ if (data.valueGain) {
+ this.transform.valueGain = data.valueGain;
+ } else if (data.saturationGain) {
+ this.transform.saturationGain = data.saturationGain;
+ }
+
+ if (this.serverControl) {
+ this.serverControl.setTransform(this.transform);
+ }
+ }
+ }, this);
+ },
+
+ /**
+ * @private
+ * @param id
+ */
+ gotoArea: function (id) {
+ this.mainView.scrollToArea(id);
+ },
+
+ /**
+ * @private
+ * @param server
+ */
+ connectToServer: function (server, onConnected) {
+ if (this.serverControl) {
+ if (this.serverControl.isConnecting()) {
+ return;
+ }
+ this.serverControl.off();
+ this.serverControl.disconnect();
+ this.transformView.clear();
+ this.effectsView.clear();
+ }
+
+ this.serverControl = new ServerControl(server, Socket);
+ this.serverControl.on({
+ connected: function () {
+ this.serverControl.getServerInfo();
+ },
+ serverInfo: function (info) {
+ var index;
+ if (!this.selectedServer.name || this.selectedServer.name.length === 0) {
+ this.selectedServer.name = info.hostname;
+ index = this.settings.indexOfServer(this.selectedServer);
+ this.settings.updateServer(index, this.selectedServer);
+ this.settingsView.fillServerList(this.settings.servers);
+ }
+ this.effects = info.effects;
+ this.transform = info.transform[0];
+ this.updateView();
+ this.showStatus('Connected to ' + this.selectedServer.name);
+ if (onConnected) {
+ onConnected();
+ }
+ },
+ error: function (message) {
+ this.serverControl = null;
+ this.showError(message);
+ }
+ }, this);
+ this.serverControl.connect();
+ },
+
+ /**
+ * @private
+ */
+ updateView: function () {
+ var i, effects = [];
+ if (this.effects) {
+ for (i = 0; i < this.effects.length; i++) {
+ effects.push({id: i, name: this.effects[i].name});
+ }
+ }
+
+ this.effectsView.clear();
+ this.effectsView.fillList(effects);
+
+ this.transformView.clear();
+ this.transformView.fillList(this.transform);
+ },
+
+ /**
+ * Shows the error text
+ * @param {string} error - Error message
+ */
+ showError: function (error) {
+ this.mainView.showError(error);
+ },
+
+ /**
+ * Shows a message
+ * @param {string} message - Text to show
+ */
+ showStatus: function (message) {
+ this.mainView.showStatus(message);
+ },
+
+ /**
+ * @private
+ * @param lock
+ */
+ lockSettingsView: function (lock) {
+ if (network.canDetectLocalAddress()) {
+ this.settingsView.enableDetectButton(!lock);
+ }
+ this.settingsView.lockList(lock);
+ },
+
+ /**
+ * @private
+ * @param onFound
+ * @param onEnd
+ */
+ searchForServer: function (onFound, onEnd) {
+ network.getLocalInterfaces(function (ips) {
+ if (ips.length === 0) {
+ onEnd();
+ return;
+ }
+
+ function checkInterface (localInterfaceAddress, ciOnFinished) {
+ var index, ipParts, addr;
+
+ index = 1;
+ ipParts = localInterfaceAddress.split('.');
+ ipParts[3] = index;
+ addr = ipParts.join('.');
+
+ function checkAddressRange (startAddress, count, carOnFinished) {
+ var ipParts, i, addr, cbCounter = 0, last;
+
+ function checkAddress (address, port, caOnFinished) {
+ var server = new ServerControl({'address': address, 'port': port}, Socket);
+ server.on({
+ 'error': function () {
+ server.disconnect();
+ caOnFinished();
+ },
+ 'connected': function () {
+ server.getServerInfo();
+ },
+ 'serverInfo': function (result) {
+ var serverInfo = {
+ 'address': address,
+ 'port': port,
+ 'priority': 50
+ };
+ server.disconnect();
+
+ if (result.hostname) {
+ serverInfo.name = result.hostname;
+ }
+
+ caOnFinished(serverInfo);
+ }
+ });
+ server.connect();
+ }
+
+ function checkAddressDoneCb (serverInfo) {
+ var ipParts, nextAddr;
+
+ if (serverInfo && onFound) {
+ onFound(serverInfo);
+ }
+
+ cbCounter++;
+ if (cbCounter === count) {
+ ipParts = startAddress.split('.');
+ ipParts[3] = parseInt(ipParts[3]) + count;
+ nextAddr = ipParts.join('.');
+ carOnFinished(nextAddr);
+ }
+ }
+
+ ipParts = startAddress.split('.');
+ last = parseInt(ipParts[3]);
+
+ for (i = 0; i < count; i++) {
+ ipParts[3] = last + i;
+ addr = ipParts.join('.');
+
+ checkAddress(addr, 19444, checkAddressDoneCb);
+ }
+ }
+
+ function checkAddressRangeCb (nextAddr) {
+ var ipParts, count = 64, lastPart;
+
+ ipParts = nextAddr.split('.');
+ lastPart = parseInt(ipParts[3]);
+ if (lastPart === 255) {
+ ciOnFinished();
+ return;
+ } else if (lastPart + 64 > 254) {
+ count = 255 - lastPart;
+ }
+
+ checkAddressRange(nextAddr, count, checkAddressRangeCb);
+ }
+
+ // do search in chunks because the dispatcher used in the ios socket plugin can handle only 64 threads
+ checkAddressRange(addr, 64, checkAddressRangeCb);
+ }
+
+ function checkInterfaceCb () {
+ if (ips.length === 0) {
+ onEnd();
+ } else {
+ checkInterface(ips.pop(), checkInterfaceCb);
+ }
+ }
+
+ checkInterface(ips.pop(), checkInterfaceCb);
+
+ }.bind(this), function (error) {
+ this.showError(error);
+ }.bind(this));
+ }
+ });
+});
diff --git a/assets/webconfig/js/app/data/ServerControl.js b/assets/webconfig/js/app/data/ServerControl.js
new file mode 100644
index 00000000..6eb9f91a
--- /dev/null
+++ b/assets/webconfig/js/app/data/ServerControl.js
@@ -0,0 +1,201 @@
+/*global define */
+define(['lib/stapes', 'utils/Tools'], function (Stapes, tools) {
+ 'use strict';
+
+ return Stapes.subclass(/** @lends ServerControl.prototype */{
+ /** @type Socket */
+ socket: null,
+ server: null,
+ connecting: false,
+
+ /**
+ * @class ServerControl
+ * @classdesc Interface for the hyperion server control. All commands are sent directly to hyperion's server.
+ * @constructs
+ * @param {object} server - Hyperion server parameter
+ * @param {string} server.address - Server address
+ * @param {number} server.port - Hyperion server port
+ * @param {function} Socket - constructor of the socket to use for communication
+ *
+ * @fires connected
+ * @fires error
+ * @fires serverInfo
+ * @fires cmdSent
+ */
+ constructor: function (server, Socket) {
+ this.server = server;
+ this.socket = new Socket();
+ this.connecting = false;
+ },
+
+ /**
+ * Try to connect to the server
+ */
+ connect: function () {
+ if (!this.server) {
+ this.emit('error', 'Missing server info');
+ } else {
+ this.connecting = true;
+ this.socket.create(function () {
+ this.socket.connect(this.server, function () {
+ this.emit('connected');
+ this.connecting = false;
+ }.bind(this), function (error) {
+ this.socket.close();
+ this.emit('error', error);
+ this.connecting = false;
+ }.bind(this));
+ }.bind(this));
+ }
+ },
+
+ /**
+ * Disconnect from the server
+ */
+ disconnect: function () {
+ this.socket.close();
+ },
+
+ /**
+ * Sends the color command to the server
+ * @param {object} color - Color to set
+ * @param {number} color.r - Red value
+ * @param {number} color.g - Green value
+ * @param {number} color.b - Blue value
+ * @param {number} duration - Duration in seconds
+ */
+ setColor: function (color, duration) {
+ var intColor, cmd;
+
+ intColor = [
+ Math.floor(color.r), Math.floor(color.g), Math.floor(color.b)
+ ];
+ cmd = {
+ command: 'color',
+ color: intColor,
+ priority: this.server.priority
+ };
+
+ if (duration) {
+ cmd.duration = duration * 1000;
+ }
+
+ this.sendCommand(cmd);
+ },
+
+ clear: function () {
+ var cmd = {
+ command: 'clear',
+ priority: this.server.priority
+ };
+ this.sendCommand(cmd);
+ },
+
+ clearall: function () {
+ var cmd = {
+ command: 'clearall'
+ };
+ this.sendCommand(cmd);
+ },
+
+ /**
+ * Sends a command to rund specified effect
+ * @param {object} effect - Effect object
+ */
+ runEffect: function (effect) {
+ var cmd;
+
+ if (!effect) {
+ return;
+ }
+
+ cmd = {
+ command: 'effect',
+ effect: {
+ name: effect.name,
+ args: effect.args
+ },
+ priority: this.server.priority
+ };
+ this.sendCommand(cmd);
+ },
+
+ /**
+ * Sends a command for color transformation
+ * @param {object} transform
+ */
+ setTransform: function (transform) {
+ var cmd;
+
+ if (!transform) {
+ return;
+ }
+
+ cmd = {
+ 'command': 'transform',
+ 'transform': transform
+ };
+
+ this.sendCommand(cmd);
+ },
+
+ /**
+ * @private
+ * @param command
+ */
+ sendCommand: function (command) {
+ var data;
+
+ if (!command) {
+ return;
+ }
+
+ if (typeof command === 'string') {
+ data = command;
+ } else {
+ data = JSON.stringify(command);
+ }
+
+ this.socket.isConnected(function (connected) {
+ if (connected) {
+ this.socket.write(data + '\n', function () {
+ this.emit('cmdSent', command);
+ }.bind(this), function (error) {
+ this.emit('error', error);
+ }.bind(this));
+ } else {
+ this.emit('error', 'No server connection');
+ }
+ }.bind(this));
+ },
+
+ /**
+ * Get the information about the hyperion server
+ */
+ getServerInfo: function () {
+ var cmd = {command: 'serverinfo'};
+
+ this.socket.isConnected(function (connected) {
+ if (connected) {
+ this.socket.write(JSON.stringify(cmd) + '\n', function () {
+ this.socket.read(function (result) {
+ var dataobj, str = tools.ab2str(result);
+ dataobj = JSON.parse(str);
+ this.emit('serverInfo', dataobj.info);
+ }.bind(this), function (error) {
+ this.emit('error', error);
+ }.bind(this));
+ }.bind(this), function (error) {
+ this.emit('error', error);
+ }.bind(this));
+ } else {
+ this.emit('error', 'No server connection');
+ }
+ }.bind(this));
+ },
+
+ isConnecting: function () {
+ return this.connecting;
+ }
+ });
+});
diff --git a/assets/webconfig/js/app/main.js b/assets/webconfig/js/app/main.js
new file mode 100644
index 00000000..dfa2796e
--- /dev/null
+++ b/assets/webconfig/js/app/main.js
@@ -0,0 +1,150 @@
+/*global require, requirejs */
+
+requirejs.config({
+ baseUrl: 'js/app',
+ paths: {
+ 'lib': '../vendor'
+ },
+ map: {
+ 'controllers/AppController': {
+ 'api/Socket': 'api/WebSocket'
+ }
+ }
+});
+
+/**
+ * @param {HTMLElement} dom
+ * @param {function} handler
+ */
+window.addPointerDownHandler = function (dom, handler) {
+ 'use strict';
+ dom.addEventListener('touchstart', handler, false);
+ dom.addEventListener('mousedown', handler, false);
+};
+
+/**
+ * @param {HTMLElement} dom
+ * @param {function} handler
+ */
+window.removePointerDownHandler = function (dom, handler) {
+ 'use strict';
+ dom.removeEventListener('touchstart', handler, false);
+ dom.removeEventListener('mousedown', handler, false);
+};
+
+/**
+ * @param {HTMLElement} dom
+ * @param {function} handler
+ */
+window.addPointerUpHandler = function (dom, handler) {
+ 'use strict';
+ dom.addEventListener('touchend', handler, false);
+ dom.addEventListener('mouseup', handler, false);
+};
+
+/**
+ * @param {HTMLElement} dom
+ * @param {function} handler
+ */
+window.removePointerUpHandler = function (dom, handler) {
+ 'use strict';
+ dom.removeEventListener('touchend', handler, false);
+ dom.removeEventListener('mouseup', handler, false);
+};
+
+/**
+ * @param {HTMLElement} dom
+ * @param {function} handler
+ */
+window.addPointerMoveHandler = function (dom, handler) {
+ 'use strict';
+ dom.addEventListener('touchmove', handler, false);
+ dom.addEventListener('mousemove', handler, false);
+};
+
+/**
+ * @param {HTMLElement} dom
+ * @param {function} handler
+ */
+window.removePointerMoveHandler = function (dom, handler) {
+ 'use strict';
+ dom.removeEventListener('touchmove', handler, false);
+ dom.removeEventListener('mousemove', handler, false);
+};
+
+/**
+ *
+ * @param {HTMLElement} dom
+ * @param {function} handler
+ */
+window.addClickHandler = function (dom, handler) {
+ 'use strict';
+ var toFire = false;
+
+ dom.addEventListener('touchstart', function (event) {
+ if (event.touches.length > 1) {
+ return;
+ }
+ toFire = true;
+ }, false);
+
+ dom.addEventListener('touchmove', function () {
+ toFire = false;
+ }, false);
+
+ dom.addEventListener('touchend', function (event) {
+ var focused;
+ if (toFire) {
+ handler.apply(this, arguments);
+
+ focused = document.querySelector(':focus');
+
+ if (focused && event.target !== focused) {
+ focused.blur();
+ }
+
+ if (event.target.tagName !== 'INPUT') {
+ event.preventDefault();
+ }
+ }
+ }, false);
+
+ dom.addEventListener('click', function () {
+ handler.apply(this, arguments);
+ }, false);
+};
+
+function checkInstallFirefoxOS() {
+ 'use strict';
+ var manifest_url, installCheck;
+
+ manifest_url = [location.protocol, '//', location.host, location.pathname.replace('index.html',''), 'manifest.webapp'].join('');
+ installCheck = navigator.mozApps.checkInstalled(manifest_url);
+
+ installCheck.onerror = function() {
+ alert('Error calling checkInstalled: ' + installCheck.error.name);
+ };
+
+ installCheck.onsuccess = function() {
+ var installLoc;
+ if(!installCheck.result) {
+ if (confirm('Do you want to install hyperion remote contorl on your device?')) {
+ installLoc = navigator.mozApps.install(manifest_url);
+ installLoc.onsuccess = function(data) {
+ };
+ installLoc.onerror = function() {
+ alert(installLoc.error.name);
+ };
+ }
+ }
+ };
+}
+
+require(['controllers/AppController'], function (AppController) {
+ 'use strict';
+ var app = new AppController();
+ app.init();
+ if (navigator.mozApps && navigator.userAgent.indexOf('Mozilla/5.0 (Mobile;') !== -1) {
+ checkInstallFirefoxOS();
+ }
+});
diff --git a/assets/webconfig/js/app/main_chrome.js b/assets/webconfig/js/app/main_chrome.js
new file mode 100644
index 00000000..786030f2
--- /dev/null
+++ b/assets/webconfig/js/app/main_chrome.js
@@ -0,0 +1,108 @@
+/*global require, requirejs */
+
+requirejs.config({
+ baseUrl: '../js/app',
+ paths: {
+ 'lib': '../vendor'
+ },
+ map: {
+ 'controllers/AppController': {
+ 'api/Socket': 'api/ChromeTcpSocket',
+ 'api/Network': 'api/ChromeNetwork'
+ },
+ 'models/Settings': {
+ 'api/LocalStorage': 'api/ChromeLocalStorage'
+ }
+ }
+});
+
+/**
+ *
+ * @param {HTMLElement} dom
+ * @param {function} handler
+ */
+window.addPointerDownHandler = function (dom, handler) {
+ 'use strict';
+ dom.addEventListener('touchstart', function (event) {
+ handler.apply(this, arguments);
+ event.preventDefault();
+ }, false);
+
+ dom.addEventListener('mousedown', function () {
+ handler.apply(this, arguments);
+ }, false);
+};
+
+/**
+ *
+ * @param {HTMLElement} dom
+ * @param {function} handler
+ */
+window.addPointerUpHandler = function (dom, handler) {
+ 'use strict';
+ dom.addEventListener('touchend', function (event) {
+ handler.apply(this, arguments);
+ event.preventDefault();
+ }, false);
+
+ dom.addEventListener('mouseup', function () {
+ handler.apply(this, arguments);
+ }, false);
+};
+
+/**
+ *
+ * @param {HTMLElement} dom
+ * @param {function} handler
+ */
+window.addPointerMoveHandler = function (dom, handler) {
+ 'use strict';
+ dom.addEventListener('touchmove', function (event) {
+ handler.apply(this, arguments);
+ event.preventDefault();
+ }, false);
+
+ dom.addEventListener('mousemove', function () {
+ handler.apply(this, arguments);
+ }, false);
+};
+
+/**
+ *
+ * @param {HTMLElement} dom
+ * @param {function} handler
+ */
+window.addClickHandler = function (dom, handler) {
+ 'use strict';
+ var toFire = false;
+
+ dom.addEventListener('touchstart', function (event) {
+ if (event.touches.length > 1) {
+ return;
+ }
+ toFire = true;
+ }, false);
+
+ dom.addEventListener('touchmove', function () {
+ toFire = false;
+ }, false);
+
+ dom.addEventListener('touchend', function (event) {
+ if (toFire) {
+ handler.apply(this, arguments);
+ if (event.target.tagName !== 'INPUT') {
+ event.preventDefault();
+ }
+ }
+ }, false);
+
+ dom.addEventListener('click', function () {
+ handler.apply(this, arguments);
+ }, false);
+};
+
+require(['controllers/AppController'], function (AppController) {
+ 'use strict';
+ var app = new AppController();
+ app.init();
+});
diff --git a/assets/webconfig/js/app/models/Settings.js b/assets/webconfig/js/app/models/Settings.js
new file mode 100644
index 00000000..5e224f03
--- /dev/null
+++ b/assets/webconfig/js/app/models/Settings.js
@@ -0,0 +1,124 @@
+/*global define */
+define(['lib/stapes', 'api/LocalStorage'], function (Stapes, LocalStorage) {
+ 'use strict';
+
+ return Stapes.subclass(/** @lends Settings.prototype */{
+ storage: null, servers: [],
+
+ /**
+ * @class Settings
+ * @classdesc Local application settings
+ * @constructs
+ * @fires saved
+ * @fires loaded
+ * @fires error
+ * @fires serverAdded
+ * @fires serverChanged
+ * @fires serverRemoved
+ */
+ constructor: function () {
+ this.storage = new LocalStorage();
+ this.storage.on({
+ error: function (message) {
+ this.emit('error', message);
+ }, got: function (settings) {
+ if (settings) {
+ this.servers = settings.servers || [];
+ }
+ this.emit('loaded');
+ }, set: function () {
+ this.emit('saved');
+ }
+ }, this);
+ },
+
+ /**
+ * Save current settings
+ */
+ save: function () {
+ this.storage.set({
+ servers: this.servers
+ });
+ },
+
+ /**
+ * Loads persistent settings
+ */
+ load: function () {
+ this.storage.get();
+ },
+
+ /**
+ * Add a server definition
+ * @param {object} server - Server information
+ */
+ addServer: function (server) {
+ if (this.indexOfServer(server) === -1) {
+ if (this.servers.length === 0) {
+ server.selected = true;
+ }
+
+ this.servers.push(server);
+ this.save();
+ this.emit('serverAdded', server);
+ }
+ },
+
+ /**
+ * Sets a server as a default server
+ * @param {number} index - Index of the server in the server list to set as default one
+ */
+ setSelectedServer: function (index) {
+ var i;
+ for (i = 0; i < this.servers.length; i++) {
+ delete this.servers[i].selected;
+ }
+ this.servers[index].selected = true;
+ this.save();
+ this.emit('serverChanged', this.servers[index]);
+ },
+
+ /**
+ * Remove a server from the list
+ * @param {number} index - Index of the server in the list to remove
+ */
+ removeServer: function (index) {
+ this.servers.splice(index, 1);
+ this.save();
+ this.emit('serverRemoved');
+ },
+
+ /**
+ * Update server information
+ * @param {number} index - Index of the server to update
+ * @param {object} server - New server information
+ */
+ updateServer: function (index, server) {
+ if (index >= 0 && index < this.servers.length) {
+ this.servers[index] = server;
+ this.save();
+ this.emit('serverChanged', server);
+ }
+ },
+
+ /**
+ * Find the server in the list.
+ * @param {object} server - Server to search index for
+ * @returns {number} - Index of the server in the list. -1 if server not found
+ */
+ indexOfServer: function (server) {
+ var i, tmp;
+
+ for (i = 0; i < this.servers.length; i++) {
+ tmp = this.servers[i];
+
+ if (tmp.port === server.port && tmp.address === server.address) {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ });
+});
diff --git a/assets/webconfig/js/app/utils/Tools.js b/assets/webconfig/js/app/utils/Tools.js
new file mode 100644
index 00000000..2e2194cb
--- /dev/null
+++ b/assets/webconfig/js/app/utils/Tools.js
@@ -0,0 +1,56 @@
+define([], function () {
+ 'use strict';
+
+ return {
+ /**
+ * Convert a string to ArrayBuffer
+ * @param {string} str String to convert
+ * @returns {ArrayBuffer} Result
+ */
+ str2ab: function (str) {
+ var i, buf = new ArrayBuffer(str.length), bufView = new Uint8Array(buf);
+ for (i = 0; i < str.length; i++) {
+ bufView[i] = str.charCodeAt(i);
+ }
+ return buf;
+ },
+
+ /**
+ * Convert an array to ArrayBuffer
+ * @param array
+ * @returns {ArrayBuffer} Result
+ */
+ a2ab: function (array) {
+ return new Uint8Array(array).buffer;
+ },
+
+ /**
+ * Convert ArrayBuffer to string
+ * @param {ArrayBuffer} buffer Buffer to convert
+ * @returns {string}
+ */
+ ab2hexstr: function (buffer) {
+ var i, str = '', ua = new Uint8Array(buffer);
+ for (i = 0; i < ua.length; i++) {
+ str += this.b2hexstr(ua[i]);
+ }
+ return str;
+ },
+
+ /**
+ * Convert byte to hexstr.
+ * @param {number} byte Byte to convert
+ */
+ b2hexstr: function (byte) {
+ return ('00' + byte.toString(16)).substr(-2);
+ },
+
+ /**
+ * @param {ArrayBuffer} buffer
+ * @returns {string}
+ */
+ ab2str: function (buffer) {
+ return String.fromCharCode.apply(null, new Uint8Array(buffer));
+ }
+ };
+});
diff --git a/assets/webconfig/js/app/views/EffectsView.js b/assets/webconfig/js/app/views/EffectsView.js
new file mode 100644
index 00000000..4f41d07d
--- /dev/null
+++ b/assets/webconfig/js/app/views/EffectsView.js
@@ -0,0 +1,64 @@
+/**
+ * hyperion remote
+ * MIT License
+ */
+
+define(['lib/stapes'], function (Stapes) {
+ 'use strict';
+
+ return Stapes.subclass(/** @lends EffectsView.prototype */{
+ /**
+ * @class EffectsView
+ * @constructs
+ */
+ constructor: function () {
+ this.bindEventHandlers();
+ },
+
+ /**
+ * @private
+ */
+ bindEventHandlers: function () {
+ window.addClickHandler(document.querySelector('#effects ul'), function (event) {
+ var selected = event.target.parentNode.querySelector('.selected');
+ if (selected) {
+ selected.classList.remove('selected');
+ }
+ event.target.classList.add('selected');
+ this.emit('effectSelected', event.target.dataset.id);
+ }.bind(this));
+ },
+
+ /**
+ * Clear the list
+ */
+ clear: function () {
+ document.querySelector('#effects ul').innerHTML = '';
+ document.querySelector('#effects .info').classList.add('hidden');
+ },
+
+ /**
+ * Fill the list
+ * @param {object} effects - Object containing effect information
+ */
+ fillList: function (effects) {
+ var dom, el, i;
+
+ dom = document.createDocumentFragment();
+
+ for (i = 0; i < effects.length; i++) {
+ el = document.createElement('li');
+ el.innerHTML = effects[i].name;
+ el.dataset.id = effects[i].id;
+ dom.appendChild(el);
+ }
+
+ document.querySelector('#effects ul').appendChild(dom);
+
+ if (effects.length === 0) {
+ document.querySelector('#effects .info').classList.remove('hidden');
+ }
+ }
+ });
+});
+
diff --git a/assets/webconfig/js/app/views/MainView.js b/assets/webconfig/js/app/views/MainView.js
new file mode 100644
index 00000000..986af3fa
--- /dev/null
+++ b/assets/webconfig/js/app/views/MainView.js
@@ -0,0 +1,396 @@
+define([
+ 'lib/stapes',
+ 'lib/tinycolor',
+ 'utils/Tools',
+ 'views/Slider'
+], function (Stapes, Tinycolor, tools, Slider) {
+ 'use strict';
+ var timer;
+
+ function showMessageField (text, type) {
+ var dom, wrapper;
+
+ dom = document.querySelector('.work .msg');
+
+ if (!dom) {
+ dom = document.createElement('div');
+ dom.classList.add('msg');
+ dom.classList.add(type);
+ wrapper = document.createElement('div');
+ wrapper.classList.add('wrapper_msg');
+ wrapper.classList.add('invisible');
+ wrapper.appendChild(dom);
+
+ document.querySelector('.work').appendChild(wrapper);
+ setTimeout(function () {
+ wrapper.classList.remove('invisible');
+ }, 0);
+ }
+
+ if (!dom.classList.contains(type)) {
+ dom.className = 'msg';
+ dom.classList.add(type);
+ }
+ dom.innerHTML = text;
+ if (timer) {
+ clearTimeout(timer);
+ }
+ timer = setTimeout(function () {
+ var error = document.querySelector('.work .wrapper_msg');
+ if (error) {
+ error.parentNode.removeChild(error);
+ }
+ }, 1600);
+ }
+
+ return Stapes.subclass(/** @lends MainView.prototype*/{
+ pointer: null,
+ colorpicker: null,
+ slider: null,
+ sliderinput: null,
+ cpradius: 0,
+ cpcenter: 0,
+ drag: false,
+ color: null,
+ brightness: 1.0,
+ inputbox: null,
+
+ /**
+ * @class MainView
+ * @construct
+ * @fires barClick
+ * @fires colorChange
+ */
+ constructor: function () {
+ var ev;
+ this.pointer = document.querySelector('#colorpicker #pointer');
+ this.colorpicker = document.querySelector('#colorpicker #colorwheelbg');
+ this.slider = new Slider({
+ element: document.getElementById('brightness'),
+ min: 0,
+ max: 1,
+ step: 0.02,
+ value: 1
+ });
+ this.inputbox = document.querySelector('#color input.value');
+
+ this.cpradius = this.colorpicker.offsetWidth / 2;
+ this.cpcenter = this.colorpicker.offsetLeft + this.cpradius;
+
+ this.bindEventHandlers();
+
+ ev = document.createEvent('Event');
+ ev.initEvent('resize', true, true);
+ window.dispatchEvent(ev);
+ },
+
+ /**
+ * @private
+ */
+ bindEventHandlers: function () {
+ var cptouchrect;
+
+ window.addEventListener('resize', function () {
+ var attrW, attrH, side, w = this.colorpicker.parentNode.clientWidth, h = this.colorpicker.parentNode.clientHeight;
+
+ attrW = this.colorpicker.getAttribute('width');
+ attrH = this.colorpicker.getAttribute('height');
+ side = attrW === 'auto' ? attrH : attrW;
+ if (w > h) {
+ if (attrH !== side) {
+ this.colorpicker.setAttribute('height', side);
+ this.colorpicker.setAttribute('width', 'auto');
+ }
+ } else if (attrW !== side) {
+ this.colorpicker.setAttribute('height', 'auto');
+ this.colorpicker.setAttribute('width', side);
+ }
+
+ this.cpradius = this.colorpicker.offsetWidth / 2;
+ this.cpcenter = this.colorpicker.offsetLeft + this.cpradius;
+ if (this.color) {
+ this.updatePointer();
+ }
+ }.bind(this));
+
+ window.addClickHandler(document.querySelector('.footer'), function (event) {
+ this.emit('barClick', event.target.parentNode.dataset.area);
+ }.bind(this));
+
+
+ this.slider.on('changeValue', function (value) {
+ this.brightness = value.value;
+ this.updateInput();
+ this.fireColorEvent();
+ }, this);
+
+ this.inputbox.addEventListener('input', function (event) {
+ var bright, rgb = new Tinycolor(event.target.value).toRgb();
+
+ if (rgb.r === 0 && rgb.g === 0 && rgb.b === 0) {
+ this.brightness = 0;
+ this.color = new Tinycolor({
+ r: 0xff,
+ g: 0xff,
+ b: 0xff
+ });
+ } else {
+ bright = Math.max(rgb.r, rgb.g, rgb.b) / 256;
+ rgb.r = Math.round(rgb.r / bright);
+ rgb.g = Math.round(rgb.g / bright);
+ rgb.b = Math.round(rgb.b / bright);
+ this.brightness = bright;
+ this.color = new Tinycolor(rgb);
+ }
+
+ this.fireColorEvent();
+ this.updatePointer();
+ this.updateSlider();
+ }.bind(this), false);
+
+ this.inputbox.addEventListener('keydown', function (event) {
+ switch (event.keyCode) {
+ case 8:
+ case 9:
+ case 16:
+ case 37:
+ case 38:
+ case 39:
+ case 40:
+ case 46:
+ break;
+ default:
+ {
+ if (event.target.value.length >= 6 && (event.target.selectionEnd - event.target.selectionStart) === 0) {
+ event.preventDefault();
+ event.stopPropagation();
+ } else if (event.keyCode < 48 || event.keyCode > 71) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+ }
+ });
+
+ cptouchrect = document.querySelector('#colorpicker .touchrect');
+ window.addPointerDownHandler(cptouchrect, function (event) {
+ this.leaveInput();
+ this.drag = true;
+ this.handleEvent(event);
+ }.bind(this));
+
+ window.addPointerMoveHandler(cptouchrect, function (event) {
+ if (this.drag) {
+ this.handleEvent(event);
+ event.preventDefault();
+ }
+ }.bind(this));
+
+ window.addPointerUpHandler(cptouchrect, function () {
+ this.drag = false;
+ }.bind(this));
+
+ window.addClickHandler(document.querySelector('#clear_button'), function () {
+ this.emit('clear');
+ }.bind(this));
+
+ window.addClickHandler(document.querySelector('#clearall_button'), function () {
+ this.emit('clearall');
+ }.bind(this));
+ },
+
+ /**
+ * @private
+ * @param event
+ */
+ handleEvent: function (event) {
+ var point, x, y;
+
+ if (event.touches) {
+ x = event.touches[0].clientX;
+ y = event.touches[0].clientY;
+ } else {
+ x = event.clientX;
+ y = event.clientY;
+ }
+ point = this.getCirclePoint(x, y);
+ this.color = this.getColorFromPoint(point);
+
+ this.updatePointer();
+ this.updateSlider();
+ this.updateInput();
+
+ this.fireColorEvent();
+ },
+
+ /**
+ * @private
+ * @param {number} x
+ * @param {number} y
+ * @returns {{x: number, y: number}}
+ */
+ getCirclePoint: function (x, y) {
+ var p = {
+ x: x,
+ y: y
+ }, c = {
+ x: this.colorpicker.offsetLeft + this.cpradius,
+ y: this.colorpicker.offsetTop + this.cpradius
+ }, n;
+
+ n = Math.sqrt(Math.pow((x - c.x), 2) + Math.pow((y - c.y), 2));
+
+ if (n > this.cpradius) {
+ p.x = (c.x) + this.cpradius * ((x - c.x) / n);
+ p.y = (c.y) + this.cpradius * ((y - c.y) / n);
+ }
+
+ return p;
+ },
+
+ /**
+ * @private
+ * @param {{x: number, y: number}} p
+ * @returns {Tinycolor}
+ */
+ getColorFromPoint: function (p) {
+ var h, t, s, x, y;
+ x = p.x - this.colorpicker.offsetLeft - this.cpradius;
+ y = this.cpradius - p.y + this.colorpicker.offsetTop;
+ t = Math.atan2(y, x);
+ h = (t * (180 / Math.PI) + 360) % 360;
+ s = Math.min(Math.sqrt(x * x + y * y) / this.cpradius, 1);
+
+ return new Tinycolor({
+ h: h,
+ s: s,
+ v: 1
+ });
+ },
+
+ /**
+ * @private
+ * @param color
+ * @returns {{x: number, y: number}}
+ */
+ getPointFromColor: function (color) {
+ var t, x, y, p = {};
+
+ t = color.h * (Math.PI / 180);
+ y = Math.sin(t) * this.cpradius * color.s;
+ x = Math.cos(t) * this.cpradius * color.s;
+
+ p.x = Math.round(x + this.colorpicker.offsetLeft + this.cpradius);
+ p.y = Math.round(this.cpradius - y + this.colorpicker.offsetTop);
+
+ return p;
+ },
+
+ /**
+ * @private
+ */
+ fireColorEvent: function () {
+ var rgb = this.color.toRgb();
+ rgb.r = Math.round(rgb.r * this.brightness);
+ rgb.g = Math.round(rgb.g * this.brightness);
+ rgb.b = Math.round(rgb.b * this.brightness);
+ this.emit('colorChange', rgb);
+ },
+
+ /**
+ *
+ * @param rgb
+ */
+ setColor: function (rgb) {
+ var bright;
+
+ if (rgb.r === 0 && rgb.g === 0 && rgb.b === 0) {
+ this.brightness = 0;
+ this.color = new Tinycolor({
+ r: 0xff,
+ g: 0xff,
+ b: 0xff
+ });
+ } else {
+ bright = Math.max(rgb.r, rgb.g, rgb.b) / 256;
+ rgb.r = Math.round(rgb.r / bright);
+ rgb.g = Math.round(rgb.g / bright);
+ rgb.b = Math.round(rgb.b / bright);
+ this.brightness = bright;
+ this.color = new Tinycolor(rgb);
+ }
+
+ this.updatePointer();
+ this.updateSlider();
+ this.updateInput();
+ },
+
+ /**
+ * @private
+ */
+ updateSlider: function () {
+ this.slider.setValue(this.brightness);
+ this.slider.dom.style.backgroundImage = '-webkit-linear-gradient(left, #000000 0%, ' + this.color.toHexString() + ' 100%)';
+ },
+
+ /**
+ * @private
+ */
+ updatePointer: function () {
+ var point = this.getPointFromColor(this.color.toHsv());
+
+ this.pointer.style.left = (point.x - this.pointer.offsetWidth / 2) + 'px';
+ this.pointer.style.top = (point.y - this.pointer.offsetHeight / 2) + 'px';
+ this.pointer.style.backgroundColor = this.color.toHexString();
+ },
+
+ /**
+ * @private
+ */
+ updateInput: function () {
+ var rgb = this.color.toRgb();
+ rgb.r = Math.round(rgb.r * this.brightness);
+ rgb.g = Math.round(rgb.g * this.brightness);
+ rgb.b = Math.round(rgb.b * this.brightness);
+
+ this.inputbox.value = tools.b2hexstr(rgb.r) + tools.b2hexstr(rgb.g) + tools.b2hexstr(rgb.b);
+ },
+
+ /**
+ * Scroll to the specific tab content
+ * @param {string} id - Id of the tab to scroll to
+ */
+ scrollToArea: function (id) {
+ var area, index;
+ document.querySelector('.footer .selected').classList.remove('selected');
+ document.querySelector('.footer .button[data-area =' + id + ']').classList.add('selected');
+
+ area = document.getElementById(id);
+ index = area.offsetLeft / area.clientWidth;
+ area.parentNode.style.left = (-index * 100) + '%';
+ },
+
+ /**
+ * Shows a status message
+ * @param {string} message - Text to show
+ */
+ showStatus: function (message) {
+ showMessageField(message, 'status');
+ },
+
+ /**
+ * Shows the error text
+ * @param {string} error - Error message
+ */
+ showError: function (error) {
+ showMessageField(error, 'error');
+ },
+
+ /**
+ * @private
+ */
+ leaveInput: function () {
+ this.inputbox.blur();
+ }
+ });
+});
diff --git a/assets/webconfig/js/app/views/ServerList.js b/assets/webconfig/js/app/views/ServerList.js
new file mode 100644
index 00000000..fb36927c
--- /dev/null
+++ b/assets/webconfig/js/app/views/ServerList.js
@@ -0,0 +1,199 @@
+define(['lib/stapes'], function (Stapes) {
+ 'use strict';
+
+ /**
+ *
+ * @param params
+ * @returns {HTMLElement}
+ */
+ function buildDom (params) {
+ var dom, label, ul, li, i, header;
+
+ dom = document.createElement('div');
+ dom.classList.add('grouplist');
+ dom.id = params.id;
+
+ header = document.createElement('div');
+ header.classList.add('header');
+ dom.appendChild(header);
+ label = document.createElement('div');
+ label.innerHTML = params.label;
+ label.classList.add('title');
+ header.appendChild(label);
+ label = document.createElement('div');
+ label.innerHTML = '';
+ label.classList.add('callout');
+ header.appendChild(label);
+
+ ul = document.createElement('ul');
+ dom.appendChild(ul);
+ if (params.list) {
+ for (i = 0; i < params.list.length; i++) {
+ li = createLine(params.list[i]);
+ ul.appendChild(li);
+ }
+ }
+
+ return dom;
+ }
+
+ function createLine (params) {
+ var dom, el, horiz, box, touch;
+
+ dom = document.createDocumentFragment();
+
+ horiz = document.createElement('div');
+ horiz.classList.add('horizontal');
+ dom.appendChild(horiz);
+
+ touch = document.createElement('div');
+ touch.classList.add('horizontal');
+ horiz.appendChild(touch);
+
+ el = document.createElement('div');
+ el.classList.add('checkbox');
+ touch.appendChild(el);
+
+ box = document.createElement('div');
+ box.classList.add('titlebox');
+ touch.appendChild(box);
+
+ el = document.createElement('label');
+ el.classList.add('title');
+ el.innerHTML = params.title || '';
+ box.appendChild(el);
+
+ el = document.createElement('label');
+ el.classList.add('subtitle');
+ el.innerHTML = params.subtitle;
+ box.appendChild(el);
+
+ el = document.createElement('div');
+ el.classList.add('touchrect');
+ touch.appendChild(el);
+
+ el = document.createElement('div');
+ el.classList.add('edit_icon');
+ el.innerHTML = '';
+ horiz.appendChild(el);
+
+ el = document.createElement('div');
+ el.classList.add('delete_icon');
+ el.innerHTML = '';
+ horiz.appendChild(el);
+
+ return dom;
+ }
+
+ return Stapes.subclass(/** @lends ServerList.prototype */{
+ /**
+ * @private
+ * @type {HTMLElement}
+ */
+ dom: null,
+
+ /**
+ * @class ServerList
+ * @constructs
+ * @param {object} params - List parameter
+ * @param {string} params.id - List id
+ * @param {string} params.label - List title
+ * @param {{title: string, subtitle: string, c}[]} [params.list] - List elements
+ *
+ * @fires add
+ * @fires remove
+ * @fires select
+ * @fires edit
+ */
+ constructor: function (params) {
+ this.dom = buildDom(params || {});
+ this.bindEventHandlers();
+ },
+
+ /**
+ * @private
+ */
+ bindEventHandlers: function () {
+ window.addClickHandler(this.dom.querySelector('.callout'), function () {
+ this.emit('add');
+ }.bind(this));
+
+ window.addClickHandler(this.dom.querySelector('ul'), function (event) {
+ if (event.target.classList.contains('delete_icon')) {
+ this.emit('remove', event.target.parentNode.parentNode.id);
+ } else if (event.target.classList.contains('edit_icon')) {
+ this.emit('edit', event.target.parentNode.parentNode.id);
+ } else if (event.target.classList.contains('touchrect')) {
+ this.emit('select', event.target.parentNode.parentNode.parentNode.id);
+ }
+ }.bind(this));
+ },
+
+ /**
+ * Returns the DOM of the list
+ * @returns {HTMLElement}
+ */
+ getDom: function () {
+ return this.dom;
+ },
+
+ /**
+ * Append a line
+ * @param id
+ * @param selected
+ * @param element
+ */
+ append: function (id, selected, element) {
+ var li = document.createElement('li');
+ li.id = id;
+ if (selected) {
+ li.classList.add('selected');
+ }
+ li.appendChild(element);
+ this.dom.querySelector('ul').appendChild(li);
+ },
+
+ /**
+ * Replace a line
+ * @param index
+ * @param element
+ */
+ replace: function (index, element) {
+ var child = this.dom.querySelector('ul li:nth-child(' + (index + 1) + ')'), li;
+ if (child) {
+ li = document.createElement('li');
+ li.appendChild(element);
+ this.dom.querySelector('ul').replaceChild(li, child);
+ }
+ },
+
+ /**
+ * Add a line
+ * @param lineParam
+ */
+ addLine: function (lineParam) {
+ var line = createLine(lineParam);
+ this.append(lineParam.id, lineParam.selected, line);
+ },
+
+ /**
+ * Clear the list
+ */
+ clear: function () {
+ this.dom.querySelector('ul').innerHTML = '';
+ },
+
+ /**
+ * Hide or show the Add button in the header
+ * @param {boolean} show - True to show, false to hide
+ */
+ showAddButton: function (show) {
+ if (show) {
+ this.dom.querySelector('.callout').classList.remove('invisible');
+ } else {
+ this.dom.querySelector('.callout').classList.add('invisible');
+ }
+ }
+ });
+});
+
diff --git a/assets/webconfig/js/app/views/SettingsView.js b/assets/webconfig/js/app/views/SettingsView.js
new file mode 100644
index 00000000..38c24f83
--- /dev/null
+++ b/assets/webconfig/js/app/views/SettingsView.js
@@ -0,0 +1,259 @@
+define(['lib/stapes', 'views/ServerList'], function (Stapes, ServerList) {
+ 'use strict';
+
+ function createLabelInputLine (params) {
+ var dom, el;
+
+ dom = document.createElement('div');
+ dom.classList.add('inputline');
+ dom.id = params.id;
+
+ el = document.createElement('label');
+ el.innerHTML = params.label;
+ dom.appendChild(el);
+
+ el = document.createElement('input');
+ if (typeof params.value === 'number') {
+ el.type = 'number';
+ }
+ el.value = params.value || '';
+ el.autocomplete='off';
+ el.autocorrect='off';
+ el.autocapitalize='off';
+ dom.appendChild(el);
+
+ return dom;
+ }
+
+ function createButtonLine (params) {
+ var dom, el;
+
+ dom = document.createElement('div');
+ dom.classList.add('inputline');
+
+ el = document.createElement('button');
+ el.innerHTML = params.label;
+ el.classList.add('OK');
+ dom.appendChild(el);
+
+ el = document.createElement('button');
+ el.innerHTML = 'Cancel';
+ el.classList.add('CANCEL');
+ dom.appendChild(el);
+
+ return dom;
+ }
+
+ function createDetectLine () {
+ var dom, el;
+
+ dom = document.createElement('div');
+ dom.classList.add('line');
+
+ el = document.createElement('button');
+ el.id = 'detect_button';
+ el.innerHTML = 'Detect';
+ dom.appendChild(el);
+
+ el = document.createElement('div');
+ el.classList.add('spinner');
+ el.classList.add('hidden');
+ dom.appendChild(el);
+
+ return dom;
+ }
+
+ return Stapes.subclass(/** @lends SettingsView.prototype */{
+ dom: null,
+ serverList: null,
+
+ /**
+ * @class SettingsView
+ * @classdesc View for the settings
+ * @constructs
+ */
+ constructor: function () {
+ var list = [], el;
+
+ this.dom = document.querySelector('#settings');
+
+ this.serverList = new ServerList({
+ id: 'serverList',
+ label: 'Server',
+ list: list
+ });
+
+ this.serverList.on({
+ add: function () {
+ var line, box;
+
+ this.enableDetectButton(false);
+ this.lockList(true);
+
+ box = document.createDocumentFragment();
+
+ line = createLabelInputLine({id: 'name', label: 'Name:'});
+ box.appendChild(line);
+ line = createLabelInputLine({id: 'address', label: 'Address:'});
+ box.appendChild(line);
+ line = createLabelInputLine({id: 'port', label: 'Port:', value: 19444});
+ box.appendChild(line);
+ line = createLabelInputLine({id: 'priority', label: 'Priority:', value: 50});
+ box.appendChild(line);
+ line = createLabelInputLine({id: 'duration', label: 'Duration (sec):', value: 0});
+ box.appendChild(line);
+
+ line = createButtonLine({label: 'Add'});
+
+ window.addClickHandler(line.firstChild, function (event) {
+ var server = {}, i, inputs = event.target.parentNode.parentNode.querySelectorAll('input');
+
+ for (i = 0; i < inputs.length; i++) {
+ server[inputs[i].parentNode.id] = inputs[i].value;
+ }
+
+ server.port = parseInt(server.port);
+ server.priority = parseInt(server.priority);
+ server.duration = parseInt(server.duration);
+
+ this.emit('serverAdded', server);
+ }.bind(this));
+
+ window.addClickHandler(line.lastChild, function () {
+ this.emit('serverAddCanceled');
+ }.bind(this));
+ box.appendChild(line);
+ this.serverList.append(null, false, box);
+ },
+ select: function (id) {
+ if (!this.dom.classList.contains('locked')) {
+ this.emit('serverSelected', parseInt(id.replace('server_', '')));
+ }
+ },
+ remove: function (id) {
+ this.emit('serverRemoved', parseInt(id.replace('server_', '')));
+ },
+ edit: function (id) {
+ this.emit('editServer', parseInt(id.replace('server_', '')));
+ }
+ }, this);
+
+ this.dom.appendChild(this.serverList.getDom());
+
+ el = createDetectLine();
+
+ window.addClickHandler(el.querySelector('button'), function () {
+ this.emit('detect');
+ }.bind(this));
+
+ this.dom.appendChild(el);
+ },
+
+ /**
+ * Fills the list of known servers
+ * @param {Array} servers
+ */
+ fillServerList: function (servers) {
+ var i, server, params;
+ this.serverList.clear();
+ for (i = 0; i < servers.length; i++) {
+ server = servers[i];
+ params = {id: 'server_' + i, title: server.name, subtitle: server.address + ':' + server.port};
+ if (server.selected) {
+ params.selected = true;
+ }
+ this.serverList.addLine(params);
+ }
+ this.serverList.showAddButton(true);
+ },
+
+ /**
+ * Shows or hides the spinner as progress indicator
+ * @param {boolean} show True to show, false to hide
+ */
+ showWaiting: function (show) {
+ if (show) {
+ this.dom.querySelector('.spinner').classList.remove('hidden');
+ } else {
+ this.dom.querySelector('.spinner').classList.add('hidden');
+ }
+ },
+
+ /**
+ * Enables or disables the detect button
+ * @param {Boolean} enabled True to enable, false to disable
+ */
+ enableDetectButton: function (enabled) {
+ if (enabled) {
+ this.dom.querySelector('#detect_button').classList.remove('hidden');
+ } else {
+ this.dom.querySelector('#detect_button').classList.add('hidden');
+ }
+ },
+
+ /**
+ * Locks the list for editing/deleting
+ * @param {Boolean} lock True to lock, false to unlock
+ */
+ lockList: function (lock) {
+ if (!lock) {
+ this.dom.classList.remove('locked');
+ this.serverList.showAddButton(true);
+ } else {
+ this.dom.classList.add('locked');
+ this.serverList.showAddButton(false);
+ }
+ },
+
+ editServer: function (serverInfo) {
+ var line, box;
+
+ this.lockList(true);
+ this.enableDetectButton(false);
+
+ box = document.createDocumentFragment();
+
+ line = createLabelInputLine({id: 'name', label: 'Name:', value: serverInfo.server.name});
+ box.appendChild(line);
+ line = createLabelInputLine({id: 'address', label: 'Address:', value: serverInfo.server.address});
+ box.appendChild(line);
+ line = createLabelInputLine({id: 'port', label: 'Port:', value: serverInfo.server.port});
+ box.appendChild(line);
+ line = createLabelInputLine({id: 'priority', label: 'Priority:', value: serverInfo.server.priority});
+ box.appendChild(line);
+ line = createLabelInputLine({id: 'duration', label: 'Duration (sec):', value: serverInfo.server.duration});
+ box.appendChild(line);
+
+ line = createButtonLine({label: 'Done'});
+
+ window.addClickHandler(line.querySelector('button.OK'), function (event) {
+ var server = {}, i, inputs = event.target.parentNode.parentNode.querySelectorAll('input');
+
+ for (i = 0; i < inputs.length; i++) {
+ server[inputs[i].parentNode.id] = inputs[i].value;
+ }
+
+ server.port = parseInt(server.port);
+ server.priority = parseInt(server.priority);
+ server.duration = parseInt(server.duration);
+ if (serverInfo.server.selected) {
+ server.selected = true;
+ }
+
+ this.emit('serverChanged', {index: serverInfo.index, server: server});
+ }.bind(this));
+
+ window.addClickHandler(line.querySelector('button.CANCEL'), function () {
+ this.emit('serverEditCanceled');
+ }.bind(this));
+ box.appendChild(line);
+
+ window.addClickHandler(box.querySelector('input'), function (event) {
+ event.stopPropagation();
+ });
+
+ this.serverList.replace(serverInfo.index, box);
+ }
+ });
+});
+
diff --git a/assets/webconfig/js/app/views/Slider.js b/assets/webconfig/js/app/views/Slider.js
new file mode 100644
index 00000000..84ab6afb
--- /dev/null
+++ b/assets/webconfig/js/app/views/Slider.js
@@ -0,0 +1,134 @@
+define(['lib/stapes'], function (Stapes) {
+ 'use strict';
+
+ function syncView (slider) {
+ var left = (slider.value - slider.min) * 100 / (slider.max - slider.min);
+ slider.dom.lastElementChild.style.left = left + '%';
+ }
+
+ function handleEvent(event, slider) {
+ var x, left, ratio, value, steppedValue;
+ if (event.touches) {
+ x = event.touches[0].clientX;
+ } else {
+ x = event.clientX;
+ }
+
+ left = x - slider.dom.getBoundingClientRect().left;
+ ratio = left / slider.dom.offsetWidth;
+ value = (slider.max - slider.min) * ratio;
+
+ steppedValue = (value - slider.min) % slider.step;
+ if (steppedValue <= slider.step / 2) {
+ value = value - steppedValue;
+ } else {
+ value = value + (slider.step - steppedValue);
+ }
+
+ value = Math.max(value, slider.min);
+ value = Math.min(value, slider.max);
+ slider.value = value;
+ slider.emit('changeValue', {
+ 'value': value,
+ 'target': slider.dom
+ });
+ }
+
+ return Stapes.subclass(/** @lends Slider.prototype */{
+ /**
+ * @private
+ * @type {Element}
+ */
+ dom: null,
+
+ /**
+ * @private
+ * @type {Number}
+ */
+ min: 0,
+
+ /**
+ * @private
+ * @type {Number}
+ */
+ max: 100,
+
+ /**
+ * @private
+ * @type {Number}
+ */
+ value: 0,
+
+ /**
+ * @private
+ * @type {Number}
+ */
+ step: 1,
+
+ /**
+ * @class Slider
+ * @constructs
+ * @fires change
+ */
+ constructor: function (params) {
+ this.dom = params.element;
+ this.setValue(params.value);
+ this.setMin(params.min);
+ this.setMax(params.max);
+ this.setStep(params.step);
+
+ this.bindEventHandlers();
+ },
+
+ /**
+ * @private
+ */
+ bindEventHandlers: function () {
+ var that = this;
+
+ function pointerMoveEventHandler(event) {
+ if (that.drag) {
+ handleEvent(event, that);
+ syncView(that);
+ event.preventDefault();
+ }
+ }
+
+ function pointerUpEventHandler() {
+ that.drag = false;
+ syncView(that);
+ window.removePointerMoveHandler(document, pointerMoveEventHandler);
+ window.removePointerUpHandler(document, pointerUpEventHandler);
+ }
+
+ window.addPointerDownHandler(this.dom, function (event) {
+ this.drag = true;
+ handleEvent(event, this);
+ syncView(this);
+ window.addPointerMoveHandler(document, pointerMoveEventHandler);
+ window.addPointerUpHandler(document, pointerUpEventHandler);
+ }.bind(this));
+ },
+
+ setValue: function(value) {
+ this.value = value || 0;
+ syncView(this);
+ },
+
+ setMin: function(value) {
+ this.min = value || 0;
+ syncView(this);
+ },
+
+ setMax: function(value) {
+ this.max = value || 100;
+ syncView(this);
+ },
+
+ setStep: function(value) {
+ this.step = value || 1;
+ syncView(this);
+ }
+ });
+});
+
diff --git a/assets/webconfig/js/app/views/TransformView.js b/assets/webconfig/js/app/views/TransformView.js
new file mode 100644
index 00000000..6fe968a6
--- /dev/null
+++ b/assets/webconfig/js/app/views/TransformView.js
@@ -0,0 +1,375 @@
+/**
+ * hyperion remote
+ * MIT License
+ */
+
+define([
+ 'lib/stapes',
+ 'views/Slider'
+], function (Stapes, Slider) {
+ 'use strict';
+
+ function onHeaderClick (event) {
+ var list = event.target.parentNode.parentNode.querySelector('ul');
+
+ if (list.clientHeight === 0) {
+ list.style.maxHeight = list.scrollHeight + 'px';
+ event.target.parentNode.parentNode.setAttribute('collapsed', 'false');
+ } else {
+ list.style.maxHeight = 0;
+ event.target.parentNode.parentNode.setAttribute('collapsed', 'true');
+ }
+ }
+
+ function createLine (id, type, icon, caption, value, min, max) {
+ var dom, el, el2, label, wrapper;
+
+ dom = document.createElement('li');
+ dom.className = type;
+
+ label = document.createElement('label');
+ label.innerHTML = caption;
+ dom.appendChild(label);
+
+ wrapper = document.createElement('div');
+ wrapper.classList.add('wrapper');
+ wrapper.id = id;
+
+ el = document.createElement('div');
+ el.classList.add('icon');
+ el.innerHTML = icon;
+ wrapper.appendChild(el);
+
+ el = document.createElement('div');
+ el.classList.add('slider');
+ el2 = document.createElement('div');
+ el2.classList.add('track');
+ el.appendChild(el2);
+ el2 = document.createElement('div');
+ el2.classList.add('thumb');
+ el.appendChild(el2);
+ el.dataset.min = min;
+ el.dataset.max = max;
+ el.dataset.value = value;
+ el.dataset.step = 0.01;
+ wrapper.appendChild(el);
+
+ el = document.createElement('input');
+ el.classList.add('value');
+ el.type = 'number';
+ el.min = min;
+ el.max = max;
+ el.step = 0.01;
+ el.value = parseFloat(Math.round(value * 100) / 100).toFixed(2);
+ wrapper.appendChild(el);
+
+ dom.appendChild(wrapper);
+
+ return dom;
+ }
+
+ function createGroup (groupInfo) {
+ var group, node, subnode, i, member;
+ group = document.createElement('div');
+ group.classList.add('group');
+ if (groupInfo.collapsed) {
+ group.setAttribute('collapsed', 'true');
+ }
+ group.id = groupInfo.id;
+
+ node = document.createElement('div');
+ node.classList.add('header');
+ group.appendChild(node);
+ subnode = document.createElement('label');
+ subnode.innerHTML = groupInfo.title;
+ node.appendChild(subnode);
+ subnode = document.createElement('label');
+ subnode.innerHTML = groupInfo.subtitle;
+ node.appendChild(subnode);
+
+ node = document.createElement('ul');
+ group.appendChild(node);
+ for (i = 0; i < groupInfo.members.length; i++) {
+ member = groupInfo.members[i];
+ subnode = createLine(member.id, member.type, member.icon, member.label, member.value, member.min,
+ member.max);
+ node.appendChild(subnode);
+ }
+
+ return group;
+ }
+
+ return Stapes.subclass(/** @lends TransformView.prototype */{
+ sliders: {},
+
+ /**
+ * @class TransformView
+ * @constructs
+ */
+ constructor: function () {
+ },
+
+ /**
+ * Clear the list
+ */
+ clear: function () {
+ document.querySelector('#transform .values').innerHTML = '';
+ },
+
+ /**
+ * @private
+ * @param change
+ */
+ onSliderChange: function (event) {
+ var data = {}, idparts, value;
+
+ idparts = event.target.parentNode.id.split('_');
+ value = parseFloat(Math.round(parseFloat(event.value) * 100) / 100);
+
+ event.target.parentNode.querySelector('.value').value = value.toFixed(2);
+
+ data[idparts[1]] = value;
+ this.emit(idparts[0], data);
+ },
+
+ /**
+ * @private
+ * @param change
+ */
+ onValueChange: function (event) {
+ var data = {}, idparts, value;
+
+ idparts = event.target.parentNode.id.split('_');
+ value = parseFloat(Math.round(parseFloat(event.target.value) * 100) / 100);
+
+ if (parseFloat(event.target.value) < parseFloat(event.target.min)) {
+ event.target.value = event.target.min;
+ } else if (parseFloat(event.target.value) > parseFloat(event.target.max)) {
+ event.target.value = event.target.max;
+ }
+ this.sliders[event.target.parentNode.id].setValue(value);
+
+ data[idparts[1]] = value;
+ this.emit(idparts[0], data);
+ },
+
+ /**
+ * fill the list
+ * @param {object} transform - Object containing transform information
+ */
+ fillList: function (transform) {
+ var dom, group, els, i, slider;
+
+ if (!transform) {
+ document.querySelector('#transform .info').classList.remove('hidden');
+ return;
+ }
+
+ dom = document.createDocumentFragment();
+
+ group = createGroup({
+ collapsed: true,
+ id: 'HSV',
+ title: 'HSV',
+ subtitle: 'HSV color corrections',
+ members: [
+ {
+ id: 'hsv_saturationGain',
+ type: 'saturation',
+ icon: '',
+ label: 'Saturation gain',
+ value: transform.saturationGain,
+ min: 0,
+ max: 5
+ },
+ {
+ id: 'hsv_valueGain',
+ type: 'value',
+ icon: '',
+ label: 'Value gain',
+ value: transform.valueGain,
+ min: 0,
+ max: 5
+ }
+ ]
+ });
+ dom.appendChild(group);
+
+ group = createGroup({
+ collapsed: true,
+ id: 'Gamma',
+ title: 'Gamma',
+ subtitle: 'Gamma correction',
+ members: [
+ {
+ id: 'gamma_r',
+ type: 'red',
+ icon: '',
+ label: 'Red',
+ value: transform.gamma[0],
+ min: 0,
+ max: 5
+ },
+ {
+ id: 'gamma_g',
+ type: 'green',
+ icon: '',
+ label: 'Green',
+ value: transform.gamma[1],
+ min: 0,
+ max: 5
+ },
+ {
+ id: 'gamma_b',
+ type: 'blue',
+ icon: '',
+ label: 'Blue',
+ value: transform.gamma[2],
+ min: 0,
+ max: 5
+ }
+ ]
+ });
+ dom.appendChild(group);
+
+ group = createGroup({
+ collapsed: true,
+ id: 'Whitelevel',
+ title: 'Whitelevel',
+ subtitle: 'Value when RGB channel is fully on',
+ members: [
+ {
+ id: 'whitelevel_r',
+ type: 'red',
+ icon: '',
+ label: 'Red',
+ value: transform.whitelevel[0],
+ min: 0,
+ max: 1
+ },
+ {
+ id: 'whitelevel_g',
+ type: 'green',
+ icon: '',
+ label: 'Green',
+ value: transform.whitelevel[1],
+ min: 0,
+ max: 1
+ },
+ {
+ id: 'whitelevel_b',
+ type: 'blue',
+ icon: '',
+ label: 'Blue',
+ value: transform.whitelevel[2],
+ min: 0,
+ max: 1
+ }
+ ]
+ });
+ dom.appendChild(group);
+
+ group = createGroup({
+ collapsed: true,
+ id: 'Blacklevel',
+ title: 'Blacklevel',
+ subtitle: 'Value when RGB channel is fully off',
+ members: [
+ {
+ id: 'blacklevel_r',
+ type: 'red',
+ icon: '',
+ label: 'Red',
+ value: transform.blacklevel[0],
+ min: 0,
+ max: 1
+ },
+ {
+ id: 'blacklevel_g',
+ type: 'green',
+ icon: '',
+ label: 'Green',
+ value: transform.blacklevel[1],
+ min: 0,
+ max: 1
+ },
+ {
+ id: 'blacklevel_b',
+ type: 'blue',
+ icon: '',
+ label: 'Blue',
+ value: transform.blacklevel[2],
+ min: 0,
+ max: 1
+ }
+ ]
+ });
+ dom.appendChild(group);
+
+ group = createGroup({
+ collapsed: true,
+ id: 'Threshold',
+ title: 'Threshold',
+ subtitle: 'Threshold for a channel',
+ members: [
+ {
+ id: 'threshold_r',
+ type: 'red',
+ icon: '',
+ label: 'Red',
+ value: transform.threshold[0],
+ min: 0,
+ max: 1
+ },
+ {
+ id: 'threshold_g',
+ type: 'green',
+ icon: '',
+ label: 'Green',
+ value: transform.threshold[1],
+ min: 0,
+ max: 1
+ },
+ {
+ id: 'threshold_b',
+ type: 'blue',
+ icon: '',
+ label: 'Blue',
+ value: transform.threshold[2],
+ min: 0,
+ max: 1
+ }
+ ]
+ });
+ dom.appendChild(group);
+
+ els = dom.querySelectorAll('.slider');
+ for (i = 0; i < els.length; i++) {
+ slider = new Slider({
+ element: els[i],
+ min: els[i].dataset.min,
+ max: els[i].dataset.max,
+ step: els[i].dataset.step,
+ value: els[i].dataset.value
+ });
+ slider.on('changeValue', this.onSliderChange, this);
+ this.sliders[els[i].parentNode.id] = slider;
+ }
+
+ els = dom.querySelectorAll('input');
+ for (i = 0; i < els.length; i++) {
+ els[i].addEventListener('input', this.onValueChange.bind(this), false);
+ }
+
+ els = dom.querySelectorAll('.header');
+ for (i = 0; i < els.length; i++) {
+ window.addClickHandler(els[i], onHeaderClick);
+ }
+
+ document.querySelector('#transform .info').classList.add('hidden');
+ document.querySelector('#transform .values').appendChild(dom);
+ }
+
+ });
+});
+
diff --git a/assets/webconfig/js/background.js b/assets/webconfig/js/background.js
new file mode 100644
index 00000000..0f68ebb4
--- /dev/null
+++ b/assets/webconfig/js/background.js
@@ -0,0 +1,17 @@
+/*global chrome */
+chrome.app.runtime.onLaunched.addListener(function () {
+ 'use strict';
+ chrome.app.window.create('index.html', {
+ 'id': 'fakeIdForSingleton',
+ 'innerBounds': {
+ 'width': 320,
+ 'height': 480,
+ 'minWidth': 320,
+ 'minHeight': 480
+ },
+ resizable: false
+ });
+});
+
+
+
diff --git a/assets/webconfig/js/vendor/require.js b/assets/webconfig/js/vendor/require.js
new file mode 100644
index 00000000..2ce09b5e
--- /dev/null
+++ b/assets/webconfig/js/vendor/require.js
@@ -0,0 +1,2054 @@
+/** vim: et:ts=4:sw=4:sts=4
+ * @license RequireJS 2.1.9 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved.
+ * Available via the MIT or new BSD license.
+ * see: http://github.com/jrburke/requirejs for details
+ */
+//Not using strict: uneven strict support in browsers, #392, and causes
+//problems with requirejs.exec()/transpiler plugins that may not be strict.
+/*jslint regexp: true, nomen: true, sloppy: true */
+/*global window, navigator, document, importScripts, setTimeout, opera */
+
+var requirejs, require, define;
+(function (global) {
+ var req, s, head, baseElement, dataMain, src,
+ interactiveScript, currentlyAddingScript, mainScript, subPath,
+ version = '2.1.9',
+ commentRegExp = /(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg,
+ cjsRequireRegExp = /[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g,
+ jsSuffixRegExp = /\.js$/,
+ currDirRegExp = /^\.\//,
+ op = Object.prototype,
+ ostring = op.toString,
+ hasOwn = op.hasOwnProperty,
+ ap = Array.prototype,
+ apsp = ap.splice,
+ isBrowser = !!(typeof window !== 'undefined' && typeof navigator !== 'undefined' && window.document),
+ isWebWorker = !isBrowser && typeof importScripts !== 'undefined',
+ //PS3 indicates loaded and complete, but need to wait for complete
+ //specifically. Sequence is 'loading', 'loaded', execution,
+ // then 'complete'. The UA check is unfortunate, but not sure how
+ //to feature test w/o causing perf issues.
+ readyRegExp = isBrowser && navigator.platform === 'PLAYSTATION 3' ?
+ /^complete$/ : /^(complete|loaded)$/,
+ defContextName = '_',
+ //Oh the tragedy, detecting opera. See the usage of isOpera for reason.
+ isOpera = typeof opera !== 'undefined' && opera.toString() === '[object Opera]',
+ contexts = {},
+ cfg = {},
+ globalDefQueue = [],
+ useInteractive = false;
+
+ function isFunction(it) {
+ return ostring.call(it) === '[object Function]';
+ }
+
+ function isArray(it) {
+ return ostring.call(it) === '[object Array]';
+ }
+
+ /**
+ * Helper function for iterating over an array. If the func returns
+ * a true value, it will break out of the loop.
+ */
+ function each(ary, func) {
+ if (ary) {
+ var i;
+ for (i = 0; i < ary.length; i += 1) {
+ if (ary[i] && func(ary[i], i, ary)) {
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Helper function for iterating over an array backwards. If the func
+ * returns a true value, it will break out of the loop.
+ */
+ function eachReverse(ary, func) {
+ if (ary) {
+ var i;
+ for (i = ary.length - 1; i > -1; i -= 1) {
+ if (ary[i] && func(ary[i], i, ary)) {
+ break;
+ }
+ }
+ }
+ }
+
+ function hasProp(obj, prop) {
+ return hasOwn.call(obj, prop);
+ }
+
+ function getOwn(obj, prop) {
+ return hasProp(obj, prop) && obj[prop];
+ }
+
+ /**
+ * Cycles over properties in an object and calls a function for each
+ * property value. If the function returns a truthy value, then the
+ * iteration is stopped.
+ */
+ function eachProp(obj, func) {
+ var prop;
+ for (prop in obj) {
+ if (hasProp(obj, prop)) {
+ if (func(obj[prop], prop)) {
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Simple function to mix in properties from source into target,
+ * but only if target does not already have a property of the same name.
+ */
+ function mixin(target, source, force, deepStringMixin) {
+ if (source) {
+ eachProp(source, function (value, prop) {
+ if (force || !hasProp(target, prop)) {
+ if (deepStringMixin && typeof value !== 'string') {
+ if (!target[prop]) {
+ target[prop] = {};
+ }
+ mixin(target[prop], value, force, deepStringMixin);
+ } else {
+ target[prop] = value;
+ }
+ }
+ });
+ }
+ return target;
+ }
+
+ //Similar to Function.prototype.bind, but the 'this' object is specified
+ //first, since it is easier to read/figure out what 'this' will be.
+ function bind(obj, fn) {
+ return function () {
+ return fn.apply(obj, arguments);
+ };
+ }
+
+ function scripts() {
+ return document.getElementsByTagName('script');
+ }
+
+ function defaultOnError(err) {
+ throw err;
+ }
+
+ //Allow getting a global that expressed in
+ //dot notation, like 'a.b.c'.
+ function getGlobal(value) {
+ if (!value) {
+ return value;
+ }
+ var g = global;
+ each(value.split('.'), function (part) {
+ g = g[part];
+ });
+ return g;
+ }
+
+ /**
+ * Constructs an error with a pointer to an URL with more information.
+ * @param {String} id the error ID that maps to an ID on a web page.
+ * @param {String} message human readable error.
+ * @param {Error} [err] the original error, if there is one.
+ *
+ * @returns {Error}
+ */
+ function makeError(id, msg, err, requireModules) {
+ var e = new Error(msg + '\nhttp://requirejs.org/docs/errors.html#' + id);
+ e.requireType = id;
+ e.requireModules = requireModules;
+ if (err) {
+ e.originalError = err;
+ }
+ return e;
+ }
+
+ if (typeof define !== 'undefined') {
+ //If a define is already in play via another AMD loader,
+ //do not overwrite.
+ return;
+ }
+
+ if (typeof requirejs !== 'undefined') {
+ if (isFunction(requirejs)) {
+ //Do not overwrite and existing requirejs instance.
+ return;
+ }
+ cfg = requirejs;
+ requirejs = undefined;
+ }
+
+ //Allow for a require config object
+ if (typeof require !== 'undefined' && !isFunction(require)) {
+ //assume it is a config object.
+ cfg = require;
+ require = undefined;
+ }
+
+ function newContext(contextName) {
+ var inCheckLoaded, Module, context, handlers,
+ checkLoadedTimeoutId,
+ config = {
+ //Defaults. Do not set a default for map
+ //config to speed up normalize(), which
+ //will run faster if there is no default.
+ waitSeconds: 7,
+ baseUrl: './',
+ paths: {},
+ pkgs: {},
+ shim: {},
+ config: {}
+ },
+ registry = {},
+ //registry of just enabled modules, to speed
+ //cycle breaking code when lots of modules
+ //are registered, but not activated.
+ enabledRegistry = {},
+ undefEvents = {},
+ defQueue = [],
+ defined = {},
+ urlFetched = {},
+ requireCounter = 1,
+ unnormalizedCounter = 1;
+
+ /**
+ * Trims the . and .. from an array of path segments.
+ * It will keep a leading path segment if a .. will become
+ * the first path segment, to help with module name lookups,
+ * which act like paths, but can be remapped. But the end result,
+ * all paths that use this function should look normalized.
+ * NOTE: this method MODIFIES the input array.
+ * @param {Array} ary the array of path segments.
+ */
+ function trimDots(ary) {
+ var i, part;
+ for (i = 0; ary[i]; i += 1) {
+ part = ary[i];
+ if (part === '.') {
+ ary.splice(i, 1);
+ i -= 1;
+ } else if (part === '..') {
+ if (i === 1 && (ary[2] === '..' || ary[0] === '..')) {
+ //End of the line. Keep at least one non-dot
+ //path segment at the front so it can be mapped
+ //correctly to disk. Otherwise, there is likely
+ //no path mapping for a path starting with '..'.
+ //This can still fail, but catches the most reasonable
+ //uses of ..
+ break;
+ } else if (i > 0) {
+ ary.splice(i - 1, 2);
+ i -= 2;
+ }
+ }
+ }
+ }
+
+ /**
+ * Given a relative module name, like ./something, normalize it to
+ * a real name that can be mapped to a path.
+ * @param {String} name the relative name
+ * @param {String} baseName a real name that the name arg is relative
+ * to.
+ * @param {Boolean} applyMap apply the map config to the value. Should
+ * only be done if this normalization is for a dependency ID.
+ * @returns {String} normalized name
+ */
+ function normalize(name, baseName, applyMap) {
+ var pkgName, pkgConfig, mapValue, nameParts, i, j, nameSegment,
+ foundMap, foundI, foundStarMap, starI,
+ baseParts = baseName && baseName.split('/'),
+ normalizedBaseParts = baseParts,
+ map = config.map,
+ starMap = map && map['*'];
+
+ //Adjust any relative paths.
+ if (name && name.charAt(0) === '.') {
+ //If have a base name, try to normalize against it,
+ //otherwise, assume it is a top-level require that will
+ //be relative to baseUrl in the end.
+ if (baseName) {
+ if (getOwn(config.pkgs, baseName)) {
+ //If the baseName is a package name, then just treat it as one
+ //name to concat the name with.
+ normalizedBaseParts = baseParts = [baseName];
+ } else {
+ //Convert baseName to array, and lop off the last part,
+ //so that . matches that 'directory' and not name of the baseName's
+ //module. For instance, baseName of 'one/two/three', maps to
+ //'one/two/three.js', but we want the directory, 'one/two' for
+ //this normalization.
+ normalizedBaseParts = baseParts.slice(0, baseParts.length - 1);
+ }
+
+ name = normalizedBaseParts.concat(name.split('/'));
+ trimDots(name);
+
+ //Some use of packages may use a . path to reference the
+ //'main' module name, so normalize for that.
+ pkgConfig = getOwn(config.pkgs, (pkgName = name[0]));
+ name = name.join('/');
+ if (pkgConfig && name === pkgName + '/' + pkgConfig.main) {
+ name = pkgName;
+ }
+ } else if (name.indexOf('./') === 0) {
+ // No baseName, so this is ID is resolved relative
+ // to baseUrl, pull off the leading dot.
+ name = name.substring(2);
+ }
+ }
+
+ //Apply map config if available.
+ if (applyMap && map && (baseParts || starMap)) {
+ nameParts = name.split('/');
+
+ for (i = nameParts.length; i > 0; i -= 1) {
+ nameSegment = nameParts.slice(0, i).join('/');
+
+ if (baseParts) {
+ //Find the longest baseName segment match in the config.
+ //So, do joins on the biggest to smallest lengths of baseParts.
+ for (j = baseParts.length; j > 0; j -= 1) {
+ mapValue = getOwn(map, baseParts.slice(0, j).join('/'));
+
+ //baseName segment has config, find if it has one for
+ //this name.
+ if (mapValue) {
+ mapValue = getOwn(mapValue, nameSegment);
+ if (mapValue) {
+ //Match, update name to the new value.
+ foundMap = mapValue;
+ foundI = i;
+ break;
+ }
+ }
+ }
+ }
+
+ if (foundMap) {
+ break;
+ }
+
+ //Check for a star map match, but just hold on to it,
+ //if there is a shorter segment match later in a matching
+ //config, then favor over this star map.
+ if (!foundStarMap && starMap && getOwn(starMap, nameSegment)) {
+ foundStarMap = getOwn(starMap, nameSegment);
+ starI = i;
+ }
+ }
+
+ if (!foundMap && foundStarMap) {
+ foundMap = foundStarMap;
+ foundI = starI;
+ }
+
+ if (foundMap) {
+ nameParts.splice(0, foundI, foundMap);
+ name = nameParts.join('/');
+ }
+ }
+
+ return name;
+ }
+
+ function removeScript(name) {
+ if (isBrowser) {
+ each(scripts(), function (scriptNode) {
+ if (scriptNode.getAttribute('data-requiremodule') === name &&
+ scriptNode.getAttribute('data-requirecontext') === context.contextName) {
+ scriptNode.parentNode.removeChild(scriptNode);
+ return true;
+ }
+ });
+ }
+ }
+
+ function hasPathFallback(id) {
+ var pathConfig = getOwn(config.paths, id);
+ if (pathConfig && isArray(pathConfig) && pathConfig.length > 1) {
+ //Pop off the first array value, since it failed, and
+ //retry
+ pathConfig.shift();
+ context.require.undef(id);
+ context.require([id]);
+ return true;
+ }
+ }
+
+ //Turns a plugin!resource to [plugin, resource]
+ //with the plugin being undefined if the name
+ //did not have a plugin prefix.
+ function splitPrefix(name) {
+ var prefix,
+ index = name ? name.indexOf('!') : -1;
+ if (index > -1) {
+ prefix = name.substring(0, index);
+ name = name.substring(index + 1, name.length);
+ }
+ return [prefix, name];
+ }
+
+ /**
+ * Creates a module mapping that includes plugin prefix, module
+ * name, and path. If parentModuleMap is provided it will
+ * also normalize the name via require.normalize()
+ *
+ * @param {String} name the module name
+ * @param {String} [parentModuleMap] parent module map
+ * for the module name, used to resolve relative names.
+ * @param {Boolean} isNormalized: is the ID already normalized.
+ * This is true if this call is done for a define() module ID.
+ * @param {Boolean} applyMap: apply the map config to the ID.
+ * Should only be true if this map is for a dependency.
+ *
+ * @returns {Object}
+ */
+ function makeModuleMap(name, parentModuleMap, isNormalized, applyMap) {
+ var url, pluginModule, suffix, nameParts,
+ prefix = null,
+ parentName = parentModuleMap ? parentModuleMap.name : null,
+ originalName = name,
+ isDefine = true,
+ normalizedName = '';
+
+ //If no name, then it means it is a require call, generate an
+ //internal name.
+ if (!name) {
+ isDefine = false;
+ name = '_@r' + (requireCounter += 1);
+ }
+
+ nameParts = splitPrefix(name);
+ prefix = nameParts[0];
+ name = nameParts[1];
+
+ if (prefix) {
+ prefix = normalize(prefix, parentName, applyMap);
+ pluginModule = getOwn(defined, prefix);
+ }
+
+ //Account for relative paths if there is a base name.
+ if (name) {
+ if (prefix) {
+ if (pluginModule && pluginModule.normalize) {
+ //Plugin is loaded, use its normalize method.
+ normalizedName = pluginModule.normalize(name, function (name) {
+ return normalize(name, parentName, applyMap);
+ });
+ } else {
+ normalizedName = normalize(name, parentName, applyMap);
+ }
+ } else {
+ //A regular module.
+ normalizedName = normalize(name, parentName, applyMap);
+
+ //Normalized name may be a plugin ID due to map config
+ //application in normalize. The map config values must
+ //already be normalized, so do not need to redo that part.
+ nameParts = splitPrefix(normalizedName);
+ prefix = nameParts[0];
+ normalizedName = nameParts[1];
+ isNormalized = true;
+
+ url = context.nameToUrl(normalizedName);
+ }
+ }
+
+ //If the id is a plugin id that cannot be determined if it needs
+ //normalization, stamp it with a unique ID so two matching relative
+ //ids that may conflict can be separate.
+ suffix = prefix && !pluginModule && !isNormalized ?
+ '_unnormalized' + (unnormalizedCounter += 1) :
+ '';
+
+ return {
+ prefix: prefix,
+ name: normalizedName,
+ parentMap: parentModuleMap,
+ unnormalized: !!suffix,
+ url: url,
+ originalName: originalName,
+ isDefine: isDefine,
+ id: (prefix ?
+ prefix + '!' + normalizedName :
+ normalizedName) + suffix
+ };
+ }
+
+ function getModule(depMap) {
+ var id = depMap.id,
+ mod = getOwn(registry, id);
+
+ if (!mod) {
+ mod = registry[id] = new context.Module(depMap);
+ }
+
+ return mod;
+ }
+
+ function on(depMap, name, fn) {
+ var id = depMap.id,
+ mod = getOwn(registry, id);
+
+ if (hasProp(defined, id) &&
+ (!mod || mod.defineEmitComplete)) {
+ if (name === 'defined') {
+ fn(defined[id]);
+ }
+ } else {
+ mod = getModule(depMap);
+ if (mod.error && name === 'error') {
+ fn(mod.error);
+ } else {
+ mod.on(name, fn);
+ }
+ }
+ }
+
+ function onError(err, errback) {
+ var ids = err.requireModules,
+ notified = false;
+
+ if (errback) {
+ errback(err);
+ } else {
+ each(ids, function (id) {
+ var mod = getOwn(registry, id);
+ if (mod) {
+ //Set error on module, so it skips timeout checks.
+ mod.error = err;
+ if (mod.events.error) {
+ notified = true;
+ mod.emit('error', err);
+ }
+ }
+ });
+
+ if (!notified) {
+ req.onError(err);
+ }
+ }
+ }
+
+ /**
+ * Internal method to transfer globalQueue items to this context's
+ * defQueue.
+ */
+ function takeGlobalQueue() {
+ //Push all the globalDefQueue items into the context's defQueue
+ if (globalDefQueue.length) {
+ //Array splice in the values since the context code has a
+ //local var ref to defQueue, so cannot just reassign the one
+ //on context.
+ apsp.apply(defQueue,
+ [defQueue.length - 1, 0].concat(globalDefQueue));
+ globalDefQueue = [];
+ }
+ }
+
+ handlers = {
+ 'require': function (mod) {
+ if (mod.require) {
+ return mod.require;
+ } else {
+ return (mod.require = context.makeRequire(mod.map));
+ }
+ },
+ 'exports': function (mod) {
+ mod.usingExports = true;
+ if (mod.map.isDefine) {
+ if (mod.exports) {
+ return mod.exports;
+ } else {
+ return (mod.exports = defined[mod.map.id] = {});
+ }
+ }
+ },
+ 'module': function (mod) {
+ if (mod.module) {
+ return mod.module;
+ } else {
+ return (mod.module = {
+ id: mod.map.id,
+ uri: mod.map.url,
+ config: function () {
+ var c,
+ pkg = getOwn(config.pkgs, mod.map.id);
+ // For packages, only support config targeted
+ // at the main module.
+ c = pkg ? getOwn(config.config, mod.map.id + '/' + pkg.main) :
+ getOwn(config.config, mod.map.id);
+ return c || {};
+ },
+ exports: defined[mod.map.id]
+ });
+ }
+ }
+ };
+
+ function cleanRegistry(id) {
+ //Clean up machinery used for waiting modules.
+ delete registry[id];
+ delete enabledRegistry[id];
+ }
+
+ function breakCycle(mod, traced, processed) {
+ var id = mod.map.id;
+
+ if (mod.error) {
+ mod.emit('error', mod.error);
+ } else {
+ traced[id] = true;
+ each(mod.depMaps, function (depMap, i) {
+ var depId = depMap.id,
+ dep = getOwn(registry, depId);
+
+ //Only force things that have not completed
+ //being defined, so still in the registry,
+ //and only if it has not been matched up
+ //in the module already.
+ if (dep && !mod.depMatched[i] && !processed[depId]) {
+ if (getOwn(traced, depId)) {
+ mod.defineDep(i, defined[depId]);
+ mod.check(); //pass false?
+ } else {
+ breakCycle(dep, traced, processed);
+ }
+ }
+ });
+ processed[id] = true;
+ }
+ }
+
+ function checkLoaded() {
+ var map, modId, err, usingPathFallback,
+ waitInterval = config.waitSeconds * 1000,
+ //It is possible to disable the wait interval by using waitSeconds of 0.
+ expired = waitInterval && (context.startTime + waitInterval) < new Date().getTime(),
+ noLoads = [],
+ reqCalls = [],
+ stillLoading = false,
+ needCycleCheck = true;
+
+ //Do not bother if this call was a result of a cycle break.
+ if (inCheckLoaded) {
+ return;
+ }
+
+ inCheckLoaded = true;
+
+ //Figure out the state of all the modules.
+ eachProp(enabledRegistry, function (mod) {
+ map = mod.map;
+ modId = map.id;
+
+ //Skip things that are not enabled or in error state.
+ if (!mod.enabled) {
+ return;
+ }
+
+ if (!map.isDefine) {
+ reqCalls.push(mod);
+ }
+
+ if (!mod.error) {
+ //If the module should be executed, and it has not
+ //been inited and time is up, remember it.
+ if (!mod.inited && expired) {
+ if (hasPathFallback(modId)) {
+ usingPathFallback = true;
+ stillLoading = true;
+ } else {
+ noLoads.push(modId);
+ removeScript(modId);
+ }
+ } else if (!mod.inited && mod.fetched && map.isDefine) {
+ stillLoading = true;
+ if (!map.prefix) {
+ //No reason to keep looking for unfinished
+ //loading. If the only stillLoading is a
+ //plugin resource though, keep going,
+ //because it may be that a plugin resource
+ //is waiting on a non-plugin cycle.
+ return (needCycleCheck = false);
+ }
+ }
+ }
+ });
+
+ if (expired && noLoads.length) {
+ //If wait time expired, throw error of unloaded modules.
+ err = makeError('timeout', 'Load timeout for modules: ' + noLoads, null, noLoads);
+ err.contextName = context.contextName;
+ return onError(err);
+ }
+
+ //Not expired, check for a cycle.
+ if (needCycleCheck) {
+ each(reqCalls, function (mod) {
+ breakCycle(mod, {}, {});
+ });
+ }
+
+ //If still waiting on loads, and the waiting load is something
+ //other than a plugin resource, or there are still outstanding
+ //scripts, then just try back later.
+ if ((!expired || usingPathFallback) && stillLoading) {
+ //Something is still waiting to load. Wait for it, but only
+ //if a timeout is not already in effect.
+ if ((isBrowser || isWebWorker) && !checkLoadedTimeoutId) {
+ checkLoadedTimeoutId = setTimeout(function () {
+ checkLoadedTimeoutId = 0;
+ checkLoaded();
+ }, 50);
+ }
+ }
+
+ inCheckLoaded = false;
+ }
+
+ Module = function (map) {
+ this.events = getOwn(undefEvents, map.id) || {};
+ this.map = map;
+ this.shim = getOwn(config.shim, map.id);
+ this.depExports = [];
+ this.depMaps = [];
+ this.depMatched = [];
+ this.pluginMaps = {};
+ this.depCount = 0;
+
+ /* this.exports this.factory
+ this.depMaps = [],
+ this.enabled, this.fetched
+ */
+ };
+
+ Module.prototype = {
+ init: function (depMaps, factory, errback, options) {
+ options = options || {};
+
+ //Do not do more inits if already done. Can happen if there
+ //are multiple define calls for the same module. That is not
+ //a normal, common case, but it is also not unexpected.
+ if (this.inited) {
+ return;
+ }
+
+ this.factory = factory;
+
+ if (errback) {
+ //Register for errors on this module.
+ this.on('error', errback);
+ } else if (this.events.error) {
+ //If no errback already, but there are error listeners
+ //on this module, set up an errback to pass to the deps.
+ errback = bind(this, function (err) {
+ this.emit('error', err);
+ });
+ }
+
+ //Do a copy of the dependency array, so that
+ //source inputs are not modified. For example
+ //"shim" deps are passed in here directly, and
+ //doing a direct modification of the depMaps array
+ //would affect that config.
+ this.depMaps = depMaps && depMaps.slice(0);
+
+ this.errback = errback;
+
+ //Indicate this module has be initialized
+ this.inited = true;
+
+ this.ignore = options.ignore;
+
+ //Could have option to init this module in enabled mode,
+ //or could have been previously marked as enabled. However,
+ //the dependencies are not known until init is called. So
+ //if enabled previously, now trigger dependencies as enabled.
+ if (options.enabled || this.enabled) {
+ //Enable this module and dependencies.
+ //Will call this.check()
+ this.enable();
+ } else {
+ this.check();
+ }
+ },
+
+ defineDep: function (i, depExports) {
+ //Because of cycles, defined callback for a given
+ //export can be called more than once.
+ if (!this.depMatched[i]) {
+ this.depMatched[i] = true;
+ this.depCount -= 1;
+ this.depExports[i] = depExports;
+ }
+ },
+
+ fetch: function () {
+ if (this.fetched) {
+ return;
+ }
+ this.fetched = true;
+
+ context.startTime = (new Date()).getTime();
+
+ var map = this.map;
+
+ //If the manager is for a plugin managed resource,
+ //ask the plugin to load it now.
+ if (this.shim) {
+ context.makeRequire(this.map, {
+ enableBuildCallback: true
+ })(this.shim.deps || [], bind(this, function () {
+ return map.prefix ? this.callPlugin() : this.load();
+ }));
+ } else {
+ //Regular dependency.
+ return map.prefix ? this.callPlugin() : this.load();
+ }
+ },
+
+ load: function () {
+ var url = this.map.url;
+
+ //Regular dependency.
+ if (!urlFetched[url]) {
+ urlFetched[url] = true;
+ context.load(this.map.id, url);
+ }
+ },
+
+ /**
+ * Checks if the module is ready to define itself, and if so,
+ * define it.
+ */
+ check: function () {
+ if (!this.enabled || this.enabling) {
+ return;
+ }
+
+ var err, cjsModule,
+ id = this.map.id,
+ depExports = this.depExports,
+ exports = this.exports,
+ factory = this.factory;
+
+ if (!this.inited) {
+ this.fetch();
+ } else if (this.error) {
+ this.emit('error', this.error);
+ } else if (!this.defining) {
+ //The factory could trigger another require call
+ //that would result in checking this module to
+ //define itself again. If already in the process
+ //of doing that, skip this work.
+ this.defining = true;
+
+ if (this.depCount < 1 && !this.defined) {
+ if (isFunction(factory)) {
+ //If there is an error listener, favor passing
+ //to that instead of throwing an error. However,
+ //only do it for define()'d modules. require
+ //errbacks should not be called for failures in
+ //their callbacks (#699). However if a global
+ //onError is set, use that.
+ if ((this.events.error && this.map.isDefine) ||
+ req.onError !== defaultOnError) {
+ try {
+ exports = context.execCb(id, factory, depExports, exports);
+ } catch (e) {
+ err = e;
+ }
+ } else {
+ exports = context.execCb(id, factory, depExports, exports);
+ }
+
+ if (this.map.isDefine) {
+ //If setting exports via 'module' is in play,
+ //favor that over return value and exports. After that,
+ //favor a non-undefined return value over exports use.
+ cjsModule = this.module;
+ if (cjsModule &&
+ cjsModule.exports !== undefined &&
+ //Make sure it is not already the exports value
+ cjsModule.exports !== this.exports) {
+ exports = cjsModule.exports;
+ } else if (exports === undefined && this.usingExports) {
+ //exports already set the defined value.
+ exports = this.exports;
+ }
+ }
+
+ if (err) {
+ err.requireMap = this.map;
+ err.requireModules = this.map.isDefine ? [this.map.id] : null;
+ err.requireType = this.map.isDefine ? 'define' : 'require';
+ return onError((this.error = err));
+ }
+
+ } else {
+ //Just a literal value
+ exports = factory;
+ }
+
+ this.exports = exports;
+
+ if (this.map.isDefine && !this.ignore) {
+ defined[id] = exports;
+
+ if (req.onResourceLoad) {
+ req.onResourceLoad(context, this.map, this.depMaps);
+ }
+ }
+
+ //Clean up
+ cleanRegistry(id);
+
+ this.defined = true;
+ }
+
+ //Finished the define stage. Allow calling check again
+ //to allow define notifications below in the case of a
+ //cycle.
+ this.defining = false;
+
+ if (this.defined && !this.defineEmitted) {
+ this.defineEmitted = true;
+ this.emit('defined', this.exports);
+ this.defineEmitComplete = true;
+ }
+
+ }
+ },
+
+ callPlugin: function () {
+ var map = this.map,
+ id = map.id,
+ //Map already normalized the prefix.
+ pluginMap = makeModuleMap(map.prefix);
+
+ //Mark this as a dependency for this plugin, so it
+ //can be traced for cycles.
+ this.depMaps.push(pluginMap);
+
+ on(pluginMap, 'defined', bind(this, function (plugin) {
+ var load, normalizedMap, normalizedMod,
+ name = this.map.name,
+ parentName = this.map.parentMap ? this.map.parentMap.name : null,
+ localRequire = context.makeRequire(map.parentMap, {
+ enableBuildCallback: true
+ });
+
+ //If current map is not normalized, wait for that
+ //normalized name to load instead of continuing.
+ if (this.map.unnormalized) {
+ //Normalize the ID if the plugin allows it.
+ if (plugin.normalize) {
+ name = plugin.normalize(name, function (name) {
+ return normalize(name, parentName, true);
+ }) || '';
+ }
+
+ //prefix and name should already be normalized, no need
+ //for applying map config again either.
+ normalizedMap = makeModuleMap(map.prefix + '!' + name,
+ this.map.parentMap);
+ on(normalizedMap,
+ 'defined', bind(this, function (value) {
+ this.init([], function () { return value; }, null, {
+ enabled: true,
+ ignore: true
+ });
+ }));
+
+ normalizedMod = getOwn(registry, normalizedMap.id);
+ if (normalizedMod) {
+ //Mark this as a dependency for this plugin, so it
+ //can be traced for cycles.
+ this.depMaps.push(normalizedMap);
+
+ if (this.events.error) {
+ normalizedMod.on('error', bind(this, function (err) {
+ this.emit('error', err);
+ }));
+ }
+ normalizedMod.enable();
+ }
+
+ return;
+ }
+
+ load = bind(this, function (value) {
+ this.init([], function () { return value; }, null, {
+ enabled: true
+ });
+ });
+
+ load.error = bind(this, function (err) {
+ this.inited = true;
+ this.error = err;
+ err.requireModules = [id];
+
+ //Remove temp unnormalized modules for this module,
+ //since they will never be resolved otherwise now.
+ eachProp(registry, function (mod) {
+ if (mod.map.id.indexOf(id + '_unnormalized') === 0) {
+ cleanRegistry(mod.map.id);
+ }
+ });
+
+ onError(err);
+ });
+
+ //Allow plugins to load other code without having to know the
+ //context or how to 'complete' the load.
+ load.fromText = bind(this, function (text, textAlt) {
+ /*jslint evil: true */
+ var moduleName = map.name,
+ moduleMap = makeModuleMap(moduleName),
+ hasInteractive = useInteractive;
+
+ //As of 2.1.0, support just passing the text, to reinforce
+ //fromText only being called once per resource. Still
+ //support old style of passing moduleName but discard
+ //that moduleName in favor of the internal ref.
+ if (textAlt) {
+ text = textAlt;
+ }
+
+ //Turn off interactive script matching for IE for any define
+ //calls in the text, then turn it back on at the end.
+ if (hasInteractive) {
+ useInteractive = false;
+ }
+
+ //Prime the system by creating a module instance for
+ //it.
+ getModule(moduleMap);
+
+ //Transfer any config to this other module.
+ if (hasProp(config.config, id)) {
+ config.config[moduleName] = config.config[id];
+ }
+
+ try {
+ req.exec(text);
+ } catch (e) {
+ return onError(makeError('fromtexteval',
+ 'fromText eval for ' + id +
+ ' failed: ' + e,
+ e,
+ [id]));
+ }
+
+ if (hasInteractive) {
+ useInteractive = true;
+ }
+
+ //Mark this as a dependency for the plugin
+ //resource
+ this.depMaps.push(moduleMap);
+
+ //Support anonymous modules.
+ context.completeLoad(moduleName);
+
+ //Bind the value of that module to the value for this
+ //resource ID.
+ localRequire([moduleName], load);
+ });
+
+ //Use parentName here since the plugin's name is not reliable,
+ //could be some weird string with no path that actually wants to
+ //reference the parentName's path.
+ plugin.load(map.name, localRequire, load, config);
+ }));
+
+ context.enable(pluginMap, this);
+ this.pluginMaps[pluginMap.id] = pluginMap;
+ },
+
+ enable: function () {
+ enabledRegistry[this.map.id] = this;
+ this.enabled = true;
+
+ //Set flag mentioning that the module is enabling,
+ //so that immediate calls to the defined callbacks
+ //for dependencies do not trigger inadvertent load
+ //with the depCount still being zero.
+ this.enabling = true;
+
+ //Enable each dependency
+ each(this.depMaps, bind(this, function (depMap, i) {
+ var id, mod, handler;
+
+ if (typeof depMap === 'string') {
+ //Dependency needs to be converted to a depMap
+ //and wired up to this module.
+ depMap = makeModuleMap(depMap,
+ (this.map.isDefine ? this.map : this.map.parentMap),
+ false,
+ !this.skipMap);
+ this.depMaps[i] = depMap;
+
+ handler = getOwn(handlers, depMap.id);
+
+ if (handler) {
+ this.depExports[i] = handler(this);
+ return;
+ }
+
+ this.depCount += 1;
+
+ on(depMap, 'defined', bind(this, function (depExports) {
+ this.defineDep(i, depExports);
+ this.check();
+ }));
+
+ if (this.errback) {
+ on(depMap, 'error', bind(this, this.errback));
+ }
+ }
+
+ id = depMap.id;
+ mod = registry[id];
+
+ //Skip special modules like 'require', 'exports', 'module'
+ //Also, don't call enable if it is already enabled,
+ //important in circular dependency cases.
+ if (!hasProp(handlers, id) && mod && !mod.enabled) {
+ context.enable(depMap, this);
+ }
+ }));
+
+ //Enable each plugin that is used in
+ //a dependency
+ eachProp(this.pluginMaps, bind(this, function (pluginMap) {
+ var mod = getOwn(registry, pluginMap.id);
+ if (mod && !mod.enabled) {
+ context.enable(pluginMap, this);
+ }
+ }));
+
+ this.enabling = false;
+
+ this.check();
+ },
+
+ on: function (name, cb) {
+ var cbs = this.events[name];
+ if (!cbs) {
+ cbs = this.events[name] = [];
+ }
+ cbs.push(cb);
+ },
+
+ emit: function (name, evt) {
+ each(this.events[name], function (cb) {
+ cb(evt);
+ });
+ if (name === 'error') {
+ //Now that the error handler was triggered, remove
+ //the listeners, since this broken Module instance
+ //can stay around for a while in the registry.
+ delete this.events[name];
+ }
+ }
+ };
+
+ function callGetModule(args) {
+ //Skip modules already defined.
+ if (!hasProp(defined, args[0])) {
+ getModule(makeModuleMap(args[0], null, true)).init(args[1], args[2]);
+ }
+ }
+
+ function removeListener(node, func, name, ieName) {
+ //Favor detachEvent because of IE9
+ //issue, see attachEvent/addEventListener comment elsewhere
+ //in this file.
+ if (node.detachEvent && !isOpera) {
+ //Probably IE. If not it will throw an error, which will be
+ //useful to know.
+ if (ieName) {
+ node.detachEvent(ieName, func);
+ }
+ } else {
+ node.removeEventListener(name, func, false);
+ }
+ }
+
+ /**
+ * Given an event from a script node, get the requirejs info from it,
+ * and then removes the event listeners on the node.
+ * @param {Event} evt
+ * @returns {Object}
+ */
+ function getScriptData(evt) {
+ //Using currentTarget instead of target for Firefox 2.0's sake. Not
+ //all old browsers will be supported, but this one was easy enough
+ //to support and still makes sense.
+ var node = evt.currentTarget || evt.srcElement;
+
+ //Remove the listeners once here.
+ removeListener(node, context.onScriptLoad, 'load', 'onreadystatechange');
+ removeListener(node, context.onScriptError, 'error');
+
+ return {
+ node: node,
+ id: node && node.getAttribute('data-requiremodule')
+ };
+ }
+
+ function intakeDefines() {
+ var args;
+
+ //Any defined modules in the global queue, intake them now.
+ takeGlobalQueue();
+
+ //Make sure any remaining defQueue items get properly processed.
+ while (defQueue.length) {
+ args = defQueue.shift();
+ if (args[0] === null) {
+ return onError(makeError('mismatch', 'Mismatched anonymous define() module: ' + args[args.length - 1]));
+ } else {
+ //args are id, deps, factory. Should be normalized by the
+ //define() function.
+ callGetModule(args);
+ }
+ }
+ }
+
+ context = {
+ config: config,
+ contextName: contextName,
+ registry: registry,
+ defined: defined,
+ urlFetched: urlFetched,
+ defQueue: defQueue,
+ Module: Module,
+ makeModuleMap: makeModuleMap,
+ nextTick: req.nextTick,
+ onError: onError,
+
+ /**
+ * Set a configuration for the context.
+ * @param {Object} cfg config object to integrate.
+ */
+ configure: function (cfg) {
+ //Make sure the baseUrl ends in a slash.
+ if (cfg.baseUrl) {
+ if (cfg.baseUrl.charAt(cfg.baseUrl.length - 1) !== '/') {
+ cfg.baseUrl += '/';
+ }
+ }
+
+ //Save off the paths and packages since they require special processing,
+ //they are additive.
+ var pkgs = config.pkgs,
+ shim = config.shim,
+ objs = {
+ paths: true,
+ config: true,
+ map: true
+ };
+
+ eachProp(cfg, function (value, prop) {
+ if (objs[prop]) {
+ if (prop === 'map') {
+ if (!config.map) {
+ config.map = {};
+ }
+ mixin(config[prop], value, true, true);
+ } else {
+ mixin(config[prop], value, true);
+ }
+ } else {
+ config[prop] = value;
+ }
+ });
+
+ //Merge shim
+ if (cfg.shim) {
+ eachProp(cfg.shim, function (value, id) {
+ //Normalize the structure
+ if (isArray(value)) {
+ value = {
+ deps: value
+ };
+ }
+ if ((value.exports || value.init) && !value.exportsFn) {
+ value.exportsFn = context.makeShimExports(value);
+ }
+ shim[id] = value;
+ });
+ config.shim = shim;
+ }
+
+ //Adjust packages if necessary.
+ if (cfg.packages) {
+ each(cfg.packages, function (pkgObj) {
+ var location;
+
+ pkgObj = typeof pkgObj === 'string' ? { name: pkgObj } : pkgObj;
+ location = pkgObj.location;
+
+ //Create a brand new object on pkgs, since currentPackages can
+ //be passed in again, and config.pkgs is the internal transformed
+ //state for all package configs.
+ pkgs[pkgObj.name] = {
+ name: pkgObj.name,
+ location: location || pkgObj.name,
+ //Remove leading dot in main, so main paths are normalized,
+ //and remove any trailing .js, since different package
+ //envs have different conventions: some use a module name,
+ //some use a file name.
+ main: (pkgObj.main || 'main')
+ .replace(currDirRegExp, '')
+ .replace(jsSuffixRegExp, '')
+ };
+ });
+
+ //Done with modifications, assing packages back to context config
+ config.pkgs = pkgs;
+ }
+
+ //If there are any "waiting to execute" modules in the registry,
+ //update the maps for them, since their info, like URLs to load,
+ //may have changed.
+ eachProp(registry, function (mod, id) {
+ //If module already has init called, since it is too
+ //late to modify them, and ignore unnormalized ones
+ //since they are transient.
+ if (!mod.inited && !mod.map.unnormalized) {
+ mod.map = makeModuleMap(id);
+ }
+ });
+
+ //If a deps array or a config callback is specified, then call
+ //require with those args. This is useful when require is defined as a
+ //config object before require.js is loaded.
+ if (cfg.deps || cfg.callback) {
+ context.require(cfg.deps || [], cfg.callback);
+ }
+ },
+
+ makeShimExports: function (value) {
+ function fn() {
+ var ret;
+ if (value.init) {
+ ret = value.init.apply(global, arguments);
+ }
+ return ret || (value.exports && getGlobal(value.exports));
+ }
+ return fn;
+ },
+
+ makeRequire: function (relMap, options) {
+ options = options || {};
+
+ function localRequire(deps, callback, errback) {
+ var id, map, requireMod;
+
+ if (options.enableBuildCallback && callback && isFunction(callback)) {
+ callback.__requireJsBuild = true;
+ }
+
+ if (typeof deps === 'string') {
+ if (isFunction(callback)) {
+ //Invalid call
+ return onError(makeError('requireargs', 'Invalid require call'), errback);
+ }
+
+ //If require|exports|module are requested, get the
+ //value for them from the special handlers. Caveat:
+ //this only works while module is being defined.
+ if (relMap && hasProp(handlers, deps)) {
+ return handlers[deps](registry[relMap.id]);
+ }
+
+ //Synchronous access to one module. If require.get is
+ //available (as in the Node adapter), prefer that.
+ if (req.get) {
+ return req.get(context, deps, relMap, localRequire);
+ }
+
+ //Normalize module name, if it contains . or ..
+ map = makeModuleMap(deps, relMap, false, true);
+ id = map.id;
+
+ if (!hasProp(defined, id)) {
+ return onError(makeError('notloaded', 'Module name "' +
+ id +
+ '" has not been loaded yet for context: ' +
+ contextName +
+ (relMap ? '' : '. Use require([])')));
+ }
+ return defined[id];
+ }
+
+ //Grab defines waiting in the global queue.
+ intakeDefines();
+
+ //Mark all the dependencies as needing to be loaded.
+ context.nextTick(function () {
+ //Some defines could have been added since the
+ //require call, collect them.
+ intakeDefines();
+
+ requireMod = getModule(makeModuleMap(null, relMap));
+
+ //Store if map config should be applied to this require
+ //call for dependencies.
+ requireMod.skipMap = options.skipMap;
+
+ requireMod.init(deps, callback, errback, {
+ enabled: true
+ });
+
+ checkLoaded();
+ });
+
+ return localRequire;
+ }
+
+ mixin(localRequire, {
+ isBrowser: isBrowser,
+
+ /**
+ * Converts a module name + .extension into an URL path.
+ * *Requires* the use of a module name. It does not support using
+ * plain URLs like nameToUrl.
+ */
+ toUrl: function (moduleNamePlusExt) {
+ var ext,
+ index = moduleNamePlusExt.lastIndexOf('.'),
+ segment = moduleNamePlusExt.split('/')[0],
+ isRelative = segment === '.' || segment === '..';
+
+ //Have a file extension alias, and it is not the
+ //dots from a relative path.
+ if (index !== -1 && (!isRelative || index > 1)) {
+ ext = moduleNamePlusExt.substring(index, moduleNamePlusExt.length);
+ moduleNamePlusExt = moduleNamePlusExt.substring(0, index);
+ }
+
+ return context.nameToUrl(normalize(moduleNamePlusExt,
+ relMap && relMap.id, true), ext, true);
+ },
+
+ defined: function (id) {
+ return hasProp(defined, makeModuleMap(id, relMap, false, true).id);
+ },
+
+ specified: function (id) {
+ id = makeModuleMap(id, relMap, false, true).id;
+ return hasProp(defined, id) || hasProp(registry, id);
+ }
+ });
+
+ //Only allow undef on top level require calls
+ if (!relMap) {
+ localRequire.undef = function (id) {
+ //Bind any waiting define() calls to this context,
+ //fix for #408
+ takeGlobalQueue();
+
+ var map = makeModuleMap(id, relMap, true),
+ mod = getOwn(registry, id);
+
+ removeScript(id);
+
+ delete defined[id];
+ delete urlFetched[map.url];
+ delete undefEvents[id];
+
+ if (mod) {
+ //Hold on to listeners in case the
+ //module will be attempted to be reloaded
+ //using a different config.
+ if (mod.events.defined) {
+ undefEvents[id] = mod.events;
+ }
+
+ cleanRegistry(id);
+ }
+ };
+ }
+
+ return localRequire;
+ },
+
+ /**
+ * Called to enable a module if it is still in the registry
+ * awaiting enablement. A second arg, parent, the parent module,
+ * is passed in for context, when this method is overriden by
+ * the optimizer. Not shown here to keep code compact.
+ */
+ enable: function (depMap) {
+ var mod = getOwn(registry, depMap.id);
+ if (mod) {
+ getModule(depMap).enable();
+ }
+ },
+
+ /**
+ * Internal method used by environment adapters to complete a load event.
+ * A load event could be a script load or just a load pass from a synchronous
+ * load call.
+ * @param {String} moduleName the name of the module to potentially complete.
+ */
+ completeLoad: function (moduleName) {
+ var found, args, mod,
+ shim = getOwn(config.shim, moduleName) || {},
+ shExports = shim.exports;
+
+ takeGlobalQueue();
+
+ while (defQueue.length) {
+ args = defQueue.shift();
+ if (args[0] === null) {
+ args[0] = moduleName;
+ //If already found an anonymous module and bound it
+ //to this name, then this is some other anon module
+ //waiting for its completeLoad to fire.
+ if (found) {
+ break;
+ }
+ found = true;
+ } else if (args[0] === moduleName) {
+ //Found matching define call for this script!
+ found = true;
+ }
+
+ callGetModule(args);
+ }
+
+ //Do this after the cycle of callGetModule in case the result
+ //of those calls/init calls changes the registry.
+ mod = getOwn(registry, moduleName);
+
+ if (!found && !hasProp(defined, moduleName) && mod && !mod.inited) {
+ if (config.enforceDefine && (!shExports || !getGlobal(shExports))) {
+ if (hasPathFallback(moduleName)) {
+ return;
+ } else {
+ return onError(makeError('nodefine',
+ 'No define call for ' + moduleName,
+ null,
+ [moduleName]));
+ }
+ } else {
+ //A script that does not call define(), so just simulate
+ //the call for it.
+ callGetModule([moduleName, (shim.deps || []), shim.exportsFn]);
+ }
+ }
+
+ checkLoaded();
+ },
+
+ /**
+ * Converts a module name to a file path. Supports cases where
+ * moduleName may actually be just an URL.
+ * Note that it **does not** call normalize on the moduleName,
+ * it is assumed to have already been normalized. This is an
+ * internal API, not a public one. Use toUrl for the public API.
+ */
+ nameToUrl: function (moduleName, ext, skipExt) {
+ var paths, pkgs, pkg, pkgPath, syms, i, parentModule, url,
+ parentPath;
+
+ //If a colon is in the URL, it indicates a protocol is used and it is just
+ //an URL to a file, or if it starts with a slash, contains a query arg (i.e. ?)
+ //or ends with .js, then assume the user meant to use an url and not a module id.
+ //The slash is important for protocol-less URLs as well as full paths.
+ if (req.jsExtRegExp.test(moduleName)) {
+ //Just a plain path, not module name lookup, so just return it.
+ //Add extension if it is included. This is a bit wonky, only non-.js things pass
+ //an extension, this method probably needs to be reworked.
+ url = moduleName + (ext || '');
+ } else {
+ //A module that needs to be converted to a path.
+ paths = config.paths;
+ pkgs = config.pkgs;
+
+ syms = moduleName.split('/');
+ //For each module name segment, see if there is a path
+ //registered for it. Start with most specific name
+ //and work up from it.
+ for (i = syms.length; i > 0; i -= 1) {
+ parentModule = syms.slice(0, i).join('/');
+ pkg = getOwn(pkgs, parentModule);
+ parentPath = getOwn(paths, parentModule);
+ if (parentPath) {
+ //If an array, it means there are a few choices,
+ //Choose the one that is desired
+ if (isArray(parentPath)) {
+ parentPath = parentPath[0];
+ }
+ syms.splice(0, i, parentPath);
+ break;
+ } else if (pkg) {
+ //If module name is just the package name, then looking
+ //for the main module.
+ if (moduleName === pkg.name) {
+ pkgPath = pkg.location + '/' + pkg.main;
+ } else {
+ pkgPath = pkg.location;
+ }
+ syms.splice(0, i, pkgPath);
+ break;
+ }
+ }
+
+ //Join the path parts together, then figure out if baseUrl is needed.
+ url = syms.join('/');
+ url += (ext || (/^data\:|\?/.test(url) || skipExt ? '' : '.js'));
+ url = (url.charAt(0) === '/' || url.match(/^[\w\+\.\-]+:/) ? '' : config.baseUrl) + url;
+ }
+
+ return config.urlArgs ? url +
+ ((url.indexOf('?') === -1 ? '?' : '&') +
+ config.urlArgs) : url;
+ },
+
+ //Delegates to req.load. Broken out as a separate function to
+ //allow overriding in the optimizer.
+ load: function (id, url) {
+ req.load(context, id, url);
+ },
+
+ /**
+ * Executes a module callback function. Broken out as a separate function
+ * solely to allow the build system to sequence the files in the built
+ * layer in the right sequence.
+ *
+ * @private
+ */
+ execCb: function (name, callback, args, exports) {
+ return callback.apply(exports, args);
+ },
+
+ /**
+ * callback for script loads, used to check status of loading.
+ *
+ * @param {Event} evt the event from the browser for the script
+ * that was loaded.
+ */
+ onScriptLoad: function (evt) {
+ //Using currentTarget instead of target for Firefox 2.0's sake. Not
+ //all old browsers will be supported, but this one was easy enough
+ //to support and still makes sense.
+ if (evt.type === 'load' ||
+ (readyRegExp.test((evt.currentTarget || evt.srcElement).readyState))) {
+ //Reset interactive script so a script node is not held onto for
+ //to long.
+ interactiveScript = null;
+
+ //Pull out the name of the module and the context.
+ var data = getScriptData(evt);
+ context.completeLoad(data.id);
+ }
+ },
+
+ /**
+ * Callback for script errors.
+ */
+ onScriptError: function (evt) {
+ var data = getScriptData(evt);
+ if (!hasPathFallback(data.id)) {
+ return onError(makeError('scripterror', 'Script error for: ' + data.id, evt, [data.id]));
+ }
+ }
+ };
+
+ context.require = context.makeRequire();
+ return context;
+ }
+
+ /**
+ * Main entry point.
+ *
+ * If the only argument to require is a string, then the module that
+ * is represented by that string is fetched for the appropriate context.
+ *
+ * If the first argument is an array, then it will be treated as an array
+ * of dependency string names to fetch. An optional function callback can
+ * be specified to execute when all of those dependencies are available.
+ *
+ * Make a local req variable to help Caja compliance (it assumes things
+ * on a require that are not standardized), and to give a short
+ * name for minification/local scope use.
+ */
+ req = requirejs = function (deps, callback, errback, optional) {
+
+ //Find the right context, use default
+ var context, config,
+ contextName = defContextName;
+
+ // Determine if have config object in the call.
+ if (!isArray(deps) && typeof deps !== 'string') {
+ // deps is a config object
+ config = deps;
+ if (isArray(callback)) {
+ // Adjust args if there are dependencies
+ deps = callback;
+ callback = errback;
+ errback = optional;
+ } else {
+ deps = [];
+ }
+ }
+
+ if (config && config.context) {
+ contextName = config.context;
+ }
+
+ context = getOwn(contexts, contextName);
+ if (!context) {
+ context = contexts[contextName] = req.s.newContext(contextName);
+ }
+
+ if (config) {
+ context.configure(config);
+ }
+
+ return context.require(deps, callback, errback);
+ };
+
+ /**
+ * Support require.config() to make it easier to cooperate with other
+ * AMD loaders on globally agreed names.
+ */
+ req.config = function (config) {
+ return req(config);
+ };
+
+ /**
+ * Execute something after the current tick
+ * of the event loop. Override for other envs
+ * that have a better solution than setTimeout.
+ * @param {Function} fn function to execute later.
+ */
+ req.nextTick = typeof setTimeout !== 'undefined' ? function (fn) {
+ setTimeout(fn, 4);
+ } : function (fn) { fn(); };
+
+ /**
+ * Export require as a global, but only if it does not already exist.
+ */
+ if (!require) {
+ require = req;
+ }
+
+ req.version = version;
+
+ //Used to filter out dependencies that are already paths.
+ req.jsExtRegExp = /^\/|:|\?|\.js$/;
+ req.isBrowser = isBrowser;
+ s = req.s = {
+ contexts: contexts,
+ newContext: newContext
+ };
+
+ //Create default context.
+ req({});
+
+ //Exports some context-sensitive methods on global require.
+ each([
+ 'toUrl',
+ 'undef',
+ 'defined',
+ 'specified'
+ ], function (prop) {
+ //Reference from contexts instead of early binding to default context,
+ //so that during builds, the latest instance of the default context
+ //with its config gets used.
+ req[prop] = function () {
+ var ctx = contexts[defContextName];
+ return ctx.require[prop].apply(ctx, arguments);
+ };
+ });
+
+ if (isBrowser) {
+ head = s.head = document.getElementsByTagName('head')[0];
+ //If BASE tag is in play, using appendChild is a problem for IE6.
+ //When that browser dies, this can be removed. Details in this jQuery bug:
+ //http://dev.jquery.com/ticket/2709
+ baseElement = document.getElementsByTagName('base')[0];
+ if (baseElement) {
+ head = s.head = baseElement.parentNode;
+ }
+ }
+
+ /**
+ * Any errors that require explicitly generates will be passed to this
+ * function. Intercept/override it if you want custom error handling.
+ * @param {Error} err the error object.
+ */
+ req.onError = defaultOnError;
+
+ /**
+ * Creates the node for the load command. Only used in browser envs.
+ */
+ req.createNode = function (config, moduleName, url) {
+ var node = config.xhtml ?
+ document.createElementNS('http://www.w3.org/1999/xhtml', 'html:script') :
+ document.createElement('script');
+ node.type = config.scriptType || 'text/javascript';
+ node.charset = 'utf-8';
+ node.async = true;
+ return node;
+ };
+
+ /**
+ * Does the request to load a module for the browser case.
+ * Make this a separate function to allow other environments
+ * to override it.
+ *
+ * @param {Object} context the require context to find state.
+ * @param {String} moduleName the name of the module.
+ * @param {Object} url the URL to the module.
+ */
+ req.load = function (context, moduleName, url) {
+ var config = (context && context.config) || {},
+ node;
+ if (isBrowser) {
+ //In the browser so use a script tag
+ node = req.createNode(config, moduleName, url);
+
+ node.setAttribute('data-requirecontext', context.contextName);
+ node.setAttribute('data-requiremodule', moduleName);
+
+ //Set up load listener. Test attachEvent first because IE9 has
+ //a subtle issue in its addEventListener and script onload firings
+ //that do not match the behavior of all other browsers with
+ //addEventListener support, which fire the onload event for a
+ //script right after the script execution. See:
+ //https://connect.microsoft.com/IE/feedback/details/648057/script-onload-event-is-not-fired-immediately-after-script-execution
+ //UNFORTUNATELY Opera implements attachEvent but does not follow the script
+ //script execution mode.
+ if (node.attachEvent &&
+ //Check if node.attachEvent is artificially added by custom script or
+ //natively supported by browser
+ //read https://github.com/jrburke/requirejs/issues/187
+ //if we can NOT find [native code] then it must NOT natively supported.
+ //in IE8, node.attachEvent does not have toString()
+ //Note the test for "[native code" with no closing brace, see:
+ //https://github.com/jrburke/requirejs/issues/273
+ !(node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code') < 0) &&
+ !isOpera) {
+ //Probably IE. IE (at least 6-8) do not fire
+ //script onload right after executing the script, so
+ //we cannot tie the anonymous define call to a name.
+ //However, IE reports the script as being in 'interactive'
+ //readyState at the time of the define call.
+ useInteractive = true;
+
+ node.attachEvent('onreadystatechange', context.onScriptLoad);
+ //It would be great to add an error handler here to catch
+ //404s in IE9+. However, onreadystatechange will fire before
+ //the error handler, so that does not help. If addEventListener
+ //is used, then IE will fire error before load, but we cannot
+ //use that pathway given the connect.microsoft.com issue
+ //mentioned above about not doing the 'script execute,
+ //then fire the script load event listener before execute
+ //next script' that other browsers do.
+ //Best hope: IE10 fixes the issues,
+ //and then destroys all installs of IE 6-9.
+ //node.attachEvent('onerror', context.onScriptError);
+ } else {
+ node.addEventListener('load', context.onScriptLoad, false);
+ node.addEventListener('error', context.onScriptError, false);
+ }
+ node.src = url;
+
+ //For some cache cases in IE 6-8, the script executes before the end
+ //of the appendChild execution, so to tie an anonymous define
+ //call to the module name (which is stored on the node), hold on
+ //to a reference to this node, but clear after the DOM insertion.
+ currentlyAddingScript = node;
+ if (baseElement) {
+ head.insertBefore(node, baseElement);
+ } else {
+ head.appendChild(node);
+ }
+ currentlyAddingScript = null;
+
+ return node;
+ } else if (isWebWorker) {
+ try {
+ //In a web worker, use importScripts. This is not a very
+ //efficient use of importScripts, importScripts will block until
+ //its script is downloaded and evaluated. However, if web workers
+ //are in play, the expectation that a build has been done so that
+ //only one script needs to be loaded anyway. This may need to be
+ //reevaluated if other use cases become common.
+ importScripts(url);
+
+ //Account for anonymous modules
+ context.completeLoad(moduleName);
+ } catch (e) {
+ context.onError(makeError('importscripts',
+ 'importScripts failed for ' +
+ moduleName + ' at ' + url,
+ e,
+ [moduleName]));
+ }
+ }
+ };
+
+ function getInteractiveScript() {
+ if (interactiveScript && interactiveScript.readyState === 'interactive') {
+ return interactiveScript;
+ }
+
+ eachReverse(scripts(), function (script) {
+ if (script.readyState === 'interactive') {
+ return (interactiveScript = script);
+ }
+ });
+ return interactiveScript;
+ }
+
+ //Look for a data-main script attribute, which could also adjust the baseUrl.
+ if (isBrowser && !cfg.skipDataMain) {
+ //Figure out baseUrl. Get it from the script tag with require.js in it.
+ eachReverse(scripts(), function (script) {
+ //Set the 'head' where we can append children by
+ //using the script's parent.
+ if (!head) {
+ head = script.parentNode;
+ }
+
+ //Look for a data-main attribute to set main script for the page
+ //to load. If it is there, the path to data main becomes the
+ //baseUrl, if it is not already set.
+ dataMain = script.getAttribute('data-main');
+ if (dataMain) {
+ //Preserve dataMain in case it is a path (i.e. contains '?')
+ mainScript = dataMain;
+
+ //Set final baseUrl if there is not already an explicit one.
+ if (!cfg.baseUrl) {
+ //Pull off the directory of data-main for use as the
+ //baseUrl.
+ src = mainScript.split('/');
+ mainScript = src.pop();
+ subPath = src.length ? src.join('/') + '/' : './';
+
+ cfg.baseUrl = subPath;
+ }
+
+ //Strip off any trailing .js since mainScript is now
+ //like a module name.
+ mainScript = mainScript.replace(jsSuffixRegExp, '');
+
+ //If mainScript is still a path, fall back to dataMain
+ if (req.jsExtRegExp.test(mainScript)) {
+ mainScript = dataMain;
+ }
+
+ //Put the data-main script in the files to load.
+ cfg.deps = cfg.deps ? cfg.deps.concat(mainScript) : [mainScript];
+
+ return true;
+ }
+ });
+ }
+
+ /**
+ * The function that handles definitions of modules. Differs from
+ * require() in that a string for the module should be the first argument,
+ * and the function to execute after dependencies are loaded should
+ * return a value to define the module corresponding to the first argument's
+ * name.
+ */
+ define = function (name, deps, callback) {
+ var node, context;
+
+ //Allow for anonymous modules
+ if (typeof name !== 'string') {
+ //Adjust args appropriately
+ callback = deps;
+ deps = name;
+ name = null;
+ }
+
+ //This module may not have dependencies
+ if (!isArray(deps)) {
+ callback = deps;
+ deps = null;
+ }
+
+ //If no name, and callback is a function, then figure out if it a
+ //CommonJS thing with dependencies.
+ if (!deps && isFunction(callback)) {
+ deps = [];
+ //Remove comments from the callback string,
+ //look for require calls, and pull them into the dependencies,
+ //but only if there are function args.
+ if (callback.length) {
+ callback
+ .toString()
+ .replace(commentRegExp, '')
+ .replace(cjsRequireRegExp, function (match, dep) {
+ deps.push(dep);
+ });
+
+ //May be a CommonJS thing even without require calls, but still
+ //could use exports, and module. Avoid doing exports and module
+ //work though if it just needs require.
+ //REQUIRES the function to expect the CommonJS variables in the
+ //order listed below.
+ deps = (callback.length === 1 ? ['require'] : ['require', 'exports', 'module']).concat(deps);
+ }
+ }
+
+ //If in IE 6-8 and hit an anonymous define() call, do the interactive
+ //work.
+ if (useInteractive) {
+ node = currentlyAddingScript || getInteractiveScript();
+ if (node) {
+ if (!name) {
+ name = node.getAttribute('data-requiremodule');
+ }
+ context = contexts[node.getAttribute('data-requirecontext')];
+ }
+ }
+
+ //Always save off evaluating the def call until the script onload handler.
+ //This allows multiple modules to be in a file without prematurely
+ //tracing dependencies, and allows for anonymous module support,
+ //where the module name is not known until the script onload event
+ //occurs. If no context, use the global queue, and get it processed
+ //in the onscript load callback.
+ (context ? context.defQueue : globalDefQueue).push([name, deps, callback]);
+ };
+
+ define.amd = {
+ jQuery: true
+ };
+
+
+ /**
+ * Executes the text. Normally just uses eval, but can be modified
+ * to use a better, environment-specific call. Only used for transpiling
+ * loader plugins, not for plain JS modules.
+ * @param {String} text the text to execute/evaluate.
+ */
+ req.exec = function (text) {
+ /*jslint evil: true */
+ return eval(text);
+ };
+
+ //Set up with config info.
+ req(cfg);
+}(this));
diff --git a/assets/webconfig/js/vendor/stapes.js b/assets/webconfig/js/vendor/stapes.js
new file mode 100644
index 00000000..106af171
--- /dev/null
+++ b/assets/webconfig/js/vendor/stapes.js
@@ -0,0 +1,594 @@
+//
+// ____ _ _
+// / ___|| |_ __ _ _ __ ___ ___ (_)___ (*)
+// \___ \| __/ _` | '_ \ / _ \/ __| | / __|
+// ___) | || (_| | |_) | __/\__ \_ | \__ \
+// |____/ \__\__,_| .__/ \___||___(_)/ |___/
+// |_| |__/
+//
+// (*) a (really) tiny Javascript MVC microframework
+//
+// (c) Hay Kranen < hay@bykr.org >
+// Released under the terms of the MIT license
+// < http://en.wikipedia.org/wiki/MIT_License >
+//
+// Stapes.js : http://hay.github.com/stapes
+(function() {
+ 'use strict';
+
+ var VERSION = "0.8.0";
+
+ // Global counter for all events in all modules (including mixed in objects)
+ var guid = 1;
+
+ // Makes _.create() faster
+ if (!Object.create) {
+ var CachedFunction = function(){};
+ }
+
+ // So we can use slice.call for arguments later on
+ var slice = Array.prototype.slice;
+
+ // Private attributes and helper functions, stored in an object so they
+ // are overwritable by plugins
+ var _ = {
+ // Properties
+ attributes : {},
+
+ eventHandlers : {
+ "-1" : {} // '-1' is used for the global event handling
+ },
+
+ guid : -1,
+
+ // Methods
+ addEvent : function(event) {
+ // If we don't have any handlers for this type of event, add a new
+ // array we can use to push new handlers
+ if (!_.eventHandlers[event.guid][event.type]) {
+ _.eventHandlers[event.guid][event.type] = [];
+ }
+
+ // Push an event object
+ _.eventHandlers[event.guid][event.type].push({
+ "guid" : event.guid,
+ "handler" : event.handler,
+ "scope" : event.scope,
+ "type" : event.type
+ });
+ },
+
+ addEventHandler : function(argTypeOrMap, argHandlerOrScope, argScope) {
+ var eventMap = {},
+ scope;
+
+ if (typeof argTypeOrMap === "string") {
+ scope = argScope || false;
+ eventMap[ argTypeOrMap ] = argHandlerOrScope;
+ } else {
+ scope = argHandlerOrScope || false;
+ eventMap = argTypeOrMap;
+ }
+
+ for (var eventString in eventMap) {
+ var handler = eventMap[eventString];
+ var events = eventString.split(" ");
+
+ for (var i = 0, l = events.length; i < l; i++) {
+ var eventType = events[i];
+ _.addEvent.call(this, {
+ "guid" : this._guid || this._.guid,
+ "handler" : handler,
+ "scope" : scope,
+ "type" : eventType
+ });
+ }
+ }
+ },
+
+ addGuid : function(object, forceGuid) {
+ if (object._guid && !forceGuid) return;
+
+ object._guid = guid++;
+
+ _.attributes[object._guid] = {};
+ _.eventHandlers[object._guid] = {};
+ },
+
+ // This is a really small utility function to save typing and produce
+ // better optimized code
+ attr : function(guid) {
+ return _.attributes[guid];
+ },
+
+ clone : function(obj) {
+ var type = _.typeOf(obj);
+
+ if (type === 'object') {
+ return _.extend({}, obj);
+ }
+
+ if (type === 'array') {
+ return obj.slice(0);
+ }
+ },
+
+ create : function(proto) {
+ if (Object.create) {
+ return Object.create(proto);
+ } else {
+ CachedFunction.prototype = proto;
+ return new CachedFunction();
+ }
+ },
+
+ createSubclass : function(props, includeEvents) {
+ props = props || {};
+ includeEvents = includeEvents || false;
+
+ var superclass = props.superclass.prototype;
+
+ // Objects always have a constructor, so we need to be sure this is
+ // a property instead of something from the prototype
+ var realConstructor = props.hasOwnProperty('constructor') ? props.constructor : function(){};
+
+ function constructor() {
+ // Be kind to people forgetting new
+ if (!(this instanceof constructor)) {
+ throw new Error("Please use 'new' when initializing Stapes classes");
+ }
+
+ // If this class has events add a GUID as well
+ if (this.on) {
+ _.addGuid( this, true );
+ }
+
+ realConstructor.apply(this, arguments);
+ }
+
+ if (includeEvents) {
+ _.extend(superclass, Events);
+ }
+
+ constructor.prototype = _.create(superclass);
+ constructor.prototype.constructor = constructor;
+
+ _.extend(constructor, {
+ extend : function() {
+ return _.extendThis.apply(this, arguments);
+ },
+
+ // We can't call this 'super' because that's a reserved keyword
+ // and fails in IE8
+ 'parent' : superclass,
+
+ proto : function() {
+ return _.extendThis.apply(this.prototype, arguments);
+ },
+
+ subclass : function(obj) {
+ obj = obj || {};
+ obj.superclass = this;
+ return _.createSubclass(obj);
+ }
+ });
+
+ // Copy all props given in the definition to the prototype
+ for (var key in props) {
+ if (key !== 'constructor' && key !== 'superclass') {
+ constructor.prototype[key] = props[key];
+ }
+ }
+
+ return constructor;
+ },
+
+ emitEvents : function(type, data, explicitType, explicitGuid) {
+ explicitType = explicitType || false;
+ explicitGuid = explicitGuid || this._guid;
+
+ // #30: make a local copy of handlers to prevent problems with
+ // unbinding the event while unwinding the loop
+ var handlers = slice.call(_.eventHandlers[explicitGuid][type]);
+
+ for (var i = 0, l = handlers.length; i < l; i++) {
+ // Clone the event to prevent issue #19
+ var event = _.extend({}, handlers[i]);
+ var scope = (event.scope) ? event.scope : this;
+
+ if (explicitType) {
+ event.type = explicitType;
+ }
+
+ event.scope = scope;
+ event.handler.call(event.scope, data, event);
+ }
+ },
+
+ // Extend an object with more objects
+ extend : function() {
+ var args = slice.call(arguments);
+ var object = args.shift();
+
+ for (var i = 0, l = args.length; i < l; i++) {
+ var props = args[i];
+ for (var key in props) {
+ object[key] = props[key];
+ }
+ }
+
+ return object;
+ },
+
+ // The same as extend, but uses the this value as the scope
+ extendThis : function() {
+ var args = slice.call(arguments);
+ args.unshift(this);
+ return _.extend.apply(this, args);
+ },
+
+ // from http://stackoverflow.com/a/2117523/152809
+ makeUuid : function() {
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+ var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
+ return v.toString(16);
+ });
+ },
+
+ removeAttribute : function(keys, silent) {
+ silent = silent || false;
+
+ // Split the key, maybe we want to remove more than one item
+ var attributes = _.trim(keys).split(" ");
+
+ // Actually delete the item
+ for (var i = 0, l = attributes.length; i < l; i++) {
+ var key = _.trim(attributes[i]);
+
+ if (key) {
+ delete _.attr(this._guid)[key];
+
+ // If 'silent' is set, do not throw any events
+ if (!silent) {
+ this.emit('change', key);
+ this.emit('change:' + key);
+ this.emit('remove', key);
+ this.emit('remove:' + key);
+ }
+ }
+ }
+ },
+
+ removeEventHandler : function(type, handler) {
+ var handlers = _.eventHandlers[this._guid];
+
+ if (type && handler) {
+ // Remove a specific handler
+ handlers = handlers[type];
+ if (!handlers) return;
+
+ for (var i = 0, l = handlers.length, h; i < l; i++) {
+ h = handlers[i].handler;
+ if (h && h === handler) {
+ handlers.splice(i--, 1);
+ l--;
+ }
+ }
+ } else if (type) {
+ // Remove all handlers for a specific type
+ delete handlers[type];
+ } else {
+ // Remove all handlers for this module
+ _.eventHandlers[this._guid] = {};
+ }
+ },
+
+ setAttribute : function(key, value, silent) {
+ silent = silent || false;
+
+ // We need to do this before we actually add the item :)
+ var itemExists = this.has(key);
+ var oldValue = _.attr(this._guid)[key];
+
+ // Is the value different than the oldValue? If not, ignore this call
+ if (value === oldValue) {
+ return;
+ }
+
+ // Actually add the item to the attributes
+ _.attr(this._guid)[key] = value;
+
+ // If 'silent' flag is set, do not throw any events
+ if (silent) {
+ return;
+ }
+
+ // Throw a generic event
+ this.emit('change', key);
+
+ // And a namespaced event as well, NOTE that we pass value instead of
+ // key here!
+ this.emit('change:' + key, value);
+
+ // Throw namespaced and non-namespaced 'mutate' events as well with
+ // the old value data as well and some extra metadata such as the key
+ var mutateData = {
+ "key" : key,
+ "newValue" : value,
+ "oldValue" : oldValue || null
+ };
+
+ this.emit('mutate', mutateData);
+ this.emit('mutate:' + key, mutateData);
+
+ // Also throw a specific event for this type of set
+ var specificEvent = itemExists ? 'update' : 'create';
+
+ this.emit(specificEvent, key);
+
+ // And a namespaced event as well, NOTE that we pass value instead of key
+ this.emit(specificEvent + ':' + key, value);
+ },
+
+ trim : function(str) {
+ return str.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
+ },
+
+ typeOf : function(val) {
+ if (val === null || typeof val === "undefined") {
+ // This is a special exception for IE, in other browsers the
+ // method below works all the time
+ return String(val);
+ } else {
+ return Object.prototype.toString.call(val).replace(/\[object |\]/g, '').toLowerCase();
+ }
+ },
+
+ updateAttribute : function(key, fn, silent) {
+ var item = this.get(key);
+
+ // In previous versions of Stapes we didn't have the check for object,
+ // but still this worked. In 0.7.0 it suddenly doesn't work anymore and
+ // we need the check. Why? I have no clue.
+ var type = _.typeOf(item);
+
+ if (type === 'object' || type === 'array') {
+ item = _.clone(item);
+ }
+
+ var newValue = fn.call(this, item, key);
+ _.setAttribute.call(this, key, newValue, silent || false);
+ }
+ };
+
+ // Can be mixed in later using Stapes.mixinEvents(object);
+ var Events = {
+ emit : function(types, data) {
+ data = (typeof data === "undefined") ? null : data;
+
+ var splittedTypes = types.split(" ");
+
+ for (var i = 0, l = splittedTypes.length; i < l; i++) {
+ var type = splittedTypes[i];
+
+ // First 'all' type events: is there an 'all' handler in the
+ // global stack?
+ if (_.eventHandlers[-1].all) {
+ _.emitEvents.call(this, "all", data, type, -1);
+ }
+
+ // Catch all events for this type?
+ if (_.eventHandlers[-1][type]) {
+ _.emitEvents.call(this, type, data, type, -1);
+ }
+
+ if (typeof this._guid === 'number') {
+ // 'all' event for this specific module?
+ if (_.eventHandlers[this._guid].all) {
+ _.emitEvents.call(this, "all", data, type);
+ }
+
+ // Finally, normal events :)
+ if (_.eventHandlers[this._guid][type]) {
+ _.emitEvents.call(this, type, data);
+ }
+ }
+ }
+ },
+
+ off : function() {
+ _.removeEventHandler.apply(this, arguments);
+ },
+
+ on : function() {
+ _.addEventHandler.apply(this, arguments);
+ }
+ };
+
+ _.Module = function() {
+
+ };
+
+ _.Module.prototype = {
+ each : function(fn, ctx) {
+ var attr = _.attr(this._guid);
+ for (var key in attr) {
+ var value = attr[key];
+ fn.call(ctx || this, value, key);
+ }
+ },
+
+ extend : function() {
+ return _.extendThis.apply(this, arguments);
+ },
+
+ filter : function(fn) {
+ var filtered = [];
+ var attributes = _.attr(this._guid);
+
+ for (var key in attributes) {
+ if ( fn.call(this, attributes[key], key)) {
+ filtered.push( attributes[key] );
+ }
+ }
+
+ return filtered;
+ },
+
+ get : function(input) {
+ if (typeof input === "string") {
+ return this.has(input) ? _.attr(this._guid)[input] : null;
+ } else if (typeof input === "function") {
+ var items = this.filter(input);
+ return (items.length) ? items[0] : null;
+ }
+ },
+
+ getAll : function() {
+ return _.clone( _.attr(this._guid) );
+ },
+
+ getAllAsArray : function() {
+ var arr = [];
+ var attributes = _.attr(this._guid);
+
+ for (var key in attributes) {
+ var value = attributes[key];
+
+ if (_.typeOf(value) === "object" && !value.id) {
+ value.id = key;
+ }
+
+ arr.push(value);
+ }
+
+ return arr;
+ },
+
+ has : function(key) {
+ return (typeof _.attr(this._guid)[key] !== "undefined");
+ },
+
+ map : function(fn, ctx) {
+ var mapped = [];
+ this.each(function(value, key) {
+ mapped.push( fn.call(ctx || this, value, key) );
+ }, ctx || this);
+ return mapped;
+ },
+
+ // Akin to set(), but makes a unique id
+ push : function(input, silent) {
+ if (_.typeOf(input) === "array") {
+ for (var i = 0, l = input.length; i < l; i++) {
+ _.setAttribute.call(this, _.makeUuid(), input[i], silent || false);
+ }
+ } else {
+ _.setAttribute.call(this, _.makeUuid(), input, silent || false);
+ }
+
+ return this;
+ },
+
+ remove : function(input, silent) {
+ if (typeof input === 'undefined') {
+ // With no arguments, remove deletes all attributes
+ _.attributes[this._guid] = {};
+ this.emit('change remove');
+ } else if (typeof input === "function") {
+ this.each(function(item, key) {
+ if (input(item)) {
+ _.removeAttribute.call(this, key, silent);
+ }
+ });
+ } else {
+ // nb: checking for exists happens in removeAttribute
+ _.removeAttribute.call(this, input, silent || false);
+ }
+
+ return this;
+ },
+
+ set : function(objOrKey, valueOrSilent, silent) {
+ if (typeof objOrKey === "object") {
+ for (var key in objOrKey) {
+ _.setAttribute.call(this, key, objOrKey[key], valueOrSilent || false);
+ }
+ } else {
+ _.setAttribute.call(this, objOrKey, valueOrSilent, silent || false);
+ }
+
+ return this;
+ },
+
+ size : function() {
+ var size = 0;
+ var attr = _.attr(this._guid);
+
+ for (var key in attr) {
+ size++;
+ }
+
+ return size;
+ },
+
+ update : function(keyOrFn, fn, silent) {
+ if (typeof keyOrFn === "string") {
+ _.updateAttribute.call(this, keyOrFn, fn, silent || false);
+ } else if (typeof keyOrFn === "function") {
+ this.each(function(value, key) {
+ _.updateAttribute.call(this, key, keyOrFn);
+ });
+ }
+
+ return this;
+ }
+ };
+
+ var Stapes = {
+ "_" : _, // private helper functions and properties
+
+ "extend" : function() {
+ return _.extendThis.apply(_.Module.prototype, arguments);
+ },
+
+ "mixinEvents" : function(obj) {
+ obj = obj || {};
+
+ _.addGuid(obj);
+
+ return _.extend(obj, Events);
+ },
+
+ "on" : function() {
+ _.addEventHandler.apply(this, arguments);
+ },
+
+ "subclass" : function(obj, classOnly) {
+ classOnly = classOnly || false;
+ obj = obj || {};
+ obj.superclass = classOnly ? function(){} : _.Module;
+ return _.createSubclass(obj, !classOnly);
+ },
+
+ "version" : VERSION
+ };
+
+ // This library can be used as an AMD module, a Node.js module, or an
+ // old fashioned global
+ if (typeof exports !== "undefined") {
+ // Server
+ if (typeof module !== "undefined" && module.exports) {
+ exports = module.exports = Stapes;
+ }
+ exports.Stapes = Stapes;
+ } else if (typeof define === "function" && define.amd) {
+ // AMD
+ define(function() {
+ return Stapes;
+ });
+ } else {
+ // Global scope
+ window.Stapes = Stapes;
+ }
+})();
diff --git a/assets/webconfig/js/vendor/tinycolor.js b/assets/webconfig/js/vendor/tinycolor.js
new file mode 100644
index 00000000..4c6f8d97
--- /dev/null
+++ b/assets/webconfig/js/vendor/tinycolor.js
@@ -0,0 +1,1107 @@
+// TinyColor v1.0.0
+// https://github.com/bgrins/TinyColor
+// Brian Grinstead, MIT License
+
+(function() {
+
+ var trimLeft = /^[\s,#]+/,
+ trimRight = /\s+$/,
+ tinyCounter = 0,
+ math = Math,
+ mathRound = math.round,
+ mathMin = math.min,
+ mathMax = math.max,
+ mathRandom = math.random;
+
+ var tinycolor = function tinycolor (color, opts) {
+
+ color = (color) ? color : '';
+ opts = opts || { };
+
+ // If input is already a tinycolor, return itself
+ if (color instanceof tinycolor) {
+ return color;
+ }
+ // If we are called as a function, call using new instead
+ if (!(this instanceof tinycolor)) {
+ return new tinycolor(color, opts);
+ }
+
+ var rgb = inputToRGB(color);
+ this._r = rgb.r,
+ this._g = rgb.g,
+ this._b = rgb.b,
+ this._a = rgb.a,
+ this._roundA = mathRound(100*this._a) / 100,
+ this._format = opts.format || rgb.format;
+ this._gradientType = opts.gradientType;
+
+ // Don't let the range of [0,255] come back in [0,1].
+ // Potentially lose a little bit of precision here, but will fix issues where
+ // .5 gets interpreted as half of the total, instead of half of 1
+ // If it was supposed to be 128, this was already taken care of by `inputToRgb`
+ if (this._r < 1) { this._r = mathRound(this._r); }
+ if (this._g < 1) { this._g = mathRound(this._g); }
+ if (this._b < 1) { this._b = mathRound(this._b); }
+
+ this._ok = rgb.ok;
+ this._tc_id = tinyCounter++;
+ };
+
+ tinycolor.prototype = {
+ isDark: function() {
+ return this.getBrightness() < 128;
+ },
+ isLight: function() {
+ return !this.isDark();
+ },
+ isValid: function() {
+ return this._ok;
+ },
+ getFormat: function() {
+ return this._format;
+ },
+ getAlpha: function() {
+ return this._a;
+ },
+ getBrightness: function() {
+ var rgb = this.toRgb();
+ return (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000;
+ },
+ setAlpha: function(value) {
+ this._a = boundAlpha(value);
+ this._roundA = mathRound(100*this._a) / 100;
+ return this;
+ },
+ toHsv: function() {
+ var hsv = rgbToHsv(this._r, this._g, this._b);
+ return { h: hsv.h * 360, s: hsv.s, v: hsv.v, a: this._a };
+ },
+ toHsvString: function() {
+ var hsv = rgbToHsv(this._r, this._g, this._b);
+ var h = mathRound(hsv.h * 360), s = mathRound(hsv.s * 100), v = mathRound(hsv.v * 100);
+ return (this._a == 1) ?
+ "hsv(" + h + ", " + s + "%, " + v + "%)" :
+ "hsva(" + h + ", " + s + "%, " + v + "%, "+ this._roundA + ")";
+ },
+ toHsl: function() {
+ var hsl = rgbToHsl(this._r, this._g, this._b);
+ return { h: hsl.h * 360, s: hsl.s, l: hsl.l, a: this._a };
+ },
+ toHslString: function() {
+ var hsl = rgbToHsl(this._r, this._g, this._b);
+ var h = mathRound(hsl.h * 360), s = mathRound(hsl.s * 100), l = mathRound(hsl.l * 100);
+ return (this._a == 1) ?
+ "hsl(" + h + ", " + s + "%, " + l + "%)" :
+ "hsla(" + h + ", " + s + "%, " + l + "%, "+ this._roundA + ")";
+ },
+ toHex: function(allow3Char) {
+ return rgbToHex(this._r, this._g, this._b, allow3Char);
+ },
+ toHexString: function(allow3Char) {
+ return '#' + this.toHex(allow3Char);
+ },
+ toHex8: function() {
+ return rgbaToHex(this._r, this._g, this._b, this._a);
+ },
+ toHex8String: function() {
+ return '#' + this.toHex8();
+ },
+ toRgb: function() {
+ return { r: mathRound(this._r), g: mathRound(this._g), b: mathRound(this._b), a: this._a };
+ },
+ toRgbString: function() {
+ return (this._a == 1) ?
+ "rgb(" + mathRound(this._r) + ", " + mathRound(this._g) + ", " + mathRound(this._b) + ")" :
+ "rgba(" + mathRound(this._r) + ", " + mathRound(this._g) + ", " + mathRound(this._b) + ", " + this._roundA + ")";
+ },
+ toPercentageRgb: function() {
+ return { r: mathRound(bound01(this._r, 255) * 100) + "%", g: mathRound(bound01(this._g, 255) * 100) + "%", b: mathRound(bound01(this._b, 255) * 100) + "%", a: this._a };
+ },
+ toPercentageRgbString: function() {
+ return (this._a == 1) ?
+ "rgb(" + mathRound(bound01(this._r, 255) * 100) + "%, " + mathRound(bound01(this._g, 255) * 100) + "%, " + mathRound(bound01(this._b, 255) * 100) + "%)" :
+ "rgba(" + mathRound(bound01(this._r, 255) * 100) + "%, " + mathRound(bound01(this._g, 255) * 100) + "%, " + mathRound(bound01(this._b, 255) * 100) + "%, " + this._roundA + ")";
+ },
+ toName: function() {
+ if (this._a === 0) {
+ return "transparent";
+ }
+
+ if (this._a < 1) {
+ return false;
+ }
+
+ return hexNames[rgbToHex(this._r, this._g, this._b, true)] || false;
+ },
+ toFilter: function(secondColor) {
+ var hex8String = '#' + rgbaToHex(this._r, this._g, this._b, this._a);
+ var secondHex8String = hex8String;
+ var gradientType = this._gradientType ? "GradientType = 1, " : "";
+
+ if (secondColor) {
+ var s = tinycolor(secondColor);
+ secondHex8String = s.toHex8String();
+ }
+
+ return "progid:DXImageTransform.Microsoft.gradient("+gradientType+"startColorstr="+hex8String+",endColorstr="+secondHex8String+")";
+ },
+ toString: function(format) {
+ var formatSet = !!format;
+ format = format || this._format;
+
+ var formattedString = false;
+ var hasAlpha = this._a < 1 && this._a >= 0;
+ var needsAlphaFormat = !formatSet && hasAlpha && (format === "hex" || format === "hex6" || format === "hex3" || format === "name");
+
+ if (needsAlphaFormat) {
+ // Special case for "transparent", all other non-alpha formats
+ // will return rgba when there is transparency.
+ if (format === "name" && this._a === 0) {
+ return this.toName();
+ }
+ return this.toRgbString();
+ }
+ if (format === "rgb") {
+ formattedString = this.toRgbString();
+ }
+ if (format === "prgb") {
+ formattedString = this.toPercentageRgbString();
+ }
+ if (format === "hex" || format === "hex6") {
+ formattedString = this.toHexString();
+ }
+ if (format === "hex3") {
+ formattedString = this.toHexString(true);
+ }
+ if (format === "hex8") {
+ formattedString = this.toHex8String();
+ }
+ if (format === "name") {
+ formattedString = this.toName();
+ }
+ if (format === "hsl") {
+ formattedString = this.toHslString();
+ }
+ if (format === "hsv") {
+ formattedString = this.toHsvString();
+ }
+
+ return formattedString || this.toHexString();
+ },
+
+ _applyModification: function(fn, args) {
+ var color = fn.apply(null, [this].concat([].slice.call(args)));
+ this._r = color._r;
+ this._g = color._g;
+ this._b = color._b;
+ this.setAlpha(color._a);
+ return this;
+ },
+ lighten: function() {
+ return this._applyModification(lighten, arguments);
+ },
+ brighten: function() {
+ return this._applyModification(brighten, arguments);
+ },
+ darken: function() {
+ return this._applyModification(darken, arguments);
+ },
+ desaturate: function() {
+ return this._applyModification(desaturate, arguments);
+ },
+ saturate: function() {
+ return this._applyModification(saturate, arguments);
+ },
+ greyscale: function() {
+ return this._applyModification(greyscale, arguments);
+ },
+ spin: function() {
+ return this._applyModification(spin, arguments);
+ },
+
+ _applyCombination: function(fn, args) {
+ return fn.apply(null, [this].concat([].slice.call(args)));
+ },
+ analogous: function() {
+ return this._applyCombination(analogous, arguments);
+ },
+ complement: function() {
+ return this._applyCombination(complement, arguments);
+ },
+ monochromatic: function() {
+ return this._applyCombination(monochromatic, arguments);
+ },
+ splitcomplement: function() {
+ return this._applyCombination(splitcomplement, arguments);
+ },
+ triad: function() {
+ return this._applyCombination(triad, arguments);
+ },
+ tetrad: function() {
+ return this._applyCombination(tetrad, arguments);
+ }
+ };
+
+ // If input is an object, force 1 into "1.0" to handle ratios properly
+ // String input requires "1.0" as input, so 1 will be treated as 1
+ tinycolor.fromRatio = function(color, opts) {
+ if (typeof color == "object") {
+ var newColor = {};
+ for (var i in color) {
+ if (color.hasOwnProperty(i)) {
+ if (i === "a") {
+ newColor[i] = color[i];
+ }
+ else {
+ newColor[i] = convertToPercentage(color[i]);
+ }
+ }
+ }
+ color = newColor;
+ }
+
+ return tinycolor(color, opts);
+ };
+
+ // Given a string or object, convert that input to RGB
+ // Possible string inputs:
+ //
+ // "red"
+ // "#f00" or "f00"
+ // "#ff0000" or "ff0000"
+ // "#ff000000" or "ff000000"
+ // "rgb 255 0 0" or "rgb (255, 0, 0)"
+ // "rgb 1.0 0 0" or "rgb (1, 0, 0)"
+ // "rgba (255, 0, 0, 1)" or "rgba 255, 0, 0, 1"
+ // "rgba (1.0, 0, 0, 1)" or "rgba 1.0, 0, 0, 1"
+ // "hsl(0, 100%, 50%)" or "hsl 0 100% 50%"
+ // "hsla(0, 100%, 50%, 1)" or "hsla 0 100% 50%, 1"
+ // "hsv(0, 100%, 100%)" or "hsv 0 100% 100%"
+ //
+ function inputToRGB(color) {
+
+ var rgb = { r: 0, g: 0, b: 0 };
+ var a = 1;
+ var ok = false;
+ var format = false;
+
+ if (typeof color == "string") {
+ color = stringInputToObject(color);
+ }
+
+ if (typeof color == "object") {
+ if (color.hasOwnProperty("r") && color.hasOwnProperty("g") && color.hasOwnProperty("b")) {
+ rgb = rgbToRgb(color.r, color.g, color.b);
+ ok = true;
+ format = String(color.r).substr(-1) === "%" ? "prgb" : "rgb";
+ }
+ else if (color.hasOwnProperty("h") && color.hasOwnProperty("s") && color.hasOwnProperty("v")) {
+ color.s = convertToPercentage(color.s);
+ color.v = convertToPercentage(color.v);
+ rgb = hsvToRgb(color.h, color.s, color.v);
+ ok = true;
+ format = "hsv";
+ }
+ else if (color.hasOwnProperty("h") && color.hasOwnProperty("s") && color.hasOwnProperty("l")) {
+ color.s = convertToPercentage(color.s);
+ color.l = convertToPercentage(color.l);
+ rgb = hslToRgb(color.h, color.s, color.l);
+ ok = true;
+ format = "hsl";
+ }
+
+ if (color.hasOwnProperty("a")) {
+ a = color.a;
+ }
+ }
+
+ a = boundAlpha(a);
+
+ return {
+ ok: ok,
+ format: color.format || format,
+ r: mathMin(255, mathMax(rgb.r, 0)),
+ g: mathMin(255, mathMax(rgb.g, 0)),
+ b: mathMin(255, mathMax(rgb.b, 0)),
+ a: a
+ };
+ }
+
+
+ // Conversion Functions
+ // --------------------
+
+ // `rgbToHsl`, `rgbToHsv`, `hslToRgb`, `hsvToRgb` modified from:
+ //
+
+ // `rgbToRgb`
+ // Handle bounds / percentage checking to conform to CSS color spec
+ //
+ // *Assumes:* r, g, b in [0, 255] or [0, 1]
+ // *Returns:* { r, g, b } in [0, 255]
+ function rgbToRgb(r, g, b){
+ return {
+ r: bound01(r, 255) * 255,
+ g: bound01(g, 255) * 255,
+ b: bound01(b, 255) * 255
+ };
+ }
+
+ // `rgbToHsl`
+ // Converts an RGB color value to HSL.
+ // *Assumes:* r, g, and b are contained in [0, 255] or [0, 1]
+ // *Returns:* { h, s, l } in [0,1]
+ function rgbToHsl(r, g, b) {
+
+ r = bound01(r, 255);
+ g = bound01(g, 255);
+ b = bound01(b, 255);
+
+ var max = mathMax(r, g, b), min = mathMin(r, g, b);
+ var h, s, l = (max + min) / 2;
+
+ if(max == min) {
+ h = s = 0; // achromatic
+ }
+ else {
+ var d = max - min;
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
+ switch(max) {
+ case r: h = (g - b) / d + (g < b ? 6 : 0); break;
+ case g: h = (b - r) / d + 2; break;
+ case b: h = (r - g) / d + 4; break;
+ }
+
+ h /= 6;
+ }
+
+ return { h: h, s: s, l: l };
+ }
+
+ // `hslToRgb`
+ // Converts an HSL color value to RGB.
+ // *Assumes:* h is contained in [0, 1] or [0, 360] and s and l are contained [0, 1] or [0, 100]
+ // *Returns:* { r, g, b } in the set [0, 255]
+ function hslToRgb(h, s, l) {
+ var r, g, b;
+
+ h = bound01(h, 360);
+ s = bound01(s, 100);
+ l = bound01(l, 100);
+
+ function hue2rgb(p, q, t) {
+ if(t < 0) t += 1;
+ if(t > 1) t -= 1;
+ if(t < 1/6) return p + (q - p) * 6 * t;
+ if(t < 1/2) return q;
+ if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
+ return p;
+ }
+
+ if(s === 0) {
+ r = g = b = l; // achromatic
+ }
+ else {
+ var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+ var p = 2 * l - q;
+ r = hue2rgb(p, q, h + 1/3);
+ g = hue2rgb(p, q, h);
+ b = hue2rgb(p, q, h - 1/3);
+ }
+
+ return { r: r * 255, g: g * 255, b: b * 255 };
+ }
+
+ // `rgbToHsv`
+ // Converts an RGB color value to HSV
+ // *Assumes:* r, g, and b are contained in the set [0, 255] or [0, 1]
+ // *Returns:* { h, s, v } in [0,1]
+ function rgbToHsv(r, g, b) {
+
+ r = bound01(r, 255);
+ g = bound01(g, 255);
+ b = bound01(b, 255);
+
+ var max = mathMax(r, g, b), min = mathMin(r, g, b);
+ var h, s, v = max;
+
+ var d = max - min;
+ s = max === 0 ? 0 : d / max;
+
+ if(max == min) {
+ h = 0; // achromatic
+ }
+ else {
+ switch(max) {
+ case r: h = (g - b) / d + (g < b ? 6 : 0); break;
+ case g: h = (b - r) / d + 2; break;
+ case b: h = (r - g) / d + 4; break;
+ }
+ h /= 6;
+ }
+ return { h: h, s: s, v: v };
+ }
+
+ // `hsvToRgb`
+ // Converts an HSV color value to RGB.
+ // *Assumes:* h is contained in [0, 1] or [0, 360] and s and v are contained in [0, 1] or [0, 100]
+ // *Returns:* { r, g, b } in the set [0, 255]
+ function hsvToRgb(h, s, v) {
+
+ h = bound01(h, 360) * 6;
+ s = bound01(s, 100);
+ v = bound01(v, 100);
+
+ var i = math.floor(h),
+ f = h - i,
+ p = v * (1 - s),
+ q = v * (1 - f * s),
+ t = v * (1 - (1 - f) * s),
+ mod = i % 6,
+ r = [v, q, p, p, t, v][mod],
+ g = [t, v, v, q, p, p][mod],
+ b = [p, p, t, v, v, q][mod];
+
+ return { r: r * 255, g: g * 255, b: b * 255 };
+ }
+
+ // `rgbToHex`
+ // Converts an RGB color to hex
+ // Assumes r, g, and b are contained in the set [0, 255]
+ // Returns a 3 or 6 character hex
+ function rgbToHex(r, g, b, allow3Char) {
+
+ var hex = [
+ pad2(mathRound(r).toString(16)),
+ pad2(mathRound(g).toString(16)),
+ pad2(mathRound(b).toString(16))
+ ];
+
+ // Return a 3 character hex if possible
+ if (allow3Char && hex[0].charAt(0) == hex[0].charAt(1) && hex[1].charAt(0) == hex[1].charAt(1) && hex[2].charAt(0) == hex[2].charAt(1)) {
+ return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0);
+ }
+
+ return hex.join("");
+ }
+ // `rgbaToHex`
+ // Converts an RGBA color plus alpha transparency to hex
+ // Assumes r, g, b and a are contained in the set [0, 255]
+ // Returns an 8 character hex
+ function rgbaToHex(r, g, b, a) {
+
+ var hex = [
+ pad2(convertDecimalToHex(a)),
+ pad2(mathRound(r).toString(16)),
+ pad2(mathRound(g).toString(16)),
+ pad2(mathRound(b).toString(16))
+ ];
+
+ return hex.join("");
+ }
+
+ // `equals`
+ // Can be called with any tinycolor input
+ tinycolor.equals = function (color1, color2) {
+ if (!color1 || !color2) { return false; }
+ return tinycolor(color1).toRgbString() == tinycolor(color2).toRgbString();
+ };
+ tinycolor.random = function() {
+ return tinycolor.fromRatio({
+ r: mathRandom(),
+ g: mathRandom(),
+ b: mathRandom()
+ });
+ };
+
+
+ // Modification Functions
+ // ----------------------
+ // Thanks to less.js for some of the basics here
+ //
+
+ function desaturate(color, amount) {
+ amount = (amount === 0) ? 0 : (amount || 10);
+ var hsl = tinycolor(color).toHsl();
+ hsl.s -= amount / 100;
+ hsl.s = clamp01(hsl.s);
+ return tinycolor(hsl);
+ }
+
+ function saturate(color, amount) {
+ amount = (amount === 0) ? 0 : (amount || 10);
+ var hsl = tinycolor(color).toHsl();
+ hsl.s += amount / 100;
+ hsl.s = clamp01(hsl.s);
+ return tinycolor(hsl);
+ }
+
+ function greyscale(color) {
+ return tinycolor(color).desaturate(100);
+ }
+
+ function lighten (color, amount) {
+ amount = (amount === 0) ? 0 : (amount || 10);
+ var hsl = tinycolor(color).toHsl();
+ hsl.l += amount / 100;
+ hsl.l = clamp01(hsl.l);
+ return tinycolor(hsl);
+ }
+
+ function brighten(color, amount) {
+ amount = (amount === 0) ? 0 : (amount || 10);
+ var rgb = tinycolor(color).toRgb();
+ rgb.r = mathMax(0, mathMin(255, rgb.r - mathRound(255 * - (amount / 100))));
+ rgb.g = mathMax(0, mathMin(255, rgb.g - mathRound(255 * - (amount / 100))));
+ rgb.b = mathMax(0, mathMin(255, rgb.b - mathRound(255 * - (amount / 100))));
+ return tinycolor(rgb);
+ }
+
+ function darken (color, amount) {
+ amount = (amount === 0) ? 0 : (amount || 10);
+ var hsl = tinycolor(color).toHsl();
+ hsl.l -= amount / 100;
+ hsl.l = clamp01(hsl.l);
+ return tinycolor(hsl);
+ }
+
+ // Spin takes a positive or negative amount within [-360, 360] indicating the change of hue.
+ // Values outside of this range will be wrapped into this range.
+ function spin(color, amount) {
+ var hsl = tinycolor(color).toHsl();
+ var hue = (mathRound(hsl.h) + amount) % 360;
+ hsl.h = hue < 0 ? 360 + hue : hue;
+ return tinycolor(hsl);
+ }
+
+ // Combination Functions
+ // ---------------------
+ // Thanks to jQuery xColor for some of the ideas behind these
+ //
+
+ function complement(color) {
+ var hsl = tinycolor(color).toHsl();
+ hsl.h = (hsl.h + 180) % 360;
+ return tinycolor(hsl);
+ }
+
+ function triad(color) {
+ var hsl = tinycolor(color).toHsl();
+ var h = hsl.h;
+ return [
+ tinycolor(color),
+ tinycolor({ h: (h + 120) % 360, s: hsl.s, l: hsl.l }),
+ tinycolor({ h: (h + 240) % 360, s: hsl.s, l: hsl.l })
+ ];
+ }
+
+ function tetrad(color) {
+ var hsl = tinycolor(color).toHsl();
+ var h = hsl.h;
+ return [
+ tinycolor(color),
+ tinycolor({ h: (h + 90) % 360, s: hsl.s, l: hsl.l }),
+ tinycolor({ h: (h + 180) % 360, s: hsl.s, l: hsl.l }),
+ tinycolor({ h: (h + 270) % 360, s: hsl.s, l: hsl.l })
+ ];
+ }
+
+ function splitcomplement(color) {
+ var hsl = tinycolor(color).toHsl();
+ var h = hsl.h;
+ return [
+ tinycolor(color),
+ tinycolor({ h: (h + 72) % 360, s: hsl.s, l: hsl.l}),
+ tinycolor({ h: (h + 216) % 360, s: hsl.s, l: hsl.l})
+ ];
+ }
+
+ function analogous(color, results, slices) {
+ results = results || 6;
+ slices = slices || 30;
+
+ var hsl = tinycolor(color).toHsl();
+ var part = 360 / slices;
+ var ret = [tinycolor(color)];
+
+ for (hsl.h = ((hsl.h - (part * results >> 1)) + 720) % 360; --results; ) {
+ hsl.h = (hsl.h + part) % 360;
+ ret.push(tinycolor(hsl));
+ }
+ return ret;
+ }
+
+ function monochromatic(color, results) {
+ results = results || 6;
+ var hsv = tinycolor(color).toHsv();
+ var h = hsv.h, s = hsv.s, v = hsv.v;
+ var ret = [];
+ var modification = 1 / results;
+
+ while (results--) {
+ ret.push(tinycolor({ h: h, s: s, v: v}));
+ v = (v + modification) % 1;
+ }
+
+ return ret;
+ }
+
+ // Utility Functions
+ // ---------------------
+
+ tinycolor.mix = function(color1, color2, amount) {
+ amount = (amount === 0) ? 0 : (amount || 50);
+
+ var rgb1 = tinycolor(color1).toRgb();
+ var rgb2 = tinycolor(color2).toRgb();
+
+ var p = amount / 100;
+ var w = p * 2 - 1;
+ var a = rgb2.a - rgb1.a;
+
+ var w1;
+
+ if (w * a == -1) {
+ w1 = w;
+ } else {
+ w1 = (w + a) / (1 + w * a);
+ }
+
+ w1 = (w1 + 1) / 2;
+
+ var w2 = 1 - w1;
+
+ var rgba = {
+ r: rgb2.r * w1 + rgb1.r * w2,
+ g: rgb2.g * w1 + rgb1.g * w2,
+ b: rgb2.b * w1 + rgb1.b * w2,
+ a: rgb2.a * p + rgb1.a * (1 - p)
+ };
+
+ return tinycolor(rgba);
+ };
+
+
+ // Readability Functions
+ // ---------------------
+ //
+
+ // `readability`
+ // Analyze the 2 colors and returns an object with the following properties:
+ // `brightness`: difference in brightness between the two colors
+ // `color`: difference in color/hue between the two colors
+ tinycolor.readability = function(color1, color2) {
+ var c1 = tinycolor(color1);
+ var c2 = tinycolor(color2);
+ var rgb1 = c1.toRgb();
+ var rgb2 = c2.toRgb();
+ var brightnessA = c1.getBrightness();
+ var brightnessB = c2.getBrightness();
+ var colorDiff = (
+ Math.max(rgb1.r, rgb2.r) - Math.min(rgb1.r, rgb2.r) +
+ Math.max(rgb1.g, rgb2.g) - Math.min(rgb1.g, rgb2.g) +
+ Math.max(rgb1.b, rgb2.b) - Math.min(rgb1.b, rgb2.b)
+ );
+
+ return {
+ brightness: Math.abs(brightnessA - brightnessB),
+ color: colorDiff
+ };
+ };
+
+ // `readable`
+ // http://www.w3.org/TR/AERT#color-contrast
+ // Ensure that foreground and background color combinations provide sufficient contrast.
+ // *Example*
+ // tinycolor.isReadable("#000", "#111") => false
+ tinycolor.isReadable = function(color1, color2) {
+ var readability = tinycolor.readability(color1, color2);
+ return readability.brightness > 125 && readability.color > 500;
+ };
+
+ // `mostReadable`
+ // Given a base color and a list of possible foreground or background
+ // colors for that base, returns the most readable color.
+ // *Example*
+ // tinycolor.mostReadable("#123", ["#fff", "#000"]) => "#000"
+ tinycolor.mostReadable = function(baseColor, colorList) {
+ var bestColor = null;
+ var bestScore = 0;
+ var bestIsReadable = false;
+ for (var i=0; i < colorList.length; i++) {
+
+ // We normalize both around the "acceptable" breaking point,
+ // but rank brightness constrast higher than hue.
+
+ var readability = tinycolor.readability(baseColor, colorList[i]);
+ var readable = readability.brightness > 125 && readability.color > 500;
+ var score = 3 * (readability.brightness / 125) + (readability.color / 500);
+
+ if ((readable && ! bestIsReadable) ||
+ (readable && bestIsReadable && score > bestScore) ||
+ ((! readable) && (! bestIsReadable) && score > bestScore)) {
+ bestIsReadable = readable;
+ bestScore = score;
+ bestColor = tinycolor(colorList[i]);
+ }
+ }
+ return bestColor;
+ };
+
+
+ // Big List of Colors
+ // ------------------
+ //
+ var names = tinycolor.names = {
+ aliceblue: "f0f8ff",
+ antiquewhite: "faebd7",
+ aqua: "0ff",
+ aquamarine: "7fffd4",
+ azure: "f0ffff",
+ beige: "f5f5dc",
+ bisque: "ffe4c4",
+ black: "000",
+ blanchedalmond: "ffebcd",
+ blue: "00f",
+ blueviolet: "8a2be2",
+ brown: "a52a2a",
+ burlywood: "deb887",
+ burntsienna: "ea7e5d",
+ cadetblue: "5f9ea0",
+ chartreuse: "7fff00",
+ chocolate: "d2691e",
+ coral: "ff7f50",
+ cornflowerblue: "6495ed",
+ cornsilk: "fff8dc",
+ crimson: "dc143c",
+ cyan: "0ff",
+ darkblue: "00008b",
+ darkcyan: "008b8b",
+ darkgoldenrod: "b8860b",
+ darkgray: "a9a9a9",
+ darkgreen: "006400",
+ darkgrey: "a9a9a9",
+ darkkhaki: "bdb76b",
+ darkmagenta: "8b008b",
+ darkolivegreen: "556b2f",
+ darkorange: "ff8c00",
+ darkorchid: "9932cc",
+ darkred: "8b0000",
+ darksalmon: "e9967a",
+ darkseagreen: "8fbc8f",
+ darkslateblue: "483d8b",
+ darkslategray: "2f4f4f",
+ darkslategrey: "2f4f4f",
+ darkturquoise: "00ced1",
+ darkviolet: "9400d3",
+ deeppink: "ff1493",
+ deepskyblue: "00bfff",
+ dimgray: "696969",
+ dimgrey: "696969",
+ dodgerblue: "1e90ff",
+ firebrick: "b22222",
+ floralwhite: "fffaf0",
+ forestgreen: "228b22",
+ fuchsia: "f0f",
+ gainsboro: "dcdcdc",
+ ghostwhite: "f8f8ff",
+ gold: "ffd700",
+ goldenrod: "daa520",
+ gray: "808080",
+ green: "008000",
+ greenyellow: "adff2f",
+ grey: "808080",
+ honeydew: "f0fff0",
+ hotpink: "ff69b4",
+ indianred: "cd5c5c",
+ indigo: "4b0082",
+ ivory: "fffff0",
+ khaki: "f0e68c",
+ lavender: "e6e6fa",
+ lavenderblush: "fff0f5",
+ lawngreen: "7cfc00",
+ lemonchiffon: "fffacd",
+ lightblue: "add8e6",
+ lightcoral: "f08080",
+ lightcyan: "e0ffff",
+ lightgoldenrodyellow: "fafad2",
+ lightgray: "d3d3d3",
+ lightgreen: "90ee90",
+ lightgrey: "d3d3d3",
+ lightpink: "ffb6c1",
+ lightsalmon: "ffa07a",
+ lightseagreen: "20b2aa",
+ lightskyblue: "87cefa",
+ lightslategray: "789",
+ lightslategrey: "789",
+ lightsteelblue: "b0c4de",
+ lightyellow: "ffffe0",
+ lime: "0f0",
+ limegreen: "32cd32",
+ linen: "faf0e6",
+ magenta: "f0f",
+ maroon: "800000",
+ mediumaquamarine: "66cdaa",
+ mediumblue: "0000cd",
+ mediumorchid: "ba55d3",
+ mediumpurple: "9370db",
+ mediumseagreen: "3cb371",
+ mediumslateblue: "7b68ee",
+ mediumspringgreen: "00fa9a",
+ mediumturquoise: "48d1cc",
+ mediumvioletred: "c71585",
+ midnightblue: "191970",
+ mintcream: "f5fffa",
+ mistyrose: "ffe4e1",
+ moccasin: "ffe4b5",
+ navajowhite: "ffdead",
+ navy: "000080",
+ oldlace: "fdf5e6",
+ olive: "808000",
+ olivedrab: "6b8e23",
+ orange: "ffa500",
+ orangered: "ff4500",
+ orchid: "da70d6",
+ palegoldenrod: "eee8aa",
+ palegreen: "98fb98",
+ paleturquoise: "afeeee",
+ palevioletred: "db7093",
+ papayawhip: "ffefd5",
+ peachpuff: "ffdab9",
+ peru: "cd853f",
+ pink: "ffc0cb",
+ plum: "dda0dd",
+ powderblue: "b0e0e6",
+ purple: "800080",
+ red: "f00",
+ rosybrown: "bc8f8f",
+ royalblue: "4169e1",
+ saddlebrown: "8b4513",
+ salmon: "fa8072",
+ sandybrown: "f4a460",
+ seagreen: "2e8b57",
+ seashell: "fff5ee",
+ sienna: "a0522d",
+ silver: "c0c0c0",
+ skyblue: "87ceeb",
+ slateblue: "6a5acd",
+ slategray: "708090",
+ slategrey: "708090",
+ snow: "fffafa",
+ springgreen: "00ff7f",
+ steelblue: "4682b4",
+ tan: "d2b48c",
+ teal: "008080",
+ thistle: "d8bfd8",
+ tomato: "ff6347",
+ turquoise: "40e0d0",
+ violet: "ee82ee",
+ wheat: "f5deb3",
+ white: "fff",
+ whitesmoke: "f5f5f5",
+ yellow: "ff0",
+ yellowgreen: "9acd32"
+ };
+
+ // Make it easy to access colors via `hexNames[hex]`
+ var hexNames = tinycolor.hexNames = flip(names);
+
+
+ // Utilities
+ // ---------
+
+ // `{ 'name1': 'val1' }` becomes `{ 'val1': 'name1' }`
+ function flip(o) {
+ var flipped = { };
+ for (var i in o) {
+ if (o.hasOwnProperty(i)) {
+ flipped[o[i]] = i;
+ }
+ }
+ return flipped;
+ }
+
+ // Return a valid alpha value [0,1] with all invalid values being set to 1
+ function boundAlpha(a) {
+ a = parseFloat(a);
+
+ if (isNaN(a) || a < 0 || a > 1) {
+ a = 1;
+ }
+
+ return a;
+ }
+
+ // Take input from [0, n] and return it as [0, 1]
+ function bound01(n, max) {
+ if (isOnePointZero(n)) { n = "100%"; }
+
+ var processPercent = isPercentage(n);
+ n = mathMin(max, mathMax(0, parseFloat(n)));
+
+ // Automatically convert percentage into number
+ if (processPercent) {
+ n = parseInt(n * max, 10) / 100;
+ }
+
+ // Handle floating point rounding errors
+ if ((math.abs(n - max) < 0.000001)) {
+ return 1;
+ }
+
+ // Convert into [0, 1] range if it isn't already
+ return (n % max) / parseFloat(max);
+ }
+
+ // Force a number between 0 and 1
+ function clamp01(val) {
+ return mathMin(1, mathMax(0, val));
+ }
+
+ // Parse a base-16 hex value into a base-10 integer
+ function parseIntFromHex(val) {
+ return parseInt(val, 16);
+ }
+
+ // Need to handle 1.0 as 100%, since once it is a number, there is no difference between it and 1
+ //
+ function isOnePointZero(n) {
+ return typeof n == "string" && n.indexOf('.') != -1 && parseFloat(n) === 1;
+ }
+
+ // Check to see if string passed in is a percentage
+ function isPercentage(n) {
+ return typeof n === "string" && n.indexOf('%') != -1;
+ }
+
+ // Force a hex value to have 2 characters
+ function pad2(c) {
+ return c.length == 1 ? '0' + c : '' + c;
+ }
+
+ // Replace a decimal with it's percentage value
+ function convertToPercentage(n) {
+ if (n <= 1) {
+ n = (n * 100) + "%";
+ }
+
+ return n;
+ }
+
+ // Converts a decimal to a hex value
+ function convertDecimalToHex(d) {
+ return Math.round(parseFloat(d) * 255).toString(16);
+ }
+ // Converts a hex value to a decimal
+ function convertHexToDecimal(h) {
+ return (parseIntFromHex(h) / 255);
+ }
+
+ var matchers = (function() {
+
+ //
+ var CSS_INTEGER = "[-\\+]?\\d+%?";
+
+ //
+ var CSS_NUMBER = "[-\\+]?\\d*\\.\\d+%?";
+
+ // Allow positive/negative integer/number. Don't capture the either/or, just the entire outcome.
+ var CSS_UNIT = "(?:" + CSS_NUMBER + ")|(?:" + CSS_INTEGER + ")";
+
+ // Actual matching.
+ // Parentheses and commas are optional, but not required.
+ // Whitespace can take the place of commas or opening paren
+ var PERMISSIVE_MATCH3 = "[\\s|\\(]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")\\s*\\)?";
+ var PERMISSIVE_MATCH4 = "[\\s|\\(]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")\\s*\\)?";
+
+ return {
+ rgb: new RegExp("rgb" + PERMISSIVE_MATCH3),
+ rgba: new RegExp("rgba" + PERMISSIVE_MATCH4),
+ hsl: new RegExp("hsl" + PERMISSIVE_MATCH3),
+ hsla: new RegExp("hsla" + PERMISSIVE_MATCH4),
+ hsv: new RegExp("hsv" + PERMISSIVE_MATCH3),
+ hex3: /^([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
+ hex6: /^([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
+ hex8: /^([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/
+ };
+ })();
+
+ // `stringInputToObject`
+ // Permissive string parsing. Take in a number of formats, and output an object
+ // based on detected format. Returns `{ r, g, b }` or `{ h, s, l }` or `{ h, s, v}`
+ function stringInputToObject(color) {
+
+ color = color.replace(trimLeft,'').replace(trimRight, '').toLowerCase();
+ var named = false;
+ if (names[color]) {
+ color = names[color];
+ named = true;
+ }
+ else if (color == 'transparent') {
+ return { r: 0, g: 0, b: 0, a: 0, format: "name" };
+ }
+
+ // Try to match string input using regular expressions.
+ // Keep most of the number bounding out of this function - don't worry about [0,1] or [0,100] or [0,360]
+ // Just return an object and let the conversion functions handle that.
+ // This way the result will be the same whether the tinycolor is initialized with string or object.
+ var match;
+ if ((match = matchers.rgb.exec(color))) {
+ return { r: match[1], g: match[2], b: match[3] };
+ }
+ if ((match = matchers.rgba.exec(color))) {
+ return { r: match[1], g: match[2], b: match[3], a: match[4] };
+ }
+ if ((match = matchers.hsl.exec(color))) {
+ return { h: match[1], s: match[2], l: match[3] };
+ }
+ if ((match = matchers.hsla.exec(color))) {
+ return { h: match[1], s: match[2], l: match[3], a: match[4] };
+ }
+ if ((match = matchers.hsv.exec(color))) {
+ return { h: match[1], s: match[2], v: match[3] };
+ }
+ if ((match = matchers.hex8.exec(color))) {
+ return {
+ a: convertHexToDecimal(match[1]),
+ r: parseIntFromHex(match[2]),
+ g: parseIntFromHex(match[3]),
+ b: parseIntFromHex(match[4]),
+ format: named ? "name" : "hex8"
+ };
+ }
+ if ((match = matchers.hex6.exec(color))) {
+ return {
+ r: parseIntFromHex(match[1]),
+ g: parseIntFromHex(match[2]),
+ b: parseIntFromHex(match[3]),
+ format: named ? "name" : "hex"
+ };
+ }
+ if ((match = matchers.hex3.exec(color))) {
+ return {
+ r: parseIntFromHex(match[1] + '' + match[1]),
+ g: parseIntFromHex(match[2] + '' + match[2]),
+ b: parseIntFromHex(match[3] + '' + match[3]),
+ format: named ? "name" : "hex"
+ };
+ }
+
+ return false;
+ }
+
+ // Node: Export function
+ if (typeof module !== "undefined" && module.exports) {
+ module.exports = tinycolor;
+ }
+ // AMD/requirejs: Define the module
+ else if (typeof define === 'function' && define.amd) {
+ define(function () {return tinycolor;});
+ }
+ // Browser: Expose to window
+ else {
+ window.tinycolor = tinycolor;
+ }
+
+})();
\ No newline at end of file
diff --git a/assets/webconfig/manifest.json b/assets/webconfig/manifest.json
new file mode 100644
index 00000000..c15d41a0
--- /dev/null
+++ b/assets/webconfig/manifest.json
@@ -0,0 +1,32 @@
+{
+ "name": "hyperion remote control",
+ "short_name": "hyperion remote",
+ "description": "Client side app for controlling the hyperion server over the local network.",
+ "author": {
+ "name": "Daniel Wiese",
+ "email": "gamadril.dev@gmail.com"
+ },
+ "manifest_version": 2,
+ "version": "0.6.0",
+ "minimum_chrome_version": "36",
+ "permissions": [
+ "storage", "system.network"
+ ],
+ "app": {
+ "background": {
+ "scripts": ["js/background.js"]
+ }
+ },
+ "sockets": {
+ "tcp": {
+ "connect": "*"
+ },
+ "udp": {
+ "send": "*",
+ "bind": "*"
+ }
+ },
+ "icons": {
+ "128": "res/icon_128.png"
+ }
+}
diff --git a/assets/webconfig/manifest.webapp b/assets/webconfig/manifest.webapp
new file mode 100644
index 00000000..8acc29e1
--- /dev/null
+++ b/assets/webconfig/manifest.webapp
@@ -0,0 +1,14 @@
+{
+ "name": "hyperion remote control",
+ "description": "Client side app for controlling the hyperion server over the local network.",
+ "launch_path": "/hyperion-remote/index.html",
+ "icons": {
+ "128": "/hyperion-remote/res/icon_128.png"
+ },
+ "developer": {
+ "name": "Daniel Wiese",
+ "email": "gamadril.dev@gmail.com",
+ "url": "https://github.com/Gamadril"
+ },
+ "default_locale": "en"
+}
\ No newline at end of file
diff --git a/assets/webconfig/res/colorwheel.png b/assets/webconfig/res/colorwheel.png
new file mode 100644
index 00000000..1db8ad5b
Binary files /dev/null and b/assets/webconfig/res/colorwheel.png differ
diff --git a/assets/webconfig/res/fontello.ttf b/assets/webconfig/res/fontello.ttf
new file mode 100644
index 00000000..00330dc2
Binary files /dev/null and b/assets/webconfig/res/fontello.ttf differ
diff --git a/assets/webconfig/res/fontello.woff b/assets/webconfig/res/fontello.woff
new file mode 100644
index 00000000..f5cf5dd5
Binary files /dev/null and b/assets/webconfig/res/fontello.woff differ
diff --git a/assets/webconfig/res/icon_128.png b/assets/webconfig/res/icon_128.png
new file mode 100644
index 00000000..a8caaf4a
Binary files /dev/null and b/assets/webconfig/res/icon_128.png differ
diff --git a/cmake/debian/postinst b/cmake/debian/postinst
index 84136321..37234585 100644
--- a/cmake/debian/postinst
+++ b/cmake/debian/postinst
@@ -20,7 +20,7 @@ install_file()
echo "--- hyperion ambilight postinstall ---"
echo "- install configuration template"
mkdir -p /etc/hyperion
-install_file /usr/share/hyperion/config/hyperion.config.json /etc/hyperion/hyperion.config.json
+install_file /usr/share/hyperion/config/hyperion.config.json.example /etc/hyperion/hyperion.config.json
HYPERION_RUNNING=false
diff --git a/config/hyperion.config.json.example b/config/hyperion.config.json.example
index de046dbf..c9ce0e9b 100644
--- a/config/hyperion.config.json.example
+++ b/config/hyperion.config.json.example
@@ -42,7 +42,7 @@
/// tuning parameters:
/// - 'saturationGain' The gain adjustement of the saturation
/// - 'luminanceGain' The gain adjustement of the luminance
- /// - 'luminanceMinimum' The minimum luminance (backlight)
+ /// - 'luminanceMinimum' The minimum luminance (backlight)
/// * 'red'/'green'/'blue' : The manipulation in the Red-Green-Blue color domain with the
/// following tuning parameters for each channel:
/// - 'threshold' The minimum required input value for the channel to be on
@@ -65,21 +65,21 @@
"leds" : "*",
"pureRed" :
{
- "redChannel" : 255,
- "greenChannel" : 0,
- "blueChannel" : 0
+ "redChannel" : 255,
+ "greenChannel" : 0,
+ "blueChannel" : 0
},
"pureGreen" :
{
- "redChannel" : 0,
- "greenChannel" : 255,
- "blueChannel" : 0
+ "redChannel" : 0,
+ "greenChannel" : 255,
+ "blueChannel" : 0
},
"pureBlue" :
{
- "redChannel" : 0,
- "greenChannel" : 0,
- "blueChannel" : 255
+ "redChannel" : 0,
+ "greenChannel" : 0,
+ "blueChannel" : 255
}
}
],
@@ -90,9 +90,9 @@
"leds" : "*",
"temperatureValues" :
{
- "red" : 255,
- "green" : 255,
- "blue" : 255
+ "red" : 255,
+ "green" : 255,
+ "blue" : 255
}
}
],
@@ -103,24 +103,24 @@
"leds" : "*",
"hsl" :
{
- "saturationGain" : 1.0000,
- "luminanceGain" : 1.0000,
- "luminanceMinimum" : 0.0000
+ "saturationGain" : 1.0000,
+ "luminanceGain" : 1.0000,
+ "luminanceMinimum" : 0.0000
},
"red" :
{
- "threshold" : 0.0000,
- "gamma" : 2.5000
+ "threshold" : 0.0000,
+ "gamma" : 2.5000
},
"green" :
{
- "threshold" : 0.0000,
- "gamma" : 2.5000
+ "threshold" : 0.0000,
+ "gamma" : 2.5000
},
"blue" :
{
- "threshold" : 0.0000,
- "gamma" : 2.5000
+ "threshold" : 0.0000,
+ "gamma" : 2.5000
}
}
],
@@ -135,14 +135,14 @@
},
/// The black border configuration, contains the following items:
- /// * enable : true if the detector should be activated
- /// * threshold : Value below which a pixel is regarded as black (value between 0.0 and 1.0)
- /// * unknownFrameCnt : Number of frames without any detection before the border is set to 0 (default 600)
- /// * borderFrameCnt : Number of frames before a consistent detected border gets set (default 50)
- /// * maxInconsistentCnt : Number of inconsistent frames that are ignored before a new border gets a chance to proof consistency
- /// * blurRemoveCnt : Number of pixels that get removed from the detected border to cut away blur (default 1)
- /// * mode : Border detection mode (values=default,classic,osd)
- "blackborderdetector" :
+ /// * enable : true if the detector should be activated
+ /// * threshold : Value below which a pixel is regarded as black (value between 0.0 and 1.0)
+ /// * unknownFrameCnt : Number of frames without any detection before the border is set to 0 (default 600)
+ /// * borderFrameCnt : Number of frames before a consistent detected border gets set (default 50)
+ /// * maxInconsistentCnt : Number of inconsistent frames that are ignored before a new border gets a chance to proof consistency
+ /// * blurRemoveCnt : Number of pixels that get removed from the detected border to cut away blur (default 1)
+ /// * mode : Border detection mode (values=default,classic,osd)
+ "blackborderdetector" :
{
"enable" : true,
"threshold" : 0.0,
@@ -154,19 +154,23 @@
},
/// The configuration of the effect engine, contains the following items:
- /// * paths : An array with absolute location(s) of directories with effects
- /// * color : Set static color after boot -> set effect to "" (empty) and input the values [R,G,B] and set duration_ms NOT to 0 (use 1) instead
- /// * effect : The effect selected as 'boot sequence'
- /// * duration_ms : The duration of the selected effect (0=endless)
- /// * priority : The priority of the selected effect/static color (default=990) HINT: lower value result in HIGHER priority!
+ /// * paths : An array with absolute/relative location(s) of directories with effects
"effects" :
{
- "paths" :
+ "paths" :
[
- "/opt/hyperion/effects"
+ "/usr/share/hyperion/effects"
]
},
+ /// Boot sequence configuration. Start effect / set color at startup of hyperion
+ /// HINT inital background color is not shown, when any other grabber is active
+ /// * color : Set initial background color on startup -> set effect to "" (empty) and input the values [R,G,B] and set duration_ms NOT to 0 (use 1) instead
+ /// * effect : The effect is shown when hyperion starts
+ /// * duration_ms : The duration of the selected effect (0=endless)
+ /// * priority : The priority of the selected effect/initial background color (default=990, if duration is 0)
+ /// when duration > 0 => priority is set to 0, otherwise priority is set to configured value
+ /// HINT: lower value result in HIGHER priority!
"bootsequence" :
{
"color" : [0,0,0],
@@ -175,12 +179,23 @@
"priority" : 990
},
+ /// Configuration of webserver integrated in hyperion.
+ /// * enable : enable the server or not
+ /// * document_root : path to hyperion webapp files
+ /// * port : the port where hyperion webapp is accasible
+ "webConfig" :
+ {
+ "enable" : true,
+ "document_root" : "/usr/share/hyperion/webconfig",
+ "port" : 8080
+ },
+
/// The configuration of the Json/Proto forwarder. Forward messages to multiple instances of Hyperion on same and/or other hosts
/// 'proto' is mostly used for video streams and 'json' for effects
- /// * proto : Proto server adress and port of your target. Syntax:[IP:PORT] -> ["127.0.0.1:19447"] or more instances to forward ["127.0.0.1:19447","192.168.0.24:19449"]
- /// * json : Json server adress and port of your target. Syntax:[IP:PORT] -> ["127.0.0.1:19446"] or more instances to forward ["127.0.0.1:19446","192.168.0.24:19448"]
- /// HINT: If you redirect to "127.0.0.1" (localhost) you could start a second hyperion with another device/led config!
- /// Be sure your client(s) is/are listening on the configured ports. The second Hyperion (if used) also needs to be configured! (HyperCon -> External -> Json Server/Proto Server)
+ /// * proto : Proto server adress and port of your target. Syntax:[IP:PORT] -> ["127.0.0.1:19447"] or more instances to forward ["127.0.0.1:19447","192.168.0.24:19449"]
+ /// * json : Json server adress and port of your target. Syntax:[IP:PORT] -> ["127.0.0.1:19446"] or more instances to forward ["127.0.0.1:19446","192.168.0.24:19448"]
+ /// HINT:If you redirect to "127.0.0.1" (localhost) you could start a second hyperion with another device/led config!
+ /// Be sure your client(s) is/are listening on the configured ports. The second Hyperion (if used) also needs to be configured! (HyperCon -> External -> Json Server/Proto Server)
"forwarder" :
{
"proto" : ["127.0.0.1:19447"],
@@ -226,31 +241,31 @@
/// The configuration of the Json server which enables the json remote interface
/// * port : Port at which the json server is started
- "jsonServer" :
+ "jsonServer" :
{
"port" : 19444
},
/// The configuration of the Proto server which enables the protobuffer remote interface
/// * port : Port at which the protobuffer server is started
- "protoServer" :
+ "protoServer" :
{
"port" : 19445
},
/// The configuration of the boblight server which enables the boblight remote interface
- /// * port : Port at which the boblight server is started
- /// * priority: Priority of the boblight server (Default=900) HINT: lower value result in HIGHER priority!
- "boblightServer" :
+ /// * port : Port at which the boblight server is started
+ /// * priority : Priority of the boblight server (Default=900) HINT: lower value result in HIGHER priority!
+ "boblightServer" :
{
"port" : 19333,
"priority" : 900
},
/// Configuration for the embedded V4L2 grabber
- /// * device : V4L2 Device to use [default="/dev/video0"]
- /// * input : V4L2 input to use [default=0]
- /// * standard : Video standard (no-change/PAL/NTSC) [default="no-change"]
+ /// * device : V4L2 Device to use [default="/dev/video0"]
+ /// * input : V4L2 input to use [default=0]
+ /// * standard : Video standard (no-change/PAL/NTSC) [default="no-change"]
/// * width : V4L2 width to set [default=-1]
/// * height : V4L2 height to set [default=-1]
/// * frameDecimation : Frame decimation factor [default=2]
@@ -264,25 +279,25 @@
/// * redSignalThreshold : Signal threshold for the red channel between 0.0 and 1.0 [default=0.0]
/// * greenSignalThreshold : Signal threshold for the green channel between 0.0 and 1.0 [default=0.0]
/// * blueSignalThreshold : Signal threshold for the blue channel between 0.0 and 1.0 [default=0.0]
- "grabber-v4l2" :
- {
- "device" : "/dev/video0",
- "input" : 0,
- "standard" : "no-change",
- "width" : -1,
- "height" : -1,
- "frameDecimation" : 2,
- "sizeDecimation" : 8,
- "priority" : 900,
- "mode" : "2D",
- "cropLeft" : 0,
- "cropRight" : 0,
- "cropTop" : 0,
- "cropBottom" : 0,
- "redSignalThreshold" : 0.0,
- "greenSignalThreshold" : 0.0,
- "blueSignalThreshold" : 0.0
- },
+ "grabber-v4l2" :
+ {
+ "device" : "/dev/video0",
+ "input" : 0,
+ "standard" : "no-change",
+ "width" : -1,
+ "height" : -1,
+ "frameDecimation" : 2,
+ "sizeDecimation" : 8,
+ "priority" : 900,
+ "mode" : "2D",
+ "cropLeft" : 0,
+ "cropRight" : 0,
+ "cropTop" : 0,
+ "cropBottom" : 0,
+ "redSignalThreshold" : 0.0,
+ "greenSignalThreshold" : 0.0,
+ "blueSignalThreshold" : 0.0
+ },
/// The configuration for each individual led. This contains the specification of the area
/// averaged of an input image for each led to determine its color. Each item in the list
@@ -293,7 +308,7 @@
/// (minimum and maximum inclusive)
/// * vscan: The fractional part of the image along the vertical used for the averaging
/// (minimum and maximum inclusive)
- "leds" :
+ "leds" :
[
{
"index" : 0,
diff --git a/include/webconfig/WebConfig.h b/include/webconfig/WebConfig.h
new file mode 100644
index 00000000..eba244c3
--- /dev/null
+++ b/include/webconfig/WebConfig.h
@@ -0,0 +1,27 @@
+#ifndef WEBCONFIG_H
+#define WEBCONFIG_H
+
+#include
+#include
+
+class StaticFileServing;
+
+class WebConfig : public QObject {
+ Q_OBJECT
+
+public:
+ explicit WebConfig (std::string baseUrl, quint16 port, QObject * parent = NULL);
+ virtual ~WebConfig (void);
+
+ void start();
+ void stop();
+
+private:
+ QObject * _parent;
+ QString _baseUrl;
+ quint16 _port;
+ StaticFileServing * _server;
+};
+
+#endif // WEBCONFIG_H
+
diff --git a/libsrc/CMakeLists.txt b/libsrc/CMakeLists.txt
index d6b1f044..cf9a3833 100644
--- a/libsrc/CMakeLists.txt
+++ b/libsrc/CMakeLists.txt
@@ -19,3 +19,7 @@ add_subdirectory(utils)
add_subdirectory(xbmcvideochecker)
add_subdirectory(effectengine)
add_subdirectory(grabber)
+
+if(ENABLE_QT5)
+ add_subdirectory(webconfig)
+endif()
diff --git a/libsrc/boblightserver/BoblightClientConnection.cpp b/libsrc/boblightserver/BoblightClientConnection.cpp
index a1c09c8d..d2725135 100644
--- a/libsrc/boblightserver/BoblightClientConnection.cpp
+++ b/libsrc/boblightserver/BoblightClientConnection.cpp
@@ -17,6 +17,7 @@
#include "hyperion/ImageProcessorFactory.h"
#include "hyperion/ImageProcessor.h"
#include "utils/ColorRgb.h"
+#include "HyperionConfig.h"
// project includes
#include "BoblightClientConnection.h"
diff --git a/libsrc/effectengine/EffectEngine.cpp b/libsrc/effectengine/EffectEngine.cpp
index c5a554cd..7e1ca682 100644
--- a/libsrc/effectengine/EffectEngine.cpp
+++ b/libsrc/effectengine/EffectEngine.cpp
@@ -16,6 +16,7 @@
// effect engine includes
#include
#include "Effect.h"
+#include "HyperionConfig.h"
EffectEngine::EffectEngine(Hyperion * hyperion, const Json::Value & jsonEffectConfig) :
_hyperion(hyperion),
diff --git a/libsrc/webconfig/CMakeLists.txt b/libsrc/webconfig/CMakeLists.txt
new file mode 100644
index 00000000..25abbbb8
--- /dev/null
+++ b/libsrc/webconfig/CMakeLists.txt
@@ -0,0 +1,53 @@
+
+# Define the current source locations
+set(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/webconfig)
+set(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/webconfig)
+
+# Group the headers that go through the MOC compiler
+set(WebConfig_QT_HEADERS
+ ${CURRENT_SOURCE_DIR}/QtHttpClientWrapper.h
+ ${CURRENT_SOURCE_DIR}/QtHttpHeader.h
+ ${CURRENT_SOURCE_DIR}/QtHttpReply.h
+ ${CURRENT_SOURCE_DIR}/QtHttpRequest.h
+ ${CURRENT_SOURCE_DIR}/QtHttpServer.h
+ ${CURRENT_SOURCE_DIR}/StaticFileServing.h
+ ${CURRENT_HEADER_DIR}/WebConfig.h
+)
+
+set(WebConfig_HEADERS
+)
+
+set(WebConfig_SOURCES
+ ${CURRENT_SOURCE_DIR}/QtHttpClientWrapper.cpp
+ ${CURRENT_SOURCE_DIR}/QtHttpHeader.cpp
+ ${CURRENT_SOURCE_DIR}/QtHttpReply.cpp
+ ${CURRENT_SOURCE_DIR}/QtHttpRequest.cpp
+ ${CURRENT_SOURCE_DIR}/QtHttpServer.cpp
+ ${CURRENT_SOURCE_DIR}/StaticFileServing.cpp
+ ${CURRENT_SOURCE_DIR}/WebConfig.cpp
+)
+
+if(ENABLE_QT5)
+ qt5_wrap_cpp(WebConfig_HEADERS_MOC ${WebConfig_QT_HEADERS})
+else()
+ qt4_wrap_cpp(WebConfigr_HEADERS_MOC ${WebConfig_QT_HEADERS})
+endif()
+
+add_library(webconfig
+ ${WebConfig_HEADERS}
+ ${WebConfig_QT_HEADERS}
+ ${WebConfig_SOURCES}
+ ${WebConfig_HEADERS_MOC}
+)
+
+if(ENABLE_QT5)
+ qt5_use_modules(webconfig Widgets Network)
+endif()
+
+target_link_libraries(webconfig
+ hyperion
+ hyperion-utils
+ ${QT_LIBRARIES}
+)
+
+
diff --git a/libsrc/webconfig/QtHttpClientWrapper.cpp b/libsrc/webconfig/QtHttpClientWrapper.cpp
new file mode 100644
index 00000000..6e88fb44
--- /dev/null
+++ b/libsrc/webconfig/QtHttpClientWrapper.cpp
@@ -0,0 +1,221 @@
+
+#include "QtHttpClientWrapper.h"
+#include "QtHttpRequest.h"
+#include "QtHttpReply.h"
+#include "QtHttpServer.h"
+#include "QtHttpHeader.h"
+
+#include
+#include
+#include
+#include
+#include
+
+const QByteArray & QtHttpClientWrapper::CRLF = QByteArrayLiteral ("\r\n");
+
+QtHttpClientWrapper::QtHttpClientWrapper (QTcpSocket * sock, QtHttpServer * parent)
+ : QObject (parent)
+ , m_guid ("")
+ , m_parsingStatus (AwaitingRequest)
+ , m_sockClient (sock)
+ , m_currentRequest (Q_NULLPTR)
+ , m_serverHandle (parent)
+{
+ connect (m_sockClient, &QTcpSocket::readyRead, this, &QtHttpClientWrapper::onClientDataReceived);
+}
+
+QString QtHttpClientWrapper::getGuid (void) {
+ if (m_guid.isEmpty ()) {
+ m_guid = QString::fromLocal8Bit (
+ QCryptographicHash::hash (
+ QByteArray::number ((quint64) (this)),
+ QCryptographicHash::Md5
+ ).toHex ()
+ );
+ }
+ return m_guid;
+}
+
+void QtHttpClientWrapper::onClientDataReceived (void) {
+ if (m_sockClient != Q_NULLPTR) {
+ while (m_sockClient->bytesAvailable ()) {
+ QByteArray line = m_sockClient->readLine ();
+ switch (m_parsingStatus) { // handle parsing steps
+ case AwaitingRequest: { // "command url version" × 1
+ QString str = QString::fromUtf8 (line).trimmed ();
+ QStringList parts = str.split (SPACE, QString::SkipEmptyParts);
+ if (parts.size () == 3) {
+ QString command = parts.at (0);
+ QString url = parts.at (1);
+ QString version = parts.at (2);
+ if (version == QtHttpServer::HTTP_VERSION) {
+ //qDebug () << "Debug : HTTP"
+ // << "command :" << command
+ // << "url :" << url
+ // << "version :" << version;
+ m_currentRequest = new QtHttpRequest (m_serverHandle);
+ m_currentRequest->setUrl (QUrl (url));
+ m_currentRequest->setCommand (command);
+ m_parsingStatus = AwaitingHeaders;
+ }
+ else {
+ m_parsingStatus = ParsingError;
+ //qWarning () << "Error : unhandled HTTP version :" << version;
+ }
+ }
+ else {
+ m_parsingStatus = ParsingError;
+ //qWarning () << "Error : incorrect HTTP command line :" << line;
+ }
+ break;
+ }
+ case AwaitingHeaders: { // "header: value" × N (until empty line)
+ QByteArray raw = line.trimmed ();
+ if (!raw.isEmpty ()) { // parse headers
+ int pos = raw.indexOf (COLON);
+ if (pos > 0) {
+ QByteArray header = raw.left (pos).trimmed ();
+ QByteArray value = raw.mid (pos +1).trimmed ();
+ //qDebug () << "Debug : HTTP"
+ // << "header :" << header
+ // << "value :" << value;
+ m_currentRequest->addHeader (header, value);
+ if (header == QtHttpHeader::ContentLength) {
+ int len = -1;
+ bool ok = false;
+ len = value.toInt (&ok, 10);
+ if (ok) {
+ m_currentRequest->addHeader (QtHttpHeader::ContentLength, QByteArray::number (len));
+ }
+ }
+ }
+ else {
+ m_parsingStatus = ParsingError;
+ qWarning () << "Error : incorrect HTTP headers line :" << line;
+ }
+ }
+ else { // end of headers
+ //qDebug () << "Debug : HTTP end of headers";
+ if (m_currentRequest->getHeader (QtHttpHeader::ContentLength).toInt () > 0) {
+ m_parsingStatus = AwaitingContent;
+ }
+ else {
+ m_parsingStatus = RequestParsed;
+ }
+ }
+ break;
+ }
+ case AwaitingContent: { // raw data × N (until EOF ??)
+ m_currentRequest->appendRawData (line);
+ //qDebug () << "Debug : HTTP"
+ // << "content :" << m_currentRequest->getRawData ().toHex ()
+ // << "size :" << m_currentRequest->getRawData ().size ();
+ if (m_currentRequest->getRawDataSize () == m_currentRequest->getHeader (QtHttpHeader::ContentLength).toInt ()) {
+ //qDebug () << "Debug : HTTP end of content";
+ m_parsingStatus = RequestParsed;
+ }
+ break;
+ }
+ default: { break; }
+ }
+ switch (m_parsingStatus) { // handle parsing status end/error
+ case RequestParsed: { // a valid request has ben fully parsed
+ QtHttpReply reply (m_serverHandle);
+ connect (&reply, &QtHttpReply::requestSendHeaders,
+ this, &QtHttpClientWrapper::onReplySendHeadersRequested);
+ connect (&reply, &QtHttpReply::requestSendData,
+ this, &QtHttpClientWrapper::onReplySendDataRequested);
+ emit m_serverHandle->requestNeedsReply (m_currentRequest, &reply); // allow app to handle request
+ m_parsingStatus = sendReplyToClient (&reply);
+ break;
+ }
+ case ParsingError: { // there was an error durin one of parsing steps
+ m_sockClient->readAll (); // clear remaining buffer to ignore content
+ QtHttpReply reply (m_serverHandle);
+ reply.setStatusCode (QtHttpReply::BadRequest);
+ reply.appendRawData (QByteArrayLiteral ("Bad Request (HTTP parsing error) !
"));
+ reply.appendRawData (CRLF);
+ m_parsingStatus = sendReplyToClient (&reply);
+ break;
+ }
+ default: { break; }
+ }
+ }
+ }
+}
+
+void QtHttpClientWrapper::onReplySendHeadersRequested (void) {
+ QtHttpReply * reply = qobject_cast (sender ());
+ if (reply != Q_NULLPTR) {
+ QByteArray data;
+ // HTTP Version + Status Code + Status Msg
+ data.append (QtHttpServer::HTTP_VERSION);
+ data.append (SPACE);
+ data.append (QByteArray::number (reply->getStatusCode ()));
+ data.append (SPACE);
+ data.append (QtHttpReply::getStatusTextForCode (reply->getStatusCode ()));
+ data.append (CRLF);
+ // Header name: header value
+ if (reply->useChunked ()) {
+ static const QByteArray & CHUNKED = QByteArrayLiteral ("chunked");
+ reply->addHeader (QtHttpHeader::TransferEncoding, CHUNKED);
+ }
+ else {
+ reply->addHeader (QtHttpHeader::ContentLength, QByteArray::number (reply->getRawDataSize ()));
+ }
+ const QList & headersList = reply->getHeadersList ();
+ foreach (const QByteArray & header, headersList) {
+ data.append (header);
+ data.append (COLON);
+ data.append (SPACE);
+ data.append (reply->getHeader (header));
+ data.append (CRLF);
+ }
+ // empty line
+ data.append (CRLF);
+ m_sockClient->write (data);
+ m_sockClient->flush ();
+ }
+}
+
+void QtHttpClientWrapper::onReplySendDataRequested (void) {
+ QtHttpReply * reply = qobject_cast (sender ());
+ if (reply != Q_NULLPTR) {
+ // content raw data
+ QByteArray data = reply->getRawData ();
+ if (reply->useChunked ()) {
+ data.prepend (QByteArray::number (data.size (), 16) % CRLF);
+ data.append (CRLF);
+ reply->resetRawData ();
+ }
+ // write to socket
+ m_sockClient->write (data);
+ m_sockClient->flush ();
+ }
+}
+
+QtHttpClientWrapper::ParsingStatus QtHttpClientWrapper::sendReplyToClient (QtHttpReply * reply) {
+ if (reply != Q_NULLPTR) {
+ if (!reply->useChunked ()) {
+ reply->appendRawData (CRLF);
+ // send all headers and all data in one shot
+ reply->requestSendHeaders ();
+ reply->requestSendData ();
+ }
+ else {
+ // last chunk
+ m_sockClient->write ("0" % CRLF % CRLF);
+ m_sockClient->flush ();
+ }
+ if (m_currentRequest != Q_NULLPTR) {
+ static const QByteArray & CLOSE = QByteArrayLiteral ("close");
+ if (m_currentRequest->getHeader (QtHttpHeader::Connection).toLower () == CLOSE) {
+ // must close connection after this request
+ m_sockClient->close ();
+ }
+ m_currentRequest->deleteLater ();
+ m_currentRequest = Q_NULLPTR;
+ }
+ }
+ return AwaitingRequest;
+}
diff --git a/libsrc/webconfig/QtHttpClientWrapper.h b/libsrc/webconfig/QtHttpClientWrapper.h
new file mode 100644
index 00000000..ae68a329
--- /dev/null
+++ b/libsrc/webconfig/QtHttpClientWrapper.h
@@ -0,0 +1,51 @@
+#ifndef QTHTTPCLIENTWRAPPER_H
+#define QTHTTPCLIENTWRAPPER_H
+
+#include
+#include
+
+class QTcpSocket;
+
+class QtHttpRequest;
+class QtHttpReply;
+class QtHttpServer;
+
+class QtHttpClientWrapper : public QObject {
+ Q_OBJECT
+
+public:
+ explicit QtHttpClientWrapper (QTcpSocket * sock, QtHttpServer * parent);
+
+ static const char SPACE = ' ';
+ static const char COLON = ':';
+ static const QByteArray & CRLF;
+
+ enum ParsingStatus {
+ ParsingError = -1,
+ AwaitingRequest = 0,
+ AwaitingHeaders = 1,
+ AwaitingContent = 2,
+ RequestParsed = 3
+ };
+
+ QString getGuid (void);
+
+private slots:
+ void onClientDataReceived (void);
+
+protected:
+ ParsingStatus sendReplyToClient (QtHttpReply * reply);
+
+protected slots:
+ void onReplySendHeadersRequested (void);
+ void onReplySendDataRequested (void);
+
+private:
+ QString m_guid;
+ ParsingStatus m_parsingStatus;
+ QTcpSocket * m_sockClient;
+ QtHttpRequest * m_currentRequest;
+ QtHttpServer * m_serverHandle;
+};
+
+#endif // QTHTTPCLIENTWRAPPER_H
diff --git a/libsrc/webconfig/QtHttpHeader.cpp b/libsrc/webconfig/QtHttpHeader.cpp
new file mode 100644
index 00000000..c3f4eb55
--- /dev/null
+++ b/libsrc/webconfig/QtHttpHeader.cpp
@@ -0,0 +1,32 @@
+
+#include "QtHttpHeader.h"
+
+#include
+
+const QByteArray & QtHttpHeader::Server = QByteArrayLiteral ("Server");
+const QByteArray & QtHttpHeader::Date = QByteArrayLiteral ("Date");
+const QByteArray & QtHttpHeader::Host = QByteArrayLiteral ("Host");
+const QByteArray & QtHttpHeader::Accept = QByteArrayLiteral ("Accept");
+const QByteArray & QtHttpHeader::Cookie = QByteArrayLiteral ("Cookie");
+const QByteArray & QtHttpHeader::ContentType = QByteArrayLiteral ("Content-Type");
+const QByteArray & QtHttpHeader::ContentLength = QByteArrayLiteral ("Content-Length");
+const QByteArray & QtHttpHeader::Connection = QByteArrayLiteral ("Connection");
+const QByteArray & QtHttpHeader::UserAgent = QByteArrayLiteral ("User-Agent");
+const QByteArray & QtHttpHeader::AcceptCharset = QByteArrayLiteral ("Accept-Charset");
+const QByteArray & QtHttpHeader::AcceptEncoding = QByteArrayLiteral ("Accept-Encoding");
+const QByteArray & QtHttpHeader::AcceptLanguage = QByteArrayLiteral ("Accept-Language");
+const QByteArray & QtHttpHeader::Authorization = QByteArrayLiteral ("Authorization");
+const QByteArray & QtHttpHeader::CacheControl = QByteArrayLiteral ("Cache-Control");
+const QByteArray & QtHttpHeader::ContentMD5 = QByteArrayLiteral ("Content-MD5");
+const QByteArray & QtHttpHeader::ProxyAuthorization = QByteArrayLiteral ("Proxy-Authorization");
+const QByteArray & QtHttpHeader::Range = QByteArrayLiteral ("Range");
+const QByteArray & QtHttpHeader::ContentEncoding = QByteArrayLiteral ("Content-Encoding");
+const QByteArray & QtHttpHeader::ContentLanguage = QByteArrayLiteral ("Content-Language");
+const QByteArray & QtHttpHeader::ContentLocation = QByteArrayLiteral ("Content-Location");
+const QByteArray & QtHttpHeader::ContentRange = QByteArrayLiteral ("Content-Range");
+const QByteArray & QtHttpHeader::Expires = QByteArrayLiteral ("Expires");
+const QByteArray & QtHttpHeader::LastModified = QByteArrayLiteral ("Last-Modified");
+const QByteArray & QtHttpHeader::Location = QByteArrayLiteral ("Location");
+const QByteArray & QtHttpHeader::SetCookie = QByteArrayLiteral ("Set-Cookie");
+const QByteArray & QtHttpHeader::TransferEncoding = QByteArrayLiteral ("Transfer-Encoding");
+const QByteArray & QtHttpHeader::ContentDisposition = QByteArrayLiteral ("Content-Disposition");
diff --git a/libsrc/webconfig/QtHttpHeader.h b/libsrc/webconfig/QtHttpHeader.h
new file mode 100644
index 00000000..9728414a
--- /dev/null
+++ b/libsrc/webconfig/QtHttpHeader.h
@@ -0,0 +1,37 @@
+#ifndef QTHTTPHEADER_H
+#define QTHTTPHEADER_H
+
+class QByteArray;
+
+class QtHttpHeader {
+public:
+ static const QByteArray & Server;
+ static const QByteArray & Date;
+ static const QByteArray & Host;
+ static const QByteArray & Accept;
+ static const QByteArray & ContentType;
+ static const QByteArray & ContentLength;
+ static const QByteArray & Connection;
+ static const QByteArray & Cookie;
+ static const QByteArray & UserAgent;
+ static const QByteArray & AcceptCharset;
+ static const QByteArray & AcceptEncoding;
+ static const QByteArray & AcceptLanguage;
+ static const QByteArray & Authorization;
+ static const QByteArray & CacheControl;
+ static const QByteArray & ContentMD5;
+ static const QByteArray & ProxyAuthorization;
+ static const QByteArray & Range;
+ static const QByteArray & ContentEncoding;
+ static const QByteArray & ContentLanguage;
+ static const QByteArray & ContentLocation;
+ static const QByteArray & ContentRange;
+ static const QByteArray & Expires;
+ static const QByteArray & LastModified;
+ static const QByteArray & Location;
+ static const QByteArray & SetCookie;
+ static const QByteArray & TransferEncoding;
+ static const QByteArray & ContentDisposition;
+};
+
+#endif // QTHTTPHEADER_H
diff --git a/libsrc/webconfig/QtHttpReply.cpp b/libsrc/webconfig/QtHttpReply.cpp
new file mode 100644
index 00000000..f33e398c
--- /dev/null
+++ b/libsrc/webconfig/QtHttpReply.cpp
@@ -0,0 +1,75 @@
+
+#include "QtHttpReply.h"
+#include "QtHttpHeader.h"
+#include "QtHttpServer.h"
+
+#include
+
+QtHttpReply::QtHttpReply (QtHttpServer * parent)
+ : QObject (parent)
+ , m_useChunked (false)
+ , m_statusCode (Ok)
+ , m_data (QByteArray ())
+ , m_serverHandle (parent)
+{
+ // set some additional headers
+ addHeader (QtHttpHeader::Date, QDateTime::currentDateTimeUtc ().toString ("ddd, dd MMM yyyy hh:mm:ss t").toUtf8 ());
+ addHeader (QtHttpHeader::Server, m_serverHandle->getServerName ().toUtf8 ());
+}
+
+int QtHttpReply::getRawDataSize (void) const {
+ return m_data.size ();
+}
+
+bool QtHttpReply::useChunked (void) const {
+ return m_useChunked;
+}
+
+QtHttpReply::StatusCode QtHttpReply::getStatusCode (void) const {
+ return m_statusCode;
+}
+
+QByteArray QtHttpReply::getRawData (void) const {
+ return m_data;
+}
+
+QList QtHttpReply::getHeadersList (void) const {
+ return m_headersHash.keys ();
+}
+
+QByteArray QtHttpReply::getHeader (const QByteArray & header) const {
+ return m_headersHash.value (header, QByteArray ());
+}
+
+const QByteArray QtHttpReply::getStatusTextForCode (QtHttpReply::StatusCode statusCode) {
+ switch (statusCode) {
+ case Ok: return QByteArrayLiteral ("OK.");
+ case BadRequest: return QByteArrayLiteral ("Bad request !");
+ case Forbidden: return QByteArrayLiteral ("Forbidden !");
+ case NotFound: return QByteArrayLiteral ("Not found !");
+ default: return QByteArrayLiteral ("");
+ }
+}
+
+void QtHttpReply::setUseChunked (bool chunked){
+ m_useChunked = chunked;
+}
+
+void QtHttpReply::setStatusCode (QtHttpReply::StatusCode statusCode) {
+ m_statusCode = statusCode;
+}
+
+void QtHttpReply::appendRawData (const QByteArray & data) {
+ m_data.append (data);
+}
+
+void QtHttpReply::addHeader (const QByteArray & header, const QByteArray & value) {
+ QByteArray key = header.trimmed ();
+ if (!key.isEmpty ()) {
+ m_headersHash.insert (key, value);
+ }
+}
+
+void QtHttpReply::resetRawData (void) {
+ m_data.clear ();
+}
diff --git a/libsrc/webconfig/QtHttpReply.h b/libsrc/webconfig/QtHttpReply.h
new file mode 100644
index 00000000..92b57da1
--- /dev/null
+++ b/libsrc/webconfig/QtHttpReply.h
@@ -0,0 +1,55 @@
+#ifndef QTHTTPREPLY_H
+#define QTHTTPREPLY_H
+
+#include
+#include
+#include
+#include
+
+class QtHttpServer;
+
+class QtHttpReply : public QObject {
+ Q_OBJECT
+ Q_ENUMS (StatusCode)
+
+public:
+ explicit QtHttpReply (QtHttpServer * parent);
+
+ enum StatusCode {
+ Ok = 200,
+ BadRequest = 400,
+ Forbidden = 403,
+ NotFound = 404,
+ InternalError = 502,
+ };
+
+ int getRawDataSize (void) const;
+ bool useChunked (void) const;
+ StatusCode getStatusCode (void) const;
+ QByteArray getRawData (void) const;
+ QList getHeadersList (void) const;
+
+ QByteArray getHeader (const QByteArray & header) const;
+
+ static const QByteArray getStatusTextForCode (StatusCode statusCode);
+
+public slots:
+ void setUseChunked (bool chunked = false);
+ void setStatusCode (StatusCode statusCode);
+ void appendRawData (const QByteArray & data);
+ void addHeader (const QByteArray & header, const QByteArray & value);
+ void resetRawData (void);
+
+signals:
+ void requestSendHeaders (void);
+ void requestSendData (void);
+
+private:
+ bool m_useChunked;
+ StatusCode m_statusCode;
+ QByteArray m_data;
+ QtHttpServer * m_serverHandle;
+ QHash m_headersHash;
+};
+
+#endif // QTHTTPREPLY_H
diff --git a/libsrc/webconfig/QtHttpRequest.cpp b/libsrc/webconfig/QtHttpRequest.cpp
new file mode 100644
index 00000000..0571c5f8
--- /dev/null
+++ b/libsrc/webconfig/QtHttpRequest.cpp
@@ -0,0 +1,60 @@
+
+#include "QtHttpRequest.h"
+#include "QtHttpHeader.h"
+#include "QtHttpServer.h"
+
+QtHttpRequest::QtHttpRequest (QtHttpServer * parent)
+ : QObject (parent)
+ , m_url (QUrl ())
+ , m_command (QString ())
+ , m_data (QByteArray ())
+ , m_serverHandle (parent)
+{
+ // set some additional headers
+ addHeader (QtHttpHeader::ContentLength, QByteArrayLiteral ("0"));
+ addHeader (QtHttpHeader::Connection, QByteArrayLiteral ("Keep-Alive"));
+}
+
+QUrl QtHttpRequest::getUrl (void) const {
+ return m_url;
+}
+
+QString QtHttpRequest::getCommand (void) const {
+ return m_command;
+}
+
+int QtHttpRequest::getRawDataSize (void) const {
+ return m_data.size ();
+}
+
+
+QByteArray QtHttpRequest::getRawData (void) const {
+ return m_data;
+}
+
+QList QtHttpRequest::getHeadersList (void) const {
+ return m_headersHash.keys ();
+}
+
+QByteArray QtHttpRequest::getHeader (const QByteArray & header) const {
+ return m_headersHash.value (header, QByteArray ());
+}
+
+void QtHttpRequest::setUrl (const QUrl & url) {
+ m_url = url;
+}
+
+void QtHttpRequest::setCommand (const QString & command) {
+ m_command = command;
+}
+
+void QtHttpRequest::addHeader (const QByteArray & header, const QByteArray & value) {
+ QByteArray key = header.trimmed ();
+ if (!key.isEmpty ()) {
+ m_headersHash.insert (key, value);
+ }
+}
+
+void QtHttpRequest::appendRawData (const QByteArray & data) {
+ m_data.append (data);
+}
diff --git a/libsrc/webconfig/QtHttpRequest.h b/libsrc/webconfig/QtHttpRequest.h
new file mode 100644
index 00000000..9224f1ed
--- /dev/null
+++ b/libsrc/webconfig/QtHttpRequest.h
@@ -0,0 +1,40 @@
+#ifndef QTHTTPREQUEST_H
+#define QTHTTPREQUEST_H
+
+#include
+#include
+#include
+#include
+#include
+
+class QtHttpServer;
+
+class QtHttpRequest : public QObject {
+ Q_OBJECT
+
+public:
+ explicit QtHttpRequest (QtHttpServer * parent);
+
+ int getRawDataSize (void) const;
+ QUrl getUrl (void) const;
+ QString getCommand (void) const;
+ QByteArray getRawData (void) const;
+ QList getHeadersList (void) const;
+
+ QByteArray getHeader (const QByteArray & header) const;
+
+public slots:
+ void setUrl (const QUrl & url);
+ void setCommand (const QString & command);
+ void addHeader (const QByteArray & header, const QByteArray & value);
+ void appendRawData (const QByteArray & data);
+
+private:
+ QUrl m_url;
+ QString m_command;
+ QByteArray m_data;
+ QtHttpServer * m_serverHandle;
+ QHash m_headersHash;
+};
+
+#endif // QTHTTPREQUEST_H
diff --git a/libsrc/webconfig/QtHttpServer.cpp b/libsrc/webconfig/QtHttpServer.cpp
new file mode 100644
index 00000000..cf0ff33f
--- /dev/null
+++ b/libsrc/webconfig/QtHttpServer.cpp
@@ -0,0 +1,66 @@
+
+#include "QtHttpServer.h"
+#include "QtHttpRequest.h"
+#include "QtHttpReply.h"
+#include "QtHttpClientWrapper.h"
+
+#include
+#include
+#include
+#include
+
+const QString & QtHttpServer::HTTP_VERSION = QStringLiteral ("HTTP/1.1");
+
+QtHttpServer::QtHttpServer (QObject * parent)
+ : QObject (parent)
+ , m_serverName (QStringLiteral ("The Qt5 HTTP Server"))
+{
+ m_sockServer = new QTcpServer (this);
+ connect (m_sockServer, &QTcpServer::newConnection, this, &QtHttpServer::onClientConnected);
+}
+
+const QString QtHttpServer::getServerName (void) const {
+ return m_serverName;
+}
+
+void QtHttpServer::start (quint16 port) {
+ if (m_sockServer->listen (QHostAddress::Any, port)) {
+ emit started (m_sockServer->serverPort ());
+ }
+ else {
+ emit error (m_sockServer->errorString ());
+ }
+}
+
+void QtHttpServer::stop (void) {
+ if (m_sockServer->isListening ()) {
+ m_sockServer->close ();
+ emit stopped ();
+ }
+}
+
+void QtHttpServer::setServerName (const QString & serverName) {
+ m_serverName = serverName;
+}
+
+void QtHttpServer::onClientConnected (void) {
+ while (m_sockServer->hasPendingConnections ()) {
+ QTcpSocket * sockClient = m_sockServer->nextPendingConnection ();
+ QtHttpClientWrapper * wrapper = new QtHttpClientWrapper (sockClient, this);
+ connect (sockClient, &QTcpSocket::disconnected, this, &QtHttpServer::onClientDisconnected);
+ m_socksClientsHash.insert (sockClient, wrapper);
+ emit clientConnected (wrapper->getGuid ());
+ }
+}
+
+void QtHttpServer::onClientDisconnected (void) {
+ QTcpSocket * sockClient = qobject_cast (sender ());
+ if (sockClient) {
+ QtHttpClientWrapper * wrapper = m_socksClientsHash.value (sockClient, Q_NULLPTR);
+ if (wrapper) {
+ emit clientDisconnected (wrapper->getGuid ());
+ wrapper->deleteLater ();
+ m_socksClientsHash.remove (sockClient);
+ }
+ }
+}
diff --git a/libsrc/webconfig/QtHttpServer.h b/libsrc/webconfig/QtHttpServer.h
new file mode 100644
index 00000000..71a6261b
--- /dev/null
+++ b/libsrc/webconfig/QtHttpServer.h
@@ -0,0 +1,48 @@
+#ifndef QTHTTPSERVER_H
+#define QTHTTPSERVER_H
+
+#include
+#include
+#include
+
+class QTcpSocket;
+class QTcpServer;
+
+class QtHttpRequest;
+class QtHttpReply;
+class QtHttpClientWrapper;
+
+class QtHttpServer : public QObject {
+ Q_OBJECT
+
+public:
+ explicit QtHttpServer (QObject * parent = Q_NULLPTR);
+
+ static const QString & HTTP_VERSION;
+
+ const QString getServerName (void) const;
+
+public slots:
+ void start (quint16 port = 0);
+ void stop (void);
+ void setServerName (const QString & serverName);
+
+signals:
+ void started (quint16 port);
+ void stopped (void);
+ void error (const QString & msg);
+ void clientConnected (const QString & guid);
+ void clientDisconnected (const QString & guid);
+ void requestNeedsReply (QtHttpRequest * request, QtHttpReply * reply);
+
+private slots:
+ void onClientConnected (void);
+ void onClientDisconnected (void);
+
+private:
+ QString m_serverName;
+ QTcpServer * m_sockServer;
+ QHash m_socksClientsHash;
+};
+
+#endif // QTHTTPSERVER_H
diff --git a/libsrc/webconfig/StaticFileServing.cpp b/libsrc/webconfig/StaticFileServing.cpp
new file mode 100644
index 00000000..9a1c91be
--- /dev/null
+++ b/libsrc/webconfig/StaticFileServing.cpp
@@ -0,0 +1,87 @@
+
+#include "StaticFileServing.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+StaticFileServing::StaticFileServing (QString baseUrl, quint16 port, QObject * parent)
+ : QObject (parent)
+ , m_baseUrl (baseUrl)
+{
+ m_mimeDb = new QMimeDatabase;
+
+ m_server = new QtHttpServer (this);
+ m_server->setServerName (QStringLiteral ("Qt Static HTTP File Server"));
+
+ connect (m_server, &QtHttpServer::started, this, &StaticFileServing::onServerStarted);
+ connect (m_server, &QtHttpServer::stopped, this, &StaticFileServing::onServerStopped);
+ connect (m_server, &QtHttpServer::error, this, &StaticFileServing::onServerError);
+ connect (m_server, &QtHttpServer::requestNeedsReply, this, &StaticFileServing::onRequestNeedsReply);
+
+ m_server->start (port);
+}
+
+StaticFileServing::~StaticFileServing ()
+{
+ m_server->stop ();
+}
+
+void StaticFileServing::onServerStarted (quint16 port)
+{
+ qDebug () << "QtHttpServer started on port" << port << m_server->getServerName ();
+}
+
+void StaticFileServing::onServerStopped () {
+ qDebug () << "QtHttpServer stopped" << m_server->getServerName ();
+}
+
+void StaticFileServing::onServerError (QString msg)
+{
+ qDebug () << "QtHttpServer error :" << msg;
+}
+
+static inline void printErrorToReply (QtHttpReply * reply, QString errorMessage)
+{
+ reply->addHeader ("Content-Type", QByteArrayLiteral ("text/plain"));
+ reply->appendRawData (errorMessage.toLocal8Bit ());
+}
+
+void StaticFileServing::onRequestNeedsReply (QtHttpRequest * request, QtHttpReply * reply)
+{
+ QString command = request->getCommand ();
+ if (command == QStringLiteral ("GET"))
+ {
+ QString path = request->getUrl ().path ();
+ if ( path == "/" || path.isEmpty() || ! QFile::exists(m_baseUrl % "/" % path) )
+ path = "index.html";
+
+ QFile file (m_baseUrl % "/" % path);
+ if (file.exists ())
+ {
+ QMimeType mime = m_mimeDb->mimeTypeForFile (file.fileName ());
+ if (file.open (QFile::ReadOnly)) {
+ QByteArray data = file.readAll ();
+ reply->addHeader ("Content-Type", mime.name ().toLocal8Bit ());
+ reply->appendRawData (data);
+ file.close ();
+ }
+ else
+ {
+ printErrorToReply (reply, "Requested file " % m_baseUrl % "/" % path % " couldn't be open for reading !");
+ }
+ }
+ else
+ {
+ printErrorToReply (reply, "Requested file " % path % " couldn't be found !");
+ }
+ }
+ else
+ {
+ printErrorToReply (reply, "Unhandled HTTP/1.1 method " % command % " on static file server !");
+ }
+}
+
diff --git a/libsrc/webconfig/StaticFileServing.h b/libsrc/webconfig/StaticFileServing.h
new file mode 100644
index 00000000..a6a72016
--- /dev/null
+++ b/libsrc/webconfig/StaticFileServing.h
@@ -0,0 +1,31 @@
+#ifndef STATICFILESERVING_H
+#define STATICFILESERVING_H
+
+#include
+#include
+
+#include "QtHttpServer.h"
+#include "QtHttpRequest.h"
+#include "QtHttpReply.h"
+#include "QtHttpHeader.h"
+
+class StaticFileServing : public QObject {
+ Q_OBJECT
+
+public:
+ explicit StaticFileServing (QString baseUrl, quint16 port, QObject * parent = NULL);
+ virtual ~StaticFileServing (void);
+
+public slots:
+ void onServerStopped (void);
+ void onServerStarted (quint16 port);
+ void onServerError (QString msg);
+ void onRequestNeedsReply (QtHttpRequest * request, QtHttpReply * reply);
+
+private:
+ QString m_baseUrl;
+ QtHttpServer * m_server;
+ QMimeDatabase * m_mimeDb;
+};
+
+#endif // STATICFILESERVING_H
diff --git a/libsrc/webconfig/WebConfig.cpp b/libsrc/webconfig/WebConfig.cpp
new file mode 100644
index 00000000..89dbf218
--- /dev/null
+++ b/libsrc/webconfig/WebConfig.cpp
@@ -0,0 +1,33 @@
+#include "webconfig/webconfig.h"
+#include "StaticFileServing.h"
+
+WebConfig::WebConfig(std::string baseUrl, quint16 port, QObject * parent) :
+ _parent(parent),
+ _baseUrl(QString::fromStdString(baseUrl)),
+ _port(port),
+ _server(nullptr)
+{
+}
+
+WebConfig::~WebConfig()
+{
+ stop();
+}
+
+
+void WebConfig::start()
+{
+ if ( _server == nullptr )
+ _server = new StaticFileServing (_baseUrl, _port, this);
+}
+
+void WebConfig::stop()
+{
+ if ( _server != nullptr )
+ {
+ delete _server;
+ _server = nullptr;
+ }
+}
+
+
diff --git a/src/hyperiond/CMakeLists.txt b/src/hyperiond/CMakeLists.txt
index ed79f4c1..3f0829e4 100644
--- a/src/hyperiond/CMakeLists.txt
+++ b/src/hyperiond/CMakeLists.txt
@@ -9,8 +9,11 @@ target_link_libraries(hyperiond
effectengine
jsonserver
boblightserver
- protoserver
+ protoserver
)
+if (ENABLE_QT5)
+ target_link_libraries(hyperiond webconfig)
+endif ()
if (ENABLE_DISPMANX)
target_link_libraries(hyperiond dispmanx-grabber)
@@ -40,4 +43,4 @@ install ( TARGETS hyperiond DESTINATION "bin" COMPONENT ambilight )
install ( DIRECTORY ${CMAKE_SOURCE_DIR}/effects DESTINATION "share/hyperion/" COMPONENT ambilight )
install ( DIRECTORY ${CMAKE_SOURCE_DIR}/bin/service DESTINATION "share/hyperion/" COMPONENT ambilight )
install ( DIRECTORY ${CMAKE_SOURCE_DIR}/config DESTINATION "share/hyperion/" COMPONENT ambilight )
-
+install ( DIRECTORY ${CMAKE_SOURCE_DIR}/assets/webconfig DESTINATION "share/hyperion/" COMPONENT ambilight )
diff --git a/src/hyperiond/hyperiond.cpp b/src/hyperiond/hyperiond.cpp
index 6a41edc0..7575f744 100644
--- a/src/hyperiond/hyperiond.cpp
+++ b/src/hyperiond/hyperiond.cpp
@@ -58,8 +58,16 @@
#include
#endif
-// JsonServer includes
+// network servers
#include
+#include
+#include
+#include
+
+#include
+#include
+
+using namespace vlofgren;
// ProtoServer includes
#include
@@ -187,7 +195,11 @@ void startXBMCVideoChecker(const Json::Value &config, XBMCVideoChecker* &xbmcVid
}
}
+#ifdef ENABLE_QT5
+void startNetworkServices(const Json::Value &config, Hyperion &hyperion, JsonServer* &jsonServer, ProtoServer* &protoServer, BoblightServer* &boblightServer, WebConfig* &webConfig, XBMCVideoChecker* &xbmcVideoChecker, QObject* parent)
+#else
void startNetworkServices(const Json::Value &config, Hyperion &hyperion, JsonServer* &jsonServer, ProtoServer* &protoServer, BoblightServer* &boblightServer, XBMCVideoChecker* &xbmcVideoChecker)
+#endif
{
// Create Json server if configuration is present
unsigned int jsonPort = 19444;
@@ -218,6 +230,24 @@ void startNetworkServices(const Json::Value &config, Hyperion &hyperion, JsonSer
}
std::cout << "INFO: Proto server created and started on port " << protoServer->getPort() << std::endl;
+#ifdef ENABLE_QT5
+ // webconfig server
+ std::string webconfigPath = "/usr/share/hyperion/webconfig";
+ quint16 webconfigPort = 80;
+ bool webconfigEnable = true;
+ if (config.isMember("webConfig"))
+ {
+ const Json::Value & webconfigConfig = config["webConfig"];
+ webconfigEnable = webconfigConfig.get("enable", true).asBool();
+ webconfigPort = webconfigConfig.get("port", 80).asUInt();
+ webconfigPath = webconfigConfig.get("document_root", "/usr/share/hyperion/webconfig").asString();
+ }
+
+ webConfig = new WebConfig(webconfigPath, webconfigPort, parent);
+ if ( webconfigEnable )
+ webConfig->start();
+#endif
+
#ifdef ENABLE_ZEROCONF
const Json::Value & deviceConfig = config["device"];
const std::string deviceName = deviceConfig.get("name", "").asString();
@@ -492,7 +522,12 @@ int main(int argc, char** argv)
JsonServer * jsonServer = nullptr;
ProtoServer * protoServer = nullptr;
BoblightServer * boblightServer = nullptr;
+#ifdef ENABLE_QT5
+ WebConfig * webConfig = nullptr;
+ startNetworkServices(config, hyperion, jsonServer, protoServer, boblightServer, webConfig, xbmcVideoChecker, &app);
+#else
startNetworkServices(config, hyperion, jsonServer, protoServer, boblightServer, xbmcVideoChecker);
+#endif
// ---- grabber -----