diff --git a/libresapi/src/README.md b/libresapi/src/README.md
new file mode 100644
index 000000000..151e6000e
--- /dev/null
+++ b/libresapi/src/README.md
@@ -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
\ No newline at end of file
diff --git a/libresapi/src/webui/Gruntfile.js b/libresapi/src/webui/Gruntfile.js
new file mode 100644
index 000000000..d76b25d17
--- /dev/null
+++ b/libresapi/src/webui/Gruntfile.js
@@ -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');
+};
\ No newline at end of file
diff --git a/libresapi/src/webui/Makefile b/libresapi/src/webui/Makefile
new file mode 100644
index 000000000..4666a111b
--- /dev/null
+++ b/libresapi/src/webui/Makefile
@@ -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
diff --git a/libresapi/src/webui/PeersTest.js b/libresapi/src/webui/PeersTest.js
new file mode 100644
index 000000000..eba0d1a9d
--- /dev/null
+++ b/libresapi/src/webui/PeersTest.js
@@ -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 ***************************
\ No newline at end of file
diff --git a/libresapi/src/webui/README.md b/libresapi/src/webui/README.md
new file mode 100644
index 000000000..03e9a107f
--- /dev/null
+++ b/libresapi/src/webui/README.md
@@ -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!
\ No newline at end of file
diff --git a/libresapi/src/webui/RsApi.js b/libresapi/src/webui/RsApi.js
new file mode 100644
index 000000000..ea7a743ef
--- /dev/null
+++ b/libresapi/src/webui/RsApi.js
@@ -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");
+ 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;
\ No newline at end of file
diff --git a/libresapi/src/webui/green-black.css b/libresapi/src/webui/green-black.css
new file mode 100644
index 000000000..6cd58e30c
--- /dev/null
+++ b/libresapi/src/webui/green-black.css
@@ -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;}
+}
diff --git a/libresapi/src/webui/gui.jsx b/libresapi/src/webui/gui.jsx
new file mode 100644
index 000000000..0ac1daca6
--- /dev/null
+++ b/libresapi/src/webui/gui.jsx
@@ -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{f.name}
;
+ };
+ return {this.state.data.map(renderOne)}
;
+ },
+ });
+
+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({loc.location}
);
+ });
+ // 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 = X
;
+ return({this.props.data.name} {remove_button} );
+ }
+ });
+ return (
+
+ {/* span reduces width to only the text length, div does not */}
+
+ add friend
+
+ avatar name locations
+ {this.state.data.map(function(peer){ return ; })}
+
+
);
+ },
+});
+
+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(
+
+
paste your friends key below
+
+
+
+ );
+ if(this.state.page === "waiting")
+ return(
+
+ waiting for response from server...
+
+ );
+ if(this.state.page === "peer")
+ return(
+
+
Do you want to add {this.state.data.name} to your friendslist?
+
yes
+
+ );
+ },
+});
+
+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 =
;
+ if(file.name.slice(-3) === "mp3")
+ playBtn = play
;
+
+ var ctrlBtn =
;
+ if(file.download_status==="paused")
+ {
+ ctrlBtn = start
;
+ }
+ else
+ {
+ ctrlBtn = pause
;
+ }
+ return(
+ {this.props.data.name}
+ {this.props.data.size}
+ {this.props.data.transfered / this.props.data.size}
+ {this.props.data.download_status}
+ {ctrlBtn} cancel
{playBtn}
+ );
+ }
+ });
+ return (
+
+
+ name
+ size
+ completed
+ download status
+ actions
+
+ {this.state.data.map(function(dl){ return ; })}
+
);
+ },
+});
+
+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(
+
+ {file.name}
+ {file.size}
+ download
+ );
+ },
+ });
+ return (
+
+ name size
+ {this.state.data.map(
+ function(file){
+ return ;
+ }
+ )}
+
);
+ }
+ });
+
+ var results =
;
+ if(this.state.search_id !== undefined)
+ {
+ results = ;
+ }
+ return(
+ );
+ },
+});
+
+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(
);
+ }
+ else
+ {
+ return(
+
+
{this.state.file.name}
+
+
+
+ );
+ }
+ },
+});
+
+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(PasswordWidget: nothing to do.
);
+ }
+ else
+ {
+ return(
+
+ );
+ }
+ },
+});
+
+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(
+
+
select a location to log in
+ {this.state.data.map(function(location){
+ return
{location.name} ({location.location})
;
+ })}
+
+ );
+ },
+});
+
+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(Retroshare is initialising... please wait...
);
+ }
+ else if(this.state.data.runstate === "waiting_account_select")
+ {
+ return( );
+ }
+ else
+ {
+ return(
+
+
runstate: {this.state.data.runstate}
+
shutdown Retroshare
+
+ );
+ }
+ },
+});
+
+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 = page not implemented: {this.state.page}
;
+ if(this.state.page === "main")
+ {
+ mainpage =
+ 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.
+
;
+ }
+ if(this.state.page === "friends")
+ {
+ mainpage =
+
+
the list updates itself when something changes. Lots of magic happens here!
+
+
;
+ }
+ if(this.state.page === "downloads")
+ {
+ mainpage = ;
+ }
+ if(this.state.page === "search")
+ {
+ mainpage = ;
+ }
+ if(this.state.page === "add_friend")
+ {
+ mainpage = ;
+ }
+ if(this.state.page === "login")
+ {
+ mainpage = ;
+ }
+ return (
+
+
+
+
+
+ Start
+
+
+ Login
+
+
+ Friends
+
+
+ Downloads
+
+
+ Search
+
+
+ {mainpage}
+
+ );
+ },
+});
+
+React.render(
+ ,
+ document.body
+);
\ No newline at end of file
diff --git a/libresapi/src/webui/index.html b/libresapi/src/webui/index.html
new file mode 100644
index 000000000..ba11d117e
--- /dev/null
+++ b/libresapi/src/webui/index.html
@@ -0,0 +1,29 @@
+
+
+
+ New webinterface for Retroshare
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ loading lots of stuff...
+
+
+
+
+
diff --git a/libresapi/src/webui/package.json b/libresapi/src/webui/package.json
new file mode 100644
index 000000000..f409c1122
--- /dev/null
+++ b/libresapi/src/webui/package.json
@@ -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/"
+ }
+}