added html/css/js files for the webinterface

git-svn-id: http://svn.code.sf.net/p/retroshare/code/trunk@8099 b45a01b8-16f6-495d-af2f-9b41ad6348cc
This commit is contained in:
electron128 2015-03-30 14:40:20 +00:00
parent c480c0c9d3
commit 8fb6670857
13 changed files with 1449 additions and 0 deletions

5
libresapi/src/README.md Normal file
View File

@ -0,0 +1,5 @@
libresapi: resource_api and new webinterface
============================================
* ./api contains a C++ backend to control retroshare from webinterfaces or scripting
* ./webui contains HTML/CSS/JavaScript files for the webinterface

View File

@ -0,0 +1,13 @@
module.exports = function(grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
watch: {
// important: exclude node_modules
files: ['**','!**/node_modules/**'],
options: {
livereload: true,
}
}
});
grunt.loadNpmTasks('grunt-contrib-watch');
};

View File

@ -0,0 +1,31 @@
REACT_VERSION = 0.13.1
JSEXTLIBS = dist/react.js dist/JSXTransformer.js
JSLIBS = RsXHRConnection.js RsApi.js
HTML = index.html
JSGUI = gui.jsx
CSS = green-black.css
all: dist $(JSEXTLIBS) $(addprefix dist/, $(JSLIBS)) $(addprefix dist/, $(HTML)) $(addprefix dist/, $(JSGUI)) $(addprefix dist/, $(CSS))
.PHONY: all
dist/react.js: dist
cd dist && wget --no-check-certificate --output-document react.js http://fb.me/react-$(REACT_VERSION).js
dist/JSXTransformer.js: dist
cd dist && wget --no-check-certificate --output-document JSXTransformer.js http://fb.me/JSXTransformer-$(REACT_VERSION).js
$(addprefix dist/, $(JSLIBS)): dist/%: %
cp $< $@
$(addprefix dist/, $(HTML)): dist/%: %
cp $< $@
$(addprefix dist/, $(JSGUI)): dist/%: %
cp $< $@
$(addprefix dist/, $(CSS)): dist/%: %
cp $< $@
dist:
mkdir dist

View File

@ -0,0 +1,231 @@
var TypesMod = require("./Types.js");
var Type = TypesMod.Type;
var string = TypesMod.string;
var bool = TypesMod.bool;
var any = TypesMod.any;
if(require.main === module)
{
var RsNodeHttpConnection = require("./RsNodeHttpConnection.js");
debugger;
var connection = new RsNodeHttpConnection();
var RsApi = require("./RsApi.js");
var RS = new RsApi(connection);
var tests = [];
PeersTest(tests);
tests.map(function(test){
test(RS);
});
}
function PeersTest(tests, doc)
{
// compound types
var location = new Type("location",
{
avatar_address: string,
groups: any,
is_online: bool,
location: string,
peer_id: any,
});
var peer_info = new Type("peer_info",
{
name: string,
pgp_id: any,
locations: [location],
});
var peers_list = new Type("peers_list",[peer_info]);
tests.push(function(RS){
console.log("testing peers module...");
console.log("expected schema is:")
console.log(graphToText(peers_list));
RS.request({path: "peers"}, function(resp){
//console.log("got response:"+JSON.stringify(resp));
var ok = peers_list.check(function(str){console.log(str);}, resp.data, [])
if(ok)
console.log("success");
else
console.log("fail");
});
});
function graphToText(top_node)
{
//var dbg = function(str){console.log(str);};
var dbg = function(str){};
dbg("called graphToText with " + top_node);
var res = "";
_visit(top_node.getObj(), 0);
return res;
function _indent(count)
{
var str = "";
for(var i = 0; i < count; i++)
{
str = str + " ";
}
return str;
}
function _visit(node, indent)
{
dbg("_visit");
if(node instanceof Array)
{
dbg("is instanceof Array");
//res = res + "[";
res = res + "array\n";
_visit(node[0], indent);
//res = res + _indent(indent) + "]\n";
}
else if(node instanceof Type && node.isLeaf())
{
dbg("is instanceof Type");
res = res + node.getName() + "\n";
}
else // Object, have to check all children
{
dbg("is Object");
//res = res + "{\n";
for(m in node.getObj())
{
res = res + _indent(indent+1) + m + ": ";
_visit(node.getObj()[m], indent+1);
}
//res = res + _indent(indent) + "}\n";
}
}
}
}
// ************ below is OLD stuff, to be removed ************
var Location =
{
avatar_address: String(),
groups: undefined,
is_online: Boolean(),
location: String(),
name: String(),
peer_id: undefined,
pgp_id: undefined,
};
var PeerInfo =
{
name: String(),
locations: [Location],
};
var PeersList = [PeerInfo];
function checkIfMatch(ref, other)
{
var ok = true;
// sets ok to false on error
function check(subref, subother, path)
{
//console.log("checking");
//console.log("path: " + path);
//console.log("subref: " +subref);
//console.log("subother: "+subother);
if(subref instanceof Array)
{
//console.log("is Array: " + path);
if(!(subother instanceof Array))
{
ok = false;
console.log("Error: not an Array " + path);
return;
}
if(subother.length == 0)
{
console.log("Warning: can't check Array of lentgh 0 " + path);
return;
}
// check first array member
check(subref[0], subother[0], path);
return;
}
// else compare as dict
for(m in subref)
{
if(!(m in subother))
{
ok = false;
console.log("Error: missing member \"" + m + "\" in "+ path);
continue;
}
if(subref[m] === undefined)
{
// undefined = don't care what it is
continue;
}
if(typeof(subref[m]) == typeof(subother[m]))
{
if(typeof(subref[m]) == "object")
{
// make deep object inspection
path.push(m);
check(subref[m], subother[m], path);
path.pop();
}
// else everthing is fine
}
else
{
ok = false;
console.log("Error: member \"" + m + "\" has wrong type in "+ path);
}
}
// TODO: check for additional members and print notice
}
check(ref, other, []);
return ok;
}
function stringifyTypes(obj)
{
if(obj instanceof Array)
{
return [stringifyTypes(obj[0])];
}
var ret = {};
for(m in obj)
{
if(typeof(obj[m]) === "object")
ret[m] = stringifyTypes(obj[m]);
else
ret[m] = typeof obj[m];
}
return ret;
}
// trick to get multiline string constants: use comment as string constant
var input = function(){/*
[{
"locations": [{
"avatar_address": "/5cfed435ebc24d2d0842f50c6443ec76/avatar_image",
"groups": null,
"is_online": true,
"location": "",
"name": "se2",
"peer_id": "5cfed435ebc24d2d0842f50c6443ec76",
"pgp_id": "985CAD914B19A212"
}],
"name": "se2"
}]
*/}.toString().slice(14,-3);
// **************** end of old stuff ***************************

View File

@ -0,0 +1,46 @@
A new approach to build a webinterface for RS
=============================================
1. get JSON encoded data from the backend, data contains a state token
2. render data with react.js
3. ask the backend if the state token from step 1 expired. If yes, then start again with step 1.
Steps 1. and 3. are common for most things, only Step 2. differs. This allows to re-use code for steps 1. and 3.
BUILD / INSTALLATION
------------
- run (requires wget, use MinGW shell on Windows)
make
- all output files are now in the "dist" folder
- use the --webinterface 9090 command line parameter to enable webui in retroshare-nogui
- set the --docroot parameter of retroshare-nogui to point to the "dist" directory
DEVELOPMENT
-----------
- Ubuntu: install nodejs package
sudo apt-get install nodejs
- Windows: download and install nodejs from http://nodejs.org
- Download development tools with the nodejs package manager (short npm)
npm install
- during development, run these two commands at the same time
while true; do make --silent; sleep 1; done
grunt watch
- command one will copy the source files to the "dist" directory if they change
- command two will tell the browser to reload if a file changes
API DOCUMENTATION
-----------------
- run
node PeersTest.js
- this will print the expected schema of the api output, and it will try to test it with real data
- run retroshare-nogui with webinterface enabled on port 9090, to test if the real output of the api matches the expected schema
CONTRIBUTE
----------
- if you are a web developer or want to become one
get in contact!
- lots of work to do, i need you!

View File

@ -0,0 +1,109 @@
/**
* JS Api for Retroshare
* @constructor
* @param {object} connection - an object which implements a request() function.
* The request function should take two parameters: an object to be send as request and a callback.
* The callback should get called with an response object on success.
*/
function RsApi(connection)
{
var runnign = true;
/**
* Send a request to the server
* @param req - the request so send
* @param {Function} cb - callback function which takes the response as parameter
*/
this.request = function(req, cb)
{
connection.request(req, cb);
};
var tokenlisteners = [];
/**
* Register a callback to be called when the state token expired.
* @param {Function} listener - the callback function, which does not take arguments
* @param token - the state token to listen for
*/
this.register_token_listener = function(listener, token)
{
tokenlisteners.push({listener:listener, token:token});
};
/**
* Unregister a previously registered callback.
*/
this.unregister_token_listener = function(listener) // no token as parameter, assuming unregister from all listening tokens
{
var to_delete = [];
for(var i=0; i<tokenlisteners.length; i++){
if(tokenlisteners[i].listener === listener){
to_delete.push(i);
}
}
for(var i=0; i<to_delete.length; i++){
// copy the last element to the current index
var index = to_delete[i];
tokenlisteners[index] = tokenlisteners[tokenlisteners.length-1];
// remove last element
tokenlisteners.pop();
}
};
/**
* start polling for state changes
*/
this.start = function(){
running = true;
setTimeout(tick, TICK_INTERVAL);
}
/**
* stop polling for state changes
*/
this.stop = function(){
running = false;
}
// ************** interal stuff **************
var TICK_INTERVAL = 3000;
function received_tokenstates(resp)
{
if(resp.data){
for(var i=0; i<resp.data.length; i++){
var token = resp.data[i];
// search the listener for this token
for(var j=0; j<tokenlisteners.length; j++){
if(tokenlisteners[j].token === token){
// call the listener
tokenlisteners[j].listener();
}
}
}
}
// schedule new update
if(running)
setTimeout(tick, TICK_INTERVAL);
};
function received_error()
{
// try again, maybe want a better logic later
if(running)
setTimeout(tick, TICK_INTERVAL);
};
function tick()
{
var data = [];
// maybe cache the token list?
// profiler will tell us if we should
for(var i=0; i<tokenlisteners.length; i++){
data.push(tokenlisteners[i].token);
}
connection.request({
path: "statetokenservice",
data: data,
}, received_tokenstates, received_error);
};
};
// with this trick, we should be able to run in browser or nodejs
if(typeof window === 'undefined')
{
// we are running in nodejs, so have to add to export
module.exports = RsApi;
}

View File

@ -0,0 +1,51 @@
var http = require('http');
/**
* Connection to the RS backend using http for running under node.js
* Mainly for testing, but could also use it for general purpose scripting.
* @constructor
*/
module.exports = function()
{
var server_hostname = "localhost";
var server_port = "9090";
var api_root_path = "/api/v2/";
this.request = function(request, callback)
{
var data;
if(request.data)
data = JSON.stringify(request.data);
else
data = "";
// NODEJS specific
var req = http.request({
host: server_hostname,
port: server_port,
path: api_root_path + request.path,
headers: {
"Content-Type": "application/json",
"Content-Length": data.length, // content length is required, else Wt will not provide the data (maybe WT does not like chunked encoding?)
}
//method: "POST",
}, function(response){
var databuffer = [];
response.on("data", function(chunk){
//console.log("got some data");
databuffer = databuffer + chunk;
})
response.on("end", function(){
//console.log("finished receiving data");
//console.log("data:"+databuffer);
callback(JSON.parse(databuffer));
})
});
//console.log("uploading data:");
//console.log(data);
req.write(data);
req.end();
}
}

View File

@ -0,0 +1,79 @@
/**
* Connection to the RS backend using XHR
* (could add other connections later, for example WebSockets)
* @constructor
*/
function RsXHRConnection(server_hostname, server_port)
{
var debug;
//debug = function(str){console.log(str);};
debug = function(str){};
//server_hostname = "localhost";
//server_port = "9090";
var api_root_path = "/api/v2/";
/**
* Send a request to the backend
* automatically encodes the request as JSON before sending it to the server
* @param {object} req - the request to send to the server
* @param {function} cb - callback function to be called to handle the response. The callback takes one object as parameter. Can be left undefined.
* @param {function} err_cb - callback function to signal a failed request. Can be undefined.
*/
this.request = function(req, cb, err_cb)
{
//var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP");
// TODO: window is not available in QML
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
//console.log("onreadystatechanged state"+xhr.readyState);
// TODO: figure out how to catch errors like connection refused
// maybe want to have to set a state variable like ok=false
// the gui could then display: "no connection to server"
if (xhr.readyState === 4) {
if(xhr.status !== 200)
{
console.log("RsXHRConnection: request failed with status: "+xhr.status);
console.log("request was:");
console.log(req);
if(err_cb !== undefined)
err_cb();
return;
}
// received response
debug("RsXHRConnection received response:");
debug(xhr.responseText);
if(false)//if(xhr.responseText === "")
{
debug("Warning: response is empty");
return;
}
try
{
var respObj = JSON.parse(xhr.responseText);
}
catch(e)
{
debug("Exception during response handling: "+e);
}
if(cb === undefined)
debug("No callback function specified");
else
cb(respObj);
}
}
// post is required for sending data
var method;
if(req.data){
method = "POST";
} else {
method = "GET";
}
xhr.open(method, "http://"+server_hostname+":"+server_port+api_root_path+req.path);
var data = JSON.stringify(req.data);
debug("RsXHRConnection sending data:");
debug(data);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(data);
};
};

View File

@ -0,0 +1,146 @@
/**
* Construct a new type from an array, object or function.
* Use instances of this class to build a schema graph.
* The schema graph must not contain data other than arrays and instances of this class.
* Use the check function to check if arbitrary JS objects matches the schema.
* @constructor
* @param {String} name - name for the new type
* @param obj - array, object or function
* array: array should contain one instance of class "Type"
* object: object members can be arrays or instances of class "Type".
* Can also have child objects, but the leaf members have to be instances of class "Type"
* function: a function which takes three parameters: log, other, stack
* must return true if other matches the type, can report errors using the function passed in log.
*/
function Type(name, obj)
{
//var dbg = function(str){console.log(str);};
var dbg = function(str){};
this.getName = function(){
return name;
}
this.isLeaf = function(){
return typeof(obj) === "function";
}
this.getObj = function(){
return obj;
}
this.check = function(log, other, stack)
{
if(typeof(obj) === "object")
{
stack.push("<"+name+">");
var ok = _check(log, obj, other, stack);
stack.pop;
return ok;
}
if(typeof(obj) === "function")
return obj(log, other, stack);
log("FATAL Error: wrong usage of new Type(), second parameter should be an object or checker function");
return false;
}
function _check(log, ref, other, stack)
{
dbg("_check");
dbg("ref=" + ref);
dbg("other=" + other);
dbg("stack=[" + stack + "]");
if(ref instanceof Array)
{
dbg("is instanceof Array");
if(other instanceof Array)
{
if(other.length > 0)
{
return _check(log, ref[0], other[0], stack);
}
else
{
log("Warning: can't check array of length 0 in ["+stack+"]");
return true;
}
}
else
{
log("Error: not an Array ["+stack+"]");
return false;
}
}
if(ref instanceof Type)
{
dbg("is instanceof Type");
return ref.check(log, other, stack);
}
else // Object, have to check all children
{
dbg("is Object");
var ok = true;
for(m in ref)
{
if(m in other)
{
stack.push(m);
ok = ok && _check(log, ref[m], other[m], stack);
stack.pop();
}
else
{
log("Error: missing member \""+m+"\" in ["+stack+"]");
ok = false;
}
}
// check for additionally undocumented members
for(m in other)
{
if(!(m in ref))
{
log("Warning: found additional member \""+m+"\" in ["+stack+"]");
}
}
return ok;
}
}
};
// basic data types
// - string
// - bool
// - any (placeholder for unknown type)
var string = new Type("string",
function(log, other, stack)
{
if(typeof(other) !== "string")
{
log("Error: not a string ["+stack+"]");
return false;
}
else
return true;
}
);
var bool = new Type("bool",
function(log, other, stack)
{
if(typeof(other) !== "boolean")
{
log("Error: not a bool ["+stack+"]");
return false;
}
else
return true;
}
);
var any = new Type("any",
function(log, other, stack)
{
return true;
}
);
exports.Type = Type;
exports.string = string;
exports.bool = bool;
exports.any = any;

View File

@ -0,0 +1,64 @@
body {
background-color: black;
color: lime;
font-family: monospace;
margin: 0em;
padding: 1.5em;
font-size: 1.1em;
}
.nav{
list-style-type: none;
padding: 0em;
margin: 0em;
}
.nav li{
display: inline;
padding: 0.1em;
margin-right: 1em;
border-width: 0.1em;
border-color: blue;
border-bottom-style: solid;
cursor: pointer;
}
td{
padding: 0.3em;
border-style: solid;
border-width: 0.1em;
border-color: lime;
}
.btn{
border-style: solid;
border-color: lime;
border-width: 0.1em;
cursor: pointer;
padding: 0.1em;
}
input, textarea{
color: lime;
font-family: monospace;
background-color: black;
border-color: lime;
}
#logo_splash{
-webkit-animation-fill-mode: forwards; /* Chrome, Safari, Opera */
animation-fill-mode: forwards;
-webkit-animation-name: logo_splash; /* Chrome, Safari, Opera */
-webkit-animation-duration: 3s; /* Chrome, Safari, Opera */
animation-name: logo_splash;
animation-duration: 3s;
text-align: center;
}
/* Chrome, Safari, Opera */
@-webkit-keyframes logo_splash {
from {opacity: 0;}
to {opacity: 1;}
}
/* Standard syntax */
@keyframes logo_splash {
from {opacity: 0;}
to {opacity: 1;}
}

625
libresapi/src/webui/gui.jsx Normal file
View File

@ -0,0 +1,625 @@
var connection = new RsXHRConnection(window.location.hostname, window.location.port);
var RS = new RsApi(connection);
RS.start();
var api_url = window.location.protocol + "//" + window.location.hostname + ":" + window.location.port + "/api/v2/";
var filestreamer_url = window.location.protocol + "//" +window.location.hostname + ":" + window.location.port + "/fstream/";
// implements automatic update using the state token system
// components using this mixin should have a member "getPath()" to specify the resource
var AutoUpdateMixin =
{
// react component lifecycle callbacks
componentDidMount: function()
{
this._aum_debug("AutoUpdateMixin did mount path="+this.getPath());
this._aum_on_data_changed();
},
componentWillUnmount: function()
{
this._aum_debug("AutoUpdateMixin will unmount path="+this.getPath());
RS.unregister_token_listener(this._aum_on_data_changed);
},
// private auto update mixin methods
_aum_debug: function(msg)
{
//console.log(msg);
},
_aum_on_data_changed: function()
{
RS.request({path: this.getPath()}, this._aum_response_callback);
},
_aum_response_callback: function(resp)
{
this._aum_debug("Mixin received data: "+JSON.stringify(resp));
// it is impossible to update the state of an unmounted component
// but it may happen that the component is unmounted before a request finishes
// if response is too late, we drop it
if(!this.isMounted())
{
this._aum_debug("AutoUpdateMixin: component not mounted. Discarding response. path="+this.getPath());
return;
}
var state = this.state;
state.data = resp.data;
this.setState(state);
RS.unregister_token_listener(this._aum_on_data_changed);
RS.register_token_listener(this._aum_on_data_changed, resp.statetoken);
},
};
// the signlaSlotServer decouples event senders from event receivers
// senders just send their events
// the server will forwards them to all receivers
// receivers have to register/unregister at the server
var signalSlotServer =
{
clients: [],
/**
* Register a client which wants to participate
* in the signal slot system. Clients must provide a function
* onSignal(signal_name, parameters)
* where signal_name is a string and parameters and array or object
*/
registerClient: function(client)
{
this.clients.push(client);
},
/**
* Unregister a previously registered client.
*/
unregisterClient : function(client) // no token as parameter, assuming unregister from all listening tokens
{
var to_delete = [];
var clients = this.clients;
for(var i=0; i<clients.length; i++){
if(clients[i] === client){
to_delete.push(i);
}
}
for(var i=0; i<to_delete.length; i++){
// copy the last element to the current index
var index = to_delete[i];
clients[index] = clients[clients.length-1];
// remove last element
clients.pop();
}
},
/**
* Emit the signale given by its name, with the optional parameters in params.
*/
emitSignal: function(signal_name, parameters)
{
var clients = this.clients;
for(var i=0; i<clients.length; i++){
clients[i].onSignal(signal_name, parameters);
}
},
}
var SignalSlotMixin =
{
conected_signals: [],
componentDidMount: function()
{
signalSlotServer.registerClient(this);
},
componentWillUnmount: function()
{
signalSlotServer.unregisterClient(this);
},
/**
* emit a signal
*/
emit: function(signal_name, parameters)
{
signalSlotServer.emitSignal(signal_name, parameters);
},
/**
* connect the callback to the signal
* the connection is automatically destroyed when this component gets unmounted
*/
connect: function(signal_name, callback)
{
this.conected_signals.push({name: signal_name, callback: callback});
},
/**
* callback for signal server
*/
onSignal: function(signal_name, parameters)
{
for(var i=0; i<this.conected_signals.length; i++){
if(this.conected_signals[i].name === signal_name){
this.conected_signals[i].callback(parameters);
}
}
},
};
var Peers = React.createClass({
mixins: [AutoUpdateMixin],
getPath: function(){return "peers";},
getInitialState: function(){
return {data: []};
},
render: function(){
var renderOne = function(f){
console.log("make one");
return <p>{f.name} <img src={api_url+f.locations[0].avatar_address} /></p>;
};
return <div>{this.state.data.map(renderOne)}</div>;
},
});
var Peers2 = React.createClass({
mixins: [AutoUpdateMixin, SignalSlotMixin],
getPath: function(){return "peers";},
getInitialState: function(){
return {data: []};
},
add_friend_handler: function(){
this.emit("url_changed", {url: "add_friend"});
},
render: function(){
var component = this;
var Peer = React.createClass({
remove_peer_handler: function(){
var yes = window.confirm("Remove "+this.props.data.name+" from friendslist?");
if(yes){
RS.request({path: component.getPath()+"/"+this.props.data.pgp_id+"/delete"});
}
},
render: function(){
var locations = this.props.data.locations.map(function(loc){
var online_style = {
width: "1em",
height: "1em",
borderRadius: "0.5em",
backgroundColor: "grey",
};
if(loc.is_online)
online_style.backgroundColor = "lime";
return(<li key={loc.peer_id}>{loc.location} <div style={online_style}></div></li>);
});
// TODO: fix the url, should get the "../api/v2" prefix from a single variable
var avatar_url = "";
if(this.props.data.locations.length > 0 && this.props.data.locations[0].avatar_address !== "")
avatar_url = api_url + component.getPath() + this.props.data.locations[0].avatar_address
var remove_button_style = {
color: "red",
fontSize: "1.5em",
padding: "0.2em",
cursor: "pointer",
};
var remove_button = <div onClick={this.remove_peer_handler} style={remove_button_style}>X</div>;
return(<tr><td><img src={avatar_url}/></td><td>{this.props.data.name}</td><td><ul>{locations}</ul></td><td>{remove_button}</td></tr>);
}
});
return (
<div>
{/* span reduces width to only the text length, div does not */}
<span onClick={this.add_friend_handler} className="btn">&#43; add friend</span>
<table>
<tr><th>avatar</th><th> name </th><th> locations</th><th></th></tr>
{this.state.data.map(function(peer){ return <Peer key={peer.name} data={peer}/>; })}
</table>
</div>);
},
});
var AddPeerWidget = React.createClass({
getInitialState: function(){
return {page: "start"};
},
add_friend_handler: function(){
var cert_string = this.refs.cert.getDOMNode().value;
if(cert_string != null){
// global replae all carriage return, because rs does not like them in certstrings
//cert_string = cert_string.replace(/\r/gm, "");
RS.request({path: "peers/examine_cert", data: {cert_string: cert_string}}, this.examine_cert_callback);
this.setState({page:"waiting", cert_string: cert_string});
}
},
examine_cert_callback: function(resp){
this.setState({page: "peer", data: resp.data});
},
final_add_handler: function(){
this.setState({page: "start"});
RS.request({path: "peers", data: {cert_string: this.state.cert_string}});
},
render: function(){
if(this.state.page === "start")
return(
<div>
<p>paste your friends key below</p>
<textarea ref="cert" cols="70" rows="16"></textarea><br/>
<input
type="button"
value="read key"
onClick={this.add_friend_handler}
/>
</div>
);
if(this.state.page === "waiting")
return(
<div>
waiting for response from server...
</div>
);
if(this.state.page === "peer")
return(
<div>
<p>Do you want to add {this.state.data.name} to your friendslist?</p>
<span onClick={this.final_add_handler} className="btn">yes</span>
</div>
);
},
});
var DownloadsWidget = React.createClass({
mixins: [AutoUpdateMixin, SignalSlotMixin],
getPath: function(){ return "transfers/downloads";},
getInitialState: function(){
return {data: []};
},
render: function(){
var widget = this;
var DL = React.createClass({
render: function()
{
var file = this.props.data;
var startFn = function(){
RS.request({
path: "transfers/control_download",
data: {
action: "start",
id: file.id,
}
}, function(){
console.log("start dl callback");
}
)};
var pauseFn = function(){
RS.request({
path: "transfers/control_download",
data: {
action: "pause",
id: file.id,
}
}, function(){
console.log("pause dl callback");
}
)};
var cancelFn = function(){
RS.request({
path: "transfers/control_download",
data: {
action: "cancel",
id: file.id,
}
}, function(){
console.log("cancel dl callback");
}
)};
var playFn = function(){
widget.emit("play_file", {name: file.name, hash: file.hash})
};
var playBtn = <div></div>;
if(file.name.slice(-3) === "mp3")
playBtn = <div className="btn" onClick={playFn}>play</div>;
var ctrlBtn = <div></div>;
if(file.download_status==="paused")
{
ctrlBtn = <div className="btn" onClick={startFn}>start</div>;
}
else
{
ctrlBtn = <div className="btn" onClick={pauseFn}>pause</div>;
}
return(<tr>
<td>{this.props.data.name}</td>
<td>{this.props.data.size}</td>
<td>{this.props.data.transfered / this.props.data.size}</td>
<td>{this.props.data.download_status}</td>
<td>{ctrlBtn} <div className="btn" onClick={cancelFn}>cancel</div> {playBtn}</td>
</tr>);
}
});
return (
<table>
<tr>
<th>name</th>
<th>size</th>
<th>completed</th>
<th>download status</th>
<th>actions</th>
</tr>
{this.state.data.map(function(dl){ return <DL key={dl.hash} data={dl}/>; })}
</table>);
},
});
var SearchWidget = React.createClass({
getInitialState: function(){
return {search_id: undefined};
},
handleSearch: function(){
console.log("searching for: "+this.refs.searchbox.getDOMNode().value);
var search_string = this.refs.searchbox.getDOMNode().value;
RS.request({path: "filesearch/create_search", data:{distant: true, search_string: search_string}}, this.onCreateSearchResponse);
},
onCreateSearchResponse: function(resp){
if(this.isMounted()){
this.setState({search_id: resp.data.search_id});
}
},
render: function(){
var ResultList = React.createClass({
mixins: [AutoUpdateMixin],
getPath: function(){ return "filesearch/"+this.props.id; },
getInitialState: function(){
return {data: []};
},
start_download: function(fileinfo){
RS.request({
path: "transfers/control_download",
data:{
action: "begin",
name: fileinfo.name,
size: fileinfo.size,
hash: fileinfo.hash,
},
});
},
render: function(){
var c2 = this;
var File = React.createClass({
render: function(){
var file = this.props.data;
return(
<tr>
<td>{file.name}</td>
<td>{file.size}</td>
<td><span onClick={function(){c2.start_download(file);}} className="btn">download</span></td>
</tr>);
},
});
return (
<table>
<tr><th>name</th><th>size</th><th></th></tr>
{this.state.data.map(
function(file){
return <File key={file.id} data={file}/>;
}
)}
</table>);
}
});
var results = <div></div>;
if(this.state.search_id !== undefined)
{
results = <ResultList id={this.state.search_id} />;
}
return(
<div>
<p>turtle file search</p>
<div>
<input type="text" ref="searchbox" />
<input
type="button"
value="search"
onClick={this.handleSearch}
/>
</div>
{results}
</div>);
},
});
var AudioPlayerWidget = React.createClass({
mixins: [SignalSlotMixin],
getInitialState: function(){
return {file: undefined};
},
componentWillMount: function(){
this.connect("play_file", this.play_file);
},
play_file: function(file){
this.setState({file: file});
},
render: function(){
if(this.state.file === undefined)
{
return(<div></div>);
}
else
{
return(
<div>
<p>{this.state.file.name}</p>
<audio controls src={filestreamer_url+this.state.file.hash} type="audio/mpeg">
</audio>
</div>
);
}
},
});
var PasswordWidget = React.createClass({
mixins: [AutoUpdateMixin],
getInitialState: function(){
return {data: {want_password: false}};
},
getPath: function(){
return "control/password";
},
sendPassword: function(){
RS.request({path: "control/password", data:{password: this.refs.password.getDOMNode().value}})
},
render: function(){
if(this.state.data.want_password === false)
{
return(<p>PasswordWidget: nothing to do.</p>);
}
else
{
return(
<div>
<p>Enter password for key {this.state.data.key_name}</p>
<input type="text" ref="password" />
<input
type="button"
value="ok"
onClick={this.sendPassword}
/>
</div>
);
}
},
});
var AccountSelectWidget = React.createClass({
mixins: [AutoUpdateMixin],
getInitialState: function(){
return {data: []};
},
getPath: function(){
return "control/locations";
},
selectAccount: function(id){
console.log("login with id="+id)
RS.request({path: "control/login", data:{id: id}});
},
render: function(){
var component = this;
return(
<div>
<div><p>select a location to log in</p></div>
{this.state.data.map(function(location){
return <div key={location.id} className="btn" onClick ={function(){component.selectAccount(location.id);}}>{location.name} ({location.location})</div>;
})}
</div>
);
},
});
var LoginWidget = React.createClass({
mixins: [AutoUpdateMixin],
getInitialState: function(){
return {data: {runstate: "waiting_init"}};
},
getPath: function(){
return "control/runstate";
},
shutdown: function(){
RS.request({path: "control/shutdown"});
},
render: function(){
if(this.state.data.runstate === "waiting_init")
{
return(<p>Retroshare is initialising... please wait...</p>);
}
else if(this.state.data.runstate === "waiting_account_select")
{
return(<AccountSelectWidget/>);
}
else
{
return(
<div>
<p>runstate: {this.state.data.runstate}</p>
<div onClick={this.shutdown} className="btn">shutdown Retroshare</div>
</div>
);
}
},
});
var MainWidget = React.createClass({
mixins: [SignalSlotMixin],
getInitialState: function(){
return {page: "login"};
},
componentWillMount: function()
{
var outer = this;
this.connect("url_changed",
function(params)
{
console.log("MainWidget received url_changed. url="+params.url);
outer.setState({page: params.url});
});
},
render: function(){
var outer = this;
var mainpage = <p>page not implemented: {this.state.page}</p>;
if(this.state.page === "main")
{
mainpage = <p>
A new webinterface for Retroshare. Build with react.js.
React allows to build a modern and user friendly single page application.
The component system makes this very simple.
Updating the GUI is also very simple: one React mixin can handle updating for all components.
</p>;
}
if(this.state.page === "friends")
{
mainpage =
<div>
<p>the list updates itself when something changes. Lots of magic happens here!</p>
<Peers2 />
</div>;
}
if(this.state.page === "downloads")
{
mainpage = <DownloadsWidget/>;
}
if(this.state.page === "search")
{
mainpage = <SearchWidget/>;
}
if(this.state.page === "add_friend")
{
mainpage = <AddPeerWidget/>;
}
if(this.state.page === "login")
{
mainpage = <LoginWidget/>;
}
return (
<div>
<PasswordWidget/>
<AudioPlayerWidget/>
<ul className="nav">
<li onClick={function(){outer.emit("url_changed", {url: "main"});}}>
Start
</li>
<li onClick={function(){outer.emit("url_changed", {url: "login"});}}>
Login
</li>
<li onClick={function(){outer.emit("url_changed", {url: "friends"});}}>
Friends
</li>
<li onClick={function(){outer.emit("url_changed", {url: "downloads"});}}>
Downloads
</li>
<li onClick={function(){outer.emit("url_changed", {url: "search"});}}>
Search
</li>
</ul>
{mainpage}
</div>
);
},
});
React.render(
<MainWidget />,
document.body
);

View File

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html>
<head>
<title>New webinterface for Retroshare</title>
<script src="react.js"></script>
<script src="JSXTransformer.js"></script>
<script src="RsXHRConnection.js"></script>
<script src="RsApi.js"></script>
<script type="text/jsx" src="gui.jsx"></script>
<!-- automatic page reload -->
<!--<script src="http://localhost:9091"></script>-->
<!-- load this last, because it contains errors -->
<script src="http://localhost:35729/livereload.js"></script>
<link href="green-black.css" rel="stylesheet">
<meta charset="utf-8">
</head>
<body>
<p>loading lots of stuff...</p>
<div id="logo_splash">
<img src="img/logo_splash.png"></img>
</div>
</body>
</html>

View File

@ -0,0 +1,20 @@
{
"name": "rswebui",
"version": "0.0.0",
"dependencies": {
},
"devDependencies": {
"grunt": "^0.4.5",
"grunt-contrib-watch": "^0.6.1",
"live-reload": "^1.1.0",
"onchange": "^1.0.0",
"parallelshell": "^1.1.1"
},
"scripts": {
"comment": "rem stuff below does not work, except the livereload",
"watch": "parallelshell \"npm run build\" \"npm run build:watch\" \"npm run livereload\"",
"watch:build": "onchange '**.html' -- 'npm run build'",
"build": "copy /Y index.html build",
"livereload": "live-reload --port 9091 build/"
}
}