snapdrop

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

commit 22be7c5cb9237a81e1f6d7d9f72592d7e0548507
parent e5eab64c6b8770e4b82d20469595b24c251a165d
Author: Robin Linus <robin_woll@capira.de>
Date:   Wed, 23 Dec 2015 13:57:13 +0100

Lots of small improvements, websockets fallback

Diffstat:
Mapp/elements/buddy-finder/buddy-finder.html | 97+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Aapp/elements/buddy-finder/personal-avatar.html | 41+++++++++++++++++++++++++++++++++++++++++
Mapp/elements/buddy-finder/user-avatar.html | 122+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Dapp/elements/contact-item/anonymous-contact-behavior.html | 337-------------------------------------------------------------------------------
Mapp/elements/elements.html | 8++++----
Mapp/elements/file-sharing/file-button-behavior.html | 1+
Mapp/elements/file-sharing/file-button.html | 2+-
Mapp/elements/file-sharing/file-drop-behavior.html | 10+++++++++-
Mapp/elements/file-sharing/file-receiver.html | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Aapp/elements/file-sharing/file-saver.html | 5+++++
Mapp/elements/file-sharing/file-selection-behavior.html | 16+++++-----------
Aapp/elements/p2p-network/binaryjs.html | 1573+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/elements/p2p-network/connection-wrapper.html | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/elements/p2p-network/file-transfer-protocol.html | 137+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mapp/elements/p2p-network/p2p-network.html | 59++++++++++++++++++++++++++++++++++++++++++++++++-----------
Aapp/elements/p2p-network/web-socket.html | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mapp/favicon.ico | 0
Mapp/images/touch/apple-touch-icon.png | 0
Mapp/images/touch/chrome-splashscreen-icon-384x384.png | 0
Mapp/images/touch/chrome-touch-icon-192x192.png | 0
Mapp/images/touch/icon-128x128.png | 0
Aapp/images/touch/logo.png | 0
Mapp/images/touch/ms-icon-144x144.png | 0
Mapp/images/touch/ms-touch-icon-144x144-precomposed.png | 0
Mapp/index.html | 28++++++++++++++--------------
Mapp/manifest.json | 24+++++++++++++-----------
Aapp/scripts/animated-bg.js | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mapp/scripts/app.js | 10+++++++++-
Mapp/styles/app-theme.html | 205+++++--------------------------------------------------------------------------
Aapp/styles/icons.html | 37+++++++++++++++++++++++++++++++++++++
Mapp/styles/main.css | 20+++++++++-----------
Dapp/styles/shared-styles.html | 23-----------------------
Mgulpfile.js | 424++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mpackage.json | 7+++++++
Aserver/ws-server.js | 106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
35 files changed, 2681 insertions(+), 923 deletions(-)

diff --git a/app/elements/buddy-finder/buddy-finder.html b/app/elements/buddy-finder/buddy-finder.html @@ -1,97 +1,100 @@ -<link rel="import" href="../../../bower_components/iron-ajax/iron-ajax.html"> -<link rel="import" href="../../../bower_components/paper-styles/paper-styles.html"> +<link rel="import" href="../../bower_components/iron-ajax/iron-ajax.html"> +<link rel="import" href="../../bower_components/paper-styles/paper-styles.html"> <link rel="import" href="../file-sharing/file-input.html"> <link rel="import" href="user-avatar.html"> +<link rel="import" href="personal-avatar.html"> <dom-module id="buddy-finder"> <template> <style> :host { - display: block; - background-color: white; + background-color: transparent; @apply(--layout-fit); @apply(--layout-horizontal); @apply(--layout-center-center); overflow: hidden; - } - - .paper-font-display1 { - color: black; - text-align: center; - margin-bottom: 16px; - display: none; + position: relative; + height: 100%; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + margin: 0; } .buddies { z-index: 1; @apply(--layout-horizontal); + @apply(--layout-center-center); + @apply(--layout-wrap); } .buddy { cursor: pointer; } - .circles { + .me { position: absolute; - bottom: -50px; + bottom: 24px; left: 50%; - width: 1140px; - margin-left: -570px; - height: 700px; - transform-origin: 570px 570px; - animation: grow 1.5s ease-out; - fill: transparent; + margin-left: -180px; } - .me { - position: absolute; - bottom: 30px; - left: 50%; - margin-left: -60px; + .explanation { + @apply(--paper-font-headline); + color: #4285f4; + text-align: center; } </style> - <div class="paper-font-display1">People near by</div> <div class="buddies"> <template is="dom-repeat" items="{{buddies}}"> - <file-input on-file-selected="_fileDropped"> - <user-avatar contact="{{item.peerId}}" class="buddy"></user-avatar> + <file-input on-file-selected="_fileSelected"> + <user-avatar contact="{{item}}" class="buddy"></user-avatar> </file-input> </template> </div> - <user-avatar contact="{{me}}" class="me"></user-avatar> - <svg class="circles" viewBox="-0.5 -0.5 1140 700"> - <circle class="circle" cx="570" cy="570" r="120" stroke="rgba(160,160,160,.15)"></circle> - <circle class="circle" cx="570" cy="570" r="210" stroke="rgba(160,160,160,.2)"></circle> - <circle class="circle" cx="570" cy="570" r="300" stroke="rgba(160,160,160,.3)"></circle> - <circle class="circle" cx="570" cy="570" r="390" stroke="rgba(160,160,160,.35)"></circle> - <circle class="circle" cx="570" cy="570" r="480" stroke="rgba(160,160,160,.4)"></circle> - <circle class="circle" cx="570" cy="570" r="570" stroke="rgba(160,160,160,.43)"></circle> - </svg> - <iron-ajax id="ajax" auto url="https://yawim.com/findbuddies/{{me}}" handle-as="json" last-response="{{buddies}}"></iron-ajax> + <div hidden$="{{buddies.length}}" class="explanation"> + Open this page on another device + <wbr>to share 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'; Polymer({ is: 'buddy-finder', properties: { - buddies: Array, + buddies: { + type: Array, + value: [] + }, me: { type: String, } }, attached: function() { //Ask server every second for changes - setInterval(function() { - this.$.ajax.generateRequest(); - }.bind(this), 1000); + 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); + } + } + }); }, - _fileDropped: function(e) { + _fileSelected: function(e) { var peerId = e.model.item.peerId; var file = e.detail; - app.p2p.connectToPeer(peerId, function() { - app.p2p.sendFile(peerId, file); - }); - console.log('Send:', file); - console.log('To:', peerId); + app.p2p.sendFile(peerId, file); } }); </script> diff --git a/app/elements/buddy-finder/personal-avatar.html b/app/elements/buddy-finder/personal-avatar.html @@ -0,0 +1,41 @@ +<link rel="import" href="../../bower_components/iron-icon/iron-icon.html"> +<link rel="import" href="../../styles/icons.html"> +<dom-module id="personal-avatar"> + <template> + <style> + :host { + @apply(--layout-vertical); + @apply(--layout-center); + width: 360px; + } + + iron-icon { + width: 80px; + height: 80px; + color: #4285f4; + } + + .paper-font-body1 { + font-size: 13px; + margin-top: 6px; + } + + .discover { + color: #4285f4; + } + </style> + <iron-icon icon="chat:wifi-tethering"></iron-icon> + <div class="paper-font-body1"> + 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. + </div> + </template> + <script> + 'use strict'; + Polymer({ + is: 'personal-avatar' + }); + </script> +</dom-module> diff --git a/app/elements/buddy-finder/user-avatar.html b/app/elements/buddy-finder/user-avatar.html @@ -1,4 +1,4 @@ -<link rel="import" href="../contact-item/anonymous-contact-behavior.html"> +<link rel="import" href="../../bower_components/paper-icon-button/paper-icon-button.html"> <dom-module id="user-avatar"> <template> <style> @@ -9,35 +9,121 @@ width: 120px; height: 120px; } - - .avatar { + + paper-icon-button { display: inline-block; - width: 52px; - height: 52px; + width: 64px !important; + height: 64px !important; border-radius: 50%; overflow: hidden; - background: #ccc; - @apply(--shadow-elevation-2dp); + padding: 12px; + margin-bottom: 4px; + background-color: #4285f4; + color: white; + } + + :host:hover paper-icon-button { + transform: scale(1.05); + } + + .paper-font-subhead { + text-align: center; } - .paper-font-subhead{ - text-align: center; + + .paper-font-body1 { + text-align: center; + width: 100%; + font-size: 13px; + color: grey; + margin-top: 2px; + } + + :host, + .paper-font-subhead, + .paper-font-body1 { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + margin: 4px; } </style> - <div class="avatar" id="avatar" item-icon></div> + <paper-icon-button icon="{{_displayIcon}}"></paper-icon-button> <div class="paper-font-subhead">{{_displayName}}</div> + <div class="paper-font-body1">{{status}}</div> </template> <script> 'use strict'; Polymer({ is: 'user-avatar', - behaviors:[Chat.AnonymousContactBehavior], - observers:['_computeBackgroundImg(contact.*)'], - _computeBackgroundImg:function(){ - console.log('avatar changed'); - var avatar = this.anonymousAccount(this.contact).avatar; - var style = this.$.avatar.style; - style.backgroundImage='url('+avatar.url+')'; - style.backgroundPosition=avatar.left+'px '+avatar.top+'px'; + properties: { + contact: Object, + _displayName: { + computed: '_computeDisplayName(contact)' + }, + _displayIcon: { + computed: '_computeDisplayIcon(contact)' + }, + status: { + type: String, + value: '' + } + }, + _computeDisplayName: function(contact) { + contact = contact.name; + if (contact.model) { + return contact.os + ' ' + contact.model; + } + contact.os = contact.os.replace('Mac OS', 'Mac'); + return contact.os + ' ' + contact.browser; + }, + _computeDisplayIcon: function(contact) { + contact = contact.name; + if (contact.type === 'mobile') { + return 'chat:phone-iphone'; + } + if (contact.type === 'tablet') { + return 'chat:tablet-mac'; + } + + return 'chat:desktop-mac'; + }, + attached: function() { + this.async(function() { + app.p2p.addEventListener('file-offered', function(e) { + if (e.detail.to === this.contact.peerId) { + this.status = 'Waiting to accept...'; + } + }.bind(this), false); + app.p2p.addEventListener('upload-started', function(e) { + if (e.detail.to === this.contact.peerId) { + this.status = 'Uploading...'; + } + }.bind(this), false); + app.p2p.addEventListener('download-started', function(e) { + if (e.detail.from === this.contact.peerId) { + this.status = 'Downloading...'; + } + }.bind(this), false); + app.p2p.addEventListener('upload-complete', function(e) { + if (e.detail.from === this.contact.peerId) { + this.status = ''; + } + }.bind(this), false); + app.p2p.addEventListener('download-complete', function(e) { + if (e.detail.from === this.contact.peerId) { + this.status = ''; + } + }.bind(this), false); + app.p2p.addEventListener('file-declined', function(e) { + if (e.detail.from === this.contact.peerId) { + this.status = ''; + } + }.bind(this), false); + app.p2p.addEventListener('upload-error', function(e) { + this.status = ''; + }.bind(this), false); + }, 200); } }); </script> diff --git a/app/elements/contact-item/anonymous-contact-behavior.html b/app/elements/contact-item/anonymous-contact-behavior.html @@ -1,337 +0,0 @@ -<script> -'use strict'; -window.Chat = window.Chat || {}; -var djb2Code = function(str) { - var hash = 5381; - for (var i = 0; i < str.length; i++) { - var character = str.charCodeAt(i); - hash = ((hash << 5) + hash) + character; /* hash * 33 + c */ - } - return hash > 0 ? hash : -hash; -}; -var animals = [ - 'Adelie', - 'Penguin', - 'Akita', - 'Bulldog', - 'Ant', - 'Fox', - 'Hare', - 'Wolf', - 'Terrier', - 'Avocet', - 'Baboon', - 'Camel', - 'Badger', - 'Barb', - 'Basenji', - 'Basking', - 'Bat', - 'Beagle', - 'Bear', - 'Collie', - 'Beaver', - 'Beetle', - 'Bichon', - 'Bird', - 'Birman', - 'Bison', - 'Bobcat', - 'Bombay', - 'Bongo', - 'Bonobo', - 'Booby', - 'Boykin', - 'Budgie', - 'Buffalo', - 'Burmese', - 'Fish', - 'Caiman', - 'Lizard', - 'Canaan', - 'Caracal', - 'Cat', - 'Catfish', - 'Cesky', - 'Fousek', - 'Chamois', - 'Cheetah', - 'Chicken', - 'Chinook', - 'Cichlid', - 'Leopard', - 'Clumber', - 'Coati', - 'Coral', - 'Tamarin', - 'Cougar', - 'Cow', - 'Coyote', - 'Crab', - 'Macaque', - 'Crane', - 'Cuscus', - 'Frog', - 'Deer', - 'Bracke', - 'Dhole', - 'Dingo', - 'Discus', - 'Dodo', - 'Dog', - 'Dogo', - 'Dolphin', - 'Donkey', - 'Drever', - 'Duck', - 'Dugong', - 'Dunker', - 'Dusky', - 'Eagle', - 'Earwig ', - 'Gorilla', - 'Echidna', - 'Emu', - 'Falcon', - 'Fennec', - 'Ferret', - 'Spitz', - 'Fly', - 'Fossa', - 'Gecko', - 'Gerbil', - 'Gharial', - 'Gibbon', - 'Giraffe', - 'Goat', - 'Oriole', - 'Goose', - 'Gopher', - 'Grouse', - 'Guppy', - 'Shark', - 'Hamster', - 'Harrier', - 'Heron', - 'Horse', - 'Human', - 'Hyena', - 'Ibis', - 'Iguana', - 'Impala', - 'Indri', - 'Insect', - 'Setter', - 'Jackal', - 'Jaguar', - 'Kakapo', - 'Kiwi', - 'Koala', - 'Lemming', - 'Lemur', - 'Liger', - 'Lion', - 'Llama', - 'Lobster', - 'Owl', - 'Lynx', - 'Mayfly', - 'Meerkat', - 'Molly', - 'Mongrel', - 'Monkey', - 'Moorhen', - 'Moose', - 'Mouse', - 'Mule', - 'Numbat', - 'Ocelot', - 'Octopus', - 'Okapi', - 'Opossum', - 'Ostrich', - 'Otter', - 'Oyster', - 'Panther', - 'Parrot', - 'Peacock', - 'Pelican', - 'Persian', - 'Pig', - 'Piranha', - 'Pointer', - 'Poodle', - 'Possum', - 'Prawn', - 'Puffin', - 'Pug', - 'Puma', - 'Pygmy', - 'Quail', - 'Quetzal', - 'Quokka', - 'Quoll', - 'Rabbit', - 'Raccoon', - 'Ragdoll', - 'Rat', - 'Robin', - 'Saola', - 'Seal', - 'Serval', - 'Sheep', - 'Shrimp', - 'Siamese', - 'Skunk', - 'Sloth', - 'Snail', - 'Snake', - 'Somali', - 'Sparrow', - 'Dogfish', - 'Sponge', - 'Squid', - 'Stoat', - 'Swan', - 'Tang', - 'Tapir', - 'Tarsier', - 'Termite', - 'Tetra', - 'Tiffany', - 'Tiger', - 'Toucan', - 'Tuatara', - 'Turkey', - 'Uakari', - 'Uguisu', - 'Vulture', - 'Wallaby', - 'Walrus', - 'Warthog', - 'Wasp', - 'Weasel', - 'Whippet', - 'Wombat', - 'Wrasse', - 'Yak', - 'Yorkie', - 'Zebra', - 'Zebu', - 'Zonkey', - 'Zorse' -]; -var bb = [ - 'Walter White', - 'Skyler White', - 'Jesse Pinkman', - 'Hank Schrader', - 'Marie Schrader', - 'Walter White, Jr.', - 'Saul Goodman', - 'Gustavo Fring', - 'Mike Ehrmantraut', - 'Lydia Rodarte-Quayle', - 'Todd Alquist', - 'Steven Gomez', - 'Detectives Kalanchoe & Munn', - 'George Merkert', - 'Sac Ramey', - 'Tim Roberts', - 'Maximino Arciniega', - 'Gale Boetticher', - 'Duane Chow', - 'Ron Forenall', - 'Barry Goodman', - 'Tyrus Kitt', - 'Chris Mara', - 'Dennis Markowski', - 'Victor', - 'Dan Wachsberger', - 'Don Eladio Vuente', - 'Juan Bolsa', - 'Hector Salamanca', - 'Tuco Salamanca', - 'Leonel Salamanca', - 'Marco Salamanca', - 'Gonzo', - 'Emilio Koyama', - 'Krazy-8 Molina', - 'Jack Welker', - 'Andrea Cantillo', - 'Brock Cantillo', - 'Jane Margolis', - 'Brandon Mayhew', - 'Combo Ortega', - 'Skinny Pete', - 'Adam Pinkman', - 'Mrs. Pinkman', - 'Jake Pinkman', - 'Wendy', - 'Huell Babineaux', - 'Ed', - 'Francesca', - 'Patrick Kuby', - 'Hugo Archuleta', - 'Ted Beneke', - 'Clovis', - 'Louis Corbett', - 'Dr. Delcavoli', - 'Lawson', - 'Donald Margolis', - 'Carmen Molina', - 'Old Joe', - 'Pamela', - 'Gretchen Schwartz', - 'Elliott Schwartz', - 'Drew Sharp', - 'Spooge', - 'Holly White', - 'Bogdan Wolynetz' -]; -Chat.AnonymousContactBehavior = { - properties: { - contact: { - type: Object, - notify: true - }, - _displayName: { - computed: '_computeDisplayName(contact)' - } - }, - _computeDisplayName: function(contact) { - if (contact === undefined || contact === null) { - return 'connecting...'; - } - if (contact === 'error' || contact === 'invite') { - return ''; - } - if (!contact.name) { - return this.anonymousAccount(contact).name; - } - return contact.name; - }, - get names() { - return bb; - }, - anonymousAccount: function(contact) { - if (contact && !contact.name) { - var peer = contact.peer || contact; - var hash = djb2Code(peer); - var i = hash % this.names.length; - var name = this.names[i]; - var marginTop = i % 2; - var marginLeft = Math.floor(i / 2) % 5; - return { - name: name, - peer: peer, - avatar: { - url: 'images/avatars.jpg', - left: -14 + 80 * marginLeft, - top: -19 + 95 * marginTop - } - }; - } - } -}; -</script> diff --git a/app/elements/elements.html b/app/elements/elements.html @@ -1,15 +1,15 @@ <link rel="import" href="../bower_components/platinum-sw/platinum-sw-cache.html"> <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"> -<!-- Configure your routes here --> +<!-- 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="../styles/shared-styles.html"> <link rel="import" href="buddy-finder/buddy-finder.html"> -<link rel="import" href="p2p-network/p2p-network.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-button-behavior.html b/app/elements/file-sharing/file-button-behavior.html @@ -8,6 +8,7 @@ Chat.FileButtonBehaviorImpl = { if (!fileInput) { fileInput = document.createElement('input'); fileInput.type = 'file'; + fileInput.multiple = 'true'; fileInput.className = 'fileInput'; fileInput.style.position = 'fixed'; fileInput.style.top = '-10000px'; diff --git a/app/elements/file-sharing/file-button.html b/app/elements/file-sharing/file-button.html @@ -1,4 +1,4 @@ -<link rel="import" href="../../../bower_components/paper-icon-button/paper-icon-button.html"> +<link rel="import" href="../../bower_components/paper-icon-button/paper-icon-button.html"> <link rel="import" href="file-button-behavior.html"> <dom-module id="file-button"> <template> diff --git a/app/elements/file-sharing/file-drop-behavior.html b/app/elements/file-sharing/file-drop-behavior.html @@ -25,7 +25,7 @@ Chat.FileDropBehaviorImpl = { dropZone.addEventListener('drop', function(event) { event.stopPropagation(); event.preventDefault(); - + //call dragend dragEnd(); @@ -36,5 +36,13 @@ Chat.FileDropBehaviorImpl = { }); } }; +document.body.addEventListener('dragover', function(e) { + e.stopPropagation(); + e.preventDefault(); +}, false); +document.body.addEventListener('drop', function(event) { + event.stopPropagation(); + event.preventDefault(); +}); Chat.FileDropBehavior = [Chat.FileDropBehaviorImpl, Chat.FileSelectionBehavior]; </script> diff --git a/app/elements/file-sharing/file-receiver.html b/app/elements/file-sharing/file-receiver.html @@ -2,51 +2,94 @@ <link rel="import" href="../../bower_components/paper-button/paper-button.html"> <link rel="import" href="../../bower_components/neon-animation/animations/scale-up-animation.html"> <link rel="import" href="../../bower_components/neon-animation/animations/fade-out-animation.html"> +<link rel="import" href="../../bower_components/iron-pages/iron-pages.html"> +<link rel="import" href="../../bower_components/paper-spinner/paper-spinner.html"> <dom-module id="file-receiver"> <template> <style> :host { display: block; - position: fixed; - z-index: 100; + } + + #dialog, + #download { + width: 300px; + z-index: 101; + } + + b { + word-break: break-word; } </style> - <paper-dialog id="dialog" entry-animation="scale-up-animation" exit-animation="fade-out-animation" with-backdro> + <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> + <div class="buttons"> + <paper-button dialog-dismiss on-tap="_decline">Discard</paper-button> + <paper-button dialog-confirm on-tap="_accept" autofocus>Download</paper-button> + </div> + </paper-dialog> + <paper-dialog id="download" entry-animation="scale-up-animation" exit-animation="fade-out-animation" with-backdrop modal> <h2>File Received</h2> - <p>You received file {{file.name}}</p> + <p>Right Click and "Save as"...</p> <div class="buttons"> - <paper-button dialog-dismiss>Dismiss</paper-button> - <paper-button dialog-confirm on-tap="_download">Download</paper-button> + <paper-button dialog-dismiss>Discard</paper-button> + <a href="{{dataUri}}" target="_blank"> + <paper-button dialog-confirm autofocus>Download</paper-button> + </a> </div> </paper-dialog> </template> <script> 'use strict'; - Polymer({ - is: 'file-receiver', - attached: function() { - this.async(function() { - app.p2p.addEventListener('file-received', function(e) { - this.fileReceived(e.detail); - }.bind(this), false); - },200); - }, - fileReceived: function(file) { - this.set('file', file); - this.$.dialog.open(); - }, - _download: function() { - var link = document.createElement('a'); - link.download = this.file.name; - // Construct the uri - var uri = this.file.dataURI; - link.href = uri; - document.body.appendChild(link); - link.click(); - // Cleanup the DOM - document.body.removeChild(link); - //delete link; - } - }); + (function() { + Polymer({ + is: 'file-receiver', + attached: function() { + this.async(function() { + app.p2p.addEventListener('file-offer', function(e) { + this.file = e.detail; + this.$.dialog.open(); + }.bind(this), false); + app.p2p.addEventListener('file-received', function(e) { + this._fileReceived(e.detail); + }.bind(this), false); + app.p2p.addEventListener('file-declined', function(e) { + app.displayToast('User declined file ' + e.detail.name); + }.bind(this), false); + app.p2p.addEventListener('upload-complete', function(e) { + app.displayToast('User received file ' + e.detail.name); + }.bind(this), false); + app.p2p.addEventListener('upload-error', function(e) { + app.displayToast('The other device did not respond. Please try again.'); + }.bind(this), false); + }, 200); + }, + _fileReceived: function(file) { + this.downloadURI(file); + }, + _decline: function() { + app.p2p.decline(this.file); + }, + _accept: function() { + app.p2p.accept(this.file); + }, + downloadURI: function(file) { + var link = document.createElement('a'); + var uri = (window.URL || window.webkitURL).createObjectURL(file.blob); + if (typeof link.download !== 'undefined') { + //download attribute is supported + link.href = uri; + link.download = file.name || 'blank'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } else { + this.dataUri = uri; + this.$.download.open(); + } + } + }); + }()); </script> </dom-module> diff --git a/app/elements/file-sharing/file-saver.html b/app/elements/file-sharing/file-saver.html @@ -0,0 +1,4 @@ +<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 @@ -3,22 +3,16 @@ window.Chat = window.Chat || {}; Chat.FileSelectionBehavior = { notifyFilesSelection: function(files) { - if(!files){ + if (!files) { console.log('no files selected...'); return; } for (var i = 0; i < files.length; i++) { var file = files[i]; - var reader = new FileReader(); - reader.onload = function(e2) { - // finished reading file data. - console.log('file dropped'); - this.fire('file-selected', { - dataURI: e2.target.result, - name: file.name - }); - }.bind(this); - reader.readAsDataURL(file); // start reading the file data. + this.fire('file-selected', { + file: file, + name: file.name + }); } } }; diff --git a/app/elements/p2p-network/binaryjs.html b/app/elements/p2p-network/binaryjs.html @@ -0,0 +1,1572 @@ +<script> + /*! binary.js build:0.2.2, development. Copyright(c) 2012 Eric Zhang <eric@ericzhang.com> MIT Licensed */ +(function(exports){ +/*! binarypack.js build:0.0.9, production. Copyright(c) 2012 Eric Zhang <eric@ericzhang.com> MIT Licensed */(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ +var BufferBuilder = require('./bufferbuilder').BufferBuilder; +var binaryFeatures = require('./bufferbuilder').binaryFeatures; + +var BinaryPack = { + unpack: function(data){ + var unpacker = new Unpacker(data); + return unpacker.unpack(); + }, + pack: function(data){ + var packer = new Packer(); + packer.pack(data); + var buffer = packer.getBuffer(); + return buffer; + } +}; + +module.exports = BinaryPack; + +function Unpacker (data){ + // Data is ArrayBuffer + this.index = 0; + this.dataBuffer = data; + this.dataView = new Uint8Array(this.dataBuffer); + this.length = this.dataBuffer.byteLength; +} + +Unpacker.prototype.unpack = function(){ + var type = this.unpack_uint8(); + if (type < 0x80){ + var positive_fixnum = type; + return positive_fixnum; + } else if ((type ^ 0xe0) < 0x20){ + var negative_fixnum = (type ^ 0xe0) - 0x20; + return negative_fixnum; + } + var size; + if ((size = type ^ 0xa0) <= 0x0f){ + return this.unpack_raw(size); + } else if ((size = type ^ 0xb0) <= 0x0f){ + return this.unpack_string(size); + } else if ((size = type ^ 0x90) <= 0x0f){ + return this.unpack_array(size); + } else if ((size = type ^ 0x80) <= 0x0f){ + return this.unpack_map(size); + } + switch(type){ + case 0xc0: + return null; + case 0xc1: + return undefined; + case 0xc2: + return false; + case 0xc3: + return true; + case 0xca: + return this.unpack_float(); + case 0xcb: + return this.unpack_double(); + case 0xcc: + return this.unpack_uint8(); + case 0xcd: + return this.unpack_uint16(); + case 0xce: + return this.unpack_uint32(); + case 0xcf: + return this.unpack_uint64(); + case 0xd0: + return this.unpack_int8(); + case 0xd1: + return this.unpack_int16(); + case 0xd2: + return this.unpack_int32(); + case 0xd3: + return this.unpack_int64(); + case 0xd4: + return undefined; + case 0xd5: + return undefined; + case 0xd6: + return undefined; + case 0xd7: + return undefined; + case 0xd8: + size = this.unpack_uint16(); + return this.unpack_string(size); + case 0xd9: + size = this.unpack_uint32(); + return this.unpack_string(size); + case 0xda: + size = this.unpack_uint16(); + return this.unpack_raw(size); + case 0xdb: + size = this.unpack_uint32(); + return this.unpack_raw(size); + case 0xdc: + size = this.unpack_uint16(); + return this.unpack_array(size); + case 0xdd: + size = this.unpack_uint32(); + return this.unpack_array(size); + case 0xde: + size = this.unpack_uint16(); + return this.unpack_map(size); + case 0xdf: + size = this.unpack_uint32(); + return this.unpack_map(size); + } +} + +Unpacker.prototype.unpack_uint8 = function(){ + var byte = this.dataView[this.index] & 0xff; + this.index++; + return byte; +}; + +Unpacker.prototype.unpack_uint16 = function(){ + var bytes = this.read(2); + var uint16 = + ((bytes[0] & 0xff) * 256) + (bytes[1] & 0xff); + this.index += 2; + return uint16; +} + +Unpacker.prototype.unpack_uint32 = function(){ + var bytes = this.read(4); + var uint32 = + ((bytes[0] * 256 + + bytes[1]) * 256 + + bytes[2]) * 256 + + bytes[3]; + this.index += 4; + return uint32; +} + +Unpacker.prototype.unpack_uint64 = function(){ + var bytes = this.read(8); + var uint64 = + ((((((bytes[0] * 256 + + bytes[1]) * 256 + + bytes[2]) * 256 + + bytes[3]) * 256 + + bytes[4]) * 256 + + bytes[5]) * 256 + + bytes[6]) * 256 + + bytes[7]; + this.index += 8; + return uint64; +} + + +Unpacker.prototype.unpack_int8 = function(){ + var uint8 = this.unpack_uint8(); + return (uint8 < 0x80 ) ? uint8 : uint8 - (1 << 8); +}; + +Unpacker.prototype.unpack_int16 = function(){ + var uint16 = this.unpack_uint16(); + return (uint16 < 0x8000 ) ? uint16 : uint16 - (1 << 16); +} + +Unpacker.prototype.unpack_int32 = function(){ + var uint32 = this.unpack_uint32(); + return (uint32 < Math.pow(2, 31) ) ? uint32 : + uint32 - Math.pow(2, 32); +} + +Unpacker.prototype.unpack_int64 = function(){ + var uint64 = this.unpack_uint64(); + return (uint64 < Math.pow(2, 63) ) ? uint64 : + uint64 - Math.pow(2, 64); +} + +Unpacker.prototype.unpack_raw = function(size){ + if ( this.length < this.index + size){ + throw new Error('BinaryPackFailure: index is out of range' + + ' ' + this.index + ' ' + size + ' ' + this.length); + } + var buf = this.dataBuffer.slice(this.index, this.index + size); + this.index += size; + + //buf = util.bufferToString(buf); + + return buf; +} + +Unpacker.prototype.unpack_string = function(size){ + var bytes = this.read(size); + var i = 0, str = '', c, code; + while(i < size){ + c = bytes[i]; + if ( c < 128){ + str += String.fromCharCode(c); + i++; + } else if ((c ^ 0xc0) < 32){ + code = ((c ^ 0xc0) << 6) | (bytes[i+1] & 63); + str += String.fromCharCode(code); + i += 2; + } else { + code = ((c & 15) << 12) | ((bytes[i+1] & 63) << 6) | + (bytes[i+2] & 63); + str += String.fromCharCode(code); + i += 3; + } + } + this.index += size; + return str; +} + +Unpacker.prototype.unpack_array = function(size){ + var objects = new Array(size); + for(var i = 0; i < size ; i++){ + objects[i] = this.unpack(); + } + return objects; +} + +Unpacker.prototype.unpack_map = function(size){ + var map = {}; + for(var i = 0; i < size ; i++){ + var key = this.unpack(); + var value = this.unpack(); + map[key] = value; + } + return map; +} + +Unpacker.prototype.unpack_float = function(){ + var uint32 = this.unpack_uint32(); + var sign = uint32 >> 31; + var exp = ((uint32 >> 23) & 0xff) - 127; + var fraction = ( uint32 & 0x7fffff ) | 0x800000; + return (sign == 0 ? 1 : -1) * + fraction * Math.pow(2, exp - 23); +} + +Unpacker.prototype.unpack_double = function(){ + var h32 = this.unpack_uint32(); + var l32 = this.unpack_uint32(); + var sign = h32 >> 31; + var exp = ((h32 >> 20) & 0x7ff) - 1023; + var hfrac = ( h32 & 0xfffff ) | 0x100000; + var frac = hfrac * Math.pow(2, exp - 20) + + l32 * Math.pow(2, exp - 52); + return (sign == 0 ? 1 : -1) * frac; +} + +Unpacker.prototype.read = function(length){ + var j = this.index; + if (j + length <= this.length) { + return this.dataView.subarray(j, j + length); + } else { + throw new Error('BinaryPackFailure: read index out of range'); + } +} + +function Packer(){ + this.bufferBuilder = new BufferBuilder(); +} + +Packer.prototype.getBuffer = function(){ + return this.bufferBuilder.getBuffer(); +} + +Packer.prototype.pack = function(value){ + var type = typeof(value); + if (type == 'string'){ + this.pack_string(value); + } else if (type == 'number'){ + if (Math.floor(value) === value){ + this.pack_integer(value); + } else{ + this.pack_double(value); + } + } else if (type == 'boolean'){ + if (value === true){ + this.bufferBuilder.append(0xc3); + } else if (value === false){ + this.bufferBuilder.append(0xc2); + } + } else if (type == 'undefined'){ + this.bufferBuilder.append(0xc0); + } else if (type == 'object'){ + if (value === null){ + this.bufferBuilder.append(0xc0); + } else { + var constructor = value.constructor; + if (constructor == Array){ + this.pack_array(value); + } else if (constructor == Blob || constructor == File) { + this.pack_bin(value); + } else if (constructor == ArrayBuffer) { + if(binaryFeatures.useArrayBufferView) { + this.pack_bin(new Uint8Array(value)); + } else { + this.pack_bin(value); + } + } else if ('BYTES_PER_ELEMENT' in value){ + if(binaryFeatures.useArrayBufferView) { + this.pack_bin(new Uint8Array(value.buffer)); + } else { + this.pack_bin(value.buffer); + } + } else if (constructor == Object){ + this.pack_object(value); + } else if (constructor == Date){ + this.pack_string(value.toString()); + } else if (typeof value.toBinaryPack == 'function'){ + this.bufferBuilder.append(value.toBinaryPack()); + } else { + throw new Error('Type "' + constructor.toString() + '" not yet supported'); + } + } + } else { + throw new Error('Type "' + type + '" not yet supported'); + } + this.bufferBuilder.flush(); +} + + +Packer.prototype.pack_bin = function(blob){ + var length = blob.length || blob.byteLength || blob.size; + if (length <= 0x0f){ + this.pack_uint8(0xa0 + length); + } else if (length <= 0xffff){ + this.bufferBuilder.append(0xda) ; + this.pack_uint16(length); + } else if (length <= 0xffffffff){ + this.bufferBuilder.append(0xdb); + this.pack_uint32(length); + } else{ + throw new Error('Invalid length'); + } + this.bufferBuilder.append(blob); +} + +Packer.prototype.pack_string = function(str){ + var length = utf8Length(str); + + if (length <= 0x0f){ + this.pack_uint8(0xb0 + length); + } else if (length <= 0xffff){ + this.bufferBuilder.append(0xd8) ; + this.pack_uint16(length); + } else if (length <= 0xffffffff){ + this.bufferBuilder.append(0xd9); + this.pack_uint32(length); + } else{ + throw new Error('Invalid length'); + } + this.bufferBuilder.append(str); +} + +Packer.prototype.pack_array = function(ary){ + var length = ary.length; + if (length <= 0x0f){ + this.pack_uint8(0x90 + length); + } else if (length <= 0xffff){ + this.bufferBuilder.append(0xdc) + this.pack_uint16(length); + } else if (length <= 0xffffffff){ + this.bufferBuilder.append(0xdd); + this.pack_uint32(length); + } else{ + throw new Error('Invalid length'); + } + for(var i = 0; i < length ; i++){ + this.pack(ary[i]); + } +} + +Packer.prototype.pack_integer = function(num){ + if ( -0x20 <= num && num <= 0x7f){ + this.bufferBuilder.append(num & 0xff); + } else if (0x00 <= num && num <= 0xff){ + this.bufferBuilder.append(0xcc); + this.pack_uint8(num); + } else if (-0x80 <= num && num <= 0x7f){ + this.bufferBuilder.append(0xd0); + this.pack_int8(num); + } else if ( 0x0000 <= num && num <= 0xffff){ + this.bufferBuilder.append(0xcd); + this.pack_uint16(num); + } else if (-0x8000 <= num && num <= 0x7fff){ + this.bufferBuilder.append(0xd1); + this.pack_int16(num); + } else if ( 0x00000000 <= num && num <= 0xffffffff){ + this.bufferBuilder.append(0xce); + this.pack_uint32(num); + } else if (-0x80000000 <= num && num <= 0x7fffffff){ + this.bufferBuilder.append(0xd2); + this.pack_int32(num); + } else if (-0x8000000000000000 <= num && num <= 0x7FFFFFFFFFFFFFFF){ + this.bufferBuilder.append(0xd3); + this.pack_int64(num); + } else if (0x0000000000000000 <= num && num <= 0xFFFFFFFFFFFFFFFF){ + this.bufferBuilder.append(0xcf); + this.pack_uint64(num); + } else{ + throw new Error('Invalid integer'); + } +} + +Packer.prototype.pack_double = function(num){ + var sign = 0; + if (num < 0){ + sign = 1; + num = -num; + } + var exp = Math.floor(Math.log(num) / Math.LN2); + var frac0 = num / Math.pow(2, exp) - 1; + var frac1 = Math.floor(frac0 * Math.pow(2, 52)); + var b32 = Math.pow(2, 32); + var h32 = (sign << 31) | ((exp+1023) << 20) | + (frac1 / b32) & 0x0fffff; + var l32 = frac1 % b32; + this.bufferBuilder.append(0xcb); + this.pack_int32(h32); + this.pack_int32(l32); +} + +Packer.prototype.pack_object = function(obj){ + var keys = Object.keys(obj); + var length = keys.length; + if (length <= 0x0f){ + this.pack_uint8(0x80 + length); + } else if (length <= 0xffff){ + this.bufferBuilder.append(0xde); + this.pack_uint16(length); + } else if (length <= 0xffffffff){ + this.bufferBuilder.append(0xdf); + this.pack_uint32(length); + } else{ + throw new Error('Invalid length'); + } + for(var prop in obj){ + if (obj.hasOwnProperty(prop)){ + this.pack(prop); + this.pack(obj[prop]); + } + } +} + +Packer.prototype.pack_uint8 = function(num){ + this.bufferBuilder.append(num); +} + +Packer.prototype.pack_uint16 = function(num){ + this.bufferBuilder.append(num >> 8); + this.bufferBuilder.append(num & 0xff); +} + +Packer.prototype.pack_uint32 = function(num){ + var n = num & 0xffffffff; + this.bufferBuilder.append((n & 0xff000000) >>> 24); + this.bufferBuilder.append((n & 0x00ff0000) >>> 16); + this.bufferBuilder.append((n & 0x0000ff00) >>> 8); + this.bufferBuilder.append((n & 0x000000ff)); +} + +Packer.prototype.pack_uint64 = function(num){ + var high = num / Math.pow(2, 32); + var low = num % Math.pow(2, 32); + this.bufferBuilder.append((high & 0xff000000) >>> 24); + this.bufferBuilder.append((high & 0x00ff0000) >>> 16); + this.bufferBuilder.append((high & 0x0000ff00) >>> 8); + this.bufferBuilder.append((high & 0x000000ff)); + this.bufferBuilder.append((low & 0xff000000) >>> 24); + this.bufferBuilder.append((low & 0x00ff0000) >>> 16); + this.bufferBuilder.append((low & 0x0000ff00) >>> 8); + this.bufferBuilder.append((low & 0x000000ff)); +} + +Packer.prototype.pack_int8 = function(num){ + this.bufferBuilder.append(num & 0xff); +} + +Packer.prototype.pack_int16 = function(num){ + this.bufferBuilder.append((num & 0xff00) >> 8); + this.bufferBuilder.append(num & 0xff); +} + +Packer.prototype.pack_int32 = function(num){ + this.bufferBuilder.append((num >>> 24) & 0xff); + this.bufferBuilder.append((num & 0x00ff0000) >>> 16); + this.bufferBuilder.append((num & 0x0000ff00) >>> 8); + this.bufferBuilder.append((num & 0x000000ff)); +} + +Packer.prototype.pack_int64 = function(num){ + var high = Math.floor(num / Math.pow(2, 32)); + var low = num % Math.pow(2, 32); + this.bufferBuilder.append((high & 0xff000000) >>> 24); + this.bufferBuilder.append((high & 0x00ff0000) >>> 16); + this.bufferBuilder.append((high & 0x0000ff00) >>> 8); + this.bufferBuilder.append((high & 0x000000ff)); + this.bufferBuilder.append((low & 0xff000000) >>> 24); + this.bufferBuilder.append((low & 0x00ff0000) >>> 16); + this.bufferBuilder.append((low & 0x0000ff00) >>> 8); + this.bufferBuilder.append((low & 0x000000ff)); +} + +function _utf8Replace(m){ + var code = m.charCodeAt(0); + + if(code <= 0x7ff) return '00'; + if(code <= 0xffff) return '000'; + if(code <= 0x1fffff) return '0000'; + if(code <= 0x3ffffff) return '00000'; + return '000000'; +} + +function utf8Length(str){ + if (str.length > 600) { + // Blob method faster for large strings + return (new Blob([str])).size; + } else { + return str.replace(/[^\u0000-\u007F]/g, _utf8Replace).length; + } +} + +},{"./bufferbuilder":2}],2:[function(require,module,exports){ +var binaryFeatures = {}; +binaryFeatures.useBlobBuilder = (function(){ + try { + new Blob([]); + return false; + } catch (e) { + return true; + } +})(); + +binaryFeatures.useArrayBufferView = !binaryFeatures.useBlobBuilder && (function(){ + try { + return (new Blob([new Uint8Array([])])).size === 0; + } catch (e) { + return true; + } +})(); + +module.exports.binaryFeatures = binaryFeatures; +var BlobBuilder = module.exports.BlobBuilder; +if (typeof window != 'undefined') { + BlobBuilder = module.exports.BlobBuilder = window.WebKitBlobBuilder || + window.MozBlobBuilder || window.MSBlobBuilder || window.BlobBuilder; +} + +function BufferBuilder(){ + this._pieces = []; + this._parts = []; +} + +BufferBuilder.prototype.append = function(data) { + if(typeof data === 'number') { + this._pieces.push(data); + } else { + this.flush(); + this._parts.push(data); + } +}; + +BufferBuilder.prototype.flush = function() { + if (this._pieces.length > 0) { + var buf = new Uint8Array(this._pieces); + if(!binaryFeatures.useArrayBufferView) { + buf = buf.buffer; + } + this._parts.push(buf); + this._pieces = []; + } +}; + +BufferBuilder.prototype.getBuffer = function() { + this.flush(); + if(binaryFeatures.useBlobBuilder) { + var builder = new BlobBuilder(); + for(var i = 0, ii = this._parts.length; i < ii; i++) { + builder.append(this._parts[i]); + } + return builder.getBlob(); + } else { + return new Blob(this._parts); + } +}; + +module.exports.BufferBuilder = BufferBuilder; + +},{}],3:[function(require,module,exports){ +var BufferBuilderExports = require('./bufferbuilder'); + +window.BufferBuilder = BufferBuilderExports.BufferBuilder; +window.binaryFeatures = BufferBuilderExports.binaryFeatures; +window.BlobBuilder = BufferBuilderExports.BlobBuilder; +window.BinaryPack = require('./binarypack'); + +},{"./binarypack":1,"./bufferbuilder":2}]},{},[3]); +/** + * Light EventEmitter. Ported from Node.js/events.js + * Eric Zhang + */ + +/** + * EventEmitter class + * Creates an object with event registering and firing methods + */ +function EventEmitter() { + // Initialise required storage variables + this._events = {}; +} + +var isArray = Array.isArray; + + +EventEmitter.prototype.addListener = function(type, listener, scope, once) { + if ('function' !== typeof listener) { + throw new Error('addListener only takes instances of Function'); + } + + // To avoid recursion in the case that type == "newListeners"! Before + // adding it to the listeners, first emit "newListeners". + this.emit('newListener', type, typeof listener.listener === 'function' ? + listener.listener : listener); + + if (!this._events[type]) { + // Optimize the case of one listener. Don't need the extra array object. + this._events[type] = listener; + } else if (isArray(this._events[type])) { + + // If we've already got an array, just append. + this._events[type].push(listener); + + } else { + // Adding the second element, need to change to array. + this._events[type] = [this._events[type], listener]; + } + +}; + +EventEmitter.prototype.on = EventEmitter.prototype.addListener; + +EventEmitter.prototype.once = function(type, listener, scope) { + if ('function' !== typeof listener) { + throw new Error('.once only takes instances of Function'); + } + + var self = this; + function g() { + self.removeListener(type, g); + listener.apply(this, arguments); + }; + + g.listener = listener; + self.on(type, g); + + return this; +}; + +EventEmitter.prototype.removeListener = function(type, listener, scope) { + if ('function' !== typeof listener) { + throw new Error('removeListener only takes instances of Function'); + } + + // does not use listeners(), so no side effect of creating _events[type] + if (!this._events[type]) return this; + + var list = this._events[type]; + + if (isArray(list)) { + var position = -1; + for (var i = 0, length = list.length; i < length; i++) { + if (list[i] === listener || + (list[i].listener && list[i].listener === listener)) + { + position = i; + break; + } + } + + if (position < 0) return this; + list.splice(position, 1); + if (list.length == 0) + delete this._events[type]; + } else if (list === listener || + (list.listener && list.listener === listener)) + { + delete this._events[type]; + } + + return this; +}; + + +EventEmitter.prototype.off = EventEmitter.prototype.removeListener; + + +EventEmitter.prototype.removeAllListeners = function(type) { + if (arguments.length === 0) { + this._events = {}; + return this; + } + + // does not use listeners(), so no side effect of creating _events[type] + if (type && this._events && this._events[type]) this._events[type] = null; + return this; +}; + +EventEmitter.prototype.listeners = function(type) { + if (!this._events[type]) this._events[type] = []; + if (!isArray(this._events[type])) { + this._events[type] = [this._events[type]]; + } + return this._events[type]; +}; + +EventEmitter.prototype.emit = function(type) { + var type = arguments[0]; + var handler = this._events[type]; + if (!handler) return false; + + if (typeof handler == 'function') { + switch (arguments.length) { + // fast cases + case 1: + handler.call(this); + break; + case 2: + handler.call(this, arguments[1]); + break; + case 3: + handler.call(this, arguments[1], arguments[2]); + break; + // slower + default: + var l = arguments.length; + var args = new Array(l - 1); + for (var i = 1; i < l; i++) args[i - 1] = arguments[i]; + handler.apply(this, args); + } + return true; + + } else if (isArray(handler)) { + var l = arguments.length; + var args = new Array(l - 1); + for (var i = 1; i < l; i++) args[i - 1] = arguments[i]; + + var listeners = handler.slice(); + for (var i = 0, l = listeners.length; i < l; i++) { + listeners[i].apply(this, args); + } + return true; + } else { + return false; + } +}; + + + + +var util = { + inherits: function(ctor, superCtor) { + ctor.super_ = superCtor; + ctor.prototype = Object.create(superCtor.prototype, { + constructor: { + value: ctor, + enumerable: false, + writable: true, + configurable: true + } + }); + }, + extend: function(dest, source) { + for(var key in source) { + if(source.hasOwnProperty(key)) { + dest[key] = source[key]; + } + } + return dest; + }, + pack: BinaryPack.pack, + unpack: BinaryPack.unpack, + setZeroTimeout: (function(global) { + var timeouts = []; + var messageName = 'zero-timeout-message'; + + // Like setTimeout, but only takes a function argument. There's + // no time argument (always zero) and no arguments (you have to + // use a closure). + function setZeroTimeoutPostMessage(fn) { + timeouts.push(fn); + global.postMessage(messageName, '*'); + } + + function handleMessage(event) { + if (event.source == global && event.data == messageName) { + if (event.stopPropagation) { + event.stopPropagation(); + } + if (timeouts.length) { + timeouts.shift()(); + } + } + } + if (global.addEventListener) { + global.addEventListener('message', handleMessage, true); + } else if (global.attachEvent) { + global.attachEvent('onmessage', handleMessage); + } + return setZeroTimeoutPostMessage; + }(this)) +}; + +exports.util = util; + + +function Stream() { + EventEmitter.call(this); +} + +util.inherits(Stream, EventEmitter); + +Stream.prototype.pipe = function(dest, options) { + var source = this; + + function ondata(chunk) { + if (dest.writable) { + if (false === dest.write(chunk) && source.pause) { + source.pause(); + } + } + } + + source.on('data', ondata); + + function ondrain() { + if (source.readable && source.resume) { + source.resume(); + } + } + + dest.on('drain', ondrain); + + // If the 'end' option is not supplied, dest.end() will be called when + // source gets the 'end' or 'close' events. Only dest.end() once. + if (!dest._isStdio && (!options || options.end !== false)) { + source.on('end', onend); + source.on('close', onclose); + } + + var didOnEnd = false; + function onend() { + if (didOnEnd) return; + didOnEnd = true; + + dest.end(); + } + + + function onclose() { + if (didOnEnd) return; + didOnEnd = true; + + dest.destroy(); + } + + // don't leave dangling pipes when there are errors. + function onerror(er) { + cleanup(); + if (this.listeners('error').length === 0) { + throw er; // Unhandled stream error in pipe. + } + } + + source.on('error', onerror); + dest.on('error', onerror); + + // remove all the event listeners that were added. + function cleanup() { + source.removeListener('data', ondata); + dest.removeListener('drain', ondrain); + + source.removeListener('end', onend); + source.removeListener('close', onclose); + + source.removeListener('error', onerror); + dest.removeListener('error', onerror); + + source.removeListener('end', cleanup); + source.removeListener('close', cleanup); + + dest.removeListener('end', cleanup); + dest.removeListener('close', cleanup); + } + + source.on('end', cleanup); + source.on('close', cleanup); + + dest.on('end', cleanup); + dest.on('close', cleanup); + + dest.emit('pipe', source); + + // Allow for unix-like usage: A.pipe(B).pipe(C) + return dest; +}; + +exports.Stream = Stream; +function BlobReadStream(source, options){ + Stream.call(this); + + options = util.extend({ + readDelay: 0, + paused: false + }, options); + + this._source = source; + this._start = 0; + this._readChunkSize = options.chunkSize || source.size; + this._readDelay = options.readDelay; + + this.readable = true; + this.paused = options.paused; + + this._read(); +} + +util.inherits(BlobReadStream, Stream); + + +BlobReadStream.prototype.pause = function(){ + this.paused = true; +}; + +BlobReadStream.prototype.resume = function(){ + this.paused = false; + this._read(); +}; + +BlobReadStream.prototype.destroy = function(){ + this.readable = false; + clearTimeout(this._timeoutId); +}; + +BlobReadStream.prototype._read = function(){ + + var self = this; + + function emitReadChunk(){ + self._emitReadChunk(); + } + + var readDelay = this._readDelay; + if (readDelay !== 0){ + this._timeoutId = setTimeout(emitReadChunk, readDelay); + } else { + util.setZeroTimeout(emitReadChunk); + } + +}; + +BlobReadStream.prototype._emitReadChunk = function(){ + + if(this.paused || !this.readable) return; + + var chunkSize = Math.min(this._source.size - this._start, this._readChunkSize); + + if(chunkSize === 0){ + this.readable = false; + this.emit("end"); + return; + } + + var sourceEnd = this._start + chunkSize; + var chunk = (this._source.slice || this._source.webkitSlice || this._source.mozSlice).call(this._source, this._start, sourceEnd); + + this._start = sourceEnd; + this._read(); + + this.emit("data", chunk); + +}; + +/* + + + + +function BlobWriteStream(options){ + + stream.Stream.call(this); + + options = _.extend({ + onFull: onFull, + onEnd: function(){}, + minBlockAllocSize: 0, + drainDelay:0 + }, options); + + this._onFull = options.onFull; + this._onEnd = options.onEnd; + this._onWrite = options.onWrite; + + this._minBlockAllocSize = options.minBlockAllocSize; + this._maxBlockAllocSize = options.maxBlockAllocSize; + this._drainDelay = options.drainDelay; + + this._buffer = new Buffer(options.minBlockAllocSize); + this._destination = this._buffer; + this._destinationPos = 0; + + this._writeQueue = []; + this._pendingOnFull = false; + this._pendingQueueDrain = false; + + this.writable = true; + this.bytesWritten = 0; +} + +util.inherits(BlobWriteStream, stream.Stream); + +BlobWriteStream.prototype.getBuffer = function(){ + return this._buffer; +}; + +BlobWriteStream.prototype.write = function(data, encoding){ + + if(!this.writable){ + throw new Error("stream is not writable"); + } + + if(!Buffer.isBuffer(data)){ + data = new Buffer(data, encoding); + } + + if(data.length){ + this._writeQueue.push(data); + } + + this._commit(); + + return this._writeQueue.length === 0; +}; + +BlobWriteStream.prototype._commit = function(){ + + var self = this; + + var destination = this._destination; + var writeQueue = this._writeQueue; + + var startDestinationPos = this._destinationPos; + + while(writeQueue.length && destination.length){ + + var head = writeQueue[0]; + + var copySize = Math.min(destination.length, head.length); + + head.copy(destination, 0, 0, copySize); + + head = head.slice(copySize); + destination = destination.slice(copySize); + + this.bytesWritten += copySize; + this._destinationPos += copySize; + + if(head.length === 0){ + writeQueue.shift(); + } + else{ + writeQueue[0] = head; + } + } + + this._destination = destination; + + bytesCommitted = this._destinationPos - startDestinationPos; + if(bytesCommitted){ + if(this._onWrite){ + + if(writeQueue.length){ + this._pendingQueueDrain = true; + } + + // By locking destination the buffer is frozen and the onWrite + // callback cannot miss any write commits + this._destination = emptyBuffer; + + var consumer = this._onWrite; + this._onWrite = null; + + consumer.call(this, function(nextCallback){ + util.setZeroTimeout(function(){ + self._destination = destination; + self._onWrite = nextCallback; + self._commit(); + }); + }, consumer); + + return; + } + } + + if(writeQueue.length){ + + this._pendingQueueDrain = true; + this._growBuffer(); + } + else if(this._pendingQueueDrain){ + + this._pendingQueueDrain = false; + + if(this._drainDelay !== 0){ + setTimeout(function(){ + self.emit("drain"); + }, this._drainDelay); + } + else{ + util.setZeroTimeout(function(){ + self.emit("drain"); + }); + } + } +}; + +BlobWriteStream.prototype._growBuffer = function(){ + + var self = this; + var writeQueue = this._writeQueue; + + var requestSize = this._minBlockAllocSize; + + var maxBlockAllocSize = this._maxBlockAllocSize; + var add = (maxBlockAllocSize === undefined ? function(a, b){return a + b;} : function(a, b){return Math.min(a + b, maxBlockAllocSize);}); + + for(var i = 0, queueLength = writeQueue.length; i < queueLength; i++){ + requestSize = add(requestSize, writeQueue[i].length); + } + + // Prevent concurrent onFull callbacks + if(this._pendingOnFull){ + return; + } + this._pendingOnFull = true; + + this._onFull(this._buffer, requestSize, function(buffer, destination){ + util.setZeroTimeout(function(){ + + self._pendingOnFull = false; + + if(!destination){ + if(self.writable){ + self.emit("error", new Error("buffer is full")); + } + self.destroy(); + return; + } + + self._buffer = buffer; + self._destination = destination; + + self._commit(); + }); + }); +}; + +BlobWriteStream.prototype.end = function(data, encoding){ + + var self = this; + + function _end(){ + self.writable = false; + self._onEnd(); + } + + if(data){ + if(this.write(data, encoding)){ + _end(); + }else{ + self.writable = false; + this.once("drain", _end); + } + } + else{ + _end(); + } +}; + +BlobWriteStream.prototype.destroy = function(){ + this.writable = false; + this._pendingQueueDrain = false; + this._writeQueue = []; +}; + +BlobWriteStream.prototype.consume = function(consume){ + + this._buffer = this._buffer.slice(consume); + this._destinationPos -= consume; +}; + +BlobWriteStream.prototype.getCommittedSlice = function(){ + return this._buffer.slice(0, this._destinationPos); +}; + +function onFull(buffer, extraSize, callback){ + var newBuffer = new Buffer(buffer.length + extraSize); + buffer.copy(newBuffer); + callback(newBuffer, newBuffer.slice(buffer.length)); +} +*/ +exports.BlobReadStream = BlobReadStream; + +function BinaryStream(socket, id, create, meta) { + if (!(this instanceof BinaryStream)) return new BinaryStream(options); + + var self = this; + + Stream.call(this); + + + this.id = id; + this._socket = socket; + + this.writable = true; + this.readable = true; + this.paused = false; + + this._closed = false; + this._ended = false; + + if(create) { + // This is a stream we are creating + this._write(1, meta, this.id); + } +} + +util.inherits(BinaryStream, Stream); + + +BinaryStream.prototype._onDrain = function() { + if(!this.paused) { + this.emit('drain'); + } +}; + +BinaryStream.prototype._onClose = function() { + // Emit close event + if (this._closed) { + return; + } + this.readable = false; + this.writable = false; + this._closed = true; + this.emit('close'); +}; + +BinaryStream.prototype._onError = function(error){ + this.readable = false; + this.writable = false; + this.emit('error', error); +}; + +// Write stream + +BinaryStream.prototype._onPause = function() { + // Emit pause event + this.paused = true; + this.emit('pause'); +}; + +BinaryStream.prototype._onResume = function() { + // Emit resume event + this.paused = false; + this.emit('resume'); + this.emit('drain'); +}; + +BinaryStream.prototype._write = function(code, data, bonus) { + if (this._socket.readyState !== this._socket.constructor.OPEN) { + return false; + } + var message = util.pack([code, data, bonus]); + return this._socket.send(message) !== false; +}; + +BinaryStream.prototype.write = function(data) { + if(this.writable) { + var out = this._write(2, data, this.id); + return !this.paused && out; + } else { + this.emit('error', new Error('Stream is not writable')); + return false; + } +}; + +BinaryStream.prototype.end = function() { + this._ended = true; + this.readable = false; + this._write(5, null, this.id); +}; + +BinaryStream.prototype.destroy = BinaryStream.prototype.destroySoon = function() { + this._onClose(); + this._write(6, null, this.id); +}; + + +// Read stream + +BinaryStream.prototype._onEnd = function() { + if(this._ended) { + return; + } + this._ended = true; + this.readable = false; + this.emit('end'); +}; + +BinaryStream.prototype._onData = function(data) { + // Dispatch + this.emit('data', data); +}; + +BinaryStream.prototype.pause = function() { + this._onPause(); + this._write(3, null, this.id); +}; + +BinaryStream.prototype.resume = function() { + this._onResume(); + this._write(4, null, this.id); +}; + + +function BinaryClient(socket, options) { + if (!(this instanceof BinaryClient)) return new BinaryClient(socket, options); + + EventEmitter.call(this); + + var self = this; + + this._options = util.extend({ + chunkSize: 40960 + }, options); + + this.streams = {}; + + if(typeof socket === 'string') { + this._nextId = 0; + this._socket = new WebSocket(socket); + } else { + // Use odd numbered ids for server originated streams + this._nextId = 1; + this._socket = socket; + } + + this._socket.binaryType = 'arraybuffer'; + + this._socket.addEventListener('open', function(){ + self.emit('open'); + }); + this._socket.addEventListener('error', function(error){ + var ids = Object.keys(self.streams); + for (var i = 0, ii = ids.length; i < ii; i++) { + self.streams[ids[i]]._onError(error); + } + self.emit('error', error); + }); + this._socket.addEventListener('close', function(code, message){ + var ids = Object.keys(self.streams); + for (var i = 0, ii = ids.length; i < ii; i++) { + self.streams[ids[i]]._onClose(); + } + self.emit('close', code, message); + }); + this._socket.addEventListener('message', function(data, flags){ + util.setZeroTimeout(function(){ + + // Message format + // [type, payload, bonus ] + // + // Reserved + // [ 0 , X , X ] + // + // + // New stream + // [ 1 , Meta , new streamId ] + // + // + // Data + // [ 2 , Data , streamId ] + // + // + // Pause + // [ 3 , null , streamId ] + // + // + // Resume + // [ 4 , null , streamId ] + // + // + // End + // [ 5 , null , streamId ] + // + // + // Close + // [ 6 , null , streamId ] + // + + data = data.data; + + try { + data = util.unpack(data); + } catch (ex) { + return self.emit('error', new Error('Received unparsable message: ' + ex)); + } + if (!(data instanceof Array)) + return self.emit('error', new Error('Received non-array message')); + if (data.length != 3) + return self.emit('error', new Error('Received message with wrong part count: ' + data.length)); + if ('number' != typeof data[0]) + return self.emit('error', new Error('Received message with non-number type: ' + data[0])); + + switch(data[0]) { + case 0: + // Reserved + break; + case 1: + var meta = data[1]; + var streamId = data[2]; + var binaryStream = self._receiveStream(streamId); + self.emit('stream', binaryStream, meta); + break; + case 2: + var payload = data[1]; + var streamId = data[2]; + var binaryStream = self.streams[streamId]; + if(binaryStream) { + binaryStream._onData(payload); + } else { + self.emit('error', new Error('Received `data` message for unknown stream: ' + streamId)); + } + break; + case 3: + var streamId = data[2]; + var binaryStream = self.streams[streamId]; + if(binaryStream) { + binaryStream._onPause(); + } else { + self.emit('error', new Error('Received `pause` message for unknown stream: ' + streamId)); + } + break; + case 4: + var streamId = data[2]; + var binaryStream = self.streams[streamId]; + if(binaryStream) { + binaryStream._onResume(); + } else { + self.emit('error', new Error('Received `resume` message for unknown stream: ' + streamId)); + } + break; + case 5: + var streamId = data[2]; + var binaryStream = self.streams[streamId]; + if(binaryStream) { + binaryStream._onEnd(); + } else { + self.emit('error', new Error('Received `end` message for unknown stream: ' + streamId)); + } + break; + case 6: + var streamId = data[2]; + var binaryStream = self.streams[streamId]; + if(binaryStream) { + binaryStream._onClose(); + } else { + self.emit('error', new Error('Received `close` message for unknown stream: ' + streamId)); + } + break; + default: + self.emit('error', new Error('Unrecognized message type received: ' + data[0])); + } + }); + }); +} + +util.inherits(BinaryClient, EventEmitter); + +BinaryClient.prototype.send = function(data, meta){ + var stream = this.createStream(meta); + if(data instanceof Stream) { + data.pipe(stream); + } else if (util.isNode === true) { + if(Buffer.isBuffer(data)) { + (new BufferReadStream(data, {chunkSize: this._options.chunkSize})).pipe(stream); + } else { + stream.write(data); + } + } else if (util.isNode !== true) { + if(data.constructor == Blob || data.constructor == File) { + (new BlobReadStream(data, {chunkSize: this._options.chunkSize})).pipe(stream); + } else if (data.constructor == ArrayBuffer) { + var blob; + if(binaryFeatures.useArrayBufferView) { + data = new Uint8Array(data); + } + if(binaryFeatures.useBlobBuilder) { + var builder = new BlobBuilder(); + builder.append(data); + blob = builder.getBlob() + } else { + blob = new Blob([data]); + } + (new BlobReadStream(blob, {chunkSize: this._options.chunkSize})).pipe(stream); + } else if (typeof data === 'object' && 'BYTES_PER_ELEMENT' in data) { + var blob; + if(!binaryFeatures.useArrayBufferView) { + // Warn + data = data.buffer; + } + if(binaryFeatures.useBlobBuilder) { + var builder = new BlobBuilder(); + builder.append(data); + blob = builder.getBlob() + } else { + blob = new Blob([data]); + } + (new BlobReadStream(blob, {chunkSize: this._options.chunkSize})).pipe(stream); + } else { + stream.write(data); + } + } + return stream; +}; + +BinaryClient.prototype._receiveStream = function(streamId){ + var self = this; + var binaryStream = new BinaryStream(this._socket, streamId, false); + binaryStream.on('close', function(){ + delete self.streams[streamId]; + }); + this.streams[streamId] = binaryStream; + return binaryStream; +}; + +BinaryClient.prototype.createStream = function(meta){ + if(this._socket.readyState !== WebSocket.OPEN) { + throw new Error('Client is not yet connected or has closed'); + return; + } + var self = this; + var streamId = this._nextId; + this._nextId += 2; + var binaryStream = new BinaryStream(this._socket, streamId, true, meta); + binaryStream.on('close', function(){ + delete self.streams[streamId]; + }); + this.streams[streamId] = binaryStream; + return binaryStream; +}; + +BinaryClient.prototype.close = BinaryClient.prototype.destroy = function() { + this._socket.close(); +}; + +exports.BinaryClient = BinaryClient; + +})(this); + +</script> +\ No newline at end of file diff --git a/app/elements/p2p-network/connection-wrapper.html b/app/elements/p2p-network/connection-wrapper.html @@ -0,0 +1,59 @@ +<link rel="import" href="p2p-network.html"> +<link rel="import" href="web-socket.html"> +<dom-module id="connection-wrapper"> + <template> + <p2p-network id="p2p" me="{{me}}"></p2p-network> + <web-socket id="ws" me="{{me}}"></web-socket> + </template> + <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; + + function rtcConnectionSupported(peerId) { + return webRTCSupported && (peerId.indexOf('rtc_') === 0); + } + Polymer({ + is: 'connection-wrapper', + properties: { + me: { + notify: true, + value: (webRTCSupported ? 'rtc_' : 'ws_') + guid() + } + }, + behaviors: [Chat.FileTransferProtocol], + _sendFile: function(toPeer, file) { + if (!rtcConnectionSupported(toPeer)) { + this.$.ws._sendFile(toPeer, file); + } else { + this.$.p2p._sendFile(toPeer, file); + } + }, + _sendSystemEvent: function(toPeer, event) { + console.log('system event', toPeer, event); + if (!rtcConnectionSupported(toPeer)) { + this.$.ws._sendSystemEvent(toPeer, event); + } else { + this.$.p2p._sendSystemEvent(toPeer, event); + } + }, + connectToPeer: function(toPeer, callback) { + if (!rtcConnectionSupported(toPeer)) { + callback(); + } else { + this.$.p2p.connectToPeer(toPeer,callback); + } + }, + }); + })(); + </script> +</dom-module> diff --git a/app/elements/p2p-network/file-transfer-protocol.html b/app/elements/p2p-network/file-transfer-protocol.html @@ -0,0 +1,137 @@ +<script> +'use strict'; +window.Chat = window.Chat || {}; +Chat.FileTransferProtocol = { + properties: { + loading: { + type: Boolean, + notify: true, + value: false, + observer: '_loadingChanged' + }, + buddies: { + notify: true + } + }, + listeners: { + 'system-event': '_onSystemMsg', + 'file-received': '_onFileReceived', + }, + _onSystemMsg: function(event) { + var msg = event.detail; + console.log('FTP received sysMsg:', msg); + + switch (msg.type) { + case 'offer': + this._onOffered(msg); + break; + case 'decline': + this._onDeclined(msg); + break; + case 'accept': + this._onAccepted(msg); + break; + case 'transfer': + this._onTransfer(msg); + break; + case 'received': + this._onReceived(msg); + break; + case 'buddies': + this._onBuddies(msg); + break; + } + }, + sendFile: function(peerId, file) { + this.set('loading', true); + this.fileToSend = file; + this.fire('file-offered', { + to: peerId + }); + this.connectToPeer(peerId, function() { + this._offer(peerId, file); + }.bind(this)); + + //set 15sec timeout + this._timeoutTimer = this.async(function() { + this._onError(); + }, 15000); + }, + _offer: function(toPeer, file) { + console.log('FTP offer file:', file, 'To:', toPeer); + + this._sendSystemEvent(toPeer, { + type: 'offer', + name: file.name + }); + }, + _onOffered: function(offer) { + console.log('FTP offered file:', offer.name, 'From:', offer.from); + this.fire('file-offer', { + from: offer.from, + name: offer.name + }); + }, + decline: function(offer) { + this._sendSystemEvent(offer.from, { + type: 'decline', + name: offer.name + }); + }, + _onDeclined: function(offer) { + this.cancelAsync(this._timeoutTimer); + delete this.fileToSend; + this.set('loading', false); + this.fire('file-declined', offer); + }, + accept: function(offer) { + this._sendSystemEvent(offer.from, { + type: 'accept', + name: offer.name + }); + this.fire('download-started', { + from: offer.from + }); + }, + _onAccepted: function(offer) { + this.cancelAsync(this._timeoutTimer); + this._sendSystemEvent(offer.from, { + type: 'transfer', + name: offer.name + }); + this.fire('upload-started', { + to: offer.from + }); + this._sendFile(offer.from, this.fileToSend); + }, + _onTransfer: function() { + this.loading = true; + }, + _onFileReceived: function(event) { + var file = event.detail; + this.loading = false; + this._sendSystemEvent(file.from, { + type: 'received', + name: file.name + }); + this.fire('download-complete', { + from: file.from + }); + console.log('FTP received:', file); + }, + _onReceived: function(offer) { + this.loading = false; + this.fire('upload-complete', offer); + }, + _onError: function() { + this.loading = false; + this.fire('upload-error'); + }, + _loadingChanged: function(loading) { + window.anim(loading); + }, + _onBuddies: function(msg) { + this.set('buddies', msg.buddies); + } +}; +</script> diff --git a/app/elements/p2p-network/p2p-network.html b/app/elements/p2p-network/p2p-network.html @@ -1,4 +1,5 @@ -<script src="../../../bower_components/peerjs/peer.min.js"></script> +<script src="../../bower_components/peerjs/peer.min.js"></script> +<link rel="import" href="file-transfer-protocol.html"> <dom-module id="p2p-network"> <template> </template> @@ -30,7 +31,7 @@ path: 'peerjs', secure: true }; - this._peer = new Peer(options); + this._peer = new Peer(this.me,options); this._peer.on('open', function(id) { console.log('My peer ID is: ' + id); this.set('me', id); @@ -65,12 +66,22 @@ if (c.label === 'file') { c.on('data', function(data) { - console.log('received!', data); + console.log(data); + var dataView = new Uint8Array(data.file); + var dataBlob = new Blob([dataView]); this.fire('file-received', { - peer: peer, - dataURI: data.dataURI, + from: peer, + blob: dataBlob, name: data.name, }); + + }.bind(this)); + } + + if (c.label === 'system') { + c.on('data', function(data) { + data.from = peer; + this.fire('system-event', data); }.bind(this)); } }, @@ -78,15 +89,32 @@ function request(requestedPeer, callback) { return function() { + //system messages channel + var s = this._peer.connect(requestedPeer, { + label: 'system' + }); + + s.on('open', function() { + this.connect(s); + if (callback) { + callback(); + } + }.bind(this)); + s.on('error', function(err) { + console.log(err); + if (err.message.indexOf('Connection is not open') > -1) { + console.err('Handle this error!!'); + } + }); + + //files channel var f = this._peer.connect(requestedPeer, { label: 'file', reliable: true }); f.on('open', function() { this.connect(f); - if (callback) { - callback(); - } + }.bind(this)); f.on('error', function(err) { console.log(err); @@ -98,7 +126,6 @@ callback(); return; } - this.set('loading', true); if (this._peerOpen) { request(requestedPeer, callback).bind(this)(); } else { @@ -107,7 +134,7 @@ }; }()), - sendFile: function(peerId, file) { + _sendFile: function(peerId, file) { var conns = this._peer.connections[peerId]; if (conns) { conns.forEach(function(conn) { @@ -115,7 +142,17 @@ conn.send(file); console.log('file send'); } - }); + }.bind(this)); + } + }, + _sendSystemEvent: function(peerId, msg) { + var conns = this._peer.connections[peerId]; + if (conns) { + conns.forEach(function(conn) { + if (conn.label === 'system') { + conn.send(msg); + } + }.bind(this)); } } }); diff --git a/app/elements/p2p-network/web-socket.html b/app/elements/p2p-network/web-socket.html @@ -0,0 +1,82 @@ +<link rel="import" href="binaryjs.html"> +<dom-module id="web-socket"> + <template> + <style> + :host { + display: block; + } + </style> + </template> + <script> + 'use strict'; + Polymer({ + is: 'web-socket', + attached: function() { + this.init(); + }, + init: function() { + var websocketUrl = (window.location.protocol === 'https:' ? 'wss://' : 'ws://') + document.location.hostname + ':9001'; + 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); + if (data.isSystemEvent) { + if (meta) { + data.from = meta.from; + } + this.fire('system-event', data); + } else { + parts.push(data); + } + }.bind(this)); + // when finished, set it as the background image + stream.on('end', function() { + var blob = new Blob(parts, { + type: meta.type + }); + console.log('file received', blob, meta); + this.fire('file-received', { + blob: blob, + name: meta.name, + from: meta.from + }); + }.bind(this)); + }.bind(this)); + this.client.on('open', function(e) { + console.log(e); + this.client.send({}, { + handshake: this.me + }); + }.bind(this)); + this.client.on('error', function(e) { + console.log(e); + }); + this.client.on('close', function(e) { + console.log(e); + //try to reconnect after 3s + this.async(this.init, 3000); + }.bind(this)); + }, + _sendFile: function(toPeer, file) { + console.log('send file!', file); + this.client.send(file.file, { + name: file.file.name, + type: file.file.type, + toPeer: toPeer + }); + }, + connectToPeer: function(peer, callback) { + callback(); + }, + _sendSystemEvent: function(toPeer, event) { + console.log('system event', toPeer, event); + event.isSystemEvent = true; + this.client.send(event, { + toPeer: toPeer + }); + } + }); + </script> +</dom-module> diff --git a/app/favicon.ico b/app/favicon.ico Binary files differ. diff --git a/app/images/touch/apple-touch-icon.png b/app/images/touch/apple-touch-icon.png Binary files differ. diff --git a/app/images/touch/chrome-splashscreen-icon-384x384.png b/app/images/touch/chrome-splashscreen-icon-384x384.png Binary files differ. diff --git a/app/images/touch/chrome-touch-icon-192x192.png b/app/images/touch/chrome-touch-icon-192x192.png Binary files differ. diff --git a/app/images/touch/icon-128x128.png b/app/images/touch/icon-128x128.png Binary files differ. diff --git a/app/images/touch/logo.png b/app/images/touch/logo.png Binary files differ. diff --git a/app/images/touch/ms-icon-144x144.png b/app/images/touch/ms-icon-144x144.png Binary files differ. diff --git a/app/images/touch/ms-touch-icon-144x144-precomposed.png b/app/images/touch/ms-touch-icon-144x144-precomposed.png Binary files differ. diff --git a/app/index.html b/app/index.html @@ -4,12 +4,12 @@ <head> <meta charset="utf-8"> <meta name="description" content=""> - <meta name="viewport" content="width=device-width, initial-scale=1"> - <meta name="generator" content="Share With Me!"> - <title>Share With Me!</title> + <meta name="viewport" content="initial-scale=1,user-scalable=no,maximum-scale=1"> + <meta name="generator" content="SnapDrop!"> + <title>SnapDrop!</title> <!-- Place favicon.ico in the `app/` directory --> <!-- Chrome for Android theme color --> - <meta name="theme-color" content="#2E3AA1"> + <meta name="theme-color" content="#3367d6"> <!-- Web Application Manifest --> <link rel="manifest" href="manifest.json"> <!-- Tile color for Win8 --> @@ -21,7 +21,7 @@ <!-- 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="Share With Me!"> + <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,23 +29,23 @@ <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"></script> + <script src="bower_components/webcomponentsjs/webcomponents-lite.js" async></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"> - <!-- For shared styles, shared-styles.html import in elements.html --> - <style is="custom-style" include="shared-styles"></style> + <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 unresolved class="fullbleed layout vertical"> +<body class="fullbleed layout vertical" loading> + <script src="scripts/animated-bg.js" inline></script> <span id="browser-sync-binding"></span> <template is="dom-bind" id="app"> - <buddy-finder me="{{me}}"></buddy-finder> - <p2p-network me="{{me}}"></p2p-network> + <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> <file-receiver></file-receiver> - <paper-toast id="toast"> - <span class="toast-hide-button" role="button" tabindex="0" onclick="app.$.toast.hide()">Ok</span> + <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."> diff --git a/app/manifest.json b/app/manifest.json @@ -1,28 +1,30 @@ { - "name": "Share With Me", - "short_name": "Share With Me", - "icons": [{ + "name": "SnapDrop", + "short_name": "SnapDrop", + "icons": [{ "src": "images/touch/icon-128x128.png", "sizes": "128x128", "type": "image/png" - }, { + }, { "src": "images/touch/apple-touch-icon.png", "sizes": "152x152", "type": "image/png" - }, { + }, { "src": "images/touch/ms-touch-icon-144x144-precomposed.png", "sizes": "144x144", "type": "image/png" - }, { + }, { "src": "images/touch/chrome-touch-icon-192x192.png", "sizes": "192x192", "type": "image/png" - },{ + }, { "src": "images/touch/chrome-splashscreen-icon-384x384.png", "sizes": "384x384", "type": "image/png" - }], - "background_color": "#3E4EB8", - "display": "standalone", - "theme_color": "#2E3AA1" + }], + "background_color": "#3367d6", + "start_url": "index.html", + "display": "standalone", + "theme_color": "#3367d6", + "orientation": "portrait" } diff --git a/app/scripts/animated-bg.js b/app/scripts/animated-bg.js @@ -0,0 +1,64 @@ +'use strict'; +(function() { + var requestAnimFrame = (function() { + return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || + function(callback) { + window.setTimeout(callback, 1000 / 60); + }; + })(); + var c = document.createElement('canvas'); + document.body.appendChild(c); + var style = c.style; + style.width = '100%'; + style.position = 'absolute'; + var ctx = c.getContext('2d'); + var x0, y0, w, h, dw; + + function init() { + w = window.innerWidth; + h = window.innerHeight; + c.width = w; + c.height = h; + x0 = w / 2; + y0 = h - 103; + dw = Math.max(w, h, 1000) / 13; + drawCircles(); + } + window.onresize = init; + + function drawCicrle(radius) { + ctx.beginPath(); + var color = Math.round(255 * (1- radius / Math.max(w, h))); + ctx.strokeStyle = 'rgba(' + color + ',' + color + ',' + color + ',0.1)'; + ctx.arc(x0, y0, radius, 0, 2 * Math.PI); + ctx.stroke(); + ctx.lineWidth = 2; + } + + var step = 0; + + function drawCircles() { + ctx.clearRect(0, 0, w, h); + for (var i = 0; i < 8; i++) { + drawCicrle(dw * i + step % dw); + } + step += 1; + } + + var loading = true; + + function animate() { + if (loading || step % dw < dw - 5) { + requestAnimFrame(function() { + drawCircles(); + animate(); + }); + } + } + window.anim = function(l) { + loading = l; + animate(); + }; + init(); + animate(); +}()); diff --git a/app/scripts/app.js b/app/scripts/app.js @@ -21,17 +21,25 @@ } }; + app.displayToast = function(msg) { + var toast = Polymer.dom(document).querySelector('#toast'); + toast.text = msg; + toast.show(); + }; + // Listen for template bound event to know when bindings // have resolved and content has been stamped to the page app.addEventListener('dom-change', function() { console.log('Our app is ready to rock!'); + app.p2p = document.querySelector('connection-wrapper'); }); // See https://github.com/Polymer/polymer/issues/1381 window.addEventListener('WebComponentsReady', function() { // imports are loaded and elements have been registered - app.p2p = document.querySelector('p2p-network'); }); + + })(document); diff --git a/app/styles/app-theme.html b/app/styles/app-theme.html @@ -1,214 +1,31 @@ -<!-- -@license -Copyright (c) 2015 The Polymer Project Authors. All rights reserved. -This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt -The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt -The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt -Code distributed by Google as part of the polymer project is also -subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt ---> - <link rel="import" href="../bower_components/polymer/polymer.html"> - <style is="custom-style"> - - /* - Polymer includes a shim for CSS Custom Properties that we can use for application theming. - Below, you'll find the default palette for the Share With Me layout. Feel free to play - with changing the colors used or generate your own palette of colours at MaterialPalette.com. - - See https://www.polymer-project.org/1.0/docs/devguide/styling.html#xscope-styling-details - for further information on custom CSS properties. - */ - - /* Application theme */ - - :root { +:root { --dark-primary-color: #303F9F; --default-primary-color: #3F51B5; --light-primary-color: #C5CAE9; - --text-primary-color: #ffffff; /*text/icons*/ + --text-primary-color: #ffffff; + /*text/icons*/ --accent-color: #FF4081; --primary-background-color: #c5cae9; --primary-text-color: #212121; --secondary-text-color: #727272; --disabled-text-color: #bdbdbd; --divider-color: #B6B6B6; - /* Components */ - /* paper-drawer-panel */ --drawer-menu-color: #ffffff; --drawer-border-color: 1px solid #ccc; --drawer-toolbar-border-color: 1px solid rgba(0, 0, 0, 0.22); - /* paper-menu */ --paper-menu-background-color: #fff; --menu-link-color: #111111; - } - - /* General styles */ - - #drawerToolbar { - color: var(--secondary-text-color); - background-color: var(--drawer-menu-color); - border-bottom: var(--drawer-toolbar-border-color); - } - - paper-scroll-header-panel { - height: 100%; - } - - paper-material { - border-radius: 2px; - height: 100%; - padding: 16px 0 16px 0; - width: calc(98.66% - 16px); - margin: 16px auto; - background: white; - } - - paper-menu iron-icon { - margin-right: 33px; - opacity: 0.54; - } - - .paper-menu > .iron-selected { - color: var(--default-primary-color); - } - - paper-menu a { - @apply(--layout-horizontal); - @apply(--layout-center); - text-decoration: none; - color: var(--menu-link-color); - font-family: 'Roboto', 'Noto', sans-serif; - -webkit-font-smoothing: antialiased; - text-rendering: optimizeLegibility; - font-size: 14px; - font-weight: 400; - line-height: 24px; - min-height: 48px; - padding: 0 16px; - } - - paper-toolbar.tall .app-name { - font-size: 40px; - font-weight: 300; - /* Required for main area's paper-scroll-header-panel custom condensing transformation */ - -webkit-transform-origin: left center; - transform-origin: left center; - } - - #mainToolbar .middle-container { - height: 100%; - margin-left: 48px; - } - - #mainToolbar:not(.tall) .middle { - font-size: 18px; - padding-bottom: 0; - } - - #mainToolbar .bottom { - margin-left: 48px; - /* Required for main area's paper-scroll-header-panel custom condensing transformation */ - -webkit-transform-origin: left center; - transform-origin: left center; - } - - /* Height of the scroll area */ - .content { - height: 900px; - } - - #toast .toast-hide-button { - color: #eeff41; - margin: 10px; - } - - /* Breakpoints */ - - /* Small */ - @media (max-width: 600px) { - - paper-material { - --menu-container-display: none; - width: calc(97.33% - 32px); - padding-left: 16px; - padding-right: 16px; - } - - paper-toolbar.tall .app-name { - font-size: 24px; - font-weight: 400; - } - - #drawer .paper-toolbar { - margin-left: 16px; - } - - } - - /* Tablet+ */ - @media (min-width: 601px) { - - paper-material { - width: calc(98% - 46px); - margin-bottom: 32px; - padding-left: 30px; - padding-right: 30px; - } - - #drawer.paper-drawer-panel > [drawer] { - border-right: 1px solid rgba(0, 0, 0, 0.14); - } - - iron-pages { - padding: 48px 62px; - } - - } - - /* Material Design Adaptive Breakpoints */ - /* - Below you'll find CSS media queries based on the breakpoint guidance - published by the Material Design team. You can choose to use, customise - or remove these breakpoints based on your needs. - - http://www.google.com/design/spec/layout/adaptive-ui.html#adaptive-ui-breakpoints - */ - - /* mobile-small */ - @media all and (min-width: 0) and (max-width: 360px) and (orientation: portrait) { } - /* mobile-large */ - @media all and (min-width: 361px) and (orientation: portrait) { } - /* mobile-small-landscape */ - @media all and (min-width: 0) and (max-width: 480px) and (orientation: landscape) { } - /* mobile-large-landscape */ - @media all and (min-width: 481px) and (orientation: landscape) { } - /* tablet-small-landscape */ - @media all and (min-width: 600px) and (max-width: 960px) and (orientation: landscape) { } - /* tablet-large-landscape */ - @media all and (min-width: 961px) and (orientation: landscape) { } - /* tablet-small */ - @media all and (min-width: 600px) and (orientation: portrait) { } - /* tablet-large */ - @media all and (min-width: 601px) and (max-width: 840px) and (orientation : portrait) { } - /* desktop-x-small-landscape */ - @media all and (min-width: 0) and (max-width: 480px) and (orientation: landscape) { } - /* desktop-x-small */ - @media all and (min-width: 0) and (max-width: 480px) and (max-aspect-ratio: 4/3) { } - /* desktop-small-landscape */ - @media all and (min-width: 481px) and (max-width: 840px) and (orientation: landscape) { } - /* desktop-small */ - @media all and (min-width: 481px) and (max-width: 840px) and (max-aspect-ratio: 4/3) { } - /* desktop-medium-landscape */ - @media all and (min-width: 841px) and (max-width: 1280px) and (orientation: landscape) { } - /* desktop-medium */ - @media all and (min-width: 841px) and (max-width: 1280px) and (max-aspect-ratio: 4/3) { } - /* desktop-large */ - @media all and (min-width: 1281px) and (max-width: 1600px) { } - /* desktop-xlarge */ - @media all and (min-width: 1601px) and (max-width: 1920px) { } +} + +paper-progress { + width: 100%; + z-index: 1; + position: absolute; + top: 0; +} </style> diff --git a/app/styles/icons.html b/app/styles/icons.html @@ -0,0 +1,37 @@ +<link rel="import" href="../bower_components/iron-iconset-svg/iron-iconset-svg.html"> +<iron-iconset-svg name="chat" size="24"> + <svg> + <defs> + <g id="notifications-off"> + <path d="M11.5 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zM18 10.5c0-3.07-2.13-5.64-5-6.32V3.5c0-.83-.67-1.5-1.5-1.5S10 2.67 10 3.5v.68c-.51.12-.99.32-1.45.56L18 14.18V10.5zm-.27 8.5l2 2L21 19.73 4.27 3 3 4.27l2.92 2.92C5.34 8.16 5 9.29 5 10.5V16l-2 2v1h14.73z" /> + </g> + <g id="share"> + <path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92 1.61 0 2.92-1.31 2.92-2.92s-1.31-2.92-2.92-2.92z" /> + </g> + <g id="call"> + <path d="M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1 .45 1 1V20c0 .55-.45 1-1 1-9.39 0-17-7.61-17-17 0-.55.45-1 1-1h3.5c.55 0 1 .45 1 1 0 1.25.2 2.45.57 3.57.11.35.03.74-.25 1.02l-2.2 2.2z" /> + </g> + <g id="wifi-tethering"> + <path d="M12 11c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm6 2c0-3.31-2.69-6-6-6s-6 2.69-6 6c0 2.22 1.21 4.15 3 5.19l1-1.74c-1.19-.7-2-1.97-2-3.45 0-2.21 1.79-4 4-4s4 1.79 4 4c0 1.48-.81 2.75-2 3.45l1 1.74c1.79-1.04 3-2.97 3-5.19zM12 3C6.48 3 2 7.48 2 13c0 3.7 2.01 6.92 4.99 8.65l1-1.73C5.61 18.53 4 15.96 4 13c0-4.42 3.58-8 8-8s8 3.58 8 8c0 2.96-1.61 5.53-4 6.92l1 1.73c2.99-1.73 5-4.95 5-8.65 0-5.52-4.48-10-10-10z" /> + </g> + <g id="attach-file"> + <path d="M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5c0-1.38 1.12-2.5 2.5-2.5s2.5 1.12 2.5 2.5v10.5c0 .55-.45 1-1 1s-1-.45-1-1V6H10v9.5c0 1.38 1.12 2.5 2.5 2.5s2.5-1.12 2.5-2.5V5c0-2.21-1.79-4-4-4S7 2.79 7 5v12.5c0 3.04 2.46 5.5 5.5 5.5s5.5-2.46 5.5-5.5V6h-1.5z" /> + </g> + <g id="desktop-mac"> + <path d="M21 2H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h7l-2 3v1h8v-1l-2-3h7c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 12H3V4h18v10z" /> + </g> + <g id="desktop-windows"> + <path d="M21 2H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h7v2H8v2h8v-2h-2v-2h7c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H3V4h18v12z" /> + </g> + <g id="smartphone"> + <path d="M17 1.01L7 1c-1.1 0-2 .9-2 2v18c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V3c0-1.1-.9-1.99-2-1.99zM17 19H7V5h10v14z" /> + </g> + <g id="phone-iphone"> + <path d="M15.5 1h-8C6.12 1 5 2.12 5 3.5v17C5 21.88 6.12 23 7.5 23h8c1.38 0 2.5-1.12 2.5-2.5v-17C18 2.12 16.88 1 15.5 1zm-4 21c-.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.5zm4.5-4H7V4h9v14z" /> + </g> + <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> + </defs> + </svg> +</iron-iconset-svg> diff --git a/app/styles/main.css b/app/styles/main.css @@ -1,14 +1,12 @@ -/* -Copyright (c) 2015 The Polymer Project Authors. All rights reserved. -This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt -The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt -The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt -Code distributed by Google as part of the polymer project is also -subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt -*/ +html, +body { + height: 100%; + width: 100%; +} body { - background: #fafafa; - font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif; - color: #333; + background: #fafafa; + font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif; + color: #333; + -webkit-font-smoothing: antialiased; } diff --git a/app/styles/shared-styles.html b/app/styles/shared-styles.html @@ -1,23 +0,0 @@ -<link rel="import" href="../bower_components/polymer/polymer.html"> -<link rel="import" href="../bower_components/paper-styles/typography.html"> - -<!-- shared styles for all elements and index.html --> -<dom-module id="shared-styles"> - <template> - <style> - .page-title { - @apply(--paper-font-display2); - } - - paper-menu a > *, paper-menu paper-item > *, paper-menu paper-icon-item > * { - pointer-events: none; - } - - @media (max-width: 600px) { - .page-title { - font-size: 24px!important; - } - } - </style> - </template> -</dom-module> diff --git a/gulpfile.js b/gulpfile.js @@ -1,5 +1,3 @@ - - 'use strict'; // Include Gulp & tools we'll use @@ -17,175 +15,185 @@ var historyApiFallback = require('connect-history-api-fallback'); var packageJson = require('./package.json'); var crypto = require('crypto'); var ensureFiles = require('./tasks/ensure-files.js'); +var inlinesource = require('gulp-inline-source'); // var ghPages = require('gulp-gh-pages'); var AUTOPREFIXER_BROWSERS = [ - 'ie >= 10', - 'ie_mob >= 10', - 'ff >= 30', - 'chrome >= 34', - 'safari >= 7', - 'opera >= 23', - 'ios >= 7', - 'android >= 4.4', - 'bb >= 10' + 'ie >= 10', + 'ie_mob >= 10', + 'ff >= 30', + 'chrome >= 34', + 'safari >= 7', + 'opera >= 23', + 'ios >= 7', + 'android >= 4.4', + 'bb >= 10' ]; var DIST = 'dist'; var dist = function(subpath) { - return !subpath ? DIST : path.join(DIST, subpath); + return !subpath ? DIST : path.join(DIST, subpath); }; var styleTask = function(stylesPath, srcs) { - return gulp.src(srcs.map(function(src) { - return path.join('app', stylesPath, src); - })) - .pipe($.changed(stylesPath, {extension: '.css'})) - .pipe($.autoprefixer(AUTOPREFIXER_BROWSERS)) - .pipe(gulp.dest('.tmp/' + stylesPath)) - .pipe($.minifyCss()) - .pipe(gulp.dest(dist(stylesPath))) - .pipe($.size({title: stylesPath})); + return gulp.src(srcs.map(function(src) { + return path.join('app', stylesPath, src); + })) + .pipe($.changed(stylesPath, { + extension: '.css' + })) + .pipe($.autoprefixer(AUTOPREFIXER_BROWSERS)) + .pipe(gulp.dest('.tmp/' + stylesPath)) + .pipe($.minifyCss()) + .pipe(gulp.dest(dist(stylesPath))) + .pipe($.size({ + title: stylesPath + })); }; var imageOptimizeTask = function(src, dest) { - return gulp.src(src) - .pipe($.imagemin({ - progressive: true, - interlaced: true - })) - .pipe(gulp.dest(dest)) - .pipe($.size({title: 'images'})); + return gulp.src(src) + .pipe($.imagemin({ + progressive: true, + interlaced: true + })) + .pipe(gulp.dest(dest)) + .pipe($.size({ + title: 'images' + })); }; var optimizeHtmlTask = function(src, dest) { - var assets = $.useref.assets({ - searchPath: ['.tmp', 'app'] - }); - - return gulp.src(src) - .pipe(assets) - // Concatenate and minify JavaScript - .pipe($.if('*.js', $.uglify({ - preserveComments: 'some' - }))) - // Concatenate and minify styles - // In case you are still using useref build blocks - .pipe($.if('*.css', $.minifyCss())) - .pipe(assets.restore()) - .pipe($.useref()) - // Minify any HTML - .pipe($.if('*.html', $.minifyHtml({ - quotes: true, - empty: true, - spare: true - }))) - // Output files - .pipe(gulp.dest(dest)) - .pipe($.size({ - title: 'html' - })); + var assets = $.useref.assets({ + searchPath: ['.tmp', 'app'] + }); + + return gulp.src(src) + .pipe(assets) + // Concatenate and minify JavaScript + .pipe($.if('*.js', $.uglify({ + preserveComments: 'some' + }))) + // Concatenate and minify styles + // In case you are still using useref build blocks + .pipe($.if('*.css', $.minifyCss())) + .pipe(assets.restore()) + .pipe($.useref()) + // Minify any HTML + .pipe($.if('*.html', $.minifyHtml({ + quotes: true, + empty: true, + spare: true + }))) + .pipe($.if('*.html', inlinesource())) + // Output files + .pipe(gulp.dest(dest)) + .pipe($.size({ + title: 'html' + })); }; // Compile and automatically prefix stylesheets gulp.task('styles', function() { - return styleTask('styles', ['**/*.css']); + return styleTask('styles', ['**/*.css']); }); gulp.task('elements', function() { - return styleTask('elements', ['**/*.css']); + return styleTask('elements', ['**/*.css']); }); // Ensure that we are not missing required files for the project // "dot" files are specifically tricky due to them being hidden on // some systems. gulp.task('ensureFiles', function(cb) { - var requiredFiles = ['.jscsrc', '.jshintrc', '.bowerrc']; + var requiredFiles = ['.jscsrc', '.jshintrc', '.bowerrc']; - ensureFiles(requiredFiles.map(function(p) { - return path.join(__dirname, p); - }), cb); + ensureFiles(requiredFiles.map(function(p) { + return path.join(__dirname, p); + }), cb); }); // Lint JavaScript gulp.task('lint', ['ensureFiles'], function() { - return gulp.src([ - 'app/scripts/**/*.js', - 'app/elements/**/*.js', - 'app/elements/**/*.html', - 'gulpfile.js' - ]) - .pipe(reload({ - stream: true, - once: true - })) - - // JSCS has not yet a extract option - .pipe($.if('*.html', $.htmlExtract())) - .pipe($.jshint()) - .pipe($.jscs()) - .pipe($.jscsStylish.combineWithHintResults()) - .pipe($.jshint.reporter('jshint-stylish')) - .pipe($.if(!browserSync.active, $.jshint.reporter('fail'))); + return gulp.src([ + 'app/scripts/**/*.js', + 'app/elements/**/*.js', + 'app/elements/**/*.html', + 'gulpfile.js' + ]) + .pipe(reload({ + stream: true, + once: true + })) + + // JSCS has not yet a extract option + .pipe($.if('*.html', $.htmlExtract())) + .pipe($.jshint()) + .pipe($.jscs()) + .pipe($.jscsStylish.combineWithHintResults()) + .pipe($.jshint.reporter('jshint-stylish')) + .pipe($.if(!browserSync.active, $.jshint.reporter('fail'))); }); // Optimize images gulp.task('images', function() { - return imageOptimizeTask('app/images/**/*', dist('images')); + return imageOptimizeTask('app/images/**/*', dist('images')); }); // Copy all files at the root level (app) gulp.task('copy', function() { - var app = gulp.src([ - 'app/*', - '!app/test', - '!app/elements', - '!app/bower_components', - '!app/cache-config.json' - ], { - dot: true - }).pipe(gulp.dest(dist())); - - // Copy over only the bower_components we need - // These are things which cannot be vulcanized - var bower = gulp.src([ - 'app/bower_components/{webcomponentsjs,platinum-sw,sw-toolbox,promise-polyfill}/**/*' - ]).pipe(gulp.dest(dist('bower_components'))); - - return merge(app, bower) - .pipe($.size({ - title: 'copy' - })); + var app = gulp.src([ + 'app/*', + '!app/test', + '!app/elements', + '!app/bower_components', + '!app/cache-config.json' + ], { + dot: true + }).pipe(gulp.dest(dist())); + + // Copy over only the bower_components we need + // These are things which cannot be vulcanized + var bower = gulp.src([ + 'app/bower_components/{webcomponentsjs,platinum-sw,sw-toolbox,promise-polyfill}/**/*' + ]).pipe(gulp.dest(dist('bower_components'))); + + return merge(app, bower) + .pipe($.size({ + title: 'copy' + })); }); // Copy web fonts to dist gulp.task('fonts', function() { - return gulp.src(['app/fonts/**']) - .pipe(gulp.dest(dist('fonts'))) - .pipe($.size({ - title: 'fonts' - })); + return gulp.src(['app/fonts/**']) + .pipe(gulp.dest(dist('fonts'))) + .pipe($.size({ + title: 'fonts' + })); }); // Scan your HTML for assets & optimize them gulp.task('html', function() { - return optimizeHtmlTask( - ['app/**/*.html', '!app/{elements,test,bower_components}/**/*.html'], - dist()); + return optimizeHtmlTask( + ['app/**/*.html', '!app/{elements,test,bower_components}/**/*.html'], + dist()); }); // Vulcanize granular configuration gulp.task('vulcanize', function() { - return gulp.src('app/elements/elements.html') - .pipe($.vulcanize({ - stripComments: true, - inlineCss: true, - inlineScripts: true - })) - .pipe(gulp.dest(dist('elements'))) - .pipe($.size({title: 'vulcanize'})); + return gulp.src('app/elements/elements.html') + .pipe($.vulcanize({ + stripComments: true, + inlineCss: true, + inlineScripts: true + })) + .pipe(gulp.dest(dist('elements'))) + .pipe($.size({ + title: 'vulcanize' + })); }); // Generate config data for the <sw-precache-cache> element. @@ -196,121 +204,123 @@ gulp.task('vulcanize', function() { // See https://github.com/PolymerElements/polymer-starter-kit#enable-service-worker-support // for more context. gulp.task('cache-config', function(callback) { - var dir = dist(); - var config = { - cacheId: packageJson.name || path.basename(__dirname), - disabled: false - }; - - glob([ - 'index.html', - './', - 'bower_components/webcomponentsjs/webcomponents-lite.min.js', - '{elements,scripts,styles}/**/*.*'], - {cwd: dir}, function(error, files) { - if (error) { - callback(error); - } else { - config.precache = files; - - var md5 = crypto.createHash('md5'); - md5.update(JSON.stringify(config.precache)); - config.precacheFingerprint = md5.digest('hex'); - - var configPath = path.join(dir, 'cache-config.json'); - fs.writeFile(configPath, JSON.stringify(config), callback); - } - }); + var dir = dist(); + var config = { + cacheId: packageJson.name || path.basename(__dirname), + disabled: false + }; + + glob([ + 'index.html', + './', + 'bower_components/webcomponentsjs/webcomponents-lite.min.js', + '{elements,scripts,styles}/**/*.*' + ], { + cwd: dir + }, function(error, files) { + if (error) { + callback(error); + } else { + config.precache = files; + + var md5 = crypto.createHash('md5'); + md5.update(JSON.stringify(config.precache)); + config.precacheFingerprint = md5.digest('hex'); + + var configPath = path.join(dir, 'cache-config.json'); + fs.writeFile(configPath, JSON.stringify(config), callback); + } + }); }); // Clean output directory gulp.task('clean', function() { - return del(['.tmp', dist()]); + return del(['.tmp', dist()]); }); // Watch files for changes & reload -gulp.task('serve', [ 'styles', 'elements', 'images'], function() { - browserSync({ - port: 5000, - notify: false, - logPrefix: 'PSK', - snippetOptions: { - rule: { - match: '<span id="browser-sync-binding"></span>', - fn: function(snippet) { - return snippet; +gulp.task('serve', ['styles', 'elements', 'images'], function() { + browserSync({ + port: 5000, + notify: false, + logPrefix: 'PSK', + ghostMode: false, + snippetOptions: { + rule: { + match: '<span id="browser-sync-binding"></span>', + fn: function(snippet) { + return snippet; + } + } + }, + // Run as an https by uncommenting 'https: true' + // Note: this uses an unsigned certificate which on first access + // will present a certificate warning in the browser. + // https: true, + server: { + baseDir: ['.tmp', 'app'], + middleware: [historyApiFallback()] } - } - }, - // Run as an https by uncommenting 'https: true' - // Note: this uses an unsigned certificate which on first access - // will present a certificate warning in the browser. - // https: true, - server: { - baseDir: ['.tmp', 'app'], - middleware: [historyApiFallback()] - } - }); - - gulp.watch(['app/**/*.html'], reload); - gulp.watch(['app/styles/**/*.css'], ['styles', reload]); - gulp.watch(['app/elements/**/*.css'], ['elements', reload]); - gulp.watch(['app/{scripts,elements}/**/{*.js,*.html}'], ['lint']); - gulp.watch(['app/images/**/*'], reload); + }); + + gulp.watch(['app/**/*.html'], reload); + gulp.watch(['app/styles/**/*.css'], ['styles', reload]); + gulp.watch(['app/elements/**/*.css'], ['elements', reload]); + gulp.watch(['app/{scripts,elements}/**/{*.js,*.html}'], ['lint']); + gulp.watch(['app/images/**/*'], reload); }); // Build and serve the output from the dist build gulp.task('serve:dist', ['default'], function() { - browserSync({ - port: 5001, - notify: false, - logPrefix: 'PSK', - snippetOptions: { - rule: { - match: '<span id="browser-sync-binding"></span>', - fn: function(snippet) { - return snippet; - } - } - }, - // Run as an https by uncommenting 'https: true' - // Note: this uses an unsigned certificate which on first access - // will present a certificate warning in the browser. - // https: true, - server: dist(), - middleware: [historyApiFallback()] - }); + browserSync({ + port: 5001, + notify: false, + logPrefix: 'PSK', + snippetOptions: { + rule: { + match: '<span id="browser-sync-binding"></span>', + fn: function(snippet) { + return snippet; + } + } + }, + // Run as an https by uncommenting 'https: true' + // Note: this uses an unsigned certificate which on first access + // will present a certificate warning in the browser. + // https: true, + server: dist(), + middleware: [historyApiFallback()] + }); }); // Build production files, the default task gulp.task('default', ['clean'], function(cb) { - // Uncomment 'cache-config' if you are going to use service workers. - runSequence( - ['copy', 'styles'], - 'elements', - ['lint', 'images', 'fonts', 'html'], - 'vulcanize', // 'cache-config', - cb); + // Uncomment 'cache-config' if you are going to use service workers. + runSequence( + ['copy', 'styles'], + 'elements', ['images', 'fonts', 'html'], //'lint', + 'vulcanize', 'cache-config', + cb); }); // Build then deploy to GitHub pages gh-pages branch gulp.task('build-deploy-gh-pages', function(cb) { - runSequence( - 'default', - 'deploy-gh-pages', - cb); + runSequence( + 'default', + 'deploy-gh-pages', + cb); }); // Deploy to GitHub pages gh-pages branch gulp.task('deploy-gh-pages', function() { - return gulp.src(dist('**/*')) - // Check if running task from Travis CI, if so run using GH_TOKEN - // otherwise run using ghPages defaults. - .pipe($.if(process.env.TRAVIS === 'true', $.ghPages({ - remoteUrl: 'https://$GH_TOKEN@github.com/polymerelements/polymer-starter-kit.git', - silent: true, - branch: 'gh-pages' - }), $.ghPages())); + return gulp.src(dist('**/*')) + // Check if running task from Travis CI, if so run using GH_TOKEN + // otherwise run using ghPages defaults. + .pipe($.if(process.env.TRAVIS === 'true', $.ghPages({ + remoteUrl: 'https://$GH_TOKEN@github.com/polymerelements/polymer-starter-kit.git', + silent: true, + branch: 'gh-pages' + }), $.ghPages())); }); // Load tasks for web-component-tester @@ -319,5 +329,5 @@ require('web-component-tester').gulp.init(gulp); // Load custom tasks from the `tasks` directory try { - require('require-dir')('tasks'); + require('require-dir')('tasks'); } catch (err) {} diff --git a/package.json b/package.json @@ -13,6 +13,7 @@ "gulp-html-extract": "^0.0.3", "gulp-if": "^2.0.0", "gulp-imagemin": "^2.2.1", + "gulp-inline-source": "^2.1.0", "gulp-jscs": "^3.0.0", "gulp-jscs-stylish": "^1.1.2", "gulp-jshint": "^1.6.3", @@ -39,5 +40,11 @@ }, "engines": { "node": ">=0.10.0" + }, + "dependencies": { + "binaryjs": "^0.2.1", + "express": "^4.13.3", + "ua-parser-js": "^0.7.10", + "ws": "^0.8.1" } } diff --git a/server/ws-server.js b/server/ws-server.js @@ -0,0 +1,106 @@ +'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!'); + + + 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; + } + meta.from = client.uuid; + + // 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 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 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 + }); + } + }); + var msg = { + buddies: buddies, + isSystemEvent: true, + type: 'buddies' + }; + client1.send(msg); + }); +} +setInterval(notifyBuddies, 4000); + +server.listen(9001); +console.log('HTTP and BinaryJS server started on port 9001');