Full Code of ShareDropio/sharedrop for AI

master 2b6a9fe5e6ca cached
93 files
131.4 KB
37.8k tokens
86 symbols
1 requests
Download .txt
Repository: ShareDropio/sharedrop
Branch: master
Commit: 2b6a9fe5e6ca
Files: 93
Total size: 131.4 KB

Directory structure:
gitextract_vxj0u4do/

├── .editorconfig
├── .ember-cli
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .prettierignore
├── .template-lintrc.js
├── .travis.yml
├── .watchmanconfig
├── Dockerfile
├── LICENSE
├── Procfile
├── Procfile.dev
├── README.md
├── app/
│   ├── app.js
│   ├── components/
│   │   ├── circular-progress.hbs
│   │   ├── circular-progress.js
│   │   ├── file-field.js
│   │   ├── modal-dialog.js
│   │   ├── peer-avatar.js
│   │   ├── peer-widget.js
│   │   ├── popover-confirm.js
│   │   ├── room-url.js
│   │   └── user-widget.js
│   ├── controllers/
│   │   ├── application.js
│   │   └── index.js
│   ├── helpers/
│   │   └── is-equal.js
│   ├── index.html
│   ├── initializers/
│   │   └── prerequisites.js
│   ├── models/
│   │   ├── peer.js
│   │   └── user.js
│   ├── router.js
│   ├── routes/
│   │   ├── application.js
│   │   ├── error.js
│   │   ├── index.js
│   │   └── room.js
│   ├── services/
│   │   ├── analytics.js
│   │   ├── avatar.js
│   │   ├── file.js
│   │   ├── room.js
│   │   └── web-rtc.js
│   ├── styles/
│   │   ├── app.sass
│   │   ├── base/
│   │   │   ├── _element_defaults.sass
│   │   │   ├── _glyphicons_filetypes.sass
│   │   │   ├── _mixins.sass
│   │   │   ├── _reset.sass
│   │   │   └── _variables.sass
│   │   ├── layout/
│   │   │   ├── _content.sass
│   │   │   ├── _footer.sass
│   │   │   ├── _header.sass
│   │   │   └── _media.sass
│   │   └── modules/
│   │       ├── _modal.sass
│   │       ├── _modules.sass
│   │       ├── _popover.sass
│   │       └── _users.sass
│   └── templates/
│       ├── about-app.hbs
│       ├── about-room.hbs
│       ├── about-you.hbs
│       ├── application.hbs
│       ├── components/
│       │   ├── modal-dialog.hbs
│       │   ├── peer-widget.hbs
│       │   ├── popover-confirm.hbs
│       │   └── user-widget.hbs
│       ├── errors/
│       │   ├── browser-unsupported.hbs
│       │   ├── filesystem-unavailable.hbs
│       │   └── popovers/
│       │       ├── connection-failed.hbs
│       │       └── multiple-files.hbs
│       └── index.hbs
├── config/
│   ├── dotenv.js
│   ├── environment.js
│   ├── optional-features.json
│   └── targets.js
├── ember-cli-build.js
├── firebase_rules.json
├── lib/
│   └── google-analytics/
│       ├── index.js
│       └── package.json
├── newrelic.js
├── package.json
├── prettier.config.js
├── public/
│   ├── .gitkeep
│   ├── .well-known/
│   │   └── brave-rewards-verification.txt
│   ├── crossdomain.xml
│   └── robots.txt
├── server.js
├── sharedrop.crx
├── testem.js
├── tests/
│   ├── helpers/
│   │   └── .gitkeep
│   ├── index.html
│   ├── integration/
│   │   └── .gitkeep
│   ├── test-helper.js
│   └── unit/
│       └── .gitkeep
└── vendor/
    ├── .gitkeep
    └── peer.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org

root = true

[*]
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
indent_style = space
indent_size = 2

[*.hbs]
insert_final_newline = false

[*.{diff,md}]
trim_trailing_whitespace = false


================================================
FILE: .ember-cli
================================================
{
  /**
    Ember CLI sends analytics information by default. The data is completely
    anonymous, but there are times when you might want to disable this behavior.

    Setting `disableAnalytics` to true will prevent any data from being sent.
  */
  "disableAnalytics": false
}


================================================
FILE: .eslintignore
================================================
# unconventional js
/blueprints/*/files/
/vendor/

# compiled output
/dist/
/tmp/

# dependencies
/bower_components/
/node_modules/

# misc
/coverage/
!.*

# ember-try
/.node_modules.ember-try/
/bower.json.ember-try
/package.json.ember-try


================================================
FILE: .eslintrc.js
================================================
const eslintPluginNode = require('eslint-plugin-node');

module.exports = {
  root: true,
  parser: 'babel-eslint',
  parserOptions: {
    ecmaVersion: 2018,
    sourceType: 'module',
    ecmaFeatures: {
      legacyDecorators: true,
    },
  },
  plugins: ['ember', 'prettier'],
  extends: ['airbnb-base', 'plugin:ember/recommended', 'prettier'],
  env: {
    browser: true,
  },
  rules: {
    'func-names': 'off',
    'no-console': 'off',
    'no-underscore-dangle': 'off',

    'import/no-extraneous-dependencies': 'off',
    'import/no-unresolved': 'off',

    'prettier/prettier': 'error',

    // TODO: Enable these
    'ember/no-jquery': 'off',
    'ember/no-observers': 'off',
    'ember/no-get': 'off',
  },
  overrides: [
    // node files
    {
      files: [
        '.eslintrc.js',
        '.template-lintrc.js',
        'ember-cli-build.js',
        'testem.js',
        'blueprints/*/index.js',
        'config/**/*.js',
        'lib/*/index.js',
        'server/**/*.js',
      ],
      parserOptions: {
        sourceType: 'script',
      },
      env: {
        browser: false,
        node: true,
      },
      plugins: ['node'],
      rules: {
        ...eslintPluginNode.configs.recommended.rules,
        // add your custom rules and overrides for node files here

        // this can be removed once the following is fixed
        // https://github.com/mysticatea/eslint-plugin-node/issues/77
        'node/no-unpublished-require': 'off',
      },
    },
  ],
};


================================================
FILE: .gitignore
================================================
# See https://help.github.com/ignore-files/ for more about ignoring files.

# compiled output
/dist/
/tmp/

# dependencies
/bower_components/
/node_modules/

# misc
/.env*
/.pnp*
/.sass-cache
/connect.lock
/coverage/
/libpeerconnection.log
/npm-debug.log*
/testem.log
/yarn-error.log

# ember-try
/.node_modules.ember-try/
/bower.json.ember-try
/package.json.ember-try


================================================
FILE: .prettierignore
================================================
##########
# Common
##########

# Duh
.git/

# Third party
/node_modules/
/vendor/

# Build products
/dist/
/tmp/
/coverage/
/.sass-cache/


================================================
FILE: .template-lintrc.js
================================================
module.exports = {
  extends: 'octane',

  // TODO: enable these
  rules: {
    'no-action': false,
    'no-curly-component-invocation': false,
    'no-implicit-this': false,
    'no-partial': false,
  },
};


================================================
FILE: .travis.yml
================================================
---
language: node_js
node_js:
  - "12"

dist: xenial

addons:
  chrome: stable

cache:
  yarn: true

env:
  global:
    # See https://git.io/vdao3 for details.
    - JOBS=1

before_install:
  - curl -o- -L https://yarnpkg.com/install.sh | bash
  - export PATH=$HOME/.yarn/bin:$PATH

script:
  - yarn test


================================================
FILE: .watchmanconfig
================================================
{
  "ignore_dirs": [
    "tmp",
    "dist"
  ]
}


================================================
FILE: Dockerfile
================================================
FROM node:14-buster
RUN mkdir -p /srv/app
WORKDIR /srv/app
COPY package.json yarn.lock ./
RUN yarn --frozen-lockfile --non-interactive

COPY . /srv/app
EXPOSE 8000
CMD [ "yarn", "develop" ]


================================================
FILE: LICENSE
================================================
The MIT License

Copyright (c) 2014-2024 Szymon Nowak

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


================================================
FILE: Procfile
================================================
web: node server.js


================================================
FILE: Procfile.dev
================================================
server: node server.js
web: ember build --watch


================================================
FILE: README.md
================================================
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/9873/200137755-47fe06a7-3c37-439f-b44c-cee9168418fa.svg">
  <img alt="ShareDrop" src="https://user-images.githubusercontent.com/9873/200137972-bfd145a2-36eb-48ad-8568-53184d3599c1.svg">
</picture>

# ShareDrop is now LimeWire
Dear ShareDrop community,
ShareDrop has been acquired by LimeWire, a leading file sharing platform with integrated AI tools. You can continue to share any files between devices, while benefitting from:
* sharing files between devices in the same network
* anonymous up- & downloads
* end-to-end encryption
* up to 40GB storage for free users
* integrated AI tools for signed-up users

Visit [sharedrop.io](https://sharedrop.io) or [limewire.com](https://limewire.com)

The Github repository will stay as-is and you can still go ahead and download and run the classic ShareDrop on your own infrastructure.

# ShareDrop Classic

ShareDrop is a web application inspired by Apple [AirDrop](http://support.apple.com/kb/ht4783) service. It allows you to transfer files directly between devices, without having to upload them to any server first. It uses [WebRTC](http://www.webrtc.org) for secure peer-to-peer file transfer and [Firebase](https://www.firebase.com) for presence management and WebRTC signaling.

ShareDrop allows you to send files to other devices in the same local network (i.e. devices with the same public IP address) without any configuration - simply open <https://www.sharedrop.io> on all devices and they will see each other. It also allows you to send files between networks - just click the `+` button in the top right corner of the page to create a room with a unique URL and share this URL with other people you want to send a file to. Once they open this page in a browser on their devices, you'll see each other's avatars.

The main difference between ShareDrop and AirDrop is that ShareDrop requires Internet connection to discover other devices, while AirDrop doesn't need one, as it creates ad-hoc wireless network between them. On the other hand, ShareDrop allows you to share files between mobile (Android and iOS) and desktop devices and even between networks.

## Supported browsers

- Chrome
- Edge (Chromium based)
- Firefox
- Opera
- Safari 13+

## Local development

1.  Setup Firebase:
    1.  [Sign up](https://www.firebase.com) for a Firebase account and create a database.
    2.  Go to "Security Rules" tab, click "Load Rules" button and select `firebase_rules.json` file.
    3.  Take note of your database URL and its secret, which can be found in "Secrets" tab.
2.  Run `npm install -g ember-cli` to install Ember CLI.
3.  Run `yarn` to install app dependencies.
4.  Run `cp .env{.sample,}` to create `.env` file. This file will be used by Foreman to set environment variables when running the app locally.
    - `SECRET` key is used to encrypt cookies and generate room name based on public IP address for `/` route. It can be any random string - you can generate one using e.g. `date | md5sum`
    - `NEW_RELIC_*` keys are only necessary in production
5.  Run `yarn develop` to start the app.

## Deployment

### Heroku

Create a new Heroku app:

```
heroku create <app-name>
```

and push the app to Heroku repo:

```
git push heroku master
```


================================================
FILE: app/app.js
================================================
import Application from '@ember/application';
import Resolver from 'ember-resolver';
import loadInitializers from 'ember-load-initializers';
import config from 'sharedrop/config/environment';
import * as Sentry from '@sentry/browser';
import { Ember as EmberIntegration } from '@sentry/integrations';

Sentry.init({
  dsn:
    'https://ba1292a9c759401dbbda4272f183408d@o432021.ingest.sentry.io/5384091',
  integrations: [new EmberIntegration()],
});

export default class App extends Application {
  modulePrefix = config.modulePrefix;

  podModulePrefix = config.podModulePrefix;

  Resolver = Resolver;
}

loadInitializers(App, config.modulePrefix);


================================================
FILE: app/components/circular-progress.hbs
================================================
<svg width="76" height="76" viewport="0 0 76 76" style={{this.style}}>
  <path class="break" transform="translate(38, 38)" d={{this.path}} />
</svg>

================================================
FILE: app/components/circular-progress.js
================================================
import Component from '@glimmer/component';
import { htmlSafe } from '@ember/template';

const COLORS = {
  blue: '0, 136, 204',
  orange: '197, 197, 51',
};

export default class CircularProgress extends Component {
  constructor(owner, args) {
    super(owner, args);

    const rgb = COLORS[args.color];
    this.style = htmlSafe(`fill: rgba(${rgb}, .5)`);
  }

  get path() {
    const π = Math.PI;
    const α = this.args.value * 360;
    const r = (α * π) / 180;
    const mid = α > 180 ? 1 : 0;
    const x = Math.sin(r) * 38;
    const y = Math.cos(r) * -38;

    return `M 0 0 v -38 A 38 38 1 ${mid} 1 ${x} ${y} z`;
  }
}


================================================
FILE: app/components/file-field.js
================================================
import TextField from '@ember/component/text-field';
import $ from 'jquery';

export default TextField.extend({
  type: 'file',
  classNames: ['invisible'],

  click(event) {
    event.stopPropagation();
  },

  change(event) {
    const input = event.target;
    const { files } = input;
    this.onChange({ files });
    this.reset();
  },

  // Hackish way to reset file input when sender cancels file transfer,
  // so if sender wants later to send the same file again,
  // the 'change' event is triggered correctly.
  reset() {
    const field = $(this.element);
    field.wrap('<form>').closest('form').get(0).reset();
    field.unwrap();
  },
});


================================================
FILE: app/components/modal-dialog.js
================================================
import Component from '@ember/component';

export default Component.extend({
  actions: {
    close() {
      // This sends an action to application route.
      // eslint-disable-next-line ember/closure-actions
      return this.onClose();
    },
  },
});


================================================
FILE: app/components/peer-avatar.js
================================================
import Component from '@ember/component';
import { alias } from '@ember/object/computed';
import { later } from '@ember/runloop';
import $ from 'jquery';

export default Component.extend({
  tagName: 'img',
  classNames: ['gravatar'],
  attributeBindings: [
    'src',
    'alt',
    'title',
    'data-sending-progress',
    'data-receiving-progress',
  ],
  src: alias('peer.avatarUrl'),
  alt: alias('peer.label'),
  title: alias('peer.uuid'),
  'data-sending-progress': alias('peer.transfer.sendingProgress'),
  'data-receiving-progress': alias('peer.transfer.receivingProgress'),

  toggleTransferCompletedClass() {
    const className = 'transfer-completed';

    later(
      this,
      function toggleClass() {
        $(this.element)
          .parent('.avatar')
          .addClass(className)
          .delay(2000)
          .queue(function removeClass() {
            $(this).removeClass(className).dequeue();
          });
      },
      250,
    );
  },

  init(...args) {
    this._super(args);

    this.toggleTransferCompletedClass = this.toggleTransferCompletedClass.bind(
      this,
    );
  },

  didInsertElement(...args) {
    this._super(args);
    const { peer } = this;

    peer.on('didReceiveFile', this.toggleTransferCompletedClass);
    peer.on('didSendFile', this.toggleTransferCompletedClass);
  },

  willDestroyElement(...args) {
    this._super(args);
    const { peer } = this;

    peer.off('didReceiveFile', this.toggleTransferCompletedClass);
    peer.off('didSendFile', this.toggleTransferCompletedClass);
  },

  // Delegate click to hidden file field in peer template
  click() {
    if (this.canSendFile()) {
      $(this.element).closest('.peer').find('input[type=file]').click();
    }
  },

  // Handle drop events
  dragEnter(event) {
    this.cancelEvent(event);

    $(this.element).parent('.avatar').addClass('hover');
  },

  dragOver(event) {
    this.cancelEvent(event);
  },

  dragLeave() {
    $(this.element).parent('.avatar').removeClass('hover');
  },

  drop(event) {
    this.cancelEvent(event);
    $(this.element).parent('.avatar').removeClass('hover');

    const { peer } = this;
    const dt = event.originalEvent.dataTransfer;
    const { files } = dt;

    if (this.canSendFile()) {
      if (!this.isTransferableBundle(files)) {
        peer.setProperties({
          state: 'error',
          errorCode: 'multiple-files',
        });
      } else {
        this.onFileDrop({ files });
      }
    }
  },

  cancelEvent(event) {
    event.stopPropagation();
    event.preventDefault();
  },

  canSendFile() {
    const { peer } = this;

    // Can't send files if another file transfer is already in progress
    return !(
      peer.get('state') === 'is_preparing_file_transfer' ||
      peer.get('transfer.file') ||
      peer.get('transfer.info')
    );
  },

  isTransferableBundle(files) {
    if (files.length === 1 && files[0] instanceof window.File) return true;

    const fileSizeLimit = 50 * 1024 * 1024;
    const bundleSizeLimit = 200 * 1024 * 1024;
    let aggregatedSize = 0;
    // eslint-disable-next-line no-restricted-syntax
    for (const file of files) {
      if (!(file instanceof window.File)) {
        return false;
      }
      if (file.size > fileSizeLimit) {
        return false;
      }
      aggregatedSize += file.size;
      if (aggregatedSize > bundleSizeLimit) {
        return false;
      }
    }
    return true;
  },
});


================================================
FILE: app/components/peer-widget.js
================================================
import Component from '@ember/component';
import { computed } from '@ember/object';
import { alias, equal, gt } from '@ember/object/computed';
import JSZip from 'jszip';

export default Component.extend({
  classNames: ['peer'],
  classNameBindings: ['peer.peer.state'],

  peer: null,
  hasCustomRoomName: false,
  webrtc: null, // TODO inject webrtc as a service

  label: alias('peer.label'),

  isIdle: equal('peer.state', 'idle'),
  isPreparingFileTransfer: equal('peer.state', 'is_preparing_file_transfer'),
  hasSelectedFile: equal('peer.state', 'has_selected_file'),
  isSendingFileInfo: equal('peer.state', 'sending_file_info'),
  isAwaitingFileInfo: equal('peer.state', 'awaiting_file_info'),
  isAwaitingResponse: equal('peer.state', 'awaiting_response'),
  hasReceivedFileInfo: equal('peer.state', 'received_file_info'),
  hasDeclinedFileTransfer: equal('peer.state', 'declined_file_transfer'),
  hasError: equal('peer.state', 'error'),

  isReceivingFile: gt('peer.transfer.receivingProgress', 0),
  isSendingFile: gt('peer.transfer.sendingProgress', 0),

  filename: computed('peer.transfer.{file,info}', function () {
    const file = this.get('peer.transfer.file');
    const info = this.get('peer.transfer.info');

    if (file) {
      return file.name;
    }
    if (info) {
      return info.name;
    }

    return null;
  }),

  errorTemplateName: computed('peer.errorCode', function () {
    const errorCode = this.get('peer.errorCode');

    return errorCode ? `errors/popovers/${errorCode}` : null;
  }),

  actions: {
    // TODO: rename to something more meaningful (e.g. askIfWantToSendFile)
    uploadFile(data) {
      const { peer } = this;
      const { files } = data;

      peer.set('state', 'is_preparing_file_transfer');
      peer.set('bundlingProgress', 0);

      // Cache the file, so that it's available
      // when the response from the recipient comes in
      this._reduceFiles(files).then((file) => {
        peer.set('transfer.file', file);
        peer.set('state', 'has_selected_file');
      });
    },

    sendFileTransferInquiry() {
      const { webrtc } = this;
      const { peer } = this;

      webrtc.connect(peer.get('peer.id'));
      peer.set('state', 'establishing_connection');
    },

    cancelFileTransfer() {
      this._cancelFileTransfer();
    },

    abortFileTransfer() {
      this._cancelFileTransfer();

      const { webrtc } = this;
      const connection = this.get('peer.peer.connection');

      webrtc.sendCancelRequest(connection);
    },

    acceptFileTransfer() {
      const { peer } = this;

      this._sendFileTransferResponse(true);

      peer.get('peer.connection').on('receiving_progress', (progress) => {
        peer.set('transfer.receivingProgress', progress);
      });
      peer.set('state', 'sending_file_data');
    },

    rejectFileTransfer() {
      const { peer } = this;

      this._sendFileTransferResponse(false);
      peer.set('transfer.info', null);
      peer.set('state', 'idle');
    },
  },

  _cancelFileTransfer() {
    const { peer } = this;

    peer.setProperties({
      'transfer.file': null,
      state: 'idle',
    });
  },

  _sendFileTransferResponse(response) {
    const { webrtc } = this;
    const { peer } = this;
    const connection = peer.get('peer.connection');

    webrtc.sendFileResponse(connection, response);
  },

  async _reduceFiles(files) {
    const { peer } = this;

    if (files.length === 1) {
      return Promise.resolve(files[0]);
    }

    const zip = new JSZip();

    Array.prototype.forEach.call(files, (file) => {
      zip.file(file.name, file);
    });

    const blob = await zip.generateAsync(
      { type: 'blob', streamFiles: true },
      (metadata) => {
        peer.set('bundlingProgress', metadata.percent / 100);
      },
    );

    return new File(
      [blob],
      `sharedrop-${new Date()
        .toISOString()
        .substring(0, 19)
        .replace('T', '-')}.zip`,
      {
        type: 'application/zip',
      },
    );
  },
});


================================================
FILE: app/components/popover-confirm.js
================================================
import Component from '@ember/component';
import { computed } from '@ember/object';

export default Component.extend({
  classNames: ['popover-confirm'],
  iconClass: computed('filename', function () {
    const { filename } = this;

    if (filename) {
      const regex = /\.([0-9a-z]+)$/i;
      const match = filename.match(regex);
      const extension = match && match[1];

      if (extension) {
        return `glyphicon-${extension.toLowerCase()}`;
      }
    }

    return undefined;
  }),

  actions: {
    confirm() {
      this.onConfirm();
    },

    cancel() {
      this.onCancel();
    },
  },
});


================================================
FILE: app/components/room-url.js
================================================
import TextField from '@ember/component/text-field';
import $ from 'jquery';

export default TextField.extend({
  classNames: ['room-url'],

  didInsertElement() {
    $(this.element).focus().select();
  },

  copyValueToClipboard() {
    if (window.ClipboardEvent) {
      const pasteEvent = new window.ClipboardEvent('paste', {
        dataType: 'text/plain',
        data: this.element.value,
      });
      document.dispatchEvent(pasteEvent);
    }
  },
});


================================================
FILE: app/components/user-widget.js
================================================
import Component from '@ember/component';

export default Component.extend({
  classNames: ['peer'],
  classNameBindings: ['peer.peer.state'],
});


================================================
FILE: app/controllers/application.js
================================================
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { v4 as uuidv4 } from 'uuid';

import User from '../models/user';

export default Controller.extend({
  avatarService: service('avatar'),

  init(...args) {
    this._super(args);

    const id = window.Sharedrop.userId;
    const ip = window.Sharedrop.publicIp;
    const avatar = this.avatarService.get();
    const you = User.create({
      uuid: id,
      public_ip: ip,
      avatarUrl: avatar.url,
      label: avatar.label,
    });

    you.set('peer.id', id);
    this.set('you', you);
  },

  actions: {
    redirect() {
      const uuid = uuidv4();
      const key = `show-instructions-for-room-${uuid}`;

      sessionStorage.setItem(key, 'yup');
      this.transitionToRoute('room', uuid);
    },
  },
});


================================================
FILE: app/controllers/index.js
================================================
import Controller, { inject as controller } from '@ember/controller';
import { alias } from '@ember/object/computed';
import $ from 'jquery';

import WebRTC from '../services/web-rtc';
import Peer from '../models/peer';

export default Controller.extend({
  application: controller('application'),
  you: alias('application.you'),
  room: null,
  webrtc: null,

  _onRoomConnected(event, data) {
    const { you } = this;
    const { room } = this;

    you.get('peer').setProperties(data.peer);
    // eslint-disable-next-line no-param-reassign
    delete data.peer;
    you.setProperties(data);

    // Initialize WebRTC
    this.set(
      'webrtc',
      new WebRTC(you.get('uuid'), {
        room: room.name,
        firebaseRef: window.Sharedrop.ref,
      }),
    );
  },

  _onRoomDisconnected() {
    this.model.clear();
    this.set('webrtc', null);
  },

  _onRoomUserAdded(event, data) {
    const { you } = this;

    if (you.get('uuid') !== data.uuid) {
      this._addPeer(data);
    }
  },

  _addPeer(attrs) {
    const peerAttrs = attrs.peer;

    // eslint-disable-next-line no-param-reassign
    delete attrs.peer;
    const peer = Peer.create(attrs);
    peer.get('peer').setProperties(peerAttrs);

    this.model.pushObject(peer);
  },

  _onRoomUserChanged(event, data) {
    const peers = this.model;
    const peer = peers.findBy('uuid', data.uuid);
    const peerAttrs = data.peer;
    const defaults = {
      uuid: null,
      public_ip: null,
    };

    if (peer) {
      // eslint-disable-next-line no-param-reassign
      delete data.peer;
      // Firebase doesn't return keys with null values,
      // so we have to add them back.
      peer.setProperties($.extend({}, defaults, data));
      peer.get('peer').setProperties(peerAttrs);
    }
  },

  _onRoomUserRemoved(event, data) {
    const peers = this.model;
    const peer = peers.findBy('uuid', data.uuid);

    peers.removeObject(peer);
  },

  _onPeerP2PIncomingConnection(event, data) {
    const { connection } = data;
    const peers = this.model;
    const peer = peers.findBy('peer.id', connection.peer);

    // Don't switch to 'connecting' state on incoming connection,
    // as p2p connection may still fail.
    peer.set('peer.connection', connection);
  },

  _onPeerDCIncomingConnection(event, data) {
    const { connection } = data;
    const peers = this.model;
    const peer = peers.findBy('peer.id', connection.peer);

    peer.set('peer.state', 'connected');
  },

  _onPeerDCIncomingConnectionError(event, data) {
    const { connection, error } = data;
    const peers = this.model;
    const peer = peers.findBy('peer.id', connection.peer);

    switch (error.type) {
      case 'failed':
        peer.setProperties({
          'peer.connection': null,
          'peer.state': 'disconnected',
          state: 'error',
          errorCode: 'connection-failed',
        });
        break;
      case 'disconnected':
        // TODO: notify both sides
        break;
      default:
        break;
    }
  },

  _onPeerP2POutgoingConnection(event, data) {
    const { connection } = data;
    const peers = this.model;
    const peer = peers.findBy('peer.id', connection.peer);

    peer.setProperties({
      'peer.connection': connection,
      'peer.state': 'connecting',
    });
  },

  _onPeerDCOutgoingConnection(event, data) {
    const { connection } = data;
    const peers = this.model;
    const peer = peers.findBy('peer.id', connection.peer);
    const file = peer.get('transfer.file');
    const { webrtc } = this;
    const info = webrtc.getFileInfo(file);

    peer.set('peer.state', 'connected');
    peer.set('state', 'awaiting_response');

    webrtc.sendFileInfo(connection, info);
    console.log('Sending a file info...', info);
  },

  _onPeerDCOutgoingConnectionError(event, data) {
    const { connection, error } = data;
    const peers = this.model;
    const peer = peers.findBy('peer.id', connection.peer);

    switch (error.type) {
      case 'failed':
        peer.setProperties({
          'peer.connection': null,
          'peer.state': 'disconnected',
          state: 'error',
          errorCode: 'connection-failed',
        });
        break;
      default:
        break;
    }
  },

  _onPeerP2PDisconnected(event, data) {
    const { connection } = data;
    const peers = this.model;
    const peer = peers.findBy('peer.id', connection.peer);

    if (peer) {
      peer.set('peer.connection', null);
      peer.set('peer.state', 'disconnected');
    }
  },

  _onPeerP2PFileInfo(event, data) {
    console.log('Peer:\t Received file info', data);

    const { connection, info } = data;
    const peers = this.model;
    const peer = peers.findBy('peer.id', connection.peer);

    peer.set('transfer.info', info);
    peer.set('state', 'received_file_info');
  },

  _onPeerP2PFileResponse(event, data) {
    console.log('Peer:\t Received file response', data);

    const { connection, response } = data;
    const peers = this.model;
    const peer = peers.findBy('peer.id', connection.peer);
    const { webrtc } = this;

    if (response) {
      const file = peer.get('transfer.file');

      connection.on('sending_progress', (progress) => {
        peer.set('transfer.sendingProgress', progress);
      });
      webrtc.sendFile(connection, file);
      peer.set('state', 'receiving_file_data');
    } else {
      peer.set('state', 'declined_file_transfer');
    }
  },

  _onPeerP2PFileCanceled(event, data) {
    const { connection } = data;
    const peers = this.model;
    const peer = peers.findBy('peer.id', connection.peer);

    connection.close();
    peer.set('transfer.receivingProgress', 0);
    peer.set('transfer.info', null);
    peer.set('state', 'idle');
  },

  _onPeerP2PFileReceived(event, data) {
    console.log('Peer:\t Received file', data);

    const { connection } = data;
    const peers = this.model;
    const peer = peers.findBy('peer.id', connection.peer);

    connection.close();
    peer.set('transfer.receivingProgress', 0);
    peer.set('transfer.info', null);
    peer.set('state', 'idle');
    peer.trigger('didReceiveFile');
  },

  _onPeerP2PFileSent(event, data) {
    console.log('Peer:\t Sent file', data);

    const { connection } = data;
    const peers = this.model;
    const peer = peers.findBy('peer.id', connection.peer);

    peer.set('transfer.sendingProgress', 0);
    peer.set('transfer.file', null);
    peer.set('state', 'idle');
    peer.trigger('didSendFile');
  },
});


================================================
FILE: app/helpers/is-equal.js
================================================
import { helper as buildHelper } from '@ember/component/helper';

export default buildHelper(([leftSide, rightSide]) => leftSide === rightSide);


================================================
FILE: app/index.html
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <title>ShareDrop</title>
    <meta
      name="description"
      content="ShareDrop is a peer-to-peer file sharing app powered by HTML5 WebRTC."
    />
    <meta name="viewport" content="width=device-width, initial-scale=1" />

    <meta name="mobile-web-app-capable" content="yes" />
    <link
      rel="shortcut icon"
      sizes="196x196"
      href="{{rootURL}}assets/images/sharedrop-icon.png"
    />
    <link
    rel="apple-touch-icon"
    sizes="196x196"
    href="{{rootURL}}assets/images/sharedrop-icon.png"
    />
    
    {{content-for "head"}}

    <link integrity="" rel="stylesheet" href="{{rootURL}}assets/vendor.css" />
    <link
      integrity=""
      rel="stylesheet"
      href="{{rootURL}}assets/sharedrop.css"
    />

    {{content-for "head-footer"}}
  </head>

  <body>
    <div class="preloader"><span>Loading...</span></div>

    {{content-for "body"}}

    <script src="https://cdn.firebase.com/js/client/2.2.9/firebase.js"></script>
    <script src="{{rootURL}}assets/vendor.js"></script>
    <script src="{{rootURL}}assets/sharedrop.js"></script>

    {{content-for "body-footer"}}
  </body>
</html>


================================================
FILE: app/initializers/prerequisites.js
================================================
/* jshint -W030 */
import $ from 'jquery';
import { Promise } from 'rsvp';
import config from 'sharedrop/config/environment';

import FileSystem from '../services/file';
import Analytics from '../services/analytics';

export function initialize(application) {
  function checkWebRTCSupport() {
    return new Promise((resolve, reject) => {
      // window.util is a part of PeerJS library
      if (window.util.supports.sctp) {
        resolve();
      } else {
        // eslint-disable-next-line prefer-promise-reject-errors
        reject('browser-unsupported');
      }
    });
  }

  function clearFileSystem() {
    return new Promise((resolve, reject) => {
      // TODO: change File into a service and require it here
      FileSystem.removeAll()
        .then(() => {
          resolve();
        })
        .catch(() => {
          // eslint-disable-next-line prefer-promise-reject-errors
          reject('filesystem-unavailable');
        });
    });
  }

  function authenticateToFirebase() {
    return new Promise((resolve, reject) => {
      const xhr = $.getJSON('/auth');
      xhr.then((data) => {
        const ref = new window.Firebase(config.FIREBASE_URL);
        // eslint-disable-next-line no-param-reassign
        application.ref = ref;
        // eslint-disable-next-line no-param-reassign
        application.userId = data.id;
        // eslint-disable-next-line no-param-reassign
        application.publicIp = data.public_ip;

        ref.authWithCustomToken(data.token, (error) => {
          if (error) {
            reject(error);
          } else {
            resolve();
          }
        });
      });
    });
  }

  // TODO: move it to a separate initializer
  function trackSizeOfReceivedFiles() {
    $.subscribe('file_received.p2p', (event, data) => {
      Analytics.trackEvent('received', {
        event_category: 'file',
        event_label: 'size',
        value: Math.round(data.info.size / 1000),
      });
    });
  }

  application.deferReadiness();

  checkWebRTCSupport()
    .then(clearFileSystem)
    .catch((error) => {
      // eslint-disable-next-line no-param-reassign
      application.error = error;
    })
    .then(authenticateToFirebase)
    .then(trackSizeOfReceivedFiles)
    .then(() => {
      application.advanceReadiness();
    });
}

export default {
  name: 'prerequisites',
  initialize,
};


================================================
FILE: app/models/peer.js
================================================
import EmberObject, { observer } from '@ember/object';
import Evented, { on } from '@ember/object/evented';

export default EmberObject.extend(Evented, {
  uuid: null,
  label: null,
  avatarUrl: null,
  public_ip: null,
  peer: null,
  transfer: null,

  init(...args) {
    this._super(args);

    const initialPeerState = EmberObject.create({
      id: null,
      connection: null,
      // State of data channel connection. Possible states:
      // - disconnected
      // - connecting
      // - connected
      state: 'disconnected',
    });
    const initialTransferState = EmberObject.create({
      file: null,
      info: null,
      sendingProgress: 0,
      receivingProgress: 0,
    });

    this.set('peer', initialPeerState);
    this.set('transfer', initialTransferState);
  },

  // Used to display popovers. Possible states:
  // - idle
  // - has_selected_file
  // - establishing_connection
  // - awaiting_response
  // - received_file_info
  // - declined_file_transfer
  // - receiving_file_data
  // - sending_file_data
  // - error
  state: 'idle',

  // Used to display error messages in popovers. Possible codes:
  // - multiple_files
  errorCode: null,

  stateChanged: on(
    'init',
    observer('state', function () {
      console.log('Peer:\t State has changed: ', this.state);

      // Automatically clear error code if transitioning to a non-error state
      if (this.state !== 'error') {
        this.set('errorCode', null);
      }
    }),
  ),
});


================================================
FILE: app/models/user.js
================================================
import Peer from './peer';

const User = Peer.extend({
  serialize() {
    const data = {
      uuid: this.uuid,
      public_ip: this.public_ip,
      label: this.label,
      avatarUrl: this.avatarUrl,
      peer: {
        id: this.get('peer.id'),
      },
    };

    return data;
  },
});

export default User;


================================================
FILE: app/router.js
================================================
import EmberRouter from '@ember/routing/router';
import config from 'sharedrop/config/environment';

export default class Router extends EmberRouter {
  location = config.locationType;

  rootURL = config.rootURL;
}

// eslint-disable-next-line array-callback-return
Router.map(function () {
  this.route('room', {
    path: '/rooms/:room_id',
  });
});


================================================
FILE: app/routes/application.js
================================================
import Route from '@ember/routing/route';

export default Route.extend({
  setupController(controller) {
    controller.set('currentRoute', this);
  },

  actions: {
    openModal(modalName) {
      return this.render(modalName, {
        outlet: 'modal',
        into: 'application',
      });
    },

    closeModal() {
      return this.disconnectOutlet({
        outlet: 'modal',
        parentView: 'application',
      });
    },
  },
});


================================================
FILE: app/routes/error.js
================================================
import Route from '@ember/routing/route';

export default Route.extend({
  renderTemplate(controller, error) {
    const errors = ['browser-unsupported', 'filesystem-unavailable'];
    const name = `errors/${error.message}`;

    if (errors.indexOf(error.message) !== -1) {
      this.render(name);
    }
  },
});


================================================
FILE: app/routes/index.js
================================================
import Route from '@ember/routing/route';
import $ from 'jquery';

import Room from '../services/room';

export default Route.extend({
  beforeModel() {
    const { error } = window.Sharedrop;

    if (error) {
      throw new Error(error);
    }
  },

  model() {
    // Get room name from the server
    return $.getJSON('/room').then((data) => data.name);
  },

  setupController(ctrl, model) {
    ctrl.set('model', []);
    ctrl.set('hasCustomRoomName', false);

    // Handle room events
    $.subscribe('connected.room', ctrl._onRoomConnected.bind(ctrl));
    $.subscribe('disconnected.room', ctrl._onRoomDisconnected.bind(ctrl));
    $.subscribe('user_added.room', ctrl._onRoomUserAdded.bind(ctrl));
    $.subscribe('user_changed.room', ctrl._onRoomUserChanged.bind(ctrl));
    $.subscribe('user_removed.room', ctrl._onRoomUserRemoved.bind(ctrl));

    // Handle peer events
    $.subscribe(
      'incoming_peer_connection.p2p',
      ctrl._onPeerP2PIncomingConnection.bind(ctrl),
    );
    $.subscribe(
      'incoming_dc_connection.p2p',
      ctrl._onPeerDCIncomingConnection.bind(ctrl),
    );
    $.subscribe(
      'incoming_dc_connection_error.p2p',
      ctrl._onPeerDCIncomingConnectionError.bind(ctrl),
    );
    $.subscribe(
      'outgoing_peer_connection.p2p',
      ctrl._onPeerP2POutgoingConnection.bind(ctrl),
    );
    $.subscribe(
      'outgoing_dc_connection.p2p',
      ctrl._onPeerDCOutgoingConnection.bind(ctrl),
    );
    $.subscribe(
      'outgoing_dc_connection_error.p2p',
      ctrl._onPeerDCOutgoingConnectionError.bind(ctrl),
    );
    $.subscribe('disconnected.p2p', ctrl._onPeerP2PDisconnected.bind(ctrl));
    $.subscribe('info.p2p', ctrl._onPeerP2PFileInfo.bind(ctrl));
    $.subscribe('response.p2p', ctrl._onPeerP2PFileResponse.bind(ctrl));
    $.subscribe('file_canceled.p2p', ctrl._onPeerP2PFileCanceled.bind(ctrl));
    $.subscribe('file_received.p2p', ctrl._onPeerP2PFileReceived.bind(ctrl));
    $.subscribe('file_sent.p2p', ctrl._onPeerP2PFileSent.bind(ctrl));

    // Join the room
    const room = new Room(model, window.Sharedrop.ref);
    room.join(ctrl.get('you').serialize());
    ctrl.set('room', room);
  },

  renderTemplate() {
    this.render();

    this.render('about_you', {
      into: 'application',
      outlet: 'about_you',
    });

    const key = 'show-instructions-for-app';
    if (!localStorage.getItem(key)) {
      this.send('openModal', 'about_app');
      localStorage.setItem(key, 'yup');
    }
  },

  actions: {
    willTransition() {
      $.unsubscribe('.room');
      $.unsubscribe('.p2p');

      // eslint-disable-next-line ember/no-controller-access-in-routes
      const controller = this.controllerFor('index');
      controller.get('room').leave();

      return true;
    },
  },
});


================================================
FILE: app/routes/room.js
================================================
import IndexRoute from './index';

export default IndexRoute.extend({
  controllerName: 'index',

  model(params) {
    // Get room name from params
    return params.room_id;
  },

  afterModel(model, transition) {
    transition.then((route) => {
      route
        .controllerFor('application')
        .set('currentUrl', window.location.href);
    });
  },

  setupController(ctrl, model) {
    // Call this method on "index" controller
    this._super(ctrl, model);

    ctrl.set('hasCustomRoomName', true);
  },

  renderTemplate(ctrl) {
    this.render('index');

    this.render('about_you', {
      into: 'application',
      outlet: 'about_you',
    });

    const room = ctrl.get('room').name;
    const key = `show-instructions-for-room-${room}`;

    if (sessionStorage.getItem(key)) {
      this.send('openModal', 'about_room');
      sessionStorage.removeItem(key);
    }
  },
});


================================================
FILE: app/services/analytics.js
================================================
export default {
  trackEvent(name, parameters) {
    if (window.gtag && typeof window.gtag === 'function') {
      window.gtag('event', name, parameters);
    }
  },
};


================================================
FILE: app/services/avatar.js
================================================
import Service from '@ember/service';
import sample from 'lodash/sample';
import startCase from 'lodash/startCase';

const AVATARS = [
  {
    name: 'Piglet',
    id: '23',
  },
  {
    name: 'Cat',
    id: '36',
  },
  {
    name: 'Fish',
    id: '37',
  },
  {
    name: 'Fox',
    id: '38',
  },
  {
    name: 'Chicken',
    id: '46',
  },
  {
    name: 'Goat',
    id: '50',
  },
  {
    name: 'Ram',
    id: '51',
  },
  {
    name: 'Sheep',
    id: '52',
  },
  {
    name: 'Bison',
    id: '59',
  },
  {
    name: 'Dog',
    id: '61',
  },
  {
    name: 'Walrus',
    id: '62',
  },
  {
    name: 'Dog',
    id: '63',
  },
  {
    name: 'Monkey',
    id: '64',
  },
  {
    name: 'Bear',
    id: '65',
  },
  {
    name: 'Lion',
    id: '66',
  },
  {
    name: 'Zebra',
    id: '67',
  },
  {
    name: 'Giraffe',
    id: '68',
  },
  {
    name: 'Bear',
    id: '71',
  },
  {
    name: 'Wolf',
    id: '74',
  },
  {
    name: 'Rhino',
    id: '86',
  },
  {
    name: 'Bat',
    id: '87',
  },
  {
    name: 'Cat',
    id: '95',
  },
  {
    name: 'Penguin',
    id: '102',
  },
  {
    name: 'Rhino',
    id: '109',
  },
  {
    name: 'Koala',
    id: '112',
  },
];

const PREFIXES = [
  'adventurous',
  'affable',
  'ambitious',
  'amiable ',
  'amusing',
  'brave',
  'bright',
  'charming',
  'compassionate',
  'convivial',
  'courageous',
  'creative',
  'diligent',
  'easygoing',
  'emotional',
  'energetic',
  'enthusiastic',
  'exuberant',
  'fearless',
  'friendly',
  'funny',
  'generous',
  'gentle',
  'good',
  'helpful',
  'honest',
  'humorous',
  'imaginative',
  'independent',
  'intelligent',
  'intuitive',
  'inventive',
  'kind',
  'loving',
  'loyal',
  'modest',
  'neat',
  'nice',
  'optimistic',
  'passionate',
  'patient',
  'persistent',
  'polite',
  'practical',
  'rational',
  'reliable',
  'reserved',
  'resourceful',
  'romantic',
  'sensible',
  'sensitive',
  'sincere',
  'sympathetic',
  'thoughtful',
  'tough',
  'understanding',
  'versatile',
  'warmhearted',
];

const Avatar = Service.extend({
  get() {
    const avatar = sample(AVATARS);
    const prefix = sample(PREFIXES);

    return {
      url: `/assets/images/avatars/${avatar.id}.svg`,
      label: startCase(`${prefix} ${avatar.name}`),
    };
  },
});

export default Avatar;


================================================
FILE: app/services/file.js
================================================
import { Promise } from 'rsvp';

const File = function (options) {
  const self = this;

  this.name = options.name;
  this.localName = `${new Date().getTime()}-${this.name}`;
  this.size = options.size;
  this.type = options.type;

  this._reset();

  return new Promise((resolve, reject) => {
    const requestFileSystem =
      window.requestFileSystem || window.webkitRequestFileSystem;

    requestFileSystem(
      window.TEMPORARY,
      options.size,
      (filesystem) => {
        self.filesystem = filesystem;
        resolve(self);
      },
      (error) => {
        self.errorHandler(error);
        reject(error);
      },
    );
  });
};

File.removeAll = function () {
  return new Promise((resolve, reject) => {
    const filer = new window.Filer();

    filer.init(
      { persistent: false },
      () => {
        filer.ls('/', (entries) => {
          function rm(entry) {
            if (entry) {
              filer.rm(entry, () => {
                rm(entries.pop());
              });
            } else {
              resolve();
            }
          }

          rm(entries.pop());
        });
      },
      (error) => {
        console.log(error);
        reject(error);
      },
    );
  });
};

File.prototype.append = function (data) {
  const self = this;
  const options = {
    create: this.create,
  };

  return new Promise((resolve, reject) => {
    self.filesystem.root.getFile(
      self.localName,
      options,
      (fileEntry) => {
        if (self.create) {
          self.create = false;
        }

        self.fileEntry = fileEntry;

        fileEntry.createWriter(
          (writer) => {
            const blob = new Blob(data, { type: self.type });

            // console.log('File: Appending ' + blob.size + ' bytes at ' + self.seek);

            // eslint-disable-next-line no-param-reassign
            writer.onwriteend = function () {
              self.seek += blob.size;
              resolve(fileEntry);
            };

            // eslint-disable-next-line no-param-reassign
            writer.onerror = function (error) {
              self.errorHandler(error);
              reject(error);
            };

            writer.seek(self.seek);
            writer.write(blob);
          },
          (error) => {
            self.errorHandler(error);
            reject(error);
          },
        );
      },
      (error) => {
        self.errorHandler(error);
        reject(error);
      },
    );
  });
};

File.prototype.save = function () {
  const self = this;

  console.log('File: Saving file: ', this.fileEntry);

  const a = document.createElement('a');
  a.download = this.name;

  function finish(link) {
    document.body.appendChild(a);
    link.addEventListener('click', () => {
      // Remove file entry from filesystem.
      setTimeout(() => {
        self.remove().then(self._reset.bind(self));
      }, 100); // Hack, but otherwise browser doesn't save the file at all.

      link.parentNode.removeChild(a);
    });
    link.click();
  }

  if (this._isWebKit()) {
    a.href = this.fileEntry.toURL();
    finish(a);
  } else {
    this.fileEntry.file((file) => {
      const URL = window.URL || window.webkitURL;
      a.href = URL.createObjectURL(file);
      finish(a);
    });
  }
};

File.prototype.errorHandler = function (error) {
  console.error('File error: ', error);
};

File.prototype.remove = function () {
  const self = this;

  return new Promise((resolve, reject) => {
    self.filesystem.root.getFile(
      self.localName,
      { create: false },
      (fileEntry) => {
        fileEntry.remove(
          () => {
            console.debug(`File: Removed file: ${self.localName}`);
            resolve(fileEntry);
          },
          (error) => {
            self.errorHandler(error);
            reject(error);
          },
        );
      },
      (error) => {
        self.errorHandler(error);
        reject(error);
      },
    );
  });
};

File.prototype._reset = function () {
  this.create = true;
  this.filesystem = null;
  this.fileEntry = null;
  this.seek = 0;
};

File.prototype._isWebKit = function () {
  return !!window.webkitRequestFileSystem;
};

export default File;


================================================
FILE: app/services/room.js
================================================
import $ from 'jquery';

// TODO: use Ember.Object.extend()
const Room = function (name, firebaseRef) {
  this._ref = firebaseRef;
  this.name = name;
};

Room.prototype.join = function (user) {
  const self = this;

  // Setup Firebase refs
  self._connectionRef = self._ref.child('.info/connected');
  self._roomRef = self._ref.child(`rooms/${this.name}`);
  self._usersRef = self._roomRef.child('users');
  self._userRef = self._usersRef.child(user.uuid);

  console.log('Room:\t Connecting to: ', this.name);

  self._connectionRef.on('value', (connectionSnapshot) => {
    // Once connected (or reconnected) to Firebase
    if (connectionSnapshot.val() === true) {
      console.log('Firebase: (Re)Connected');

      // Remove yourself from the room when disconnected
      self._userRef.onDisconnect().remove();

      // Join the room
      self._userRef.set(user, (error) => {
        if (error) {
          console.warn('Firebase: Adding user to the room failed: ', error);
        } else {
          console.log('Firebase: User added to the room');
          // Create a copy of user data,
          // so that deleting properties won't affect the original variable
          $.publish('connected.room', $.extend(true, {}, user));
        }
      });

      self._usersRef.on('child_added', (userAddedSnapshot) => {
        const addedUser = userAddedSnapshot.val();

        console.log('Room:\t user_added: ', addedUser);
        $.publish('user_added.room', addedUser);
      });

      self._usersRef.on(
        'child_removed',
        (userRemovedSnapshot) => {
          const removedUser = userRemovedSnapshot.val();

          console.log('Room:\t user_removed: ', removedUser);
          $.publish('user_removed.room', removedUser);
        },
        () => {
          // Handle case when the whole room is removed from Firebase
          $.publish('disconnected.room');
        },
      );

      self._usersRef.on('child_changed', (userChangedSnapshot) => {
        const changedUser = userChangedSnapshot.val();

        console.log('Room:\t user_changed: ', changedUser);
        $.publish('user_changed.room', changedUser);
      });
    } else {
      console.log('Firebase: Disconnected');

      $.publish('disconnected.room');
      self._usersRef.off();
    }
  });

  return this;
};

Room.prototype.update = function (attrs) {
  this._userRef.update(attrs);
};

Room.prototype.leave = function () {
  this._userRef.remove();
  this._usersRef.off();
};

export default Room;


================================================
FILE: app/services/web-rtc.js
================================================
// TODO:
// - provide TURN server config once it's possible to create rooms with custom names
// - use Ember.Object.extend()

import $ from 'jquery';
import File from './file';

const WebRTC = function (id, options) {
  const defaults = {
    config: { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] },
    debug: 3,
  };

  this.conn = new window.Peer(id, $.extend(defaults, options));

  this.files = {
    outgoing: {},
    incoming: {},
  };

  // Listen for incoming connections
  this.conn.on('connection', (connection) => {
    $.publish('incoming_peer_connection.p2p', { connection });

    connection.on('open', () => {
      console.log('Peer:\t Data channel connection opened: ', connection);
      $.publish('incoming_dc_connection.p2p', { connection });
    });

    connection.on('error', (error) => {
      console.log('Peer:\t Data channel connection error', error);
      $.publish('incoming_dc_connection_error.p2p', {
        connection,
        error,
      });
    });

    this._onConnection(connection);
  });

  this.conn.on('close', () => {
    console.log('Peer:\t Connection to server closed.');
  });

  this.conn.on('error', (error) => {
    console.log('Peer:\t Error while connecting to server: ', error);
  });
};

WebRTC.CHUNK_MTU = 16000;
WebRTC.CHUNKS_PER_ACK = 64;

WebRTC.prototype.connect = function (id) {
  const connection = this.conn.connect(id, {
    label: 'file',
    reliable: true,
    serialization: 'none', // we handle serialization ourselves
  });

  connection.on('open', () => {
    console.log('Peer:\t Data channel connection opened: ', connection);
    $.publish('outgoing_dc_connection.p2p', { connection });
  });

  connection.on('error', (error) => {
    console.log('Peer:\t Data channel connection error', error);
    $.publish('outgoing_dc_connection_error.p2p', {
      connection,
      error,
    });
  });

  $.publish('outgoing_peer_connection.p2p', { connection });
  this._onConnection(connection);
};

WebRTC.prototype._onConnection = function (connection) {
  const self = this;

  console.log('Peer:\t Opening data channel connection...', connection);

  connection.on('data', (data) => {
    // Lame type check
    if (data.byteLength !== undefined) {
      // ArrayBuffer
      self._onBinaryData(data, connection);
    } else {
      // JSON string
      self._onJSONData(JSON.parse(data), connection);
    }
  });

  connection.on('close', () => {
    $.publish('disconnected.p2p', { connection });
    console.log('Peer:\t P2P connection closed: ', connection);
  });
};

WebRTC.prototype._onBinaryData = function (data, connection) {
  const self = this;
  const incoming = this.files.incoming[connection.peer];
  const { info, file, block, receivedChunkNum } = incoming;
  const chunksPerAck = WebRTC.CHUNKS_PER_ACK;

  // TODO move it after requesting a new block to speed things up
  connection.emit(
    'receiving_progress',
    (receivedChunkNum + 1) / info.chunksTotal,
  );
  // console.log('Got chunk no ' + (receivedChunkNum + 1) + ' out of ' + info.chunksTotal);

  block.push(data);

  incoming.receivedChunkNum = receivedChunkNum + 1;
  const nextChunkNum = incoming.receivedChunkNum;
  const lastChunkInFile = receivedChunkNum === info.chunksTotal - 1;
  const lastChunkInBlock =
    receivedChunkNum > 0 && (receivedChunkNum + 1) % chunksPerAck === 0;

  if (lastChunkInFile || lastChunkInBlock) {
    file.append(block).then(() => {
      if (lastChunkInFile) {
        file.save();

        $.publish('file_received.p2p', {
          blob: file,
          info,
          connection,
        });
      } else {
        // console.log('Requesting block starting at: ' + (nextChunkNum));
        incoming.block = [];
        self._requestFileBlock(connection, nextChunkNum);
      }
    });
  }
};

WebRTC.prototype._onJSONData = function (data, connection) {
  switch (data.type) {
    case 'info': {
      const info = data.payload;

      $.publish('info.p2p', {
        connection,
        info,
      });

      // Store incoming file info for later
      this.files.incoming[connection.peer] = {
        info,
        file: null,
        block: [],
        receivedChunkNum: 0,
      };

      console.log('Peer:\t File info: ', data);
      break;
    }
    case 'cancel': {
      $.publish('file_canceled.p2p', {
        connection,
      });

      console.log('Peer:\t Sender canceled file transfer');
      break;
    }
    case 'response': {
      const response = data.payload;

      // If recipient rejected the file, delete stored file
      if (!response) {
        delete this.files.outgoing[connection.peer];
      }

      $.publish('response.p2p', {
        connection,
        response,
      });

      console.log('Peer:\t File response: ', data);
      break;
    }
    case 'block_request': {
      const { file } = this.files.outgoing[connection.peer];

      // console.log('Peer:\t Block request: ', data.payload);

      this._sendBlock(connection, file, data.payload);
      break;
    }
    default:
      console.log('Peer:\t Unknown message: ', data);
  }
};

WebRTC.prototype.getFileInfo = function (file) {
  return {
    lastModifiedDate: file.lastModifiedDate,
    name: file.name,
    size: file.size,
    type: file.type,
    chunksTotal: Math.ceil(file.size / WebRTC.CHUNK_MTU),
  };
};

WebRTC.prototype.sendFileInfo = function (connection, info) {
  const message = {
    type: 'info',
    payload: info,
  };

  connection.send(JSON.stringify(message));
};

WebRTC.prototype.sendCancelRequest = function (connection) {
  const message = {
    type: 'cancel',
  };

  connection.send(JSON.stringify(message));
};

WebRTC.prototype.sendFileResponse = function (connection, response) {
  const message = {
    type: 'response',
    payload: response,
  };

  if (response) {
    // If recipient accepted the file, request required space to store the file on HTML5 filesystem
    const incoming = this.files.incoming[connection.peer];
    const { info } = incoming;

    new File({ name: info.name, size: info.size, type: info.type }).then(
      (file) => {
        incoming.file = file;
        connection.send(JSON.stringify(message));
      },
    );
  } else {
    // Otherwise, delete stored file info
    delete this.files.incoming[connection.peer];
    connection.send(JSON.stringify(message));
  }
};

WebRTC.prototype.sendFile = function (connection, file) {
  // Save the file for later
  this.files.outgoing[connection.peer] = {
    file,
    info: this.getFileInfo(file),
  };

  // Send the first block. Next ones will be requested by recipient.
  this._sendBlock(connection, file, 0);
};

WebRTC.prototype._requestFileBlock = function (connection, chunkNum) {
  const message = {
    type: 'block_request',
    payload: chunkNum,
  };
  connection.send(JSON.stringify(message));
};

WebRTC.prototype._sendBlock = function (connection, file, beginChunkNum) {
  const { info } = this.files.outgoing[connection.peer];
  const chunkSize = WebRTC.CHUNK_MTU;
  const chunksPerAck = WebRTC.CHUNKS_PER_ACK;
  const remainingChunks = info.chunksTotal - beginChunkNum;
  const chunksToSend = Math.min(remainingChunks, chunksPerAck);
  const endChunkNum = beginChunkNum + chunksToSend - 1;
  const blockBegin = beginChunkNum * chunkSize;
  const blockEnd = endChunkNum * chunkSize + chunkSize;
  const reader = new FileReader();
  let chunkNum;

  // Read the whole block from file
  const blockBlob = file.slice(blockBegin, blockEnd);

  // console.log('Sending block: start chunk: ' + beginChunkNum + ' end chunk: ' + endChunkNum);
  // console.log('Sending block: start byte : ' + begin + ' end byte : ' + end);

  reader.onload = function (event) {
    if (reader.readyState === FileReader.DONE) {
      const blockBuffer = event.target.result;

      for (
        chunkNum = beginChunkNum;
        chunkNum < endChunkNum + 1;
        chunkNum += 1
      ) {
        // Send each chunk (begin index is inclusive, end index is exclusive)
        const bufferBegin = (chunkNum % chunksPerAck) * chunkSize;
        const bufferEnd = Math.min(
          bufferBegin + chunkSize,
          blockBuffer.byteLength,
        );
        const chunkBuffer = blockBuffer.slice(bufferBegin, bufferEnd);

        connection.send(chunkBuffer);

        // console.log('Sent chunk: start byte: ' + begin + ' end byte: ' + end + ' length: ' + chunkBuffer.byteLength);
        // console.log('Sent chunk no ' + (chunkNum + 1) + ' out of ' + info.chunksTotal);

        connection.emit('sending_progress', (chunkNum + 1) / info.chunksTotal);
      }

      if (endChunkNum === info.chunksTotal - 1) {
        $.publish('file_sent.p2p', { connection });
      }
    }
  };

  reader.readAsArrayBuffer(blockBlob);
};

export default WebRTC;


================================================
FILE: app/styles/app.sass
================================================
@import base/reset

@import base/variables
@import base/mixins
@import base/element_defaults
@import base/glyphicons_filetypes

@import modules/modules
@import modules/modal
@import modules/users
@import modules/popover

@import layout/header
@import layout/content
@import layout/footer
@import layout/media


================================================
FILE: app/styles/base/_element_defaults.sass
================================================
*, *::before, *::after
  box-sizing: border-box

html
  height: 100%
  font-family: $font-family
  font-size: 10px

body
  height: 100%

a
  text-decoration: none

b, strong
  font-weight: bold

input, select
  font-family: inherit
  padding: 1rem
  border: 1px solid #ccc
  width: 100%
  border-radius: .3rem
  font-size: 1.4rem

.invisible
  height: 0
  width: 0
  opacity: 0


================================================
FILE: app/styles/base/_glyphicons_filetypes.sass
================================================
/* GLYPHICONS FILETYPES 1.8 */

@font-face
  font-family: "Glyphicons Filetypes"
  src: url(/assets/fonts/glyphicons/glyphicons-filetypes-regular.woff) format("woff"), url(/assets/fonts/glyphicons/glyphicons-filetypes-regular.ttf) format("truetype"), url(/assets/fonts/glyphicons/glyphicons-filetypes-regular.svg#glyphicons_filetypesregular) format("svg")

$icon-prefix: glyphicon

[class*="#{$icon-prefix}-"]
  display: inline-block
  vertical-align: middle
  font:
    family: "Glyphicons Filetypes"
    style: normal
    weight: normal
    size: 1em

.#{$icon-prefix}-spin
  animation: #{$icon-prefix}-spin 2s infinite linear

@-moz-keyframes #{$icon-prefix}-spin
  0%
    -moz-transform: rotate(0deg)
  100%
    -moz-transform: rotate(359deg)
@-webkit-keyframes #{$icon-prefix}-spin
  0%
    -webkit-transform: rotate(0deg)
  100%
    -webkit-transform: rotate(359deg)
@-o-keyframes #{$icon-prefix}-spin
  0%
    -o-transform: rotate(0deg)
  100%
    -o-transform: rotate(359deg)
@-ms-keyframes #{$icon-prefix}-spin
  0%
    -ms-transform: rotate(0deg)
  100%
    -ms-transform: rotate(359deg)
@keyframes #{$icon-prefix}-spin
  0%
    transform: rotate(0deg)
  100%
    transform: rotate(359deg)

[class*="#{$icon-prefix}-"]:before
  content: "\E120"

.#{$icon-prefix}-txt:before
  content: "\E001"
.#{$icon-prefix}-doc:before
  content: "\E002"
.#{$icon-prefix}-rtf:before
  content: "\E003"
.#{$icon-prefix}-log:before
  content: "\E004"
.#{$icon-prefix}-tex:before
  content: "\E005"
.#{$icon-prefix}-msg:before
  content: "\E006"
.#{$icon-prefix}-text:before
  content: "\E007"
.#{$icon-prefix}-wpd:before
  content: "\E008"
.#{$icon-prefix}-wps:before
  content: "\E009"
.#{$icon-prefix}-docx:before
  content: "\E010"
.#{$icon-prefix}-page:before
  content: "\E011"
.#{$icon-prefix}-csv:before
  content: "\E012"
.#{$icon-prefix}-dat:before
  content: "\E013"
.#{$icon-prefix}-tar:before
  content: "\E014"
.#{$icon-prefix}-xml:before
  content: "\E015"
.#{$icon-prefix}-vcf:before
  content: "\E016"
.#{$icon-prefix}-pps:before
  content: "\E017"
.#{$icon-prefix}-key:before
  content: "\E018"
.#{$icon-prefix}-ppt:before
  content: "\E019"
.#{$icon-prefix}-pptx:before
  content: "\E020"
.#{$icon-prefix}-sdf:before
  content: "\E021"
.#{$icon-prefix}-gbr:before
  content: "\E022"
.#{$icon-prefix}-ged:before
  content: "\E023"
.#{$icon-prefix}-mp3:before
  content: "\E024"
.#{$icon-prefix}-m4a:before
  content: "\E025"
.#{$icon-prefix}-waw:before
  content: "\E026"
.#{$icon-prefix}-wma:before
  content: "\E027"
.#{$icon-prefix}-mpa:before
  content: "\E028"
.#{$icon-prefix}-iff:before
  content: "\E029"
.#{$icon-prefix}-aif:before
  content: "\E030"
.#{$icon-prefix}-ra:before
  content: "\E031"
.#{$icon-prefix}-mid:before
  content: "\E032"
.#{$icon-prefix}-m3v:before
  content: "\E033"
.#{$icon-prefix}-e_3gp:before
  content: "\E034"
.#{$icon-prefix}-shf:before
  content: "\E035"
.#{$icon-prefix}-avi:before
  content: "\E036"
.#{$icon-prefix}-asx:before
  content: "\E037"
.#{$icon-prefix}-mp4:before
  content: "\E038"
.#{$icon-prefix}-e_3g2:before
  content: "\E039"
.#{$icon-prefix}-mpg:before
  content: "\E040"
.#{$icon-prefix}-asf:before
  content: "\E041"
.#{$icon-prefix}-vob:before
  content: "\E042"
.#{$icon-prefix}-wmv:before
  content: "\E043"
.#{$icon-prefix}-mov:before
  content: "\E044"
.#{$icon-prefix}-srt:before
  content: "\E045"
.#{$icon-prefix}-m4v:before
  content: "\E046"
.#{$icon-prefix}-flv:before
  content: "\E047"
.#{$icon-prefix}-rm:before
  content: "\E048"
.#{$icon-prefix}-png:before
  content: "\E049"
.#{$icon-prefix}-psd:before
  content: "\E050"
.#{$icon-prefix}-psp:before
  content: "\E051"
.#{$icon-prefix}-jpg:before, .#{$icon-prefix}-jpeg:before
  content: "\E052"
.#{$icon-prefix}-tif:before
  content: "\E053"
.#{$icon-prefix}-tiff:before
  content: "\E054"
.#{$icon-prefix}-gif:before
  content: "\E055"
.#{$icon-prefix}-bmp:before
  content: "\E056"
.#{$icon-prefix}-tga:before
  content: "\E057"
.#{$icon-prefix}-thm:before
  content: "\E058"
.#{$icon-prefix}-yuv:before
  content: "\E059"
.#{$icon-prefix}-dds:before
  content: "\E060"
.#{$icon-prefix}-ai:before
  content: "\E061"
.#{$icon-prefix}-eps:before
  content: "\E062"
.#{$icon-prefix}-ps:before
  content: "\E063"
.#{$icon-prefix}-svg:before
  content: "\E064"
.#{$icon-prefix}-pdf:before
  content: "\E065"
.#{$icon-prefix}-pct:before
  content: "\E066"
.#{$icon-prefix}-indd:before
  content: "\E067"
.#{$icon-prefix}-xlr:before
  content: "\E068"
.#{$icon-prefix}-xls:before
  content: "\E069"
.#{$icon-prefix}-xlsx:before
  content: "\E070"
.#{$icon-prefix}-db:before
  content: "\E071"
.#{$icon-prefix}-dbf:before
  content: "\E072"
.#{$icon-prefix}-mdb:before
  content: "\E073"
.#{$icon-prefix}-pdb:before
  content: "\E074"
.#{$icon-prefix}-sql:before
  content: "\E075"
.#{$icon-prefix}-aacd:before
  content: "\E076"
.#{$icon-prefix}-app:before
  content: "\E077"
.#{$icon-prefix}-exe:before
  content: "\E078"
.#{$icon-prefix}-com:before
  content: "\E079"
.#{$icon-prefix}-bat:before
  content: "\E080"
.#{$icon-prefix}-apk:before
  content: "\E081"
.#{$icon-prefix}-jar:before
  content: "\E082"
.#{$icon-prefix}-hsf:before
  content: "\E083"
.#{$icon-prefix}-pif:before
  content: "\E084"
.#{$icon-prefix}-vb:before
  content: "\E085"
.#{$icon-prefix}-cgi:before
  content: "\E086"
.#{$icon-prefix}-css:before
  content: "\E087"
.#{$icon-prefix}-js:before
  content: "\E088"
.#{$icon-prefix}-php:before
  content: "\E089"
.#{$icon-prefix}-xhtml:before
  content: "\E090"
.#{$icon-prefix}-htm:before
  content: "\E091"
.#{$icon-prefix}-html:before
  content: "\E092"
.#{$icon-prefix}-asp:before
  content: "\E093"
.#{$icon-prefix}-cer:before
  content: "\E094"
.#{$icon-prefix}-jsp:before
  content: "\E095"
.#{$icon-prefix}-cfm:before
  content: "\E096"
.#{$icon-prefix}-aspx:before
  content: "\E097"
.#{$icon-prefix}-rss:before
  content: "\E098"
.#{$icon-prefix}-csr:before
  content: "\E099"
.#{$icon-prefix}-less:before
  content: "\003C"
.#{$icon-prefix}-otf:before
  content: "\E101"
.#{$icon-prefix}-ttf:before
  content: "\E102"
.#{$icon-prefix}-font:before
  content: "\E103"
.#{$icon-prefix}-fnt:before
  content: "\E104"
.#{$icon-prefix}-eot:before
  content: "\E105"
.#{$icon-prefix}-woff:before
  content: "\E106"
.#{$icon-prefix}-zip:before
  content: "\E107"
.#{$icon-prefix}-zipx:before
  content: "\E108"
.#{$icon-prefix}-rar:before
  content: "\E109"
.#{$icon-prefix}-targ:before
  content: "\E110"
.#{$icon-prefix}-sitx:before
  content: "\E111"
.#{$icon-prefix}-deb:before
  content: "\E112"
.#{$icon-prefix}-e_7z:before
  content: "\E113"
.#{$icon-prefix}-pkg:before
  content: "\E114"
.#{$icon-prefix}-rpm:before
  content: "\E115"
.#{$icon-prefix}-cbr:before
  content: "\E116"
.#{$icon-prefix}-gz:before
  content: "\E117"
.#{$icon-prefix}-dmg:before
  content: "\E118"
.#{$icon-prefix}-cue:before
  content: "\E119"
.#{$icon-prefix}-bin:before
  content: "\E120"
.#{$icon-prefix}-iso:before
  content: "\E121"
.#{$icon-prefix}-hdf:before
  content: "\E122"
.#{$icon-prefix}-vcd:before
  content: "\E123"
.#{$icon-prefix}-bak:before
  content: "\E124"
.#{$icon-prefix}-tmp:before
  content: "\E125"
.#{$icon-prefix}-ics:before
  content: "\E126"
.#{$icon-prefix}-msi:before
  content: "\E127"
.#{$icon-prefix}-cfg:before
  content: "\E128"
.#{$icon-prefix}-ini:before
  content: "\E129"
.#{$icon-prefix}-prf:before
  content: "\E130"


================================================
FILE: app/styles/base/_mixins.sass
================================================
=ellipsis
  overflow: hidden
  white-space: nowrap
  text-overflow: ellipsis

=button_reset
  margin: 0
  padding: 0
  display: inline-block
  border: none
  background: none
  outline: none
  width: auto
  cursor: pointer
  font-family: inherit

=shape($size, $shape)
  display: inline-block
  width: $size
  height: $size
  line-height: $size
  text-align: center
  vertical-align: middle
  @if $shape == circle
    border-radius: 50%


================================================
FILE: app/styles/base/_reset.sass
================================================
a,abbr,acronym,address,applet,article,aside,audio,b,big,blockquote,body,canvas,caption,center,cite,code,dd,del,details,dfn,div,dl,dt,em,embed,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,header,hgroup,html,i,iframe,img,ins,kbd,label,legend,li,mark,menu,nav,object,ol,output,p,pre,q,ruby,s,samp,section,small,span,strike,strong,sub,summary,sup,table,tbody,td,tfoot,th,thead,time,tr,tt,u,ul,var,video
    margin: 0
    padding: 0
    border: 0
    font: inherit
    font-size: 100%
    vertical-align: baseline

html
    line-height: 1

ol,ul
    list-style: none

table
    border-collapse: collapse
    border-spacing: 0

caption,td,th
    text-align: left
    font-weight: 400
    vertical-align: middle

blockquote,q
    quotes: none

blockquote:after,blockquote:before,q:after,q:before
    content: ""
    content: none

a img
    border: 0

article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary
    display: block


================================================
FILE: app/styles/base/_variables.sass
================================================
$font-family: "Helvetica Neue", sans-serif

$blue: #0088cc
$green: #a4c540


================================================
FILE: app/styles/layout/_content.sass
================================================
.l-content
  position: relative
  height: 100vh
  min-height: 600px

.visually-hidden
  clip: rect(0 0 0 0)
  clip-path: inset(50%)
  height: 1px
  overflow: hidden
  position: absolute
  white-space: nowrap
  width: 1px

.ribbon
  position: fixed
  left: -80px
  bottom: 0px
  width: 300px
  height: 64px
  transform: rotate(45deg)
  z-index: 999
  background: linear-gradient(-180deg, rgb(0, 91, 187) 50%, rgb(255, 213, 0) 50%)
  opacity: 0.8


================================================
FILE: app/styles/layout/_footer.sass
================================================
.l-footer
  position: fixed
  z-index: 200
  bottom: 0
  left: 0
  right: 0
  background-color: rgba(white,.6)
  text-align: center
  color: #b0b0b0
  a
    opacity: .6
    transition: opacity .2s linear
    &:hover
      opacity: 1
    > span
      display: none

  .about
    display: inline-block
    font-size: 1.1rem
    line-height: 1.4
    margin-top: .2em
    padding: 0 10px

  .logos
    display: flex
    align-items: center
    justify-content: space-between
    padding: 10px

  .left,
  .right
    display: flex

  .github
    width: 20px
    height: 20px
    background: transparent url("../assets/images/github.svg") no-repeat center

  .twitter
    width: 80px
    height: 20px
    margin-left: 8px

  .donate
    img
      height: 20px


================================================
FILE: app/styles/layout/_header.sass
================================================
.l-header
  .navbar
    position: fixed
    z-index: 10
    top: 0
    left: 0
    width: 100%
    height: 60px
    background: transparent
    color: black
    user-select: none
    .logo
      position: relative
      height: 38px
      width: 162px
      margin: 15px 0 0 15px
      background: transparent url("../assets/images/sharedrop.svg") no-repeat left top
      .logo-title
        display: none
      .logo-subtitle
        position: absolute
        bottom: 0
        left: 44px
        font-size: 1rem
        font-weight: bold
      img
        vertical-align: top

    .nav
      position: absolute
      top: 15px
      right: 15px
      > li
        float: left
        margin-left: 15px
        font-size: 1.4rem
        line-height: 30px
        a
          display: inline-block
          transition: opacity .175s linear
          color: black
          img
            vertical-align: middle
          &:hover
            opacity: .6

    .icon-create-room
      font-size: 28px
      line-height: 24px

    .icon-help
      +shape(18px, circle)
      border: 1px solid black
      font-size: 1.2rem
      opacity: .18
      margin-top: -2px


================================================
FILE: app/styles/layout/_media.sass
================================================
@media (max-height: 520px)
  .modal-body
    height: auto
    bottom: auto
    margin: 15px auto

@media (max-width: 768px)
  .preloader
    width: 240px
    height: 41px
    background-size: 240px 41px

  .modal-body
    width: 90%
    height: auto
    bottom: auto
    margin: 15px auto
    .note
      display: block

  .l-content
    padding: 80px 0 115px
    min-height: inherit
    height: auto

  .user
    .peer
      position: relative
      left: auto !important
      bottom: auto !important
      height: 106px
      width: 100%
      padding: 15px
      border-bottom: 1px solid #eee
      margin: 0 !important
      text-align: left
      .avatar
        z-index: 2
      .user-info
        z-index: 1
        position: absolute
        top: 30px
        left: 0
        padding-left: 170px
        width: 100%
        .user-email
          font-size: 1.4rem
        .user-label
          font-size: 1.8rem
        .user-ip
          font-size: 1.2rem
          color: #808080
        .user-connection-status
          top: 0
          left: -9.2rem
          margin-top: 0
          transform: scale(1.75, 1.75)
    &.you
      .peer
        border-bottom: none

  .l-header
    .navbar
      z-index: 200
      background: rgba(white,.7)
      .email
        display: none
      .nav
        > li
          margin-left: 10px

  .l-footer
    padding-top: 10px

  .circles
    display: none

  .error
    width: auto
    font-size: 1.4rem
    padding: 0 15px


================================================
FILE: app/styles/modules/_modal.sass
================================================
.modal-overlay
  position: fixed
  z-index: 300
  top: 0
  bottom: 0
  left: 0
  right: 0
  background-color: rgba(black,.6)
  overflow: auto

.modal-body
  position: absolute
  z-index: 301
  top: 10%
  left: 0
  right: 0
  margin: auto
  background-color: white
  width: 580px
  padding: 20px
  margin-bottom: 40px
  border-radius: 5px
  h3
    font-size: 1.8rem
    font-weight: bold
    margin-bottom: 0.5em
  h4
    font-size: 1.5rem
    font-weight: bold
    margin-bottom: 0.5em
  p
    font-size: 1.4rem
    line-height: 1.4em
    margin-bottom: 1.42em
    &.note

      font-size: 1.1rem

  a
    color: $blue
    opacity: .6
    &:hover
      opacity: 1

  .qr-code
    div
      padding: 15px
    img
      margin-left: auto
      margin-right: auto

  .actions
    text-align: center
    button
      +button_reset
      font-size: 1.6rem
      cursor: pointer
      border-radius: 5px
      background: rgba($blue,.8)
      color: #fff
      padding: 14px 80px
      text-shadow: rgba(black,.3) 0 -1px 0
      transition: background .3s
      margin-bottom: 20px
      &:hover
        background: $blue

  .logo
    height: 38px
    margin-bottom: 20px
    background: transparent url("../assets/images/sharedrop.svg") no-repeat left
    span
      display: none

  .plus-icon
    font-weight: bold
    font-size: 2rem


================================================
FILE: app/styles/modules/_modules.sass
================================================
.preloader
  position: absolute
  left: 0
  right: 0
  top: 0
  bottom: 0
  margin: auto
  width: 324px
  height: 56px
  background: transparent url("../assets/images/sharedrop.svg") no-repeat center
  background-size: 324px 56px
  transition: opacity .2s
  > span
    position: absolute
    text-align: center
    width: 100%
    bottom: -18px
    font-size: 1.4rem

.ember-application
  .preloader
    opacity: 0

.error
  position: absolute
  top: 0
  bottom: 0
  left: 0
  right: 0
  margin: auto
  width: 50rem
  height: 15rem
  text-align: center
  font-size: 1.8rem
  line-height: 1.5em

.circles
  position: absolute
  bottom: 0
  left: 50%
  width: 1140px
  margin-left: -570px
  height: 700px
  z-index: -1
  transform-origin: 570px 570px
  animation: grow 1.5s ease-out
  .circle
    stroke-width: .4
    fill: rgba(0,0,0,0)

@keyframes grow
    50%
      transform: scale(1.5, 1.5)
      opacity: .2

    51%
        transform: scale(0.5, 0.5)
        opacity: 0


================================================
FILE: app/styles/modules/_popover.sass
================================================
$popover-border-color: #c0c0c0

.popover
  position: absolute
  bottom: 100%
  left: 50%
  transform: translateX(-50%)
  z-index: 10
  background-color: #fff
  border: 1px solid $popover-border-color
  padding: 10px
  border-radius: 5px
  width: 360px
  box-shadow: rgba(black,.3) 0 1px 3px
  text-align: left
  margin-bottom: 5px
  &::after
    position: absolute
    bottom: 0
    left: 50%
    margin: 0 0 -5px -5px
    content: ''
    width: 10px
    height: 10px
    background: inherit
    transform: rotate(45deg)
    border: 1px solid transparent
    border-right-color: $popover-border-color
    border-bottom-color: $popover-border-color

.popover-body
  position: relative
  padding-left: 60px
  p
    word-break: break-all
    overflow: hidden
    font-size: 12px
    line-height: 1.4em
    margin-bottom: 1em
    min-height: 28px

.popover-icon
  position: absolute
  left: 0
  top: 0
  font-size: 50px

.popover-buttons
  text-align: right


@media (max-width: 768px)
  .popover
    left: 0
    right: 0
    top: 0
    bottom: 0
    border: none
    width: auto
    box-shadow: none
    border-radius: 0
    margin: 0
    transform: none
    background-color: rgba(#f0f0f0, 0.9)
    &::after
      display: none

  .popover-buttons
    button
      font-size: 18px



================================================
FILE: app/styles/modules/_users.sass
================================================
$user-size: 76px

.user
  user-select: none

  .peer
    position: absolute
    left: 50%
    bottom: 300px
    width: $user-size
    height: $user-size
    margin-left: -$user-size / 2
    text-align: center
    .avatar
      position: relative
      width: $user-size
      height: $user-size
      transition: all .2s ease-in-out
      svg
        top: 0
        bottom: 0
        z-index: -1

    .gravatar
      position: absolute
      top: 5px
      left: 5px
      z-index: 1
      border: 1px solid #c0c0c0
      box-shadow: rgba(black, 0.2) 0 0 3px
      width: $user-size - 10
      height: $user-size - 10
      border-radius: 50%
      animation: shadow .8s ease-in
      transition: all .2s ease-in-out

    .user-info
      position: absolute
      top: $user-size
      left: 50%
      width: 140px
      margin-left: -70px
      .user-label, .user-email
        font-weight: bold
        color: #606060
        padding-bottom: .4rem
      .user-email
        font-size: 1rem
        +ellipsis
      .user-label
        font-size: 1.4rem
      .user-ip
        position: relative
        display: inline-block
        font-size: 1rem
        line-height: 1.2em
        color: #808080
        > strong
          display: block
      .user-connection-status
        position: absolute
        left: -1rem
        top: 50%
        margin-top: -.3rem
        width: .6rem
        height: .6rem
        border-radius: 50%
        &.disconnected
          display: none
        &.connecting
          background: rgba($blue,.5)
          animation: blink .75s infinite
        &.connected
          background: rgba($green,.8)

      select
        appearance: none
        border: none
        font-size: 1rem
        color: #808080
        padding-right: 10px
        outline: none
        background: transparent url("../images/select-arrow.svg") no-repeat 66px 50%
        // firefox fix - remove arrow from select
        text-indent: 0.01px
        text-overflow: ''

    &:nth-of-type(2)
      margin-left: -186px
      bottom: 225px
    &:nth-of-type(3)
      margin-left: 120px
      bottom: 225px
    &:nth-of-type(4)
      margin-left: -186px
      bottom: 365px
    &:nth-of-type(5)
      margin-left: 120px
      bottom: 365px
    &:nth-of-type(6)
      margin-left: -326px
      bottom: 180px
    &:nth-of-type(7)
      margin-left: 260px
      bottom: 180px
    &:nth-of-type(8)
      margin-left: -366px
      bottom: 320px
    &:nth-of-type(9)
      margin-left: 300px
      bottom: 320px
    &:nth-of-type(10)
      margin-left: -436px
      bottom: 90px
    &:nth-of-type(11)
      margin-left: 370px
      bottom: 90px
    &:nth-of-type(12)
      bottom: 400px
    &:nth-of-type(13)
      margin-left: -236px
      bottom: 90px
    &:nth-of-type(14)
      margin-left: 170px
      bottom: 90px

  &.you
    .peer
      bottom: 90px

  &.others
    .peer
      .avatar
        cursor: pointer
        &:hover, &.hover
          transform: scale(1.1, 1.1)
          .gravatar
            border-color: rgba($blue,.8)
        &::after
          opacity: 0
          position: absolute
          pointer-events: none
          top: 5px
          left: 5px
          z-index: 100
          content: "L"
          color: white
          font-size: 3rem
          font-weight: bold
          background: rgba($green,.8)
          border: 1px solid white
          transform: scaleX(-1) rotate(-45deg)

          // +circle
          display: inline-block
          width: 66px
          height: $user-size - 10
          line-height: $user-size - 10
          text-align: center
          vertical-align: middle
          border-radius: 50%

          transition: opacity .3s
        &.transfer-completed
          &::after
            opacity: 1

@keyframes blink
  0%
    opacity: 1
  50%
    opacity: 0

@keyframes shadow
  0%
    opacity: 0
  50%
    opacity: 1
    box-shadow: rgba(black, .3) 0 0 15px


================================================
FILE: app/templates/about-app.hbs
================================================
{{#modal-dialog onClose=(action "closeModal" target=currentRoute)}}
  <h2 class="logo"><span>ShareDrop</span></h2>
  <h3>What is it?</h3>
  <p>
    ShareDrop is a free, <a href="https://github.com/szimek/sharedrop">open-source</a> web app that allows you to easily and securely share files directly between devices without uploading them to any server first.
  </p>

  <h3>How to use it?</h3>
  <h4>Sharing files between devices in a local network<sup>*</sup></h4>
  <p>
    To send a file to another device in the same local network, open this page (i.e. <a href="https://www.sharedrop.io">www.sharedrop.io</a>) on both devices. Drag and drop a file directly on another person's avatar or click the avatar and select the file you want to send. The file transfer will start once the recipient accepts the file.
  </p>

  <h4>Sharing files between devices in different networks</h4>
  <p>
    To send a file to another device in a different network, click <span class="plus-icon">+</span> button in the upper right corner of the page and follow further instructions.
  </p>

  <h4>VPNs</h4>
  <p>
    Sharedrop does not work with VPNs, please deactivate any VPN your device might be using.
  </p>

  <h3>Security</h3>
  <p>ShareDrop uses a secure and encrypted peer-to-peer connection to transfer information about the file (its name and size) and file data itself. This means that this data is never transfered through any intermediate server but directly between the sender and recipient devices. To achieve this, ShareDrop uses a technology called WebRTC (Web Real-Time Communication), which is provided natively by browsers. You can read more about WebRTC security <a href="https://bloggeek.me/is-webrtc-safe/">here</a>.</p>

  <h3>Feedback</h3>
  <p>Got a problem with using ShareDrop, suggestion how to improve it or just want to say hi? Send an email to <a href="mailto:support@sharedrop.io">support@sharedrop.io</a> or report an issue on <a href="https://github.com/szimek/sharedrop/issues">GitHub</a>.</p>

  <div class="actions">
    <button {{action "closeModal"}} type="button">Got it!</button>
    <p class="note"><sup>*</sup>Devices need to have the same <a href="https://www.google.com/search?q=what+is+my+ip" target="_blank" rel="noopener noreferrer">public IP</a> to see each other.</p>
  </div>
{{/modal-dialog}}


================================================
FILE: app/templates/about-room.hbs
================================================
{{#modal-dialog onClose=(action "closeModal" target=currentRoute)}}
  <h2 class="logo"><span>ShareDrop</span></h2>
  <h3>Share files between devices in different networks</h3>

  <p>
    Copy provided address and send it to the other person...
  </p>

  <p>
    {{room-url value=currentUrl readonly="readonly" style="display: block; margin: auto;"}}
  </p>

  <p>
    Or you can scan it on the other device.
  </p>


  <p class="qr-code">
    {{qr-code text=currentUrl}}
  </p>

  <p>
    Once the other person open this page in a browser, you'll see each other's avatars.
  </p>

  <p>
    Drag and drop a file directly on other person's avatar or click the avatar and select the file you want to send. The file transfer will start once the recipient accepts the file.
  </p>

  <div class="actions">
    <button {{action "closeModal"}} type="button">Got it!</button>
  </div>
{{/modal-dialog}}


================================================
FILE: app/templates/about-you.hbs
================================================
ShareDrop lets you share files with others.
Other people will see you as <b>{{you.label}}</b>.

================================================
FILE: app/templates/application.hbs
================================================
<header class="l-header">
  <nav class="navbar">
    <h1 class="logo">
      <span class="logo-title">ShareDrop</span>
      <span class="logo-subtitle">P2P file transfer</span>
    </h1>

    <ul class="nav">
      <li>
        <a href="javascript:void(0)" class="icon-create-room" {{action "redirect"}} title="Create a room. You'll leave the room you're currently in.">+</a>
      </li>
      <li>
        <a href="javascript:void(0)" class="icon-help" {{action "openModal" "about_app"}} title="About">?</a>
      </li>
    </ul>
  </nav>
</header>

{{outlet}}
{{outlet "modal"}}

<footer class="l-footer">
  <p class="about">{{outlet "about_you"}}</p>

  <div class="logos">
    <div class="left"></div>
    <div class="right">
      <iframe title="twitter" loading="lazy" class="twitter" src="https://platform.twitter.com/widgets/tweet_button.html?url=https%3A%2F%2Fwww.sharedrop.io&text=ShareDrop%20%E2%80%93%20easily%20and%20securely%20share%20files%20of%20any%20size%20directly%20between%20devices%20using%20your%20browser&count=none" scrolling="no" frameborder="0" allowtransparency="true" ></iframe>
      <a href="https://github.com/szimek/sharedrop" class="github" target="_blank" rel="noopener noreferrer"><span>Github</span></a>
    </div>
  </div>
</footer>

<a href="https://war.ukraine.ua/" class="ribbon" target="_blank"><span class="visually-hidden">We stand with Ukraine!</span></a>


================================================
FILE: app/templates/components/modal-dialog.hbs
================================================
{{! template-lint-disable no-invalid-interactive }}
<div class="modal-overlay" {{action "close"}}></div>
<div class="modal-body">
  {{yield}}
</div>
{{! template-lint-enable no-invalid-interactive }}

================================================
FILE: app/templates/components/peer-widget.hbs
================================================
{{! Sender related messages }}
{{#if hasSelectedFile}}
  {{#popover-confirm
    onConfirm=(action "sendFileTransferInquiry")
    onCancel=(action "cancelFileTransfer")
    confirmButtonLabel="Send"
    cancelButtonLabel="Cancel"
    filename=filename
  }}
    Do you want to send <strong>"{{filename}}"</strong> to <strong>"{{label}}"</strong>?
  {{/popover-confirm}}
{{/if}}

{{#if isAwaitingResponse}}
  {{#popover-confirm
    onCancel=(action "abortFileTransfer")
    cancelButtonLabel="Cancel"
    filename=filename
  }}
    Waiting for <strong>"{{label}}"</strong> to accept&hellip;
  {{/popover-confirm}}
{{/if}}

{{#if hasDeclinedFileTransfer}}
  {{#popover-confirm
    onConfirm=(action "cancelFileTransfer")
    confirmButtonLabel="Ok"
    filename=filename
  }}
    <strong>"{{label}}"</strong> has declined your request.
  {{/popover-confirm}}
{{/if}}

{{#if hasError}}
  {{#popover-confirm
    onConfirm=(action "cancelFileTransfer")
    confirmButtonLabel="Ok"
    filename=filename
  }}
    {{partial errorTemplateName}}
  {{/popover-confirm}}
{{/if}}

{{! Recipient related popups }}
{{#if hasReceivedFileInfo}}
  {{#popover-confirm
    onConfirm=(action "acceptFileTransfer")
    onCancel=(action "rejectFileTransfer")
    confirmButtonLabel="Save"
    cancelButtonLabel="Decline"
    filename=filename
  }}
    <strong>"{{label}}"</strong> wants to send you <strong>"{{filename}}"</strong>.
  {{/popover-confirm}}
{{/if}}

<div class="avatar">
  {{#if isPreparingFileTransfer}}
    <CircularProgress @value={{peer.bundlingProgress}} @color="orange" />
  {{else if peer.transfer}}
    {{#if peer.transfer.receivingProgress}}
      <CircularProgress @value={{peer.transfer.receivingProgress}} @color="blue" />
    {{else if peer.transfer.sendingProgress}}
      <CircularProgress @value={{peer.transfer.sendingProgress}} @color="blue" />
    {{/if}}
  {{/if}}

  {{peer-avatar peer=peer onFileDrop=(action "uploadFile")}}
</div>

<div class="user-info">
  <div class="user-ip">
    <div class="user-connection-status {{peer.peer.state}}"></div>
    <span>{{peer.label}}</span>

    {{#if isPreparingFileTransfer}}
      <div>
        Bundling files...
        <br><br>
        TIP: You can archive files on your device beforehand to speed up the operation
      </div>
    {{else if isReceivingFile}}
      <div>Receiving file...</div>
    {{else if isSendingFile}}
      <div>Sending file...</div>
    {{/if}}
  </div>
</div>

{{file-field multiple=true onChange=(action "uploadFile")}}

================================================
FILE: app/templates/components/popover-confirm.hbs
================================================
<div class="popover">
  <div class="popover-body">
    <div class="popover-icon">
      <i class={{iconClass}}></i>
    </div>

    <p>{{yield}}</p>
  </div>

  <div class="popover-buttons">
    {{#if cancelButtonLabel}}
      <button type="button" {{action "cancel"}}>{{cancelButtonLabel}}</button>
    {{/if}}

    {{#if confirmButtonLabel}}
      <button type="button" {{action "confirm"}}>{{confirmButtonLabel}}</button>
    {{/if}}
  </div>
</div>



================================================
FILE: app/templates/components/user-widget.hbs
================================================
<div class="avatar">
  <img class="gravatar" src={{user.avatarUrl}} alt={{users.label}} title="peer id: {{user.uuid}}">
</div>
<div class="user-info">
  <div class="user-label">You</div>
  <div class="user-ip">
    <span>{{user.label}}</span>
  </div>
</div>


================================================
FILE: app/templates/errors/browser-unsupported.hbs
================================================
<div class="error">
  <p>We're really sorry, but your browser is not supported.<br>Please use the latest desktop or Android version of<br><b>Chrome</b>, <b>Opera</b> or <b>Firefox</b>.</p>
</div>


================================================
FILE: app/templates/errors/filesystem-unavailable.hbs
================================================
<div class="error">
  <p>Uh oh. Looks like there's some issue and we won't be able<br>to save your files.</p>
  <p>If you've opened this app in incognito/private window,<br>try again in a normal one.</p>
</div>


================================================
FILE: app/templates/errors/popovers/connection-failed.hbs
================================================
It was not possible to establish direct connection with the other peer.


================================================
FILE: app/templates/errors/popovers/multiple-files.hbs
================================================
The files you have selected exceed the maximum allowed size of 200MB
<br><br>
TIP: You can send single files without size restriction


================================================
FILE: app/templates/index.hbs
================================================
<main class="l-content">
  <div class="user others">
    {{#each model as |peer|}}
      {{peer-widget peer=peer hasCustomRoomName=hasCustomRoomName webrtc=webrtc}}
    {{/each}}
  </div>

  {{#if you.uuid}}
    <div class="user you">
      {{user-widget user=you}}
    </div>
  {{/if}}

  <svg class="circles" viewBox="-0.5 -0.5 1140 700">
    <circle class="circle" cx="570" cy="570" r="30"  stroke="rgba(160,160,160, 1)" />
    <circle class="circle" cx="570" cy="570" r="100" stroke="rgba(160,160,160,.9)" />
    <circle class="circle" cx="570" cy="570" r="200" stroke="rgba(160,160,160,.8)" />
    <circle class="circle" cx="570" cy="570" r="300" stroke="rgba(160,160,160,.7)" />
    <circle class="circle" cx="570" cy="570" r="400" stroke="rgba(160,160,160,.6)" />
    <circle class="circle" cx="570" cy="570" r="500" stroke="rgba(160,160,160,.5)" />
  </svg>
</main>


================================================
FILE: config/dotenv.js
================================================
module.exports = function () {
  return {
    clientAllowedKeys: ['FIREBASE_URL'],
    // Fail build when there is missing any of clientAllowedKeys environment variables.
    // By default false.
    failOnMissingKey: false,
  };
};


================================================
FILE: config/environment.js
================================================
module.exports = function (environment) {
  const ENV = {
    modulePrefix: 'sharedrop',
    environment,
    rootURL: '/',
    locationType: 'auto',
    EmberENV: {
      FEATURES: {
        // Here you can enable experimental features on an ember canary build
        // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true
      },
      EXTEND_PROTOTYPES: {
        // Prevent Ember Data from overriding Date.parse.
        Date: false,
      },
    },

    APP: {
      // Here you can pass flags/options to your application instance
      // when it is created
    },

    FIREBASE_URL: process.env.FIREBASE_URL,

    exportApplicationGlobal: true,
  };

  if (environment === 'development') {
    // ENV.APP.LOG_RESOLVER = true;
    // ENV.APP.LOG_ACTIVE_GENERATION = true;
    // ENV.APP.LOG_TRANSITIONS = true;
    // ENV.APP.LOG_TRANSITIONS_INTERNAL = true;
    // ENV.APP.LOG_VIEW_LOOKUPS = true;
  }

  if (environment === 'test') {
    // Testem prefers this...
    ENV.locationType = 'none';

    // keep test console output quieter
    ENV.APP.LOG_ACTIVE_GENERATION = false;
    ENV.APP.LOG_VIEW_LOOKUPS = false;

    ENV.APP.rootElement = '#ember-testing';
    ENV.APP.autoboot = false;
  }

  if (environment === 'production') {
    ENV.googleAnalyticsId = 'UA-41889586-2';
  }

  return ENV;
};


================================================
FILE: config/optional-features.json
================================================
{
  "application-template-wrapper": false,
  "default-async-observers": true,
  "jquery-integration": true,
  "template-only-glimmer-components": true
}


================================================
FILE: config/targets.js
================================================
const browsers = [
  'last 2 Chrome versions',
  'last 2 Firefox versions',
  'last 2 Safari versions',
  'last 2 iOS versions',
  'last 2 Edge versions',
];

module.exports = {
  browsers,
};


================================================
FILE: ember-cli-build.js
================================================
/* eslint-env node */
const EmberApp = require('ember-cli/lib/broccoli/ember-app');

module.exports = function (defaults) {
  const app = new EmberApp(defaults, {
    // Don't include SVG files, because of animal icons being loaded dynamically
    fingerprint: {
      extensions: ['js', 'css', 'png', 'jpg', 'gif', 'map'],
    },

    // Generate source maps in production as well
    sourcemaps: { enabled: true },

    sassOptions: { extension: 'sass' },

    SRI: { enabled: false },
  });

  // Use `app.import` to add additional libraries to the generated
  // output files.
  //
  // If you need to use different assets in different
  // environments, specify an object as the first parameter. That
  // object's keys should be the environment name and the values
  // should be the asset to use in that environment.
  //
  // If the library that you are including contains AMD or ES6
  // modules that you would like to import into your application
  // please specify an object with the list of modules as keys
  // along with the exports of each module as its value.

  app.import('vendor/ba-tiny-pubsub.min.js');
  app.import('vendor/filer.min.js');
  app.import('vendor/idb.filesystem.min.js');
  app.import('vendor/peer.js');

  return app.toTree();
};


================================================
FILE: firebase_rules.json
================================================
{
    "rules": {
        "rooms": {
            "$roomid": {
                // You can see people in the room only if you are in it as well
                ".read": "auth != null && data.child('users').hasChild(auth.id)",
                "users": {
                    "$userid": {
                        // You can modify only your own info
                        ".write": "auth != null && $userid == auth.id",

                        // Ensure that all required attributes are there
                        ".validate": "newData.hasChildren(['uuid', 'public_ip', 'peer']) && newData.child('peer').hasChildren(['id'])"

                    }
                },
                "messages": {
                    // You can send message to anybody in the room
                    ".write": "auth != null",
                    "$userid": {
                        // You can read only messages sent to you
                        ".read": "auth != null && $userid == auth.id"
                    }
                }
            }
        }
    }
}


================================================
FILE: lib/google-analytics/index.js
================================================
const { name } = require('./package');

module.exports = {
  name,

  isDevelopingAddon() {
    return true;
  },

  contentFor(type, config) {
    const id = config.googleAnalyticsId;

    if (type === 'head' && id) {
      return `
        <script async src="https://www.googletagmanager.com/gtag/js?id=${id}"></script>
        <script>
          window.dataLayer = window.dataLayer || [];
          function gtag(){dataLayer.push(arguments);}
          gtag('js', new Date());
          gtag('config', '${id}');
        </script>
      `;
    }

    return '';
  },
};


================================================
FILE: lib/google-analytics/package.json
================================================
{
  "name": "google-analytics",
  "keywords": [
    "ember-addon"
  ]
}


================================================
FILE: newrelic.js
================================================
/**
 * New Relic agent configuration.
 *
 * See lib/config.defaults.js in the agent distribution for a more complete
 * description of configuration variables and their potential values.
 */
exports.config = {
  /**
   * Array of application names.
   */
  app_name: ['ShareDrop'],

  logging: {
    /**
     * Level at which to log. 'trace' is most useful to New Relic when diagnosing
     * issues with the agent, 'info' and higher will impose the least overhead on
     * production applications.
     */
    level: 'info',
  },
};


================================================
FILE: package.json
================================================
{
  "name": "sharedrop",
  "version": "1.0.0",
  "private": true,
  "description": "P2P file sharing",
  "license": "MIT",
  "author": "Szymon Nowak",
  "directories": {
    "doc": "doc",
    "test": "tests"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/szimek/sharedrop.git"
  },
  "scripts": {
    "build": "ember build --environment=production",
    "lint": "npm-run-all --aggregate-output --continue-on-error --parallel lint:*",
    "develop": "yarn install --frozen-lockfile && nf --procfile=Procfile.dev start",
    "dev": "yarn develop",
    "lint:hbs": "ember-template-lint .",
    "lint:js": "eslint .",
    "start": "ember serve",
    "test": "npm-run-all lint:* test:*",
    "test:ember": "ember test"
  },
  "devDependencies": {
    "@ember/jquery": "^1.1.0",
    "@ember/optional-features": "^2.0.0",
    "@glimmer/component": "^1.0.2",
    "@glimmer/tracking": "^1.0.2",
    "babel-eslint": "^10.1.0",
    "broccoli-asset-rev": "^3.0.0",
    "ember-auto-import": "^1.6.0",
    "ember-cli": "~3.21.2",
    "ember-cli-app-version": "^3.2.0",
    "ember-cli-babel": "^7.22.1",
    "ember-cli-dependency-checker": "^3.2.0",
    "ember-cli-dotenv": "^3.1.0",
    "ember-cli-htmlbars": "^5.3.1",
    "ember-cli-inject-live-reload": "^2.0.2",
    "ember-cli-sass": "^10.0.0",
    "ember-cli-sri": "^2.1.1",
    "ember-cli-terser": "^4.0.0",
    "ember-export-application-global": "^2.0.1",
    "ember-fetch": "^8.0.2",
    "ember-load-initializers": "^2.1.1",
    "ember-maybe-import-regenerator": "^0.1.6",
    "ember-qrcode-shim": "^0.4.0",
    "ember-qunit": "^4.6.0",
    "ember-resolver": "^8.0.2",
    "ember-source": "~3.21.3",
    "ember-template-lint": "^2.13.0",
    "eslint": "^7.10.0",
    "eslint-config-airbnb-base": "^14.1.0",
    "eslint-config-prettier": "^6.12.0",
    "eslint-plugin-ember": "^9.2.0",
    "eslint-plugin-import": "^2.22.1",
    "eslint-plugin-node": "^11.1.0",
    "eslint-plugin-prettier": "^3.0.1",
    "foreman": "3.0.1",
    "husky": "^4.3.0",
    "lint-staged": "^10.4.0",
    "loader.js": "^4.7.0",
    "npm-run-all": "^4.1.5",
    "prettier": "^2.1.2",
    "qunit-dom": "^1.5.0",
    "sass": "^1.26.11"
  },
  "dependencies": {
    "@sentry/browser": "5.24.2",
    "@sentry/integrations": "5.24.2",
    "body-parser": "^1.10.0",
    "compression": "^1.2.2",
    "cookie-parser": "^1.3.3",
    "cookie-session": "^1.1.0",
    "express": "^4.10.6",
    "firebase-token-generator": "~2.0.0",
    "jszip": "^3.5.0",
    "lodash": "^4.17.20",
    "morgan": "^1.5.0",
    "newrelic": "^6.13.1",
    "stream": "^0.0.2",
    "uuid": "^8.3.1"
  },
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "*.js": [
      "yarn run prettier --write",
      "yarn run lint:hbs",
      "yarn run lint:js"
    ]
  },
  "engines": {
    "node": "^14.0.0"
  },
  "ember": {
    "edition": "octane"
  },
  "ember-addon": {
    "paths": [
      "lib/google-analytics"
    ]
  }
}


================================================
FILE: prettier.config.js
================================================
// https://prettier.io/docs/en/options.html

module.exports = {
  printWidth: 80,
  tabWidth: 2,
  useTabs: false,
  semi: true,
  singleQuote: true,
  trailingComma: 'all',
  bracketSpacing: true,
  jsxBracketSameLine: false,
  arrowParens: 'always',
};


================================================
FILE: public/.gitkeep
================================================


================================================
FILE: public/.well-known/brave-rewards-verification.txt
================================================
This is a Brave Rewards publisher verification file.

Domain: sharedrop.io
Token: d463c371edd92de3a09b0ccf1ce8143b9ee7f8c5611734f7ab58e2b67934b4bb


================================================
FILE: public/crossdomain.xml
================================================
<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy SYSTEM "http://www.adobe.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
  <!-- Read this: www.adobe.com/devnet/articles/crossdomain_policy_file_spec.html -->

  <!-- Most restrictive policy: -->
  <site-control permitted-cross-domain-policies="none"/>

  <!-- Least restrictive policy: -->
  <!--
  <site-control permitted-cross-domain-policies="all"/>
  <allow-access-from domain="*" to-ports="*" secure="false"/>
  <allow-http-request-headers-from domain="*" headers="*" secure="false"/>
  -->
</cross-domain-policy>


================================================
FILE: public/robots.txt
================================================
# http://www.robotstxt.org
User-agent: *
Disallow:


================================================
FILE: server.js
================================================
/* eslint-env node */

if (process.env.NODE_ENV === 'production') {
  // eslint-disable-next-line global-require
  require('newrelic');
}

// Room server
const http = require('http');
const path = require('path');
const express = require('express');
const logger = require('morgan');
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const cookieSession = require('cookie-session');
const compression = require('compression');
const { v4: uuidv4 } = require('uuid');
const crypto = require('crypto');
const FirebaseTokenGenerator = require('firebase-token-generator');

const firebaseTokenGenerator = new FirebaseTokenGenerator(
  process.env.FIREBASE_SECRET,
);
const app = express();
const secret = process.env.SECRET;
const base = ['dist'];

app.enable('trust proxy');

app.use(logger('combined'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(cookieParser());
app.use(
  cookieSession({
    cookie: {
      // secure: true,
      httpOnly: true,
      maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
    },
    secret,
    proxy: true,
  }),
);
app.use(compression());

//
// Web server
//
base.forEach((dir) => {
  const subdirs = ['assets', '.well-known'];

  subdirs.forEach((subdir) => {
    app.use(
      `/${subdir}`,
      express.static(`${dir}/${subdir}`, {
        maxAge: 31104000000, // ~1 year
      }),
    );
  });
});

//
// API server
//
app.get('/', (req, res) => {
  const root = path.join(__dirname, base[0]);
  console.log({ root });
  res.sendFile(`${root}/index.html`);
});

app.get('/rooms/:id', (req, res) => {
  const root = path.join(__dirname, base[0]);
  res.sendFile(`${root}/index.html`);
});

app.get('/room', (req, res) => {
  const ip = req.headers['cf-connecting-ip'] || req.ip;
  const name = crypto.createHmac('md5', secret).update(ip).digest('hex');

  res.json({ name });
});

app.get('/auth', (req, res) => {
  const ip = req.headers['cf-connecting-ip'] || req.ip;
  const uid = uuidv4();
  const token = firebaseTokenGenerator.createToken(
    { uid, id: uid }, // will be available in Firebase security rules as 'auth'
    { expires: 32503680000 }, // 01.01.3000 00:00
  );

  res.json({ id: uid, token, public_ip: ip });
});

http
  .createServer(app)
  .listen(process.env.PORT)
  .on('listening', () => {
    console.log(
      `Started ShareDrop web server at http://localhost:${process.env.PORT}...`,
    );
  });


================================================
FILE: sharedrop.crx
================================================
{
  "name": "ShareDrop",
  "description": "Simple file sharing",
  "version": "1",
  "app": {
    "urls": [
      "https:/www.sharedrop.io/"
    ],
    "launch": {
      "web_url": "https:/www.sharedrop.io/"
    }
  },
  "icons": {
    "128": "sharedrop-icon-128.png"
  },
  "permissions": [
    "notifications"
  ]
}


================================================
FILE: testem.js
================================================
module.exports = {
  test_page: 'tests/index.html?hidepassed',
  disable_watching: true,
  launch_in_ci: ['Chrome'],
  launch_in_dev: ['Chrome'],
  browser_start_timeout: 120,
  browser_args: {
    Chrome: {
      ci: [
        // --no-sandbox is needed when running Chrome inside a container
        process.env.CI ? '--no-sandbox' : null,
        '--headless',
        '--disable-dev-shm-usage',
        '--disable-software-rasterizer',
        '--mute-audio',
        '--remote-debugging-port=0',
        '--window-size=1440,900',
      ].filter(Boolean),
    },
  },
};


================================================
FILE: tests/helpers/.gitkeep
================================================


================================================
FILE: tests/index.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>EmberCliTest Tests</title>
    <meta name="description" content="">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    {{content-for "head"}}
    {{content-for "test-head"}}

    <link rel="stylesheet" href="{{rootURL}}assets/vendor.css">
    <link rel="stylesheet" href="{{rootURL}}assets/sharedrop.css">
    <link rel="stylesheet" href="{{rootURL}}assets/test-support.css">

    {{content-for "head-footer"}}
    {{content-for "test-head-footer"}}
  </head>
  <body>
    {{content-for "body"}}
    {{content-for "test-body"}}

    <script src="/testem.js" integrity=""></script>
    <script src="{{rootURL}}assets/vendor.js"></script>
    <script src="{{rootURL}}assets/test-support.js"></script>
    <script src="{{rootURL}}assets/sharedrop.js"></script>
    <script src="{{rootURL}}assets/tests.js"></script>

    {{content-for "body-footer"}}
    {{content-for "test-body-footer"}}
  </body>
</html>


================================================
FILE: tests/integration/.gitkeep
================================================


================================================
FILE: tests/test-helper.js
================================================
/* eslint */
import { setApplication } from '@ember/test-helpers';
import { start } from 'ember-qunit';
import Application from 'sharedrop/app';
import config from 'sharedrop/config/environment';

setApplication(Application.create(config.APP));

start();


================================================
FILE: tests/unit/.gitkeep
================================================


================================================
FILE: vendor/.gitkeep
================================================


================================================
FILE: vendor/peer.js
================================================
/*! peerjs.js build:0.3.7, development. Copyright(c) 2013 Michelle Bu <michelle@michellebu.com> */
(function(exports){
/**
 * Light EventEmitter. Ported from Node.js/events.js
 * Eric Zhang
 */

/**
 * EventEmitter class
 * Creates an object with event registering and firing methods
 */
function EventEmitter() {
  // Initialise required storage variables
  this._events = {};
}

var isArray = Array.isArray;


EventEmitter.prototype.addListener = function(type, listener) {
  if ('function' !== typeof listener) {
    throw new Error('addListener only takes instances of Function');
  }

  // To avoid recursion in the case that type == "newListeners"! Before
  // adding it to the listeners, first emit "newListeners".
  this.emit('newListener', type, typeof listener.listener === 'function' ?
            listener.listener : listener);

  if (!this._events[type]) {
    // Optimize the case of one listener. Don't need the extra array object.
    this._events[type] = listener;
  } else if (isArray(this._events[type])) {

    // If we've already got an array, just append.
    this._events[type].push(listener);

  } else {
    // Adding the second element, need to change to array.
    this._events[type] = [this._events[type], listener];
  }
  return this;
};

EventEmitter.prototype.on = EventEmitter.prototype.addListener;

EventEmitter.prototype.once = function(type, listener) {
  if ('function' !== typeof listener) {
    throw new Error('.once only takes instances of Function');
  }

  var self = this;
  function g() {
    self.removeListener(type, g);
    listener.apply(this, arguments);
  }

  g.listener = listener;
  self.on(type, g);

  return this;
};

EventEmitter.prototype.removeListener = function(type, listener) {
  if ('function' !== typeof listener) {
    throw new Error('removeListener only takes instances of Function');
  }

  // does not use listeners(), so no side effect of creating _events[type]
  if (!this._events[type]) return this;

  var list = this._events[type];

  if (isArray(list)) {
    var position = -1;
    for (var i = 0, length = list.length; i < length; i++) {
      if (list[i] === listener ||
          (list[i].listener && list[i].listener === listener))
      {
        position = i;
        break;
      }
    }

    if (position < 0) return this;
    list.splice(position, 1);
    if (list.length === 0)
      delete this._events[type];
  } else if (list === listener ||
             (list.listener && list.listener === listener))
  {
    delete this._events[type];
  }

  return this;
};


EventEmitter.prototype.off = EventEmitter.prototype.removeListener;

EventEmitter.prototype.removeAllListeners = function(type) {
  if (arguments.length === 0) {
    this._events = {};
    return this;
  }

  // does not use listeners(), so no side effect of creating _events[type]
  if (type && this._events && this._events[type]) this._events[type] = null;
  return this;
};

EventEmitter.prototype.listeners = function(type) {
  if (!this._events[type]) this._events[type] = [];
  if (!isArray(this._events[type])) {
    this._events[type] = [this._events[type]];
  }
  return this._events[type];
};

EventEmitter.prototype.emit = function(type) {
  type = arguments[0];

  var handler = this._events[type];
  var l, args, i;

  if (!handler) return false;

  if (typeof handler == 'function') {
    switch (arguments.length) {
      // fast cases
      case 1:
        handler.call(this);
        break;
      case 2:
        handler.call(this, arguments[1]);
        break;
      case 3:
        handler.call(this, arguments[1], arguments[2]);
        break;
      // slower
      default:
        l = arguments.length;
        args = new Array(l - 1);
        for (i = 1; i < l; i++) args[i - 1] = arguments[i];
        handler.apply(this, args);
    }
    return true;

  } else if (isArray(handler)) {
    l = arguments.length;
    args = new Array(l - 1);
    for (i = 1; i < l; i++) args[i - 1] = arguments[i];

    var listeners = handler.slice();
    for (i = 0, l = listeners.length; i < l; i++) {
      listeners[i].apply(this, args);
    }
    return true;
  } else {
    return false;
  }
};

exports.RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription;
exports.RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
exports.RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate;

var defaultConfig = {
    'iceServers': [{
        'urls': 'stun:stun.l.google.com:19302',
    }]
};

var util = {
  noop: function() {},

  // Logging logic
  logLevel: 0,
  setLogLevel: function(level) {
    var debugLevel = parseInt(level, 10);
    if (!isNaN(parseInt(level, 10))) {
      util.logLevel = debugLevel;
    } else {
      // If they are using truthy/falsy values for debug
      util.logLevel = level ? 3 : 0;
    }
    util.log = util.warn = util.error = util.noop;
    if (util.logLevel > 0) {
      util.error = util._printWith('ERROR');
    }
    if (util.logLevel > 1) {
      util.warn = util._printWith('WARNING');
    }
    if (util.logLevel > 2) {
      util.log = util._print;
    }
  },
  setLogFunction: function(fn) {
    if (fn.constructor !== Function) {
      util.warn('The log function you passed in is not a function. Defaulting to regular logs.');
    } else {
      util._print = fn;
    }
  },

  _printWith: function(prefix) {
    return function() {
      var copy = Array.prototype.slice.call(arguments);
      copy.unshift(prefix);
      util._print.apply(util, copy);
    };
  },
  _print: function () {
    var err = false;
    var copy = Array.prototype.slice.call(arguments);
    copy.unshift('PeerJS: ');
    for (var i = 0, l = copy.length; i < l; i++){
      if (copy[i] instanceof Error) {
        copy[i] = '(' + copy[i].name + ') ' + copy[i].message;
        err = true;
      }
    }
    err ? console.error.apply(console, copy) : console.log.apply(console, copy);
  },
  //

  // Returns browser-agnostic default config
  defaultConfig: defaultConfig,
  //

  // Returns the current browser.
  browser: (function() {
    if (window.mozRTCPeerConnection) {
      return 'Firefox';
    } else if (window.webkitRTCPeerConnection) {
      return 'Chrome';
    } else if (window.RTCPeerConnection) {
      return 'Supported';
    } else {
      return 'Unsupported';
    }
  })(),
  //

  // Lists which features are supported
  supports: (function() {
    if (typeof RTCPeerConnection === 'undefined') {
      return {};
    }

    var data = true;
    var audioVideo = true;

    var binaryBlob = false;
    var sctp = true;
    var onnegotiationneeded = !!window.webkitRTCPeerConnection;

    var pc, dc;
    try {
      pc = new RTCPeerConnection(defaultConfig, {optional: [{RtpDataChannels: true}]});
    } catch (e) {
      data = false;
      audioVideo = false;
    }

    if (data) {
      try {
        dc = pc.createDataChannel('_PEERJSTEST');
      } catch (e) {
        sctp = false;
        data = false;
      }
    }

    if (data) {
      // Binary test
      try {
        dc.binaryType = 'blob';
        binaryBlob = true;
      } catch (e) {
        console.log(e);
      }
    }

    // FIXME: not really the best check...
    if (audioVideo) {
      audioVideo = !!pc.addStream;
    }

    // FIXME: this is not great because in theory it doesn't work for
    // av-only browsers (?).
    if (!onnegotiationneeded && data) {
      // sync default check.
      var negotiationPC = new RTCPeerConnection(defaultConfig, {optional: [{RtpDataChannels: true}]});
      negotiationPC.onnegotiationneeded = function() {
        onnegotiationneeded = true;
        // async check.
        if (util && util.supports) {
          util.supports.onnegotiationneeded = true;
        }
      };

      negotiationPC.createDataChannel('_PEERJSNEGOTIATIONTEST');

      setTimeout(function() {
        negotiationPC.close();
      }, 1000);
    }

    if (pc) {
      pc.close();
    }

    return {
      audioVideo: audioVideo,
      data: data,
      binaryBlob: binaryBlob,
      binary: sctp, // deprecated; sctp implies binary support.
      reliable: sctp, // deprecated; sctp implies reliable data.
      sctp: sctp,
      onnegotiationneeded: onnegotiationneeded
    };
  }()),
  //

  // Ensure alphanumeric ids
  validateId: function(id) {
    // Allow empty ids
    return !id || /^[A-Za-z0-9]+(?:[ _-][A-Za-z0-9]+)*$/.exec(id);
  },

  debug: false,

  inherits: function(ctor, superCtor) {
    ctor.super_ = superCtor;
    ctor.prototype = Object.create(superCtor.prototype, {
      constructor: {
        value: ctor,
        enumerable: false,
        writable: true,
        configurable: true
      }
    });
  },
  extend: function(dest, source) {
    for(var key in source) {
      if(source.hasOwnProperty(key)) {
        dest[key] = source[key];
      }
    }
    return dest;
  },

  log: function () {
    if (util.debug) {
      var err = false;
      var copy = Array.prototype.slice.call(arguments);
      copy.unshift('PeerJS: ');
      for (var i = 0, l = copy.length; i < l; i++){
        if (copy[i] instanceof Error) {
          copy[i] = '(' + copy[i].name + ') ' + copy[i].message;
          err = true;
        }
      }
      err ? console.error.apply(console, copy) : console.log.apply(console, copy);
    }
  },

  setZeroTimeout: (function(global) {
    var timeouts = [];
    var messageName = 'zero-timeout-message';

    // Like setTimeout, but only takes a function argument. There's
    // no time argument (always zero) and no arguments (you have to
    // use a closure).
    function setZeroTimeoutPostMessage(fn) {
      timeouts.push(fn);
      global.postMessage(messageName, '*');
    }

    function handleMessage(event) {
      if (event.source == global && event.data == messageName) {
        if (event.stopPropagation) {
          event.stopPropagation();
        }
        if (timeouts.length) {
          timeouts.shift()();
        }
      }
    }
    if (global.addEventListener) {
      global.addEventListener('message', handleMessage, true);
    } else if (global.attachEvent) {
      global.attachEvent('onmessage', handleMessage);
    }
    return setZeroTimeoutPostMessage;
  }(this)),

  randomToken: function () {
    return Math.random().toString(36).substr(2);
  },

  isSecure: function() {
    return location.protocol === 'https:';
  }
};

exports.util = util;
/**
 * A peer who can initiate connections with other peers.
 */
function Peer(id, options) {
  EventEmitter.call(this);

  // Configurize options
  this.options = util.extend({
    debug: 0, // 1: Errors, 2: Warnings, 3: All logs
    config: util.defaultConfig
  }, options);

  // Set a custom log function if present
  if (options.logFunction) {
    util.setLogFunction(options.logFunction);
  }
  util.setLogLevel(options.debug);

  // Sanity checks
  // Ensure WebRTC supported
  if (!util.supports.audioVideo && !util.supports.data ) {
    this._delayedAbort('browser-incompatible', 'The current browser does not support WebRTC');
    return;
  }
  // Ensure alphanumeric id
  if (!util.validateId(id)) {
    this._delayedAbort('invalid-id', 'ID "' + id + '" is invalid');
    return;
  }

  // States
  this.destroyed = false; // Connections have been killed
  this.disconnected = false; // Connection to PeerServer killed manually but P2P connections still active
  this.open = false; // Sockets and such are not yet open.

  // References
  this.connections = {}; // DataConnections for this peer.
  this._lostMessages = {}; // src => [list of messages]

  // Start the connections
  this._initialize(id);
}

util.inherits(Peer, EventEmitter);

/** Initialize a connection with the server. */
Peer.prototype._initialize = function (id) {
  this.id = id;

  // Firebase
  this._ref = this.options.firebaseRef;
  this._connectionRef = this._ref.child('.info/connected');
  this._messagesRef = this._ref.child('rooms/' + this.options.room + '/messages');
  this._receivedMessagesRef = this._messagesRef.child(id);

  this._connectionRef.on('value', (connectionSnapshot) => {
    if (connectionSnapshot.val() === true) {
      // Remove received messages on disconnect
      this._receivedMessagesRef.onDisconnect().remove();

      // Listen to incoming messages
      this._receivedMessagesRef.on('child_added', (snapshot) => {
        const message = snapshot.val();
        this._handleMessage(message);
      });
    } else {
      this._receivedMessagesRef.off();
    }
  });

  // The connection to the server is open
  this.emit('open', this.id);
  this.open = true;
};

/** Handles messages from the server. */
Peer.prototype._handleMessage = function (message) {
  var type = message.type;
  var payload = message.payload;
  var peer = message.src;
  var connectionId, connection;

  switch (type) {
    case 'LEAVE': // Another peer has closed its connection to this peer.
      util.log('Received leave message from', peer);
      this._cleanupPeer(peer);
      break;

    case 'EXPIRE': // The offer sent to a peer has expired without response.
      this.emit('error', new Error('Could not connect to peer ' + peer));
      break;

    case 'OFFER': // we should consider switching this to CALL/CONNECT, but this is the least breaking option.
      connectionId = payload.connectionId;
      connection = this.getConnection(peer, connectionId);

      if (connection) {
        util.warn('Offer received for existing Connection ID:', connectionId);
        //connection.handleMessage(message);
      } else {
        // Create a new connection.
        if (payload.type === 'media') {
          connection = new MediaConnection(peer, this, {
            connectionId: connectionId,
            _payload: payload,
            metadata: payload.metadata
          });
          this._addConnection(peer, connection);
          this.emit('call', connection);
        } else if (payload.type === 'data') {
          connection = new DataConnection(peer, this, {
            connectionId: connectionId,
            _payload: payload,
            metadata: payload.metadata,
            label: payload.label,
            serialization: payload.serialization,
            reliable: payload.reliable
          });
          this._addConnection(peer, connection);
          this.emit('connection', connection);
        } else {
          util.warn('Received malformed connection type:', payload.type);
          return;
        }
        // Find messages.
        var messages = this._getMessages(connectionId);
        for (var i = 0, ii = messages.length; i < ii; i += 1) {
          connection.handleMessage(messages[i]);
        }
      }
      break;

    case 'ERROR':
      connectionId = payload.connectionId;
      connection = this.getConnection(peer, connectionId);

      var error = new Error(payload.message);
      error.type = payload.type;

      if (connection) {
        connection.emit('error', error);
      }
      break;

    default:
      if (!payload) {
        util.warn('You received a malformed message from ' + peer + ' of type ' + type);
        return;
      }

      var id = payload.connectionId;
      connection = this.getConnection(peer, id);

      if (connection && connection.pc) {
        // Pass it on.
        connection.handleMessage(message);
      } else if (id) {
        // Store for possible later use
        this._storeMessage(id, message);
      } else {
        util.warn('You received an unrecognized message:', message);
      }
      break;
  }
};

/** Stores messages without a set up connection, to be claimed later. */
Peer.prototype._storeMessage = function (connectionId, message) {
  if (!this._lostMessages[connectionId]) {
    this._lostMessages[connectionId] = [];
  }
  this._lostMessages[connectionId].push(message);
};

/** Retrieve messages from lost message store */
Peer.prototype._getMessages = function (connectionId) {
  var messages = this._lostMessages[connectionId];
  if (messages) {
    delete this._lostMessages[connectionId];
    return messages;
  } else {
    return [];
  }
};

/**
 * Returns a DataConnection to the specified peer. See documentation for a
 * complete list of options.
 */
Peer.prototype.connect = function(peer, options) {
  if (this.disconnected) {
    util.warn('You cannot connect to a new Peer because you called '
        + '.disconnect() on this Peer and ended your connection with the'
        + ' server. You can create a new Peer to reconnect.');
    this.emit('error', new Error('Cannot connect to new Peer after disconnecting from server.'));
    return;
  }

  var connection = new DataConnection(peer, this, options);
  this._addConnection(peer, connection);
  return connection;
};

/**
 * Returns a MediaConnection to the specified peer. See documentation for a
 * complete list of options.
 */
Peer.prototype.call = function(peer, stream, options) {
  if (this.disconnected) {
    util.warn('You cannot connect to a new Peer because you called '
        + '.disconnect() on this Peer and ended your connection with the'
        + ' server. You can create a new Peer to reconnect.');
    this.emit('error', new Error('Cannot connect to new Peer after disconnecting from server.'));
    return;
  }
  if (!stream) {
    util.error('To call a peer, you must provide a stream from your browser\'s `getUserMedia`.');
    return;
  }
  options = options || {};
  options._stream = stream;
  var call = new MediaConnection(peer, this, options);
  this._addConnection(peer, call);
  return call;
};

/** Add a data/media connection to this peer. */
Peer.prototype._addConnection = function(peer, connection) {
  if (!this.connections[peer]) {
    this.connections[peer] = [];
  }
  this.connections[peer].push(connection);
};

/** Retrieve a data/media connection for this peer. */
Peer.prototype.getConnection = function(peer, id) {
  var connections = this.connections[peer];
  if (!connections) {
    return null;
  }
  for (var i = 0, ii = connections.length; i < ii; i++) {
    if (connections[i].id === id) {
      return connections[i];
    }
  }
  return null;
};

Peer.prototype._delayedAbort = function(type, message) {
  var self = this;
  util.setZeroTimeout(function (){
    self._abort(type, message);
  });
};

/** Destroys the Peer and emits an error message. */
Peer.prototype._abort = function(type, message) {
  util.error('Aborting. Error:', message);
  var err = new Error(message);
  err.type = type;
  this.destroy();
  this.emit('error', err);
};

/**
 * Destroys the Peer: closes all active connections as well as the connection
 *  to the server.
 * Warning: The peer can no longer create or accept connections after being
 *  destroyed.
 */
Peer.prototype.destroy = function() {
  if (!this.destroyed) {
    this._cleanup();
    this.destroyed = true;
  }
};


/** Disconnects every connection on this peer. */
Peer.prototype._cleanup = function() {
  if (this.connections) {
    var peers = Object.keys(this.connections);
    for (var i = 0, ii = peers.length; i < ii; i++) {
      this._cleanupPeer(peers[i]);
    }
  }
  this.emit('close');
};

/** Closes all connections to this peer. */
Peer.prototype._cleanupPeer = function(peer) {
  var connections = this.connections[peer];
  for (var j = 0, jj = connections.length; j < jj; j += 1) {
    connections[j].close();
  }
};

exports.Peer = Peer;
/**
 * Wraps a DataChannel between two Peers.
 */
function DataConnection(peer, provider, options) {
  if (!(this instanceof DataConnection)) return new DataConnection(peer, provider, options);
  EventEmitter.call(this);

  this.options = util.extend({
    serialization: 'binary',
    reliable: false,
    metadata: {} // Firebase doesn't allow undefined values
  }, options);

  // Connection is not open yet.
  this.open = false;
  this.type = 'data';
  this.peer = peer;
  this.provider = provider;

  this.id = this.options.connectionId || DataConnection._idPrefix + util.randomToken();

  this.label = this.options.label || this.id;
  this.metadata = this.options.metadata;
  this.serialization = this.options.serialization;
  this.reliable = this.options.reliable;

  // Data channel buffering.
  this._buffer = [];
  this._buffering = false;
  this.bufferSize = 0;

  // For storing large data.
  this._chunkedData = {};

  if (this.options._payload) {
    this._peerBrowser = this.options._payload.browser;
  }

  Negotiator.startConnection(
    this,
    this.options._payload || {
      originator: true
    }
  );
}

util.inherits(DataConnection, EventEmitter);

DataConnection._idPrefix = 'dc_';

/** Called by the Negotiator when the DataChannel is ready. */
DataConnection.prototype.initialize = function(dc) {
  this._dc = this.dataChannel = dc;
  this._configureDataChannel();
};

DataConnection.prototype._configureDataChannel = function() {
  var self = this;

  if (util.supports.sctp) {
    this._dc.binaryType = 'arraybuffer';
  }

  this._dc.onopen = function() {
    util.log('Data channel connection success');
    self.open = true;
    self.emit('open');
  };

  this._dc.onmessage = function (e) {
    self._handleDataMessage(e);
  };

  this._dc.onclose = function (e) {
    util.log('DataChannel closed for:', self.peer);
    util.log(e);
    self.close();
  };
};

// Handles a DataChannel message.
DataConnection.prototype._handleDataMessage = function(e) {
  var data = e.data;

  if (data.byteLength !== undefined) {
    // ArrayBuffer
  } else {
    // String (JSON)
    data = JSON.parse(data);
  }

  this.emit('data', data);
};

/**
 * Exposed functionality for users.
 */

/** Allows user to close connection. */
DataConnection.prototype.close = function() {
  if (!this.open) {
    return;
  }
  this.open = false;
  Negotiator.cleanup(this);
  this.emit('close');
};

/** Allows user to send data. */
DataConnection.prototype.send = function(data) {
  if (!this.open) {
    this.emit('error', new Error('Connection is not open. You should listen for the `open` event before sending messages.'));
    return;
  }

  // Lame type check
  if (data.byteLength === undefined) {
    // JSON string
    data = JSON.stringify(data);
  }

  this._bufferedSend(data);
};

DataConnection.prototype._bufferedSend = function(msg) {
  if (this._buffering || !this._trySend(msg)) {
    this._buffer.push(msg);
    this.bufferSize = this._buffer.length;
  }
};

// Returns true if the send succeeds.
DataConnection.prototype._trySend = function(msg) {
  try {
    this._dc.send(msg);
  } catch (e) {
    this._buffering = true;

    var self = this;
    setTimeout(function() {
      // Try again.
      self._buffering = false;
      self._tryBuffer();
    }, 100);
    return false;
  }
  return true;
};

// Try to send the first message in the buffer.
DataConnection.prototype._tryBuffer = function() {
  if (this._buffer.length === 0) {
    return;
  }

  var msg = this._buffer[0];

  if (this._trySend(msg)) {
    this._buffer.shift();
    this.bufferSize = this._buffer.length;
    this._tryBuffer();
  }
};

DataConnection.prototype.handleMessage = function(message) {
  var payload = message.payload;

  switch (message.type) {
    case 'ANSWER':
      this._peerBrowser = payload.browser;

      // Forward to negotiator
      Negotiator.handleSDP(message.type, this, payload.sdp);
      break;
    case 'CANDIDATE':
      Negotiator.handleCandidate(this, payload.candidate);
      break;
    default:
      util.warn('Unrecognized message type:', message.type, 'from peer:', this.peer);
      break;
  }
};

/**
 * Wraps the streaming interface between two Peers.
 */
function MediaConnection(peer, provider, options) {
  if (!(this instanceof MediaConnection)) return new MediaConnection(peer, provider, options);
  EventEmitter.call(this);

  this.options = util.extend({}, options);

  this.open = false;
  this.type = 'media';
  this.peer = peer;
  this.provider = provider;
  this.metadata = this.options.metadata;
  this.localStream = this.options._stream;

  this.id = this.options.connectionId || MediaConnection._idPrefix + util.randomToken();
  if (this.localStream) {
    Negotiator.startConnection(
      this,
      {_stream: this.localStream, originator: true}
    );
  }
}

util.inherits(MediaConnection, EventEmitter);

MediaConnection._idPrefix = 'mc_';

MediaConnection.prototype.addStream = function(remoteStream) {
  util.log('Receiving stream', remoteStream);

  this.remoteStream = remoteStream;
  this.emit('stream', remoteStream); // Should we call this `open`?

};

MediaConnection.prototype.handleMessage = function(message) {
  var payload = message.payload;

  switch (message.type) {
    case 'ANSWER':
      // Forward to negotiator
      Negotiator.handleSDP(message.type, this, payload.sdp);
      this.open = true;
      break;
    case 'CANDIDATE':
      Negotiator.handleCandidate(this, payload.candidate);
      break;
    default:
      util.warn('Unrecognized message type:', message.type, 'from peer:', this.peer);
      break;
  }
};

MediaConnection.prototype.answer = function(stream) {
  if (this.localStream) {
    util.warn('Local stream already exists on this MediaConnection. Are you answering a call twice?');
    return;
  }

  this.options._payload._stream = stream;

  this.localStream = stream;
  Negotiator.startConnection(
    this,
    this.options._payload
  );

  // Retrieve lost messages stored because PeerConnection not set up.
  var messages = this.provider._getMessages(this.id);
  for (var i = 0, ii = messages.length; i < ii; i += 1) {
    this.handleMessage(messages[i]);
  }
  this.open = true;
};

/**
 * Exposed functionality for users.
 */

/** Allows user to close connection. */
MediaConnection.prototype.close = function() {
  if (!this.open) {
    return;
  }
  this.open = false;
  Negotiator.cleanup(this);
  this.emit('close');
};

/**
 * Manages all negotiations between Peers.
 */
var Negotiator = {
  pcs: {
    data: {},
    media: {}
  }, // type => {peerId: {pc_id: pc}}.
  //providers: {}, // provider's id => providers (there may be multiple providers/client.
  queue: [] // connections that are delayed due to a PC being in use.
};

Negotiator._idPrefix = 'pc_';

/** Returns a PeerConnection object set up correctly (for data, media). */
Negotiator.startConnection = function(connection, options) {
  var pc = Negotiator._getPeerConnection(connection, options);

  if (connection.type === 'media' && options._stream) {
    // Add the stream.
    pc.addStream(options._stream);
  }

  // Set the connection's PC.
  connection.pc = pc;
  // What do we need to do now?
  if (options.originator) {
    if (connection.type === 'data') {
      // Create the datachannel.
      var config = {};
      // Dropping reliable:false support, since it seems to be crashing
      // Chrome.
      /*if (util.supports.sctp && !options.reliable) {
        // If we have canonical reliable support...
        config = {maxRetransmits: 0};
      }*/
      // Fallback to ensure older browsers don't crash.
      if (!util.supports.sctp) {
        config = {reliable: options.reliable};
      }
      var dc = pc.createDataChannel(connection.label, config);
      connection.initialize(dc);
    }

    if (!util.supports.onnegotiationneeded) {
      Negotiator._makeOffer(connection);
    }
  } else {
    Negotiator.handleSDP('OFFER', connection, options.sdp);
  }
};

Negotiator._getPeerConnection = function (connection, options) {
  if (!Negotiator.pcs[connection.type]) {
    util.error(connection.type + ' is not a valid connection type. Maybe you overrode the `type` property somewhere.');
  }

  if (!Negotiator.pcs[connection.type][connection.peer]) {
    Negotiator.pcs[connection.type][connection.peer] = {};
  }

  var pc;

  if (options.pc) { // Simplest case: PC id already provided for us.
    pc = Negotiator.pcs[connection.type][connection.peer][options.pc];
  }

  if (!pc || pc.signalingState !== 'stable') {
    pc = Negotiator._startPeerConnection(connection);
  }
  return pc;
};

/** Start a PC. */
Negotiator._startPeerConnection = function(connection) {
  util.log('Creating RTCPeerConnection.');

  var id = Negotiator._idPrefix + util.randomToken();
  var optional = {};

  if (connection.type === 'data' && !util.supports.sctp) {
    optional = {optional: [{RtpDataChannels: true}]};
  } else if (connection.type === 'media') {
    // Interop req for chrome.
    optional = {optional: [{DtlsSrtpKeyAgreement: true}]};
  }

  var pc = new RTCPeerConnection(connection.provider.options.config, optional);
  Negotiator.pcs[connection.type][connection.peer][id] = pc;

  Negotiator._setupListeners(connection, pc, id);

  return pc;
};

/** Set up various WebRTC listeners. */
Negotiator._setupListeners = function(connection, pc) {
  var provider = connection.provider,
      src = provider.id,
      dst = connection.peer,
      connectionId = connection.id;

  // ICE CANDIDATES.
  util.log('Listening for ICE candidates.');

  pc.onicecandidate = function(evt) {
    if (evt.candidate) {
      util.log('Received ICE candidates for:', dst);

      // For some reason Firefox requires toJSON call
      var c = evt.candidate,
          candidate = 'toJSON' in c ? c.toJSON() : c;

      provider._messagesRef.child(dst).push({
        type: 'CANDIDATE',
        payload: {
          candidate: candidate,
          type: connection.type,
          connectionId: connectionId
        },
        src: src,
        dst: dst
      });
    }
  };

  pc.oniceconnectionstatechange = function() {
    var error;

    switch (pc.iceConnectionState) {
      case 'failed':
        util.log("ICE connection state is 'failed', notifying " + dst);

        error = new Error("ICE connection state is 'failed'");
        error.type = 'failed';

        connection.provider._messagesRef.child(dst).push({
          type: 'ERROR',
          payload: {
            message: error.message,
            type: error.type,
            connectionId: connectionId
          },
          src: src,
          dst: dst
        });

        connection.emit('error', error);
        connection.close();
        break;

      case 'disconnected':
        util.log("ICE connection state is 'disconnected', closing connections to " + dst);

        error = new Error("ICE connection state is 'disconnected'");
        error.type = 'disconnected';

        connection.emit('error', error);
        connection.close();
        break;

      case 'completed':
        pc.onicecandidate = util.noop;
        break;
    }
  };

  // Fallback for older Chrome impls.
  pc.onicechange = pc.oniceconnectionstatechange;

  // ONNEGOTIATIONNEEDED (Chrome)
  util.log('Listening for `negotiationneeded`');
  pc.onnegotiationneeded = function() {
    util.log('`negotiationneeded` triggered');
    if (pc.signalingState == 'stable') {
      Negotiator._makeOffer(connection);
    } else {
      util.log('onnegotiationneeded triggered when not stable. Is another connection being established?');
    }
  };

  // DATACONNECTION.
  util.log('Listening for data channel');
  // Fired between offer and answer, so options should already be saved
  // in the options hash.
  pc.ondatachannel = function(evt) {
    util.log('Received data channel');
    var dc = evt.channel;
    var connection = provider.getConnection(dst, connectionId);
    connection.initialize(dc);
  };

  // MEDIACONNECTION.
  util.log('Listening for remote stream');
  pc.onaddstream = function(evt) {
    util.log('Received remote stream');
    var stream = evt.stream;
    provider.getConnection(dst, connectionId).addStream(stream);
  };
};

Negotiator.cleanup = function(connection) {
  util.log('Cleaning up PeerConnection to ' + connection.peer);

  var pc = connection.pc;

  if (!!pc && (pc.readyState !== 'closed' || pc.signalingState !== 'closed')) {
    pc.close();
    connection.pc = null;
  }
};

Negotiator._makeOffer = function(connection) {
  var pc = connection.pc,
      src = connection.provider.id,
      dst = connection.peer;

  pc
    .createOffer(connection.options.constraints || {})
    .then(function (offer) {
      util.log('Created offer.');

      pc
        .setLocalDescription(offer)
        .then(function () {
        // For some reason Firefox requires toJSON call
        var o = offer;
        offer = 'toJSON' in o ? o.toJSON() : o;

        util.log('Set localDescription: offer', 'for:', dst);
        connection.provider._messagesRef.child(dst).push({
          type: 'OFFER',
          payload: {
            sdp: offer,
            type: connection.type,
            label: connection.label,
            connectionId: connection.id,
            reliable: connection.reliable,
            serialization: connection.serialization,
            metadata: connection.metadata,
            browser: util.browser
          },
          src: src,
          dst: dst
        });
      })
      .catch(function (err) {
        connection.provider.emit('error', err);
        util.log('Failed to setLocalDescription, ', err);
      });
  })
  .catch(function (err) {
    connection.provider.emit('error', err);
    util.log('Failed to createOffer, ', err);
  });
};

Negotiator._makeAnswer = function(connection) {
  var pc = connection.pc,
      provider = connection.provider,
      src = provider.id,
      dst = connection.peer;

  pc
    .createAnswer()
    .then(function(answer) {
      util.log('Created answer.');

      pc
        .setLocalDescription(answer)
        .then(function() {
        // For some reason Firefox requires toJSON call
        var a = answer;
        answer = 'toJSON' in a ? a.toJSON() : a;

        util.log('Set localDescription: answer', 'for:', dst);
        provider._messagesRef.child(dst).push({
          type: 'ANSWER',
          payload: {
            sdp: answer,
            type: connection.type,
            connectionId: connection.id,
            browser: util.browser
          },
          src: src,
          dst: dst
        });
      })
      .catch(function(err) {
        connection.provider.emit('error', err);
        util.log('Failed to setLocalDescription, ', err);
      });
    })
    .catch(function(err) {
      connection.provider.emit('error', err);
      util.log('Failed to create answer, ', err);
    });
};

/** Handle an SDP. */
Negotiator.handleSDP = function(type, connection, sdp) {
  sdp = new RTCSessionDescription(sdp);
  var pc = connection.pc;

  util.log('Setting remote description', sdp);
  pc
    .setRemoteDescription(sdp)
    .then(function() {
      util.log('Set remoteDescription:', type, 'for:', connection.peer);

      if (type === 'OFFER') {
        Negotiator._makeAnswer(connection);
      }
    })
    .catch(function(err) {
      connection.provider.emit('error', err);
      util.log('Failed to setRemoteDescription, ', err);
    });
};

/** Handle a candidate. */
Negotiator.handleCandidate = function(connection, ice) {
  var candidate = ice.candidate;
  var sdpMLineIndex = ice.sdpMLineIndex;
  connection.pc.addIceCandidate(new RTCIceCandidate({
    sdpMLineIndex: sdpMLineIndex,
    candidate: candidate
  }));
  util.log('Added ICE candidate for:', connection.peer);
};

})(this);
Download .txt
gitextract_vxj0u4do/

├── .editorconfig
├── .ember-cli
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .prettierignore
├── .template-lintrc.js
├── .travis.yml
├── .watchmanconfig
├── Dockerfile
├── LICENSE
├── Procfile
├── Procfile.dev
├── README.md
├── app/
│   ├── app.js
│   ├── components/
│   │   ├── circular-progress.hbs
│   │   ├── circular-progress.js
│   │   ├── file-field.js
│   │   ├── modal-dialog.js
│   │   ├── peer-avatar.js
│   │   ├── peer-widget.js
│   │   ├── popover-confirm.js
│   │   ├── room-url.js
│   │   └── user-widget.js
│   ├── controllers/
│   │   ├── application.js
│   │   └── index.js
│   ├── helpers/
│   │   └── is-equal.js
│   ├── index.html
│   ├── initializers/
│   │   └── prerequisites.js
│   ├── models/
│   │   ├── peer.js
│   │   └── user.js
│   ├── router.js
│   ├── routes/
│   │   ├── application.js
│   │   ├── error.js
│   │   ├── index.js
│   │   └── room.js
│   ├── services/
│   │   ├── analytics.js
│   │   ├── avatar.js
│   │   ├── file.js
│   │   ├── room.js
│   │   └── web-rtc.js
│   ├── styles/
│   │   ├── app.sass
│   │   ├── base/
│   │   │   ├── _element_defaults.sass
│   │   │   ├── _glyphicons_filetypes.sass
│   │   │   ├── _mixins.sass
│   │   │   ├── _reset.sass
│   │   │   └── _variables.sass
│   │   ├── layout/
│   │   │   ├── _content.sass
│   │   │   ├── _footer.sass
│   │   │   ├── _header.sass
│   │   │   └── _media.sass
│   │   └── modules/
│   │       ├── _modal.sass
│   │       ├── _modules.sass
│   │       ├── _popover.sass
│   │       └── _users.sass
│   └── templates/
│       ├── about-app.hbs
│       ├── about-room.hbs
│       ├── about-you.hbs
│       ├── application.hbs
│       ├── components/
│       │   ├── modal-dialog.hbs
│       │   ├── peer-widget.hbs
│       │   ├── popover-confirm.hbs
│       │   └── user-widget.hbs
│       ├── errors/
│       │   ├── browser-unsupported.hbs
│       │   ├── filesystem-unavailable.hbs
│       │   └── popovers/
│       │       ├── connection-failed.hbs
│       │       └── multiple-files.hbs
│       └── index.hbs
├── config/
│   ├── dotenv.js
│   ├── environment.js
│   ├── optional-features.json
│   └── targets.js
├── ember-cli-build.js
├── firebase_rules.json
├── lib/
│   └── google-analytics/
│       ├── index.js
│       └── package.json
├── newrelic.js
├── package.json
├── prettier.config.js
├── public/
│   ├── .gitkeep
│   ├── .well-known/
│   │   └── brave-rewards-verification.txt
│   ├── crossdomain.xml
│   └── robots.txt
├── server.js
├── sharedrop.crx
├── testem.js
├── tests/
│   ├── helpers/
│   │   └── .gitkeep
│   ├── index.html
│   ├── integration/
│   │   └── .gitkeep
│   ├── test-helper.js
│   └── unit/
│       └── .gitkeep
└── vendor/
    ├── .gitkeep
    └── peer.js
Download .txt
SYMBOL INDEX (86 symbols across 23 files)

FILE: app/app.js
  class App (line 14) | class App extends Application {

FILE: app/components/circular-progress.js
  constant COLORS (line 4) | const COLORS = {
  class CircularProgress (line 9) | class CircularProgress extends Component {
    method constructor (line 10) | constructor(owner, args) {
    method path (line 17) | get path() {

FILE: app/components/file-field.js
  method click (line 8) | click(event) {
  method change (line 12) | change(event) {
  method reset (line 22) | reset() {

FILE: app/components/modal-dialog.js
  method close (line 5) | close() {

FILE: app/components/peer-avatar.js
  method toggleTransferCompletedClass (line 22) | toggleTransferCompletedClass() {
  method init (line 40) | init(...args) {
  method didInsertElement (line 48) | didInsertElement(...args) {
  method willDestroyElement (line 56) | willDestroyElement(...args) {
  method click (line 65) | click() {
  method dragEnter (line 72) | dragEnter(event) {
  method dragOver (line 78) | dragOver(event) {
  method dragLeave (line 82) | dragLeave() {
  method drop (line 86) | drop(event) {
  method cancelEvent (line 106) | cancelEvent(event) {
  method canSendFile (line 111) | canSendFile() {
  method isTransferableBundle (line 122) | isTransferableBundle(files) {

FILE: app/components/peer-widget.js
  method uploadFile (line 51) | uploadFile(data) {
  method sendFileTransferInquiry (line 66) | sendFileTransferInquiry() {
  method cancelFileTransfer (line 74) | cancelFileTransfer() {
  method abortFileTransfer (line 78) | abortFileTransfer() {
  method acceptFileTransfer (line 87) | acceptFileTransfer() {
  method rejectFileTransfer (line 98) | rejectFileTransfer() {
  method _cancelFileTransfer (line 107) | _cancelFileTransfer() {
  method _sendFileTransferResponse (line 116) | _sendFileTransferResponse(response) {
  method _reduceFiles (line 124) | async _reduceFiles(files) {

FILE: app/components/popover-confirm.js
  method confirm (line 23) | confirm() {
  method cancel (line 27) | cancel() {

FILE: app/components/room-url.js
  method didInsertElement (line 7) | didInsertElement() {
  method copyValueToClipboard (line 11) | copyValueToClipboard() {

FILE: app/controllers/application.js
  method init (line 10) | init(...args) {
  method redirect (line 28) | redirect() {

FILE: app/controllers/index.js
  method _onRoomConnected (line 14) | _onRoomConnected(event, data) {
  method _onRoomDisconnected (line 33) | _onRoomDisconnected() {
  method _onRoomUserAdded (line 38) | _onRoomUserAdded(event, data) {
  method _addPeer (line 46) | _addPeer(attrs) {
  method _onRoomUserChanged (line 57) | _onRoomUserChanged(event, data) {
  method _onRoomUserRemoved (line 76) | _onRoomUserRemoved(event, data) {
  method _onPeerP2PIncomingConnection (line 83) | _onPeerP2PIncomingConnection(event, data) {
  method _onPeerDCIncomingConnection (line 93) | _onPeerDCIncomingConnection(event, data) {
  method _onPeerDCIncomingConnectionError (line 101) | _onPeerDCIncomingConnectionError(event, data) {
  method _onPeerP2POutgoingConnection (line 123) | _onPeerP2POutgoingConnection(event, data) {
  method _onPeerDCOutgoingConnection (line 134) | _onPeerDCOutgoingConnection(event, data) {
  method _onPeerDCOutgoingConnectionError (line 149) | _onPeerDCOutgoingConnectionError(event, data) {
  method _onPeerP2PDisconnected (line 168) | _onPeerP2PDisconnected(event, data) {
  method _onPeerP2PFileInfo (line 179) | _onPeerP2PFileInfo(event, data) {
  method _onPeerP2PFileResponse (line 190) | _onPeerP2PFileResponse(event, data) {
  method _onPeerP2PFileCanceled (line 211) | _onPeerP2PFileCanceled(event, data) {
  method _onPeerP2PFileReceived (line 222) | _onPeerP2PFileReceived(event, data) {
  method _onPeerP2PFileSent (line 236) | _onPeerP2PFileSent(event, data) {

FILE: app/initializers/prerequisites.js
  function initialize (line 9) | function initialize(application) {

FILE: app/models/peer.js
  method init (line 12) | init(...args) {

FILE: app/models/user.js
  method serialize (line 4) | serialize() {

FILE: app/router.js
  class Router (line 4) | class Router extends EmberRouter {

FILE: app/routes/application.js
  method setupController (line 4) | setupController(controller) {
  method openModal (line 9) | openModal(modalName) {
  method closeModal (line 16) | closeModal() {

FILE: app/routes/error.js
  method renderTemplate (line 4) | renderTemplate(controller, error) {

FILE: app/routes/index.js
  method beforeModel (line 7) | beforeModel() {
  method model (line 15) | model() {
  method setupController (line 20) | setupController(ctrl, model) {
  method renderTemplate (line 69) | renderTemplate() {
  method willTransition (line 85) | willTransition() {

FILE: app/routes/room.js
  method model (line 6) | model(params) {
  method afterModel (line 11) | afterModel(model, transition) {
  method setupController (line 19) | setupController(ctrl, model) {
  method renderTemplate (line 26) | renderTemplate(ctrl) {

FILE: app/services/analytics.js
  method trackEvent (line 2) | trackEvent(name, parameters) {

FILE: app/services/avatar.js
  constant AVATARS (line 5) | const AVATARS = [
  constant PREFIXES (line 108) | const PREFIXES = [
  method get (line 170) | get() {

FILE: app/services/file.js
  function rm (line 40) | function rm(entry) {
  function finish (line 121) | function finish(link) {

FILE: lib/google-analytics/index.js
  method isDevelopingAddon (line 6) | isDevelopingAddon() {
  method contentFor (line 10) | contentFor(type, config) {

FILE: vendor/peer.js
  function EventEmitter (line 12) | function EventEmitter() {
  function g (line 53) | function g() {
  function setZeroTimeoutPostMessage (line 376) | function setZeroTimeoutPostMessage(fn) {
  function handleMessage (line 381) | function handleMessage(event) {
  function Peer (line 412) | function Peer(id, options) {
  function DataConnection (line 710) | function DataConnection(peer, provider, options) {
  function MediaConnection (line 893) | function MediaConnection(peer, provider, options) {
Condensed preview — 93 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (146K chars).
[
  {
    "path": ".editorconfig",
    "chars": 367,
    "preview": "# EditorConfig helps developers define and maintain consistent\n# coding styles between different editors and IDEs\n# edit"
  },
  {
    "path": ".ember-cli",
    "chars": 280,
    "preview": "{\n  /**\n    Ember CLI sends analytics information by default. The data is completely\n    anonymous, but there are times "
  },
  {
    "path": ".eslintignore",
    "chars": 240,
    "preview": "# unconventional js\n/blueprints/*/files/\n/vendor/\n\n# compiled output\n/dist/\n/tmp/\n\n# dependencies\n/bower_components/\n/no"
  },
  {
    "path": ".eslintrc.js",
    "chars": 1488,
    "preview": "const eslintPluginNode = require('eslint-plugin-node');\n\nmodule.exports = {\n  root: true,\n  parser: 'babel-eslint',\n  pa"
  },
  {
    "path": ".gitignore",
    "chars": 369,
    "preview": "# See https://help.github.com/ignore-files/ for more about ignoring files.\n\n# compiled output\n/dist/\n/tmp/\n\n# dependenci"
  },
  {
    "path": ".prettierignore",
    "chars": 139,
    "preview": "##########\n# Common\n##########\n\n# Duh\n.git/\n\n# Third party\n/node_modules/\n/vendor/\n\n# Build products\n/dist/\n/tmp/\n/cover"
  },
  {
    "path": ".template-lintrc.js",
    "chars": 208,
    "preview": "module.exports = {\n  extends: 'octane',\n\n  // TODO: enable these\n  rules: {\n    'no-action': false,\n    'no-curly-compon"
  },
  {
    "path": ".travis.yml",
    "chars": 306,
    "preview": "---\nlanguage: node_js\nnode_js:\n  - \"12\"\n\ndist: xenial\n\naddons:\n  chrome: stable\n\ncache:\n  yarn: true\n\nenv:\n  global:\n   "
  },
  {
    "path": ".watchmanconfig",
    "chars": 49,
    "preview": "{\n  \"ignore_dirs\": [\n    \"tmp\",\n    \"dist\"\n  ]\n}\n"
  },
  {
    "path": "Dockerfile",
    "chars": 190,
    "preview": "FROM node:14-buster\nRUN mkdir -p /srv/app\nWORKDIR /srv/app\nCOPY package.json yarn.lock ./\nRUN yarn --frozen-lockfile --n"
  },
  {
    "path": "LICENSE",
    "chars": 1078,
    "preview": "The MIT License\n\nCopyright (c) 2014-2024 Szymon Nowak\n\nPermission is hereby granted, free of charge, to any person obtai"
  },
  {
    "path": "Procfile",
    "chars": 20,
    "preview": "web: node server.js\n"
  },
  {
    "path": "Procfile.dev",
    "chars": 48,
    "preview": "server: node server.js\nweb: ember build --watch\n"
  },
  {
    "path": "README.md",
    "chars": 3323,
    "preview": "<picture>\n  <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://user-images.githubusercontent.com/9873/20013775"
  },
  {
    "path": "app/app.js",
    "chars": 652,
    "preview": "import Application from '@ember/application';\nimport Resolver from 'ember-resolver';\nimport loadInitializers from 'ember"
  },
  {
    "path": "app/components/circular-progress.hbs",
    "chars": 148,
    "preview": "<svg width=\"76\" height=\"76\" viewport=\"0 0 76 76\" style={{this.style}}>\n  <path class=\"break\" transform=\"translate(38, 38"
  },
  {
    "path": "app/components/circular-progress.js",
    "chars": 631,
    "preview": "import Component from '@glimmer/component';\nimport { htmlSafe } from '@ember/template';\n\nconst COLORS = {\n  blue: '0, 13"
  },
  {
    "path": "app/components/file-field.js",
    "chars": 655,
    "preview": "import TextField from '@ember/component/text-field';\nimport $ from 'jquery';\n\nexport default TextField.extend({\n  type: "
  },
  {
    "path": "app/components/modal-dialog.js",
    "chars": 257,
    "preview": "import Component from '@ember/component';\n\nexport default Component.extend({\n  actions: {\n    close() {\n      // This se"
  },
  {
    "path": "app/components/peer-avatar.js",
    "chars": 3433,
    "preview": "import Component from '@ember/component';\nimport { alias } from '@ember/object/computed';\nimport { later } from '@ember/"
  },
  {
    "path": "app/components/peer-widget.js",
    "chars": 4019,
    "preview": "import Component from '@ember/component';\nimport { computed } from '@ember/object';\nimport { alias, equal, gt } from '@e"
  },
  {
    "path": "app/components/popover-confirm.js",
    "chars": 617,
    "preview": "import Component from '@ember/component';\nimport { computed } from '@ember/object';\n\nexport default Component.extend({\n "
  },
  {
    "path": "app/components/room-url.js",
    "chars": 463,
    "preview": "import TextField from '@ember/component/text-field';\nimport $ from 'jquery';\n\nexport default TextField.extend({\n  classN"
  },
  {
    "path": "app/components/user-widget.js",
    "chars": 147,
    "preview": "import Component from '@ember/component';\n\nexport default Component.extend({\n  classNames: ['peer'],\n  classNameBindings"
  },
  {
    "path": "app/controllers/application.js",
    "chars": 821,
    "preview": "import Controller from '@ember/controller';\nimport { inject as service } from '@ember/service';\nimport { v4 as uuidv4 } "
  },
  {
    "path": "app/controllers/index.js",
    "chars": 6497,
    "preview": "import Controller, { inject as controller } from '@ember/controller';\nimport { alias } from '@ember/object/computed';\nim"
  },
  {
    "path": "app/helpers/is-equal.js",
    "chars": 145,
    "preview": "import { helper as buildHelper } from '@ember/component/helper';\n\nexport default buildHelper(([leftSide, rightSide]) => "
  },
  {
    "path": "app/index.html",
    "chars": 1268,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"I"
  },
  {
    "path": "app/initializers/prerequisites.js",
    "chars": 2365,
    "preview": "/* jshint -W030 */\nimport $ from 'jquery';\nimport { Promise } from 'rsvp';\nimport config from 'sharedrop/config/environm"
  },
  {
    "path": "app/models/peer.js",
    "chars": 1491,
    "preview": "import EmberObject, { observer } from '@ember/object';\nimport Evented, { on } from '@ember/object/evented';\n\nexport defa"
  },
  {
    "path": "app/models/user.js",
    "chars": 316,
    "preview": "import Peer from './peer';\n\nconst User = Peer.extend({\n  serialize() {\n    const data = {\n      uuid: this.uuid,\n      p"
  },
  {
    "path": "app/router.js",
    "chars": 354,
    "preview": "import EmberRouter from '@ember/routing/router';\nimport config from 'sharedrop/config/environment';\n\nexport default clas"
  },
  {
    "path": "app/routes/application.js",
    "chars": 445,
    "preview": "import Route from '@ember/routing/route';\n\nexport default Route.extend({\n  setupController(controller) {\n    controller."
  },
  {
    "path": "app/routes/error.js",
    "chars": 314,
    "preview": "import Route from '@ember/routing/route';\n\nexport default Route.extend({\n  renderTemplate(controller, error) {\n    const"
  },
  {
    "path": "app/routes/index.js",
    "chars": 2782,
    "preview": "import Route from '@ember/routing/route';\nimport $ from 'jquery';\n\nimport Room from '../services/room';\n\nexport default "
  },
  {
    "path": "app/routes/room.js",
    "chars": 897,
    "preview": "import IndexRoute from './index';\n\nexport default IndexRoute.extend({\n  controllerName: 'index',\n\n  model(params) {\n    "
  },
  {
    "path": "app/services/analytics.js",
    "chars": 170,
    "preview": "export default {\n  trackEvent(name, parameters) {\n    if (window.gtag && typeof window.gtag === 'function') {\n      wind"
  },
  {
    "path": "app/services/avatar.js",
    "chars": 2302,
    "preview": "import Service from '@ember/service';\nimport sample from 'lodash/sample';\nimport startCase from 'lodash/startCase';\n\ncon"
  },
  {
    "path": "app/services/file.js",
    "chars": 4206,
    "preview": "import { Promise } from 'rsvp';\n\nconst File = function (options) {\n  const self = this;\n\n  this.name = options.name;\n  t"
  },
  {
    "path": "app/services/room.js",
    "chars": 2509,
    "preview": "import $ from 'jquery';\n\n// TODO: use Ember.Object.extend()\nconst Room = function (name, firebaseRef) {\n  this._ref = fi"
  },
  {
    "path": "app/services/web-rtc.js",
    "chars": 8772,
    "preview": "// TODO:\n// - provide TURN server config once it's possible to create rooms with custom names\n// - use Ember.Object.exte"
  },
  {
    "path": "app/styles/app.sass",
    "chars": 309,
    "preview": "@import base/reset\n\n@import base/variables\n@import base/mixins\n@import base/element_defaults\n@import base/glyphicons_fil"
  },
  {
    "path": "app/styles/base/_element_defaults.sass",
    "chars": 378,
    "preview": "*, *::before, *::after\n  box-sizing: border-box\n\nhtml\n  height: 100%\n  font-family: $font-family\n  font-size: 10px\n\nbody"
  },
  {
    "path": "app/styles/base/_glyphicons_filetypes.sass",
    "chars": 7411,
    "preview": "/* GLYPHICONS FILETYPES 1.8 */\n\n@font-face\n  font-family: \"Glyphicons Filetypes\"\n  src: url(/assets/fonts/glyphicons/gly"
  },
  {
    "path": "app/styles/base/_mixins.sass",
    "chars": 437,
    "preview": "=ellipsis\n  overflow: hidden\n  white-space: nowrap\n  text-overflow: ellipsis\n\n=button_reset\n  margin: 0\n  padding: 0\n  d"
  },
  {
    "path": "app/styles/base/_reset.sass",
    "chars": 967,
    "preview": "a,abbr,acronym,address,applet,article,aside,audio,b,big,blockquote,body,canvas,caption,center,cite,code,dd,del,details,d"
  },
  {
    "path": "app/styles/base/_variables.sass",
    "chars": 75,
    "preview": "$font-family: \"Helvetica Neue\", sans-serif\n\n$blue: #0088cc\n$green: #a4c540\n"
  },
  {
    "path": "app/styles/layout/_content.sass",
    "chars": 445,
    "preview": ".l-content\n  position: relative\n  height: 100vh\n  min-height: 600px\n\n.visually-hidden\n  clip: rect(0 0 0 0)\n  clip-path:"
  },
  {
    "path": "app/styles/layout/_footer.sass",
    "chars": 754,
    "preview": ".l-footer\n  position: fixed\n  z-index: 200\n  bottom: 0\n  left: 0\n  right: 0\n  background-color: rgba(white,.6)\n  text-al"
  },
  {
    "path": "app/styles/layout/_header.sass",
    "chars": 1165,
    "preview": ".l-header\n  .navbar\n    position: fixed\n    z-index: 10\n    top: 0\n    left: 0\n    width: 100%\n    height: 60px\n    back"
  },
  {
    "path": "app/styles/layout/_media.sass",
    "chars": 1474,
    "preview": "@media (max-height: 520px)\n  .modal-body\n    height: auto\n    bottom: auto\n    margin: 15px auto\n\n@media (max-width: 768"
  },
  {
    "path": "app/styles/modules/_modal.sass",
    "chars": 1332,
    "preview": ".modal-overlay\n  position: fixed\n  z-index: 300\n  top: 0\n  bottom: 0\n  left: 0\n  right: 0\n  background-color: rgba(black"
  },
  {
    "path": "app/styles/modules/_modules.sass",
    "chars": 975,
    "preview": ".preloader\n  position: absolute\n  left: 0\n  right: 0\n  top: 0\n  bottom: 0\n  margin: auto\n  width: 324px\n  height: 56px\n "
  },
  {
    "path": "app/styles/modules/_popover.sass",
    "chars": 1280,
    "preview": "$popover-border-color: #c0c0c0\n\n.popover\n  position: absolute\n  bottom: 100%\n  left: 50%\n  transform: translateX(-50%)\n "
  },
  {
    "path": "app/styles/modules/_users.sass",
    "chars": 3930,
    "preview": "$user-size: 76px\n\n.user\n  user-select: none\n\n  .peer\n    position: absolute\n    left: 50%\n    bottom: 300px\n    width: $"
  },
  {
    "path": "app/templates/about-app.hbs",
    "chars": 2330,
    "preview": "{{#modal-dialog onClose=(action \"closeModal\" target=currentRoute)}}\n  <h2 class=\"logo\"><span>ShareDrop</span></h2>\n  <h3"
  },
  {
    "path": "app/templates/about-room.hbs",
    "chars": 896,
    "preview": "{{#modal-dialog onClose=(action \"closeModal\" target=currentRoute)}}\n  <h2 class=\"logo\"><span>ShareDrop</span></h2>\n  <h3"
  },
  {
    "path": "app/templates/about-you.hbs",
    "chars": 94,
    "preview": "ShareDrop lets you share files with others.\nOther people will see you as <b>{{you.label}}</b>."
  },
  {
    "path": "app/templates/application.hbs",
    "chars": 1402,
    "preview": "<header class=\"l-header\">\n  <nav class=\"navbar\">\n    <h1 class=\"logo\">\n      <span class=\"logo-title\">ShareDrop</span>\n "
  },
  {
    "path": "app/templates/components/modal-dialog.hbs",
    "chars": 199,
    "preview": "{{! template-lint-disable no-invalid-interactive }}\n<div class=\"modal-overlay\" {{action \"close\"}}></div>\n<div class=\"mod"
  },
  {
    "path": "app/templates/components/peer-widget.hbs",
    "chars": 2502,
    "preview": "{{! Sender related messages }}\n{{#if hasSelectedFile}}\n  {{#popover-confirm\n    onConfirm=(action \"sendFileTransferInqui"
  },
  {
    "path": "app/templates/components/popover-confirm.hbs",
    "chars": 454,
    "preview": "<div class=\"popover\">\n  <div class=\"popover-body\">\n    <div class=\"popover-icon\">\n      <i class={{iconClass}}></i>\n    "
  },
  {
    "path": "app/templates/components/user-widget.hbs",
    "chars": 259,
    "preview": "<div class=\"avatar\">\n  <img class=\"gravatar\" src={{user.avatarUrl}} alt={{users.label}} title=\"peer id: {{user.uuid}}\">\n"
  },
  {
    "path": "app/templates/errors/browser-unsupported.hbs",
    "chars": 196,
    "preview": "<div class=\"error\">\n  <p>We're really sorry, but your browser is not supported.<br>Please use the latest desktop or Andr"
  },
  {
    "path": "app/templates/errors/filesystem-unavailable.hbs",
    "chars": 211,
    "preview": "<div class=\"error\">\n  <p>Uh oh. Looks like there's some issue and we won't be able<br>to save your files.</p>\n  <p>If yo"
  },
  {
    "path": "app/templates/errors/popovers/connection-failed.hbs",
    "chars": 72,
    "preview": "It was not possible to establish direct connection with the other peer.\n"
  },
  {
    "path": "app/templates/errors/popovers/multiple-files.hbs",
    "chars": 134,
    "preview": "The files you have selected exceed the maximum allowed size of 200MB\n<br><br>\nTIP: You can send single files without siz"
  },
  {
    "path": "app/templates/index.hbs",
    "chars": 874,
    "preview": "<main class=\"l-content\">\n  <div class=\"user others\">\n    {{#each model as |peer|}}\n      {{peer-widget peer=peer hasCust"
  },
  {
    "path": "config/dotenv.js",
    "chars": 233,
    "preview": "module.exports = function () {\n  return {\n    clientAllowedKeys: ['FIREBASE_URL'],\n    // Fail build when there is missi"
  },
  {
    "path": "config/environment.js",
    "chars": 1302,
    "preview": "module.exports = function (environment) {\n  const ENV = {\n    modulePrefix: 'sharedrop',\n    environment,\n    rootURL: '"
  },
  {
    "path": "config/optional-features.json",
    "chars": 153,
    "preview": "{\n  \"application-template-wrapper\": false,\n  \"default-async-observers\": true,\n  \"jquery-integration\": true,\n  \"template-"
  },
  {
    "path": "config/targets.js",
    "chars": 193,
    "preview": "const browsers = [\n  'last 2 Chrome versions',\n  'last 2 Firefox versions',\n  'last 2 Safari versions',\n  'last 2 iOS ve"
  },
  {
    "path": "ember-cli-build.js",
    "chars": 1266,
    "preview": "/* eslint-env node */\nconst EmberApp = require('ember-cli/lib/broccoli/ember-app');\n\nmodule.exports = function (defaults"
  },
  {
    "path": "firebase_rules.json",
    "chars": 1051,
    "preview": "{\n    \"rules\": {\n        \"rooms\": {\n            \"$roomid\": {\n                // You can see people in the room only if y"
  },
  {
    "path": "lib/google-analytics/index.js",
    "chars": 572,
    "preview": "const { name } = require('./package');\n\nmodule.exports = {\n  name,\n\n  isDevelopingAddon() {\n    return true;\n  },\n\n  con"
  },
  {
    "path": "lib/google-analytics/package.json",
    "chars": 72,
    "preview": "{\n  \"name\": \"google-analytics\",\n  \"keywords\": [\n    \"ember-addon\"\n  ]\n}\n"
  },
  {
    "path": "newrelic.js",
    "chars": 535,
    "preview": "/**\n * New Relic agent configuration.\n *\n * See lib/config.defaults.js in the agent distribution for a more complete\n * "
  },
  {
    "path": "package.json",
    "chars": 2978,
    "preview": "{\n  \"name\": \"sharedrop\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"description\": \"P2P file sharing\",\n  \"license\": \"MIT"
  },
  {
    "path": "prettier.config.js",
    "chars": 255,
    "preview": "// https://prettier.io/docs/en/options.html\n\nmodule.exports = {\n  printWidth: 80,\n  tabWidth: 2,\n  useTabs: false,\n  sem"
  },
  {
    "path": "public/.gitkeep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "public/.well-known/brave-rewards-verification.txt",
    "chars": 147,
    "preview": "This is a Brave Rewards publisher verification file.\n\nDomain: sharedrop.io\nToken: d463c371edd92de3a09b0ccf1ce8143b9ee7f8"
  },
  {
    "path": "public/crossdomain.xml",
    "chars": 585,
    "preview": "<?xml version=\"1.0\"?>\n<!DOCTYPE cross-domain-policy SYSTEM \"http://www.adobe.com/xml/dtds/cross-domain-policy.dtd\">\n<cro"
  },
  {
    "path": "public/robots.txt",
    "chars": 51,
    "preview": "# http://www.robotstxt.org\nUser-agent: *\nDisallow:\n"
  },
  {
    "path": "server.js",
    "chars": 2446,
    "preview": "/* eslint-env node */\n\nif (process.env.NODE_ENV === 'production') {\n  // eslint-disable-next-line global-require\n  requi"
  },
  {
    "path": "sharedrop.crx",
    "chars": 318,
    "preview": "{\n  \"name\": \"ShareDrop\",\n  \"description\": \"Simple file sharing\",\n  \"version\": \"1\",\n  \"app\": {\n    \"urls\": [\n      \"https"
  },
  {
    "path": "testem.js",
    "chars": 574,
    "preview": "module.exports = {\n  test_page: 'tests/index.html?hidepassed',\n  disable_watching: true,\n  launch_in_ci: ['Chrome'],\n  l"
  },
  {
    "path": "tests/helpers/.gitkeep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/index.html",
    "chars": 1060,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n   "
  },
  {
    "path": "tests/integration/.gitkeep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/test-helper.js",
    "chars": 255,
    "preview": "/* eslint */\nimport { setApplication } from '@ember/test-helpers';\nimport { start } from 'ember-qunit';\nimport Applicati"
  },
  {
    "path": "tests/unit/.gitkeep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "vendor/.gitkeep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "vendor/peer.js",
    "chars": 35176,
    "preview": "/*! peerjs.js build:0.3.7, development. Copyright(c) 2013 Michelle Bu <michelle@michellebu.com> */\n(function(exports){\n/"
  }
]

About this extraction

This page contains the full source code of the ShareDropio/sharedrop GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 93 files (131.4 KB), approximately 37.8k tokens, and a symbol index with 86 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!