mirror of
https://github.com/RetroShare/RetroShare.git
synced 2025-03-06 21:56:07 -05:00
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:
parent
c480c0c9d3
commit
8fb6670857
5
libresapi/src/README.md
Normal file
5
libresapi/src/README.md
Normal 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
|
13
libresapi/src/webui/Gruntfile.js
Normal file
13
libresapi/src/webui/Gruntfile.js
Normal 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');
|
||||
};
|
31
libresapi/src/webui/Makefile
Normal file
31
libresapi/src/webui/Makefile
Normal 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
|
231
libresapi/src/webui/PeersTest.js
Normal file
231
libresapi/src/webui/PeersTest.js
Normal 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 ***************************
|
46
libresapi/src/webui/README.md
Normal file
46
libresapi/src/webui/README.md
Normal 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!
|
109
libresapi/src/webui/RsApi.js
Normal file
109
libresapi/src/webui/RsApi.js
Normal 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;
|
||||
}
|
51
libresapi/src/webui/RsNodeHttpConnection.js
Normal file
51
libresapi/src/webui/RsNodeHttpConnection.js
Normal 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();
|
||||
}
|
||||
}
|
79
libresapi/src/webui/RsXHRConnection.js
Normal file
79
libresapi/src/webui/RsXHRConnection.js
Normal 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);
|
||||
};
|
||||
};
|
146
libresapi/src/webui/Types.js
Normal file
146
libresapi/src/webui/Types.js
Normal 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;
|
64
libresapi/src/webui/green-black.css
Normal file
64
libresapi/src/webui/green-black.css
Normal 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
625
libresapi/src/webui/gui.jsx
Normal 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">+ 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
|
||||
);
|
29
libresapi/src/webui/index.html
Normal file
29
libresapi/src/webui/index.html
Normal 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>
|
20
libresapi/src/webui/package.json
Normal file
20
libresapi/src/webui/package.json
Normal 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/"
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user