Version 2 initial commit
3
.bowerrc
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"directory": "app/bower_components"
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
# EditorConfig helps developers define and maintain consistent
|
||||
# coding styles between different editors and IDEs
|
||||
# editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
|
||||
[*]
|
||||
|
||||
# Change these settings to your own preference
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# We recommend you to keep these unchanged
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
1
.gitattributes
vendored
|
@ -1 +0,0 @@
|
|||
* text=auto
|
4
.gitignore
vendored
|
@ -1,4 +1,2 @@
|
|||
node_modules
|
||||
bower_components
|
||||
.tmp
|
||||
.publish/
|
||||
.DS_Store
|
8
.jscsrc
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"preset": "google",
|
||||
"disallowSpacesInAnonymousFunctionExpression": null,
|
||||
"disallowTrailingWhitespace": null,
|
||||
"validateIndentation": null,
|
||||
"maximumLineLength": 100,
|
||||
"excludeFiles": ["node_modules/**"]
|
||||
}
|
25
.jshintrc
|
@ -1,25 +0,0 @@
|
|||
{
|
||||
"node": true,
|
||||
"browser": true,
|
||||
"bitwise": true,
|
||||
"camelcase": true,
|
||||
"curly": true,
|
||||
"eqeqeq": true,
|
||||
"immed": true,
|
||||
"indent": 2,
|
||||
"latedef": true,
|
||||
"noarg": true,
|
||||
"quotmark": "single",
|
||||
"undef": true,
|
||||
"unused": true,
|
||||
"newcap": false,
|
||||
"globals": {
|
||||
"wrap": true,
|
||||
"unwrap": true,
|
||||
"Polymer": true,
|
||||
"Platform": true,
|
||||
"page": true,
|
||||
"app": true,
|
||||
"Chat": true
|
||||
}
|
||||
}
|
19
LICENSE.md
|
@ -1,19 +0,0 @@
|
|||
# License
|
||||
|
||||
Everything in this repo is BSD style license unless otherwise specified.
|
||||
|
||||
Copyright (c) 2015 Robin Linus. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"README": "This is the cache config for the dev server. The service worker cache is disabled, and it is recommended that you leave this as-is. In the dist environment, this file will be auto-generated based on the contents of your dist/ directory.",
|
||||
"disabled": true
|
||||
}
|
|
@ -1,168 +0,0 @@
|
|||
<link rel="import" href="../../bower_components/paper-icon-button/paper-icon-button.html">
|
||||
<link rel="import" href="../file-sharing/file-input-behavior.html">
|
||||
<link rel="import" href="../text-sharing/text-input-behavior.html">
|
||||
<dom-module id="buddy-avatar">
|
||||
<template>
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
@apply(--layout-vertical);
|
||||
@apply(--layout-center-center);
|
||||
width: 120px;
|
||||
height: 124px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
paper-icon-button {
|
||||
display: inline-block;
|
||||
width: 64px !important;
|
||||
height: 64px !important;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
padding: 12px;
|
||||
margin-bottom: 6px;
|
||||
background-color: #4285f4;
|
||||
color: white;
|
||||
}
|
||||
|
||||
:host:hover paper-icon-button {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.paper-font-subhead {
|
||||
text-align: center;
|
||||
margin-top: 0px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.paper-font-body1 {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
font-size: 13px;
|
||||
color: grey;
|
||||
margin-top: 0px !important;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
:host,
|
||||
.paper-font-subhead,
|
||||
.paper-font-body1 {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
width: 120px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media all and (min-height: 440px) {
|
||||
:host([only]) {
|
||||
padding: 20vh;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
@apply(--layout-vertical);
|
||||
@apply(--layout-center);
|
||||
height: 112px;
|
||||
padding-top: 16px;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
<div class="container">
|
||||
<paper-icon-button icon="{{_displayIcon}}"></paper-icon-button>
|
||||
<div class="paper-font-subhead">{{_displayName}}</div>
|
||||
<div class="paper-font-body1">{{status}}</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
'use strict';
|
||||
Polymer({
|
||||
is: 'buddy-avatar',
|
||||
behaviors: [Snapdrop.FileInputBehavior, Snapdrop.TextInputBehavior],
|
||||
properties: {
|
||||
contact: Object,
|
||||
_displayName: {
|
||||
computed: '_computeDisplayName(contact)'
|
||||
},
|
||||
_displayIcon: {
|
||||
computed: '_computeDisplayIcon(contact)'
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
defaultStatus: {
|
||||
computed: '_computeDefaultStatus(contact)'
|
||||
}
|
||||
},
|
||||
_computeDisplayName: function(contact) {
|
||||
if (!contact.name.os) {
|
||||
return contact.name;
|
||||
}
|
||||
return this._computeDeviceName(contact.name);
|
||||
},
|
||||
_computeDeviceName: function(contact) {
|
||||
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.device || contact.name;
|
||||
if (contact.type === 'mobile') {
|
||||
return 'chat:phone-iphone';
|
||||
}
|
||||
if (contact.type === 'tablet') {
|
||||
return 'chat:tablet-mac';
|
||||
}
|
||||
return 'chat:desktop-mac';
|
||||
},
|
||||
_computeDefaultStatus: function(contact) {
|
||||
var status = contact.device ? this._computeDeviceName(contact.device) : '';
|
||||
this.status = status;
|
||||
return status;
|
||||
},
|
||||
attached: function() {
|
||||
this.async(function() {
|
||||
app.conn.addEventListener('file-offered', function(e) {
|
||||
if (e.detail.to === this.contact.peerId) {
|
||||
this.status = 'Waiting to accept...';
|
||||
}
|
||||
}.bind(this), false);
|
||||
app.conn.addEventListener('upload-started', function(e) {
|
||||
if (e.detail.to === this.contact.peerId) {
|
||||
this.status = 'Uploading...';
|
||||
}
|
||||
}.bind(this), false);
|
||||
app.conn.addEventListener('download-started', function(e) {
|
||||
if (e.detail.from === this.contact.peerId) {
|
||||
this.status = 'Downloading...';
|
||||
}
|
||||
}.bind(this), false);
|
||||
app.conn.addEventListener('upload-complete', function(e) {
|
||||
if (e.detail.from === this.contact.peerId) {
|
||||
this.status = this.defaultStatus;
|
||||
}
|
||||
}.bind(this), false);
|
||||
app.conn.addEventListener('download-complete', function(e) {
|
||||
if (e.detail.from === this.contact.peerId) {
|
||||
this.status = this.defaultStatus;
|
||||
}
|
||||
}.bind(this), false);
|
||||
app.conn.addEventListener('file-declined', function(e) {
|
||||
if (e.detail.from === this.contact.peerId) {
|
||||
this.status = this.defaultStatus;
|
||||
}
|
||||
}.bind(this), false);
|
||||
app.conn.addEventListener('upload-error', function(e) {
|
||||
this.status = this.defaultStatus;
|
||||
}.bind(this), false);
|
||||
}, 200);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</dom-module>
|
|
@ -1,186 +0,0 @@
|
|||
<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="buddy-avatar.html">
|
||||
<link rel="import" href="personal-avatar.html">
|
||||
<dom-module id="buddy-finder">
|
||||
<template>
|
||||
<style>
|
||||
:host {
|
||||
background-color: transparent;
|
||||
@apply(--layout-fit);
|
||||
@apply(--layout-horizontal);
|
||||
@apply(--layout-center-center);
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
margin: 0;
|
||||
--paper-tooltip: {
|
||||
font-size: 14px;
|
||||
background-color: #4285f4;
|
||||
}
|
||||
}
|
||||
|
||||
.buddies {
|
||||
z-index: 1;
|
||||
@apply(--layout-horizontal);
|
||||
@apply(--layout-center-center);
|
||||
@apply(--layout-wrap);
|
||||
}
|
||||
|
||||
.explanation {
|
||||
@apply(--paper-font-headline);
|
||||
color: #4285f4;
|
||||
text-align: center;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.short {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #727272;
|
||||
}
|
||||
|
||||
.short a {
|
||||
color: #4285f4;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.explanation:hover a {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
@media all and (max-width: 600px) {
|
||||
.explanation {
|
||||
width: 340px;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-height: 440px) {
|
||||
.buddies {
|
||||
padding-top: 56px;
|
||||
@apply(--layout-self-start);
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-height: 600px) {
|
||||
.buddies[two-lines] {
|
||||
padding-top: 56px;
|
||||
@apply(--layout-self-start);
|
||||
}
|
||||
}
|
||||
|
||||
.explanation2 {
|
||||
display: none;
|
||||
position: absolute;
|
||||
width: 296px;
|
||||
margin-left: -148px;
|
||||
left: 50%;
|
||||
@apply(--paper-font-title);
|
||||
color: rgba(66, 133, 244, 0.65);
|
||||
text-align: center;
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity 500ms ease-out;
|
||||
-moz-transition: opacity 500ms ease-out;
|
||||
-o-transition: opacity 500ms ease-out;
|
||||
transition: opacity 500ms ease-out;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
@media all and (min-height: 441px) and (max-height: 559px) {
|
||||
.explanation2 {
|
||||
display: block;
|
||||
top: 64px;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-height: 560px) {
|
||||
.explanation2 {
|
||||
display: block;
|
||||
top: 128px;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<div class="explanation2" hidden$="{{!_showExplanation}}">
|
||||
{{_clickExplanation1}} Device to send File.
|
||||
<br> {{_clickExplanation2}} to send a Text.
|
||||
</div>
|
||||
<div class="buddies" two-lines$="{{twoLinesOfBuddies}}">
|
||||
<template is="dom-repeat" items="{{buddies}}">
|
||||
<buddy-avatar on-file-selected="_fileSelected" only$="{{!buddies.1}}" contact="{{item}}"></buddy-avatar>
|
||||
</template>
|
||||
</div>
|
||||
<div hidden$="{{buddies.0}}" class="explanation">
|
||||
Open Snapdrop on other devices to send files.
|
||||
<div class="short">
|
||||
Short link: <a href="http://yg.gl" target="_blank">yg.gl</a>
|
||||
</div>
|
||||
</div>
|
||||
<personal-avatar class="me"></personal-avatar>
|
||||
</template>
|
||||
<script>
|
||||
'use strict';
|
||||
(function() {
|
||||
var isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
Polymer({
|
||||
is: 'buddy-finder',
|
||||
properties: {
|
||||
buddies: {
|
||||
type: Array,
|
||||
notify: true
|
||||
},
|
||||
me: {
|
||||
type: String,
|
||||
},
|
||||
_showExplanation: {
|
||||
computed: '_computeShowExplanation(buddies.length)',
|
||||
value: false
|
||||
},
|
||||
twoLinesOfBuddies: {
|
||||
value: false
|
||||
},
|
||||
_clickExplanation1: {
|
||||
value: (function() {
|
||||
if (isMobile) {
|
||||
return 'Tap';
|
||||
} else {
|
||||
return 'Click';
|
||||
}
|
||||
})
|
||||
},
|
||||
_clickExplanation2: {
|
||||
value: (function() {
|
||||
if (isMobile) {
|
||||
return 'Long Press';
|
||||
} else {
|
||||
return 'Right Click';
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
observers: [
|
||||
'_buddiesChanged(buddies.splices)'
|
||||
],
|
||||
_fileSelected: function(e) {
|
||||
var peerId = e.model.item.peerId;
|
||||
var file = e.detail;
|
||||
app.conn.sendFile(peerId, file);
|
||||
},
|
||||
_computeShowExplanation: function(nBuddies) {
|
||||
if (!nBuddies || nBuddies === 0) {
|
||||
return false;
|
||||
}
|
||||
if (nBuddies < 3) {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
_buddiesChanged: function() {
|
||||
var length = window.innerWidth / 120;
|
||||
this.set('twoLinesOfBuddies', (this.buddies.length > length));
|
||||
}
|
||||
});
|
||||
}());
|
||||
</script>
|
||||
</dom-module>
|
|
@ -1,51 +0,0 @@
|
|||
<link rel="import" href="../../bower_components/paper-dialog/paper-dialog.html">
|
||||
<link rel="import" href="../../bower_components/paper-input/paper-input.html">
|
||||
<dom-module id="device-name-dialog">
|
||||
<template>
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
paper-dialog {
|
||||
width: 400px;
|
||||
max-width: 90%
|
||||
}
|
||||
</style>
|
||||
<paper-dialog id="dialog" entry-animation="scale-up-animation" exit-animation="fade-out-animation" with-backdrop>
|
||||
<h2>Name this Device</h2>
|
||||
<p>
|
||||
<paper-input id="input" value="{{deviceName}}" label="Name this Device" char-counter maxlength="18" on-keypress="_keyPressed" autofocus></paper-input>
|
||||
</p>
|
||||
<div class="buttons">
|
||||
<paper-button dialog-dismiss>Cancel</paper-button>
|
||||
<paper-button on-tap="_save">Rename</paper-button>
|
||||
</div>
|
||||
</paper-dialog>
|
||||
</template>
|
||||
<script>
|
||||
'use strict';
|
||||
Polymer({
|
||||
is: 'device-name-dialog',
|
||||
properties: {
|
||||
deviceName: {
|
||||
notify: true
|
||||
}
|
||||
},
|
||||
open: function() {
|
||||
this.$.dialog.open();
|
||||
},
|
||||
_keyPressed: function(e) {
|
||||
if (e.which === 13 || e.charCode === 13) {
|
||||
this.$.input.inputElement.blur();
|
||||
this._save();
|
||||
}
|
||||
},
|
||||
_save: function() {
|
||||
this.$.dialog.close();
|
||||
this.fire('save-device-name', this.deviceName);
|
||||
}
|
||||
|
||||
});
|
||||
</script>
|
||||
</dom-module>
|
|
@ -1,99 +0,0 @@
|
|||
<link rel="import" href="../../bower_components/paper-input/paper-input.html">
|
||||
<link rel="import" href="../../bower_components/iron-localstorage/iron-localstorage.html">
|
||||
<link rel="import" href="device-name-dialog.html">
|
||||
<dom-module id="device-name">
|
||||
<template>
|
||||
<style>
|
||||
:host {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.name-label {
|
||||
@apply(--paper-font-subhead);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
width: 160px;
|
||||
line-height: 12px !important;
|
||||
}
|
||||
|
||||
:root {
|
||||
/* Label and underline color when the input is not focused */
|
||||
--paper-input-container-color: #333;
|
||||
/* Label and underline color when the input is focused */
|
||||
--paper-input-container-focus-color: #4285f4;
|
||||
/* Label and underline color when the input is invalid */
|
||||
--paper-input-container-invalid-color: red;
|
||||
/* Input foreground color */
|
||||
--paper-input-container-input-color: #333;
|
||||
}
|
||||
|
||||
@media all and (max-height: 370px) {
|
||||
:host {}
|
||||
}
|
||||
|
||||
paper-dialog {
|
||||
width: 300px;
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
<div class="name-label" hidden$="{{name}}">My Device</div>
|
||||
<div class="name-label" hidden$="{{!name}}">{{name}}</div>
|
||||
<iron-localstorage name="device-name" value="{{name}}" iron-localstorage-load="_nameChanged"></iron-localstorage>
|
||||
</template>
|
||||
<script>
|
||||
'use strict';
|
||||
Polymer({
|
||||
is: 'device-name',
|
||||
properties: {
|
||||
name: {
|
||||
observer: '_nameChanged'
|
||||
}
|
||||
},
|
||||
open: function() {
|
||||
this.deviceNameDialog.open();
|
||||
},
|
||||
_nameChanged: function(name) {
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
this.cancelAsync(this.timer);
|
||||
this.timer = this.async(function() {
|
||||
if (!app.conn.notifyServer) {
|
||||
this._nameChanged(name);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this._sendNameToServer(name);
|
||||
} catch (e) {
|
||||
this._nameChanged(name);
|
||||
}
|
||||
|
||||
}, 300);
|
||||
|
||||
},
|
||||
_sendNameToServer: function(name) {
|
||||
app.conn.notifyServer({
|
||||
serverMsg: 'device-name',
|
||||
name: name
|
||||
});
|
||||
},
|
||||
_initialize: function() {
|
||||
console.log('initialize name');
|
||||
},
|
||||
get deviceNameDialog() {
|
||||
var deviceNameDialog = document.querySelector('device-name-dialog');
|
||||
if (!deviceNameDialog) {
|
||||
deviceNameDialog = Polymer.Base.create('device-name-dialog');
|
||||
deviceNameDialog.addEventListener('save-device-name', function(e) {
|
||||
this.name = e.detail;
|
||||
console.log(this.name);
|
||||
}.bind(this));
|
||||
document.body.appendChild(deviceNameDialog);
|
||||
};
|
||||
deviceNameDialog.deviceName = this.name;
|
||||
return deviceNameDialog;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</dom-module>
|
|
@ -1,85 +0,0 @@
|
|||
<link rel="import" href="../../bower_components/iron-icon/iron-icon.html">
|
||||
<link rel="import" href="../../styles/icons.html">
|
||||
<link rel="import" href="device-name.html">
|
||||
<dom-module id="personal-avatar">
|
||||
<template>
|
||||
<style>
|
||||
:host {
|
||||
@apply(--layout-vertical);
|
||||
@apply(--layout-center);
|
||||
width: 360px;
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
left: 50%;
|
||||
margin-left: -180px;
|
||||
z-index: 12;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:host:hover iron-icon,
|
||||
:host:hover device-name {
|
||||
color: #3367d6;
|
||||
}
|
||||
|
||||
:host:hover iron-icon {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
iron-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
color: #4285f4;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.paper-font-body1 {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.discover {
|
||||
color: #4285f4;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
@media all and (max-width: 370px) {
|
||||
.discover {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-height: 380px) {
|
||||
:host {
|
||||
bottom: 4px;
|
||||
}
|
||||
iron-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.slogan {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<device-name id="deviceName"></device-name>
|
||||
<iron-icon icon="chat:wifi-tethering"></iron-icon>
|
||||
<div class="paper-font-body1 slogan">
|
||||
The easiest way to transfer data across devices.
|
||||
</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',
|
||||
listeners: {
|
||||
'tap': '_openDeviceNameDialog'
|
||||
},
|
||||
_openDeviceNameDialog: function() {
|
||||
this.$.deviceName.open();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</dom-module>
|
|
@ -1,95 +0,0 @@
|
|||
<link rel="import" href="../../bower_components/paper-dialog/paper-dialog.html">
|
||||
<dom-module id="donate-dialog-impl">
|
||||
<template>
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
paper-dialog {
|
||||
max-width: 324px;
|
||||
}
|
||||
|
||||
iron-icon {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 4px;
|
||||
color: #4285f4;
|
||||
}
|
||||
|
||||
p {
|
||||
padding: 0 48px;
|
||||
margin: 4px;
|
||||
@apply(--paper-font-subhead);
|
||||
}
|
||||
|
||||
paper-dialog > a {
|
||||
text-decoration: none;
|
||||
color: #52524F;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
paper-dialog {
|
||||
margin: 24px
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<paper-dialog id="dialog" with-backdrop entry-animation="scale-up-animation" exit-animation="fade-out-animation" modal>
|
||||
<h2>Do you like Snapdrop?</h2>
|
||||
<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&cmd=_donations&business=robin@capira.de¤cy_code=USD&item_name=Snapdrop&shipping=0" target="_blank" dialog-confirm>
|
||||
<p>
|
||||
<iron-icon icon="chat:local-cafe"></iron-icon>
|
||||
<span>Donate a cup of Coffee <br>to the Developers!</span>
|
||||
</p>
|
||||
</a>
|
||||
<div class="buttons">
|
||||
<paper-button dialog-dismiss>No, thanks.</paper-button>
|
||||
<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&cmd=_donations&business=robin@capira.de¤cy_code=USD&item_name=Snapdrop&shipping=0" target="_blank">
|
||||
<paper-button dialog-confirm>Donate</paper-button>
|
||||
</a>
|
||||
</div>
|
||||
</paper-dialog>
|
||||
</template>
|
||||
<script>
|
||||
'use strict';
|
||||
Polymer({
|
||||
is: 'donate-dialog-impl',
|
||||
open: function() {
|
||||
this.$.dialog.open();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</dom-module>
|
||||
<dom-module id="donate-dialog">
|
||||
<script>
|
||||
'use strict';
|
||||
Polymer({
|
||||
is: 'donate-dialog',
|
||||
properties: {
|
||||
chance: {
|
||||
value: 1 / 6
|
||||
}
|
||||
},
|
||||
attached: function() {
|
||||
window.donateDialog = this;
|
||||
},
|
||||
get dialog() {
|
||||
var dialog = document.querySelector('donate-dialog-impl');
|
||||
if (!dialog) {
|
||||
dialog = Polymer.Base.create('donate-dialog-impl');
|
||||
document.body.appendChild(dialog);
|
||||
}
|
||||
return dialog;
|
||||
},
|
||||
open: function() {
|
||||
var chance = Math.random() <= this.chance;
|
||||
if (chance) {
|
||||
this.dialog.open();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</dom-module>
|
|
@ -1,15 +0,0 @@
|
|||
<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">
|
||||
<link rel="import" href="../bower_components/neon-animation/neon-animated-pages.html">
|
||||
|
||||
<!-- Add your elements here -->
|
||||
<link rel="import" href="../styles/app-theme.html">
|
||||
<link rel="import" href="x-cards/about-page.html">
|
||||
<link rel="import" href="x-cards/x-cards.html">
|
||||
<link rel="import" href="buddy-finder/buddy-finder.html">
|
||||
<link rel="import" href="p2p-network/connection-wrapper.html">
|
||||
<link rel="import" href="file-sharing/file-receiver.html">
|
||||
<link rel="import" href="donate-dialog/donate-dialog.html">
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
<link rel="import" href="file-selection-behavior.html">
|
||||
<script>
|
||||
'use strict';
|
||||
window.Snapdrop = window.Snapdrop || {};
|
||||
Snapdrop.FileButtonBehaviorImpl = {
|
||||
get fileInput() {
|
||||
var fileInput = Polymer.dom(this).querySelector('.fileInput');
|
||||
if (!fileInput) {
|
||||
fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.multiple = 'false';
|
||||
fileInput.className = 'fileInput';
|
||||
fileInput.style.position = 'fixed';
|
||||
fileInput.style.top = '-10000px';
|
||||
fileInput.style.left = '-10000px';
|
||||
fileInput.style.opacity = 0;
|
||||
Polymer.dom(this).appendChild(fileInput);
|
||||
}
|
||||
return fileInput;
|
||||
},
|
||||
attached: function() {
|
||||
this.fileInput.onchange = function() {
|
||||
var files = this.fileInput.files;
|
||||
this.notifyFilesSelection(files);
|
||||
}.bind(this);
|
||||
this.addEventListener('click', function(e) {
|
||||
var button = e.which || e.button;
|
||||
if (button !== 1) {
|
||||
return;
|
||||
}
|
||||
this.fileInput.value = null;
|
||||
this.fileInput.click();
|
||||
}.bind(this), false);
|
||||
}
|
||||
};
|
||||
Snapdrop.FileButtonBehavior = [Snapdrop.FileButtonBehaviorImpl, Snapdrop.FileSelectionBehavior];
|
||||
</script>
|
|
@ -1,14 +0,0 @@
|
|||
<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>
|
||||
<paper-icon-button id="btn" icon="chat:attach-file" on-tap="_upload"></paper-icon-button>
|
||||
</template>
|
||||
<script>
|
||||
'use strict';
|
||||
Polymer({
|
||||
is: 'file-button',
|
||||
behaviors: [Snapdrop.FileButtonBehavior]
|
||||
});
|
||||
</script>
|
||||
</dom-module>
|
|
@ -1,48 +0,0 @@
|
|||
<link rel="import" href="file-selection-behavior.html">
|
||||
<script>
|
||||
'use strict';
|
||||
window.Snapdrop = window.Snapdrop || {};
|
||||
Snapdrop.FileDropBehaviorImpl = {
|
||||
attached: function() {
|
||||
var dropZone = this;
|
||||
|
||||
dropZone.addEventListener('dragover', function(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'copy';
|
||||
dropZone.style.transform = 'scale(1.2)';
|
||||
}, false);
|
||||
|
||||
var dragEnd = function() {
|
||||
dropZone.style.transform = 'scale(1)';
|
||||
};
|
||||
|
||||
dropZone.addEventListener('dragleave', dragEnd, false);
|
||||
dropZone.addEventListener('dragexit', dragEnd, false);
|
||||
dropZone.addEventListener('dragend', dragEnd, false);
|
||||
|
||||
// Get file data on drop
|
||||
dropZone.addEventListener('drop', function(event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
//drop is a dragend
|
||||
dragEnd();
|
||||
|
||||
// Get files
|
||||
var files = event.dataTransfer.files;
|
||||
// Notify Selection
|
||||
this.notifyFilesSelection(files);
|
||||
});
|
||||
}
|
||||
};
|
||||
document.body.addEventListener('dragover', function(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}, false);
|
||||
document.body.addEventListener('drop', function(event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
});
|
||||
Snapdrop.FileDropBehavior = [Snapdrop.FileDropBehaviorImpl, Snapdrop.FileSelectionBehavior];
|
||||
</script>
|
|
@ -1,6 +0,0 @@
|
|||
<link rel="import" href="file-drop-behavior.html">
|
||||
<link rel="import" href="file-button-behavior.html">
|
||||
<script>
|
||||
'use strict';
|
||||
Snapdrop.FileInputBehavior = [Snapdrop.FileDropBehavior,Snapdrop.FileButtonBehavior];
|
||||
</script>
|
|
@ -1,100 +0,0 @@
|
|||
<link rel="import" href="../../bower_components/paper-dialog/paper-dialog.html">
|
||||
<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">
|
||||
<link rel="import" href="../sound-notification/sound-notification-behavior.html">
|
||||
<dom-module id="file-receiver">
|
||||
<template>
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#dialog,
|
||||
#download {
|
||||
width: 324px;
|
||||
z-index: 101;
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.filename {
|
||||
word-break: break-all;
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
<paper-dialog id="dialog" entry-animation="scale-up-animation" exit-animation="fade-out-animation" with-backdrop modal>
|
||||
<h2>Download File</h2>
|
||||
<p><b class="filename">{{file.name}}</b></p>
|
||||
<div class="buttons">
|
||||
<paper-button dialog-dismiss on-tap="_decline">Ignore</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>Open File or Right Click and "Save as"...</p>
|
||||
<div class="buttons">
|
||||
<paper-button dialog-dismiss>Discard</paper-button>
|
||||
<a href="{{dataUri}}" target="_blank">
|
||||
<paper-button dialog-confirm autofocus>Open File</paper-button>
|
||||
</a>
|
||||
</div>
|
||||
</paper-dialog>
|
||||
</template>
|
||||
<script>
|
||||
'use strict';
|
||||
(function() {
|
||||
Polymer({
|
||||
is: 'file-receiver',
|
||||
behaviors: [Snapdrop.SoundNotificationBehavior],
|
||||
attached: function() {
|
||||
this.async(function() {
|
||||
app.conn.addEventListener('file-offer', function(e) {
|
||||
this.file = e.detail;
|
||||
this.$.dialog.open();
|
||||
this.playSound();
|
||||
}.bind(this), false);
|
||||
app.conn.addEventListener('file-received', function(e) {
|
||||
this._fileReceived(e.detail);
|
||||
}.bind(this), false);
|
||||
app.conn.addEventListener('file-declined', function(e) {
|
||||
app.displayToast('User declined file ' + e.detail.name);
|
||||
}.bind(this), false);
|
||||
app.conn.addEventListener('upload-complete', function(e) {
|
||||
app.displayToast('User received file ' + e.detail.name);
|
||||
}.bind(this), false);
|
||||
app.conn.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.conn.decline(this.file);
|
||||
},
|
||||
_accept: function() {
|
||||
app.conn.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>
|
|
@ -1,21 +0,0 @@
|
|||
<script>
|
||||
'use strict';
|
||||
window.Snapdrop = window.Snapdrop || {};
|
||||
Snapdrop.FileSelectionBehavior = {
|
||||
notifyFilesSelection: function(files) {
|
||||
if (!files) {
|
||||
console.log('no files selected...');
|
||||
return;
|
||||
}
|
||||
this._fileSelected(files[0]); //single select
|
||||
},
|
||||
_fileSelected: function(file) {
|
||||
if (file) {
|
||||
this.fire('file-selected', {
|
||||
file: file,
|
||||
name: file.name
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -1,45 +0,0 @@
|
|||
<script>
|
||||
'use strict';
|
||||
window.Snapdrop = window.Snapdrop || {};
|
||||
Snapdrop.InvitationLinkBehavior = {
|
||||
properties: {
|
||||
contact: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
_copy: function(e) {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
Polymer.Base.create('textarea');
|
||||
var copyTextarea = this.textarea;
|
||||
copyTextarea.value = this.link;
|
||||
copyTextarea.select();
|
||||
try {
|
||||
var successful = document.execCommand('copy');
|
||||
if (successful) {
|
||||
app.displayToast('Copied invitation link to clipboard. Share it to send files to friends!');
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('Oops, unable to copy', err);
|
||||
}
|
||||
copyTextarea.blur();
|
||||
},
|
||||
get link() {
|
||||
return 'http://' + window.location.host + '/' + this.contact;
|
||||
},
|
||||
get textarea() {
|
||||
var textarea = document.querySelector('#copytextarea');
|
||||
if (!textarea) {
|
||||
textarea = Polymer.Base.create('textarea');
|
||||
textarea.id = 'copytextarea';
|
||||
var style = textarea.style;
|
||||
style.position = 'absolute';
|
||||
style.top = '-10000px';
|
||||
document.body.appendChild(textarea);
|
||||
}
|
||||
return textarea;
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -1,29 +0,0 @@
|
|||
<link rel="import" href="invitation-link-behavior.html">
|
||||
<link rel="import" href="../../bower_components/paper-tooltip/paper-tooltip.html">
|
||||
<dom-module id="invitation-link">
|
||||
<template>
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 16px;
|
||||
z-index: 3;
|
||||
}
|
||||
</style>
|
||||
<paper-icon-button icon="chat:share" on-tap="_copy" id="btn"></paper-icon-button>
|
||||
<paper-tooltip
|
||||
for="btn"
|
||||
position="bottom"
|
||||
offset="14">
|
||||
Get an Invitation Link to send files accross different networks.
|
||||
</paper-tooltip>
|
||||
</template>
|
||||
<script>
|
||||
'use strict';
|
||||
Polymer({
|
||||
is: 'invitation-link',
|
||||
behaviors: [Snapdrop.InvitationLinkBehavior]
|
||||
});
|
||||
</script>
|
||||
</dom-module>
|
|
@ -1,59 +0,0 @@
|
|||
<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() {
|
||||
window.webRTCSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection || window.webkitRTCPeerConnection);
|
||||
|
||||
function rtcConnectionSupported(peerId) {
|
||||
return window.webRTCSupported && (peerId.indexOf('rtc_') === 0);
|
||||
}
|
||||
Polymer({
|
||||
is: 'connection-wrapper',
|
||||
properties: {
|
||||
me: {
|
||||
notify: true
|
||||
},
|
||||
},
|
||||
behaviors: [Snapdrop.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);
|
||||
}
|
||||
},
|
||||
_onHandshake: function(event) {
|
||||
var me = event.uuid;
|
||||
this.set('me', me);
|
||||
if (window.webRTCSupported) {
|
||||
this.$.p2p.initialize();
|
||||
}
|
||||
},
|
||||
notifyServer: function(msg) {
|
||||
this.$.ws.client.send({}, msg);
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</dom-module>
|
|
@ -1,159 +0,0 @@
|
|||
<script>
|
||||
'use strict';
|
||||
window.Snapdrop = window.Snapdrop || {};
|
||||
Snapdrop.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 'handshake':
|
||||
this._onHandshake(msg);
|
||||
break;
|
||||
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;
|
||||
case 'text':
|
||||
this._onTextReceived(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);
|
||||
if(window.donateDialog){
|
||||
window.donateDialog.open();
|
||||
}
|
||||
},
|
||||
_onError: function() {
|
||||
this.loading = false;
|
||||
this.fire('upload-error');
|
||||
},
|
||||
_loadingChanged: function(loading) {
|
||||
window.anim(loading);
|
||||
},
|
||||
_onBuddies: function(msg) {
|
||||
this.set('buddies', msg.buddies);
|
||||
},
|
||||
sendText: function(toPeer, text) {
|
||||
console.log('FTP send text:', text, 'To:', toPeer);
|
||||
this.connectToPeer(toPeer, function() {
|
||||
this._sendSystemEvent(toPeer, {
|
||||
type: 'text',
|
||||
text: text
|
||||
});
|
||||
}.bind(this));
|
||||
|
||||
},
|
||||
_onTextReceived: function(msg) {
|
||||
this.fire('text-received', msg);
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -1,171 +0,0 @@
|
|||
<script src="../../bower_components/peerjs/peer.min.js"></script>
|
||||
<link rel="import" href="file-transfer-protocol.html">
|
||||
<dom-module id="p2p-network">
|
||||
<template>
|
||||
</template>
|
||||
<script>
|
||||
'use strict';
|
||||
Polymer({
|
||||
is: 'p2p-network',
|
||||
properties: {
|
||||
me: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
attached: function() {
|
||||
this._connectedPeers = {};
|
||||
this._initCallbacks = [];
|
||||
this._unsendMsgs = {};
|
||||
window.onunload = window.onbeforeunload = function() {
|
||||
if (!!this._peer && !this._peer.destroyed) {
|
||||
this._peer.destroy();
|
||||
}
|
||||
}.bind(this);
|
||||
},
|
||||
initialize: function() {
|
||||
clearInterval(this.reconnectTimer);
|
||||
this.reconnectTimer = undefined;
|
||||
var options;
|
||||
if (window.debug) {
|
||||
options = {
|
||||
host: window.location.hostname,
|
||||
port: 3002,
|
||||
path: 'peerjs'
|
||||
};
|
||||
} else {
|
||||
options = {
|
||||
host: 'snapdrop.net',
|
||||
port: 443,
|
||||
path: 'peerjs',
|
||||
secure: true
|
||||
};
|
||||
}
|
||||
this._peer = new Peer(this.me, options);
|
||||
this._peer.on('open', function(id) {
|
||||
console.log('My peer ID is: ' + id);
|
||||
this.set('me', id);
|
||||
this._peerOpen = true;
|
||||
this._initCallbacks.forEach(function(cb) {
|
||||
cb();
|
||||
});
|
||||
}.bind(this));
|
||||
|
||||
this._peer.on('connection', this.connect.bind(this));
|
||||
this._peer.on('error', function(err) {
|
||||
console.error(err);
|
||||
//ugly hack to find out error type
|
||||
if (err.message.indexOf('Could not connect to peer') > -1) {
|
||||
delete this._connectedPeers[this.peer];
|
||||
return;
|
||||
}
|
||||
if (err.message.indexOf('Lost connection to server') > -1) {
|
||||
this._peer.destroy();
|
||||
this._reconnect();
|
||||
return;
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
connect: function(c) {
|
||||
var peer = c.peer;
|
||||
|
||||
if (c.label === 'file') {
|
||||
c.on('data', function(data) {
|
||||
console.log(data);
|
||||
var dataView = new Uint8Array(data.file);
|
||||
var dataBlob = new Blob([dataView]);
|
||||
this.fire('file-received', {
|
||||
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));
|
||||
}
|
||||
},
|
||||
connectToPeer: (function() {
|
||||
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.error('Handle this error!!', err);
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
//files channel
|
||||
var f = this._peer.connect(requestedPeer, {
|
||||
label: 'file',
|
||||
reliable: true
|
||||
});
|
||||
f.on('open', function() {
|
||||
this.connect(f);
|
||||
|
||||
}.bind(this));
|
||||
f.on('error', function(err) {
|
||||
console.log(err);
|
||||
});
|
||||
};
|
||||
}
|
||||
return function(requestedPeer, callback) {
|
||||
if (this._peer.connections[requestedPeer]) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
if (this._peerOpen) {
|
||||
request(requestedPeer, callback).bind(this)();
|
||||
} else {
|
||||
this._initCallbacks.push(request(requestedPeer, callback).bind(this));
|
||||
}
|
||||
|
||||
};
|
||||
}()),
|
||||
_sendFile: function(peerId, file) {
|
||||
var conns = this._peer.connections[peerId];
|
||||
if (conns) {
|
||||
conns.forEach(function(conn) {
|
||||
if (conn.label === 'file') {
|
||||
conn.send(file);
|
||||
console.log('send file via WebRTC');
|
||||
}
|
||||
}.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));
|
||||
}
|
||||
},
|
||||
_reconnect: function(e) {
|
||||
//try to reconnect after 3s
|
||||
if (!this.reconnectTimer) {
|
||||
this.reconnectTimer = setInterval(this.initialize.bind(this), 3000);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</dom-module>
|
|
@ -1,25 +0,0 @@
|
|||
<script>
|
||||
'use strict';
|
||||
var vis = (function() {
|
||||
var stateKey,
|
||||
eventKey,
|
||||
keys = {
|
||||
hidden: "visibilitychange",
|
||||
webkitHidden: "webkitvisibilitychange",
|
||||
mozHidden: "mozvisibilitychange",
|
||||
msHidden: "msvisibilitychange"
|
||||
};
|
||||
for (stateKey in keys) {
|
||||
if (stateKey in document) {
|
||||
eventKey = keys[stateKey];
|
||||
break;
|
||||
}
|
||||
}
|
||||
return function(c) {
|
||||
if (c) {
|
||||
document.addEventListener(eventKey, c);
|
||||
}
|
||||
return !document[stateKey];
|
||||
};
|
||||
})();
|
||||
</script>
|
|
@ -1,89 +0,0 @@
|
|||
<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() {
|
||||
clearInterval(this.reconnectTimer);
|
||||
this.reconnectTimer = undefined;
|
||||
var websocketUrl = (window.debug ? 'ws://' + window.location.hostname + ':3002' : 'wss://snapdrop.net') + '/binary';
|
||||
this.client = new BinaryClient(websocketUrl);
|
||||
this.client.on('stream', function(stream, meta) {
|
||||
// collect stream data
|
||||
var parts = [];
|
||||
stream.on('data', function(data) {
|
||||
//console.log('part received', meta, data);
|
||||
if (data.isSystemEvent) {
|
||||
if (meta) {
|
||||
data.from = meta.from;
|
||||
}
|
||||
this.fire('system-event', data);
|
||||
} else {
|
||||
parts.push(data);
|
||||
}
|
||||
}.bind(this));
|
||||
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({}, {
|
||||
serverMsg: 'rtc-support',
|
||||
rtc: window.webRTCSupported
|
||||
});
|
||||
}.bind(this));
|
||||
this.client.on('error', function(e) {
|
||||
this._reconnect(e);
|
||||
}.bind(this));
|
||||
this.client.on('close', function(e) {
|
||||
this._reconnect(e);
|
||||
}.bind(this));
|
||||
},
|
||||
_sendFile: function(toPeer, file) {
|
||||
console.log('send file via WebSocket', 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
|
||||
});
|
||||
},
|
||||
_reconnect: function(e) {
|
||||
console.log('disconnected', e);
|
||||
//try to reconnect after 3s
|
||||
if (!this.reconnectTimer) {
|
||||
this.reconnectTimer = setInterval(this.init.bind(this), 3000);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</dom-module>
|
|
@ -1,24 +0,0 @@
|
|||
<link rel="import" href="sound-notification.html">
|
||||
<script>
|
||||
'use strict';
|
||||
Snapdrop = window.Snapdrop || {};
|
||||
Snapdrop.SoundNotificationBehavior = {
|
||||
sounds: function() {
|
||||
var sounds = document.querySelector('sound-notification');
|
||||
if (!sounds) {
|
||||
sounds = Polymer.Base.create('sound-notification');
|
||||
document.body.appendChild(sounds);
|
||||
}
|
||||
return sounds;
|
||||
},
|
||||
attached: function() {
|
||||
//lazy load sound files
|
||||
setTimeout(function() {
|
||||
this.sounds();
|
||||
}.bind(this), 1000);
|
||||
},
|
||||
playSound: function(e) {
|
||||
this.sounds().play();
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -1,56 +0,0 @@
|
|||
<dom-module id="sound-notification">
|
||||
<template>
|
||||
<audio id="blop" preload="auto" autobuffer="true">
|
||||
<source src="/sounds/blop.mp3" id="mp3Source" type="audio/mpeg">
|
||||
<source src="/sounds/blop.ogg" id="oggSource" type="audio/ogg">
|
||||
</audio>
|
||||
</template>
|
||||
</dom-module>
|
||||
<script>
|
||||
'use strict';
|
||||
Polymer({
|
||||
is: 'sound-notification',
|
||||
properties: {
|
||||
volumes: {
|
||||
value: {
|
||||
'blop': 0.8,
|
||||
}
|
||||
}
|
||||
},
|
||||
attached: function() {
|
||||
// mobiles don't like autoplay - the first play must be triggert by user interaction
|
||||
var that = this;
|
||||
var hackListener = function() {
|
||||
that.volumes.blop = 0.1;
|
||||
that.play();
|
||||
document.body.removeEventListener('touchstart', hackListener, false);
|
||||
that.volumes.blop = 0.8;
|
||||
};
|
||||
document.body.addEventListener('touchstart', hackListener, false);
|
||||
},
|
||||
play: function() {
|
||||
this._play('blop');
|
||||
},
|
||||
_play: function(sound) {
|
||||
var audio = this.$[sound];
|
||||
if (!audio) {
|
||||
console.warn('audio ', sound, ' doesn\'t exist.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (audio.readyState > 0) {
|
||||
audio.volume = this.volumes[sound];
|
||||
audio.pause();
|
||||
audio.currentTime = 0;
|
||||
audio.play();
|
||||
} else {
|
||||
console.warn('audio not ready yet...');
|
||||
//play when ready
|
||||
//TODO: play only if ready within next ~500ms
|
||||
audio.addEventListener('loadedmetadata', function() {
|
||||
this._play(sound);
|
||||
}.bind(this), false);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
|
@ -1,36 +0,0 @@
|
|||
<script>
|
||||
'use strict';
|
||||
(function(document) {
|
||||
var copyTextarea = document.createElement('textarea');
|
||||
copyTextarea.setAttribute('id', 'clipboard-textarea');
|
||||
var style = copyTextarea.style;
|
||||
style.position = 'absolute';
|
||||
style.top = '-10000px';
|
||||
document.body.appendChild(copyTextarea);
|
||||
|
||||
window.Snapdrop.ClipboardBehavior = {
|
||||
copyToClipboard: function(content) {
|
||||
copyTextarea.value = content;
|
||||
var range = document.createRange();
|
||||
range.selectNode(copyTextarea);
|
||||
window.getSelection().addRange(range);
|
||||
|
||||
try {
|
||||
// Now that we've selected the anchor text, execute the copy command
|
||||
var successful = document.execCommand('copy');
|
||||
if (successful) {
|
||||
app.displayToast('Copied text to clipboard. Paste it where you want!');
|
||||
} else {
|
||||
console.log('failed to copy to clipboard', successful);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('Oops, unable to copy', err);
|
||||
}
|
||||
|
||||
// Remove the selections - NOTE: Should use
|
||||
// removeRange(range) when it is supported
|
||||
window.getSelection().removeAllRanges();
|
||||
}
|
||||
};
|
||||
}(document));
|
||||
</script>
|
|
@ -1,48 +0,0 @@
|
|||
<link rel="import" href="text-input-dialog.html">
|
||||
<script>
|
||||
'use strict';
|
||||
window.Snapdrop = window.Snapdrop || {};
|
||||
(function() {
|
||||
var textInput = Polymer.Base.create('text-input-dialog');
|
||||
textInput.className = 'textInput';
|
||||
document.body.appendChild(textInput);
|
||||
Snapdrop.TextInputBehavior = {
|
||||
properties: {
|
||||
contact: Object,
|
||||
},
|
||||
get textInput() {
|
||||
var textInput = Polymer.dom(document).querySelector('.textInput');
|
||||
return textInput;
|
||||
},
|
||||
openTextDialog: function() {
|
||||
this.textInput.open(this.contact);
|
||||
},
|
||||
|
||||
listeners: {
|
||||
'contextmenu': '_handleContextMenu',
|
||||
'down': '_handleDown',
|
||||
'up': '_handleUp',
|
||||
},
|
||||
_handleContextMenu: function(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
ev.cancelBubble = true;
|
||||
this.cancelAsync(this.pressTimer);
|
||||
this.openTextDialog();
|
||||
return false;
|
||||
},
|
||||
_handleUp: function(e) {
|
||||
this.cancelAsync(this.pressTimer);
|
||||
},
|
||||
_handleDown: function(ev) {
|
||||
this.pressTimer = this.async(function() {
|
||||
this.openTextDialog();
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
ev.cancelBubble = true;
|
||||
return false;
|
||||
}, 800);
|
||||
},
|
||||
};
|
||||
}());
|
||||
</script>
|
|
@ -1,210 +0,0 @@
|
|||
<link rel="import" href="../../bower_components/paper-dialog/paper-dialog.html">
|
||||
<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">
|
||||
<link rel="import" href="../../bower_components/paper-input/paper-textarea.html">
|
||||
<link rel="import" href="linkify.html">
|
||||
<link rel="import" href="clipboard-behavior.html">
|
||||
<link rel="import" href="../sound-notification/sound-notification-behavior.html">
|
||||
<dom-module id="text-input-dialog">
|
||||
<template>
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#sendDialog,
|
||||
#receiveDialog {
|
||||
width: 324px;
|
||||
z-index: 101;
|
||||
max-height: 320px;
|
||||
overflow: hidden;
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
@media all and (max-height: 600px) {
|
||||
#sendDialog {
|
||||
padding-top: 24px;
|
||||
top: 0px !important;
|
||||
}
|
||||
}
|
||||
|
||||
#receivedText {
|
||||
word-break: break-all;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
paper-textarea {
|
||||
max-height: 200px;
|
||||
width: calc(100% - 48px);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#receivedText {
|
||||
max-height: 200px;
|
||||
overflow: hidden;
|
||||
width: calc(100% - 48px);
|
||||
text-overflow: ellipsis;
|
||||
-webkit-line-clamp: 9;
|
||||
clamp: 9;
|
||||
}
|
||||
</style>
|
||||
<paper-dialog id="sendDialog" entry-animation="scale-up-animation" exit-animation="fade-out-animation" with-backdrop modal>
|
||||
<h2>Send Text</h2>
|
||||
<paper-textarea id="textInput" label="Enter Text" value="{{textToSend}}" autofocus></paper-textarea>
|
||||
<div class="buttons">
|
||||
<paper-button dialog-dismiss>Discard</paper-button>
|
||||
<paper-button dialog-dismiss on-tap="_send">Send</paper-button>
|
||||
</div>
|
||||
</paper-dialog>
|
||||
<paper-dialog id="receiveDialog" entry-animation="scale-up-animation" exit-animation="fade-out-animation" with-backdrop modal>
|
||||
<h2>Text Received</h2>
|
||||
<div>
|
||||
<div id="receivedText">
|
||||
</div>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<paper-button dialog-dismiss>Close</paper-button>
|
||||
<paper-button on-tap="_copy" autofocus hidden$="{{!clipboardSupported}}">Copy</paper-button>
|
||||
<a href="tel:{{tel}}" hidden$="{{!tel}}">
|
||||
<paper-button autofocus dialog-dismiss>Call</paper-button>
|
||||
</a>
|
||||
<a href="{{url}}" hidden$="{{!url}}" target="_blank">
|
||||
<paper-button autofocus dialog-dismiss>Open</paper-button>
|
||||
</a>
|
||||
</div>
|
||||
</paper-dialog>
|
||||
</template>
|
||||
<script>
|
||||
'use strict';
|
||||
(function() {
|
||||
/*
|
||||
*
|
||||
* /^\+?[0-9x]*$/ is the first usuful Text sent via Snapdrop 2015/1/2 5:30
|
||||
*
|
||||
*/
|
||||
var phoneNumbers = /^\+?[0-9x/ ]*$/;
|
||||
var urls = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+)((?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\.\!\/\\\w]*))?)/;
|
||||
|
||||
Polymer({
|
||||
is: 'text-input-dialog',
|
||||
behaviors: [Snapdrop.ClipboardBehavior, Snapdrop.SoundNotificationBehavior],
|
||||
properties: {
|
||||
textToSend: {
|
||||
type: String
|
||||
},
|
||||
receivedText: {
|
||||
type: String
|
||||
},
|
||||
contact: {
|
||||
type: Object
|
||||
},
|
||||
tel: {
|
||||
computed: '_isPhoneNumber(receivedText)',
|
||||
value: false
|
||||
},
|
||||
url: {
|
||||
computed: '_isUrl(receivedText)',
|
||||
value: false
|
||||
},
|
||||
clipboardSupported: {
|
||||
value: false
|
||||
},
|
||||
fallback: {
|
||||
computed: '_isFallback(url,tel,clipboardSupported)',
|
||||
value: false
|
||||
}
|
||||
},
|
||||
open: function(contact) {
|
||||
this.contact = contact;
|
||||
this.$.sendDialog.open();
|
||||
},
|
||||
attached: function() {
|
||||
// clipboard must be initalized by user interaction
|
||||
var that = this;
|
||||
var hackListener = function() {
|
||||
document.body.removeEventListener('touchstart', hackListener, false);
|
||||
document.body.removeEventListener('click', hackListener, false);
|
||||
// wait 1s to tell the ui that copy is supported
|
||||
that.async(function() {
|
||||
that.clipboardSupported = document.queryCommandSupported && document.queryCommandSupported('copy');
|
||||
}, 1000);
|
||||
};
|
||||
document.body.addEventListener('touchstart', hackListener, false);
|
||||
document.body.addEventListener('click', hackListener, false);
|
||||
|
||||
|
||||
this.async(function() {
|
||||
app.conn.addEventListener('text-received', function(e) {
|
||||
var receivedText = e.detail.text;
|
||||
if (!receivedText || receivedText.trim() === '') {
|
||||
this.playSound();
|
||||
return;
|
||||
}
|
||||
this.receivedText = receivedText;
|
||||
this.$.receivedText.textContent = receivedText;
|
||||
window.linkifyElement(this.$.receivedText, {}, document);
|
||||
this.$.receiveDialog.open();
|
||||
this.playSound();
|
||||
}.bind(this), false);
|
||||
}, 200);
|
||||
|
||||
this.$.textInput.addEventListener('keypress', function(e) {
|
||||
if (e.which === 13 || e.charCode === 13) {
|
||||
var key;
|
||||
var isShift;
|
||||
if (window.event) {
|
||||
key = window.event.keyCode;
|
||||
isShift = !!window.event.shiftKey; // typecast to boolean
|
||||
} else {
|
||||
key = e.which;
|
||||
isShift = !!e.shiftKey;
|
||||
}
|
||||
if (!isShift) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this._send();
|
||||
}
|
||||
}
|
||||
}.bind(this), false);
|
||||
|
||||
|
||||
},
|
||||
_send: function() {
|
||||
this.$.sendDialog.close();
|
||||
app.conn.sendText(this.contact.peerId, this.textToSend);
|
||||
},
|
||||
_copy: function() {
|
||||
this.copyToClipboard(this.receivedText);
|
||||
|
||||
this.$.receiveDialog.close();
|
||||
console.log('text copied', this.receivedText);
|
||||
},
|
||||
_isPhoneNumber: function(text) {
|
||||
if (!text || text.length < 5 || text.length > 100) {
|
||||
return false;
|
||||
}
|
||||
if (phoneNumbers.test(text)) {
|
||||
return text;
|
||||
}
|
||||
|
||||
},
|
||||
_isUrl: function(text) {
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
if (urls.test(text)) {
|
||||
return text;
|
||||
}
|
||||
|
||||
},
|
||||
_isFallback: function(url, tel, clipboardSupported) {
|
||||
return (!url && !tel && !clipboardSupported);
|
||||
}
|
||||
});
|
||||
}());
|
||||
</script>
|
||||
</dom-module>
|
|
@ -1,229 +0,0 @@
|
|||
<link rel="import" href="../../bower_components/iron-flex-layout/iron-flex-layout.html">
|
||||
<link rel="import" href="../../bower_components/neon-animation/neon-shared-element-animatable-behavior.html">
|
||||
<link rel="import" href="../../bower_components/neon-animation/neon-animations.html">
|
||||
<link rel="import" href="../../bower_components/paper-styles/paper-styles-classes.html">
|
||||
<link rel="import" href="../../bower_components/iron-icon/iron-icon.html">
|
||||
<dom-module id="about-page">
|
||||
<template>
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
color: white;
|
||||
z-index: 3;
|
||||
--paper-tooltip: {
|
||||
font-size: 14px;
|
||||
background-color: white;
|
||||
color: #4285f4;
|
||||
}
|
||||
--paper-tooltip-opacity:0.95;
|
||||
}
|
||||
|
||||
#placeholder {
|
||||
opacity: 0;
|
||||
background-color: #4285f4;
|
||||
@apply(--layout-fit);
|
||||
}
|
||||
|
||||
#container {
|
||||
@apply(--layout-fit);
|
||||
@apply(--layout-vertical);
|
||||
@apply(--layout-center-center);
|
||||
background-color: #4285f4;
|
||||
padding: 64px 32px 64px 32px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
}
|
||||
|
||||
.slogan {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.paper-font-headline {
|
||||
margin-bottom: 8px;
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.center {
|
||||
@apply(--layout-vertical);
|
||||
@apply(--layout-center-center);
|
||||
}
|
||||
|
||||
#footer {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
margin-left: -160px;
|
||||
width: 320px;
|
||||
bottom: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media all and (max-height: 370px) {
|
||||
#footer {
|
||||
width: 320px;
|
||||
bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.donate-icon {
|
||||
padding-top: 32px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.paper-font-subhead {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
a.paper-font-subhead {
|
||||
padding-top: 32px;
|
||||
}
|
||||
|
||||
.share {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.social {
|
||||
margin: 16px;
|
||||
width: 228px;
|
||||
}
|
||||
|
||||
#btn,
|
||||
.social a {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.social a {
|
||||
text-decoration: none;
|
||||
padding: 2px 9px 0 9px;
|
||||
}
|
||||
|
||||
.share a:hover,
|
||||
#btn:hover {
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
<div id="placeholder"></div>
|
||||
<div id="container">
|
||||
<div class="share">
|
||||
<paper-icon-button id="btn" icon="chat:close" on-tap="_switch"></paper-icon-button>
|
||||
</div>
|
||||
<div class="center">
|
||||
<iron-icon icon="chat:wifi-tethering" class="logo"></iron-icon>
|
||||
<div class="paper-font-headline">Snapdrop</div>
|
||||
<div class="slogan">The easiest way to transfer files across devices.</div>
|
||||
<div class="social">
|
||||
<a href="https://twitter.com/intent/tweet?text=https://snapdrop.net%20by%20@robin_linus%20&" target="_blank">
|
||||
<iron-icon icon="chat:twitter"></iron-icon>
|
||||
<paper-tooltip position="bottom" offset="14">
|
||||
Tweet about Snapdrop!
|
||||
</paper-tooltip>
|
||||
</a>
|
||||
<a href="https://www.facebook.com/RobinLinus" target="_blank">
|
||||
<iron-icon icon="chat:facebook"></iron-icon>
|
||||
<paper-tooltip position="bottom" offset="14">
|
||||
Like my Facebook Page!
|
||||
</paper-tooltip>
|
||||
</a>
|
||||
<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&cmd=_donations&business=robin@capira.de¤cy_code=USD&item_name=Snapdrop&shipping=0" target="_blank">
|
||||
<iron-icon icon="chat:local-cafe"></iron-icon>
|
||||
<paper-tooltip position="bottom" offset="14">
|
||||
You like Snapdrop?
|
||||
<br> Buy me a cup of coffee!
|
||||
</paper-tooltip>
|
||||
</a>
|
||||
<a href="https://github.com/yougrow/snapdrop" target="_blank" class="github">
|
||||
<iron-icon icon="chat:github"></iron-icon>
|
||||
<paper-tooltip position="bottom" offset="14">
|
||||
Get involved!
|
||||
</paper-tooltip>
|
||||
</a>
|
||||
<a href="https://github.com/yougrow/snapdrop#frequently-asked-questions" target="_blank" class="github">
|
||||
<iron-icon icon="chat:help-outline"></iron-icon>
|
||||
<paper-tooltip position="bottom" offset="14">
|
||||
Frequently Asked Questions
|
||||
</paper-tooltip>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<span id="footer"><a href="https://twitter.com/robin_linus" target="_blank">Built with ♥ by Robin Linus</a></span>
|
||||
</div>
|
||||
</template>
|
||||
</dom-module>
|
||||
<script>
|
||||
(function() {
|
||||
Polymer({
|
||||
is: 'about-page',
|
||||
behaviors: [
|
||||
Polymer.NeonSharedElementAnimatableBehavior
|
||||
],
|
||||
properties: {
|
||||
animationConfig: {
|
||||
value: function() {
|
||||
return {
|
||||
'entry': [{
|
||||
name: 'ripple-animation',
|
||||
id: 'ripple',
|
||||
toPage: this
|
||||
}, {
|
||||
name: 'fade-out-animation',
|
||||
node: this.$.placeholder,
|
||||
timing: {
|
||||
delay: 250
|
||||
}
|
||||
}, {
|
||||
name: 'fade-in-animation',
|
||||
node: this.$.container,
|
||||
timing: {
|
||||
delay: 50
|
||||
}
|
||||
}],
|
||||
'exit': [{
|
||||
name: 'opaque-animation',
|
||||
node: this.$.placeholder
|
||||
}, {
|
||||
name: 'fade-out-animation',
|
||||
node: this.$.container,
|
||||
timing: {
|
||||
duration: 0
|
||||
}
|
||||
}, {
|
||||
name: 'reverse-ripple-animation',
|
||||
id: 'reverse-ripple',
|
||||
fromPage: this
|
||||
}]
|
||||
};
|
||||
}
|
||||
},
|
||||
sharedElements: {
|
||||
value: function() {
|
||||
return {
|
||||
'ripple': this.$.placeholder,
|
||||
'reverse-ripple': this.$.placeholder
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
_switch: function() {
|
||||
document.querySelector('#pages').select(0);
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
|
@ -1,86 +0,0 @@
|
|||
<dom-module id="settings-page">
|
||||
<template>
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
color: white;
|
||||
z-index: 3
|
||||
}
|
||||
|
||||
#placeholder {
|
||||
opacity: 0;
|
||||
background-color: #4285f4;
|
||||
@apply(--layout-fit);
|
||||
}
|
||||
|
||||
#container {
|
||||
@apply(--layout-fit);
|
||||
@apply(--layout-vertical);
|
||||
@apply(--layout-center-center);
|
||||
background-color: #4285f4;
|
||||
padding: 64px 32px 64px 32px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
</template>
|
||||
<script>
|
||||
'use strict';
|
||||
Polymer({
|
||||
is: 'settings-page',
|
||||
behaviors: [
|
||||
Polymer.NeonSharedElementAnimatableBehavior
|
||||
],
|
||||
properties: {
|
||||
animationConfig: {
|
||||
value: function() {
|
||||
return {
|
||||
'entry': [{
|
||||
name: 'ripple-animation',
|
||||
id: 'ripple',
|
||||
toPage: this
|
||||
}, {
|
||||
name: 'fade-out-animation',
|
||||
node: this.$.placeholder,
|
||||
timing: {
|
||||
delay: 250
|
||||
}
|
||||
}, {
|
||||
name: 'fade-in-animation',
|
||||
node: this.$.container,
|
||||
timing: {
|
||||
delay: 50
|
||||
}
|
||||
}],
|
||||
'exit': [{
|
||||
name: 'opaque-animation',
|
||||
node: this.$.placeholder
|
||||
}, {
|
||||
name: 'fade-out-animation',
|
||||
node: this.$.container,
|
||||
timing: {
|
||||
duration: 0
|
||||
}
|
||||
}, {
|
||||
name: 'reverse-ripple-animation',
|
||||
id: 'reverse-ripple',
|
||||
fromPage: this
|
||||
}]
|
||||
};
|
||||
}
|
||||
},
|
||||
sharedElements: {
|
||||
value: function() {
|
||||
return {
|
||||
'ripple': this.$.placeholder,
|
||||
'reverse-ripple': this.$.placeholder
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
_switch: function() {
|
||||
document.querySelector('#pages').select(0);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</dom-module>
|
|
@ -1,105 +0,0 @@
|
|||
<link rel="import" href="../../bower_components/iron-flex-layout/iron-flex-layout.html">
|
||||
<link rel="import" href="../../bower_components/neon-animation/neon-shared-element-animatable-behavior.html">
|
||||
<link rel="import" href="../../bower_components/neon-animation/neon-animations.html">
|
||||
<link rel="import" href="../../bower_components/paper-icon-button/paper-icon-button.html">
|
||||
<link rel="import" href="../../bower_components/paper-tooltip/paper-tooltip.html">
|
||||
<link rel="import" href="../../styles/icons.html">
|
||||
<link rel="import" href="../invitation-link/invitation-link.html">
|
||||
|
||||
<dom-module id="x-cards">
|
||||
<template>
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#placeholder {
|
||||
opacity: 0;
|
||||
background-color: grey;
|
||||
@apply(--layout-fit);
|
||||
}
|
||||
|
||||
paper-icon-button {
|
||||
color: #52524F;
|
||||
}
|
||||
|
||||
paper-icon-button:hover {
|
||||
color: #4285f4;
|
||||
}
|
||||
|
||||
.share {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.share a {
|
||||
color: #52524F;
|
||||
text-decoration: none;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.share a:hover,
|
||||
#btn:hover {
|
||||
color: #4285f4;
|
||||
}
|
||||
|
||||
</style>
|
||||
<div id="placeholder"></div>
|
||||
<div id="container">
|
||||
<div class="share">
|
||||
<paper-icon-button id="btn" icon="chat:info-outline" on-tap="_switch"></paper-icon-button>
|
||||
</div>
|
||||
<content select="div"></content>
|
||||
</div>
|
||||
</template>
|
||||
</dom-module>
|
||||
<script>
|
||||
(function() {
|
||||
Polymer({
|
||||
is: 'x-cards',
|
||||
behaviors: [
|
||||
Polymer.NeonSharedElementAnimatableBehavior
|
||||
],
|
||||
properties: {
|
||||
animationConfig: {
|
||||
value: function() {
|
||||
return {
|
||||
'entry': [{
|
||||
name: 'reverse-ripple-animation',
|
||||
id: 'reverse-ripple',
|
||||
toPage: this
|
||||
}],
|
||||
'exit': [{
|
||||
name: 'fade-out-animation',
|
||||
node: this.$.container,
|
||||
timing: {
|
||||
delay: 150,
|
||||
duration: 0
|
||||
}
|
||||
}, {
|
||||
name: 'ripple-animation',
|
||||
id: 'ripple',
|
||||
fromPage: this
|
||||
}]
|
||||
};
|
||||
}
|
||||
},
|
||||
sharedElements: {
|
||||
value: function() {
|
||||
return {
|
||||
'ripple': this.$.btn,
|
||||
'reverse-ripple': this.$.btn
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
_switch: function() {
|
||||
document.querySelector('#pages').select(1);
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
BIN
app/favicon.ico
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 33 KiB |
100
app/index.html
|
@ -1,100 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="generator" content="Snapdrop">
|
||||
<title>Snapdrop</title>
|
||||
<link rel="shortcut icon" href="favicon.ico?v=3" />
|
||||
<!-- Place favicon.ico in the `app/` directory -->
|
||||
<!-- Chrome for Android theme color -->
|
||||
<meta name="theme-color" content="#3367d6">
|
||||
<!-- Web Application Manifest -->
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<!-- Tile color for Win8 -->
|
||||
<meta name="msapplication-TileColor" content="#3372DF">
|
||||
<!-- Add to homescreen for Chrome on Android -->
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="application-name" content="PSK">
|
||||
<link rel="icon" sizes="192x192" href="images/touch/chrome-touch-icon-192x192.png">
|
||||
<link rel="fluid-icon" type="image/png" href="images/touch/chrome-touch-icon-192x192.png">
|
||||
<meta name="description" content="Snapdrop is an easy way to transfer files. Instantly share images, video, PDF, and links across devices. Peer2Peer, Private, Secure and Open Source. No Setup, No Signup.">
|
||||
<meta property="og:image" content="https://snapdrop.net/images/touch/chrome-splashscreen-icon-384x384.png" />
|
||||
<meta property="og:url" content="https://snapdrop.net/" />
|
||||
<meta name="twitter:image" content="https://snapdrop.net/images/touch/chrome-splashscreen-icon-384x384.png" />
|
||||
<meta name="twitter:author" content="@RobinLinus" />
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:author" content="https://facebook.com/RobinLinus" />
|
||||
<meta property="fb:pages" content="451189218422617" />
|
||||
<meta property="fb:profile_id" content="451189218422617" />
|
||||
<meta name="twitter:description" content="Snapdrop is an easy way to transfer files. Instantly share images, video, PDF, and links across devices. Peer2Peer, Private, Secure and Open Source. No Setup, No Signup.">
|
||||
<!-- Add to homescreen for Safari on iOS -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="Snapdrop">
|
||||
<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">
|
||||
<!-- build:css styles/main.css -->
|
||||
<link rel="stylesheet" href="styles/main.css">
|
||||
<!-- endbuild-->
|
||||
<!-- build:js bower_components/webcomponentsjs/webcomponents-lite.min.js async foo="1" -->
|
||||
<script src="bower_components/webcomponentsjs/webcomponents-lite.js"></script>
|
||||
<!-- endbuild -->
|
||||
<!-- Because this project uses vulcanize this should be your only html import
|
||||
in this file. All other imports should go in elements.html -->
|
||||
<link rel="import" href="elements/elements.html" async>
|
||||
<script>
|
||||
window.debug = true;
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body class="layout vertical">
|
||||
<script src="scripts/animated-bg.js" inline></script>
|
||||
<span id="browser-sync-binding"></span>
|
||||
<template is="dom-bind" id="app">
|
||||
<connection-wrapper me="{{me}}" loading="{{loading}}" buddies="{{buddies}}"></connection-wrapper>
|
||||
<neon-animated-pages id="pages" selected="0">
|
||||
<x-cards on-switch="_showAbout">
|
||||
<div>
|
||||
<paper-progress indeterminate hidden$="{{!loading}}"></paper-progress>
|
||||
<buddy-finder me="{{me}}" active$="{{loading}}" buddies="{{buddies}}"></buddy-finder>
|
||||
</div>
|
||||
</x-cards>
|
||||
<about-page on-switch="_showApp">
|
||||
</about-page>
|
||||
</neon-animated-pages>
|
||||
<file-receiver></file-receiver>
|
||||
<paper-toast id="toast" duration="6000">
|
||||
</paper-toast>
|
||||
<paper-toast id="caching-complete" duration="6000" text="Caching complete! This app will work offline.">
|
||||
</paper-toast>
|
||||
<donate-dialog></donate-dialog>
|
||||
<platinum-sw-register auto-register clients-claim skip-waiting base-uri="bower_components/platinum-sw/bootstrap" on-service-worker-installed="displayInstalledToast">
|
||||
<platinum-sw-cache default-cache-strategy="fastest" cache-config-file="cache-config.json">
|
||||
</platinum-sw-cache>
|
||||
</platinum-sw-register>
|
||||
</template>
|
||||
<!-- build:js scripts/app.js -->
|
||||
<script src="scripts/app.js"></script>
|
||||
<!-- endbuild-->
|
||||
<script>
|
||||
(function(i, s, o, g, r, a, m) {
|
||||
i['GoogleAnalyticsObject'] = r;
|
||||
i[r] = i[r] || function() {
|
||||
(i[r].q = i[r].q || []).push(arguments)
|
||||
}, i[r].l = 1 * new Date();
|
||||
a = s.createElement(o),
|
||||
m = s.getElementsByTagName(o)[0];
|
||||
a.async = 1;
|
||||
a.src = g;
|
||||
m.parentNode.insertBefore(a, m)
|
||||
})(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga');
|
||||
|
||||
ga('create', 'UA-71686975-1', 'auto');
|
||||
ga('send', 'pageview');
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,4 +0,0 @@
|
|||
# www.robotstxt.org
|
||||
|
||||
User-agent: *
|
||||
Disallow:
|
|
@ -1,65 +0,0 @@
|
|||
'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;
|
||||
var offset = h > 380 ? 100 : 65;
|
||||
x0 = w / 2;
|
||||
y0 = h - offset;
|
||||
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();
|
||||
}());
|
|
@ -1,47 +0,0 @@
|
|||
(function(document) {
|
||||
'use strict';
|
||||
|
||||
var app = document.querySelector('#app');
|
||||
|
||||
// Sets app default base URL
|
||||
app.baseUrl = '/';
|
||||
|
||||
|
||||
// don't display the install prompt if the user has *already* installed
|
||||
window.addEventListener('beforeinstallprompt', function(event) {
|
||||
if (window.matchMedia('(display-mode: standalone)').matches) {
|
||||
return event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
app.displayInstalledToast = function() {
|
||||
// Check to make sure caching is actually enabled—it won't be in the dev environment.
|
||||
if (!Polymer.dom(document).querySelector('platinum-sw-cache').disabled) {
|
||||
Polymer.dom(document).querySelector('#caching-complete').show();
|
||||
}
|
||||
};
|
||||
|
||||
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.conn = 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._showAbout=function(){
|
||||
document.querySelector('#pages').select(0);
|
||||
};
|
||||
})(document);
|
|
@ -1,35 +0,0 @@
|
|||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:400,300,500,700">
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<style is="custom-style">
|
||||
:root {
|
||||
--dark-primary-color: #303F9F;
|
||||
--default-primary-color: #3F51B5;
|
||||
--light-primary-color: #C5CAE9;
|
||||
--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;
|
||||
}
|
||||
|
||||
paper-progress {
|
||||
width: 100%;
|
||||
z-index: 10000;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
neon-animated-pages{
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
|
@ -1,64 +0,0 @@
|
|||
<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>
|
||||
<g id="info-outline">
|
||||
<path d="M11 17h2v-6h-2v6zm1-15C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zM11 9h2V7h-2v2z" />
|
||||
</g>
|
||||
<g id="close">
|
||||
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" />
|
||||
</g>
|
||||
<g id="local-cafe">
|
||||
<path d="M20 3H4v10c0 2.21 1.79 4 4 4h6c2.21 0 4-1.79 4-4v-3h2c1.11 0 2-.89 2-2V5c0-1.11-.89-2-2-2zm0 5h-2V5h2v3zM2 21h18v-2H2v2z" />
|
||||
</g>
|
||||
<g id="menu">
|
||||
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z" />
|
||||
</g>
|
||||
<g id="help-outline">
|
||||
<path d="M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z" />
|
||||
</g>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="twitter" x="0px" y="0px" width="430.117px" height="430.117px" viewBox="0 0 430.117 430.117" style="enable-background:new 0 0 430.117 430.117;" xml:space="preserve">
|
||||
<g>
|
||||
<path id="twitter1" d="M381.384,198.639c24.157-1.993,40.543-12.975,46.849-27.876 c-8.714,5.353-35.764,11.189-50.703,5.631c-0.732-3.51-1.55-6.844-2.353-9.854c-11.383-41.798-50.357-75.472-91.194-71.404 c3.304-1.334,6.655-2.576,9.996-3.691c4.495-1.61,30.868-5.901,26.715-15.21c-3.5-8.188-35.722,6.188-41.789,8.067 c8.009-3.012,21.254-8.193,22.673-17.396c-12.27,1.683-24.315,7.484-33.622,15.919c3.36-3.617,5.909-8.025,6.45-12.769 C241.68,90.963,222.563,133.113,207.092,174c-12.148-11.773-22.915-21.044-32.574-26.192 c-27.097-14.531-59.496-29.692-110.355-48.572c-1.561,16.827,8.322,39.201,36.8,54.08c-6.17-0.826-17.453,1.017-26.477,3.178 c3.675,19.277,15.677,35.159,48.169,42.839c-14.849,0.98-22.523,4.359-29.478,11.642c6.763,13.407,23.266,29.186,52.953,25.947 c-33.006,14.226-13.458,40.571,13.399,36.642C113.713,320.887,41.479,317.409,0,277.828 c108.299,147.572,343.716,87.274,378.799-54.866c26.285,0.224,41.737-9.105,51.318-19.39 C414.973,206.142,393.023,203.486,381.384,198.639z" />
|
||||
</g>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="facebook" x="0px" y="0px" width="90px" height="90px" viewBox="0 0 90 90" style="enable-background:new 0 0 90 90;" xml:space="preserve">
|
||||
<path id="Facebook__x28_alt_x29_" d="M90,15.001C90,7.119,82.884,0,75,0H15C7.116,0,0,7.119,0,15.001v59.998 C0,82.881,7.116,90,15.001,90H45V56H34V41h11v-5.844C45,25.077,52.568,16,61.875,16H74v15H61.875C60.548,31,59,32.611,59,35.024V41 h15v15H59v34h16c7.884,0,15-7.119,15-15.001V15.001z" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="github" x="0px" y="0px" width="438.549px" height="438.549px" viewBox="0 0 438.549 438.549" style="enable-background:new 0 0 438.549 438.549;" xml:space="preserve">
|
||||
<g>
|
||||
<path d="M409.132,114.573c-19.608-33.596-46.205-60.194-79.798-79.8C295.736,15.166,259.057,5.365,219.271,5.365 c-39.781,0-76.472,9.804-110.063,29.408c-33.596,19.605-60.192,46.204-79.8,79.8C9.803,148.168,0,184.854,0,224.63 c0,47.78,13.94,90.745,41.827,128.906c27.884,38.164,63.906,64.572,108.063,79.227c5.14,0.954,8.945,0.283,11.419-1.996 c2.475-2.282,3.711-5.14,3.711-8.562c0-0.571-0.049-5.708-0.144-15.417c-0.098-9.709-0.144-18.179-0.144-25.406l-6.567,1.136 c-4.187,0.767-9.469,1.092-15.846,1c-6.374-0.089-12.991-0.757-19.842-1.999c-6.854-1.231-13.229-4.086-19.13-8.559 c-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559 c-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-0.951-2.568-2.098-3.711-3.429c-1.142-1.331-1.997-2.663-2.568-3.997 c-0.572-1.335-0.098-2.43,1.427-3.289c1.525-0.859,4.281-1.276,8.28-1.276l5.708,0.853c3.807,0.763,8.516,3.042,14.133,6.851 c5.614,3.806,10.229,8.754,13.846,14.842c4.38,7.806,9.657,13.754,15.846,17.847c6.184,4.093,12.419,6.136,18.699,6.136 c6.28,0,11.704-0.476,16.274-1.423c4.565-0.952,8.848-2.383,12.847-4.285c1.713-12.758,6.377-22.559,13.988-29.41 c-10.848-1.14-20.601-2.857-29.264-5.14c-8.658-2.286-17.605-5.996-26.835-11.14c-9.235-5.137-16.896-11.516-22.985-19.126 c-6.09-7.614-11.088-17.61-14.987-29.979c-3.901-12.374-5.852-26.648-5.852-42.826c0-23.035,7.52-42.637,22.557-58.817 c-7.044-17.318-6.379-36.732,1.997-58.24c5.52-1.715,13.706-0.428,24.554,3.853c10.85,4.283,18.794,7.952,23.84,10.994 c5.046,3.041,9.089,5.618,12.135,7.708c17.705-4.947,35.976-7.421,54.818-7.421s37.117,2.474,54.823,7.421l10.849-6.849 c7.419-4.57,16.18-8.758,26.262-12.565c10.088-3.805,17.802-4.853,23.134-3.138c8.562,21.509,9.325,40.922,2.279,58.24 c15.036,16.18,22.559,35.787,22.559,58.817c0,16.178-1.958,30.497-5.853,42.966c-3.9,12.471-8.941,22.457-15.125,29.979 c-6.191,7.521-13.901,13.85-23.131,18.986c-9.232,5.14-18.182,8.85-26.84,11.136c-8.662,2.286-18.415,4.004-29.263,5.146 c9.894,8.562,14.842,22.077,14.842,40.539v60.237c0,3.422,1.19,6.279,3.572,8.562c2.379,2.279,6.136,2.95,11.276,1.995 c44.163-14.653,80.185-41.062,108.068-79.226c27.88-38.161,41.825-81.126,41.825-128.906 C438.536,184.851,428.728,148.168,409.132,114.573z" />
|
||||
</svg>
|
||||
</defs>
|
||||
</svg>
|
||||
</iron-iconset-svg>
|
|
@ -1,16 +0,0 @@
|
|||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #fafafa;
|
||||
font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
color: #333;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
importScripts('bower_components/platinum-sw/service-worker.js');
|
22
bower.json
|
@ -1,22 +0,0 @@
|
|||
{
|
||||
"name": "sharewithme",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"iron-elements": "PolymerElements/iron-elements#^1.0.0",
|
||||
"neon-elements": "PolymerElements/neon-elements#^1.0.0",
|
||||
"page": "visionmedia/page.js#~1.6.4",
|
||||
"paper-elements": "PolymerElements/paper-elements#^1.0.1",
|
||||
"platinum-elements": "PolymerElements/platinum-elements#^1.1.0",
|
||||
"polymer": "Polymer/polymer#^1.2.0",
|
||||
"paper-menu": "PolymerElements/paper-menu#4fecb43601",
|
||||
"peerjs": "~0.3.14",
|
||||
"clipboard": "~1.5.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"web-component-tester": "*"
|
||||
},
|
||||
"ignore": [],
|
||||
"resolutions": {
|
||||
"paper-menu": "4fecb43601"
|
||||
}
|
||||
}
|
BIN
client/images/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
client/images/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
client/images/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
client/images/favicon-96x96.png
Normal file
After Width: | Height: | Size: 8.9 KiB |
BIN
client/images/logo_blue_512x512.png
Normal file
After Width: | Height: | Size: 68 KiB |
BIN
client/images/logo_transparent_128x128.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
client/images/logo_transparent_512x512.png
Normal file
After Width: | Height: | Size: 78 KiB |
BIN
client/images/logo_transparent_white_512x512.png
Normal file
After Width: | Height: | Size: 52 KiB |
BIN
client/images/logo_white_512x512.png
Normal file
After Width: | Height: | Size: 62 KiB |
BIN
client/images/mstile-150x150.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
251
client/images/safari-pinned-tab.svg
Normal file
|
@ -0,0 +1,251 @@
|
|||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.11, written by Peter Selinger 2001-2013
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M2325 4214 c-225 -34 -366 -76 -544 -161 -361 -172 -651 -455 -830
|
||||
-809 -135 -268 -194 -532 -186 -839 2 -88 6 -173 9 -190 3 -16 8 -43 11 -60 3
|
||||
-16 7 -41 10 -55 3 -14 7 -36 10 -50 9 -51 54 -188 86 -265 94 -229 217 -413
|
||||
389 -585 111 -111 139 -136 212 -186 29 -20 60 -42 68 -49 8 -7 34 -22 57 -35
|
||||
36 -20 43 -21 55 -9 7 8 14 20 15 26 2 7 20 38 40 70 21 32 38 63 38 68 0 6
|
||||
10 9 22 9 12 -1 21 4 20 10 -1 6 3 10 8 9 6 -1 12 7 12 18 2 18 -1 17 -19 -6
|
||||
-29 -38 -32 -25 -5 24 13 22 26 38 30 35 3 -3 4 2 2 13 -2 10 -4 20 -4 23 -1
|
||||
3 -21 16 -46 29 -28 15 -44 29 -41 37 3 8 0 11 -10 7 -18 -7 -46 16 -37 31 3
|
||||
6 3 8 -2 4 -5 -5 -24 4 -44 19 -55 40 -180 166 -181 181 0 7 -4 10 -10 7 -5
|
||||
-3 -10 1 -10 9 0 9 -4 16 -9 16 -4 0 -27 26 -49 58 -29 42 -38 62 -30 70 7 7
|
||||
5 10 -7 9 -10 -1 -20 4 -23 12 -3 9 0 11 9 6 8 -5 11 -4 6 1 -5 5 -14 9 -20 9
|
||||
-7 0 -13 9 -15 19 -2 11 -10 26 -18 34 -8 7 -12 18 -9 23 4 5 2 9 -2 9 -5 0
|
||||
-14 9 -21 21 -10 15 -10 24 -2 34 8 10 8 15 -1 21 -8 4 -9 3 -5 -4 4 -7 3 -12
|
||||
-2 -12 -9 0 -39 69 -47 108 -2 11 -8 29 -13 39 -5 10 -8 28 -7 40 1 12 -2 20
|
||||
-7 17 -4 -3 -9 4 -9 15 -2 23 -3 29 -12 51 -11 29 -29 161 -23 175 3 8 2 15
|
||||
-2 15 -4 0 -7 24 -8 54 -1 48 1 54 17 51 10 -2 21 0 24 4 2 5 -5 8 -18 7 -19
|
||||
-1 -22 4 -22 34 0 27 4 35 17 34 10 -1 15 3 12 8 -3 5 -12 7 -20 4 -17 -7 -18
|
||||
6 -1 23 6 8 7 11 2 8 -7 -3 -9 9 -7 34 3 25 8 37 16 32 8 -4 8 -3 0 6 -8 9 -9
|
||||
25 -2 54 5 23 12 51 14 62 2 11 6 30 9 43 3 12 7 32 10 43 11 53 28 95 43 107
|
||||
10 7 11 12 4 12 -9 0 -9 5 -3 18 5 9 22 45 38 80 19 42 32 61 42 57 11 -4 11
|
||||
-2 2 9 -7 8 -8 16 -4 18 4 1 18 23 31 48 24 47 50 85 92 130 14 15 30 36 36
|
||||
46 6 11 16 19 22 19 6 0 14 4 19 9 5 5 3 6 -4 2 -20 -11 -15 1 11 29 14 15 29
|
||||
24 34 21 6 -3 9 2 8 12 0 10 6 16 16 16 10 -1 15 3 12 8 -7 10 36 45 49 40 5
|
||||
-1 6 2 3 7 -8 12 21 34 34 26 6 -4 9 -2 8 3 -4 12 52 62 70 62 6 0 12 4 12 8
|
||||
0 4 15 16 33 27 17 10 34 21 37 25 12 16 101 51 111 44 8 -5 9 -3 4 6 -6 10
|
||||
-4 12 9 7 10 -4 15 -3 11 3 -6 10 56 40 88 43 9 1 17 5 17 10 0 4 4 6 8 3 4
|
||||
-2 14 -1 22 4 9 5 19 5 27 -2 11 -8 12 -7 7 7 -5 12 -4 16 4 11 6 -3 13 -2 16
|
||||
3 4 5 14 8 24 7 9 -2 19 0 22 5 3 4 21 10 40 12 19 3 35 6 35 7 0 3 42 10 87
|
||||
14 26 2 51 7 56 10 6 3 13 0 15 -6 4 -10 6 -10 6 0 1 16 151 17 151 1 0 -6 6
|
||||
-4 14 3 17 17 89 12 79 -6 -4 -6 1 -5 10 3 9 7 17 10 17 5 0 -5 19 -9 43 -10
|
||||
43 -1 91 -7 137 -19 14 -4 32 -8 40 -9 8 -1 22 -5 30 -8 8 -3 51 -17 95 -32
|
||||
43 -15 82 -30 85 -34 3 -4 12 -8 20 -10 16 -3 121 -53 130 -62 3 -3 17 -11 32
|
||||
-19 15 -8 36 -22 47 -32 10 -10 21 -15 25 -12 3 3 8 -2 12 -11 3 -9 11 -16 17
|
||||
-16 33 0 237 -202 320 -317 180 -253 268 -536 262 -847 -1 -83 -5 -162 -9
|
||||
-176 -3 -13 -8 -41 -11 -61 -3 -20 -10 -50 -15 -68 -5 -17 -9 -35 -9 -39 -1
|
||||
-4 -13 -43 -27 -87 -35 -107 -110 -257 -180 -357 -78 -112 -258 -290 -366
|
||||
-362 -49 -32 -88 -62 -88 -67 0 -4 10 -25 21 -46 33 -60 142 -247 146 -253 3
|
||||
-2 18 3 34 12 16 10 37 15 47 12 14 -5 15 -4 2 5 -8 6 -12 11 -7 12 4 1 10 2
|
||||
15 3 4 0 18 11 31 23 13 12 27 20 31 18 5 -3 11 2 14 11 4 9 13 14 22 10 8 -3
|
||||
12 -2 9 3 -7 12 84 83 96 75 5 -3 6 2 3 10 -4 11 0 16 11 16 9 0 13 5 10 10
|
||||
-6 10 9 15 30 10 5 -1 7 2 3 6 -11 10 12 35 25 27 5 -3 7 -2 4 4 -4 5 5 20 18
|
||||
31 14 12 25 27 25 34 0 6 3 9 6 6 6 -7 33 18 51 48 6 10 16 18 23 16 6 -1 9 2
|
||||
5 7 -3 6 2 18 12 28 10 10 30 38 46 61 15 23 32 42 37 42 6 0 9 6 8 13 -2 6 3
|
||||
11 11 9 8 -2 11 3 8 11 -7 19 21 59 35 51 6 -4 8 1 3 15 -4 15 -2 21 9 21 9 0
|
||||
13 6 10 14 -3 8 0 17 5 21 6 3 9 11 6 16 -4 5 -2 9 3 9 4 0 15 16 22 35 9 24
|
||||
19 34 28 30 12 -4 13 -3 2 10 -9 11 -9 15 -1 15 7 0 10 4 7 9 -3 5 3 28 13 52
|
||||
17 40 22 54 32 84 1 6 10 17 19 25 9 8 10 11 3 6 -10 -6 -10 1 1 32 8 23 20
|
||||
68 27 101 6 34 15 59 18 56 4 -2 5 10 3 27 -3 17 -1 28 4 25 5 -3 9 11 10 31
|
||||
1 97 5 133 12 129 4 -3 7 6 7 19 0 13 -3 24 -6 24 -4 0 -3 17 0 38 4 20 6 61
|
||||
4 90 -1 29 2 50 7 47 5 -3 12 0 16 6 4 8 3 9 -4 5 -16 -10 -34 28 -20 42 9 9
|
||||
8 12 -2 12 -13 0 -13 2 0 10 12 8 12 10 1 10 -11 0 -11 3 0 17 8 9 9 14 3 10
|
||||
-12 -7 -29 97 -19 114 4 5 2 9 -2 9 -5 0 -10 14 -11 31 -1 17 -7 39 -12 49 -6
|
||||
10 -5 21 1 28 5 7 6 10 2 7 -8 -6 -65 168 -67 202 -1 11 -5 19 -10 15 -5 -3
|
||||
-7 2 -3 11 4 10 2 17 -4 17 -6 0 -8 7 -5 16 3 8 2 12 -4 9 -9 -6 -14 8 -11 28
|
||||
0 4 -4 7 -10 7 -5 0 -8 4 -5 9 4 5 1 11 -4 13 -6 2 -12 10 -14 18 -1 8 -8 25
|
||||
-14 38 -7 12 -9 22 -5 22 4 0 -1 6 -12 13 -11 9 -15 19 -10 27 5 9 4 11 -3 6
|
||||
-7 -4 -12 -2 -12 4 0 6 -11 27 -25 47 -13 20 -21 41 -18 47 3 6 2 8 -2 3 -11
|
||||
-9 -46 43 -38 57 3 6 3 8 -2 4 -4 -4 -24 14 -44 40 -45 59 -49 64 -89 107 -19
|
||||
20 -30 40 -26 47 4 6 3 8 -4 5 -11 -8 -112 85 -112 103 0 6 -4 9 -9 5 -5 -3
|
||||
-17 5 -25 17 -9 12 -16 18 -16 13 0 -6 -3 -6 -8 0 -11 16 -111 89 -182 134
|
||||
-36 22 -69 44 -75 48 -5 5 -45 24 -87 44 -43 20 -74 40 -70 44 4 5 2 5 -4 2
|
||||
-11 -6 -54 7 -109 33 -23 11 -128 43 -195 58 -84 20 -104 24 -230 41 -91 13
|
||||
-339 10 -435 -5z"/>
|
||||
<path d="M2470 3856 c0 -2 8 -10 18 -17 15 -13 16 -12 3 4 -13 16 -21 21 -21
|
||||
13z"/>
|
||||
<path d="M2333 3825 c0 -8 4 -12 9 -9 5 3 6 10 3 15 -9 13 -12 11 -12 -6z"/>
|
||||
<path d="M2365 3820 c-3 -6 1 -7 9 -4 18 7 21 14 7 14 -6 0 -13 -4 -16 -10z"/>
|
||||
<path d="M1993 3715 c0 -8 4 -12 9 -9 5 3 6 10 3 15 -9 13 -12 11 -12 -6z"/>
|
||||
<path d="M1936 3658 c3 -5 10 -6 15 -3 13 9 11 12 -6 12 -8 0 -12 -4 -9 -9z"/>
|
||||
<path d="M1710 3555 c5 -7 6 -16 2 -21 -4 -5 -3 -5 2 -1 15 12 15 34 0 34 -9
|
||||
0 -11 -4 -4 -12z"/>
|
||||
<path d="M1650 3515 c5 -7 6 -16 2 -21 -4 -5 -3 -5 2 -1 15 12 15 34 0 34 -9
|
||||
0 -11 -4 -4 -12z"/>
|
||||
<path d="M2425 3508 c-152 -17 -331 -84 -468 -175 -88 -57 -238 -206 -292
|
||||
-288 -86 -132 -146 -280 -170 -423 -16 -99 -14 -299 5 -377 6 -27 14 -61 16
|
||||
-74 10 -48 65 -178 108 -254 48 -84 140 -202 181 -233 14 -10 25 -25 25 -32 0
|
||||
-7 4 -11 8 -8 4 2 23 -9 42 -26 31 -27 94 -72 127 -91 7 -5 24 13 44 47 18 30
|
||||
36 53 40 50 4 -2 6 4 5 14 0 9 4 16 11 15 7 -2 10 3 7 11 -3 7 2 21 11 30 9 9
|
||||
13 16 9 16 -3 0 -2 6 3 13 22 28 75 130 65 123 -13 -8 -100 48 -109 70 -3 8
|
||||
-9 14 -14 14 -17 0 -99 95 -94 108 3 8 0 10 -7 6 -8 -5 -9 -2 -5 10 4 10 3 15
|
||||
-3 11 -12 -7 -44 41 -35 55 4 6 1 9 -5 8 -12 -3 -51 87 -42 96 3 4 1 6 -5 6
|
||||
-18 0 -37 121 -36 225 2 84 7 140 13 140 2 0 5 16 14 59 3 16 13 38 23 49 10
|
||||
11 13 17 6 14 -8 -5 -8 -1 0 18 8 16 17 23 27 19 11 -4 12 -2 3 7 -9 9 -6 20
|
||||
10 49 34 58 79 110 90 103 6 -3 8 -2 4 3 -3 5 1 18 9 29 8 10 14 16 14 12 0
|
||||
-4 13 7 29 24 17 17 35 28 41 24 6 -3 9 -1 8 7 -2 7 5 12 14 12 10 -1 16 3 15
|
||||
9 -2 11 124 77 136 70 4 -2 7 1 7 7 0 7 6 10 14 7 8 -3 16 -2 18 3 2 4 19 11
|
||||
38 15 19 3 51 9 70 12 46 9 186 9 230 0 19 -4 50 -10 67 -13 19 -4 30 -11 26
|
||||
-17 -3 -6 -1 -7 6 -3 16 10 53 -3 45 -16 -4 -7 -2 -8 5 -4 16 11 83 -22 75
|
||||
-36 -4 -6 -3 -8 4 -5 14 9 43 -3 36 -14 -3 -5 1 -7 8 -4 7 3 27 -8 44 -23 17
|
||||
-15 47 -41 66 -58 53 -47 114 -133 152 -215 96 -207 83 -452 -34 -649 -43 -74
|
||||
-145 -181 -213 -227 -53 -35 -60 -12 57 -210 l78 -132 24 15 c13 9 24 13 24 9
|
||||
0 -5 8 2 19 13 10 12 21 19 25 15 3 -3 6 -1 6 6 0 8 6 11 16 7 8 -3 12 -2 9 4
|
||||
-3 6 -1 10 6 10 7 0 9 3 6 7 -4 3 9 17 28 30 19 13 35 29 35 35 0 6 10 3 22
|
||||
-8 16 -14 19 -14 10 -2 -11 14 -9 21 18 50 18 18 28 26 24 18 -4 -9 -3 -12 2
|
||||
-7 5 5 9 16 9 25 0 9 11 22 25 29 14 7 19 13 12 13 -10 0 -9 4 2 16 9 8 21 24
|
||||
27 35 9 17 13 18 27 7 15 -12 16 -11 4 4 -12 15 -11 22 7 47 12 16 21 32 21
|
||||
36 0 4 9 20 21 36 11 16 17 29 13 29 -4 0 1 7 12 16 10 8 12 12 4 8 -12 -6
|
||||
-13 -5 -4 7 6 8 17 35 25 62 7 26 16 47 20 47 4 0 6 6 6 13 -3 34 28 165 37
|
||||
160 8 -5 8 -3 0 8 -9 13 -10 25 -4 47 6 21 4 172 -3 172 -4 0 -3 10 4 21 6 12
|
||||
7 19 1 15 -5 -3 -12 15 -16 42 -3 26 -9 61 -12 77 -4 17 -7 36 -7 43 0 6 -4
|
||||
12 -9 12 -5 0 -7 4 -3 9 3 5 1 12 -4 15 -5 4 -12 24 -16 46 -4 22 -10 40 -14
|
||||
40 -5 0 -15 18 -23 39 -15 36 -14 39 1 35 10 -4 9 -1 -4 7 -29 18 -45 42 -39
|
||||
59 3 8 2 11 -2 7 -4 -4 -16 5 -25 20 -14 21 -15 29 -6 36 8 6 6 7 -6 3 -11 -4
|
||||
-20 1 -24 12 -4 9 -13 22 -21 28 -8 6 -12 16 -8 23 4 6 4 10 -1 9 -5 -2 -38
|
||||
25 -73 59 -36 34 -89 80 -117 101 -29 21 -49 43 -45 47 4 5 2 5 -5 2 -6 -4
|
||||
-19 0 -27 9 -9 8 -16 13 -16 10 0 -4 -17 4 -37 17 -162 99 -432 151 -658 125z"/>
|
||||
<path d="M1435 3301 c-3 -5 -2 -12 3 -15 5 -3 9 1 9 9 0 17 -3 19 -12 6z"/>
|
||||
<path d="M1461 3276 c-9 -11 -9 -16 1 -22 7 -4 10 -4 6 1 -4 4 -3 14 3 22 6 7
|
||||
9 13 6 13 -2 0 -10 -6 -16 -14z"/>
|
||||
<path d="M1389 3217 c6 -8 7 -18 3 -22 -4 -5 -1 -5 6 -1 10 6 10 11 1 22 -6 8
|
||||
-14 14 -16 14 -3 0 0 -6 6 -13z"/>
|
||||
<path d="M1350 3200 c0 -5 5 -10 11 -10 5 0 7 5 4 10 -3 6 -8 10 -11 10 -2 0
|
||||
-4 -4 -4 -10z"/>
|
||||
<path d="M2520 3140 c-9 -6 -10 -10 -3 -10 6 0 15 5 18 10 8 12 4 12 -15 0z"/>
|
||||
<path d="M1339 3113 c-13 -16 -12 -17 4 -4 16 13 21 21 13 21 -2 0 -10 -8 -17
|
||||
-17z"/>
|
||||
<path d="M2595 3120 c-3 -5 -1 -10 4 -10 6 0 11 5 11 10 0 6 -2 10 -4 10 -3 0
|
||||
-8 -4 -11 -10z"/>
|
||||
<path d="M1336 3075 c-9 -26 -7 -32 5 -12 6 10 9 21 6 23 -2 3 -7 -2 -11 -11z"/>
|
||||
<path d="M2330 3079 c0 -5 5 -7 10 -4 6 3 10 8 10 11 0 2 -4 4 -10 4 -5 0 -10
|
||||
-5 -10 -11z"/>
|
||||
<path d="M2239 3068 c-5 -18 -6 -38 -1 -34 7 8 12 36 6 36 -2 0 -4 -1 -5 -2z"/>
|
||||
<path d="M1274 3049 c-3 -6 -2 -15 3 -20 5 -5 9 -1 9 11 0 23 -2 24 -12 9z"/>
|
||||
<path d="M2185 3020 c-3 -6 1 -7 9 -4 18 7 21 14 7 14 -6 0 -13 -4 -16 -10z"/>
|
||||
<path d="M2255 3019 c-3 -4 2 -6 10 -5 21 3 28 13 10 13 -9 0 -18 -4 -20 -8z"/>
|
||||
<path d="M2153 2995 c0 -8 4 -12 9 -9 5 3 6 10 3 15 -9 13 -12 11 -12 -6z"/>
|
||||
<path d="M2116 2982 c-3 -5 1 -9 9 -9 8 0 12 4 9 9 -3 4 -7 8 -9 8 -2 0 -6 -4
|
||||
-9 -8z"/>
|
||||
<path d="M2086 2962 c-3 -5 1 -9 9 -9 8 0 12 4 9 9 -3 4 -7 8 -9 8 -2 0 -6 -4
|
||||
-9 -8z"/>
|
||||
<path d="M1216 2922 c-3 -5 1 -9 9 -9 8 0 12 4 9 9 -3 4 -7 8 -9 8 -2 0 -6 -4
|
||||
-9 -8z"/>
|
||||
<path d="M2070 2919 c0 -5 5 -7 10 -4 6 3 10 8 10 11 0 2 -4 4 -10 4 -5 0 -10
|
||||
-5 -10 -11z"/>
|
||||
<path d="M1210 2896 c0 -2 8 -10 18 -17 15 -13 16 -12 3 4 -13 16 -21 21 -21
|
||||
13z"/>
|
||||
<path d="M1195 2860 c-3 -5 -1 -10 4 -10 6 0 11 5 11 10 0 6 -2 10 -4 10 -3 0
|
||||
-8 -4 -11 -10z"/>
|
||||
<path d="M1256 2858 c3 -5 10 -6 15 -3 13 9 11 12 -6 12 -8 0 -12 -4 -9 -9z"/>
|
||||
<path d="M1993 2835 c0 -8 4 -12 9 -9 4 3 8 9 8 15 0 5 -4 9 -8 9 -5 0 -9 -7
|
||||
-9 -15z"/>
|
||||
<path d="M2030 2839 c0 -5 5 -7 10 -4 6 3 10 8 10 11 0 2 -4 4 -10 4 -5 0 -10
|
||||
-5 -10 -11z"/>
|
||||
<path d="M1190 2825 c0 -7 30 -13 34 -7 3 4 -4 9 -15 9 -10 1 -19 0 -19 -2z"/>
|
||||
<path d="M1193 2803 c4 -3 1 -13 -6 -22 -11 -14 -10 -14 5 -2 16 12 16 31 1
|
||||
31 -4 0 -3 -3 0 -7z"/>
|
||||
<path d="M2493 2795 c-122 -27 -209 -94 -260 -202 -64 -138 -24 -318 92 -415
|
||||
39 -33 101 -68 120 -68 8 0 15 -4 15 -8 0 -13 143 -14 196 -1 27 7 68 25 91
|
||||
41 23 15 47 28 53 28 6 0 9 3 7 8 -3 4 1 13 9 20 8 7 14 9 14 5 1 -4 10 9 21
|
||||
30 11 20 24 38 29 38 6 1 14 2 19 3 5 0 13 4 17 8 3 4 -3 6 -15 3 -23 -4 -29
|
||||
10 -7 18 7 3 14 16 14 29 2 41 9 63 20 63 6 0 14 4 19 8 4 5 1 7 -7 5 -12 -2
|
||||
-15 7 -15 45 0 26 -4 47 -9 47 -5 0 -2 8 5 17 10 11 10 14 2 9 -8 -4 -13 -2
|
||||
-13 8 0 8 -5 27 -11 43 -6 15 -11 30 -12 33 -2 7 -24 34 -64 80 -40 45 -132
|
||||
96 -193 105 -25 3 -52 8 -60 10 -8 2 -43 -3 -77 -10z"/>
|
||||
<path d="M1953 2775 c0 -8 4 -12 9 -9 5 3 6 10 3 15 -9 13 -12 11 -12 -6z"/>
|
||||
<path d="M1987 2779 c7 -7 15 -10 18 -7 3 3 -2 9 -12 12 -14 6 -15 5 -6 -5z"/>
|
||||
<path d="M4375 2761 c-3 -5 -2 -12 3 -15 5 -3 9 1 9 9 0 17 -3 19 -12 6z"/>
|
||||
<path d="M3630 2739 c0 -5 5 -7 10 -4 6 3 10 8 10 11 0 2 -4 4 -10 4 -5 0 -10
|
||||
-5 -10 -11z"/>
|
||||
<path d="M1929 2723 c-13 -16 -12 -17 4 -4 16 13 21 21 13 21 -2 0 -10 -8 -17
|
||||
-17z"/>
|
||||
<path d="M1987 2719 c7 -7 15 -10 18 -7 3 3 -2 9 -12 12 -14 6 -15 5 -6 -5z"/>
|
||||
<path d="M1949 2683 c-13 -16 -12 -17 4 -4 16 13 21 21 13 21 -2 0 -10 -8 -17
|
||||
-17z"/>
|
||||
<path d="M1910 2681 c0 -6 4 -12 8 -15 5 -3 9 1 9 9 0 8 -4 15 -9 15 -4 0 -8
|
||||
-4 -8 -9z"/>
|
||||
<path d="M1155 2661 c-3 -5 -2 -12 3 -15 5 -3 9 1 9 9 0 17 -3 19 -12 6z"/>
|
||||
<path d="M1894 2640 c0 -13 4 -16 10 -10 7 7 7 13 0 20 -6 6 -10 3 -10 -10z"/>
|
||||
<path d="M3667 2639 c7 -7 15 -10 18 -7 3 3 -2 9 -12 12 -14 6 -15 5 -6 -5z"/>
|
||||
<path d="M1879 2593 c-13 -16 -12 -17 4 -4 16 13 21 21 13 21 -2 0 -10 -8 -17
|
||||
-17z"/>
|
||||
<path d="M4416 2542 c-3 -5 1 -9 9 -9 8 0 12 4 9 9 -3 4 -7 8 -9 8 -2 0 -6 -4
|
||||
-9 -8z"/>
|
||||
<path d="M1894 2476 c1 -8 5 -18 8 -22 4 -3 5 1 4 10 -1 8 -5 18 -8 22 -4 3
|
||||
-5 -1 -4 -10z"/>
|
||||
<path d="M2936 2447 c3 -10 9 -15 12 -12 3 3 0 11 -7 18 -10 9 -11 8 -5 -6z"/>
|
||||
<path d="M4415 2441 c-3 -5 -2 -12 3 -15 5 -3 9 1 9 9 0 17 -3 19 -12 6z"/>
|
||||
<path d="M1890 2421 c0 -6 4 -13 10 -16 6 -3 7 1 4 9 -7 18 -14 21 -14 7z"/>
|
||||
<path d="M1186 2358 c3 -5 10 -6 15 -3 13 9 11 12 -6 12 -8 0 -12 -4 -9 -9z"/>
|
||||
<path d="M1865 2360 c-3 -6 1 -7 9 -4 18 7 21 14 7 14 -6 0 -13 -4 -16 -10z"/>
|
||||
<path d="M2926 2258 c3 -5 10 -6 15 -3 13 9 11 12 -6 12 -8 0 -12 -4 -9 -9z"/>
|
||||
<path d="M1153 2235 c0 -8 4 -12 9 -9 5 3 6 10 3 15 -9 13 -12 11 -12 -6z"/>
|
||||
<path d="M3633 2215 c0 -8 4 -12 9 -9 5 3 6 10 3 15 -9 13 -12 11 -12 -6z"/>
|
||||
<path d="M2873 2175 c0 -8 4 -12 9 -9 5 3 6 10 3 15 -9 13 -12 11 -12 -6z"/>
|
||||
<path d="M1186 2158 c3 -5 10 -6 15 -3 13 9 11 12 -6 12 -8 0 -12 -4 -9 -9z"/>
|
||||
<path d="M3653 2149 c-2 -23 3 -25 10 -4 4 8 3 16 -1 19 -4 3 -9 -4 -9 -15z"/>
|
||||
<path d="M4385 2120 c-3 -6 1 -7 9 -4 18 7 21 14 7 14 -6 0 -13 -4 -16 -10z"/>
|
||||
<path d="M2770 2099 c0 -5 5 -7 10 -4 6 3 10 8 10 11 0 2 -4 4 -10 4 -5 0 -10
|
||||
-5 -10 -11z"/>
|
||||
<path d="M1230 1939 c0 -5 5 -7 10 -4 6 3 10 8 10 11 0 2 -4 4 -10 4 -5 0 -10
|
||||
-5 -10 -11z"/>
|
||||
<path d="M3519 1833 c-13 -16 -12 -17 4 -4 16 13 21 21 13 21 -2 0 -10 -8 -17
|
||||
-17z"/>
|
||||
<path d="M4260 1846 c0 -2 8 -10 18 -17 15 -13 16 -12 3 4 -13 16 -21 21 -21
|
||||
13z"/>
|
||||
<path d="M4251 1804 c0 -11 3 -14 6 -6 3 7 2 16 -1 19 -3 4 -6 -2 -5 -13z"/>
|
||||
<path d="M2210 1779 c0 -5 5 -7 10 -4 6 3 10 8 10 11 0 2 -4 4 -10 4 -5 0 -10
|
||||
-5 -10 -11z"/>
|
||||
<path d="M3467 1779 c7 -7 15 -10 18 -7 3 3 -2 9 -12 12 -14 6 -15 5 -6 -5z"/>
|
||||
<path d="M3430 1739 c0 -5 5 -7 10 -4 6 3 10 8 10 11 0 2 -4 4 -10 4 -5 0 -10
|
||||
-5 -10 -11z"/>
|
||||
<path d="M4230 1746 c0 -2 7 -7 16 -10 8 -3 12 -2 9 4 -6 10 -25 14 -25 6z"/>
|
||||
<path d="M3396 1713 c-6 -14 -5 -15 5 -6 7 7 10 15 7 18 -3 3 -9 -2 -12 -12z"/>
|
||||
<path d="M2136 1679 c4 -8 30 -6 38 2 3 3 -5 5 -19 5 -13 0 -22 -3 -19 -7z"/>
|
||||
<path d="M4255 1680 c-3 -5 -1 -10 4 -10 6 0 11 5 11 10 0 6 -2 10 -4 10 -3 0
|
||||
-8 -4 -11 -10z"/>
|
||||
<path d="M3354 1662 c4 -3 14 -7 22 -8 9 -1 13 0 10 4 -4 3 -14 7 -22 8 -9 1
|
||||
-13 0 -10 -4z"/>
|
||||
<path d="M3296 1638 c3 -5 10 -6 15 -3 13 9 11 12 -6 12 -8 0 -12 -4 -9 -9z"/>
|
||||
<path d="M4236 1638 c3 -5 10 -6 15 -3 13 9 11 12 -6 12 -8 0 -12 -4 -9 -9z"/>
|
||||
<path d="M3294 1595 c0 -13 3 -22 7 -19 8 4 6 30 -2 38 -3 3 -5 -5 -5 -19z"/>
|
||||
<path d="M1435 1600 c-3 -5 -1 -10 4 -10 6 0 11 5 11 10 0 6 -2 10 -4 10 -3 0
|
||||
-8 -4 -11 -10z"/>
|
||||
<path d="M2076 1601 c-3 -5 2 -15 12 -22 15 -12 16 -12 5 2 -7 9 -10 19 -6 22
|
||||
3 4 4 7 0 7 -3 0 -8 -4 -11 -9z"/>
|
||||
<path d="M3253 1595 c0 -8 4 -12 9 -9 5 3 6 10 3 15 -9 13 -12 11 -12 -6z"/>
|
||||
<path d="M2115 1580 c3 -5 8 -10 11 -10 2 0 4 5 4 10 0 6 -5 10 -11 10 -5 0
|
||||
-7 -4 -4 -10z"/>
|
||||
<path d="M3199 1553 c-13 -16 -12 -17 4 -4 9 7 17 15 17 17 0 8 -8 3 -21 -13z"/>
|
||||
<path d="M3240 1520 c-9 -6 -10 -10 -3 -10 6 0 15 5 18 10 8 12 4 12 -15 0z"/>
|
||||
<path d="M4105 1479 c-3 -4 2 -6 10 -5 21 3 28 13 10 13 -9 0 -18 -4 -20 -8z"/>
|
||||
<path d="M4033 1355 c0 -8 4 -12 9 -9 5 3 6 10 3 15 -9 13 -12 11 -12 -6z"/>
|
||||
<path d="M4006 1298 c3 -5 10 -6 15 -3 13 9 11 12 -6 12 -8 0 -12 -4 -9 -9z"/>
|
||||
<path d="M3940 1285 c0 -2 6 -8 13 -14 10 -8 14 -7 14 2 0 8 -6 14 -14 14 -7
|
||||
0 -13 -1 -13 -2z"/>
|
||||
<path d="M3827 1139 c7 -9 10 -19 6 -22 -3 -4 -1 -7 5 -7 17 0 15 16 -5 31
|
||||
-16 12 -17 12 -6 -2z"/>
|
||||
<path d="M1810 1059 c0 -5 5 -7 10 -4 6 3 10 8 10 11 0 2 -4 4 -10 4 -5 0 -10
|
||||
-5 -10 -11z"/>
|
||||
<path d="M3719 1053 c-13 -16 -12 -17 4 -4 16 13 21 21 13 21 -2 0 -10 -8 -17
|
||||
-17z"/>
|
||||
<path d="M1774 1049 c-3 -6 -2 -15 3 -20 5 -5 9 -1 9 11 0 23 -2 24 -12 9z"/>
|
||||
<path d="M3679 1028 c-5 -16 -4 -46 2 -42 4 2 7 13 6 24 -1 17 -5 26 -8 18z"/>
|
||||
<path d="M1710 965 c0 -7 30 -13 34 -7 3 4 -4 9 -15 9 -10 1 -19 0 -19 -2z"/>
|
||||
<path d="M3610 939 c0 -5 5 -7 10 -4 6 3 10 8 10 11 0 2 -4 4 -10 4 -5 0 -10
|
||||
-5 -10 -11z"/>
|
||||
<path d="M3570 879 c0 -5 5 -7 10 -4 6 3 10 8 10 11 0 2 -4 4 -10 4 -5 0 -10
|
||||
-5 -10 -11z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 16 KiB |
BIN
client/images/snapdrop-graphics.sketch
Executable file
BIN
client/images/twitter-stream.jpg
Normal file
After Width: | Height: | Size: 44 KiB |
200
client/index.html
Normal file
|
@ -0,0 +1,200 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<!-- Web App Config -->
|
||||
<title>Snapdrop</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<meta name="theme-color" content="#3367d6">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="Snapdrop">
|
||||
<meta name="msapplication-TileColor" content="#3372DF">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<!-- Descriptions -->
|
||||
<meta property="og:title" content="Snapdrop">
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:url" content="https://snapdrop.net/">
|
||||
<meta property="og:author" content="https://facebook.com/RobinLinus">
|
||||
<meta name="twitter:author" content="@RobinLinus">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:description" content="Instantly share images, videos, PDFs, and links with people nearby. Peer2Peer and Open Source. No Setup, No Signup.">
|
||||
<meta name="og:description" content="Instantly share images, videos, PDFs, and links with people nearby. Peer2Peer and Open Source. No Setup, No Signup.">
|
||||
<!-- Icons -->
|
||||
<link rel="icon" sizes="96x96" href="images/favicon-96x96.png?">
|
||||
<link rel="shortcut icon" href="images/favicon-96x96.png?">
|
||||
<link rel="apple-touch-icon" href="images/apple-touch-icon.png">
|
||||
<meta name="msapplication-TileImage" content="images/mstile-150x150.png">
|
||||
<link rel="fluid-icon" type="image/png" href="images/android-chrome-192x192.png">
|
||||
<meta name="twitter:image" content="https://snapdrop.net/images/twitter-stream.jpg">
|
||||
<meta property="og:image" content="https://snapdrop.net/images/twitter-stream.jpg">
|
||||
<!-- Resources -->
|
||||
<link rel="stylesheet" type="text/css" href="styles.css">
|
||||
<link rel="manifest" href="manifest.json">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Peers -->
|
||||
<x-peers class="center"></x-peers>
|
||||
<x-no-peers>
|
||||
<h2>Open Snapdrop on other devices to send files.</h2>
|
||||
<div class="font-body1">Short link: <a href="http://yg.gl">yg.gl</a></div>
|
||||
</x-no-peers>
|
||||
<x-instructions desktop="Click to send files or right click to send a message." mobile="Tap to send files or long tap to send a message."></x-instructions>
|
||||
<!-- Footer -->
|
||||
<footer class="column">
|
||||
<svg class="icon logo">
|
||||
<use xlink:href="#wifi-tethering" />
|
||||
</svg>
|
||||
<div>The easiest way to transfer data across devices.</div>
|
||||
<div class="font-body2">Allow me to be discovered by: Everyone in this network.</div>
|
||||
</footer>
|
||||
<!-- Receive Dialog -->
|
||||
<x-dialog id="receiveDialog">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<h3>File Received</h3>
|
||||
<div class="font-subheading" id="fileName">Filename</div>
|
||||
<div class="font-body2" id="fileSize"></div>
|
||||
<div class="row-reverse">
|
||||
<a class="button" close id="download" title="Download File" autofocus>Download</a>
|
||||
<button class="button" close>Ignore</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</x-dialog>
|
||||
<!-- Send Text Dialog -->
|
||||
<x-dialog id="sendTextDialog">
|
||||
<form action="#">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<h3>Send a Message</h3>
|
||||
<input id="textInput" placeholder="Send a message" autocomplete="off" autofocus>
|
||||
<div class="row-reverse">
|
||||
<button class="button" type="submit" close>Send</button>
|
||||
<a class="button" close>Cancel</a>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</form>
|
||||
</x-dialog>
|
||||
<!-- Receive Text Dialog -->
|
||||
<x-dialog id="receiveTextDialog">
|
||||
<x-background class="full center">
|
||||
<x-paper shadow="2">
|
||||
<h3>Message Received</h3>
|
||||
<div class="font-subheading" id="text"></div>
|
||||
<div class="row-reverse">
|
||||
<button class="button" id="copy" close autofocus>Copy</button>
|
||||
<button class="button" close>Close</button>
|
||||
</div>
|
||||
</x-paper>
|
||||
</x-background>
|
||||
</x-dialog>
|
||||
<!-- Toast -->
|
||||
<div class="toast-container full center">
|
||||
<x-toast class="row" shadow="1" id="toast">File Transfer Completed</x-toast>
|
||||
</div>
|
||||
<!-- Info Page -->
|
||||
<div id="info" class="full center column">
|
||||
<a href="#" class="close icon-button">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#close" />
|
||||
</svg>
|
||||
</a>
|
||||
<svg class="icon logo">
|
||||
<use xlink:href="#wifi-tethering" />
|
||||
</svg>
|
||||
<h1>Snapdrop</h1>
|
||||
<div class="font-subheading">The easiest way to transfer files across devices.</div>
|
||||
<div class="row">
|
||||
<a class="icon-button" target="_blank" href="https://github.com/RobinLinus/snapdrop" title="Snapdrop on Github">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#github" />
|
||||
</svg>
|
||||
</a>
|
||||
<a class="icon-button" target="_blank" href="https://twitter.com/intent/tweet?text=https://snapdrop.net%20by%20@robin_linus%20&" title="tweet about Snapdrop">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#twitter" />
|
||||
</svg>
|
||||
</a>
|
||||
<a class="icon-button" target="_blank" href="https://github.com/RobinLinus/snapdrop#frequently-asked-questions" title="frequently asked questions">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#help-outline" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#info" class="icon-button" title="about Snapdrop">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#info-outline" />
|
||||
</svg>
|
||||
<div class="info-background"></div>
|
||||
</a>
|
||||
<a id="notification" class="icon-button" hidden title="enable Notifications">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#notifications" />
|
||||
</svg>
|
||||
</a>
|
||||
<!-- SVG Icon Library -->
|
||||
<svg style="display: none;">
|
||||
<symbol id=wifi-tethering viewBox="0 0 24 24">
|
||||
<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"></path>
|
||||
</symbol>
|
||||
<symbol id=desktop-mac viewBox="0 0 24 24">
|
||||
<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"></path>
|
||||
</symbol>
|
||||
<symbol id=phone-iphone viewBox="0 0 24 24">
|
||||
<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"></path>
|
||||
</symbol>
|
||||
<symbol id=tablet-mac viewBox="0 0 24 24">
|
||||
<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"></path>
|
||||
</symbol>
|
||||
<symbol id=info-outline viewBox="0 0 24 24">
|
||||
<path d="M11 17h2v-6h-2v6zm1-15C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zM11 9h2V7h-2v2z"></path>
|
||||
</symbol>
|
||||
<symbol id=close viewBox="0 0 24 24">
|
||||
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"></path>
|
||||
</symbol>
|
||||
<symbol id=help-outline viewBox="0 0 24 24">
|
||||
<path d="M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z"></path>
|
||||
</symbol>
|
||||
<symbol id="twitter">
|
||||
<path d="M23.954 4.569c-.885.389-1.83.654-2.825.775 1.014-.611 1.794-1.574 2.163-2.723-.951.555-2.005.959-3.127 1.184-.896-.959-2.173-1.559-3.591-1.559-2.717 0-4.92 2.203-4.92 4.917 0 .39.045.765.127 1.124C7.691 8.094 4.066 6.13 1.64 3.161c-.427.722-.666 1.561-.666 2.475 0 1.71.87 3.213 2.188 4.096-.807-.026-1.566-.248-2.228-.616v.061c0 2.385 1.693 4.374 3.946 4.827-.413.111-.849.171-1.296.171-.314 0-.615-.03-.916-.086.631 1.953 2.445 3.377 4.604 3.417-1.68 1.319-3.809 2.105-6.102 2.105-.39 0-.779-.023-1.17-.067 2.189 1.394 4.768 2.209 7.557 2.209 9.054 0 13.999-7.496 13.999-13.986 0-.209 0-.42-.015-.63.961-.689 1.8-1.56 2.46-2.548l-.047-.02z" />
|
||||
</symbol>
|
||||
<symbol id="github">
|
||||
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
|
||||
</symbol>
|
||||
<g id="notifications">
|
||||
<path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z" />
|
||||
</g>
|
||||
</svg>
|
||||
<!-- Scripts -->
|
||||
<script type="text/javascript" src="scripts/network.js"></script>
|
||||
<script type="text/javascript" src="scripts/ui.js"></script>
|
||||
<!-- Sounds -->
|
||||
<audio id="blop" preload="auto" autobuffer="true">
|
||||
<source src="/sounds/blop.mp3" type="audio/mpeg">
|
||||
<source src="/sounds/blop.ogg" type="audio/ogg">
|
||||
</audio>
|
||||
<!-- no script -->
|
||||
<noscript>
|
||||
<x-noscript class="full center column">
|
||||
<h1>Enable Javascript</h1>
|
||||
<h3>Snapdrop works only with Javascript.</h3>
|
||||
</x-noscript>
|
||||
<style>
|
||||
x-noscript {
|
||||
background: #599cfc;
|
||||
color: white;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
a[href="#info"] {
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
</noscript>
|
||||
</body>
|
|
@ -2,28 +2,28 @@
|
|||
"name": "Snapdrop",
|
||||
"short_name": "Snapdrop",
|
||||
"icons": [{
|
||||
"src": "images/touch/icon-128x128.png",
|
||||
"sizes": "128x128",
|
||||
"src": "images/favicon-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png"
|
||||
}, {
|
||||
"src": "images/touch/apple-touch-icon.png",
|
||||
"src": "images/apple-touch-icon.png",
|
||||
"sizes": "152x152",
|
||||
"type": "image/png"
|
||||
}, {
|
||||
"src": "images/touch/ms-touch-icon-144x144-precomposed.png",
|
||||
"src": "images/mstile-150x150.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png"
|
||||
}, {
|
||||
"src": "images/touch/chrome-touch-icon-192x192.png",
|
||||
"src": "images/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
}, {
|
||||
"src": "images/touch/chrome-splashscreen-icon-384x384.png",
|
||||
"sizes": "384x384",
|
||||
"src": "logo_transparent_white_512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}],
|
||||
"background_color": "#3367d6",
|
||||
"start_url": "index.html",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"theme_color": "#3367d6"
|
||||
}
|
34
client/scripts/network-v2.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
class ServerConnection {
|
||||
|
||||
}
|
||||
|
||||
class Connection {
|
||||
|
||||
}
|
||||
|
||||
class WSConnection extends Connection {
|
||||
|
||||
}
|
||||
|
||||
class RTCConnection extends Connection {
|
||||
|
||||
}
|
||||
|
||||
class Peer {
|
||||
|
||||
constructor(serverConnection) {
|
||||
this._ws = new WSConnection(serverConnection);
|
||||
this._rtc = new RTCConnection(serverConnection);
|
||||
this._fileReceiver = new FileReceiver(this);
|
||||
this._fileSender = new FileSender(this);
|
||||
}
|
||||
|
||||
send(message) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Peers {
|
||||
|
||||
}
|
475
client/scripts/network.js
Normal file
|
@ -0,0 +1,475 @@
|
|||
class ServerConnection {
|
||||
|
||||
constructor() {
|
||||
this._connect();
|
||||
Events.on('beforeunload', e => this._disconnect(), false);
|
||||
}
|
||||
|
||||
_connect() {
|
||||
const ws = new WebSocket(this._endpoint());
|
||||
ws.binaryType = 'arraybuffer';
|
||||
ws.onopen = e => console.log('WS: server connection opened');
|
||||
ws.onmessage = e => this._onMessage(e.data);
|
||||
ws.onclose = e => this._onDisconnect();
|
||||
ws.onerror = e => console.error(e);
|
||||
this._socket = ws;
|
||||
clearTimeout(this._reconnectTimer);
|
||||
}
|
||||
|
||||
_onMessage(msg) {
|
||||
msg = JSON.parse(msg);
|
||||
console.log('WS:', msg);
|
||||
switch (msg.type) {
|
||||
case 'peers':
|
||||
Events.fire('peers', msg.peers);
|
||||
break;
|
||||
case 'peer-joined':
|
||||
Events.fire('peer-joined', msg.peer);
|
||||
break;
|
||||
case 'peer-left':
|
||||
Events.fire('peer-left', msg.peerId);
|
||||
break;
|
||||
case 'signal':
|
||||
Events.fire('signal', msg);
|
||||
break;
|
||||
case 'ping':
|
||||
this.send({ type: 'pong' });
|
||||
break;
|
||||
default:
|
||||
console.error('WS: unkown message type', msg)
|
||||
}
|
||||
}
|
||||
|
||||
send(message) {
|
||||
if (this._socket.readyState !== this._socket.OPEN) return;
|
||||
this._socket.send(JSON.stringify(message));
|
||||
}
|
||||
|
||||
_endpoint() {
|
||||
// hack to detect if deployment or development environment
|
||||
const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws';
|
||||
const host = location.hostname.startsWith('localhost') ? 'localhost:3000' : (location.host + '/server');
|
||||
const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback';
|
||||
const url = protocol + '://' + host + webrtc;
|
||||
return url;
|
||||
}
|
||||
|
||||
_disconnect() {
|
||||
this.send({ type: 'disconnect' });
|
||||
this._socket.close();
|
||||
}
|
||||
|
||||
_onDisconnect() {
|
||||
console.log('WS: server disconnected');
|
||||
Events.fire('notify-user', 'Connection lost. Retry in 5 seconds...');
|
||||
clearTimeout(this._reconnectTimer);
|
||||
this._reconnectTimer = setTimeout(_ => this._connect(), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
class Peer {
|
||||
|
||||
constructor(serverConnection, peerId) {
|
||||
this._server = serverConnection;
|
||||
this._peerId = peerId;
|
||||
this._filesQueue = [];
|
||||
this._busy = false;
|
||||
}
|
||||
|
||||
sendJSON(message) {
|
||||
this._send(JSON.stringify(message));
|
||||
}
|
||||
|
||||
sendFiles(files) {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
this._filesQueue.push(files[i]);
|
||||
}
|
||||
if (this._busy) return;
|
||||
this._dequeueFile();
|
||||
}
|
||||
|
||||
_dequeueFile() {
|
||||
if (!this._filesQueue.length) return;
|
||||
this._busy = true;
|
||||
const file = this._filesQueue.shift();
|
||||
this._sendFile(file);
|
||||
}
|
||||
|
||||
_sendFile(file) {
|
||||
this.sendJSON({
|
||||
type: 'header',
|
||||
name: file.name,
|
||||
mime: file.type,
|
||||
size: file.size,
|
||||
});
|
||||
this._chunker = new FileChunker(file,
|
||||
chunk => this._send(chunk),
|
||||
offset => this._onPartitionEnd(offset));
|
||||
this._chunker.nextPartition();
|
||||
}
|
||||
|
||||
_onPartitionEnd(offset) {
|
||||
this.sendJSON({ type: 'partition', offset: offset });
|
||||
}
|
||||
|
||||
_onReceivedPartitionEnd(offset) {
|
||||
this.sendJSON({ type: 'partition_received', offset: offset });
|
||||
}
|
||||
|
||||
_sendNextPartition() {
|
||||
if (!this._chunker || this._chunker.isFileEnd()) return;
|
||||
this._chunker.nextPartition();
|
||||
}
|
||||
|
||||
_sendProgress(progress) {
|
||||
this.sendJSON({ type: 'progress', progress: progress });
|
||||
}
|
||||
|
||||
_onMessage(message) {
|
||||
if (typeof message !== 'string') {
|
||||
this._onChunkReceived(message);
|
||||
return;
|
||||
}
|
||||
message = JSON.parse(message);
|
||||
console.log('RTC:', message);
|
||||
switch (message.type) {
|
||||
case 'header':
|
||||
this._onFileHeader(message);
|
||||
break;
|
||||
case 'partition':
|
||||
this._onReceivedPartitionEnd(message);
|
||||
break;
|
||||
case 'partition_received':
|
||||
this._sendNextPartition();
|
||||
break;
|
||||
case 'progress':
|
||||
this._onDownloadProgress(message.progress);
|
||||
break;
|
||||
case 'transfer-complete':
|
||||
this._onTransferCompleted();
|
||||
break;
|
||||
case 'text':
|
||||
this._onTextReceived(message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_onFileHeader(header) {
|
||||
this._lastProgress = 0;
|
||||
this._digester = new FileDigester({
|
||||
name: header.name,
|
||||
mime: header.mime,
|
||||
size: header.size
|
||||
}, file => this._onFileReceived(file));
|
||||
}
|
||||
|
||||
_onChunkReceived(chunk) {
|
||||
this._digester.unchunk(chunk);
|
||||
const progress = this._digester.progress;
|
||||
this._onDownloadProgress(progress);
|
||||
|
||||
// occasionally notify sender about our progress
|
||||
if (progress - this._lastProgress < 0.01) return;
|
||||
this._lastProgress = progress;
|
||||
this._sendProgress(progress);
|
||||
}
|
||||
|
||||
_onDownloadProgress(progress) {
|
||||
Events.fire('file-progress', {
|
||||
sender: this._peerId,
|
||||
progress: progress
|
||||
});
|
||||
}
|
||||
|
||||
_onFileReceived(proxyFile) {
|
||||
Events.fire('file-received', proxyFile);
|
||||
this.sendJSON({ type: 'transfer-complete' });
|
||||
// this._digester = null;
|
||||
}
|
||||
|
||||
_onTransferCompleted() {
|
||||
this._onDownloadProgress(1);
|
||||
this._reader = null;
|
||||
this._busy = false;
|
||||
this._dequeueFile();
|
||||
Events.fire('notify-user', 'File transfer completed.');
|
||||
}
|
||||
|
||||
sendText(text) {
|
||||
this.sendJSON({
|
||||
type: 'text',
|
||||
text: btoa(unescape(encodeURIComponent(text)))
|
||||
});
|
||||
}
|
||||
|
||||
_onTextReceived(message) {
|
||||
Events.fire('text-received', {
|
||||
text: decodeURIComponent(escape(atob(message.text))),
|
||||
sender: this._peerId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class RTCPeer extends Peer {
|
||||
|
||||
constructor(serverConnection, peerId) {
|
||||
super(serverConnection, peerId);
|
||||
if (!peerId) return; // we will listen for a caller
|
||||
this._start(peerId, true);
|
||||
}
|
||||
|
||||
_start(peerId, isCaller) {
|
||||
if (!this._peer) {
|
||||
this._isCaller = isCaller;
|
||||
this._peerId = peerId;
|
||||
this._peer = new RTCPeerConnection(RTCPeer.config);
|
||||
this._peer.onicecandidate = e => this._onIceCandidate(e);
|
||||
this._peer.onconnectionstatechange = e => console.log('RTC: state changed:', this._peer.connectionState);
|
||||
}
|
||||
|
||||
if (isCaller) {
|
||||
this._createChannel();
|
||||
} else {
|
||||
this._peer.ondatachannel = e => this._onChannelOpened(e);
|
||||
}
|
||||
}
|
||||
|
||||
_createChannel() {
|
||||
const channel = this._peer.createDataChannel('data-channel', { reliable: true });
|
||||
channel.binaryType = 'arraybuffer';
|
||||
channel.onopen = e => this._onChannelOpened(e)
|
||||
this._peer.createOffer(d => this._onDescription(d), e => this._onError(e));
|
||||
}
|
||||
|
||||
_onDescription(description) {
|
||||
// description.sdp = description.sdp.replace('b=AS:30', 'b=AS:1638400');
|
||||
this._peer.setLocalDescription(description,
|
||||
_ => this._sendSignal({ sdp: description }),
|
||||
e => this._onError(e));
|
||||
}
|
||||
|
||||
_onIceCandidate(event) {
|
||||
if (!event.candidate) return;
|
||||
this._sendSignal({ ice: event.candidate });
|
||||
}
|
||||
|
||||
_sendSignal(signal) {
|
||||
signal.type = 'signal';
|
||||
signal.to = this._peerId;
|
||||
this._server.send(signal);
|
||||
}
|
||||
|
||||
onServerMessage(message) {
|
||||
if (!this._peer) this._start(message.sender, false);
|
||||
const conn = this._peer;
|
||||
|
||||
if (message.sdp) {
|
||||
this._peer.setRemoteDescription(new RTCSessionDescription(message.sdp), () => {
|
||||
if (message.sdp.type !== 'offer') return;
|
||||
this._peer.createAnswer(d => this._onDescription(d), e => this._onError(e));
|
||||
}, e => this._onError(e));
|
||||
} else if (message.ice) {
|
||||
this._peer.addIceCandidate(new RTCIceCandidate(message.ice));
|
||||
}
|
||||
}
|
||||
|
||||
_onChannelOpened(event) {
|
||||
console.log('RTC: channel opened with', this._peerId);
|
||||
const channel = event.channel || event.target;
|
||||
channel.onmessage = e => this._onMessage(e.data);
|
||||
channel.onclose = e => this._onChannelClosed();
|
||||
this._channel = channel;
|
||||
}
|
||||
|
||||
_onChannelClosed() {
|
||||
console.log('RTC: channel closed ', this._peerId);
|
||||
if (!this.isCaller) return;
|
||||
this._start(this._peerId, true); // reopen the channel
|
||||
}
|
||||
|
||||
_send(message) {
|
||||
this._channel.send(message);
|
||||
}
|
||||
|
||||
_onError(error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
refresh() {
|
||||
// check if channel open. otherwise create one
|
||||
if (this._peer && this._channel && this._channel.readyState !== 'open') return;
|
||||
this._createChannel(this._peerId, this._isCaller);
|
||||
}
|
||||
}
|
||||
|
||||
class PeersManager {
|
||||
|
||||
constructor(serverConnection) {
|
||||
this.peers = {};
|
||||
this._server = serverConnection;
|
||||
Events.on('signal', e => this._onMessage(e.detail));
|
||||
Events.on('peers', e => this._onPeers(e.detail));
|
||||
Events.on('files-selected', e => this._onFilesSelected(e.detail));
|
||||
Events.on('send-text', e => this._onSendText(e.detail));
|
||||
Events.on('peer-left', e => this._onPeerLeft(e.detail));
|
||||
}
|
||||
|
||||
_onMessage(message) {
|
||||
if (!this.peers[message.sender]) {
|
||||
this.peers[message.sender] = new RTCPeer(this._server);
|
||||
}
|
||||
this.peers[message.sender].onServerMessage(message);
|
||||
}
|
||||
|
||||
_onPeers(peers) {
|
||||
peers.forEach(peer => {
|
||||
if (this.peers[peer.id]) {
|
||||
this.peers[peer.id].refresh();
|
||||
return;
|
||||
}
|
||||
if (window.isRtcSupported && peer.rtcSupported) {
|
||||
this.peers[peer.id] = new RTCPeer(this._server, peer.id);
|
||||
} else {
|
||||
this.peers[peer.id] = new WSPeer(this._server, peer.id);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
sendTo(peerId, message) {
|
||||
this.peers[peerId].send(message);
|
||||
}
|
||||
|
||||
_onFilesSelected(message) {
|
||||
this.peers[message.to].sendFiles(message.files);
|
||||
}
|
||||
|
||||
_onSendText(message) {
|
||||
this.peers[message.to].sendText(message.text);
|
||||
}
|
||||
|
||||
_onPeerLeft(peerId) {
|
||||
const peer = this.peers[peerId];
|
||||
delete this.peers[peerId];
|
||||
if (!peer || !peer._peer) return;
|
||||
peer._peer.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class WSPeer {
|
||||
_send(message) {
|
||||
message.to = this._peerId;
|
||||
this._server.send(message);
|
||||
}
|
||||
}
|
||||
|
||||
class FileChunker {
|
||||
|
||||
constructor(file, onChunk, onPartitionEnd) {
|
||||
this._chunkSize = 64000;
|
||||
this._maxPartitionSize = 1e6;
|
||||
this._offset = 0;
|
||||
this._partitionSize = 0;
|
||||
this._file = file;
|
||||
this._onChunk = onChunk;
|
||||
this._onPartitionEnd = onPartitionEnd;
|
||||
this._reader = new FileReader();
|
||||
this._reader.addEventListener('load', e => this._onChunkRead(e.target.result));
|
||||
}
|
||||
|
||||
nextPartition() {
|
||||
this._partitionSize = 0;
|
||||
this._readChunk();
|
||||
}
|
||||
|
||||
_readChunk() {
|
||||
const chunk = this._file.slice(this._offset, this._offset + this._chunkSize);
|
||||
this._reader.readAsArrayBuffer(chunk);
|
||||
}
|
||||
|
||||
_onChunkRead(chunk) {
|
||||
this._offset += chunk.byteLength;
|
||||
this._partitionSize += chunk.byteLength;
|
||||
this._onChunk(chunk);
|
||||
if (this._isPartitionEnd() || this.isFileEnd()) {
|
||||
this._onPartitionEnd(this._partitionSize);
|
||||
return;
|
||||
}
|
||||
this._readChunk();
|
||||
}
|
||||
|
||||
repeatPartition() {
|
||||
this._offset -= this._partitionSize;
|
||||
this._nextPartition();
|
||||
}
|
||||
|
||||
_isPartitionEnd() {
|
||||
return this._partitionSize >= this._maxPartitionSize;
|
||||
}
|
||||
|
||||
isFileEnd() {
|
||||
return this._offset > this._file.size;
|
||||
}
|
||||
|
||||
get progress() {
|
||||
return this._offset / this._file.size;
|
||||
}
|
||||
}
|
||||
|
||||
class FileDigester {
|
||||
constructor(meta, callback) {
|
||||
this._buffer = [];
|
||||
this._bytesReceived = 0;
|
||||
this._size = meta.size;
|
||||
this._mime = meta.mime || 'application/octet-stream';
|
||||
this._name = meta.name;
|
||||
this._callback = callback;
|
||||
}
|
||||
|
||||
unchunk(chunk) {
|
||||
this._buffer.push(chunk);
|
||||
this._bytesReceived += chunk.byteLength || chunk.size;
|
||||
const totalChunks = this._buffer.length;
|
||||
this.progress = this._bytesReceived / this._size;
|
||||
if (this._bytesReceived < this._size) return;
|
||||
|
||||
let received = new Blob(this._buffer, { type: this._mime }); // pass a useful mime type here
|
||||
let url = URL.createObjectURL(received);
|
||||
this._callback({
|
||||
name: this._name,
|
||||
mime: this._mime,
|
||||
size: this._size,
|
||||
url: url
|
||||
});
|
||||
this._callback = null;
|
||||
}
|
||||
}
|
||||
|
||||
class Events {
|
||||
static fire(type, detail) {
|
||||
window.dispatchEvent(new CustomEvent(type, { detail: detail }));
|
||||
}
|
||||
|
||||
static on(type, callback) {
|
||||
return window.addEventListener(type, callback, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
window.isRtcSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection);
|
||||
|
||||
RTCPeer.config = {
|
||||
'iceServers': [{
|
||||
urls: 'stun:stun.stunprotocol.org:3478'
|
||||
}, {
|
||||
urls: 'stun:stun.l.google.com:19302'
|
||||
}, {
|
||||
urls: 'turn:turn.bistri.com:80',
|
||||
credential: 'homeo',
|
||||
username: 'homeo'
|
||||
}, {
|
||||
urls: 'turn:turn.anyfirewall.com:443?transport=tcp',
|
||||
credential: 'webrtc',
|
||||
username: 'webrtc'
|
||||
}]
|
||||
}
|
521
client/scripts/ui.js
Normal file
|
@ -0,0 +1,521 @@
|
|||
const $ = query => document.getElementById(query);
|
||||
const $$ = query => document.body.querySelector(query);
|
||||
const isURL = text => /^((https?:\/\/|www)[^\s]+)/g.test(text.toLowerCase());
|
||||
const isDownloadSupported = (typeof document.createElement('a').download !== 'undefined');
|
||||
const isProductionEnvironment = !window.location.host.startsWith('localhost');
|
||||
|
||||
class PeersUI {
|
||||
|
||||
constructor() {
|
||||
Events.on('peer-joined', e => this._onPeerJoined(e.detail));
|
||||
Events.on('peer-left', e => this._onPeerLeft(e.detail));
|
||||
Events.on('peers', e => this._onPeers(e.detail));
|
||||
Events.on('file-progress', e => this._onFileProgress(e.detail));
|
||||
}
|
||||
|
||||
_onPeerJoined(peer) {
|
||||
if (document.getElementById(peer.id)) return;
|
||||
const peerUI = new PeerUI(peer);
|
||||
$$('x-peers').appendChild(peerUI.$el);
|
||||
}
|
||||
|
||||
_onPeers(peers) {
|
||||
this._clearPeers();
|
||||
peers.forEach(peer => this._onPeerJoined(peer));
|
||||
}
|
||||
|
||||
_onPeerLeft(peerId) {
|
||||
const peer = $(peerId);
|
||||
if (!peer) return;
|
||||
peer.remove();
|
||||
}
|
||||
|
||||
_onFileProgress(progress) {
|
||||
const peerId = progress.sender || progress.recipient;
|
||||
const peer = $(peerId);
|
||||
if (!peer) return;
|
||||
peer.ui.setProgress(progress.progress);
|
||||
}
|
||||
|
||||
_clearPeers() {
|
||||
const $peers = $$('x-peers').innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
class PeerUI {
|
||||
|
||||
html() {
|
||||
return `
|
||||
<label class="column center">
|
||||
<input type="file" multiple>
|
||||
<x-icon shadow="1">
|
||||
<svg class="icon"><use xlink:href="#"/></svg>
|
||||
</x-icon>
|
||||
<div class="progress">
|
||||
<div class="circle"></div>
|
||||
<div class="circle right"></div>
|
||||
</div>
|
||||
<div class="name font-subheading"></div>
|
||||
<div class="status font-body2"></div>
|
||||
</label>`
|
||||
}
|
||||
|
||||
constructor(peer) {
|
||||
this._peer = peer;
|
||||
this._initDom();
|
||||
this._bindListeners(this.$el);
|
||||
}
|
||||
|
||||
_initDom() {
|
||||
const el = document.createElement('x-peer');
|
||||
el.id = this._peer.id;
|
||||
el.innerHTML = this.html();
|
||||
el.ui = this;
|
||||
el.querySelector('svg use').setAttribute('xlink:href', this._icon());
|
||||
el.querySelector('.name').textContent = this._name();
|
||||
this.$el = el;
|
||||
this.$progress = el.querySelector('.progress');
|
||||
}
|
||||
|
||||
_bindListeners(el) {
|
||||
el.querySelector('input').addEventListener('change', e => this._onFilesSelected(e));
|
||||
el.addEventListener('drop', e => this._onDrop(e));
|
||||
el.addEventListener('dragend', e => this._onDragEnd(e));
|
||||
el.addEventListener('dragleave', e => this._onDragEnd(e));
|
||||
el.addEventListener('dragover', e => this._onDragOver(e));
|
||||
el.addEventListener('contextmenu', e => this._onRightClick(e));
|
||||
el.addEventListener('touchstart', e => this._onTouchStart(e));
|
||||
el.addEventListener('touchend', e => this._onTouchEnd(e));
|
||||
// prevent browser's default file drop behavior
|
||||
Events.on('dragover', e => e.preventDefault());
|
||||
Events.on('drop', e => e.preventDefault());
|
||||
}
|
||||
|
||||
_name() {
|
||||
if (this._peer.name.model) {
|
||||
return this._peer.name.os + ' ' + this._peer.name.model;
|
||||
}
|
||||
this._peer.name.os = this._peer.name.os.replace('Mac OS', 'Mac');
|
||||
return this._peer.name.os + ' ' + this._peer.name.browser;
|
||||
}
|
||||
|
||||
_icon() {
|
||||
const device = this._peer.name.device || this._peer.name;
|
||||
if (device.type === 'mobile') {
|
||||
return '#phone-iphone';
|
||||
}
|
||||
if (device.type === 'tablet') {
|
||||
return '#tablet-mac';
|
||||
}
|
||||
return '#desktop-mac';
|
||||
}
|
||||
|
||||
_onFilesSelected(e) {
|
||||
const $input = e.target;
|
||||
const files = $input.files;
|
||||
Events.fire('files-selected', {
|
||||
files: files,
|
||||
to: this._peer.id
|
||||
});
|
||||
$input.value = null; // reset input
|
||||
this.setProgress(0.01);
|
||||
}
|
||||
|
||||
setProgress(progress) {
|
||||
if (progress > 0) {
|
||||
this.$el.setAttribute('transfer', '1');
|
||||
}
|
||||
if (progress > 0.5) {
|
||||
this.$progress.classList.add('over50');
|
||||
} else {
|
||||
this.$progress.classList.remove('over50');
|
||||
}
|
||||
const degrees = `rotate(${360 * progress}deg)`;
|
||||
this.$progress.style.setProperty('--progress', degrees);
|
||||
if (progress >= 1) {
|
||||
this.setProgress(0);
|
||||
this.$el.removeAttribute('transfer');
|
||||
}
|
||||
}
|
||||
|
||||
_onDrop(e) {
|
||||
e.preventDefault();
|
||||
const files = e.dataTransfer.files;
|
||||
Events.fire('files-selected', {
|
||||
files: files,
|
||||
to: this._peer.id
|
||||
});
|
||||
this._onDragEnd();
|
||||
}
|
||||
|
||||
_onDragOver() {
|
||||
this.$el.setAttribute('drop', 1);
|
||||
}
|
||||
|
||||
_onDragEnd() {
|
||||
this.$el.removeAttribute('drop');
|
||||
}
|
||||
|
||||
_onRightClick(e) {
|
||||
e.preventDefault();
|
||||
Events.fire('text-recipient', this._peer.id);
|
||||
}
|
||||
|
||||
_onTouchStart(e) {
|
||||
this._touchStart = Date.now();
|
||||
this._touchTimer = setTimeout(_ => this._onTouchEnd(), 610);
|
||||
}
|
||||
|
||||
_onTouchEnd(e) {
|
||||
if (Date.now() - this._touchStart < 500) {
|
||||
clearTimeout(this._touchTimer);
|
||||
} else { // this was a long tap
|
||||
if (e) e.preventDefault();
|
||||
Events.fire('text-recipient', this._peer.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Dialog {
|
||||
constructor(id) {
|
||||
this.$el = $(id);
|
||||
this.$el.querySelectorAll('[close]').forEach(el => el.addEventListener('click', e => this.hide()))
|
||||
this.$autoFocus = this.$el.querySelector('[autofocus]');
|
||||
}
|
||||
|
||||
show() {
|
||||
this.$el.setAttribute('show', 1);
|
||||
if (this.$autoFocus) this.$autoFocus.focus();
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.$el.removeAttribute('show');
|
||||
document.activeElement.blur();
|
||||
window.blur();
|
||||
}
|
||||
}
|
||||
|
||||
class ReceiveDialog extends Dialog {
|
||||
|
||||
constructor() {
|
||||
super('receiveDialog');
|
||||
Events.on('file-received', e => {
|
||||
this._nextFile(e.detail);
|
||||
window.blop.play();
|
||||
});
|
||||
this._filesQueue = [];
|
||||
}
|
||||
|
||||
_nextFile(nextFile) {
|
||||
if (nextFile) this._filesQueue.push(nextFile);
|
||||
if (this._busy) return;
|
||||
this._busy = true;
|
||||
const file = this._filesQueue.shift();
|
||||
this._displayFile(file);
|
||||
}
|
||||
|
||||
_dequeueFile() {
|
||||
if (!this._filesQueue.length) { // nothing to do
|
||||
this._busy = false;
|
||||
return;
|
||||
}
|
||||
// dequeue next file
|
||||
setTimeout(_ => {
|
||||
this._busy = false;
|
||||
this._nextFile();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
_displayFile(file) {
|
||||
const $a = this.$el.querySelector('#download');
|
||||
$a.href = file.url;
|
||||
$a.download = file.name;
|
||||
|
||||
this.$el.querySelector('#fileName').textContent = file.name;
|
||||
this.$el.querySelector('#fileSize').textContent = this._formatFileSize(file.size);
|
||||
this.show();
|
||||
|
||||
if (!isDownloadSupported) return;
|
||||
// $a.target = "_blank"; // fallback
|
||||
$a.target = "_system"; // fallback
|
||||
$a.href = 'external:' + $a.href;
|
||||
}
|
||||
|
||||
_formatFileSize(bytes) {
|
||||
if (bytes >= 1e9) {
|
||||
return (Math.round(bytes / 1e8) / 10) + ' GB';
|
||||
} else if (bytes >= 1e6) {
|
||||
return (Math.round(bytes / 1e5) / 10) + ' MB';
|
||||
} else if (bytes > 1000) {
|
||||
return Math.round(bytes / 1000) + ' KB';
|
||||
} else {
|
||||
return bytes + ' Bytes';
|
||||
}
|
||||
}
|
||||
|
||||
hide() {
|
||||
super.hide();
|
||||
this._dequeueFile();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class SendTextDialog extends Dialog {
|
||||
constructor() {
|
||||
super('sendTextDialog');
|
||||
Events.on('text-recipient', e => this._onRecipient(e.detail))
|
||||
this.$text = this.$el.querySelector('#textInput');
|
||||
const button = this.$el.querySelector('form');
|
||||
button.addEventListener('submit', e => this._send(e));
|
||||
}
|
||||
|
||||
_onRecipient(recipient) {
|
||||
this._recipient = recipient;
|
||||
this.show();
|
||||
this.$text.setSelectionRange(0, this.$text.value.length)
|
||||
}
|
||||
|
||||
_send(e) {
|
||||
e.preventDefault();
|
||||
Events.fire('send-text', {
|
||||
to: this._recipient,
|
||||
text: this.$text.value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ReceiveTextDialog extends Dialog {
|
||||
constructor() {
|
||||
super('receiveTextDialog');
|
||||
Events.on('text-received', e => this._onText(e.detail))
|
||||
this.$text = this.$el.querySelector('#text');
|
||||
const $copy = this.$el.querySelector('#copy');
|
||||
copy.addEventListener('click', _ => this._onCopy());
|
||||
}
|
||||
|
||||
_onText(e) {
|
||||
this.$text.innerHTML = '';
|
||||
const text = e.text;
|
||||
if (isURL(text)) {
|
||||
const $a = document.createElement('a');
|
||||
$a.href = text;
|
||||
$a.target = '_blank';
|
||||
$a.textContent = text;
|
||||
this.$text.appendChild($a);
|
||||
} else {
|
||||
this.$text.textContent = text;
|
||||
}
|
||||
this.show();
|
||||
window.blop.play();
|
||||
}
|
||||
|
||||
_onCopy() {
|
||||
if (!document.copy(this.$text.textContent)) return;
|
||||
Events.fire('notify-user', 'Copied to clipboard');
|
||||
}
|
||||
}
|
||||
|
||||
class Toast extends Dialog {
|
||||
constructor() {
|
||||
super('toast');
|
||||
Events.on('notify-user', e => this._onNotfiy(e.detail));
|
||||
}
|
||||
|
||||
_onNotfiy(message) {
|
||||
this.$el.textContent = message;
|
||||
this.show();
|
||||
setTimeout(_ => this.hide(), 3000);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Notifications {
|
||||
|
||||
constructor() {
|
||||
// Check if the browser supports notifications
|
||||
if (!('Notification' in window)) return;
|
||||
// Check whether notification permissions have already been granted
|
||||
|
||||
if (Notification.permission !== 'granted') {
|
||||
this.$button = $('notification');
|
||||
this.$button.removeAttribute('hidden');
|
||||
this.$button.addEventListener('click', e => this._requestPermission());
|
||||
}
|
||||
Events.on('text-received', e => this._messageNotification(e.detail.text));
|
||||
Events.on('file-received', e => this._downloadNotification(e.detail.name));
|
||||
}
|
||||
|
||||
_requestPermission() {
|
||||
Notification.requestPermission(permission => {
|
||||
if (permission !== 'granted') {
|
||||
Events.fire('notify-user', Notifications.PERMISSION_ERROR || 'Error');
|
||||
return;
|
||||
}
|
||||
this._notify('Even more snappy sharing!');
|
||||
this.$button.setAttribute('hidden', 1);
|
||||
});
|
||||
}
|
||||
|
||||
_notify(message, body) {
|
||||
var img = '/images/logo_transparent_128x128.png';
|
||||
return new Notification(message, {
|
||||
body: body,
|
||||
icon: img,
|
||||
vibrate: [200, 100, 200, 100, 200, 100, 400],
|
||||
});
|
||||
}
|
||||
|
||||
_messageNotification(message) {
|
||||
if (isURL(message)) {
|
||||
const notification = this._notify(message, 'Click to open link');
|
||||
notification.onclick = e => window.open(message, '_blank', null, true);
|
||||
} else {
|
||||
const notification = this._notify(message, 'Click to copy text');
|
||||
notification.onclick = e => document.copy(message);
|
||||
}
|
||||
}
|
||||
|
||||
_downloadNotification(message) {
|
||||
const notification = this._notify(message, 'Click to download');
|
||||
if (window.isDownloadSupported) return;
|
||||
notification.onclick = e => {
|
||||
document.querySelector('x-dialog [download]').click();
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Snapdrop {
|
||||
constructor() {
|
||||
const server = new ServerConnection();
|
||||
const peers = new PeersManager(server);
|
||||
const peersUI = new PeersUI();
|
||||
Events.on('load', e => {
|
||||
const receiveDialog = new ReceiveDialog();
|
||||
const sendTextDialog = new SendTextDialog();
|
||||
const receiveTextDialog = new ReceiveTextDialog();
|
||||
const toast = new Toast();
|
||||
const notifications = new Notifications();
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const snapdrop = new Snapdrop();
|
||||
|
||||
document.copy = text => {
|
||||
// A <span> contains the text to copy
|
||||
const span = document.createElement('span');
|
||||
span.textContent = text;
|
||||
span.style.whiteSpace = 'pre'; // Preserve consecutive spaces and newlines
|
||||
|
||||
// Paint the span outside the viewport
|
||||
span.style.position = 'absolute';
|
||||
span.style.left = '-9999px';
|
||||
span.style.top = '-9999px';
|
||||
|
||||
const win = window;
|
||||
const selection = win.getSelection();
|
||||
win.document.body.appendChild(span);
|
||||
|
||||
const range = win.document.createRange();
|
||||
selection.removeAllRanges();
|
||||
range.selectNode(span);
|
||||
selection.addRange(range);
|
||||
|
||||
let success = false;
|
||||
try {
|
||||
success = win.document.execCommand('copy');
|
||||
} catch (err) {}
|
||||
|
||||
selection.removeAllRanges();
|
||||
span.remove();
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
if ('serviceWorker' in navigator && isProductionEnvironment) {
|
||||
navigator.serviceWorker
|
||||
.register('/service-worker.js')
|
||||
.then(e => console.log("Service Worker Registered"));
|
||||
}
|
||||
|
||||
// Background Animation
|
||||
Events.on('load', () => {
|
||||
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';
|
||||
style.zIndex = -1;
|
||||
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;
|
||||
var offset = h > 380 ? 100 : 65;
|
||||
x0 = w / 2;
|
||||
y0 = h - offset;
|
||||
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.animateBackground = function(l) {
|
||||
loading = l;
|
||||
animate();
|
||||
};
|
||||
init();
|
||||
animate();
|
||||
setTimeout(e => window.animateBackground(false), 3000);
|
||||
});
|
||||
|
||||
Notifications.PERMISSION_ERROR = `
|
||||
Notifications permission has been blocked
|
||||
as the user has dismissed the permission prompt several times.
|
||||
This can be reset in Page Info
|
||||
which can be accessed by clicking the lock icon next to the URL.`;
|
||||
|
||||
document.body.onclick = e => { // safari hack to fix audio
|
||||
document.body.onclick = null;
|
||||
if (!(/.*Version.*Safari.*/.test(navigator.userAgent))) return;
|
||||
blop.play();
|
||||
}
|
0
app/sounds/blop.mp3 → client/sounds/blop.mp3
Executable file → Normal file
640
client/styles.css
Normal file
|
@ -0,0 +1,640 @@
|
|||
/* Constants */
|
||||
|
||||
:root {
|
||||
--icon-size: 24px;
|
||||
--primary-color: #4285f4;
|
||||
--peer-width: 120px;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.row-reverse {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.full {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
|
||||
body {
|
||||
background: #fafafa;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Roboto, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
color: #333;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 34px;
|
||||
font-weight: 400;
|
||||
letter-spacing: -.01em;
|
||||
line-height: 40px;
|
||||
margin: 8px 0 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 400;
|
||||
letter-spacing: -.012em;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.font-subheading {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.font-body1,
|
||||
body {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.font-body2 {
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--primary-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Icons */
|
||||
|
||||
.icon {
|
||||
width: var(--icon-size);
|
||||
height: var(--icon-size);
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Shadows */
|
||||
|
||||
[shadow="1"] {
|
||||
box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.14),
|
||||
0 1px 8px 0 rgba(0, 0, 0, 0.12),
|
||||
0 3px 3px -2px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
[shadow="2"] {
|
||||
box-shadow: 0 4px 5px 0 rgba(0, 0, 0, 0.14),
|
||||
0 1px 10px 0 rgba(0, 0, 0, 0.12),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/* Animations */
|
||||
|
||||
@keyframes fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Peers List */
|
||||
|
||||
x-peers {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
flex-flow: row wrap;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Empty Peers List */
|
||||
|
||||
x-no-peers {
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
/* prevent flickering on load */
|
||||
animation: fade-in 300ms;
|
||||
animation-delay: 500ms;
|
||||
animation-fill-mode: backwards;
|
||||
}
|
||||
|
||||
x-no-peers h2 {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
x-peers:not(:empty)+x-no-peers {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Peer */
|
||||
|
||||
x-peer {
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
x-peer label {
|
||||
width: var(--peer-width);
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
x-peer .name {
|
||||
width: var(--peer-width);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
x-peer x-icon {
|
||||
--icon-size: 40px;
|
||||
width: var(--icon-size);
|
||||
padding: 12px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
transition: transform 150ms;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
x-peer:not([transfer]):hover x-icon,
|
||||
x-peer:not([transfer]):focus x-icon {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
x-peer[transfer] x-icon {
|
||||
box-shadow: none;
|
||||
opacity: 0.8;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.status {
|
||||
height: 18px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
x-peer[transfer] .status:before {
|
||||
content: 'Transfering...';
|
||||
}
|
||||
|
||||
x-peer x-icon {
|
||||
animation: pop 600ms ease-out 1;
|
||||
}
|
||||
|
||||
@keyframes pop {
|
||||
0% {
|
||||
transform: scale(0.7);
|
||||
}
|
||||
40% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
x-peer[drop] x-icon {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
align-items: center;
|
||||
padding: 0 0 16px 0;
|
||||
}
|
||||
|
||||
footer .logo {
|
||||
--icon-size: 80px;
|
||||
margin-bottom: 8px;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
footer .font-body2 {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
@media (min-height: 800px) {
|
||||
footer {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Dialog */
|
||||
|
||||
x-dialog x-background {
|
||||
background: rgba(0, 0, 0, 0.61);
|
||||
z-index: 10;
|
||||
transition: opacity 300ms;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
x-dialog x-paper {
|
||||
z-index: 3;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 16px 24px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
box-sizing: border-box;
|
||||
transition: transform 300ms;
|
||||
}
|
||||
|
||||
x-dialog:not([show]) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
x-dialog:not([show]) x-paper {
|
||||
transform: scale(0.1);
|
||||
}
|
||||
|
||||
x-dialog:not([show]) x-background {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
x-dialog .row-reverse>.button {
|
||||
margin-top: 16px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Receive Dialog */
|
||||
|
||||
#receiveTextDialog #text {
|
||||
width: 100%;
|
||||
word-break: break-all;
|
||||
max-height: 300px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
-webkit-user-select: all;
|
||||
-moz-user-select: all;
|
||||
user-select: all;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/* Button */
|
||||
|
||||
.button {
|
||||
padding: 0 16px;
|
||||
box-sizing: border-box;
|
||||
min-height: 36px;
|
||||
border: none;
|
||||
outline: none;
|
||||
min-width: 100px;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background: inherit;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.button,
|
||||
.icon-button {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.button:before,
|
||||
.icon-button:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: currentColor;
|
||||
opacity: 0;
|
||||
transition: opacity 300ms;
|
||||
}
|
||||
|
||||
.button:hover:before,
|
||||
.icon-button:hover:before {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
.button:before {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.button:focus:before {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
button::-moz-focus-inner {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Icon Button */
|
||||
|
||||
.icon-button {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.icon-button:before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Text Input */
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 16px 24px;
|
||||
background: #f1f3f4;
|
||||
border-radius: 50px;
|
||||
margin: 8px 0;
|
||||
line-height: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Info Animation */
|
||||
|
||||
#info {
|
||||
text-align: center;
|
||||
color: white;
|
||||
transition: opacity 300ms;
|
||||
will-change: opacity;
|
||||
z-index: 11;
|
||||
transition-delay: 300ms;
|
||||
}
|
||||
|
||||
#info:not(:target) {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition-delay: 0;
|
||||
}
|
||||
|
||||
#info .logo {
|
||||
--icon-size: 96px;
|
||||
}
|
||||
|
||||
#info .close {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.info-background {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.info-background:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
top: -20px;
|
||||
left: -32px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary-color);
|
||||
transform: scale(0);
|
||||
transition: transform 800ms cubic-bezier(0.77, 0, 0.175, 1);
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
#info:target+a>.info-background:before {
|
||||
transform: scale(100);
|
||||
}
|
||||
|
||||
a[href="#info"] {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
color: #333;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
#info .row a {
|
||||
color: currentColor;
|
||||
margin: 8px 8px -16px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/* Loading Indicator */
|
||||
|
||||
.progress {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
clip: rect(0px, 80px, 80px, 40px);
|
||||
--progress: rotate(0deg);
|
||||
transition: transform 200ms;
|
||||
}
|
||||
|
||||
.circle {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border: 4px solid var(--primary-color);
|
||||
border-radius: 40px;
|
||||
position: absolute;
|
||||
clip: rect(0px, 40px, 80px, 0px);
|
||||
will-change: transform;
|
||||
transform: var(--progress);
|
||||
}
|
||||
|
||||
.over50 {
|
||||
clip: rect(auto, auto, auto, auto);
|
||||
}
|
||||
|
||||
.over50 .circle.right {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Toast */
|
||||
|
||||
.toast-container {
|
||||
padding: 0 8px 24px;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
x-toast {
|
||||
position: absolute;
|
||||
min-height: 48px;
|
||||
bottom: 24px;
|
||||
width: 100%;
|
||||
max-width: 344px;
|
||||
border-radius: 8px;
|
||||
background-color: #323232;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: 8px 24px;
|
||||
z-index: 20;
|
||||
transition: opacity 200ms, transform 300ms ease-out;
|
||||
cursor: default;
|
||||
line-height: 24px;
|
||||
border-radius: 6px;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
x-toast:not([show]):not(:hover) {
|
||||
opacity: 0;
|
||||
transform: translateY(100px);
|
||||
}
|
||||
|
||||
#notification {
|
||||
position: absolute;
|
||||
right: 56px;
|
||||
top: 12px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Instructions */
|
||||
|
||||
x-instructions {
|
||||
position: absolute;
|
||||
top: 120px;
|
||||
opacity: 0.5;
|
||||
transition: opacity 300ms;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
x-instructions:before {
|
||||
content: attr(mobile);
|
||||
}
|
||||
|
||||
x-peers:empty~x-instructions {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Responsive Styles */
|
||||
|
||||
@media (min-height: 800px) {
|
||||
x-toast {
|
||||
right: 24px;
|
||||
}
|
||||
footer {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-height: 800px),
|
||||
screen and (min-width: 1100px) {
|
||||
x-instructions:before {
|
||||
content: attr(desktop);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 420px) {
|
||||
x-instructions {
|
||||
top: 24px;
|
||||
}
|
||||
footer .logo {
|
||||
--icon-size: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@supports (-webkit-overflow-scrolling: touch) {
|
||||
/* CSS specific to iOS devices */
|
||||
html {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
x-instructions:before {
|
||||
content: attr(mobile);
|
||||
}
|
||||
}
|
23
dist/index.js
vendored
|
@ -1,23 +0,0 @@
|
|||
'use strict';
|
||||
var express = require('express');
|
||||
var compression = require('compression');
|
||||
var app = express();
|
||||
var http = require('http');
|
||||
var ExpressPeerServer = require('peer').ExpressPeerServer;
|
||||
var wsServer = require('./server/ws-server.js');
|
||||
|
||||
var server = http.createServer(app);
|
||||
|
||||
// Serve up content from public directory
|
||||
app.use(compression());
|
||||
app.use(express.static(__dirname + '/public'));
|
||||
|
||||
var port = process.env.PORT || 3002;
|
||||
server.listen(port);
|
||||
wsServer.create(server);
|
||||
app.use('/peerjs', ExpressPeerServer(server, {
|
||||
debug: true
|
||||
}));
|
||||
|
||||
|
||||
console.log('listening on port ' + port);
|
14
dist/package.json
vendored
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"binaryjs": "^0.2.1",
|
||||
"compression": "^1.6.0",
|
||||
"express": "^4.13.3",
|
||||
"peer": "^0.2.8",
|
||||
"ua-parser-js": "^0.7.10",
|
||||
"ws": "^1.1.1"
|
||||
}
|
||||
}
|
22671
dist/public/elements/elements.html
vendored
18
dist/public/index.html
vendored
|
@ -1,18 +0,0 @@
|
|||
<!doctype html><html lang="en"><head>
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width initial-scale=1.0, maximum-scale=1.0, user-scalable=no"><meta name="generator" content="Snapdrop"><title>Snapdrop</title><link rel="shortcut icon" href="favicon.ico?v=3"><meta name="theme-color" content="#3367d6"><link rel="manifest" href="manifest.json"><meta name="msapplication-TileColor" content="#3372DF"><meta name="mobile-web-app-capable" content="yes"><meta name="application-name" content="PSK"><link rel="icon" sizes="192x192" href="images/touch/chrome-touch-icon-192x192.png"><link rel="fluid-icon" type="image/png" href="images/touch/chrome-touch-icon-192x192.png"><meta name="description" content="Snapdrop is an easy way to transfer files. Instantly share images, video, PDF, and links across devices. Peer2Peer, Private, Secure and Open Source. No Setup, No Signup."><meta property="og:image" content="https://snapdrop.net/images/touch/chrome-splashscreen-icon-384x384.png"><meta property="og:url" content="https://snapdrop.net/"><meta name="twitter:image" content="https://snapdrop.net/images/touch/chrome-splashscreen-icon-384x384.png"><meta name="twitter:author" content="@RobinLinus"><meta property="og:type" content="article"><meta property="og:author" content="https://facebook.com/RobinLinus"><meta property="fb:pages" content="451189218422617"><meta property="fb:profile_id" content="451189218422617"><meta name="twitter:description" content="Snapdrop is an easy way to transfer files. Instantly share images, video, PDF, and links across devices. Peer2Peer, Private, Secure and Open Source. No Setup, No Signup."><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black"><meta name="apple-mobile-web-app-title" content="Snapdrop"><link rel="apple-touch-icon" href="images/touch/apple-touch-icon.png"><meta name="msapplication-TileImage" content="images/touch/ms-touch-icon-144x144-precomposed.png"><link rel="stylesheet" href="styles/main.css"><script src="bower_components/webcomponentsjs/webcomponents-lite.min.js" async="" foo="1"></script><link rel="import" href="elements/elements.html" async="true"></head><body class="layout vertical">
|
||||
<script>"use strict";!function(){function n(){u=window.innerWidth,m=window.innerHeight,a.width=u,a.height=m;var n=m>370?100:65;w=u/2,d=m-n,c=Math.max(u,m,1e3)/13,i()}function t(n){s.beginPath();var t=Math.round(255*(1-n/Math.max(u,m)));s.strokeStyle="rgba("+t+","+t+","+t+",0.1)",s.arc(w,d,n,0,2*Math.PI),s.stroke(),s.lineWidth=2}function i(){s.clearRect(0,0,u,m);for(var n=0;8>n;n++)t(c*n+h%c);h+=1}function e(){(f||c-5>h%c)&&o(function(){i(),e()})}var o=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(n){window.setTimeout(n,1e3/60)}}(),a=document.createElement("canvas");document.body.appendChild(a);var r=a.style;r.width="100%",r.position="absolute";var w,d,u,m,c,s=a.getContext("2d");window.onresize=n;var h=0,f=!0;window.anim=function(n){f=n,e()},n(),e()}();</script><span id="browser-sync-binding"></span><template is="dom-bind" id="app"><connection-wrapper me="{{me}}" loading="{{loading}}" buddies="{{buddies}}"></connection-wrapper><neon-animated-pages id="pages" selected="0"><x-cards on-switch="_showAbout"><div><paper-progress indeterminate="" hidden$="{{!loading}}"></paper-progress><buddy-finder me="{{me}}" active$="{{loading}}" buddies="{{buddies}}"></buddy-finder></div></x-cards><about-page on-switch="_showApp"></about-page></neon-animated-pages><file-receiver></file-receiver><paper-toast id="toast" duration="6000"></paper-toast><paper-toast id="caching-complete" duration="6000" text="Caching complete! This app will work offline."></paper-toast><donate-dialog></donate-dialog><platinum-sw-register auto-register="" clients-claim="" skip-waiting="" base-uri="bower_components/platinum-sw/bootstrap" on-service-worker-installed="displayInstalledToast"><platinum-sw-cache default-cache-strategy="fastest" cache-config-file="cache-config.json"></platinum-sw-cache></platinum-sw-register></template><script src="scripts/app.js"></script><script>
|
||||
(function(i, s, o, g, r, a, m) {
|
||||
i['GoogleAnalyticsObject'] = r;
|
||||
i[r] = i[r] || function() {
|
||||
(i[r].q = i[r].q || []).push(arguments)
|
||||
}, i[r].l = 1 * new Date();
|
||||
a = s.createElement(o),
|
||||
m = s.getElementsByTagName(o)[0];
|
||||
a.async = 1;
|
||||
a.src = g;
|
||||
m.parentNode.insertBefore(a, m)
|
||||
})(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga');
|
||||
|
||||
ga('create', 'UA-71686975-1', 'auto');
|
||||
ga('send', 'pageview');
|
||||
</script></body></html>
|
29
dist/public/manifest.json
vendored
|
@ -1,29 +0,0 @@
|
|||
{
|
||||
"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": "#3367d6",
|
||||
"start_url": "index.html",
|
||||
"display": "standalone",
|
||||
"theme_color": "#3367d6"
|
||||
}
|
1
dist/public/scripts/app.js
vendored
|
@ -1 +0,0 @@
|
|||
!function(e){"use strict";var o=e.querySelector("#app");o.baseUrl="/",""===window.location.port,o.displayInstalledToast=function(){Polymer.dom(e).querySelector("platinum-sw-cache").disabled||Polymer.dom(e).querySelector("#caching-complete").show()},o.displayToast=function(o){var t=Polymer.dom(e).querySelector("#toast");t.text=o,t.show()},o.addEventListener("dom-change",function(){console.log("Our app is ready to rock!"),o.conn=e.querySelector("connection-wrapper")}),window.addEventListener("WebComponentsReady",function(){}),o._showAbout=function(){e.querySelector("#pages").select(1)},o._showAbout=function(){e.querySelector("#pages").select(0)}}(document);
|
BIN
dist/public/sounds/blop.mp3
vendored
BIN
dist/public/sounds/blop.ogg
vendored
1
dist/public/styles/main.css
vendored
|
@ -1 +0,0 @@
|
|||
body,html{height:100%;width:100%;padding:0;margin:0}body{background:#fafafa;font-family:Roboto,'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333;-webkit-font-smoothing:antialiased;overflow-x:hidden}#ads,#ads2{display:none}@media screen and (min-width:520px){#ads{display:block;position:absolute;top:8px;left:50%;margin-left:-150px}}@media screen and (min-width:720px){#ads{display:none}#ads2{display:block;position:absolute;bottom:4px;left:4px}}
|
5
dist/readme.md
vendored
|
@ -1,5 +0,0 @@
|
|||
# Run a Snapdrop Server
|
||||
- `npm install`
|
||||
- `node index.js`
|
||||
- TODO: SSL connection (i.e nginx)
|
||||
- ( Please do a PR if you've build an alternative index.js with a self-signed cert )
|
154
dist/server/ws-server.js
vendored
|
@ -1,154 +0,0 @@
|
|||
'use strict';
|
||||
var parser = require('ua-parser-js');
|
||||
|
||||
// Start Binary.js server
|
||||
var BinaryServer = require('binaryjs').BinaryServer;
|
||||
|
||||
exports.create = function(server) {
|
||||
|
||||
// link it to express
|
||||
var bs = BinaryServer({
|
||||
server: server,
|
||||
path: '/binary'
|
||||
});
|
||||
|
||||
function guid() {
|
||||
function s4() {
|
||||
return Math.floor((1 + Math.random()) * 0x10000)
|
||||
.toString(16)
|
||||
.substring(1);
|
||||
}
|
||||
return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
|
||||
s4() + '-' + s4() + s4() + s4();
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
function hash(text) {
|
||||
// A string hashing function based on Daniel J. Bernstein's popular 'times 33' hash algorithm.
|
||||
var h = 5381,
|
||||
index = text.length;
|
||||
while (index) {
|
||||
h = (h * 33) ^ text.charCodeAt(--index);
|
||||
}
|
||||
return h >>> 0;
|
||||
}
|
||||
|
||||
function getIP(socket) {
|
||||
return socket.upgradeReq.headers['x-forwarded-for'] || socket.upgradeReq.connection.remoteAddress;
|
||||
}
|
||||
// Wait for new user connections
|
||||
bs.on('connection', function(client) {
|
||||
|
||||
client.uuidRaw = guid();
|
||||
//ip is hashed to prevent injections by spoofing the 'x-forwarded-for' header
|
||||
// client.hashedIp = 1; //use this to test locally
|
||||
client.hashedIp = hash(getIP(client._socket));
|
||||
|
||||
client.deviceName = getDeviceName(client._socket.upgradeReq);
|
||||
|
||||
// Incoming stream from browsers
|
||||
client.on('stream', function(stream, meta) {
|
||||
if (meta && meta.serverMsg === 'rtc-support') {
|
||||
client.uuid = (meta.rtc ? 'rtc_' : '') + client.uuidRaw;
|
||||
client.send({
|
||||
isSystemEvent: true,
|
||||
type: 'handshake',
|
||||
name: client.deviceName,
|
||||
uuid: client.uuid
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (meta && meta.serverMsg === 'device-name') {
|
||||
//max name length = 40
|
||||
if (meta.name && meta.name.length > 40) {
|
||||
return;
|
||||
}
|
||||
client.name = meta.name;
|
||||
return;
|
||||
}
|
||||
|
||||
meta.from = client.uuid;
|
||||
|
||||
// broadcast to the other client
|
||||
for (var id in bs.clients) {
|
||||
if (bs.clients.hasOwnProperty(id)) {
|
||||
var otherClient = bs.clients[id];
|
||||
if (otherClient !== client && meta.toPeer === otherClient.uuid) {
|
||||
var send = otherClient.createStream(meta);
|
||||
stream.pipe(send, meta);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function forEachClient(fn) {
|
||||
for (var id in bs.clients) {
|
||||
if (bs.clients.hasOwnProperty(id)) {
|
||||
var client = bs.clients[id];
|
||||
fn(client);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function notifyBuddies() {
|
||||
var locations = {};
|
||||
//group all clients by location (by public ip address)
|
||||
forEachClient(function(client) {
|
||||
var ip = client.hashedIp;
|
||||
locations[ip] = locations[ip] || [];
|
||||
locations[ip].push({
|
||||
socket: client,
|
||||
contact: {
|
||||
peerId: client.uuid,
|
||||
name: client.name || client.deviceName,
|
||||
device: client.name ? client.deviceName : undefined
|
||||
}
|
||||
});
|
||||
});
|
||||
//notify every location
|
||||
Object.keys(locations).forEach(function(locationKey) {
|
||||
//notify every client of all other clients in this location
|
||||
var location = locations[locationKey];
|
||||
location.forEach(function(client) {
|
||||
//all other clients
|
||||
var buddies = location.reduce(function(result, otherClient) {
|
||||
if (otherClient !== client) {
|
||||
result.push(otherClient.contact);
|
||||
}
|
||||
return result;
|
||||
}, []);
|
||||
var currState = hash(JSON.stringify(buddies));
|
||||
console.log(currState);
|
||||
var socket = client.socket;
|
||||
//protocol
|
||||
var msg = {
|
||||
buddies: buddies,
|
||||
isSystemEvent: true,
|
||||
type: 'buddies'
|
||||
};
|
||||
//send only if state changed
|
||||
if (currState !== socket.lastState) {
|
||||
socket.send(msg);
|
||||
socket.lastState = currState;
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setInterval(notifyBuddies, 3000);
|
||||
};
|
359
gulpfile.js
|
@ -1,359 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
// Include Gulp & tools we'll use
|
||||
var autoprefixer = require('gulp-autoprefixer');
|
||||
var useref = require('gulp-useref');
|
||||
var vulcanize = require('vulcanize');
|
||||
var size = require('gulp-size');
|
||||
var gulp = require('gulp');
|
||||
var ghPages = require('gulp-gh-pages');
|
||||
var gulpIf = require('gulp-if');
|
||||
var jscs = require('gulp-jscs');
|
||||
var jscsStylish = require('gulp-jscs-stylish');
|
||||
var htmlExtract = require('gulp-html-extract');
|
||||
var imagemin = require('gulp-imagemin');
|
||||
var cleanCSS = require('gulp-clean-css');
|
||||
var changed = require('gulp-changed');
|
||||
var del = require('del');
|
||||
var uglify = require('gulp-uglify');
|
||||
var jshint = require('gulp-jshint');
|
||||
var runSequence = require('run-sequence');
|
||||
var browserSync = require('browser-sync');
|
||||
var reload = browserSync.reload;
|
||||
var merge = require('merge-stream');
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
var glob = require('glob-all');
|
||||
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 proxy = require('proxy-middleware');
|
||||
var url = require('url');
|
||||
var minifyHTML = require('gulp-htmlmin');
|
||||
var replace = require('gulp-replace');
|
||||
|
||||
// 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'
|
||||
];
|
||||
|
||||
var DIST = 'dist';
|
||||
|
||||
var dist = function(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(cleanCSS())
|
||||
.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'
|
||||
}));
|
||||
};
|
||||
|
||||
var optimizeHtmlTask = function(src, dest) {
|
||||
var assets = useref.assets({
|
||||
searchPath: ['.tmp', 'app']
|
||||
});
|
||||
|
||||
return gulp.src(src)
|
||||
.pipe(assets)
|
||||
// Concatenate and minify JavaScript
|
||||
.pipe(gulpIf('*.js', uglify({
|
||||
preserveComments: 'some'
|
||||
})))
|
||||
// Concatenate and minify styles
|
||||
// In case you are still using useref build blocks
|
||||
.pipe(gulpIf('*.css', cleanCSS()))
|
||||
.pipe(assets.restore())
|
||||
.pipe(useref())
|
||||
// Minify any HTML
|
||||
.pipe(gulpIf('*.html', minifyHTML({
|
||||
quotes: true,
|
||||
empty: true,
|
||||
spare: true
|
||||
})))
|
||||
.pipe(gulpIf('*.html', inlinesource()))
|
||||
.pipe(replace('window.debug = true;', ''))
|
||||
// Output files
|
||||
.pipe(gulp.dest(dest))
|
||||
.pipe(size({
|
||||
title: 'html'
|
||||
}));
|
||||
};
|
||||
|
||||
// Compile and automatically prefix stylesheets
|
||||
gulp.task('styles', function() {
|
||||
return styleTask('styles', ['**/*.css']);
|
||||
});
|
||||
|
||||
gulp.task('elements', function() {
|
||||
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'];
|
||||
|
||||
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(gulpIf('*.html', htmlExtract()))
|
||||
.pipe(jshint())
|
||||
.pipe(jscs())
|
||||
.pipe(jscsStylish.combineWithHintResults())
|
||||
.pipe(jshint.reporter('jshint-stylish'))
|
||||
.pipe(gulpIf(!browserSync.active, jshint.reporter('fail')));
|
||||
});
|
||||
|
||||
// Optimize images
|
||||
gulp.task('images', function() {
|
||||
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'
|
||||
}));
|
||||
});
|
||||
|
||||
// Copy web fonts to dist
|
||||
gulp.task('fonts', function() {
|
||||
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());
|
||||
});
|
||||
|
||||
// Vulcanize granular configuration
|
||||
gulp.task('vulcanize', function() {
|
||||
return gulp.src('app/elements/elements.html')
|
||||
.pipe(vulcanize({
|
||||
stripComments: true,
|
||||
stripExclude:['app/bower_components/font-roboto/roboto.html'],
|
||||
inlineCss: true,
|
||||
inlineScripts: true
|
||||
}))
|
||||
.pipe(minifyHTML({
|
||||
empty: true
|
||||
}))
|
||||
.pipe(gulp.dest(dist('elements')))
|
||||
.pipe(size({
|
||||
title: 'vulcanize'
|
||||
}));
|
||||
});
|
||||
|
||||
// Generate config data for the <sw-precache-cache> element.
|
||||
// This include a list of files that should be precached, as well as a (hopefully unique) cache
|
||||
// id that ensure that multiple PSK projects don't share the same Cache Storage.
|
||||
// This task does not run by default, but if you are interested in using service worker caching
|
||||
// in your project, please enable it within the 'default' task.
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Clean output directory
|
||||
gulp.task('clean', function() {
|
||||
return del(['.tmp', dist()]);
|
||||
});
|
||||
|
||||
// Watch files for changes & reload
|
||||
gulp.task('serve', ['styles', 'elements', 'images'], function() {
|
||||
var peerjsProxy = url.parse('http://localhost:3002/peerjs');
|
||||
peerjsProxy.route = '/peerjs';
|
||||
var websocketProxy = url.parse('http://localhost:3002/binary');
|
||||
websocketProxy.route = '/binary';
|
||||
browserSync({
|
||||
port: 5000,
|
||||
notify: false,
|
||||
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: [proxy(peerjsProxy), proxy(websocketProxy), 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);
|
||||
});
|
||||
|
||||
// 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()]
|
||||
});
|
||||
});
|
||||
|
||||
// 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', ['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);
|
||||
});
|
||||
|
||||
// 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(gulpIf(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
|
||||
// Adds tasks for `gulp test:local` and `gulp test:remote`
|
||||
require('web-component-tester').gulp.init(gulp);
|
||||
|
||||
// Load custom tasks from the `tasks` directory
|
||||
try {
|
||||
require('require-dir')('tasks');
|
||||
} catch (err) {}
|
23
index.js
|
@ -1,23 +0,0 @@
|
|||
'use strict';
|
||||
var express = require('express');
|
||||
var compression = require('compression');
|
||||
var app = express();
|
||||
var http = require('http');
|
||||
var ExpressPeerServer = require('peer').ExpressPeerServer;
|
||||
var wsServer = require('./server/ws-server.js');
|
||||
|
||||
var server = http.createServer(app);
|
||||
|
||||
// Serve up content from public directory
|
||||
app.use(compression());
|
||||
app.use(express.static(__dirname + '/public'));
|
||||
|
||||
var port = process.env.PORT || 3002;
|
||||
server.listen(port);
|
||||
wsServer.create(server);
|
||||
app.use('/peerjs', ExpressPeerServer(server, {
|
||||
debug: true
|
||||
}));
|
||||
|
||||
|
||||
console.log('listening on port ' + port);
|
60
nginx.conf.example
Normal file
|
@ -0,0 +1,60 @@
|
|||
# This is an configuration example. Please adjust to fit your environment (especially root location and server_name).
|
||||
# The nginx user requires read permissions to the root location.
|
||||
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
error_log /var/log/nginx/error.log;
|
||||
pid /run/nginx.pid;
|
||||
|
||||
# Load dynamic modules. See /usr/share/nginx/README.dynamic.
|
||||
include /usr/share/nginx/modules/*.conf;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# Load modular configuration files from the /etc/nginx/conf.d directory.
|
||||
# See http://nginx.org/en/docs/ngx_core_module.html#include
|
||||
# for more information.
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
|
||||
server {
|
||||
server_name your.domain;
|
||||
root /path/to/secret-snapdrop/client;
|
||||
|
||||
# Load configuration files for the default server block.
|
||||
include /etc/nginx/default.d/*.conf;
|
||||
|
||||
location /server {
|
||||
proxy_pass http://localhost:3000/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_http_version 1.1;
|
||||
}
|
||||
|
||||
|
||||
listen [::]:80 ;
|
||||
listen 80 ;
|
||||
}
|
||||
}
|
54
package.json
|
@ -1,54 +0,0 @@
|
|||
{
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"browser-sync": "^2.10.1",
|
||||
"connect-history-api-fallback": "^1.1.0",
|
||||
"del": "^2.0.2",
|
||||
"glob-all": "^3.0.1",
|
||||
"gulp": "^3.9.0",
|
||||
"gulp-autoprefixer": "^3.1.0",
|
||||
"gulp-cache": "^0.4.0",
|
||||
"gulp-changed": "^1.0.0",
|
||||
"gulp-clean-css": "^2.0.13",
|
||||
"gulp-gh-pages": "^0.5.4",
|
||||
"gulp-html-extract": "^0.0.3",
|
||||
"gulp-htmlmin": "^3.0.0",
|
||||
"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",
|
||||
"gulp-load-plugins": "^1.1.0",
|
||||
"gulp-rename": "^1.2.0",
|
||||
"gulp-replace": "^0.5.4",
|
||||
"gulp-size": "^2.0.0",
|
||||
"gulp-uglify": "^1.2.0",
|
||||
"gulp-useref": "^2.1.0",
|
||||
"gulp-vulcanize": "^6.0.0",
|
||||
"jshint-stylish": "^2.0.0",
|
||||
"merge-stream": "^1.0.0",
|
||||
"proxy-middleware": "^0.15.0",
|
||||
"require-dir": "^0.3.0",
|
||||
"run-sequence": "^1.0.2",
|
||||
"url": "^0.11.0",
|
||||
"vulcanize": ">= 1.4.2",
|
||||
"web-component-tester": "^4.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "gulp test:local",
|
||||
"start": "gulp serve",
|
||||
"lint": "gulp lint"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"binaryjs": "^0.2.1",
|
||||
"compression": "^1.6.0",
|
||||
"express": "^4.13.3",
|
||||
"peer": "^0.2.8",
|
||||
"ua-parser-js": "^0.7.10",
|
||||
"ws": "^1.1.1"
|
||||
}
|
||||
}
|
217
server/index.js
Normal file
|
@ -0,0 +1,217 @@
|
|||
const parser = require('ua-parser-js');
|
||||
|
||||
class SnapdropServer {
|
||||
|
||||
constructor(port) {
|
||||
const WebSocket = require('ws');
|
||||
this._wss = new WebSocket.Server({
|
||||
port: port
|
||||
});
|
||||
this._wss.on('connection', (socket, request) => this._onConnection(new Peer(socket, request)));
|
||||
this._wss.on('headers', (headers, response) => this._onHeaders(headers, response));
|
||||
|
||||
this._rooms = {};
|
||||
this._timerID = 0;
|
||||
|
||||
console.log('Snapdrop is running on port', port);
|
||||
}
|
||||
|
||||
_onConnection(peer) {
|
||||
this._joinRoom(peer);
|
||||
peer.socket.on('message', message => this._onMessage(peer, message));
|
||||
this._keepAlive(peer);
|
||||
}
|
||||
|
||||
_onHeaders(headers, response) {
|
||||
if (response.headers.cookie && response.headers.cookie.indexOf('peerid=') > -1) return;
|
||||
response.peerId = Peer.uuid();
|
||||
headers.push('Set-Cookie: peerid=' + response.peerId);
|
||||
}
|
||||
|
||||
_onMessage(sender, message) {
|
||||
message = JSON.parse(message);
|
||||
|
||||
switch (message.type) {
|
||||
case 'disconnect':
|
||||
this._leaveRoom(sender);
|
||||
case 'pong':
|
||||
sender.lastBeat = Date.now();
|
||||
}
|
||||
|
||||
// relay message to recipient
|
||||
if (message.to) {
|
||||
const recipientId = message.to; // TODO: sanitize
|
||||
const recipient = this._rooms[sender.ip][recipientId];
|
||||
delete message.to;
|
||||
// add sender id
|
||||
message.sender = sender.id;
|
||||
this._send(recipient, message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_joinRoom(peer) {
|
||||
// if room doesn't exist, create it
|
||||
if (!this._rooms[peer.ip]) {
|
||||
this._rooms[peer.ip] = {};
|
||||
}
|
||||
|
||||
// console.log(peer.id, ' joined the room', peer.ip);
|
||||
// notify all other peers
|
||||
for (const otherPeerId in this._rooms[peer.ip]) {
|
||||
const otherPeer = this._rooms[peer.ip][otherPeerId];
|
||||
this._send(otherPeer, {
|
||||
type: 'peer-joined',
|
||||
peer: peer.getInfo()
|
||||
});
|
||||
}
|
||||
|
||||
// notify peer about the other peers
|
||||
const otherPeers = [];
|
||||
for (const otherPeerId in this._rooms[peer.ip]) {
|
||||
otherPeers.push(this._rooms[peer.ip][otherPeerId].getInfo());
|
||||
}
|
||||
|
||||
this._send(peer, {
|
||||
type: 'peers',
|
||||
peers: otherPeers
|
||||
});
|
||||
|
||||
// add peer to room
|
||||
this._rooms[peer.ip][peer.id] = peer;
|
||||
}
|
||||
|
||||
_leaveRoom(peer) {
|
||||
// delete the peer
|
||||
this._cancelKeepAlive(peer);
|
||||
if (!this._rooms[peer.ip]) return;
|
||||
|
||||
delete this._rooms[peer.ip][peer.id];
|
||||
|
||||
peer.socket.terminate();
|
||||
//if room is empty, delete the room
|
||||
if (!Object.keys(this._rooms[peer.ip]).length) {
|
||||
delete this._rooms[peer.ip];
|
||||
} else {
|
||||
// notify all other peers
|
||||
for (const otherPeerId in this._rooms[peer.ip]) {
|
||||
const otherPeer = this._rooms[peer.ip][otherPeerId];
|
||||
this._send(otherPeer, {
|
||||
type: 'peer-left',
|
||||
peerId: peer.id
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_send(peer, message) {
|
||||
if (!peer) return console.error('undefined peer');
|
||||
message = JSON.stringify(message);
|
||||
peer.socket.send(message, error => {
|
||||
if (error) this._leaveRoom(peer);
|
||||
});
|
||||
}
|
||||
|
||||
_keepAlive(peer) {
|
||||
var timeout = 10000;
|
||||
// console.log(Date.now() - peer.lastBeat);
|
||||
if (Date.now() - peer.lastBeat > 2 * timeout) {
|
||||
this._leaveRoom(peer);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._wss.readyState == this._wss.OPEN) {
|
||||
this._send(peer, {
|
||||
type: 'ping'
|
||||
});
|
||||
}
|
||||
peer.timerId = setTimeout(() => this._keepAlive(peer), timeout);
|
||||
}
|
||||
|
||||
_cancelKeepAlive(peer) {
|
||||
if (peer.timerId) {
|
||||
clearTimeout(peer.timerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
class Peer {
|
||||
|
||||
constructor(socket, request) {
|
||||
// set socket
|
||||
this.socket = socket;
|
||||
|
||||
|
||||
// set remote ip
|
||||
if (request.headers['x-forwarded-for'])
|
||||
this.ip = request.headers['x-forwarded-for'].split(/\s*,\s*/)[0];
|
||||
else
|
||||
this.ip = request.connection.remoteAddress;
|
||||
|
||||
if (request.peerId) {
|
||||
this.id = request.peerId;
|
||||
} else {
|
||||
this.id = request.headers.cookie.replace('peerid=', '');
|
||||
}
|
||||
// set peer id
|
||||
// is WebRTC supported ?
|
||||
this.rtcSupported = request.url.indexOf('webrtc') > -1;
|
||||
// set name
|
||||
this.setName(request);
|
||||
// for keepalive
|
||||
this.timerId = 0;
|
||||
this.lastBeat = Date.now();
|
||||
}
|
||||
|
||||
// return uuid of form xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
|
||||
static uuid() {
|
||||
let uuid = '',
|
||||
ii;
|
||||
for (ii = 0; ii < 32; ii += 1) {
|
||||
switch (ii) {
|
||||
case 8:
|
||||
case 20:
|
||||
uuid += '-';
|
||||
uuid += (Math.random() * 16 | 0).toString(16);
|
||||
break;
|
||||
case 12:
|
||||
uuid += '-';
|
||||
uuid += '4';
|
||||
break;
|
||||
case 16:
|
||||
uuid += '-';
|
||||
uuid += (Math.random() * 4 | 8).toString(16);
|
||||
break;
|
||||
default:
|
||||
uuid += (Math.random() * 16 | 0).toString(16);
|
||||
}
|
||||
}
|
||||
return uuid;
|
||||
};
|
||||
|
||||
toString() {
|
||||
return `<Peer id=${this.id} ip=${this.ip} rtcSupported=${this.rtcSupported}>`
|
||||
}
|
||||
|
||||
setName(req) {
|
||||
var ua = parser(req.headers['user-agent']);
|
||||
this.name = {
|
||||
model: ua.device.model,
|
||||
os: ua.os.name,
|
||||
browser: ua.browser.name,
|
||||
type: ua.device.type
|
||||
};
|
||||
}
|
||||
|
||||
getInfo() {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
rtcSupported: this.rtcSupported
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const server = new SnapdropServer(process.env.PORT || 3000);
|
26
server/package-lock.json
generated
Normal file
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"name": "snapdrop",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"async-limiter": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz",
|
||||
"integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg=="
|
||||
},
|
||||
"ua-parser-js": {
|
||||
"version": "0.7.18",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.18.tgz",
|
||||
"integrity": "sha512-LtzwHlVHwFGTptfNSgezHp7WUlwiqb0gA9AALRbKaERfxwJoiX0A73QbTToxteIAuIaFshhgIZfqK8s7clqgnA=="
|
||||
},
|
||||
"ws": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-6.0.0.tgz",
|
||||
"integrity": "sha512-c2UlYcAZp1VS8AORtpq6y4RJIkJ9dQz18W32SpR/qXGfLDZ2jU4y4wKvvZwqbi7U6gxFQTeE+urMbXU/tsDy4w==",
|
||||
"requires": {
|
||||
"async-limiter": "~1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
15
server/package.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"name": "snapdrop",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"ua-parser-js": "^0.7.18",
|
||||
"ws": "^6.0.0"
|
||||
}
|
||||
}
|
|
@ -1,154 +0,0 @@
|
|||
'use strict';
|
||||
var parser = require('ua-parser-js');
|
||||
|
||||
// Start Binary.js server
|
||||
var BinaryServer = require('binaryjs').BinaryServer;
|
||||
|
||||
exports.create = function(server) {
|
||||
|
||||
// link it to express
|
||||
var bs = BinaryServer({
|
||||
server: server,
|
||||
path: '/binary'
|
||||
});
|
||||
|
||||
function guid() {
|
||||
function s4() {
|
||||
return Math.floor((1 + Math.random()) * 0x10000)
|
||||
.toString(16)
|
||||
.substring(1);
|
||||
}
|
||||
return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
|
||||
s4() + '-' + s4() + s4() + s4();
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
function hash(text) {
|
||||
// A string hashing function based on Daniel J. Bernstein's popular 'times 33' hash algorithm.
|
||||
var h = 5381,
|
||||
index = text.length;
|
||||
while (index) {
|
||||
h = (h * 33) ^ text.charCodeAt(--index);
|
||||
}
|
||||
return h >>> 0;
|
||||
}
|
||||
|
||||
function getIP(socket) {
|
||||
return socket.upgradeReq.headers['x-forwarded-for'] || socket.upgradeReq.connection.remoteAddress;
|
||||
}
|
||||
// Wait for new user connections
|
||||
bs.on('connection', function(client) {
|
||||
|
||||
client.uuidRaw = guid();
|
||||
//ip is hashed to prevent injections by spoofing the 'x-forwarded-for' header
|
||||
// client.hashedIp = 1; //use this to test locally
|
||||
client.hashedIp = hash(getIP(client._socket));
|
||||
|
||||
client.deviceName = getDeviceName(client._socket.upgradeReq);
|
||||
|
||||
// Incoming stream from browsers
|
||||
client.on('stream', function(stream, meta) {
|
||||
if (meta && meta.serverMsg === 'rtc-support') {
|
||||
client.uuid = (meta.rtc ? 'rtc_' : '') + client.uuidRaw;
|
||||
client.send({
|
||||
isSystemEvent: true,
|
||||
type: 'handshake',
|
||||
name: client.deviceName,
|
||||
uuid: client.uuid
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (meta && meta.serverMsg === 'device-name') {
|
||||
//max name length = 40
|
||||
if (meta.name && meta.name.length > 40) {
|
||||
return;
|
||||
}
|
||||
client.name = meta.name;
|
||||
return;
|
||||
}
|
||||
|
||||
meta.from = client.uuid;
|
||||
|
||||
// broadcast to the other client
|
||||
for (var id in bs.clients) {
|
||||
if (bs.clients.hasOwnProperty(id)) {
|
||||
var otherClient = bs.clients[id];
|
||||
if (otherClient !== client && meta.toPeer === otherClient.uuid) {
|
||||
var send = otherClient.createStream(meta);
|
||||
stream.pipe(send, meta);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function forEachClient(fn) {
|
||||
for (var id in bs.clients) {
|
||||
if (bs.clients.hasOwnProperty(id)) {
|
||||
var client = bs.clients[id];
|
||||
fn(client);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function notifyBuddies() {
|
||||
var locations = {};
|
||||
//group all clients by location (by public ip address)
|
||||
forEachClient(function(client) {
|
||||
var ip = client.hashedIp;
|
||||
locations[ip] = locations[ip] || [];
|
||||
locations[ip].push({
|
||||
socket: client,
|
||||
contact: {
|
||||
peerId: client.uuid,
|
||||
name: client.name || client.deviceName,
|
||||
device: client.name ? client.deviceName : undefined
|
||||
}
|
||||
});
|
||||
});
|
||||
//notify every location
|
||||
Object.keys(locations).forEach(function(locationKey) {
|
||||
//notify every client of all other clients in this location
|
||||
var location = locations[locationKey];
|
||||
location.forEach(function(client) {
|
||||
//all other clients
|
||||
var buddies = location.reduce(function(result, otherClient) {
|
||||
if (otherClient !== client) {
|
||||
result.push(otherClient.contact);
|
||||
}
|
||||
return result;
|
||||
}, []);
|
||||
var currState = hash(JSON.stringify(buddies));
|
||||
console.log(currState);
|
||||
var socket = client.socket;
|
||||
//protocol
|
||||
var msg = {
|
||||
buddies: buddies,
|
||||
isSystemEvent: true,
|
||||
type: 'buddies'
|
||||
};
|
||||
//send only if state changed
|
||||
if (currState !== socket.lastState) {
|
||||
socket.send(msg);
|
||||
socket.lastState = currState;
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setInterval(notifyBuddies, 3000);
|
||||
};
|
|
@ -1,34 +0,0 @@
|
|||
var fs = require('fs');
|
||||
|
||||
/**
|
||||
* @param {Array<string>} files
|
||||
* @param {Function} cb
|
||||
*/
|
||||
|
||||
function ensureFiles(files, cb) {
|
||||
var missingFiles = files.reduce(function(prev, filePath) {
|
||||
var fileFound = false;
|
||||
|
||||
try {
|
||||
fileFound = fs.statSync(filePath).isFile();
|
||||
} catch (e) { }
|
||||
|
||||
if (!fileFound) {
|
||||
prev.push(filePath + ' Not Found');
|
||||
}
|
||||
|
||||
return prev;
|
||||
}, []);
|
||||
|
||||
if (missingFiles.length) {
|
||||
var err = new Error('Missing Required Files\n' + missingFiles.join('\n'));
|
||||
}
|
||||
|
||||
if (cb) {
|
||||
cb(err);
|
||||
} else if (err) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ensureFiles;
|
18
wct.conf.js
|
@ -1,18 +0,0 @@
|
|||
var path = require('path');
|
||||
|
||||
var ret = {
|
||||
'suites': ['app/test'],
|
||||
'webserver': {
|
||||
'pathMappings': []
|
||||
}
|
||||
};
|
||||
|
||||
var mapping = {};
|
||||
var rootPath = (__dirname).split(path.sep).slice(-1)[0];
|
||||
|
||||
mapping['/components/' + rootPath +
|
||||
'/app/bower_components'] = 'bower_components';
|
||||
|
||||
ret.webserver.pathMappings.push(mapping);
|
||||
|
||||
module.exports = ret;
|