snapdrop

A Progressive Web App for local file sharing
git clone http://git.hanabi.in/repos/snapdrop.git
Log | Files | Refs | README | LICENSE

commit bda1a15750e4a3bdf4124ababf5dd06a5a8d0640
parent f1ad168e402a9a36991c65c2009f0e9ac77a7b4c
Author: Robin Linus <robin_woll@capira.de>
Date:   Sat, 26 Dec 2015 13:33:16 +0100

first working version

Diffstat:
MREADME.md | 42+++++++++++++++++++++---------------------
Mapp/elements/buddy-finder/buddy-finder.html | 29++++-------------------------
Mapp/elements/buddy-finder/personal-avatar.html | 4++--
Mapp/elements/buddy-finder/user-avatar.html | 5++---
Mapp/elements/elements.html | 6+++---
Mapp/elements/file-sharing/file-receiver.html | 9+++++----
Dapp/elements/file-sharing/file-saver.html | 5-----
Mapp/elements/file-sharing/file-selection-behavior.html | 7+++++--
Mapp/elements/p2p-network/connection-wrapper.html | 28+++++++++++++---------------
Mapp/elements/p2p-network/file-transfer-protocol.html | 3+++
Mapp/elements/p2p-network/p2p-network.html | 35++++++++++++++++++++---------------
Mapp/elements/p2p-network/web-socket.html | 23++++++++++++++---------
Aapp/elements/x-cards/x-card.html | 138+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/elements/x-cards/x-cards.html | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mapp/index.html | 46+++++++++++++++++++++++++++++++++++++---------
Mapp/manifest.json | 4++--
Mapp/scripts/app.js | 8++++++--
Mapp/styles/app-theme.html | 3+++
Mapp/styles/icons.html | 6++++++
Adomains.txt | 27+++++++++++++++++++++++++++
Mgulpfile.js | 10+++++++++-
Aindex.js | 22++++++++++++++++++++++
Mpackage.json | 10+++++++---
Mserver/ws-server.js | 195+++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
24 files changed, 542 insertions(+), 206 deletions(-)

diff --git a/README.md b/README.md @@ -1,5 +1,5 @@ ![](https://cloud.githubusercontent.com/assets/110953/7877439/6a69d03e-0590-11e5-9fac-c614246606de.png) -## Share With Me +## Snapdrop > A starting point for building web applications with Polymer 1.0 @@ -14,11 +14,11 @@ * [Recipes](/docs/README.md/) for ES2015 support, Polymer performance, using Chrome Dev Editor, Deploying to GitHub Pages, Deploying to Firebase, and Mobile Chrome Apps ### Demo -See latest Share With Me Demo (from master) at http://polymerelements.github.io/polymer-starter-kit +See latest Snapdrop Demo (from master) at http://polymerelements.github.io/polymer-starter-kit ### Tutorials -Check out the Share With Me tutorials on [polymer-project.org](https://polymer-project.org): +Check out the Snapdrop tutorials on [polymer-project.org](https://polymer-project.org): * [Set up the PSK](https://www.polymer-project.org/1.0/docs/start/psk/set-up.html) * [Create a page](https://www.polymer-project.org/1.0/docs/start/psk/create-a-page.html) @@ -26,7 +26,7 @@ Check out the Share With Me tutorials on [polymer-project.org](https://polymer-p ## Getting Started -To take advantage of Share With Me you need to: +To take advantage of Snapdrop you need to: 1. Get a copy of the code. 2. Install the dependencies if you don't already have them. @@ -35,21 +35,21 @@ To take advantage of Share With Me you need to: ### Get the code -[Download](https://github.com/polymerelements/polymer-starter-kit/releases/latest) and extract Share With Me to where you want to work. The project comes in two flavours - Light and Full. +[Download](https://github.com/polymerelements/polymer-starter-kit/releases/latest) and extract Snapdrop to where you want to work. The project comes in two flavours - Light and Full. -**Beginners**: Try Share With Me Light. This doesn't require any extra dependencies nor knowledge of modern front-end tooling. This option is good for prototyping if you haven't build a Polymer app before. +**Beginners**: Try Snapdrop Light. This doesn't require any extra dependencies nor knowledge of modern front-end tooling. This option is good for prototyping if you haven't build a Polymer app before. -**Intermediate - Advanced**: Use the full version of Share With Me. This comes with all the build tools you'll need for testing and productionising your app so it's nice and lean. You'll need to run a few extra commands to install the tools we recommend but it's worth it to make sure your final app is super optimised. +**Intermediate - Advanced**: Use the full version of Snapdrop. This comes with all the build tools you'll need for testing and productionising your app so it's nice and lean. You'll need to run a few extra commands to install the tools we recommend but it's worth it to make sure your final app is super optimised. -:warning: **Important**: Share With Me, and Share With Me Light, both contain dotfiles (files starting with a `.`). If you're copying the contents of the Starter Kit to a new location make sure you bring along these dotfiles as well! On Mac, [enable showing hidden files](http://ianlunn.co.uk/articles/quickly-showhide-hidden-files-mac-os-x-mavericks/), then try extracting/copying Share With Me again. This time the dotfiles needed should be visible so you can copy them over without issues. +:warning: **Important**: Snapdrop, and Snapdrop Light, both contain dotfiles (files starting with a `.`). If you're copying the contents of the Starter Kit to a new location make sure you bring along these dotfiles as well! On Mac, [enable showing hidden files](http://ianlunn.co.uk/articles/quickly-showhide-hidden-files-mac-os-x-mavericks/), then try extracting/copying Snapdrop again. This time the dotfiles needed should be visible so you can copy them over without issues. -Rob Dodson has a fantastic [PolyCast video](https://www.youtube.com/watch?v=xz-yixRxZN8) available that walks through using Share With Me. An [end-to-end with Polymer](https://www.youtube.com/watch?v=1f_Tj_JnStA) and Share With Me talk is also available. +Rob Dodson has a fantastic [PolyCast video](https://www.youtube.com/watch?v=xz-yixRxZN8) available that walks through using Snapdrop. An [end-to-end with Polymer](https://www.youtube.com/watch?v=1f_Tj_JnStA) and Snapdrop talk is also available. ### Install dependencies #### Quick-start (for experienced users) -With Node.js installed, run the following one liner from the root of your Share With Me download: +With Node.js installed, run the following one liner from the root of your Snapdrop download: ```sh npm install -g gulp bower && npm install && bower install @@ -154,7 +154,7 @@ These style files are located in the [styles folder](app/styles/). ## Unit Testing -Web apps built with Share With Me come configured with support for [Web Component Tester](https://github.com/Polymer/web-component-tester) - Polymer's preferred tool for authoring and running unit tests. This makes testing your element based applications a pleasant experience. +Web apps built with Snapdrop come configured with support for [Web Component Tester](https://github.com/Polymer/web-component-tester) - Polymer's preferred tool for authoring and running unit tests. This makes testing your element based applications a pleasant experience. [Read more](https://github.com/Polymer/web-component-tester#html-suites) about using Web Component tester. @@ -181,13 +181,13 @@ Components installed by Bower live in the `app/bower_components` directory. This ## Service Worker -Share With Me offers an optional offline experience thanks to Service Worker and the [Platinum Service Worker elements](https://github.com/PolymerElements/platinum-sw). New to Service Worker? Read the following [introduction](http://www.html5rocks.com/en/tutorials/service-worker/introduction/) to understand how it works. +Snapdrop offers an optional offline experience thanks to Service Worker and the [Platinum Service Worker elements](https://github.com/PolymerElements/platinum-sw). New to Service Worker? Read the following [introduction](http://www.html5rocks.com/en/tutorials/service-worker/introduction/) to understand how it works. Our optional offline setup should work well for relatively simple applications. For more complex apps, we recommend learning how Service Worker works so that you can make the most of the Platinum Service Worker element abstractions. ### Enable Service Worker support? -To enable Service Worker support for Share With Me project use these 3 steps: +To enable Service Worker support for Snapdrop project use these 3 steps: 1. Uncomment Service Worker code in index.html ```HTML @@ -272,16 +272,16 @@ If you find anything to still be stale, you can also try navigating to `chrome:s #### Disable Service Worker support after you enabled it -If for any reason you need to disable Service Worker support after previously enabling it, you can remove it from your Share With Me project using these 4 steps: +If for any reason you need to disable Service Worker support after previously enabling it, you can remove it from your Snapdrop project using these 4 steps: 1. Remove references to the platinum-sw elements from your application [index](https://github.com/PolymerElements/polymer-starter-kit/blob/master/app/index.html). 2. Remove the two Platinum Service Worker elements (platinum-sw/..) in [app/elements/elements.html](https://github.com/PolymerElements/polymer-starter-kit/blob/master/app/elements/elements.html) 3. Remove 'precache' from the list in the 'default' gulp task ([gulpfile.js](https://github.com/PolymerElements/polymer-starter-kit/blob/master/gulpfile.js)) -4. Navigate to `chrome://serviceworker-internals` and unregister any Service Workers registered by Share With Me for your app just in case there's a copy of it cached. +4. Navigate to `chrome://serviceworker-internals` and unregister any Service Workers registered by Snapdrop for your app just in case there's a copy of it cached. ## Yeoman support -[generator-polymer](https://github.com/yeoman/generator-polymer/releases) now includes support for Share With Me out of the box. +[generator-polymer](https://github.com/yeoman/generator-polymer/releases) now includes support for Snapdrop out of the box. ## Frequently Asked Questions @@ -308,7 +308,7 @@ own local setup. ### Where can I find the application layouts from your Google I/O 2015 talk? App layouts live in a separate repository called [app-layout-templates](https://github.com/PolymerElements/app-layout-templates). -You can select a template and copy over the relevant parts you would like to reuse to Share With Me. +You can select a template and copy over the relevant parts you would like to reuse to Snapdrop. You will probably need to change paths to where your Iron and Paper dependencies can be found to get everything working. This can be done by adding them to the [`elements.html`](https://github.com/PolymerElements/polymer-starter-kit/blob/master/app/elements/elements.html) import. @@ -366,14 +366,14 @@ If you are not using the build-blocks, but still wish for additional files (e.g ### I'm finding the installation/tooling here overwhelming. What should I do? -Don't worry! We've got your covered. Share With Me tries to offer everything you need to build and optimize your apps for production, which is why we include the tooling we do. We realise however that our tooling setup may not be for everyone. +Don't worry! We've got your covered. Snapdrop tries to offer everything you need to build and optimize your apps for production, which is why we include the tooling we do. We realise however that our tooling setup may not be for everyone. -If you find that you just want the simplest setup possible, we recommend using Share With Me light, which is available from the [Releases](https://github.com/PolymerElements/polymer-starter-kit/releases) page. This takes next to no time to setup. +If you find that you just want the simplest setup possible, we recommend using Snapdrop light, which is available from the [Releases](https://github.com/PolymerElements/polymer-starter-kit/releases) page. This takes next to no time to setup. ## Licensing -Like other Google projects, Share With Me includes Google license headers at the top of several of our source files. Google's open-source licensing requires that this header be kept in place (sorry!), however we acknowledge that you may need to add your own licensing to files you modify. This can be done by appending your own extensions to these headers. +Like other Google projects, Snapdrop includes Google license headers at the top of several of our source files. Google's open-source licensing requires that this header be kept in place (sorry!), however we acknowledge that you may need to add your own licensing to files you modify. This can be done by appending your own extensions to these headers. ## Contributing -Share With Me is a new project and is an ongoing effort by the Web Component community. We welcome your bug reports, PRs for improvements, docs and anything you think would improve the experience for other Polymer developers. +Snapdrop is a new project and is an ongoing effort by the Web Component community. We welcome your bug reports, PRs for improvements, docs and anything you think would improve the experience for other Polymer developers. diff --git a/app/elements/buddy-finder/buddy-finder.html b/app/elements/buddy-finder/buddy-finder.html @@ -52,12 +52,10 @@ </file-input> </template> </div> - <div hidden$="{{buddies.length}}" class="explanation"> - Open this page on another device - <wbr>to share files. + <div hidden$="{{buddies.0}}" class="explanation"> + Open this page on other devices<br> to send files. </div> <personal-avatar class="me"></personal-avatar> - <!-- <iron-ajax id="ajax" auto url="https://yawim.com/findbuddies/{{me}}" handle-as="json" last-response="{{buddies}}"></iron-ajax> --> </template> <script> 'use strict'; @@ -66,30 +64,11 @@ properties: { buddies: { type: Array, - value: [] + notify: true }, me: { type: String, - } - }, - attached: function() { - //Ask server every second for changes - var ajax = this.$.ajax; - - function request() { - //ajax.generateRequest(); - } - var intervalId = setInterval(request, 1000); - document.addEventListener('visibilitychange', function() { - if (document.hidden) { - clearInterval(intervalId); - intervalId = 0; - } else { - if (!intervalId) { - intervalId = setInterval(request, 1000); - } - } - }); + }, }, _fileSelected: function(e) { var peerId = e.model.item.peerId; diff --git a/app/elements/buddy-finder/personal-avatar.html b/app/elements/buddy-finder/personal-avatar.html @@ -13,11 +13,11 @@ width: 80px; height: 80px; color: #4285f4; + margin-bottom: 6px; } .paper-font-body1 { font-size: 13px; - margin-top: 6px; } .discover { @@ -26,7 +26,7 @@ </style> <iron-icon icon="chat:wifi-tethering"></iron-icon> <div class="paper-font-body1"> - SnapDrop lets you share instantly with people near by. + Snapdrop lets you share instantly with people near by. </div> <div class="paper-font-body1 discover"> Allow me to be discovered by: Everyone in this network. diff --git a/app/elements/buddy-finder/user-avatar.html b/app/elements/buddy-finder/user-avatar.html @@ -7,7 +7,7 @@ @apply(--layout-vertical); @apply(--layout-center); width: 120px; - height: 120px; + height: 152px; } paper-icon-button { @@ -45,7 +45,7 @@ -moz-user-select: none; -ms-user-select: none; user-select: none; - margin: 4px; + margin-top: 4px; } </style> <paper-icon-button icon="{{_displayIcon}}"></paper-icon-button> @@ -85,7 +85,6 @@ if (contact.type === 'tablet') { return 'chat:tablet-mac'; } - return 'chat:desktop-mac'; }, attached: function() { diff --git a/app/elements/elements.html b/app/elements/elements.html @@ -2,14 +2,14 @@ <link rel="import" href="../bower_components/platinum-sw/platinum-sw-register.html"> <link rel="import" href="../bower_components/paper-toast/paper-toast.html"> <link rel="import" href="../bower_components/paper-progress/paper-progress.html"> - - +<link rel="import" href="../bower_components/neon-animation/neon-animated-pages.html"> <!-- Configure your routes here <link rel="import" href="routing.html"> --> <!-- Add your elements here --> <link rel="import" href="../styles/app-theme.html"> +<link rel="import" href="x-cards/x-card.html"> +<link rel="import" href="x-cards/x-cards.html"> <link rel="import" href="buddy-finder/buddy-finder.html"> <link rel="import" href="p2p-network/connection-wrapper.html"> <link rel="import" href="file-sharing/file-receiver.html"> - diff --git a/app/elements/file-sharing/file-receiver.html b/app/elements/file-sharing/file-receiver.html @@ -17,13 +17,14 @@ z-index: 101; } - b { + .filename { + word-break: break-all; word-break: break-word; } </style> <paper-dialog id="dialog" entry-animation="scale-up-animation" exit-animation="fade-out-animation" with-backdrop modal> <h2>Download File</h2> - <p><b>{{file.name}}</b></p> + <p><b class="filename">{{file.name}}</b></p> <div class="buttons"> <paper-button dialog-dismiss on-tap="_decline">Discard</paper-button> <paper-button dialog-confirm on-tap="_accept" autofocus>Download</paper-button> @@ -31,11 +32,11 @@ </paper-dialog> <paper-dialog id="download" entry-animation="scale-up-animation" exit-animation="fade-out-animation" with-backdrop modal> <h2>File Received</h2> - <p>Right Click and "Save as"...</p> + <p>Open File or Right Click and "Save as"...</p> <div class="buttons"> <paper-button dialog-dismiss>Discard</paper-button> <a href="{{dataUri}}" target="_blank"> - <paper-button dialog-confirm autofocus>Download</paper-button> + <paper-button dialog-confirm autofocus>Open File</paper-button> </a> </div> </paper-dialog> diff --git a/app/elements/file-sharing/file-saver.html b/app/elements/file-sharing/file-saver.html @@ -1,4 +0,0 @@ -<script> - var saveAs=saveAs||function(view){"use strict";if(typeof navigator!=="undefined"&&/MSIE [1-9]\./.test(navigator.userAgent)){return}var doc=view.document,get_URL=function(){return view.URL||view.webkitURL||view},save_link=doc.createElementNS("http://www.w3.org/1999/xhtml","a"),can_use_save_link="download"in save_link,click=function(node){var event=new MouseEvent("click");node.dispatchEvent(event)},is_safari=/Version\/[\d\.]+.*Safari/.test(navigator.userAgent),webkit_req_fs=view.webkitRequestFileSystem,req_fs=view.requestFileSystem||webkit_req_fs||view.mozRequestFileSystem,throw_outside=function(ex){(view.setImmediate||view.setTimeout)(function(){throw ex},0)},force_saveable_type="application/octet-stream",fs_min_size=0,arbitrary_revoke_timeout=500,revoke=function(file){var revoker=function(){if(typeof file==="string"){get_URL().revokeObjectURL(file)}else{file.remove()}};if(view.chrome){revoker()}else{setTimeout(revoker,arbitrary_revoke_timeout)}},dispatch=function(filesaver,event_types,event){event_types=[].concat(event_types);var i=event_types.length;while(i--){var listener=filesaver["on"+event_types[i]];if(typeof listener==="function"){try{listener.call(filesaver,event||filesaver)}catch(ex){throw_outside(ex)}}}},auto_bom=function(blob){if(/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)){return new Blob(["\ufeff",blob],{type:blob.type})}return blob},FileSaver=function(blob,name,no_auto_bom){if(!no_auto_bom){blob=auto_bom(blob)}var filesaver=this,type=blob.type,blob_changed=false,object_url,target_view,dispatch_all=function(){dispatch(filesaver,"writestart progress write writeend".split(" "))},fs_error=function(){if(target_view&&is_safari&&typeof FileReader!=="undefined"){var reader=new FileReader;reader.onloadend=function(){var base64Data=reader.result;target_view.location.href="data:attachment/file"+base64Data.slice(base64Data.search(/[,;]/));filesaver.readyState=filesaver.DONE;dispatch_all()};reader.readAsDataURL(blob);filesaver.readyState=filesaver.INIT;return}if(blob_changed||!object_url){object_url=get_URL().createObjectURL(blob)}if(target_view){target_view.location.href=object_url}else{var new_tab=view.open(object_url,"_blank");if(new_tab==undefined&&is_safari){view.location.href=object_url}}filesaver.readyState=filesaver.DONE;dispatch_all();revoke(object_url)},abortable=function(func){return function(){if(filesaver.readyState!==filesaver.DONE){return func.apply(this,arguments)}}},create_if_not_found={create:true,exclusive:false},slice;filesaver.readyState=filesaver.INIT;if(!name){name="download"}if(can_use_save_link){object_url=get_URL().createObjectURL(blob);setTimeout(function(){save_link.href=object_url;save_link.download=name;click(save_link);dispatch_all();revoke(object_url);filesaver.readyState=filesaver.DONE});return}if(view.chrome&&type&&type!==force_saveable_type){slice=blob.slice||blob.webkitSlice;blob=slice.call(blob,0,blob.size,force_saveable_type);blob_changed=true}if(webkit_req_fs&&name!=="download"){name+=".download"}if(type===force_saveable_type||webkit_req_fs){target_view=view}if(!req_fs){fs_error();return}fs_min_size+=blob.size;req_fs(view.TEMPORARY,fs_min_size,abortable(function(fs){fs.root.getDirectory("saved",create_if_not_found,abortable(function(dir){var save=function(){dir.getFile(name,create_if_not_found,abortable(function(file){file.createWriter(abortable(function(writer){writer.onwriteend=function(event){target_view.location.href=file.toURL();filesaver.readyState=filesaver.DONE;dispatch(filesaver,"writeend",event);revoke(file)};writer.onerror=function(){var error=writer.error;if(error.code!==error.ABORT_ERR){fs_error()}};"writestart progress write abort".split(" ").forEach(function(event){writer["on"+event]=filesaver["on"+event]});writer.write(blob);filesaver.abort=function(){writer.abort();filesaver.readyState=filesaver.DONE};filesaver.readyState=filesaver.WRITING}),fs_error)}),fs_error)};dir.getFile(name,{create:false},abortable(function(file){file.remove();save()}),abortable(function(ex){if(ex.code===ex.NOT_FOUND_ERR){save()}else{fs_error()}}))}),fs_error)}),fs_error)},FS_proto=FileSaver.prototype,saveAs=function(blob,name,no_auto_bom){return new FileSaver(blob,name,no_auto_bom)};if(typeof navigator!=="undefined"&&navigator.msSaveOrOpenBlob){return function(blob,name,no_auto_bom){if(!no_auto_bom){blob=auto_bom(blob)}return navigator.msSaveOrOpenBlob(blob,name||"download")}}FS_proto.abort=function(){var filesaver=this;filesaver.readyState=filesaver.DONE;dispatch(filesaver,"abort")};FS_proto.readyState=FS_proto.INIT=0;FS_proto.WRITING=1;FS_proto.DONE=2;FS_proto.error=FS_proto.onwritestart=FS_proto.onprogress=FS_proto.onwrite=FS_proto.onabort=FS_proto.onerror=FS_proto.onwriteend=null;return saveAs}(typeof self!=="undefined"&&self||typeof window!=="undefined"&&window||this.content);if(typeof module!=="undefined"&&module.exports){module.exports.saveAs=saveAs}else if(typeof define!=="undefined"&&define!==null&&define.amd!=null){define([],function(){return saveAs})} - -</script> -\ No newline at end of file diff --git a/app/elements/file-sharing/file-selection-behavior.html b/app/elements/file-sharing/file-selection-behavior.html @@ -7,8 +7,11 @@ Chat.FileSelectionBehavior = { console.log('no files selected...'); return; } - for (var i = 0; i < files.length; i++) { - var file = files[i]; + this._fileSelected(files[0]); //single select + //files.forEach(this._fileSelected.bind(this)); //multi-select + }, + _fileSelected: function(file) { + if (file) { this.fire('file-selected', { file: file, name: file.name diff --git a/app/elements/p2p-network/connection-wrapper.html b/app/elements/p2p-network/connection-wrapper.html @@ -8,27 +8,17 @@ <script> 'use strict'; (function() { - function guid() { - function s4() { - return Math.floor((1 + Math.random()) * 0x10000) - .toString(16) - .substring(1); - } - return s4() + s4() + '-' + s4() + '-' + s4() + '-' + - s4() + '-' + s4() + s4() + s4(); - } - var webRTCSupported = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection || window.webkitRTCPeerConnection; + window.webRTCSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection || window.webkitRTCPeerConnection); function rtcConnectionSupported(peerId) { - return webRTCSupported && (peerId.indexOf('rtc_') === 0); + return window.webRTCSupported && (peerId.indexOf('rtc_') === 0); } Polymer({ is: 'connection-wrapper', properties: { me: { - notify: true, - value: (webRTCSupported ? 'rtc_' : 'ws_') + guid() - } + notify: true + }, }, behaviors: [Chat.FileTransferProtocol], _sendFile: function(toPeer, file) { @@ -50,9 +40,17 @@ if (!rtcConnectionSupported(toPeer)) { callback(); } else { - this.$.p2p.connectToPeer(toPeer,callback); + this.$.p2p.connectToPeer(toPeer, callback); } }, + _onHandshake: function(event) { + var me = event.uuid; + console.log('i am'); + this.set('me', me); + if (window.webRTCSupported) { + this.$.p2p.initialize(); + } + } }); })(); </script> diff --git a/app/elements/p2p-network/file-transfer-protocol.html b/app/elements/p2p-network/file-transfer-protocol.html @@ -22,6 +22,9 @@ Chat.FileTransferProtocol = { console.log('FTP received sysMsg:', msg); switch (msg.type) { + case 'handshake': + this._onHandshake(msg); + break; case 'offer': this._onOffered(msg); break; diff --git a/app/elements/p2p-network/p2p-network.html b/app/elements/p2p-network/p2p-network.html @@ -9,8 +9,7 @@ is: 'p2p-network', properties: { me: { - type: String, - notify: true, + type: String } }, attached: function() { @@ -22,16 +21,24 @@ this._peer.destroy(); } }.bind(this); - this._initialize(); }, - _initialize: function() { - var options = { - host: 'yawim.com', - port: 443, - path: 'peerjs', - secure: true - }; - this._peer = new Peer(this.me,options); + initialize: function() { + var options; + if (window.debug) { + options = { + host: window.location.hostname, + port: 3002, + path: 'peerjs' + }; + } else { + options = { + host: 'snapdrop.net', + port: 443, + path: 'peerjs', + secure: true + }; + } + this._peer = new Peer(this.me, options); this._peer.on('open', function(id) { console.log('My peer ID is: ' + id); this.set('me', id); @@ -53,12 +60,10 @@ if (err.message.indexOf('Lost connection to server') > -1) { this._peer.destroy(); this.set('me', this.me); - this._initialize(); + this.async(this._initialize, 3000); return; } }.bind(this)); - - }, connect: function(c) { @@ -140,7 +145,7 @@ conns.forEach(function(conn) { if (conn.label === 'file') { conn.send(file); - console.log('file send'); + console.log('file send via WebRTC'); } }.bind(this)); } diff --git a/app/elements/p2p-network/web-socket.html b/app/elements/p2p-network/web-socket.html @@ -15,13 +15,13 @@ this.init(); }, init: function() { - var websocketUrl = (window.location.protocol === 'https:' ? 'wss://' : 'ws://') + document.location.hostname + ':9001'; + var websocketUrl = (window.debug ? 'ws://' + window.location.hostname + ':3002' : 'wss://snapdrop.net') + '/binary'; this.client = new BinaryClient(websocketUrl); this.client.on('stream', function(stream, meta) { // collect stream data var parts = []; stream.on('data', function(data) { - console.log('part received', meta, data); + //console.log('part received', meta, data); if (data.isSystemEvent) { if (meta) { data.from = meta.from; @@ -45,22 +45,22 @@ }.bind(this)); }.bind(this)); this.client.on('open', function(e) { + this.cancelAsync(this.reconnectTimer); console.log(e); this.client.send({}, { - handshake: this.me + serverMsg: 'rtc-support', + rtc: window.webRTCSupported }); }.bind(this)); this.client.on('error', function(e) { - console.log(e); - }); + this._reconnect(e); + }.bind(this)); this.client.on('close', function(e) { - console.log(e); - //try to reconnect after 3s - this.async(this.init, 3000); + this._reconnect(e); }.bind(this)); }, _sendFile: function(toPeer, file) { - console.log('send file!', file); + console.log('send file via WebSocket', file); this.client.send(file.file, { name: file.file.name, type: file.file.type, @@ -76,6 +76,11 @@ this.client.send(event, { toPeer: toPeer }); + }, + _reconnect: function(e) { + console.log('disconnected', e); + //try to reconnect after 3s + this.reconnectTimer = this.async(this.init, 3000); } }); </script> diff --git a/app/elements/x-cards/x-card.html b/app/elements/x-cards/x-card.html @@ -0,0 +1,138 @@ +<link rel="import" href="../../bower_components/iron-flex-layout/iron-flex-layout.html"> +<link rel="import" href="../../bower_components/neon-animation/neon-shared-element-animatable-behavior.html"> +<link rel="import" href="../../bower_components/neon-animation/neon-animations.html"> +<link rel="import" href="../../bower_components/paper-styles/paper-styles-classes.html"> +<link rel="import" href="../../bower_components/iron-icon/iron-icon.html"> +<dom-module id="x-card"> + <template> + <style> + :host { + display: block; + overflow: hidden; + color: white; + z-index: 3 + } + + #placeholder { + opacity: 0; + background-color: #4285f4; + @apply(--layout-fit); + } + + paper-icon-button { + position: absolute; + top: 16px; + right: 16px; + z-index: 2; + } + + #container { + @apply(--layout-fit); + @apply(--layout-vertical); + @apply(--layout-center-center); + background-color: #4285f4; + padding: 64px 32px 64px 32px; + box-sizing: border-box; + } + + iron-icon { + width: 80px; + height: 80px; + } + + .paper-font-subhead { + text-align: center; + } + + a { + text-decoration: none; + color: white; + @apply(--layout-self-end); + } + + .center { + @apply(--layout-vertical); + @apply(--layout-center-center); + } + + #footer { + position: absolute; + left: 50%; + margin-left: -160px; + width: 320px; + bottom: 24px; + text-align: center; + } + </style> + <paper-icon-button id="btn" icon="chat:close" on-tap="_switch"></paper-icon-button> + <div id="placeholder"></div> + <div id="container"> + <div class="center"> + <iron-icon icon="chat:wifi-tethering"></iron-icon> + <div class="paper-font-headline">Snapdrop</div> + <div class="paper-font-subhead">The easiest way to send files across devices.</div> + </div> + <span id="footer">Built with &#9829; by <a href="mailto:robin@capira.de">Robin Linus</a></span> + </div> + </template> +</dom-module> +<script> +(function() { + Polymer({ + is: 'x-card', + behaviors: [ + Polymer.NeonSharedElementAnimatableBehavior + ], + properties: { + animationConfig: { + value: function() { + return { + 'entry': [{ + name: 'ripple-animation', + id: 'ripple', + toPage: this + }, { + name: 'fade-out-animation', + node: this.$.placeholder, + timing: { + delay: 250 + } + }, { + name: 'fade-in-animation', + node: this.$.container, + timing: { + delay: 50 + } + }], + 'exit': [{ + name: 'opaque-animation', + node: this.$.placeholder + }, { + name: 'fade-out-animation', + node: this.$.container, + timing: { + duration: 0 + } + }, { + name: 'reverse-ripple-animation', + id: 'reverse-ripple', + fromPage: this + }] + }; + } + }, + sharedElements: { + value: function() { + return { + 'ripple': this.$.placeholder, + 'reverse-ripple': this.$.placeholder + }; + } + } + }, + _switch: function() { + document.querySelector('#pages').select(0); + } + }); +})(); +</script> diff --git a/app/elements/x-cards/x-cards.html b/app/elements/x-cards/x-cards.html @@ -0,0 +1,83 @@ +<link rel="import" href="../../bower_components/iron-flex-layout/iron-flex-layout.html"> +<link rel="import" href="../../bower_components/neon-animation/neon-shared-element-animatable-behavior.html"> +<link rel="import" href="../../bower_components/neon-animation/neon-animations.html"> +<link rel="import" href="../../bower_components/paper-icon-button/paper-icon-button.html"> +<dom-module id="x-cards"> + <template> + <style> + :host { + display: block; + overflow: hidden; + } + + #placeholder { + opacity: 0; + background-color: grey; + @apply(--layout-fit); + } + + paper-icon-button { + position: absolute; + top: 16px; + right: 16px; + z-index: 2; + color: #313131; + } + + paper-icon-button:hover { + color: #4285f4; + } + </style> + <div id="placeholder"></div> + <div id="container"> + <paper-icon-button id="btn" icon="chat:info-outline" on-tap="_switch"></paper-icon-button> + <content select="div"></content> + </div> + </template> +</dom-module> +<script> +(function() { + Polymer({ + is: 'x-cards', + behaviors: [ + Polymer.NeonSharedElementAnimatableBehavior + ], + properties: { + animationConfig: { + value: function() { + return { + 'entry': [{ + name: 'reverse-ripple-animation', + id: 'reverse-ripple', + toPage: this + }], + 'exit': [{ + name: 'fade-out-animation', + node: this.$.container, + timing: { + delay: 150, + duration: 0 + } + }, { + name: 'ripple-animation', + id: 'ripple', + fromPage: this + }] + }; + } + }, + sharedElements: { + value: function() { + return { + 'ripple': this.$.btn, + 'reverse-ripple': this.$.btn + }; + } + } + }, + _switch: function() { + document.querySelector('#pages').select(1); + } + }); +})(); +</script> diff --git a/app/index.html b/app/index.html @@ -3,10 +3,11 @@ <head> <meta charset="utf-8"> - <meta name="description" content=""> + <meta name="description" content="Snapdrop lets you instantly share files with people near by. It is a web-based clone of Apple's Airdrop."> <meta name="viewport" content="initial-scale=1,user-scalable=no,maximum-scale=1"> - <meta name="generator" content="SnapDrop!"> - <title>SnapDrop!</title> + <meta name="generator" content="Snapdrop"> + <title>Snapdrop</title> + <link rel="shortcut icon" href="favicon.ico?v=2" /> <!-- Place favicon.ico in the `app/` directory --> <!-- Chrome for Android theme color --> <meta name="theme-color" content="#3367d6"> @@ -18,10 +19,12 @@ <meta name="mobile-web-app-capable" content="yes"> <meta name="application-name" content="PSK"> <link rel="icon" sizes="192x192" href="images/touch/chrome-touch-icon-192x192.png"> + <link rel="fluid-icon" type="image/png" href="images/touch/chrome-touch-icon-192x192.png"> + <meta property="og:image" content="https://snapdrop.net/images/touch/chrome-touch-icon-192x192.png" /> <!-- Add to homescreen for Safari on iOS --> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="black"> - <meta name="apple-mobile-web-app-title" content="SnapDrop!"> + <meta name="apple-mobile-web-app-title" content="Snapdrop"> <link rel="apple-touch-icon" href="images/touch/apple-touch-icon.png"> <!-- Tile icon for Win8 (144x144) --> <meta name="msapplication-TileImage" content="images/touch/ms-touch-icon-144x144-precomposed.png"> @@ -29,25 +32,34 @@ <link rel="stylesheet" href="styles/main.css"> <!-- endbuild--> <!-- build:js bower_components/webcomponentsjs/webcomponents-lite.min.js --> - <script src="bower_components/webcomponentsjs/webcomponents-lite.js" async></script> + <script src="bower_components/webcomponentsjs/webcomponents-lite.js" async="1"></script> <!-- endbuild --> <!-- Because this project uses vulcanize this should be your only html import in this file. All other imports should go in elements.html --> <link rel="import" href="elements/elements.html" async> - <meta name="description" content="SnapDrop lets you instantly share files with people near by. It is a web-based clone of Apple's Airdrop."> </head> <body class="fullbleed layout vertical" loading> <script src="scripts/animated-bg.js" inline></script> + <script> + window.debug = true; + </script> <span id="browser-sync-binding"></span> <template is="dom-bind" id="app"> - <paper-progress indeterminate hidden$="{{!loading}}"></paper-progress> - <buddy-finder me="{{me}}" active$="{{loading}}" buddies="{{buddies}}"></buddy-finder> <connection-wrapper me="{{me}}" loading="{{loading}}" buddies="{{buddies}}"></connection-wrapper> + <neon-animated-pages id="pages" selected="0"> + <x-cards on-switch="_showAbout"> + <div> + <paper-progress indeterminate hidden$="{{!loading}}"></paper-progress> + <buddy-finder me="{{me}}" active$="{{loading}}" buddies="{{buddies}}"></buddy-finder> + </div> + </x-cards> + <x-card on-switch="_showApp"> + </x-card> + </neon-animated-pages> <file-receiver></file-receiver> <paper-toast id="toast" duration="6000"> </paper-toast> - <!-- Uncomment next block to enable Service Worker support (1/2) --> <paper-toast id="caching-complete" duration="6000" text="Caching complete! This app will work offline."> </paper-toast> <platinum-sw-register auto-register clients-claim skip-waiting base-uri="bower_components/platinum-sw/bootstrap" on-service-worker-installed="displayInstalledToast"> @@ -58,6 +70,22 @@ <!-- build:js scripts/app.js --> <script src="scripts/app.js"></script> <!-- endbuild--> + <script> + (function(i, s, o, g, r, a, m) { + i['GoogleAnalyticsObject'] = r; + i[r] = i[r] || function() { + (i[r].q = i[r].q || []).push(arguments) + }, i[r].l = 1 * new Date(); + a = s.createElement(o), + m = s.getElementsByTagName(o)[0]; + a.async = 1; + a.src = g; + m.parentNode.insertBefore(a, m) + })(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga'); + + ga('create', 'UA-71686975-1', 'auto'); + ga('send', 'pageview'); + </script> </body> </html> diff --git a/app/manifest.json b/app/manifest.json @@ -1,6 +1,6 @@ { - "name": "SnapDrop", - "short_name": "SnapDrop", + "name": "Snapdrop", + "short_name": "Snapdrop", "icons": [{ "src": "images/touch/icon-128x128.png", "sizes": "128x128", diff --git a/app/scripts/app.js b/app/scripts/app.js @@ -40,6 +40,10 @@ }); - - + app._showAbout=function(){ + document.querySelector('#pages').select(1); + }; + app._showAbout=function(){ + document.querySelector('#pages').select(0); + }; })(document); diff --git a/app/styles/app-theme.html b/app/styles/app-theme.html @@ -28,4 +28,7 @@ paper-progress { position: absolute; top: 0; } +neon-animated-pages{ + height: 100%; +} </style> diff --git a/app/styles/icons.html b/app/styles/icons.html @@ -32,6 +32,12 @@ <g id="tablet-mac"> <path d="M18.5 0h-14C3.12 0 2 1.12 2 2.5v19C2 22.88 3.12 24 4.5 24h14c1.38 0 2.5-1.12 2.5-2.5v-19C21 1.12 19.88 0 18.5 0zm-7 23c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm7.5-4H4V3h15v16z" /> </g> + <g id="info-outline"> + <path d="M11 17h2v-6h-2v6zm1-15C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zM11 9h2V7h-2v2z" /> + </g> + <g id="close"> + <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" /> + </g> </defs> </svg> </iron-iconset-svg> diff --git a/domains.txt b/domains.txt @@ -0,0 +1,27 @@ +syncas.com +nearbyc.com +websynca.com +websyncr.com +syncronr.com +syncronify.com +syncats.com +websyncr.com +geofilebox.com +geomiao.com +geofilenet.com geofile.net +geoshareit.com +geobitbox.com +geodropr.com +localsyncr.com +localfiledrop.com +geobitbin.com +localbitbox.com +geodropx.com +geobytedrop.com +geochatbox.com +geoblinc.com +geosyncit.com +geowebdrop.com +localmeow.com +mybitbox.com +geodropme.com diff --git a/gulpfile.js b/gulpfile.js @@ -16,6 +16,9 @@ var packageJson = require('./package.json'); var crypto = require('crypto'); var ensureFiles = require('./tasks/ensure-files.js'); var inlinesource = require('gulp-inline-source'); +var proxy = require('proxy-middleware'); +var url = require('url'); +var minifyHTML = require('gulp-minify-html'); // var ghPages = require('gulp-gh-pages'); @@ -190,6 +193,7 @@ gulp.task('vulcanize', function() { inlineCss: true, inlineScripts: true })) + .pipe(minifyHTML({ empty: true })) .pipe(gulp.dest(dist('elements'))) .pipe($.size({ title: 'vulcanize' @@ -240,6 +244,10 @@ gulp.task('clean', function() { // Watch files for changes & reload gulp.task('serve', ['styles', 'elements', 'images'], function() { + var peerjsProxy = url.parse('http://localhost:3002/peerjs'); + peerjsProxy.route = '/peerjs'; + var websocketProxy = url.parse('http://localhost:3002/binary'); + websocketProxy.route = '/binary'; browserSync({ port: 5000, notify: false, @@ -259,7 +267,7 @@ gulp.task('serve', ['styles', 'elements', 'images'], function() { // https: true, server: { baseDir: ['.tmp', 'app'], - middleware: [historyApiFallback()] + middleware: [proxy(peerjsProxy),proxy(websocketProxy), historyApiFallback()] } }); diff --git a/index.js b/index.js @@ -0,0 +1,22 @@ +'use strict'; +var express = require('express'); +var compression = require('compression'); +var app = express(); +var http = require('http'); +var ExpressPeerServer = require('peer').ExpressPeerServer; +var wsServer = require('./server/ws-server.js'); + +var server = http.createServer(app); + +// Serve up content from public directory +app.use(compression()); +app.use(express.static(__dirname + '/public')); + +var port = process.env.PORT || 3002; +server.listen(port); +wsServer.create(server); +app.use('/peerjs', ExpressPeerServer(server, { + debug: true +})); + +console.log('listening on port ' + port); diff --git a/package.json b/package.json @@ -1,11 +1,11 @@ { "private": true, "devDependencies": { - "browser-sync": "^2.7.7", + "browser-sync": "^2.10.1", "connect-history-api-fallback": "^1.1.0", "del": "^2.0.2", "glob-all": "^3.0.1", - "gulp": "^3.8.5", + "gulp": "^3.9.0", "gulp-autoprefixer": "^3.1.0", "gulp-cache": "^0.4.0", "gulp-changed": "^1.0.0", @@ -19,7 +19,7 @@ "gulp-jshint": "^1.6.3", "gulp-load-plugins": "^1.1.0", "gulp-minify-css": "^1.2.1", - "gulp-minify-html": "^1.0.2", + "gulp-minify-html": "^1.0.5", "gulp-rename": "^1.2.0", "gulp-replace": "^0.5.4", "gulp-size": "^2.0.0", @@ -28,8 +28,10 @@ "gulp-vulcanize": "^6.0.0", "jshint-stylish": "^2.0.0", "merge-stream": "^1.0.0", + "proxy-middleware": "^0.15.0", "require-dir": "^0.3.0", "run-sequence": "^1.0.2", + "url": "^0.11.0", "vulcanize": ">= 1.4.2", "web-component-tester": "^4.0.0" }, @@ -43,7 +45,9 @@ }, "dependencies": { "binaryjs": "^0.2.1", + "compression": "^1.6.0", "express": "^4.13.3", + "peer": "^0.2.8", "ua-parser-js": "^0.7.10", "ws": "^0.8.1" } diff --git a/server/ws-server.js b/server/ws-server.js @@ -1,106 +1,131 @@ 'use strict'; -var fs = require('fs'); var parser = require('ua-parser-js'); -// Serve client side statically -var express = require('express'); -var app = express(); -app.use(express.static(__dirname + '/public')); - -// var https = require('https'); -// var server = https.createServer({ -// key: fs.readFileSync('/var/www/sharewithme/ssl/privkey.pem').toString(), -// cert: fs.readFileSync('/var/www/sharewithme/ssl/fullchain.pem').toString() -// }, app); - -var http = require('http'); -var server = http.createServer(app); - // Start Binary.js server var BinaryServer = require('binaryjs').BinaryServer; -// link it to express -var bs = BinaryServer({ - server: server -}); - -function getDeviceName(req) { - var ua = parser(req.headers['user-agent']); - return { - model: ua.device.model, - os: ua.os.name, - browser: ua.browser.name, - type: ua.device.type - }; -} -// Wait for new user connections -bs.on('connection', function(client) { - console.log('connection received!'); +exports.create = function(server) { + // link it to express + var bs = BinaryServer({ + server: server, + path: '/binary' + }); - client.deviceName = getDeviceName(client._socket.upgradeReq); - - // Incoming stream from browsers - client.on('stream', function(stream, meta) { - console.log('stream received!', meta); - if (meta.handshake) { - client.uuid = meta.handshake; - return; + function guid() { + function s4() { + return Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); } - meta.from = client.uuid; + return s4() + s4() + '-' + s4() + '-' + s4() + '-' + + s4() + '-' + s4() + s4() + s4(); + } - // broadcast to all other clients - for (var id in bs.clients) { - if (bs.clients.hasOwnProperty(id)) { - var otherClient = bs.clients[id]; - if (otherClient !== client && meta.toPeer === otherClient.uuid) { - var send = otherClient.createStream(meta); - stream.pipe(send, meta); + function getDeviceName(req) { + var ua = parser(req.headers['user-agent']); + return { + model: ua.device.model, + os: ua.os.name, + browser: ua.browser.name, + type: ua.device.type + }; + } + // Wait for new user connections + bs.on('connection', function(client) { + console.log('connection received!', client._socket.upgradeReq.connection.remoteAddress); + + client.uuidRaw = guid(); + + client.deviceName = getDeviceName(client._socket.upgradeReq); + + // Incoming stream from browsers + client.on('stream', function(stream, meta) { + console.log('stream received!', meta); + if (meta && meta.serverMsg === 'rtc-support') { + client.uuid = (meta.rtc ? 'rtc_' : '') + client.uuidRaw; + client.send({ + isSystemEvent: true, + type: 'handshake', + uuid: client.uuid + }); + return; + } + meta.from = client.uuid; + + // broadcast to the other client + for (var id in bs.clients) { + if (bs.clients.hasOwnProperty(id)) { + var otherClient = bs.clients[id]; + if (otherClient !== client && meta.toPeer === otherClient.uuid) { + var send = otherClient.createStream(meta); + stream.pipe(send, meta); + } } } - } + }); }); -}); + function forEachClient(fn) { + for (var id in bs.clients) { + if (bs.clients.hasOwnProperty(id)) { + var client = bs.clients[id]; + fn(client); + } + } + } + function getIP(socket) { + return socket.upgradeReq.headers['x-forwarded-for'] || socket.upgradeReq.connection.remoteAddress; + } -function forEachClient(fn) { - for (var id in bs.clients) { - if (bs.clients.hasOwnProperty(id)) { - var client = bs.clients[id]; - fn(client); + function hash(text) { + // A string hashing function based on Daniel J. Bernstein's popular 'times 33' hash algorithm. + var h = 5381, + index = text.length; + while (index) { + h = (h * 33) ^ text.charCodeAt(--index); } + return h >>> 0; } -} - -function getIP(socket) { - return socket.upgradeReq.headers['x-forwarded-for'] || socket.upgradeReq.connection.remoteAddress; -} -function notifyBuddies() { - //TODO: This should be possible in linear time - forEachClient(function(client1) { - var buddies = []; - var myIP = getIP(client1._socket); - forEachClient(function(client2) { - var otherIP = getIP(client2._socket); - console.log(myIP, otherIP); - if (client1 !== client2 && myIP === otherIP) { - buddies.push({ - peerId: client2.uuid, - name: client2.deviceName - }); - } + function notifyBuddiesX() { + var locations = {}; + //group all clients by location (by public ip address) + forEachClient(function(client) { + //ip is hashed to prevent injections by spoofing the 'x-forwarded-for' header + var ip = hash(getIP(client._socket)); + locations[ip] = locations[ip] || []; + locations[ip].push({ + socket: client, + contact: { + peerId: client.uuid, + name: client.deviceName, + } + }); }); - var msg = { - buddies: buddies, - isSystemEvent: true, - type: 'buddies' - }; - client1.send(msg); - }); -} -setInterval(notifyBuddies, 4000); + //notify every location + Object.keys(locations).forEach(function(locationKey) { + //notify every client of all other clients in this location + var location = locations[locationKey]; + location.forEach(function(client) { + //all other clients + var buddies = location.reduce(function(result, otherClient) { + if (otherClient !== client) { + result.push(otherClient.contact); + } + return result; + }, []); + //protocol + var msg = { + buddies: buddies, + isSystemEvent: true, + type: 'buddies' + }; + client.socket.send(msg); + }); + }); + } -server.listen(9001); -console.log('HTTP and BinaryJS server started on port 9001'); + setInterval(notifyBuddiesX, 5000); +};