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 + + + {this.state.data.map(function(peer){ return ; })} +
    avatar name locations
    +
    ); + }, +}); + +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 ( + + + + + + + + + {this.state.data.map(function(dl){ return
    ; })} +
    namesizecompleteddownload statusactions
    ); + }, +}); + +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 ( + + + {this.state.data.map( + function(file){ + return ; + } + )} +
    namesize
    ); + } + }); + + var results =
    ; + if(this.state.search_id !== undefined) + { + results = ; + } + return( +
    +

    turtle file search

    +
    + + +
    + {results} +
    ); + }, +}); + +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( +
    +

    Enter password for key {this.state.data.key_name}

    + + +
    + ); + } + }, +}); + +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/" + } +}