integrated webserver ... (#697)

* initial commit of webconfig

* update example config with webconfig and fix format of file
update debian postinst script for install example config
This commit is contained in:
redPanther 2016-06-12 22:27:24 +02:00 committed by brindosch
parent d2f47251f5
commit 7dfb9f1967
55 changed files with 8952 additions and 76 deletions

View File

@ -116,10 +116,10 @@ find_package(GitVersion)
configure_file("${PROJECT_SOURCE_DIR}/HyperionConfig.h.in" "${PROJECT_BINARY_DIR}/HyperionConfig.h") configure_file("${PROJECT_SOURCE_DIR}/HyperionConfig.h.in" "${PROJECT_BINARY_DIR}/HyperionConfig.h")
include_directories("${PROJECT_BINARY_DIR}") include_directories("${PROJECT_BINARY_DIR}")
if(ENABLE_QT5) if( NOT ENABLE_QT5)
ADD_DEFINITIONS ( -DENABLE_QT5 ) #ADD_DEFINITIONS ( -DENABLE_QT5 )
#find_package(Qt5Widgets) #find_package(Qt5Widgets)
else() #else()
# Add specific cmake modules to find qt4 (default version finds first available QT which might not be qt4) # 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) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake/qt4)
endif() endif()

View File

@ -32,6 +32,7 @@
// Define to enable profiler for development purpose // Define to enable profiler for development purpose
#cmakedefine ENABLE_PROFILER #cmakedefine ENABLE_PROFILER
#cmakedefine ENABLE_QT5
// the hyperion build id string // the hyperion build id string
#define HYPERION_VERSION_ID "${HYPERION_VERSION_ID}" #define HYPERION_VERSION_ID "${HYPERION_VERSION_ID}"

View File

@ -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';
}

View File

@ -0,0 +1,69 @@
<!DOCTYPE html>
<html manifest="manifest.appcache">
<head>
<title>Hyperion remote control</title>
<meta charset="utf-8"/>
<meta name="format-detection" content="telephone=no"/>
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width"/>
<link href="css/index.css" rel="stylesheet">
<link rel="apple-touch-icon" href="res/icon_128.png">
<meta name="apple-mobile-web-app-capable" content="yes">
</head>
<body>
<div id="app">
<div class="work">
<div class="container">
<div id="color" class="contentarea">
<div id="colorpicker">
<img id="colorwheelbg" src="res/colorwheel.png" width="auto" height="90%"/>
<div id="pointer"></div>
<div class="touchrect"></div>
</div>
<div class="slider" id="brightness">
<div class="track"></div>
<div class="thumb"></div>
</div>
<div id="buttonctrl">
<div class="icon" id="clear_button">&#xe807;</div>
<input type="text" class="value" autocomplete="off" autocorrect="off" autocapitalize="off"/>
<div class="icon" id="clearall_button">&#xe617;</div>
</div>
</div>
<div id="effects" class="contentarea">
<label class="info">Need server connection to fetch list.</label>
<ul></ul>
</div>
<div id="transform" class="contentarea">
<label class="info">Need server connection to fetch list.</label>
<div class="values"></div>
</div>
<div id="settings" class="contentarea">
</div>
</div>
</div>
<div class="footer">
<div class="button selected" id="colorButton" data-area="color">
<div class="icon">&#xe800;</div>
<div class="title">Color</div>
<div class="touchrect"></div>
</div>
<div class="button" id="effectsButton" data-area="effects">
<div class="icon">&#xe602;</div>
<div class="title">Effects</div>
<div class="touchrect"></div>
</div>
<div class="button" id="thresholdButton" data-area="transform">
<div class="icon">&#xe601;</div>
<div class="title">Transform</div>
<div class="touchrect"></div>
</div>
<div class="button" id="settingsButton" data-area="settings">
<div class="icon">&#x6e;</div>
<div class="title">Settings</div>
<div class="touchrect"></div>
</div>
</div>
</div>
<script src="js/vendor/require.js" data-main="js/app/main"></script>
</body>
</html>

View File

@ -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));
}
});
});

View File

@ -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);
});

View File

@ -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);
});

View File

@ -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');
}
});
});

View File

@ -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);
});

View File

@ -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);
});

View File

@ -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);
});

View File

@ -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));
}
});
});

View File

@ -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;
}
});
});

View File

@ -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();
}
});

View File

@ -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();
});

View File

@ -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;
}
});
});

View File

@ -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));
}
};
});

View File

@ -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');
}
}
});
});

View File

@ -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();
}
});
});

View File

@ -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 = '&#xe804;';
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 = '&#xe801;';
horiz.appendChild(el);
el = document.createElement('div');
el.classList.add('delete_icon');
el.innerHTML = '&#xe612;';
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');
}
}
});
});

View File

@ -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);
}
});
});

View File

@ -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);
}
});
});

View File

@ -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: '&#xe806;',
label: 'Saturation gain',
value: transform.saturationGain,
min: 0,
max: 5
},
{
id: 'hsv_valueGain',
type: 'value',
icon: '&#xe805;',
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: '&#xe800;',
label: 'Red',
value: transform.gamma[0],
min: 0,
max: 5
},
{
id: 'gamma_g',
type: 'green',
icon: '&#xe800;',
label: 'Green',
value: transform.gamma[1],
min: 0,
max: 5
},
{
id: 'gamma_b',
type: 'blue',
icon: '&#xe800;',
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: '&#xe800;',
label: 'Red',
value: transform.whitelevel[0],
min: 0,
max: 1
},
{
id: 'whitelevel_g',
type: 'green',
icon: '&#xe800;',
label: 'Green',
value: transform.whitelevel[1],
min: 0,
max: 1
},
{
id: 'whitelevel_b',
type: 'blue',
icon: '&#xe800;',
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: '&#xe800;',
label: 'Red',
value: transform.blacklevel[0],
min: 0,
max: 1
},
{
id: 'blacklevel_g',
type: 'green',
icon: '&#xe800;',
label: 'Green',
value: transform.blacklevel[1],
min: 0,
max: 1
},
{
id: 'blacklevel_b',
type: 'blue',
icon: '&#xe800;',
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: '&#xe800;',
label: 'Red',
value: transform.threshold[0],
min: 0,
max: 1
},
{
id: 'threshold_g',
type: 'green',
icon: '&#xe800;',
label: 'Green',
value: transform.threshold[1],
min: 0,
max: 1
},
{
id: 'threshold_b',
type: 'blue',
icon: '&#xe800;',
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);
}
});
});

View File

@ -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
});
});

2054
assets/webconfig/js/vendor/require.js vendored Normal file

File diff suppressed because it is too large Load Diff

594
assets/webconfig/js/vendor/stapes.js vendored Normal file
View File

@ -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;
}
})();

1107
assets/webconfig/js/vendor/tinycolor.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -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"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -20,7 +20,7 @@ install_file()
echo "--- hyperion ambilight postinstall ---" echo "--- hyperion ambilight postinstall ---"
echo "- install configuration template" echo "- install configuration template"
mkdir -p /etc/hyperion 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 HYPERION_RUNNING=false

View File

@ -154,19 +154,23 @@
}, },
/// The configuration of the effect engine, contains the following items: /// The configuration of the effect engine, contains the following items:
/// * paths : An array with absolute location(s) of directories with effects /// * paths : An array with absolute/relative 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!
"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" : "bootsequence" :
{ {
"color" : [0,0,0], "color" : [0,0,0],
@ -175,11 +179,22 @@
"priority" : 990 "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 /// 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' 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"] /// * 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"] /// * 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! /// 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) /// 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" : "forwarder" :
{ {
@ -240,7 +255,7 @@
/// The configuration of the boblight server which enables the boblight remote interface /// The configuration of the boblight server which enables the boblight remote interface
/// * port : Port at which the boblight server is started /// * port : Port at which the boblight server is started
/// * priority: Priority of the boblight server (Default=900) HINT: lower value result in HIGHER priority! /// * priority : Priority of the boblight server (Default=900) HINT: lower value result in HIGHER priority!
"boblightServer" : "boblightServer" :
{ {
"port" : 19333, "port" : 19333,

View File

@ -0,0 +1,27 @@
#ifndef WEBCONFIG_H
#define WEBCONFIG_H
#include <QObject>
#include <string>
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

View File

@ -19,3 +19,7 @@ add_subdirectory(utils)
add_subdirectory(xbmcvideochecker) add_subdirectory(xbmcvideochecker)
add_subdirectory(effectengine) add_subdirectory(effectengine)
add_subdirectory(grabber) add_subdirectory(grabber)
if(ENABLE_QT5)
add_subdirectory(webconfig)
endif()

View File

@ -17,6 +17,7 @@
#include "hyperion/ImageProcessorFactory.h" #include "hyperion/ImageProcessorFactory.h"
#include "hyperion/ImageProcessor.h" #include "hyperion/ImageProcessor.h"
#include "utils/ColorRgb.h" #include "utils/ColorRgb.h"
#include "HyperionConfig.h"
// project includes // project includes
#include "BoblightClientConnection.h" #include "BoblightClientConnection.h"

View File

@ -16,6 +16,7 @@
// effect engine includes // effect engine includes
#include <effectengine/EffectEngine.h> #include <effectengine/EffectEngine.h>
#include "Effect.h" #include "Effect.h"
#include "HyperionConfig.h"
EffectEngine::EffectEngine(Hyperion * hyperion, const Json::Value & jsonEffectConfig) : EffectEngine::EffectEngine(Hyperion * hyperion, const Json::Value & jsonEffectConfig) :
_hyperion(hyperion), _hyperion(hyperion),

View File

@ -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}
)

View File

@ -0,0 +1,221 @@
#include "QtHttpClientWrapper.h"
#include "QtHttpRequest.h"
#include "QtHttpReply.h"
#include "QtHttpServer.h"
#include "QtHttpHeader.h"
#include <QCryptographicHash>
#include <QTcpSocket>
#include <QStringBuilder>
#include <QStringList>
#include <QDateTime>
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 ("<h1>Bad Request (HTTP parsing error) !</h1>"));
reply.appendRawData (CRLF);
m_parsingStatus = sendReplyToClient (&reply);
break;
}
default: { break; }
}
}
}
}
void QtHttpClientWrapper::onReplySendHeadersRequested (void) {
QtHttpReply * reply = qobject_cast<QtHttpReply *> (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<QByteArray> & 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<QtHttpReply *> (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;
}

View File

@ -0,0 +1,51 @@
#ifndef QTHTTPCLIENTWRAPPER_H
#define QTHTTPCLIENTWRAPPER_H
#include <QObject>
#include <QString>
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

View File

@ -0,0 +1,32 @@
#include "QtHttpHeader.h"
#include <QByteArray>
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");

View File

@ -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

View File

@ -0,0 +1,75 @@
#include "QtHttpReply.h"
#include "QtHttpHeader.h"
#include "QtHttpServer.h"
#include <QDateTime>
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<QByteArray> 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 ();
}

View File

@ -0,0 +1,55 @@
#ifndef QTHTTPREPLY_H
#define QTHTTPREPLY_H
#include <QObject>
#include <QByteArray>
#include <QHash>
#include <QList>
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<QByteArray> 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<QByteArray, QByteArray> m_headersHash;
};
#endif // QTHTTPREPLY_H

View File

@ -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<QByteArray> 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);
}

View File

@ -0,0 +1,40 @@
#ifndef QTHTTPREQUEST_H
#define QTHTTPREQUEST_H
#include <QObject>
#include <QString>
#include <QByteArray>
#include <QHash>
#include <QUrl>
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<QByteArray> 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<QByteArray, QByteArray> m_headersHash;
};
#endif // QTHTTPREQUEST_H

View File

@ -0,0 +1,66 @@
#include "QtHttpServer.h"
#include "QtHttpRequest.h"
#include "QtHttpReply.h"
#include "QtHttpClientWrapper.h"
#include <QTcpServer>
#include <QTcpSocket>
#include <QHostAddress>
#include <QDebug>
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<QTcpSocket *> (sender ());
if (sockClient) {
QtHttpClientWrapper * wrapper = m_socksClientsHash.value (sockClient, Q_NULLPTR);
if (wrapper) {
emit clientDisconnected (wrapper->getGuid ());
wrapper->deleteLater ();
m_socksClientsHash.remove (sockClient);
}
}
}

View File

@ -0,0 +1,48 @@
#ifndef QTHTTPSERVER_H
#define QTHTTPSERVER_H
#include <QObject>
#include <QString>
#include <QHash>
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<QTcpSocket *, QtHttpClientWrapper *> m_socksClientsHash;
};
#endif // QTHTTPSERVER_H

View File

@ -0,0 +1,87 @@
#include "StaticFileServing.h"
#include <QStringBuilder>
#include <QUrlQuery>
#include <QDebug>
#include <QList>
#include <QPair>
#include <QFile>
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 !");
}
}

View File

@ -0,0 +1,31 @@
#ifndef STATICFILESERVING_H
#define STATICFILESERVING_H
#include <QObject>
#include <QMimeDatabase>
#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

View File

@ -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;
}
}

View File

@ -11,6 +11,9 @@ target_link_libraries(hyperiond
boblightserver boblightserver
protoserver protoserver
) )
if (ENABLE_QT5)
target_link_libraries(hyperiond webconfig)
endif ()
if (ENABLE_DISPMANX) if (ENABLE_DISPMANX)
target_link_libraries(hyperiond dispmanx-grabber) 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}/effects DESTINATION "share/hyperion/" COMPONENT ambilight )
install ( DIRECTORY ${CMAKE_SOURCE_DIR}/bin/service 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}/config DESTINATION "share/hyperion/" COMPONENT ambilight )
install ( DIRECTORY ${CMAKE_SOURCE_DIR}/assets/webconfig DESTINATION "share/hyperion/" COMPONENT ambilight )

View File

@ -58,8 +58,16 @@
#include <QHostInfo> #include <QHostInfo>
#endif #endif
// JsonServer includes // network servers
#include <jsonserver/JsonServer.h> #include <jsonserver/JsonServer.h>
#include <protoserver/ProtoServer.h>
#include <boblightserver/BoblightServer.h>
#include <webconfig/WebConfig.h>
#include <sys/prctl.h>
#include <utils/logger.h>
using namespace vlofgren;
// ProtoServer includes // ProtoServer includes
#include <protoserver/ProtoServer.h> #include <protoserver/ProtoServer.h>
@ -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) void startNetworkServices(const Json::Value &config, Hyperion &hyperion, JsonServer* &jsonServer, ProtoServer* &protoServer, BoblightServer* &boblightServer, XBMCVideoChecker* &xbmcVideoChecker)
#endif
{ {
// Create Json server if configuration is present // Create Json server if configuration is present
unsigned int jsonPort = 19444; 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; 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 #ifdef ENABLE_ZEROCONF
const Json::Value & deviceConfig = config["device"]; const Json::Value & deviceConfig = config["device"];
const std::string deviceName = deviceConfig.get("name", "").asString(); const std::string deviceName = deviceConfig.get("name", "").asString();
@ -492,7 +522,12 @@ int main(int argc, char** argv)
JsonServer * jsonServer = nullptr; JsonServer * jsonServer = nullptr;
ProtoServer * protoServer = nullptr; ProtoServer * protoServer = nullptr;
BoblightServer * boblightServer = 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); startNetworkServices(config, hyperion, jsonServer, protoServer, boblightServer, xbmcVideoChecker);
#endif
// ---- grabber ----- // ---- grabber -----