├── src ├── main │ ├── windows │ │ ├── index.js │ │ └── main.js │ ├── lib │ │ ├── Crypto.js │ │ ├── Server.js │ │ ├── util.js │ │ ├── Queue.js │ │ ├── Wormholes.js │ │ └── Peers.js │ ├── menu.js │ └── index.js ├── consts.js ├── config.js └── renderer │ ├── lib │ ├── util.js │ └── notifications.js │ ├── index.js │ └── components │ ├── Toolbar.js │ ├── WormholeModal.js │ ├── Wormholes.js │ ├── Drop.js │ └── App.js ├── static ├── fonts │ ├── ionicons.woff2 │ ├── ionicons4.woff2 │ └── ionicons5.woff2 ├── scss │ ├── themes │ │ ├── default │ │ │ ├── _index.scss │ │ │ ├── _light.scss │ │ │ └── _dark.scss │ │ ├── vibrancy │ │ │ ├── _index.scss │ │ │ ├── _dark.scss │ │ │ └── _light.scss │ │ └── _index.scss │ ├── components │ │ ├── Button.scss │ │ ├── Notification.scss │ │ ├── Modal.scss │ │ └── App.scss │ ├── index.scss │ └── ionicons.scss ├── index.html └── img │ ├── spacedrop-small.svg │ └── spacedrop.svg ├── .gitmodules ├── .editorconfig ├── .babelrc ├── .gitignore ├── scripts └── sass-loader.js ├── gulpfile.js ├── webpack.config.js ├── readme.md └── package.json /src/main/windows/index.js: -------------------------------------------------------------------------------- 1 | exports.main = require('./main') -------------------------------------------------------------------------------- /static/fonts/ionicons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hr/spacedrop/master/static/fonts/ionicons.woff2 -------------------------------------------------------------------------------- /static/fonts/ionicons4.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hr/spacedrop/master/static/fonts/ionicons4.woff2 -------------------------------------------------------------------------------- /static/fonts/ionicons5.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hr/spacedrop/master/static/fonts/ionicons5.woff2 -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/main/lib/simple-peer"] 2 | path = src/main/lib/simple-peer 3 | url = https://github.com/HR/simple-peer 4 | -------------------------------------------------------------------------------- /static/scss/themes/default/_index.scss: -------------------------------------------------------------------------------- 1 | @import 'dark'; 2 | @import 'light'; 3 | 4 | $theme: ( 5 | light: $lightTheme, 6 | dark: $darkTheme, 7 | ) -------------------------------------------------------------------------------- /static/scss/themes/vibrancy/_index.scss: -------------------------------------------------------------------------------- 1 | @import 'dark'; 2 | @import 'light'; 3 | 4 | $theme: ( 5 | light: $lightTheme, 6 | dark: $darkTheme, 7 | ) -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | quote_type = single 11 | max_line_length = 80 -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/env", "@babel/react"], 3 | "plugins": [ 4 | "@babel/plugin-proposal-class-properties", 5 | [ 6 | "@babel/plugin-transform-runtime", 7 | { 8 | "regenerator": true 9 | } 10 | ] 11 | ] 12 | } -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | SpaceDrop 7 | 8 | 9 |
10 | 11 | -------------------------------------------------------------------------------- /static/scss/themes/default/_light.scss: -------------------------------------------------------------------------------- 1 | $lightTheme: ( 2 | primaryColor: #e5e5ea, 3 | primaryForegroundColor: #111113, 4 | 5 | primaryBackgroundColor: #fafaff, 6 | secondaryBackgroundColor: #d1d1d6, 7 | thirdlyBackgroundColor: #c7c7cc, 8 | 9 | dangerColor: #dc3545, 10 | onlineColor: #01a452, 11 | ); -------------------------------------------------------------------------------- /src/consts.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** 3 | * Constants (static) 4 | */ 5 | 6 | module.exports = { 7 | DROP_TYPE: { 8 | DOWNLOAD: '↓', 9 | UPLOAD: '↑' 10 | }, 11 | DROP_STATUS: { 12 | DONE: 'done', 13 | PAUSED: 'paused', 14 | PENDING: 'pending', 15 | FAILED: 'failed' 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /static/scss/themes/default/_dark.scss: -------------------------------------------------------------------------------- 1 | $darkTheme: ( 2 | primaryColor: #2c2c2e, 3 | primaryForegroundColor: #f2f2f7, 4 | 5 | // primaryBackgroundColor: #1c1c1e, 6 | primaryBackgroundColor: #111113, 7 | secondaryBackgroundColor: #3a3a3c, 8 | thirdlyBackgroundColor: #48484a, 9 | 10 | dangerColor: #dc3545, 11 | onlineColor: #01a452 12 | ); -------------------------------------------------------------------------------- /static/scss/themes/vibrancy/_dark.scss: -------------------------------------------------------------------------------- 1 | $darkTheme: ( 2 | primaryColor: rgba(44,44,46, 0.8), 3 | 4 | primaryBackgroundColor: rgba(17,17,19, 0.8), 5 | 6 | primaryForegroundColor: rgba(242,242,247, 1), 7 | 8 | // primaryBackgroundColor: #1c1c1e, 9 | secondaryBackgroundColor: #3a3a3c, 10 | thirdlyBackgroundColor: #48484a, 11 | 12 | dangerColor: #dc3545, 13 | onlineColor: #01a452 14 | ) -------------------------------------------------------------------------------- /src/main/lib/Crypto.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** 3 | * Crypto class 4 | * Manages all keys and provides all crypto functionality 5 | */ 6 | 7 | const E2EE = require('./e2ee'), 8 | keytar = require('keytar'), 9 | SERVICE = 'spacedrop' 10 | 11 | module.exports = class Crypto extends E2EE { 12 | constructor (store) { 13 | const getSecretIdentity = publicKey => 14 | keytar.getPassword(SERVICE, publicKey) 15 | const setSecretIdentity = (publicKey, secretKey) => 16 | keytar.setPassword(SERVICE, publicKey, secretKey) 17 | 18 | super({ store, getSecretIdentity, setSecretIdentity }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /static/scss/themes/_index.scss: -------------------------------------------------------------------------------- 1 | @import 'default'; 2 | 3 | @mixin themify() { 4 | .theme-light { 5 | $internalTheme: map-get($theme, 'light') !global; 6 | @content; 7 | $internalTheme: null !global; 8 | } 9 | 10 | .theme-dark { 11 | $internalTheme: map-get($theme, 'dark') !global; 12 | @content; 13 | $internalTheme: null !global; 14 | } 15 | } 16 | 17 | @mixin shadow { 18 | box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 19 | 0 4px 6px -2px rgba(0, 0, 0, 0.05); 20 | } 21 | 22 | @function themed($key: 'primaryColor') { 23 | @return map-get($internalTheme, $key); 24 | } 25 | 26 | @function to-string($value) { 27 | @return inspect($value); 28 | } 29 | 30 | $horizontal-spacing: 0.8rem; 31 | $side-margin: 12px; 32 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** 3 | * Configuration 4 | */ 5 | 6 | const path = require('path'), 7 | { app } = require('electron'), 8 | { is } = require('electron-util') 9 | 10 | const CONFIG = { 11 | LOAD_URL: `file://${path.join(__dirname, '../app/index.html')}`, 12 | WS_URI: 'wss://spacedrop.xyz', 13 | DB_PATH: path.join(app.getPath('userData'), 'spacedrop.db'), 14 | DROPS_DIR: path.join(app.getPath('userData'), 'drops'), 15 | MAIN_WIN_WIDTH: 875, 16 | MAIN_WIN_HEIGHT: 500 17 | } 18 | 19 | const CONFIG_DEV = { 20 | ...CONFIG, 21 | LOAD_URL: 'http://localhost:9000', 22 | WS_URI: 'ws://localhost:7000', 23 | // MAIN_WIN_WIDTH: 875, 24 | MAIN_WIN_WIDTH: 420, 25 | MAIN_WIN_HEIGHT: 500 26 | } 27 | 28 | module.exports = is.development ? CONFIG_DEV : CONFIG 29 | -------------------------------------------------------------------------------- /static/scss/components/Button.scss: -------------------------------------------------------------------------------- 1 | @import '../themes'; 2 | 3 | @include themify() { 4 | button { 5 | display: inline-block; 6 | border: none; 7 | border-radius: 0.25rem; 8 | padding: 5px 20px; 9 | outline: none; 10 | margin: 0; 11 | text-decoration: none; 12 | background: themed('primaryColor'); 13 | color: themed('primaryForegroundColor'); 14 | font-size: 15px; 15 | font-weight: bold; 16 | text-align: center; 17 | transition: filter 250ms ease-in-out; 18 | } 19 | 20 | button:active, button:hover, button:focus { 21 | background: themed('primaryColor') !important; 22 | border: none; 23 | box-shadow: none !important; 24 | } 25 | 26 | button:active, button:focus { 27 | opacity: 0.6; 28 | } 29 | 30 | button:disabled { 31 | background: themed('primaryColor'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore build folder 2 | app/ 3 | 4 | # Logs 5 | logs 6 | *.log 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # node-waf configuration 23 | .lock-wscript 24 | 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/Release 27 | .eslintcache 28 | 29 | # Dependency directory 30 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 31 | node_modules 32 | 33 | # OSX 34 | .DS_Store 35 | 36 | # App packaged 37 | release 38 | dist 39 | dll 40 | *.map 41 | 42 | npm-debug.log.* 43 | *.css.d.ts 44 | *.sass.d.ts 45 | *.scss.d.ts 46 | 47 | 48 | 49 | e2ee -------------------------------------------------------------------------------- /static/scss/themes/vibrancy/_light.scss: -------------------------------------------------------------------------------- 1 | $lightTheme: ( 2 | primaryColor: #0084ff, 3 | 4 | primaryBackgroundColor: transparent, 5 | contentBackgroundColor: #fffe, 6 | secondaryBackgroundColor: #fff, 7 | transparentBackgroundColor: #fffe, 8 | overlayBackgroundColor: #eeea, 9 | 10 | primaryForegroundColor: #000000, 11 | primaryColorForegroundColor: #fff, 12 | secondaryForegroundColor: #555, 13 | mutedForegroundColor: #a5a5a5, 14 | 15 | highlightBorderColor: #eee, 16 | 17 | sidebarBackgroundColor: transparent, 18 | sidebarSelectedItemBackgroundColor: #fffe, 19 | 20 | modalBackgroundColor: #fffe, 21 | modalOverlayColor: #1119, 22 | modalShadowColor: #0003, 23 | 24 | toolbarButtonColor: #0084ff, 25 | buttonBackgroundColor: #0084ff, 26 | buttonForegroundColor: #fff, 27 | 28 | dangerColor: #dc3545, 29 | dangerForegroundColor: #fff, 30 | onlineColor: #01A452, 31 | ); -------------------------------------------------------------------------------- /scripts/sass-loader.js: -------------------------------------------------------------------------------- 1 | const path = require('path'), 2 | glob = require('glob') 3 | 4 | module.exports = { 5 | loader: 'sass-loader', 6 | options: { 7 | sassOptions: loaderContext => { 8 | const { resourcePath, rootContext } = loaderContext 9 | const dir = path.dirname(resourcePath) 10 | return { 11 | indentWidth: 4, 12 | webpackImporter: false, 13 | includePaths: ['static/scss'], 14 | importer: function (url, prev, done) { 15 | // Add support for sass @import globs 16 | const absUrl = path.join(dir, url) 17 | globs = absUrl.includes('*') && glob.sync(absUrl) 18 | 19 | if (globs) { 20 | const contents = globs 21 | .map(p => `@import '${p.replace(dir + path.sep, '')}';`) 22 | .join('\n') 23 | 24 | return { contents } 25 | } 26 | 27 | return null 28 | } 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const spawn = require('child_process').spawn 2 | const { watch } = require('gulp') 3 | var p 4 | 5 | const SRC_FILES = [ 6 | './src/main/*.js', 7 | './src/main/windows/*.js', 8 | './src/main/lib/*.js' 9 | ] 10 | const ELECTRON = __dirname + '/node_modules/.bin/electron' 11 | const DEBUG = false 12 | let args = ['.'] 13 | // Start the electron process. 14 | async function electron () { 15 | // kill previous spawned process 16 | if (p) { 17 | p.kill() 18 | } 19 | 20 | if (DEBUG) args.unshift('--inspect=5858') 21 | // `spawn` a child `gulp` process linked to the parent `stdio` 22 | p = await spawn(ELECTRON, args, { 23 | stdio: 'inherit', 24 | env: { 25 | ...process.env, 26 | DEBUG: 'simple-peer' 27 | } 28 | }) 29 | } 30 | 31 | exports.default = () => { 32 | watch( 33 | SRC_FILES, 34 | { 35 | queue: false, 36 | ignoreInitial: false // Execute task on startup 37 | }, 38 | electron 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /static/scss/components/Notification.scss: -------------------------------------------------------------------------------- 1 | @import '../themes'; 2 | 3 | @include themify() { 4 | .notification-container { 5 | position: absolute; 6 | z-index: 4200; 7 | bottom: 0; 8 | width: 100%; 9 | } 10 | 11 | .notification { 12 | margin: 0 auto; 13 | margin-bottom: 20px; 14 | width: 300px; 15 | background: themed('secondaryBackgroundColor'); 16 | backdrop-filter: blur(20px); 17 | text-align: left; 18 | font-size: 14px; 19 | overflow: hidden; 20 | border-radius: 0.25rem; 21 | padding: 10px 20px; 22 | font-weight: bold; 23 | display: flex; 24 | align-items: center; 25 | justify-content: space-between; 26 | justify-items: center; 27 | @include shadow; 28 | } 29 | 30 | .notification > .dismiss:hover { 31 | opacity: 0.8; 32 | } 33 | 34 | .notification > .dismiss { 35 | cursor: pointer; 36 | } 37 | 38 | .error.notification { 39 | background: themed('dangerColor'); 40 | color: themed('primaryForegroundColor'); 41 | 42 | > .dismiss { 43 | color: themed('primaryForegroundColor'); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /static/scss/components/Modal.scss: -------------------------------------------------------------------------------- 1 | @import '../themes'; 2 | 3 | @include themify() { 4 | .modal-dialog { 5 | margin: $side-margin; 6 | } 7 | 8 | .modal-content { 9 | margin: auto; 10 | border-radius: 0.25rem; 11 | padding: 5px; 12 | background: themed('primaryColor'); 13 | 14 | button, 15 | button:active, 16 | button:hover, 17 | button:focus { 18 | background: themed('primaryBackgroundColor') !important; 19 | } 20 | 21 | button.close, 22 | button.close:active, 23 | button.close:hover, 24 | button.close:focus { 25 | background: transparent !important; 26 | } 27 | 28 | label { 29 | font-size: 0.9rem; 30 | text-transform: uppercase; 31 | } 32 | 33 | input:disabled { 34 | filter: brightness(60%); 35 | } 36 | } 37 | 38 | .modal-header { 39 | border-bottom: none; 40 | } 41 | 42 | .modal-body input { 43 | background: themed('secondaryBackgroundColor'); 44 | color: themed('primaryForegroundColor'); 45 | border: none; 46 | } 47 | 48 | // .modal-body textarea::placeholder, 49 | // .modal-body input::placeholder { 50 | // color: themed('primaryForegroundColor'); 51 | // } 52 | 53 | .modal-footer { 54 | border-top: none; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /static/scss/index.scss: -------------------------------------------------------------------------------- 1 | @import '~bootstrap/scss/bootstrap'; 2 | 3 | @import 'components/App.scss'; 4 | @import 'components/Button.scss'; 5 | @import 'components/Modal.scss'; 6 | @import 'components/Notification.scss'; 7 | @import 'ionicons'; 8 | @import 'themes'; 9 | 10 | @include themify() { 11 | margin: 0; 12 | padding: 0; 13 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 14 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 15 | sans-serif; 16 | -webkit-font-smoothing: antialiased; 17 | background-color: themed('primaryBackgroundColor'); 18 | 19 | code { 20 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 21 | monospace; 22 | } 23 | 24 | * { 25 | -webkit-appearance: none; 26 | -webkit-tap-highlight-color: transparent; 27 | -webkit-touch-callout: none; 28 | -webkit-user-select: none; 29 | user-select: none; 30 | color: themed('primaryForegroundColor'); 31 | } 32 | 33 | *:focus { 34 | outline: none; 35 | } 36 | 37 | *::-webkit-scrollbar { 38 | width: 0 !important; 39 | } 40 | 41 | a, .clickable { 42 | cursor: pointer; 43 | } 44 | 45 | .spacedrop-logo { 46 | fill: themed('primaryForegroundColor'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/renderer/lib/util.js: -------------------------------------------------------------------------------- 1 | // Makes PGP error messages user friendly 2 | export function friendlyError (error) { 3 | return error.message.slice( 4 | error.message.lastIndexOf('Error'), 5 | error.message.length 6 | ) 7 | } 8 | // Makes a deep clone of an object 9 | export function clone (obj) { 10 | return JSON.parse(JSON.stringify(obj)) 11 | } 12 | 13 | // Generates a hash code for a string 14 | export function hashCode (string) { 15 | for (var i = 0, h = 0; i < string.length; i++) 16 | h = (Math.imul(31, h) + string.charCodeAt(i)) | 0 17 | return h 18 | } 19 | 20 | // Returns the initials of a name 21 | export function initialsise (name) { 22 | let iname = name.toUpperCase().split(' ') 23 | let initials = name[0] 24 | if (iname.length > 1) { 25 | initials += iname[iname.length - 1][0] 26 | } 27 | return initials 28 | } 29 | 30 | // Turns an object into a react clasaName compatible list 31 | export function classList (classes) { 32 | if (!Array.isArray(classes)) { 33 | // Turn into an array if not already 34 | classes = Object.entries(classes) 35 | .filter(entry => entry[1]) 36 | .map(entry => entry[0]) 37 | } 38 | return classes.join(' ') 39 | } 40 | 41 | // Checks if an object is empty 42 | export function isEmpty (obj) { 43 | return Object.keys(obj).length === 0 && obj.constructor === Object 44 | } 45 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebPackPlugin = require('html-webpack-plugin'), 2 | MiniCssExtractPlugin = require('mini-css-extract-plugin'), 3 | // sassLoader = require('./scripts/sass-loader.js'), 4 | path = require('path') 5 | 6 | module.exports = { 7 | entry: path.resolve(__dirname, 'src/renderer/index.js'), 8 | target: 'electron-renderer', 9 | output: { 10 | publicPath: '', 11 | path: path.resolve(__dirname, 'app'), 12 | filename: 'app.js' 13 | }, 14 | devtool: 'eval-cheap-source-map', 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.(js|jsx)$/, 19 | exclude: /node_modules/, 20 | use: ['babel-loader'] 21 | }, 22 | { 23 | test: /\.s[ac]ss$/i, 24 | use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'] 25 | }, 26 | { 27 | test: /\.html$/, 28 | use: { 29 | loader: 'html-loader' 30 | } 31 | }, 32 | { 33 | test: /\.(woff|woff2|eot|ttf|otf)$/, 34 | use: { 35 | loader: 'file-loader' 36 | } 37 | } 38 | ] 39 | }, 40 | plugins: [ 41 | new MiniCssExtractPlugin({ 42 | filename: 'app.css' 43 | }), 44 | new HtmlWebPackPlugin({ 45 | template: './static/index.html', 46 | filename: './index.html' 47 | }) 48 | ], 49 | devServer: { 50 | open: false, 51 | port: 9000 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/renderer/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './components/App' 4 | import { NotificationProvider } from './lib/notifications' 5 | import { darkMode } from 'electron-util' 6 | // Show dark mode if it is already enabled in the operating system 7 | let isDarkMode = darkMode.isEnabled 8 | 9 | // Sets the UI theme appropriately 10 | function setTheme () { 11 | if (isDarkMode) { 12 | // Use dark theme 13 | document.body.classList.remove('theme-light') 14 | document.body.classList.add('theme-dark') 15 | } else { 16 | // Use light theme 17 | document.body.classList.remove('theme-dark') 18 | document.body.classList.add('theme-light') 19 | } 20 | } 21 | 22 | // Switches the UI theme between light and dark 23 | function switchTheme () { 24 | isDarkMode = !isDarkMode 25 | setTheme() 26 | } 27 | 28 | // Add a shortcut to switch themes 29 | window.onkeyup = function (e) { 30 | // ctrl + t 31 | if (e.ctrlKey && e.key === 't') { 32 | switchTheme() 33 | } 34 | } 35 | 36 | // Blur after click 37 | document.addEventListener('click', function (e) { 38 | if (document.activeElement.toString() == '[object HTMLButtonElement]') { 39 | document.activeElement.blur() 40 | } 41 | }) 42 | 43 | // Set theme accordingly 44 | setTheme() 45 | 46 | // Render the entire UI 47 | ReactDOM.render( 48 | 49 | 50 | , 51 | document.getElementById('app') 52 | ) 53 | -------------------------------------------------------------------------------- /src/renderer/components/Toolbar.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Row, Col, Button } from 'react-bootstrap' 3 | import { classList } from '../lib/util' 4 | 5 | export default function Toolbar (props) { 6 | return ( 7 |
8 | 9 | SpaceDrop 10 | 11 | 12 | 13 | 21 | 28 | 29 | 30 | {/* 33 | */} 36 | 50 | 51 | 52 |
53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /src/renderer/components/WormholeModal.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import { Modal, Form, Button } from 'react-bootstrap' 3 | 4 | export default function WormholeModal (props) { 5 | const { state, type, disabledId, onSubmit, ...rprops } = props 6 | const initName = state ? state.name : '' 7 | const initID = state ? state.id : '' 8 | console.log(initName, initID) 9 | const [name, setName] = useState(initName) 10 | const [id, setID] = useState(initID) 11 | // Clear on dismissal 12 | useEffect(() => { 13 | setName(initName) 14 | setID(initID) 15 | }, [props.show]) 16 | 17 | return ( 18 | 24 | 25 | 26 | {type} a wormhole 27 | 28 | 29 | 30 | 31 | Name 32 | setName(event.target.value)} 37 | /> 38 | 39 | 40 | Spacedrop ID 41 | setID(event.target.value)} 47 | /> 48 | 49 | 50 | 51 | 52 | 53 | 54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /src/main/lib/Server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** 3 | * Signal class 4 | * Sends and receives signals from peers and signal events from server 5 | */ 6 | 7 | const WebSocket = require('ws'), 8 | EventEmitter = require('events'), 9 | { encode } = require('querystring'), 10 | { WS_URI } = require('../../config'), 11 | CONNECTED_STATE = 1 12 | 13 | module.exports = class Server extends EventEmitter { 14 | constructor () { 15 | // Ensure singleton 16 | if (!!Server.instance) { 17 | return Server.instance 18 | } 19 | 20 | // Call EventEmitter constructor 21 | super() 22 | 23 | this._id = null 24 | this._ws = null 25 | 26 | // Bindings 27 | this.connect = this.connect.bind(this) 28 | this._emit = this._emit.bind(this) 29 | this.send = this.send.bind(this) 30 | 31 | Server.instance = this 32 | } 33 | 34 | isConnected () { 35 | return this._ws.readyState === CONNECTED_STATE 36 | } 37 | 38 | setId(userId) { 39 | this._id = userId 40 | } 41 | 42 | // Connects to signal server 43 | connect (authRequest) { 44 | // Build ws uri with authentication querystring data 45 | const wsAuthURI = WS_URI + '?' + encode(authRequest) 46 | 47 | this._ws = new WebSocket(wsAuthURI) 48 | // Add event listeners 49 | this._ws.on('message', this._emit) 50 | this._ws.on('open', () => { 51 | this.emit('connect') 52 | }) 53 | this._ws.on('close', () => { 54 | this.emit('disconnect') 55 | }) 56 | this._ws.on('error', err => this.emit('error', err)) 57 | } 58 | 59 | // Sends signal to a peer (via server) 60 | send (type, extras = {}, cb) { 61 | const msg = JSON.stringify({ type, senderId: this._id, ...extras }) 62 | if (!cb) { 63 | return new Promise(resolve => this._ws.send(msg, null, resolve)) 64 | } 65 | this._ws.send(msg, null, cb) 66 | } 67 | 68 | // Emits a received signal event 69 | _emit (msg) { 70 | const { event, data } = JSON.parse(msg) 71 | console.log(`Signal event: ${event}`) 72 | this.emit(event, data) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/windows/main.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const main = (module.exports = { 4 | init, 5 | secondInstance, 6 | activate, 7 | send, 8 | win: null 9 | }) 10 | const { app, BrowserWindow } = require('electron'), 11 | { waitUntil } = require('../lib/util'), 12 | { LOAD_URL, MAIN_WIN_WIDTH, MAIN_WIN_HEIGHT } = require('../../config') 13 | 14 | // Create and initializes a new main window 15 | async function init () { 16 | let isWindowReady = false 17 | const win = (main.win = new BrowserWindow({ 18 | title: app.name, 19 | show: false, 20 | width: MAIN_WIN_WIDTH, 21 | height: MAIN_WIN_HEIGHT, 22 | minWidth: 700, 23 | minHeight: 400, 24 | // secondaryBackgroundColor: '#00FFFFFF', 25 | frame: false, 26 | transparent: true, 27 | // vibrancy: 'appearance-based', 28 | titleBarStyle: 'hiddenInset', 29 | webPreferences: { 30 | nodeIntegration: true, 31 | contextIsolation: false, 32 | enableRemoteModule: true 33 | // webSecurity: false // To allow local image loading 34 | } 35 | })) 36 | 37 | win.on('ready-to-show', () => { 38 | isWindowReady = true 39 | win.show() 40 | }) 41 | 42 | win.on('closed', () => { 43 | // Dereference the window 44 | // For multiple windows store them in an array 45 | main.win = undefined 46 | }) 47 | 48 | await win.loadURL(LOAD_URL) 49 | // Wait until window has loaded 50 | await waitUntil(() => isWindowReady, 6000) 51 | return win 52 | } 53 | 54 | // Handles second instance of window 55 | function secondInstance () { 56 | if (main.win) { 57 | // Show existing window if it already exists 58 | if (main.win.isMinimized()) { 59 | main.win.restore() 60 | } 61 | 62 | main.win.show() 63 | } 64 | } 65 | 66 | // Activates the window 67 | function activate () { 68 | if (!main.win) { 69 | // Create the main window if it doesn't exist already 70 | main.win = init() 71 | } 72 | } 73 | 74 | // Sends an IPC message to the renderer (the window) 75 | function send (channel, ...args) { 76 | if (main.win) { 77 | main.win.webContents.send(channel, ...args) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/lib/util.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | waitUntil, 5 | parseAddress, 6 | isEmpty, 7 | chunk, 8 | hexToUint8, 9 | strToUint8, 10 | Uint8ToHex, 11 | isString 12 | } 13 | 14 | // Wait until a condition is true 15 | function waitUntil (conditionFn, timeout, pollInterval = 30) { 16 | const start = Date.now() 17 | return new Promise((resolve, reject) => { 18 | ;(function wait () { 19 | if (conditionFn()) return resolve() 20 | else if (timeout && Date.now() - start >= timeout) 21 | return reject(new Error(`Timeout ${timeout} for waitUntil exceeded`)) 22 | else setTimeout(wait, pollInterval) 23 | })() 24 | }) 25 | } 26 | 27 | // Parses name/email address of format '[name] <[email]>' 28 | function parseAddress (address) { 29 | // Check if unknown 30 | address = address && address.length ? address[0] : 'Unknown' 31 | // Check if it has an email as well (follows the format) 32 | if (!address.includes('<')) { 33 | return { name: address } 34 | } 35 | 36 | let [name, email] = address.split('<').map(n => n.trim().replace(/>/g, '')) 37 | return { name, email } 38 | } 39 | 40 | // Checks if an object is empty 41 | function isEmpty (obj) { 42 | return Object.keys(obj).length === 0 && obj.constructor === Object 43 | } 44 | 45 | // Splits a buffer into chunks of a given size 46 | function chunk (buffer, chunkSize) { 47 | if (!Buffer.isBuffer(buffer)) throw new Error('Buffer is required') 48 | 49 | let result = [], 50 | i = 0, 51 | len = buffer.length 52 | 53 | while (i < len) { 54 | // If it does not equally divide then set last to whatever remains 55 | result.push(buffer.slice(i, Math.min((i += chunkSize), len))) 56 | } 57 | 58 | return result 59 | } 60 | 61 | // Converts a hex string into a Uint8Array 62 | function hexToUint8 (hex) { 63 | return Uint8Array.from(Buffer.from(hex, 'hex')) 64 | } 65 | 66 | // Converts a string into a Uint8Array 67 | function strToUint8 (hex) { 68 | return Uint8Array.from(Buffer.from(hex)) 69 | } 70 | 71 | function Uint8ToHex (uint8) { 72 | return Buffer.from(uint8).toString('hex') 73 | } 74 | 75 | // Checks if the given object is a string 76 | function isString (obj) { 77 | return typeof obj === 'string' || obj instanceof String 78 | } 79 | -------------------------------------------------------------------------------- /src/renderer/lib/notifications.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { hashCode } from './util' 3 | const Ctx = React.createContext() 4 | 5 | /** 6 | * Components 7 | *****************************/ 8 | 9 | const NotificationContainer = props => ( 10 |
11 | ) 12 | const Notification = ({ children, type, onDismiss, dismissable }) => ( 13 |
17 | {children} 18 | {dismissable && } 19 |
20 | ) 21 | 22 | /** 23 | * Provider 24 | *****************************/ 25 | 26 | export function NotificationProvider ({ children }) { 27 | const [notifications, setNotifications] = React.useState([]) 28 | 29 | // Dismiss a notification 30 | const dismiss = id => { 31 | const newNotifications = notifications.filter(n => n.id !== id) 32 | setNotifications(newNotifications) 33 | } 34 | 35 | // Dismiss 36 | const onDismiss = id => () => dismiss(id) 37 | 38 | // Clears all notifications 39 | const clear = () => { 40 | setNotifications([]) 41 | } 42 | 43 | // Show a notification 44 | const show = (content, type, dismissable = true, duration) => { 45 | type = type || '' 46 | const id = hashCode(content) 47 | const notification = { id, type, content, dismissable } 48 | setNotifications(notifications => { 49 | const alreadyShowing = notifications.find(n => n.id == id) 50 | // Do not show if already showing 51 | return alreadyShowing ? notifications : [notification, ...notifications] 52 | }) 53 | 54 | // Dismiss after duration if specified 55 | if (duration) setTimeout(onDismiss(id), duration) 56 | } 57 | 58 | return ( 59 | 60 | {children} 61 | 62 | {notifications.map(({ content, id, ...rest }) => ( 63 | 64 | {content} 65 | 66 | ))} 67 | 68 | 69 | ) 70 | } 71 | 72 | /** 73 | * Consumer 74 | *****************************/ 75 | export const useNotifications = isComponent => 76 | isComponent ? Ctx : React.useContext(Ctx) 77 | -------------------------------------------------------------------------------- /src/renderer/components/Wormholes.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react' 2 | import { Col, Row, Nav, Tab } from 'react-bootstrap' 3 | import { classList } from '../lib/util' 4 | import Drop from './Drop' 5 | 6 | export default function Wormholes (props) { 7 | const endRef = useRef(null) 8 | 9 | // Scrolls to the bottom 10 | function scrollToBottom () { 11 | endRef.current.scrollIntoView({ block: 'end', behavior: 'smooth' }) 12 | } 13 | 14 | // Scroll to the bottom 15 | // useEffect(scrollToBottom) 16 | 17 | // useEffect(() => ) 18 | 19 | const { wormholes } = props 20 | 21 | const tabs = wormholes.map(hole => ( 22 | 26 | 27 | {hole.name} 28 |
29 |
30 |
31 | )) 32 | 33 | const tabContent = wormholes.map(hole => { 34 | const drops = Object.values(hole.drops) 35 | return ( 36 | 37 | {drops.length ? ( 38 | drops.map(drop => ( 39 | props.onResumeClick(hole.id, drop.id)} 42 | onPauseClick={() => props.onPauseClick(hole.id, drop.id)} 43 | onDeleteClick={() => props.onDeleteClick(hole.id, drop.id)} 44 | {...drop} 45 | /> 46 | )) 47 | ) : ( 48 |
49 | 50 |
51 | Create a drop to send a file 52 |
53 | )} 54 |
55 | ) 56 | }) 57 | 58 | const tabContainer = ( 59 | 60 | 61 | 62 | 63 | 64 | 65 | {tabContent} 66 | 67 | 68 | 69 | ) 70 | 71 | return ( 72 |
73 | {wormholes.length ? ( 74 | tabContainer 75 | ) : ( 76 |
77 | 78 |
79 | Create a new wormhole to send files 80 |
81 | )} 82 |
83 | ) 84 | } 85 | -------------------------------------------------------------------------------- /src/main/lib/Queue.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** 3 | * Queue class 4 | * (Message) task queue 5 | */ 6 | const EventEmitter = require('events') 7 | 8 | module.exports = class Queue extends EventEmitter { 9 | constructor (timeout) { 10 | // Call EventEmitter constructor 11 | super() 12 | 13 | this._queue = [] 14 | this._pendingCount = 0 15 | this._processing = -1 16 | this._timeout = timeout 17 | // this._responseTimeout = responseTimeout 18 | this._idle = true 19 | } 20 | 21 | // Adds a task 22 | // TODO: Add timeout interval for task hangup 23 | add (fn, id) { 24 | let timer 25 | let removeWhenDone = true 26 | 27 | if (id) { 28 | // If an id not passed then remove when done (not tracked by caller) 29 | removeWhenDone = false 30 | } 31 | 32 | const run = async () => { 33 | console.log('Running task', id) 34 | 35 | try { 36 | // const promise = fn.apply(null, args) 37 | await fn() 38 | // Set timeout for response 39 | // timer = setTimeout(() => { 40 | // throw new Error('Timeout') 41 | // }, this._timeout) 42 | console.log('Finished task', id) 43 | } catch (error) { 44 | console.error(error) 45 | this._error(id, error) 46 | } 47 | 48 | // Trigger next when done 49 | this._next() 50 | } 51 | 52 | this._queue.push({ id, run, timer, removeWhenDone }) 53 | this._pendingCount++ 54 | 55 | if (this._idle) { 56 | // Start processing 57 | console.log('Idle, start', id) 58 | this._next() 59 | } 60 | } 61 | 62 | // Remove task by id 63 | remove (id) { 64 | const index = this._queue.findIndex(task => task.id === id) 65 | if (index < 0) return false 66 | return this._remove(index) 67 | } 68 | 69 | // Processes next task in queue 70 | async _next () { 71 | if (!this._pendingCount) return (this._idle = true) // Finished processing 72 | console.log(this._queue, this._processing, this._pendingCount) 73 | this._idle = false 74 | this._pendingCount-- 75 | if (this._processing > 0) { 76 | const { removeWhenDone } = this._queue[this._processing] 77 | if (removeWhenDone) this._remove(this._processing) 78 | } 79 | // Run task 80 | this._queue[++this._processing].run() 81 | } 82 | 83 | // Removes task at given position in queue 84 | _remove (index) { 85 | console.log('Removed task at index', index) 86 | // clearTimeout(this._queue[index].timer) 87 | return delete this._queue[index] 88 | } 89 | 90 | // When a task fails 91 | _error (id, error) { 92 | this.emit('error', id, error) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/lib/Wormholes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { DROP_STATUS } = require('../../consts'), 4 | { isEmpty } = require('./util'), 5 | DB_KEY = 'wormholes' 6 | 7 | module.exports = class Wormholes { 8 | constructor (store) { 9 | this._store = store 10 | this._wormholes = this._store.get(DB_KEY, {}) 11 | } 12 | 13 | // Wormhole ops 14 | 15 | getAll () { 16 | return this._wormholes 17 | } 18 | 19 | getList () { 20 | return Object.values(this._wormholes) 21 | } 22 | 23 | getActive () { 24 | const arr = Object.values(this._wormholes) 25 | if (!arr.length) return '' 26 | const online = arr.find(w => w.online) 27 | return (online && online.id) || arr[0].id 28 | } 29 | 30 | get (id) { 31 | return this._wormholes[id] 32 | } 33 | 34 | has (id) { 35 | return !!this._wormholes[id] 36 | } 37 | 38 | add (id, name) { 39 | this._wormholes[id] = { 40 | id, 41 | name, 42 | drops: {} 43 | } 44 | this._saveAll() 45 | } 46 | 47 | update (id, state) { 48 | Object.assign(this._wormholes[id], state) 49 | this._saveAll() 50 | } 51 | 52 | delete (id) { 53 | delete this._wormholes[id] 54 | this._saveAll() 55 | } 56 | 57 | getDropList (id) { 58 | return Object.entries(this._wormholes[id].drops) 59 | } 60 | 61 | // Gets a drop 62 | getDrop (id, dropId) { 63 | return this._wormholes[id].drops[dropId] 64 | } 65 | 66 | // Adds a drop 67 | addDrop (id, dropId, drop) { 68 | drop.status = DROP_STATUS.PENDING 69 | this._wormholes[id].drops[dropId] = drop 70 | this._saveAll() 71 | } 72 | 73 | // Deletes a drop 74 | deleteDrop (id, dropId) { 75 | delete this._wormholes[id].drops[dropId] 76 | this._saveAll() 77 | } 78 | 79 | // Find the wormhole id of a drop with its id 80 | findIdByDropId (dropId) { 81 | return Object.entries(this._wormholes).find(w => 82 | Object.entries(w.drops).includes(dropId) 83 | ) 84 | } 85 | 86 | // Updates a drop 87 | updateDrop (id, dropId, updates) { 88 | if (!this._wormholes[id] || isEmpty(updates)) return 89 | if (updates.eta === 0) updates.status = DROP_STATUS.DONE 90 | const drop = Object.assign(this._wormholes[id].drops[dropId], updates) 91 | this._saveAll() 92 | return drop 93 | } 94 | 95 | // Pause all pending drops 96 | pauseDrops () { 97 | for (var w in this._wormholes) { 98 | if (!this._wormholes.hasOwnProperty(w)) continue 99 | for (var d in this._wormholes[w].drops) { 100 | if ( 101 | !this._wormholes[w].drops.hasOwnProperty(d) || 102 | this._wormholes[w].drops[d].status !== DROP_STATUS.PENDING 103 | ) 104 | continue 105 | 106 | this._wormholes[w].drops[d].status = DROP_STATUS.PAUSED 107 | } 108 | } 109 | console.log('Pausing drops', JSON.stringify(this._wormholes)) 110 | this._saveAll() 111 | } 112 | 113 | clearDrops () { 114 | Object.entries(this._wormholes).map(k => (this._wormholes[k].drops = {})) 115 | } 116 | 117 | // Saves wormhole to the store 118 | _saveAll () { 119 | this._store.set(DB_KEY, this._wormholes) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /static/img/spacedrop-small.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Shape Copy 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /static/img/spacedrop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Shape 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/menu.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const { app, Menu, shell } = require('electron') 4 | const { DROPS_DIR } = require('../config') 5 | const { 6 | is, 7 | appMenu, 8 | aboutMenuItem, 9 | openUrlMenuItem, 10 | openNewGitHubIssue, 11 | debugInfo 12 | } = require('electron-util') 13 | 14 | const helpSubmenu = [ 15 | openUrlMenuItem({ 16 | label: 'Website', 17 | url: 'https://github.com/hr/spacedrop' 18 | }), 19 | openUrlMenuItem({ 20 | label: 'Source Code', 21 | url: 'https://github.com/hr/spacedrop' 22 | }), 23 | { 24 | label: 'Report an Issue…', 25 | click () { 26 | const body = ` 27 | 28 | 29 | 30 | --- 31 | 32 | ${debugInfo()}` 33 | 34 | openNewGitHubIssue({ 35 | user: 'hr', 36 | repo: 'spacedrop', 37 | body 38 | }) 39 | } 40 | } 41 | ] 42 | 43 | if (!is.macos) { 44 | helpSubmenu.push( 45 | { 46 | type: 'separator' 47 | }, 48 | aboutMenuItem({ 49 | icon: path.join(__dirname, 'build', 'icon.png'), 50 | text: 'Created by Habib Rehman' 51 | }) 52 | ) 53 | } 54 | 55 | const debugSubmenu = [ 56 | { 57 | label: 'Show App Data', 58 | click () { 59 | shell.openItem(app.getPath('userData')) 60 | } 61 | }, 62 | { 63 | type: 'separator' 64 | }, 65 | { 66 | label: 'Delete Drops', 67 | click () { 68 | app.emit('delete-drops') 69 | shell.moveItemToTrash(DROPS_DIR) 70 | } 71 | }, 72 | { 73 | label: 'Delete App Data', 74 | click () { 75 | shell.moveItemToTrash(app.getPath('userData')) 76 | app.relaunch() 77 | app.quit() 78 | } 79 | } 80 | ] 81 | 82 | const defaultTemplate = [ 83 | { 84 | role: 'fileMenu', 85 | submenu: [ 86 | { 87 | label: 'New Wormhole', 88 | click () { 89 | app.emit('create-wormhole') 90 | } 91 | }, 92 | { 93 | label: 'Send File...', 94 | click () { 95 | app.emit('render-event', 'create-drop') 96 | } 97 | }, 98 | { 99 | label: 'Copy Spacedrop ID', 100 | click () { 101 | app.emit('render-event', 'copy-id') 102 | } 103 | }, 104 | { 105 | type: 'separator' 106 | }, 107 | { 108 | id: 'connect-mothership', 109 | label: 'Connect Mothership', 110 | click () { 111 | app.emit('connect-mothership') 112 | } 113 | }, 114 | { 115 | id: 'open-wormholes', 116 | label: 'Open Wormholes', 117 | click () { 118 | app.emit('open-wormholes') 119 | } 120 | }, 121 | { 122 | type: 'separator' 123 | }, 124 | { 125 | role: 'close' 126 | } 127 | ] 128 | }, 129 | { 130 | role: 'editMenu' 131 | }, 132 | { 133 | role: 'viewMenu' 134 | }, 135 | { 136 | role: 'windowMenu' 137 | }, 138 | { 139 | role: 'help', 140 | submenu: helpSubmenu 141 | } 142 | ] 143 | 144 | const macosTemplate = [appMenu(), ...defaultTemplate] 145 | 146 | const template = process.platform === 'darwin' ? macosTemplate : defaultTemplate 147 | 148 | if (is.development) { 149 | template.push({ 150 | label: 'Debug', 151 | submenu: debugSubmenu 152 | }) 153 | } 154 | 155 | module.exports = Menu.buildFromTemplate(template) 156 | -------------------------------------------------------------------------------- /static/scss/components/App.scss: -------------------------------------------------------------------------------- 1 | @import '../themes'; 2 | 3 | @include themify() { 4 | .container { 5 | overflow: hidden; 6 | padding: 0; 7 | } 8 | 9 | .toolbar { 10 | padding: 10px $side-margin $side-margin $side-margin; 11 | 12 | .title { 13 | text-align: center; 14 | margin-bottom: 14px; 15 | font-size: 13px; 16 | vertical-align: text-top; 17 | } 18 | 19 | .profile { 20 | position: relative; 21 | 22 | .status { 23 | position: absolute; 24 | vertical-align: middle; 25 | margin-left: 8px; 26 | top: -2px; 27 | right: -2px; 28 | width: 8px; 29 | height: 8px; 30 | border-radius: 100%; 31 | background-color: themed('dangerColor'); 32 | } 33 | 34 | .status.online { 35 | background-color: themed('onlineColor'); 36 | } 37 | } 38 | 39 | .actions { 40 | padding: 0px; 41 | 42 | i { 43 | font-size: 16px; 44 | } 45 | 46 | .icon { 47 | height: 16px; 48 | } 49 | } 50 | 51 | .actions > .col:last-child { 52 | text-align: right; 53 | } 54 | 55 | button { 56 | margin: 0px 3px; 57 | } 58 | 59 | button:first-of-type { 60 | margin-left: 0; 61 | } 62 | 63 | button:last-of-type { 64 | margin-right: 0; 65 | } 66 | } 67 | 68 | .wormholes { 69 | .nav { 70 | flex-wrap: unset; 71 | overflow-x: auto !important; 72 | white-space: nowrap; 73 | margin: $side-margin; 74 | } 75 | 76 | .nav-link { 77 | &:not(.active):hover { 78 | opacity: 0.6; 79 | color: themed('primaryForegroundColor'); 80 | } 81 | 82 | &.active { 83 | background-color: themed('primaryColor'); 84 | color: themed('primaryForegroundColor'); 85 | font-weight: bold; 86 | } 87 | } 88 | 89 | .tab-content { 90 | overflow-y: auto; 91 | height: 69vh; 92 | } 93 | 94 | .noholes { 95 | margin: auto; 96 | text-align: center; 97 | opacity: 0.6; 98 | margin: 5rem; 99 | 100 | i { 101 | font-size: 7rem; 102 | } 103 | } 104 | 105 | .online { 106 | display: inline-block; 107 | vertical-align: middle; 108 | margin-left: 8px; 109 | width: 6px; 110 | height: 6px; 111 | border-radius: 100%; 112 | background-color: themed('onlineColor'); 113 | } 114 | 115 | .drop { 116 | margin: $side-margin; 117 | padding-top: $side-margin; 118 | padding-bottom: $side-margin; 119 | border-radius: 0.25rem; 120 | background: themed('primaryColor'); 121 | 122 | .progress { 123 | background: themed('secondaryBackgroundColor'); 124 | } 125 | 126 | .name-row { 127 | .name { 128 | font-weight: bold; 129 | font-size: 18px; 130 | white-space: nowrap; 131 | overflow: hidden; 132 | text-overflow: ellipsis; 133 | flex-grow: 3; 134 | } 135 | 136 | i { 137 | margin-left: 15px; 138 | cursor: pointer; 139 | } 140 | 141 | i:active, 142 | i:focus { 143 | opacity: 0.6; 144 | } 145 | } 146 | 147 | .status { 148 | margin-top: 4px; 149 | margin-bottom: 8px; 150 | font-size: 13px; 151 | font-weight: 300; 152 | } 153 | } 154 | 155 | .drop:first-child { 156 | margin-top: 0px; 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | Spacedrop 4 |
5 | SpaceDrop 6 |
7 |
8 |

9 | 10 |

A decentralized end-to-end encrypted file sharing app.

11 |

12 | 13 | Download latest release 15 | 16 |

17 | 18 | > Spacedrop > Airdrop 19 | 20 | A peer-to-peer end-to-end encrypted file sharing desktop app. Like _Airdrop_ 21 | but works over the internet rather than bluetooth/Wi-Fi, hence, _Spacedrop_. 22 | 23 | 24 | Implements the secure [signal 25 | protocol](https://signal.org/docs/specifications/doubleratchet/) to enable the 26 | end-to-end encryption of file transfers with authentication. 27 | 28 | 29 | More info at https://habibrehman.com/work/spacedrop 30 | 31 | 32 | 33 |
34 |
35 |

36 | 37 | 38 | 39 | 40 |

41 | 42 | ## Features 43 | - [x] Peer-to-peer file sharing 44 | - [x] End-to-end encryption 45 | - [x] Persistent Wormholes (tunnels/contacts) for recurring shares 46 | - [x] Pause/resume/cancel shares 47 | - [x] Dark Mode 48 | - [ ] Persist unfinished shares 49 | - [ ] Share files from file manager(s) 50 | - [ ] Wi-Fi/LAN file shares 51 | - [ ] Web-service 52 | 53 | You are welcome to open pull requests to help implement the features still to 54 | do! 55 | 56 | ## Install 57 | 58 | _macOS 10.10+, Linux, and Windows 7+ are supported (64-bit only)._ 59 | 60 | **macOS** 61 | 62 | [**Download**](https://github.com/hr/spacedrop/releases/latest) the `.dmg` file. 63 | 64 | **Linux** 65 | 66 | [**Download**](https://github.com/hr/spacedrop/releases/latest) the `.AppImage` or `.deb` file. 67 | 68 | _The AppImage needs to be [made executable](http://discourse.appimage.org/t/how-to-make-an-appimage-executable/80) after download._ 69 | 70 | **Windows** 71 | 72 | [**Download**](https://github.com/hr/spacedrop/releases/latest) the `.exe` file. 73 | 74 | 75 | ## Dev 76 | 77 | Needs the Spacedrop Server as well (https://github.com/HR/spacedrop-server/) 78 | 79 | ### Setup 80 | 81 | Clone the repos 82 | 83 | ``` 84 | $ git clone --recurse-submodules https://github.com/HR/spacedrop.git 85 | $ git clone https://github.com/HR/spacedrop-server.git 86 | ``` 87 | 88 | Install deps for both repos 89 | 90 | ``` 91 | $ yarn 92 | ``` 93 | 94 | ### Run 95 | 96 | For faster dev, run the bundler (webpack) 97 | 98 | ``` 99 | $ yarn run bundler 100 | ``` 101 | 102 | In a new tty, run the app 103 | 104 | ``` 105 | $ gulp 106 | ``` 107 | 108 | To test the app locally with another app, just run a second instance in a new 109 | tty 110 | 111 | ``` 112 | $ gulp 113 | ``` 114 | 115 | N.B. on macOS, you may be prompted to allow incoming connections everytime you 116 | run it. Unfortunately the only way to make that go away currently is to disable 117 | your firewall temporarily. 118 | 119 | ### Publish 120 | 121 | ``` 122 | $ npm run release 123 | ``` 124 | 125 | After Travis finishes building your app, open the release draft it created and 126 | click "Publish". 127 | 128 | 129 | <> with <3 by Habib Rehman 130 | -------------------------------------------------------------------------------- /src/renderer/components/Drop.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { basename } from 'path' 3 | import { classList } from '../lib/util' 4 | import { DROP_TYPE, DROP_STATUS } from '../../consts' 5 | import filesize from 'filesize' 6 | import { Row, Col, ProgressBar } from 'react-bootstrap' 7 | 8 | const remote = require('electron').remote 9 | const { shell } = remote 10 | 11 | // Select greatest time unit for eta 12 | function sec2time (sec) { 13 | const time = parseFloat(sec).toFixed(3), 14 | hrs = Math.floor(time / 60 / 60), 15 | mins = Math.floor(time / 60) % 60, 16 | secs = Math.floor(time - mins * 60) 17 | 18 | return ( 19 | (hrs && hrs + ' hours') || 20 | (mins && mins + ' minutes') || 21 | (secs && secs + ' seconds') 22 | ) 23 | } 24 | 25 | export default function Drop (props) { 26 | const { 27 | onResumeClick, 28 | onPauseClick, 29 | onDeleteClick, 30 | id, 31 | name, 32 | type, 33 | path, 34 | percentage, 35 | transferred, 36 | length, 37 | speed, 38 | eta, 39 | status 40 | } = props 41 | 42 | const isDownload = type === DROP_TYPE.DOWNLOAD 43 | const percent = Math.round(percentage || 0) 44 | const etaStr = sec2time(eta || 0) 45 | const tranStr = filesize(transferred || 0) 46 | const lenStr = filesize(length || 0) 47 | const speedStr = filesize(speed || 0) 48 | const typeStr = isDownload ? 'Received' : 'Sent' 49 | let statusStr, 50 | action, 51 | progressVariant = !isDownload && 'success', 52 | isDone = false 53 | 54 | switch (status) { 55 | case DROP_STATUS.DONE: 56 | statusStr = `${type} Finished` 57 | isDone = true 58 | break 59 | case DROP_STATUS.PAUSED: 60 | statusStr = `${type} Paused` 61 | action = !isDownload && ( 62 | 67 | ) 68 | break 69 | case DROP_STATUS.PENDING: 70 | statusStr = 71 | `${type} ${speedStr}/s ` + 72 | `- ${typeStr} ${tranStr} of ${lenStr} (${percent}%), ${etaStr} left` 73 | action = !isDownload && ( 74 | 79 | ) 80 | break 81 | case DROP_STATUS.FAILED: 82 | statusStr = `${type} Failed` 83 | isDone = true 84 | progressVariant = 'danger' 85 | break 86 | default: 87 | throw new Error('Unknown drop status') 88 | } 89 | 90 | return ( 91 | 92 | 93 | 94 | shell.openPath(path)} 98 | > 99 | {name} 100 | 101 | 102 | shell.showItemInFolder(path)} 105 | title={`Show ${name} in file manager`} 106 | /> 107 | {action} 108 | 113 | 114 | 115 | 116 | {statusStr} 117 | 118 | 119 | 120 | 126 | 127 | 128 | 129 | 130 | ) 131 | } 132 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spacedrop", 3 | "productName": "Spacedrop", 4 | "version": "0.0.1", 5 | "private": true, 6 | "description": "A decentralized end-to-end encrypted messaging app", 7 | "license": "MIT", 8 | "repository": "hr/spacedrop", 9 | "homepage": "./", 10 | "author": { 11 | "name": "Habib Rehman", 12 | "email": "h@rehman.email", 13 | "url": "https://github.com/HR" 14 | }, 15 | "main": "./src/main/index.js", 16 | "scripts": { 17 | "postinstall": "electron-builder install-app-deps", 18 | "rebuild": "electron-rebuild -f .", 19 | "test": "npm run lint", 20 | "start": "{ npm run bundle & gulp; }", 21 | "bundler": "BROWSER=none webpack serve --mode development --env development", 22 | "bundle": "webpack --mode production", 23 | "electron": "electron .", 24 | "pack": "electron-builder --dir", 25 | "build": "electron-builder --macos --linux --windows", 26 | "build:mac": "electron-builder -m", 27 | "build:lin": "electron-builder -l --x64 --ia32", 28 | "build:win": "electron-builder -w --x64 --ia32", 29 | "release": "np" 30 | }, 31 | "dependencies": { 32 | "bootstrap": "^4.6.0", 33 | "brake": "^1.0.1", 34 | "electron-context-menu": "^2.5.0", 35 | "electron-debug": "^3.2.0", 36 | "electron-store": "^7.0.2", 37 | "electron-unhandled": "^3.0.2", 38 | "electron-util": "^0.14.2", 39 | "filesize": "^6.1.0", 40 | "futoin-hkdf": "^1.3.3", 41 | "keytar": "^7.4.0", 42 | "moment": "^2.29.1", 43 | "progress-stream": "^2.0.0", 44 | "react": "^17.0.1", 45 | "react-bootstrap": "^1.5.1", 46 | "react-dom": "^17.0.1", 47 | "tweetnacl": "^1.0.3", 48 | "update-electron-app": "^2.0.1", 49 | "wrtc": "^0.4.7", 50 | "ws": "^7.4.3" 51 | }, 52 | "devDependencies": { 53 | "@babel/core": "^7.13.8", 54 | "@babel/plugin-proposal-class-properties": "^7.13.0", 55 | "@babel/plugin-transform-runtime": "^7.13.9", 56 | "@babel/preset-env": "^7.13.9", 57 | "@babel/preset-react": "^7.12.13", 58 | "@babel/runtime": "^7.13.9", 59 | "babel-loader": "^8.2.2", 60 | "css-loader": "^5.1.1", 61 | "electron": "^12.0.0", 62 | "electron-builder": "^22.10.5", 63 | "electron-rebuild": "^2.3.5", 64 | "file-loader": "^6.2.0", 65 | "glob": "^7.1.6", 66 | "gulp": "^4.0.2", 67 | "gulp-watch": "^5.0.1", 68 | "html-loader": "^2.1.1", 69 | "html-webpack-plugin": "^5.2.0", 70 | "mini-css-extract-plugin": "^1.3.9", 71 | "node-sass": "^5.0.0", 72 | "np": "^7.4.0", 73 | "sass-loader": "^11.0.1", 74 | "style-loader": "^2.0.0", 75 | "webpack": "^5.24.2", 76 | "webpack-cli": "^4.5.0", 77 | "webpack-dev-server": "^3.11.2" 78 | }, 79 | "eslintConfig": { 80 | "extends": "react-app" 81 | }, 82 | "xo": { 83 | "envs": [ 84 | "node", 85 | "browser" 86 | ] 87 | }, 88 | "np": { 89 | "publish": false, 90 | "releaseDraft": false 91 | }, 92 | "build": { 93 | "appId": "com.hr.spacedrop", 94 | "mac": { 95 | "category": "public.app-category.social-networking", 96 | "darkModeSupport": true 97 | }, 98 | "dmg": { 99 | "iconSize": 160, 100 | "contents": [ 101 | { 102 | "x": 180, 103 | "y": 170 104 | }, 105 | { 106 | "x": 480, 107 | "y": 170, 108 | "type": "link", 109 | "path": "/Applications" 110 | } 111 | ] 112 | }, 113 | "linux": { 114 | "target": [ 115 | "AppImage", 116 | "deb" 117 | ], 118 | "category": "Network;Chat" 119 | } 120 | }, 121 | "browserslist": { 122 | "production": [ 123 | ">0.2%", 124 | "not dead", 125 | "not op_mini all" 126 | ], 127 | "development": [ 128 | "last 1 chrome version", 129 | "last 1 firefox version", 130 | "last 1 safari version" 131 | ] 132 | }, 133 | "peerDependencies": { 134 | "bootstrap": "^4.6.0", 135 | "react-bootstrap": "^1.5.1" 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/renderer/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { ipcRenderer, remote, clipboard } from 'electron' 3 | import { Container } from 'react-bootstrap' 4 | import { useNotifications } from '../lib/notifications' 5 | import { clone } from '../lib/util' 6 | import Toolbar from './Toolbar' 7 | import Wormholes from './Wormholes' 8 | import WormholeModal from './WormholeModal' 9 | import '../../../static/scss/index.scss' 10 | 11 | const { dialog } = remote 12 | // Notification ref 13 | let notifications = null 14 | // Initial modal state used to reset modals 15 | const initModalsState = { 16 | createWormhole: false, 17 | editWormhole: false 18 | } 19 | // Root component 20 | export default class App extends React.Component { 21 | static contextType = useNotifications(true) 22 | constructor (props) { 23 | super(props) 24 | this.state = { 25 | online: false, 26 | identity: '', 27 | wormholes: [], 28 | active: '', 29 | ...clone(initModalsState) 30 | } 31 | 32 | // Bindings 33 | this.activate = this.activate.bind(this) 34 | this.updateState = this.updateState.bind(this) 35 | this.closeModals = this.closeModals.bind(this) 36 | this.openModal = this.openModal.bind(this) 37 | this.dropFileHandler = this.dropFileHandler.bind(this) 38 | this.createWormhole = this.createWormhole.bind(this) 39 | this.editWormhole = this.editWormhole.bind(this) 40 | this.copyIdentity = this.copyIdentity.bind(this) 41 | 42 | // Add event listeners 43 | ipcRenderer.on('open-modal', (event, ...args) => this.openModal(...args)) 44 | ipcRenderer.on('create-drop', this.dropFileHandler) 45 | ipcRenderer.on('copy-id', this.copyIdentity) 46 | ipcRenderer.on('update-state', this.updateState) 47 | } 48 | 49 | componentDidMount () { 50 | // Init notifications via the context 51 | notifications = this.context 52 | // Let main process show notifications 53 | ipcRenderer.on('notify', (event, ...args) => notifications.show(...args)) 54 | // Load state from main if not already loaded 55 | ipcRenderer.send('do-update-state') 56 | } 57 | 58 | activate (active) { 59 | ipcRenderer.send('activate-wormhole', active) 60 | this.setState({ active }) 61 | } 62 | 63 | // Updates internal state thereby updating the UI 64 | updateState (event, state, resetState) { 65 | let newState = { ...state } 66 | if (resetState) { 67 | // Reset state 68 | Object.assign(newState, clone(initModalsState)) 69 | notifications && notifications.clear() 70 | } 71 | console.log('updateState', newState, resetState) 72 | this.setState(newState, () => console.log('Updated state', this.state)) 73 | } 74 | 75 | // Closes all the modals 76 | closeModals () { 77 | this.setState({ 78 | ...clone(initModalsState) 79 | }) 80 | } 81 | 82 | // Shows the specified modal 83 | openModal (name, state) { 84 | console.log('Opening modal ' + name) 85 | let newModalState = clone(initModalsState) 86 | newModalState[name] = state ? state : true 87 | this.setState(newModalState) 88 | } 89 | 90 | // Handles sending a file 91 | async dropFileHandler () { 92 | if (!this.state.wormholes.length) 93 | return notifications.show('No wormhole', 'error', true, 3000) 94 | if (!this.state.active) 95 | return notifications.show('No wormhole selected', 'error', true, 3000) 96 | 97 | const wormhole = this.state.wormholes.find(w => w.id === this.state.active) 98 | if (!wormhole.online) 99 | return notifications.show( 100 | 'Wormhole closed (offline)', 101 | 'error', 102 | true, 103 | 3000 104 | ) 105 | const title = 'Select the file to send' 106 | // Filter based on type selected 107 | const filters = [{ name: 'All Files', extensions: ['*'] }] 108 | const { canceled, filePaths } = await dialog.showOpenDialog( 109 | remote.getCurrentWindow(), 110 | { 111 | properties: ['openFile'], 112 | title, 113 | filters 114 | } 115 | ) 116 | // Ignore if user cancelled 117 | if (canceled || !filePaths) return 118 | console.log(filePaths) 119 | ipcRenderer.send('drop', this.state.active, filePaths[0]) 120 | } 121 | 122 | createWormhole ({ id, name }) { 123 | if (!id || !name) 124 | return notifications.show('Details missing', 'error', true, 3000) 125 | 126 | const alreadyExists = this.state.wormholes.find(w => w.id === id) 127 | const isMe = this.state.identity === id 128 | if (alreadyExists || isMe) 129 | return notifications.show('Already added', 'error', true, 3000) 130 | 131 | // Show persistent composing notification 132 | notifications.show('Warping space-time...', null, false) 133 | 134 | ipcRenderer.send('create-wormhole', id, name) 135 | } 136 | 137 | editWormhole ({ id, name }) { 138 | if (!name) return notifications.show('Name missing', 'error', true, 3000) 139 | 140 | // Show persistent composing notification 141 | notifications.show('Rewarping space-time...', null, false) 142 | 143 | ipcRenderer.send('edit-wormhole', id, name) 144 | } 145 | 146 | copyIdentity (e) { 147 | clipboard.writeText(this.state.identity) 148 | notifications.show('Copied Spacedrop ID', null, true, 3000) 149 | if (e) e.preventDefault() 150 | } 151 | 152 | // Render the App UI 153 | render () { 154 | const activeWormholeOnline = Object.values(this.state.wormholes).find( 155 | w => w.id === this.state.active && w.online === true 156 | ) 157 | return ( 158 |
159 | this.closeModals()} 163 | onSubmit={this.createWormhole} 164 | /> 165 | this.closeModals()} 170 | onSubmit={this.editWormhole} 171 | disabledId={true} 172 | /> 173 | 174 | this.openModal('createWormhole')} 178 | onSendClick={this.dropFileHandler} 179 | onCopyIdentityClick={this.copyIdentity} 180 | /> 181 | 186 | ipcRenderer.send('resume-drop', ...args) 187 | } 188 | onPauseClick={(...args) => ipcRenderer.send('pause-drop', ...args)} 189 | onDeleteClick={(...args) => 190 | ipcRenderer.send('delete-drop', ...args) 191 | } 192 | /> 193 | 194 |
195 | ) 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/main/lib/Peers.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** 3 | * Peers class 4 | * Manages peer connections and communication 5 | */ 6 | 7 | const stream = require('stream'), 8 | EventEmitter = require('events'), 9 | path = require('path'), 10 | util = require('util'), 11 | fs = require('fs'), 12 | brake = require('brake'), 13 | wrtc = require('wrtc'), 14 | moment = require('moment'), 15 | progress = require('progress-stream'), 16 | Peer = require('./simple-peer'), 17 | Queue = require('./Queue'), 18 | { DROPS_DIR } = require('../../config'), 19 | // http://viblast.com/blog/2015/2/5/webrtc-data-channel-message-size/ 20 | DROP_CHUNK_SIZE = 16 * 1024, // (16kb) 21 | DROP_STREAM_RATE = 50, // ms 22 | DROP_STAT_INTERVAL = 800 // ms 23 | 24 | const { mkdir, stat } = fs.promises 25 | const pipeline = util.promisify(stream.pipeline) 26 | 27 | module.exports = class Peers extends EventEmitter { 28 | constructor (signal, crypto) { 29 | // Ensure singleton 30 | if (!!Peers.instance) { 31 | return Peers.instance 32 | } 33 | 34 | // Call EventEmitter constructor 35 | super() 36 | 37 | this._peers = {} 38 | this._requests = {} 39 | this._transfers = [] 40 | this._signal = signal 41 | this._crypto = crypto 42 | this._sendingQueue = new Queue() 43 | this._receivingQueue = new Queue() 44 | 45 | // Bindings 46 | this._addPeer = this._addPeer.bind(this) 47 | this._onSignalRequest = this._onSignalRequest.bind(this) 48 | this._onSignalAccept = this._onSignalAccept.bind(this) 49 | this._onSignal = this._onSignal.bind(this) 50 | this._onSignalReceiverOffline = this._onSignalReceiverOffline.bind(this) 51 | 52 | // Add queue event listeners 53 | this._sendingQueue.on('error', (...args) => 54 | this.emit('send-error', ...args) 55 | ) 56 | this._receivingQueue.on('error', (...args) => 57 | this.emit('receive-error', ...args) 58 | ) 59 | 60 | // Add signal event listeners 61 | this._signal.on('signal-request', this._onSignalRequest) 62 | this._signal.on('signal-accept', this._onSignalAccept) 63 | this._signal.on('signal', this._onSignal) 64 | this._signal.on('not-found', this._onSignalReceiverOffline) 65 | 66 | Peers.instance = this 67 | } 68 | 69 | // Connects to given peer 70 | connect (userId) { 71 | // Start connection 72 | const signalRequest = (this._requests[userId] = { 73 | receiverId: userId, 74 | timestamp: new Date().toISOString() 75 | }) 76 | // Send a signal request to peer 77 | this._signal.send('signal-request', signalRequest) 78 | console.log('Connecting with', userId) 79 | } 80 | 81 | // Disconnects from given peer 82 | disconnect (userId) { 83 | this._removePeer(userId) 84 | console.log('Disconnected from', userId) 85 | } 86 | 87 | // Checks if given peer is connected 88 | isConnected (id) { 89 | return this._peers.hasOwnProperty(id) 90 | } 91 | 92 | // Checks if transfer is in progress 93 | hasTransfer (id) { 94 | return this._transfers.includes(id) 95 | } 96 | 97 | // Queues a chat message to be sent to given peer 98 | send (dropId, ...args) { 99 | this._sendingQueue.add(() => this._sendFile(...args), dropId) 100 | } 101 | 102 | // Handles signal requests 103 | _onSignalRequest ({ senderId, timestamp }) { 104 | console.log('Signal request received') 105 | 106 | const request = this._requests[senderId] 107 | // If a request to the sender has not already been sent then just accept it 108 | // Add receiver to receive signal 109 | if (!request) { 110 | this._addReceiver(senderId) 111 | this._signal.send('signal-accept', { receiverId: senderId }) 112 | console.log('Signal request not sent to sender so accepted') 113 | return 114 | } 115 | 116 | // Parse request times 117 | const requestTime = moment(request.timestamp) 118 | const receivedRequestTime = moment(timestamp) 119 | 120 | // If received request was sent before own request then accept it 121 | // Add receiver to receive signal and forget own request 122 | // Avoids race condition when both peers send signal-requests 123 | if (receivedRequestTime.isBefore(requestTime)) { 124 | this._addReceiver(senderId) 125 | this._signal.send('signal-accept', { receiverId: senderId }) 126 | delete this._requests[senderId] 127 | console.log('Signal request sent before own so accepted') 128 | } 129 | 130 | // Otherwise don't do anything (wait for signal-accept as the sender) 131 | } 132 | 133 | // Handles accepted signal requests 134 | _onSignalAccept ({ senderId }) { 135 | console.log('Signal request accepted') 136 | // Start signalling 137 | this._addSender(senderId) 138 | delete this._requests[senderId] 139 | } 140 | 141 | // Handles new signals 142 | _onSignal ({ senderId, data }) { 143 | // Ensure peer to signal exists 144 | if (!this._peers[senderId]) { 145 | throw new Error(`Peer ${senderId} not yet added`) 146 | } 147 | 148 | this._peers[senderId].signal(data) 149 | } 150 | 151 | // Handles offline receivers 152 | _onSignalReceiverOffline ({ receiverId }) { 153 | if (this._requests[receiverId]) { 154 | console.log('Signal receiver offline') 155 | // Delete request to allow offline peer to connect if it comes online 156 | delete this._requests[receiverId] 157 | } 158 | } 159 | 160 | // Removes given peer by id 161 | _removePeer (id) { 162 | if (this._peers[id]) { 163 | this._peers[id].destroy() 164 | delete this._peers[id] 165 | } 166 | } 167 | 168 | _removeDrop (id) { 169 | this._transfers = this._transfers.filter(t => t !== id) 170 | } 171 | 172 | // Adds sender to initiate a connection with receiving peer 173 | _addSender (...args) { 174 | this._addPeer(true, ...args) 175 | } 176 | 177 | // Adds a receiver to Initiate a connection with sending peer 178 | _addReceiver (...args) { 179 | this._addPeer(false, ...args) 180 | } 181 | 182 | // Initiates a connection with the given peer and sets up communication 183 | _addPeer (initiator, userId) { 184 | const peer = (this._peers[userId] = new Peer({ 185 | initiator, 186 | wrtc, 187 | reconnectTimer: 1000 188 | })) 189 | const type = initiator ? 'Sender' : 'Receiver' 190 | 191 | peer.on('signal', data => { 192 | // Trickle signal data to the peer 193 | this._signal.send('signal', { 194 | receiverId: userId, 195 | data 196 | }) 197 | console.log(type, 'got signal and sent') 198 | }) 199 | 200 | peer.on('connect', async () => { 201 | // Initialises a chat session 202 | const keyMessage = await this._crypto.initSession(userId) 203 | // Send the master secret public key with signature to the user 204 | this._sendMessage('key', userId, keyMessage, false) 205 | 206 | this.emit('connect', userId, initiator) 207 | }) 208 | 209 | peer.on('close', () => { 210 | this._removePeer(userId) 211 | this.emit('disconnect', userId) 212 | }) 213 | 214 | peer.on('error', err => this.emit('error', userId, err)) 215 | 216 | peer.on('data', data => 217 | // Queue to receive 218 | this._receivingQueue.add(() => 219 | this._onMessage(userId, data.toString('utf8')) 220 | ) 221 | ) 222 | 223 | peer.on('datachannel', (datachannel, id) => 224 | // Queue to receive 225 | this._receivingQueue.add(() => 226 | this._onDataChannel(userId, datachannel, id) 227 | ) 228 | ) 229 | } 230 | 231 | // Handles new messages 232 | async _onMessage (userId, data) { 233 | // Try to deserialize message 234 | console.log('------> Got new message', data) 235 | const { type, ...message } = JSON.parse(data) 236 | 237 | if (type === 'key') { 238 | // Start a new crypto session with received key 239 | this._crypto.startSession(userId, message) 240 | return 241 | } 242 | } 243 | 244 | // Handles new data channels (drops/file streams) 245 | async _onDataChannel (userId, receivingStream, rawDrop) { 246 | console.log('------> Received a new drop (datachannel)', rawDrop) 247 | const encDrop = JSON.parse(rawDrop) 248 | let { message, decipher } = await this._crypto.decrypt( 249 | userId, 250 | encDrop, 251 | true 252 | ) 253 | // Ignore if validation failed 254 | if (!message) return 255 | const dropDir = path.join(DROPS_DIR, userId) 256 | // Recursively make media directory 257 | await mkdir(dropDir, { recursive: true }) 258 | const filePath = path.join(dropDir, message.name) 259 | console.log('Writing to', filePath) 260 | 261 | this.emit('drop', userId, { path: filePath, ...message }) 262 | 263 | const fileWriteStream = fs.createWriteStream(filePath) 264 | const tracker = progress({ 265 | length: message.size, 266 | time: DROP_STAT_INTERVAL 267 | }) 268 | 269 | tracker.on('progress', progress => 270 | this.emit('progress', userId, message.id, progress) 271 | ) 272 | // Stream content 273 | await pipeline(receivingStream, decipher, tracker, fileWriteStream) 274 | } 275 | 276 | // Sends a message to given peer 277 | _sendMessage (type, receiverId, message) { 278 | // TODO: Queue message if not connected / no session for later 279 | if (!this.isConnected(receiverId)) return false 280 | 281 | const peer = this._peers[receiverId] 282 | 283 | const serializedMessage = JSON.stringify({ 284 | type, 285 | ...message 286 | }) 287 | 288 | // Simply send message if no file to stream 289 | peer.write(serializedMessage) 290 | console.log(type, 'sent', message) 291 | } 292 | 293 | // Sends a file to given peer 294 | async _sendFile (receiverId, drop, filePath) { 295 | // TODO: Queue message if not connected / no session for later 296 | if (!this.isConnected(receiverId)) return false 297 | 298 | const peer = this._peers[receiverId] 299 | 300 | if (!drop.size) { 301 | // Add size 302 | const { size } = await stat(filePath) 303 | drop.size = size 304 | } 305 | 306 | // Encrypt message 307 | const {packet, cipher} = await this._crypto.encrypt(receiverId, drop, true) 308 | 309 | const serializedDrop = JSON.stringify(packet) 310 | 311 | // Stream file 312 | console.log('Streaming', drop, filePath) 313 | // Resume transfer is drop already exists 314 | let opts = drop.transferred ? { start: drop.transferred } : {} 315 | const fileReadStream = fs.createReadStream(filePath, opts) 316 | const sendingStream = peer.createDataChannel(serializedDrop) 317 | const tracker = progress({ 318 | length: drop.size, 319 | time: DROP_STAT_INTERVAL 320 | }) 321 | 322 | tracker.on('progress', progress => 323 | this.emit('progress', receiverId, drop.id, progress) 324 | ) 325 | 326 | this._transfers.push(drop.id) 327 | 328 | const pipe = pipeline( 329 | fileReadStream, 330 | cipher, 331 | // Throttle stream (backpressure) 332 | brake(DROP_CHUNK_SIZE, { period: DROP_STREAM_RATE }), 333 | tracker, 334 | sendingStream 335 | ) 336 | 337 | this.on('pause-drop', dropId => { 338 | if (dropId === drop.id) { 339 | console.log('Pausing drop', drop) 340 | fileReadStream.unpipe(cipher) 341 | console.log('Stream paused?', fileReadStream.isPaused()) 342 | } 343 | }) 344 | 345 | this.on('resume-drop', dropId => { 346 | if (dropId === drop.id) { 347 | console.log('Resuming drop', drop) 348 | fileReadStream.pipe(cipher) 349 | } 350 | }) 351 | 352 | this.on('destroy-drop', dropId => { 353 | if (dropId === drop.id) { 354 | pipe.destroy() 355 | } 356 | }) 357 | 358 | await pipe 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /src/main/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** 3 | * Main App 4 | *****************************/ 5 | const { app, Menu, ipcMain, screen } = require('electron'), 6 | { basename } = require('path'), 7 | { is, openNewGitHubIssue, debugInfo } = require('electron-util'), 8 | debug = require('electron-debug'), 9 | unhandled = require('electron-unhandled'), 10 | contextMenu = require('electron-context-menu'), 11 | packageJson = require('../../package.json'), 12 | { DROP_TYPE, DROP_STATUS } = require('../consts'), 13 | Store = require('electron-store'), 14 | Crypto = require('./lib/Crypto'), 15 | Server = require('./lib/Server'), 16 | Peers = require('./lib/Peers'), 17 | Wormholes = require('./lib/Wormholes'), 18 | menu = require('./menu'), 19 | windows = require('./windows') 20 | let db = 'launch', 21 | secondWin = false 22 | 23 | unhandled({ 24 | reportButton: error => { 25 | openNewGitHubIssue({ 26 | user: 'hr', 27 | repo: 'spacedrop', 28 | body: `\`\`\`\n${error.stack}\n\`\`\`\n\n---\n\n${debugInfo()}` 29 | }) 30 | } 31 | }) 32 | // debug() 33 | app.setAppUserModelId(packageJson.build.appId) 34 | 35 | if (!is.development) { 36 | // Prevent multiple instances of the app 37 | if (!app.requestSingleInstanceLock()) { 38 | app.quit() 39 | } 40 | 41 | // Someone tried to run a second instance, so focus the main window 42 | app.on('second-instance', windows.main.secondInstance) 43 | } else { 44 | // Allow multiple instances of the app in dev 45 | if (!app.requestSingleInstanceLock()) { 46 | console.info('Second instance') 47 | db += '2' 48 | secondWin = true 49 | } 50 | } 51 | 52 | app.on('window-all-closed', () => { 53 | if (!is.macos) { 54 | app.quit() 55 | } 56 | }) 57 | 58 | app.on('activate', windows.main.activate) 59 | 60 | /** 61 | * Main 62 | *****************************/ 63 | ;(async () => { 64 | console.log('\n\n\n**********************************************> New run\n') 65 | 66 | await app.whenReady() 67 | Menu.setApplicationMenu(menu) 68 | 69 | const store = new Store({ name: db }) 70 | const crypto = new Crypto(store) 71 | const server = new Server() 72 | const peers = new Peers(server, crypto) 73 | const wormholes = new Wormholes(store) 74 | 75 | initContextMenu() 76 | 77 | /** 78 | * App events 79 | *****************************/ 80 | app.on('will-quit', () => { 81 | console.log('Clnosing Spacedrop...') 82 | wormholes.pauseDrops() 83 | }) 84 | 85 | app.on('delete-drops', () => { 86 | wormholes.clearDrops() 87 | updateState() 88 | }) 89 | app.on('connect-mothership', () => connectMothership()) 90 | app.on('open-wormholes', () => { 91 | if (!server.isConnected()) 92 | notifyError(`Connect with Mothership to open wormholes`) 93 | openWormholes() 94 | }) 95 | app.on('create-wormhole', () => 96 | windows.main.send('open-modal', 'createWormhole') 97 | ) 98 | app.on('render-event', event => { 99 | windows.main.send(event) 100 | }) 101 | 102 | /** 103 | * Server events 104 | *****************************/ 105 | server.on('connect', () => { 106 | openWormholes() 107 | updateState({ online: true }) 108 | }) 109 | server.on('disconnect', () => { 110 | notifyError('Connection with Mothership failed') 111 | updateState({ online: false }) 112 | }) 113 | server.on('error', err => { 114 | if (err.code !== 'ECONNREFUSED') 115 | notifyError(`Connection error: ${err.message}`) 116 | }) 117 | 118 | /** 119 | * IPC events 120 | *****************************/ 121 | // When a wormhole is created by user 122 | ipcMain.on('create-wormhole', createWormholeHandler) 123 | ipcMain.on('edit-wormhole', editWormholeHandler) 124 | // When a wormhole is selected by user 125 | ipcMain.on('activate-wormhole', (e, id) => store.set('state.lastActive', id)) 126 | // When a file is sent by user 127 | ipcMain.on('drop', dropHandler) 128 | ipcMain.on('resume-drop', resumeDropHandler) 129 | ipcMain.on('pause-drop', pauseDropHandler) 130 | ipcMain.on('delete-drop', deleteDropHandler) 131 | 132 | /** 133 | * Peers events 134 | *****************************/ 135 | // When a new connection with a user is established 136 | peers.on('connect', peerConnectHandler) 137 | // When a connection with a user is closed 138 | peers.on('disconnect', peerDisconnectHandler) 139 | // When a connection error with a user occurs 140 | peers.on('error', peerErrorHandler) 141 | // When a new message from a user is received 142 | peers.on('drop', peerDropHandler) 143 | // When a new progress update for a drop is received 144 | peers.on('progress', peerDropProgressHandler) 145 | // Failed to send a drop 146 | peers.on('send-error', peerDropSendFailHandler) 147 | // Failed to receive a drop 148 | peers.on('receive-error', peerDropReceiveFailHandler) 149 | 150 | /** 151 | * Init 152 | *****************************/ 153 | // Init crypto and main window 154 | const [identity] = await Promise.all([crypto.init(), windows.main.init()]) 155 | console.log(identity) 156 | 157 | // TODO: remove in prod 158 | if (secondWin) { 159 | const displays = screen.getAllDisplays() 160 | const display = displays[displays.length - 1] 161 | console.log(display) 162 | const { x, y, width, height } = display.bounds 163 | const win = windows.main.win.getBounds() 164 | windows.main.win.setPosition( 165 | Math.round(x + (width - win.width) / 2), 166 | Math.round(y + (height - win.height) / 2) 167 | ) 168 | } 169 | 170 | let lastActive = store.get('state.lastActive', false) 171 | 172 | if (!lastActive) { 173 | lastActive = wormholes.getActive() 174 | store.set('state.lastActive', lastActive) 175 | } 176 | 177 | // Populate UI 178 | updateState({ identity: identity.publicKey, active: lastActive }) 179 | ipcMain.on('do-update-state', () => 180 | updateState({ 181 | identity: identity.publicKey, 182 | active: store.get('state.lastActive', '') 183 | }) 184 | ) 185 | 186 | server.setId(identity.publicKey) 187 | // Establish connection with the Mothership (signalling server) 188 | connectMothership() 189 | 190 | /** 191 | * Handlers 192 | *****************************/ 193 | 194 | function initContextMenu () { 195 | contextMenu({ 196 | menu: actions => [], 197 | prepend: (defaultActions, params, browserWindow) => { 198 | console.log('Params:', params) 199 | const wormholeId = 200 | params.linkURL.includes('#wormhole') && 201 | params.linkURL.split('#').pop() 202 | return [ 203 | { 204 | label: 'Edit wormhole', 205 | // Only show it when right-clicking text 206 | visible: wormholeId, 207 | click: () => 208 | windows.main.send( 209 | 'open-modal', 210 | 'editWormhole', 211 | wormholes.get(wormholeId) 212 | ) 213 | }, 214 | { 215 | label: 'Delete wormhole', 216 | // Only show it when right-clicking text 217 | visible: wormholeId, 218 | click: () => { 219 | // Cancel all pending drops 220 | wormholes 221 | .getDropList(wormholeId) 222 | .filter(drop => drop.status === DROP_STATUS.PENDING) 223 | .forEach(drop => peers.emit('destroy-drop', drop.id)) 224 | wormholes.delete(wormholeId) 225 | updateState() 226 | console.log('Deleted wormhole', wormholeId) 227 | } 228 | } 229 | ] 230 | } 231 | }) 232 | } 233 | 234 | function notifyError (message) { 235 | windows.main.send('notify', message, 'error', true, 4000) 236 | } 237 | 238 | async function connectMothership () { 239 | const authRequest = crypto.generateAuthRequest() 240 | server.connect(authRequest) 241 | } 242 | 243 | function openWormholes () { 244 | wormholes 245 | .getList() 246 | .filter(wormhole => !peers.isConnected(wormhole.id)) // Ignore ones already connecting to 247 | .forEach(wormhole => peers.connect(wormhole.id)) 248 | } 249 | 250 | /* IPC handlers */ 251 | function updateState (extraState, reset = false) { 252 | let state = { 253 | wormholes: wormholes 254 | .getList() 255 | .map(w => Object.assign(w, { online: peers.isConnected(w.id) })) 256 | } 257 | if (extraState) Object.assign(state, extraState) 258 | console.log(state) 259 | windows.main.send('update-state', state, reset) 260 | } 261 | 262 | function createWormholeHandler (event, id, name) { 263 | wormholes.add(id, name) 264 | updateState(null, true) 265 | if (!server.isConnected()) 266 | return notifyError('Connect with Mothership to open wormhole') 267 | // open wormhole 268 | peers.connect(id) 269 | } 270 | 271 | function editWormholeHandler (event, id, name) { 272 | wormholes.update(id, { name }) 273 | updateState(null, true) 274 | } 275 | 276 | async function dropHandler (event, id, filePath) { 277 | if (!peers.isConnected(id)) return notifyError('Wormhole closed (offline)') 278 | // Construct message 279 | let drop = { 280 | name: basename(filePath), 281 | timestamp: Date.now() 282 | } 283 | 284 | // Set the id of the message to its hash 285 | drop.id = crypto.hash(JSON.stringify(drop)) 286 | console.log('Dropping ', drop, ' to ', id) 287 | // Optimistically update UI 288 | wormholes.addDrop(id, drop.id, { 289 | type: DROP_TYPE.UPLOAD, 290 | path: filePath, 291 | ...drop 292 | }) 293 | updateState() 294 | 295 | // Send the message 296 | peers.send(drop.id, id, drop, filePath) 297 | } 298 | 299 | function peerDropSendFailHandler (dropId) { 300 | const wId = wormholes.findIdByDropId(dropId) 301 | const drop = wormholes.updateDrop(wId, dropId, { 302 | status: DROP_STATUS.FAILED 303 | }) 304 | notifyError(`Failed to send ${drop.name} :(`) 305 | updateState() 306 | } 307 | 308 | function peerDropReceiveFailHandler (dropId) { 309 | const wId = wormholes.findIdByDropId(dropId) 310 | const drop = wormholes.updateDrop(wId, dropId, { 311 | status: DROP_STATUS.FAILED 312 | }) 313 | notifyError(`Failed to receive ${drop.name} :(`) 314 | updateState() 315 | } 316 | 317 | function pauseDropHandler (e, holeId, dropId) { 318 | peers.emit('pause-drop', dropId) 319 | wormholes.updateDrop(holeId, dropId, { status: DROP_STATUS.PAUSED }) 320 | updateState() 321 | } 322 | 323 | function resumeDropHandler (e, holeId, dropId) { 324 | if (!peers.isConnected(holeId)) 325 | return notifyError('Wormhole closed (offline)') 326 | 327 | if (!peers.hasTransfer(dropId)) { 328 | // Re-initiate transfer 329 | const drop = wormholes.getDrop(holeId, dropId) 330 | peers.send(dropId, holeId, drop, drop.path) 331 | } else { 332 | peers.emit('resume-drop', dropId) 333 | } 334 | 335 | wormholes.updateDrop(holeId, dropId, { status: DROP_STATUS.PENDING }) 336 | updateState() 337 | } 338 | 339 | function deleteDropHandler (e, holeId, dropId) { 340 | console.log('Deleting', holeId, dropId) 341 | peers.emit('destroy-drop', dropId) 342 | wormholes.deleteDrop(holeId, dropId) 343 | updateState() 344 | } 345 | 346 | /* Peers handlers */ 347 | function peerConnectHandler (userId) { 348 | console.log('Connected with', userId) 349 | 350 | // New wormhole 351 | if (!wormholes.has(userId)) { 352 | wormholes.add(userId, userId.slice(0, 6) + '...') 353 | } 354 | 355 | // Update UI 356 | updateState() 357 | } 358 | function peerDisconnectHandler (userId) { 359 | console.log('Disconnected with', userId) 360 | // Update UI 361 | updateState() 362 | } 363 | function peerErrorHandler (userId, err) { 364 | console.log('Error connecting with peer', userId) 365 | console.error(err) 366 | } 367 | function peerDropHandler (senderId, drop) { 368 | console.log('Got drop', drop) 369 | wormholes.addDrop(senderId, drop.id, { 370 | type: DROP_TYPE.DOWNLOAD, 371 | ...drop 372 | }) 373 | updateState() 374 | } 375 | 376 | function peerDropProgressHandler (receiverId, dropId, progress) { 377 | // console.log('Got progress', receiverId, dropId, progress) 378 | wormholes.updateDrop(receiverId, dropId, progress) 379 | updateState() 380 | } 381 | })() 382 | -------------------------------------------------------------------------------- /static/scss/ionicons.scss: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | /*! 3 | Ionicons, v4.6.3 4 | Created by Ben Sperry for the Ionic Framework, http://ionicons.com/ 5 | https://twitter.com/benjsperry https://twitter.com/ionicframework 6 | MIT License: https://github.com/driftyco/ionicons 7 | 8 | Android-style icons originally built by Google’s 9 | Material Design Icons: https://github.com/google/material-design-icons 10 | used under CC BY http://creativecommons.org/licenses/by/4.0/ 11 | Modified icons to fit ionicon’s grid from original. 12 | https://cdnjs.cloudflare.com/ajax/libs/ionicons/4.6.3/css/ionicons.css 13 | */ 14 | @font-face { 15 | font-family: "Ionicons"; 16 | src: url("../fonts/ionicons.woff2") format("woff2"); 17 | font-weight: normal; 18 | font-style: normal; 19 | } 20 | .ion, .ionicons, 21 | .ion-ios-add:before, 22 | .ion-ios-add-circle:before, 23 | .ion-ios-add-circle-outline:before, 24 | .ion-ios-airplane:before, 25 | .ion-ios-alarm:before, 26 | .ion-ios-albums:before, 27 | .ion-ios-alert:before, 28 | .ion-ios-american-football:before, 29 | .ion-ios-analytics:before, 30 | .ion-ios-aperture:before, 31 | .ion-ios-apps:before, 32 | .ion-ios-appstore:before, 33 | .ion-ios-archive:before, 34 | .ion-ios-arrow-back:before, 35 | .ion-ios-arrow-down:before, 36 | .ion-ios-arrow-dropdown:before, 37 | .ion-ios-arrow-dropdown-circle:before, 38 | .ion-ios-arrow-dropleft:before, 39 | .ion-ios-arrow-dropleft-circle:before, 40 | .ion-ios-arrow-dropright:before, 41 | .ion-ios-arrow-dropright-circle:before, 42 | .ion-ios-arrow-dropup:before, 43 | .ion-ios-arrow-dropup-circle:before, 44 | .ion-ios-arrow-forward:before, 45 | .ion-ios-arrow-round-back:before, 46 | .ion-ios-arrow-round-down:before, 47 | .ion-ios-arrow-round-forward:before, 48 | .ion-ios-arrow-round-up:before, 49 | .ion-ios-arrow-up:before, 50 | .ion-ios-at:before, 51 | .ion-ios-attach:before, 52 | .ion-ios-backspace:before, 53 | .ion-ios-barcode:before, 54 | .ion-ios-baseball:before, 55 | .ion-ios-basket:before, 56 | .ion-ios-basketball:before, 57 | .ion-ios-battery-charging:before, 58 | .ion-ios-battery-dead:before, 59 | .ion-ios-battery-full:before, 60 | .ion-ios-beaker:before, 61 | .ion-ios-bed:before, 62 | .ion-ios-beer:before, 63 | .ion-ios-bicycle:before, 64 | .ion-ios-bluetooth:before, 65 | .ion-ios-boat:before, 66 | .ion-ios-body:before, 67 | .ion-ios-bonfire:before, 68 | .ion-ios-book:before, 69 | .ion-ios-bookmark:before, 70 | .ion-ios-bookmarks:before, 71 | .ion-ios-bowtie:before, 72 | .ion-ios-briefcase:before, 73 | .ion-ios-browsers:before, 74 | .ion-ios-brush:before, 75 | .ion-ios-bug:before, 76 | .ion-ios-build:before, 77 | .ion-ios-bulb:before, 78 | .ion-ios-bus:before, 79 | .ion-ios-business:before, 80 | .ion-ios-cafe:before, 81 | .ion-ios-calculator:before, 82 | .ion-ios-calendar:before, 83 | .ion-ios-call:before, 84 | .ion-ios-camera:before, 85 | .ion-ios-car:before, 86 | .ion-ios-card:before, 87 | .ion-ios-cart:before, 88 | .ion-ios-cash:before, 89 | .ion-ios-cellular:before, 90 | .ion-ios-chatboxes:before, 91 | .ion-ios-chatbubbles:before, 92 | .ion-ios-checkbox:before, 93 | .ion-ios-checkbox-outline:before, 94 | .ion-ios-checkmark:before, 95 | .ion-ios-checkmark-circle:before, 96 | .ion-ios-checkmark-circle-outline:before, 97 | .ion-ios-clipboard:before, 98 | .ion-ios-clock:before, 99 | .ion-ios-close:before, 100 | .ion-ios-close-circle:before, 101 | .ion-ios-close-circle-outline:before, 102 | .ion-ios-cloud:before, 103 | .ion-ios-cloud-circle:before, 104 | .ion-ios-cloud-done:before, 105 | .ion-ios-cloud-download:before, 106 | .ion-ios-cloud-outline:before, 107 | .ion-ios-cloud-upload:before, 108 | .ion-ios-cloudy:before, 109 | .ion-ios-cloudy-night:before, 110 | .ion-ios-code:before, 111 | .ion-ios-code-download:before, 112 | .ion-ios-code-working:before, 113 | .ion-ios-cog:before, 114 | .ion-ios-color-fill:before, 115 | .ion-ios-color-filter:before, 116 | .ion-ios-color-palette:before, 117 | .ion-ios-color-wand:before, 118 | .ion-ios-compass:before, 119 | .ion-ios-construct:before, 120 | .ion-ios-contact:before, 121 | .ion-ios-contacts:before, 122 | .ion-ios-contract:before, 123 | .ion-ios-contrast:before, 124 | .ion-ios-copy:before, 125 | .ion-ios-create:before, 126 | .ion-ios-crop:before, 127 | .ion-ios-cube:before, 128 | .ion-ios-cut:before, 129 | .ion-ios-desktop:before, 130 | .ion-ios-disc:before, 131 | .ion-ios-document:before, 132 | .ion-ios-done-all:before, 133 | .ion-ios-download:before, 134 | .ion-ios-easel:before, 135 | .ion-ios-egg:before, 136 | .ion-ios-exit:before, 137 | .ion-ios-expand:before, 138 | .ion-ios-eye:before, 139 | .ion-ios-eye-off:before, 140 | .ion-ios-fastforward:before, 141 | .ion-ios-female:before, 142 | .ion-ios-filing:before, 143 | .ion-ios-film:before, 144 | .ion-ios-finger-print:before, 145 | .ion-ios-fitness:before, 146 | .ion-ios-flag:before, 147 | .ion-ios-flame:before, 148 | .ion-ios-flash:before, 149 | .ion-ios-flash-off:before, 150 | .ion-ios-flashlight:before, 151 | .ion-ios-flask:before, 152 | .ion-ios-flower:before, 153 | .ion-ios-folder:before, 154 | .ion-ios-folder-open:before, 155 | .ion-ios-football:before, 156 | .ion-ios-funnel:before, 157 | .ion-ios-gift:before, 158 | .ion-ios-git-branch:before, 159 | .ion-ios-git-commit:before, 160 | .ion-ios-git-compare:before, 161 | .ion-ios-git-merge:before, 162 | .ion-ios-git-network:before, 163 | .ion-ios-git-pull-request:before, 164 | .ion-ios-glasses:before, 165 | .ion-ios-globe:before, 166 | .ion-ios-grid:before, 167 | .ion-ios-hammer:before, 168 | .ion-ios-hand:before, 169 | .ion-ios-happy:before, 170 | .ion-ios-headset:before, 171 | .ion-ios-heart:before, 172 | .ion-ios-heart-dislike:before, 173 | .ion-ios-heart-empty:before, 174 | .ion-ios-heart-half:before, 175 | .ion-ios-help:before, 176 | .ion-ios-help-buoy:before, 177 | .ion-ios-help-circle:before, 178 | .ion-ios-help-circle-outline:before, 179 | .ion-ios-home:before, 180 | .ion-ios-hourglass:before, 181 | .ion-ios-ice-cream:before, 182 | .ion-ios-image:before, 183 | .ion-ios-images:before, 184 | .ion-ios-infinite:before, 185 | .ion-ios-information:before, 186 | .ion-ios-information-circle:before, 187 | .ion-ios-information-circle-outline:before, 188 | .ion-ios-jet:before, 189 | .ion-ios-journal:before, 190 | .ion-ios-key:before, 191 | .ion-ios-keypad:before, 192 | .ion-ios-laptop:before, 193 | .ion-ios-leaf:before, 194 | .ion-ios-link:before, 195 | .ion-ios-list:before, 196 | .ion-ios-list-box:before, 197 | .ion-ios-locate:before, 198 | .ion-ios-lock:before, 199 | .ion-ios-log-in:before, 200 | .ion-ios-log-out:before, 201 | .ion-ios-magnet:before, 202 | .ion-ios-mail:before, 203 | .ion-ios-mail-open:before, 204 | .ion-ios-mail-unread:before, 205 | .ion-ios-male:before, 206 | .ion-ios-man:before, 207 | .ion-ios-map:before, 208 | .ion-ios-medal:before, 209 | .ion-ios-medical:before, 210 | .ion-ios-medkit:before, 211 | .ion-ios-megaphone:before, 212 | .ion-ios-menu:before, 213 | .ion-ios-mic:before, 214 | .ion-ios-mic-off:before, 215 | .ion-ios-microphone:before, 216 | .ion-ios-moon:before, 217 | .ion-ios-more:before, 218 | .ion-ios-move:before, 219 | .ion-ios-musical-note:before, 220 | .ion-ios-musical-notes:before, 221 | .ion-ios-navigate:before, 222 | .ion-ios-notifications:before, 223 | .ion-ios-notifications-off:before, 224 | .ion-ios-notifications-outline:before, 225 | .ion-ios-nuclear:before, 226 | .ion-ios-nutrition:before, 227 | .ion-ios-open:before, 228 | .ion-ios-options:before, 229 | .ion-ios-outlet:before, 230 | .ion-ios-paper:before, 231 | .ion-ios-paper-plane:before, 232 | .ion-ios-partly-sunny:before, 233 | .ion-ios-pause:before, 234 | .ion-ios-paw:before, 235 | .ion-ios-people:before, 236 | .ion-ios-person:before, 237 | .ion-ios-person-add:before, 238 | .ion-ios-phone-landscape:before, 239 | .ion-ios-phone-portrait:before, 240 | .ion-ios-photos:before, 241 | .ion-ios-pie:before, 242 | .ion-ios-pin:before, 243 | .ion-ios-pint:before, 244 | .ion-ios-pizza:before, 245 | .ion-ios-planet:before, 246 | .ion-ios-play:before, 247 | .ion-ios-play-circle:before, 248 | .ion-ios-podium:before, 249 | .ion-ios-power:before, 250 | .ion-ios-pricetag:before, 251 | .ion-ios-pricetags:before, 252 | .ion-ios-print:before, 253 | .ion-ios-pulse:before, 254 | .ion-ios-qr-scanner:before, 255 | .ion-ios-quote:before, 256 | .ion-ios-radio:before, 257 | .ion-ios-radio-button-off:before, 258 | .ion-ios-radio-button-on:before, 259 | .ion-ios-rainy:before, 260 | .ion-ios-recording:before, 261 | .ion-ios-redo:before, 262 | .ion-ios-refresh:before, 263 | .ion-ios-refresh-circle:before, 264 | .ion-ios-remove:before, 265 | .ion-ios-remove-circle:before, 266 | .ion-ios-remove-circle-outline:before, 267 | .ion-ios-reorder:before, 268 | .ion-ios-repeat:before, 269 | .ion-ios-resize:before, 270 | .ion-ios-restaurant:before, 271 | .ion-ios-return-left:before, 272 | .ion-ios-return-right:before, 273 | .ion-ios-reverse-camera:before, 274 | .ion-ios-rewind:before, 275 | .ion-ios-ribbon:before, 276 | .ion-ios-rocket:before, 277 | .ion-ios-rose:before, 278 | .ion-ios-sad:before, 279 | .ion-ios-save:before, 280 | .ion-ios-school:before, 281 | .ion-ios-search:before, 282 | .ion-ios-send:before, 283 | .ion-ios-settings:before, 284 | .ion-ios-share:before, 285 | .ion-ios-share-alt:before, 286 | .ion-ios-shirt:before, 287 | .ion-ios-shuffle:before, 288 | .ion-ios-skip-backward:before, 289 | .ion-ios-skip-forward:before, 290 | .ion-ios-snow:before, 291 | .ion-ios-speedometer:before, 292 | .ion-ios-square:before, 293 | .ion-ios-square-outline:before, 294 | .ion-ios-star:before, 295 | .ion-ios-star-half:before, 296 | .ion-ios-star-outline:before, 297 | .ion-ios-stats:before, 298 | .ion-ios-stopwatch:before, 299 | .ion-ios-subway:before, 300 | .ion-ios-sunny:before, 301 | .ion-ios-swap:before, 302 | .ion-ios-switch:before, 303 | .ion-ios-sync:before, 304 | .ion-ios-tablet-landscape:before, 305 | .ion-ios-tablet-portrait:before, 306 | .ion-ios-tennisball:before, 307 | .ion-ios-text:before, 308 | .ion-ios-thermometer:before, 309 | .ion-ios-thumbs-down:before, 310 | .ion-ios-thumbs-up:before, 311 | .ion-ios-thunderstorm:before, 312 | .ion-ios-time:before, 313 | .ion-ios-timer:before, 314 | .ion-ios-today:before, 315 | .ion-ios-train:before, 316 | .ion-ios-transgender:before, 317 | .ion-ios-trash:before, 318 | .ion-ios-trending-down:before, 319 | .ion-ios-trending-up:before, 320 | .ion-ios-trophy:before, 321 | .ion-ios-tv:before, 322 | .ion-ios-umbrella:before, 323 | .ion-ios-undo:before, 324 | .ion-ios-unlock:before, 325 | .ion-ios-videocam:before, 326 | .ion-ios-volume-high:before, 327 | .ion-ios-volume-low:before, 328 | .ion-ios-volume-mute:before, 329 | .ion-ios-volume-off:before, 330 | .ion-ios-walk:before, 331 | .ion-ios-wallet:before, 332 | .ion-ios-warning:before, 333 | .ion-ios-watch:before, 334 | .ion-ios-water:before, 335 | .ion-ios-wifi:before, 336 | .ion-ios-wine:before, 337 | .ion-ios-woman:before, 338 | .ion-logo-android:before, 339 | .ion-logo-angular:before, 340 | .ion-logo-apple:before, 341 | .ion-logo-bitbucket:before, 342 | .ion-logo-bitcoin:before, 343 | .ion-logo-buffer:before, 344 | .ion-logo-chrome:before, 345 | .ion-logo-closed-captioning:before, 346 | .ion-logo-codepen:before, 347 | .ion-logo-css3:before, 348 | .ion-logo-designernews:before, 349 | .ion-logo-dribbble:before, 350 | .ion-logo-dropbox:before, 351 | .ion-logo-euro:before, 352 | .ion-logo-facebook:before, 353 | .ion-logo-flickr:before, 354 | .ion-logo-foursquare:before, 355 | .ion-logo-freebsd-devil:before, 356 | .ion-logo-game-controller-a:before, 357 | .ion-logo-game-controller-b:before, 358 | .ion-logo-github:before, 359 | .ion-logo-google:before, 360 | .ion-logo-googleplus:before, 361 | .ion-logo-hackernews:before, 362 | .ion-logo-html5:before, 363 | .ion-logo-instagram:before, 364 | .ion-logo-ionic:before, 365 | .ion-logo-ionitron:before, 366 | .ion-logo-javascript:before, 367 | .ion-logo-linkedin:before, 368 | .ion-logo-markdown:before, 369 | .ion-logo-model-s:before, 370 | .ion-logo-no-smoking:before, 371 | .ion-logo-nodejs:before, 372 | .ion-logo-npm:before, 373 | .ion-logo-octocat:before, 374 | .ion-logo-pinterest:before, 375 | .ion-logo-playstation:before, 376 | .ion-logo-polymer:before, 377 | .ion-logo-python:before, 378 | .ion-logo-reddit:before, 379 | .ion-logo-rss:before, 380 | .ion-logo-sass:before, 381 | .ion-logo-skype:before, 382 | .ion-logo-slack:before, 383 | .ion-logo-snapchat:before, 384 | .ion-logo-steam:before, 385 | .ion-logo-tumblr:before, 386 | .ion-logo-tux:before, 387 | .ion-logo-twitch:before, 388 | .ion-logo-twitter:before, 389 | .ion-logo-usd:before, 390 | .ion-logo-vimeo:before, 391 | .ion-logo-vk:before, 392 | .ion-logo-whatsapp:before, 393 | .ion-logo-windows:before, 394 | .ion-logo-wordpress:before, 395 | .ion-logo-xbox:before, 396 | .ion-logo-xing:before, 397 | .ion-logo-yahoo:before, 398 | .ion-logo-yen:before, 399 | .ion-logo-youtube:before, 400 | .ion-spacedrop:before, 401 | .ion-spacedrop-small:before, 402 | .ion-md-add:before, 403 | .ion-md-add-circle:before, 404 | .ion-md-add-circle-outline:before, 405 | .ion-md-airplane:before, 406 | .ion-md-alarm:before, 407 | .ion-md-albums:before, 408 | .ion-md-alert:before, 409 | .ion-md-american-football:before, 410 | .ion-md-analytics:before, 411 | .ion-md-aperture:before, 412 | .ion-md-apps:before, 413 | .ion-md-appstore:before, 414 | .ion-md-archive:before, 415 | .ion-md-arrow-back:before, 416 | .ion-md-arrow-down:before, 417 | .ion-md-arrow-dropdown:before, 418 | .ion-md-arrow-dropdown-circle:before, 419 | .ion-md-arrow-dropleft:before, 420 | .ion-md-arrow-dropleft-circle:before, 421 | .ion-md-arrow-dropright:before, 422 | .ion-md-arrow-dropright-circle:before, 423 | .ion-md-arrow-dropup:before, 424 | .ion-md-arrow-dropup-circle:before, 425 | .ion-md-arrow-forward:before, 426 | .ion-md-arrow-round-back:before, 427 | .ion-md-arrow-round-down:before, 428 | .ion-md-arrow-round-forward:before, 429 | .ion-md-arrow-round-up:before, 430 | .ion-md-arrow-up:before, 431 | .ion-md-at:before, 432 | .ion-md-attach:before, 433 | .ion-md-backspace:before, 434 | .ion-md-barcode:before, 435 | .ion-md-baseball:before, 436 | .ion-md-basket:before, 437 | .ion-md-basketball:before, 438 | .ion-md-battery-charging:before, 439 | .ion-md-battery-dead:before, 440 | .ion-md-battery-full:before, 441 | .ion-md-beaker:before, 442 | .ion-md-bed:before, 443 | .ion-md-beer:before, 444 | .ion-md-bicycle:before, 445 | .ion-md-bluetooth:before, 446 | .ion-md-boat:before, 447 | .ion-md-body:before, 448 | .ion-md-bonfire:before, 449 | .ion-md-book:before, 450 | .ion-md-bookmark:before, 451 | .ion-md-bookmarks:before, 452 | .ion-md-bowtie:before, 453 | .ion-md-briefcase:before, 454 | .ion-md-browsers:before, 455 | .ion-md-brush:before, 456 | .ion-md-bug:before, 457 | .ion-md-build:before, 458 | .ion-md-bulb:before, 459 | .ion-md-bus:before, 460 | .ion-md-business:before, 461 | .ion-md-cafe:before, 462 | .ion-md-calculator:before, 463 | .ion-md-calendar:before, 464 | .ion-md-call:before, 465 | .ion-md-camera:before, 466 | .ion-md-car:before, 467 | .ion-md-card:before, 468 | .ion-md-cart:before, 469 | .ion-md-cash:before, 470 | .ion-md-cellular:before, 471 | .ion-md-chatboxes:before, 472 | .ion-md-chatbubbles:before, 473 | .ion-md-checkbox:before, 474 | .ion-md-checkbox-outline:before, 475 | .ion-md-checkmark:before, 476 | .ion-md-checkmark-circle:before, 477 | .ion-md-checkmark-circle-outline:before, 478 | .ion-md-clipboard:before, 479 | .ion-md-clock:before, 480 | .ion-md-close:before, 481 | .ion-md-close-circle:before, 482 | .ion-md-close-circle-outline:before, 483 | .ion-md-cloud:before, 484 | .ion-md-cloud-circle:before, 485 | .ion-md-cloud-done:before, 486 | .ion-md-cloud-download:before, 487 | .ion-md-cloud-outline:before, 488 | .ion-md-cloud-upload:before, 489 | .ion-md-cloudy:before, 490 | .ion-md-cloudy-night:before, 491 | .ion-md-code:before, 492 | .ion-md-code-download:before, 493 | .ion-md-code-working:before, 494 | .ion-md-cog:before, 495 | .ion-md-color-fill:before, 496 | .ion-md-color-filter:before, 497 | .ion-md-color-palette:before, 498 | .ion-md-color-wand:before, 499 | .ion-md-compass:before, 500 | .ion-md-construct:before, 501 | .ion-md-contact:before, 502 | .ion-md-contacts:before, 503 | .ion-md-contract:before, 504 | .ion-md-contrast:before, 505 | .ion-md-copy:before, 506 | .ion-md-create:before, 507 | .ion-md-crop:before, 508 | .ion-md-cube:before, 509 | .ion-md-cut:before, 510 | .ion-md-desktop:before, 511 | .ion-md-disc:before, 512 | .ion-md-document:before, 513 | .ion-md-done-all:before, 514 | .ion-md-download:before, 515 | .ion-md-easel:before, 516 | .ion-md-egg:before, 517 | .ion-md-exit:before, 518 | .ion-md-expand:before, 519 | .ion-md-eye:before, 520 | .ion-md-eye-off:before, 521 | .ion-md-fastforward:before, 522 | .ion-md-female:before, 523 | .ion-md-filing:before, 524 | .ion-md-film:before, 525 | .ion-md-finger-print:before, 526 | .ion-md-fitness:before, 527 | .ion-md-flag:before, 528 | .ion-md-flame:before, 529 | .ion-md-flash:before, 530 | .ion-md-flash-off:before, 531 | .ion-md-flashlight:before, 532 | .ion-md-flask:before, 533 | .ion-md-flower:before, 534 | .ion-md-folder:before, 535 | .ion-md-folder-open:before, 536 | .ion-md-football:before, 537 | .ion-md-funnel:before, 538 | .ion-md-gift:before, 539 | .ion-md-git-branch:before, 540 | .ion-md-git-commit:before, 541 | .ion-md-git-compare:before, 542 | .ion-md-git-merge:before, 543 | .ion-md-git-network:before, 544 | .ion-md-git-pull-request:before, 545 | .ion-md-glasses:before, 546 | .ion-md-globe:before, 547 | .ion-md-grid:before, 548 | .ion-md-hammer:before, 549 | .ion-md-hand:before, 550 | .ion-md-happy:before, 551 | .ion-md-headset:before, 552 | .ion-md-heart:before, 553 | .ion-md-heart-dislike:before, 554 | .ion-md-heart-empty:before, 555 | .ion-md-heart-half:before, 556 | .ion-md-help:before, 557 | .ion-md-help-buoy:before, 558 | .ion-md-help-circle:before, 559 | .ion-md-help-circle-outline:before, 560 | .ion-md-home:before, 561 | .ion-md-hourglass:before, 562 | .ion-md-ice-cream:before, 563 | .ion-md-image:before, 564 | .ion-md-images:before, 565 | .ion-md-infinite:before, 566 | .ion-md-information:before, 567 | .ion-md-information-circle:before, 568 | .ion-md-information-circle-outline:before, 569 | .ion-md-jet:before, 570 | .ion-md-journal:before, 571 | .ion-md-key:before, 572 | .ion-md-keypad:before, 573 | .ion-md-laptop:before, 574 | .ion-md-leaf:before, 575 | .ion-md-link:before, 576 | .ion-md-list:before, 577 | .ion-md-list-box:before, 578 | .ion-md-locate:before, 579 | .ion-md-lock:before, 580 | .ion-md-log-in:before, 581 | .ion-md-log-out:before, 582 | .ion-md-magnet:before, 583 | .ion-md-mail:before, 584 | .ion-md-mail-open:before, 585 | .ion-md-mail-unread:before, 586 | .ion-md-male:before, 587 | .ion-md-man:before, 588 | .ion-md-map:before, 589 | .ion-md-medal:before, 590 | .ion-md-medical:before, 591 | .ion-md-medkit:before, 592 | .ion-md-megaphone:before, 593 | .ion-md-menu:before, 594 | .ion-md-mic:before, 595 | .ion-md-mic-off:before, 596 | .ion-md-microphone:before, 597 | .ion-md-moon:before, 598 | .ion-md-more:before, 599 | .ion-md-move:before, 600 | .ion-md-musical-note:before, 601 | .ion-md-musical-notes:before, 602 | .ion-md-navigate:before, 603 | .ion-md-notifications:before, 604 | .ion-md-notifications-off:before, 605 | .ion-md-notifications-outline:before, 606 | .ion-md-nuclear:before, 607 | .ion-md-nutrition:before, 608 | .ion-md-open:before, 609 | .ion-md-options:before, 610 | .ion-md-outlet:before, 611 | .ion-md-paper:before, 612 | .ion-md-paper-plane:before, 613 | .ion-md-partly-sunny:before, 614 | .ion-md-pause:before, 615 | .ion-md-paw:before, 616 | .ion-md-people:before, 617 | .ion-md-person:before, 618 | .ion-md-person-add:before, 619 | .ion-md-phone-landscape:before, 620 | .ion-md-phone-portrait:before, 621 | .ion-md-photos:before, 622 | .ion-md-pie:before, 623 | .ion-md-pin:before, 624 | .ion-md-pint:before, 625 | .ion-md-pizza:before, 626 | .ion-md-planet:before, 627 | .ion-md-play:before, 628 | .ion-md-play-circle:before, 629 | .ion-md-podium:before, 630 | .ion-md-power:before, 631 | .ion-md-pricetag:before, 632 | .ion-md-pricetags:before, 633 | .ion-md-print:before, 634 | .ion-md-pulse:before, 635 | .ion-md-qr-scanner:before, 636 | .ion-md-quote:before, 637 | .ion-md-radio:before, 638 | .ion-md-radio-button-off:before, 639 | .ion-md-radio-button-on:before, 640 | .ion-md-rainy:before, 641 | .ion-md-recording:before, 642 | .ion-md-redo:before, 643 | .ion-md-refresh:before, 644 | .ion-md-refresh-circle:before, 645 | .ion-md-remove:before, 646 | .ion-md-remove-circle:before, 647 | .ion-md-remove-circle-outline:before, 648 | .ion-md-reorder:before, 649 | .ion-md-repeat:before, 650 | .ion-md-resize:before, 651 | .ion-md-restaurant:before, 652 | .ion-md-return-left:before, 653 | .ion-md-return-right:before, 654 | .ion-md-reverse-camera:before, 655 | .ion-md-rewind:before, 656 | .ion-md-ribbon:before, 657 | .ion-md-rocket:before, 658 | .ion-md-rose:before, 659 | .ion-md-sad:before, 660 | .ion-md-save:before, 661 | .ion-md-school:before, 662 | .ion-md-search:before, 663 | .ion-md-send:before, 664 | .ion-md-settings:before, 665 | .ion-md-share:before, 666 | .ion-md-share-alt:before, 667 | .ion-md-shirt:before, 668 | .ion-md-shuffle:before, 669 | .ion-md-skip-backward:before, 670 | .ion-md-skip-forward:before, 671 | .ion-md-snow:before, 672 | .ion-md-speedometer:before, 673 | .ion-md-square:before, 674 | .ion-md-square-outline:before, 675 | .ion-md-star:before, 676 | .ion-md-star-half:before, 677 | .ion-md-star-outline:before, 678 | .ion-md-stats:before, 679 | .ion-md-stopwatch:before, 680 | .ion-md-subway:before, 681 | .ion-md-sunny:before, 682 | .ion-md-swap:before, 683 | .ion-md-switch:before, 684 | .ion-md-sync:before, 685 | .ion-md-tablet-landscape:before, 686 | .ion-md-tablet-portrait:before, 687 | .ion-md-tennisball:before, 688 | .ion-md-text:before, 689 | .ion-md-thermometer:before, 690 | .ion-md-thumbs-down:before, 691 | .ion-md-thumbs-up:before, 692 | .ion-md-thunderstorm:before, 693 | .ion-md-time:before, 694 | .ion-md-timer:before, 695 | .ion-md-today:before, 696 | .ion-md-train:before, 697 | .ion-md-transgender:before, 698 | .ion-md-trash:before, 699 | .ion-md-trending-down:before, 700 | .ion-md-trending-up:before, 701 | .ion-md-trophy:before, 702 | .ion-md-tv:before, 703 | .ion-md-umbrella:before, 704 | .ion-md-undo:before, 705 | .ion-md-unlock:before, 706 | .ion-md-videocam:before, 707 | .ion-md-volume-high:before, 708 | .ion-md-volume-low:before, 709 | .ion-md-volume-mute:before, 710 | .ion-md-volume-off:before, 711 | .ion-md-walk:before, 712 | .ion-md-wallet:before, 713 | .ion-md-warning:before, 714 | .ion-md-watch:before, 715 | .ion-md-water:before, 716 | .ion-md-wifi:before, 717 | .ion-md-wine:before, 718 | .ion-md-woman:before { 719 | display: inline-block; 720 | font-family: "Ionicons"; 721 | speak: none; 722 | font-style: normal; 723 | font-weight: normal; 724 | font-variant: normal; 725 | text-transform: none; 726 | text-rendering: auto; 727 | line-height: 1; 728 | -webkit-font-smoothing: antialiased; 729 | -moz-osx-font-smoothing: grayscale; 730 | } 731 | 732 | .ion-spacedrop:before { 733 | content: "ﺢ"; 734 | } 735 | 736 | .ion-spacedrop-small:before { 737 | content: "ﺡ"; 738 | } 739 | 740 | .ion-ios-add:before { 741 | content: ""; 742 | } 743 | 744 | .ion-ios-add-circle:before { 745 | content: ""; 746 | } 747 | 748 | .ion-ios-add-circle-outline:before { 749 | content: ""; 750 | } 751 | 752 | .ion-ios-airplane:before { 753 | content: ""; 754 | } 755 | 756 | .ion-ios-alarm:before { 757 | content: ""; 758 | } 759 | 760 | .ion-ios-albums:before { 761 | content: ""; 762 | } 763 | 764 | .ion-ios-alert:before { 765 | content: ""; 766 | } 767 | 768 | .ion-ios-american-football:before { 769 | content: ""; 770 | } 771 | 772 | .ion-ios-analytics:before { 773 | content: ""; 774 | } 775 | 776 | .ion-ios-aperture:before { 777 | content: ""; 778 | } 779 | 780 | .ion-ios-apps:before { 781 | content: ""; 782 | } 783 | 784 | .ion-ios-appstore:before { 785 | content: ""; 786 | } 787 | 788 | .ion-ios-archive:before { 789 | content: ""; 790 | } 791 | 792 | .ion-ios-arrow-back:before { 793 | content: ""; 794 | } 795 | 796 | .ion-ios-arrow-down:before { 797 | content: ""; 798 | } 799 | 800 | .ion-ios-arrow-dropdown:before { 801 | content: ""; 802 | } 803 | 804 | .ion-ios-arrow-dropdown-circle:before { 805 | content: ""; 806 | } 807 | 808 | .ion-ios-arrow-dropleft:before { 809 | content: ""; 810 | } 811 | 812 | .ion-ios-arrow-dropleft-circle:before { 813 | content: ""; 814 | } 815 | 816 | .ion-ios-arrow-dropright:before { 817 | content: ""; 818 | } 819 | 820 | .ion-ios-arrow-dropright-circle:before { 821 | content: ""; 822 | } 823 | 824 | .ion-ios-arrow-dropup:before { 825 | content: ""; 826 | } 827 | 828 | .ion-ios-arrow-dropup-circle:before { 829 | content: ""; 830 | } 831 | 832 | .ion-ios-arrow-forward:before { 833 | content: ""; 834 | } 835 | 836 | .ion-ios-arrow-round-back:before { 837 | content: ""; 838 | } 839 | 840 | .ion-ios-arrow-round-down:before { 841 | content: ""; 842 | } 843 | 844 | .ion-ios-arrow-round-forward:before { 845 | content: ""; 846 | } 847 | 848 | .ion-ios-arrow-round-up:before { 849 | content: ""; 850 | } 851 | 852 | .ion-ios-arrow-up:before { 853 | content: ""; 854 | } 855 | 856 | .ion-ios-at:before { 857 | content: ""; 858 | } 859 | 860 | .ion-ios-attach:before { 861 | content: ""; 862 | } 863 | 864 | .ion-ios-backspace:before { 865 | content: ""; 866 | } 867 | 868 | .ion-ios-barcode:before { 869 | content: ""; 870 | } 871 | 872 | .ion-ios-baseball:before { 873 | content: ""; 874 | } 875 | 876 | .ion-ios-basket:before { 877 | content: ""; 878 | } 879 | 880 | .ion-ios-basketball:before { 881 | content: ""; 882 | } 883 | 884 | .ion-ios-battery-charging:before { 885 | content: ""; 886 | } 887 | 888 | .ion-ios-battery-dead:before { 889 | content: ""; 890 | } 891 | 892 | .ion-ios-battery-full:before { 893 | content: ""; 894 | } 895 | 896 | .ion-ios-beaker:before { 897 | content: ""; 898 | } 899 | 900 | .ion-ios-bed:before { 901 | content: ""; 902 | } 903 | 904 | .ion-ios-beer:before { 905 | content: ""; 906 | } 907 | 908 | .ion-ios-bicycle:before { 909 | content: ""; 910 | } 911 | 912 | .ion-ios-bluetooth:before { 913 | content: ""; 914 | } 915 | 916 | .ion-ios-boat:before { 917 | content: ""; 918 | } 919 | 920 | .ion-ios-body:before { 921 | content: ""; 922 | } 923 | 924 | .ion-ios-bonfire:before { 925 | content: ""; 926 | } 927 | 928 | .ion-ios-book:before { 929 | content: ""; 930 | } 931 | 932 | .ion-ios-bookmark:before { 933 | content: ""; 934 | } 935 | 936 | .ion-ios-bookmarks:before { 937 | content: ""; 938 | } 939 | 940 | .ion-ios-bowtie:before { 941 | content: ""; 942 | } 943 | 944 | .ion-ios-briefcase:before { 945 | content: ""; 946 | } 947 | 948 | .ion-ios-browsers:before { 949 | content: ""; 950 | } 951 | 952 | .ion-ios-brush:before { 953 | content: ""; 954 | } 955 | 956 | .ion-ios-bug:before { 957 | content: ""; 958 | } 959 | 960 | .ion-ios-build:before { 961 | content: ""; 962 | } 963 | 964 | .ion-ios-bulb:before { 965 | content: ""; 966 | } 967 | 968 | .ion-ios-bus:before { 969 | content: ""; 970 | } 971 | 972 | .ion-ios-business:before { 973 | content: ""; 974 | } 975 | 976 | .ion-ios-cafe:before { 977 | content: ""; 978 | } 979 | 980 | .ion-ios-calculator:before { 981 | content: ""; 982 | } 983 | 984 | .ion-ios-calendar:before { 985 | content: ""; 986 | } 987 | 988 | .ion-ios-call:before { 989 | content: ""; 990 | } 991 | 992 | .ion-ios-camera:before { 993 | content: ""; 994 | } 995 | 996 | .ion-ios-car:before { 997 | content: ""; 998 | } 999 | 1000 | .ion-ios-card:before { 1001 | content: ""; 1002 | } 1003 | 1004 | .ion-ios-cart:before { 1005 | content: ""; 1006 | } 1007 | 1008 | .ion-ios-cash:before { 1009 | content: ""; 1010 | } 1011 | 1012 | .ion-ios-cellular:before { 1013 | content: ""; 1014 | } 1015 | 1016 | .ion-ios-chatboxes:before { 1017 | content: ""; 1018 | } 1019 | 1020 | .ion-ios-chatbubbles:before { 1021 | content: ""; 1022 | } 1023 | 1024 | .ion-ios-checkbox:before { 1025 | content: ""; 1026 | } 1027 | 1028 | .ion-ios-checkbox-outline:before { 1029 | content: ""; 1030 | } 1031 | 1032 | .ion-ios-checkmark:before { 1033 | content: ""; 1034 | } 1035 | 1036 | .ion-ios-checkmark-circle:before { 1037 | content: ""; 1038 | } 1039 | 1040 | .ion-ios-checkmark-circle-outline:before { 1041 | content: ""; 1042 | } 1043 | 1044 | .ion-ios-clipboard:before { 1045 | content: ""; 1046 | } 1047 | 1048 | .ion-ios-clock:before { 1049 | content: ""; 1050 | } 1051 | 1052 | .ion-ios-close:before { 1053 | content: ""; 1054 | } 1055 | 1056 | .ion-ios-close-circle:before { 1057 | content: ""; 1058 | } 1059 | 1060 | .ion-ios-close-circle-outline:before { 1061 | content: ""; 1062 | } 1063 | 1064 | .ion-ios-cloud:before { 1065 | content: ""; 1066 | } 1067 | 1068 | .ion-ios-cloud-circle:before { 1069 | content: ""; 1070 | } 1071 | 1072 | .ion-ios-cloud-done:before { 1073 | content: ""; 1074 | } 1075 | 1076 | .ion-ios-cloud-download:before { 1077 | content: ""; 1078 | } 1079 | 1080 | .ion-ios-cloud-outline:before { 1081 | content: ""; 1082 | } 1083 | 1084 | .ion-ios-cloud-upload:before { 1085 | content: ""; 1086 | } 1087 | 1088 | .ion-ios-cloudy:before { 1089 | content: ""; 1090 | } 1091 | 1092 | .ion-ios-cloudy-night:before { 1093 | content: ""; 1094 | } 1095 | 1096 | .ion-ios-code:before { 1097 | content: ""; 1098 | } 1099 | 1100 | .ion-ios-code-download:before { 1101 | content: ""; 1102 | } 1103 | 1104 | .ion-ios-code-working:before { 1105 | content: ""; 1106 | } 1107 | 1108 | .ion-ios-cog:before { 1109 | content: ""; 1110 | } 1111 | 1112 | .ion-ios-color-fill:before { 1113 | content: ""; 1114 | } 1115 | 1116 | .ion-ios-color-filter:before { 1117 | content: ""; 1118 | } 1119 | 1120 | .ion-ios-color-palette:before { 1121 | content: ""; 1122 | } 1123 | 1124 | .ion-ios-color-wand:before { 1125 | content: ""; 1126 | } 1127 | 1128 | .ion-ios-compass:before { 1129 | content: ""; 1130 | } 1131 | 1132 | .ion-ios-construct:before { 1133 | content: ""; 1134 | } 1135 | 1136 | .ion-ios-contact:before { 1137 | content: ""; 1138 | } 1139 | 1140 | .ion-ios-contacts:before { 1141 | content: ""; 1142 | } 1143 | 1144 | .ion-ios-contract:before { 1145 | content: ""; 1146 | } 1147 | 1148 | .ion-ios-contrast:before { 1149 | content: ""; 1150 | } 1151 | 1152 | .ion-ios-copy:before { 1153 | content: ""; 1154 | } 1155 | 1156 | .ion-ios-create:before { 1157 | content: ""; 1158 | } 1159 | 1160 | .ion-ios-crop:before { 1161 | content: ""; 1162 | } 1163 | 1164 | .ion-ios-cube:before { 1165 | content: ""; 1166 | } 1167 | 1168 | .ion-ios-cut:before { 1169 | content: ""; 1170 | } 1171 | 1172 | .ion-ios-desktop:before { 1173 | content: ""; 1174 | } 1175 | 1176 | .ion-ios-disc:before { 1177 | content: ""; 1178 | } 1179 | 1180 | .ion-ios-document:before { 1181 | content: ""; 1182 | } 1183 | 1184 | .ion-ios-done-all:before { 1185 | content: ""; 1186 | } 1187 | 1188 | .ion-ios-download:before { 1189 | content: ""; 1190 | } 1191 | 1192 | .ion-ios-easel:before { 1193 | content: ""; 1194 | } 1195 | 1196 | .ion-ios-egg:before { 1197 | content: ""; 1198 | } 1199 | 1200 | .ion-ios-exit:before { 1201 | content: ""; 1202 | } 1203 | 1204 | .ion-ios-expand:before { 1205 | content: ""; 1206 | } 1207 | 1208 | .ion-ios-eye:before { 1209 | content: ""; 1210 | } 1211 | 1212 | .ion-ios-eye-off:before { 1213 | content: ""; 1214 | } 1215 | 1216 | .ion-ios-fastforward:before { 1217 | content: ""; 1218 | } 1219 | 1220 | .ion-ios-female:before { 1221 | content: ""; 1222 | } 1223 | 1224 | .ion-ios-filing:before { 1225 | content: ""; 1226 | } 1227 | 1228 | .ion-ios-film:before { 1229 | content: ""; 1230 | } 1231 | 1232 | .ion-ios-finger-print:before { 1233 | content: ""; 1234 | } 1235 | 1236 | .ion-ios-fitness:before { 1237 | content: ""; 1238 | } 1239 | 1240 | .ion-ios-flag:before { 1241 | content: ""; 1242 | } 1243 | 1244 | .ion-ios-flame:before { 1245 | content: ""; 1246 | } 1247 | 1248 | .ion-ios-flash:before { 1249 | content: ""; 1250 | } 1251 | 1252 | .ion-ios-flash-off:before { 1253 | content: ""; 1254 | } 1255 | 1256 | .ion-ios-flashlight:before { 1257 | content: ""; 1258 | } 1259 | 1260 | .ion-ios-flask:before { 1261 | content: ""; 1262 | } 1263 | 1264 | .ion-ios-flower:before { 1265 | content: ""; 1266 | } 1267 | 1268 | .ion-ios-folder:before { 1269 | content: ""; 1270 | } 1271 | 1272 | .ion-ios-folder-open:before { 1273 | content: ""; 1274 | } 1275 | 1276 | .ion-ios-football:before { 1277 | content: ""; 1278 | } 1279 | 1280 | .ion-ios-funnel:before { 1281 | content: ""; 1282 | } 1283 | 1284 | .ion-ios-gift:before { 1285 | content: ""; 1286 | } 1287 | 1288 | .ion-ios-git-branch:before { 1289 | content: ""; 1290 | } 1291 | 1292 | .ion-ios-git-commit:before { 1293 | content: ""; 1294 | } 1295 | 1296 | .ion-ios-git-compare:before { 1297 | content: ""; 1298 | } 1299 | 1300 | .ion-ios-git-merge:before { 1301 | content: ""; 1302 | } 1303 | 1304 | .ion-ios-git-network:before { 1305 | content: ""; 1306 | } 1307 | 1308 | .ion-ios-git-pull-request:before { 1309 | content: ""; 1310 | } 1311 | 1312 | .ion-ios-glasses:before { 1313 | content: ""; 1314 | } 1315 | 1316 | .ion-ios-globe:before { 1317 | content: ""; 1318 | } 1319 | 1320 | .ion-ios-grid:before { 1321 | content: ""; 1322 | } 1323 | 1324 | .ion-ios-hammer:before { 1325 | content: ""; 1326 | } 1327 | 1328 | .ion-ios-hand:before { 1329 | content: ""; 1330 | } 1331 | 1332 | .ion-ios-happy:before { 1333 | content: ""; 1334 | } 1335 | 1336 | .ion-ios-headset:before { 1337 | content: ""; 1338 | } 1339 | 1340 | .ion-ios-heart:before { 1341 | content: ""; 1342 | } 1343 | 1344 | .ion-ios-heart-dislike:before { 1345 | content: ""; 1346 | } 1347 | 1348 | .ion-ios-heart-empty:before { 1349 | content: ""; 1350 | } 1351 | 1352 | .ion-ios-heart-half:before { 1353 | content: ""; 1354 | } 1355 | 1356 | .ion-ios-help:before { 1357 | content: ""; 1358 | } 1359 | 1360 | .ion-ios-help-buoy:before { 1361 | content: ""; 1362 | } 1363 | 1364 | .ion-ios-help-circle:before { 1365 | content: ""; 1366 | } 1367 | 1368 | .ion-ios-help-circle-outline:before { 1369 | content: ""; 1370 | } 1371 | 1372 | .ion-ios-home:before { 1373 | content: ""; 1374 | } 1375 | 1376 | .ion-ios-hourglass:before { 1377 | content: ""; 1378 | } 1379 | 1380 | .ion-ios-ice-cream:before { 1381 | content: ""; 1382 | } 1383 | 1384 | .ion-ios-image:before { 1385 | content: ""; 1386 | } 1387 | 1388 | .ion-ios-images:before { 1389 | content: ""; 1390 | } 1391 | 1392 | .ion-ios-infinite:before { 1393 | content: ""; 1394 | } 1395 | 1396 | .ion-ios-information:before { 1397 | content: ""; 1398 | } 1399 | 1400 | .ion-ios-information-circle:before { 1401 | content: ""; 1402 | } 1403 | 1404 | .ion-ios-information-circle-outline:before { 1405 | content: ""; 1406 | } 1407 | 1408 | .ion-ios-jet:before { 1409 | content: ""; 1410 | } 1411 | 1412 | .ion-ios-journal:before { 1413 | content: ""; 1414 | } 1415 | 1416 | .ion-ios-key:before { 1417 | content: ""; 1418 | } 1419 | 1420 | .ion-ios-keypad:before { 1421 | content: ""; 1422 | } 1423 | 1424 | .ion-ios-laptop:before { 1425 | content: ""; 1426 | } 1427 | 1428 | .ion-ios-leaf:before { 1429 | content: ""; 1430 | } 1431 | 1432 | .ion-ios-link:before { 1433 | content: ""; 1434 | } 1435 | 1436 | .ion-ios-list:before { 1437 | content: ""; 1438 | } 1439 | 1440 | .ion-ios-list-box:before { 1441 | content: ""; 1442 | } 1443 | 1444 | .ion-ios-locate:before { 1445 | content: ""; 1446 | } 1447 | 1448 | .ion-ios-lock:before { 1449 | content: ""; 1450 | } 1451 | 1452 | .ion-ios-log-in:before { 1453 | content: ""; 1454 | } 1455 | 1456 | .ion-ios-log-out:before { 1457 | content: ""; 1458 | } 1459 | 1460 | .ion-ios-magnet:before { 1461 | content: ""; 1462 | } 1463 | 1464 | .ion-ios-mail:before { 1465 | content: ""; 1466 | } 1467 | 1468 | .ion-ios-mail-open:before { 1469 | content: ""; 1470 | } 1471 | 1472 | .ion-ios-mail-unread:before { 1473 | content: ""; 1474 | } 1475 | 1476 | .ion-ios-male:before { 1477 | content: ""; 1478 | } 1479 | 1480 | .ion-ios-man:before { 1481 | content: ""; 1482 | } 1483 | 1484 | .ion-ios-map:before { 1485 | content: ""; 1486 | } 1487 | 1488 | .ion-ios-medal:before { 1489 | content: ""; 1490 | } 1491 | 1492 | .ion-ios-medical:before { 1493 | content: ""; 1494 | } 1495 | 1496 | .ion-ios-medkit:before { 1497 | content: ""; 1498 | } 1499 | 1500 | .ion-ios-megaphone:before { 1501 | content: ""; 1502 | } 1503 | 1504 | .ion-ios-menu:before { 1505 | content: ""; 1506 | } 1507 | 1508 | .ion-ios-mic:before { 1509 | content: ""; 1510 | } 1511 | 1512 | .ion-ios-mic-off:before { 1513 | content: ""; 1514 | } 1515 | 1516 | .ion-ios-microphone:before { 1517 | content: ""; 1518 | } 1519 | 1520 | .ion-ios-moon:before { 1521 | content: ""; 1522 | } 1523 | 1524 | .ion-ios-more:before { 1525 | content: ""; 1526 | } 1527 | 1528 | .ion-ios-move:before { 1529 | content: ""; 1530 | } 1531 | 1532 | .ion-ios-musical-note:before { 1533 | content: ""; 1534 | } 1535 | 1536 | .ion-ios-musical-notes:before { 1537 | content: ""; 1538 | } 1539 | 1540 | .ion-ios-navigate:before { 1541 | content: ""; 1542 | } 1543 | 1544 | .ion-ios-notifications:before { 1545 | content: ""; 1546 | } 1547 | 1548 | .ion-ios-notifications-off:before { 1549 | content: ""; 1550 | } 1551 | 1552 | .ion-ios-notifications-outline:before { 1553 | content: ""; 1554 | } 1555 | 1556 | .ion-ios-nuclear:before { 1557 | content: ""; 1558 | } 1559 | 1560 | .ion-ios-nutrition:before { 1561 | content: ""; 1562 | } 1563 | 1564 | .ion-ios-open:before { 1565 | content: ""; 1566 | } 1567 | 1568 | .ion-ios-options:before { 1569 | content: ""; 1570 | } 1571 | 1572 | .ion-ios-outlet:before { 1573 | content: ""; 1574 | } 1575 | 1576 | .ion-ios-paper:before { 1577 | content: ""; 1578 | } 1579 | 1580 | .ion-ios-paper-plane:before { 1581 | content: ""; 1582 | } 1583 | 1584 | .ion-ios-partly-sunny:before { 1585 | content: ""; 1586 | } 1587 | 1588 | .ion-ios-pause:before { 1589 | content: ""; 1590 | } 1591 | 1592 | .ion-ios-paw:before { 1593 | content: ""; 1594 | } 1595 | 1596 | .ion-ios-people:before { 1597 | content: ""; 1598 | } 1599 | 1600 | .ion-ios-person:before { 1601 | content: ""; 1602 | } 1603 | 1604 | .ion-ios-person-add:before { 1605 | content: ""; 1606 | } 1607 | 1608 | .ion-ios-phone-landscape:before { 1609 | content: ""; 1610 | } 1611 | 1612 | .ion-ios-phone-portrait:before { 1613 | content: ""; 1614 | } 1615 | 1616 | .ion-ios-photos:before { 1617 | content: ""; 1618 | } 1619 | 1620 | .ion-ios-pie:before { 1621 | content: ""; 1622 | } 1623 | 1624 | .ion-ios-pin:before { 1625 | content: ""; 1626 | } 1627 | 1628 | .ion-ios-pint:before { 1629 | content: ""; 1630 | } 1631 | 1632 | .ion-ios-pizza:before { 1633 | content: ""; 1634 | } 1635 | 1636 | .ion-ios-planet:before { 1637 | content: ""; 1638 | } 1639 | 1640 | .ion-ios-play:before { 1641 | content: ""; 1642 | } 1643 | 1644 | .ion-ios-play-circle:before { 1645 | content: ""; 1646 | } 1647 | 1648 | .ion-ios-podium:before { 1649 | content: ""; 1650 | } 1651 | 1652 | .ion-ios-power:before { 1653 | content: ""; 1654 | } 1655 | 1656 | .ion-ios-pricetag:before { 1657 | content: ""; 1658 | } 1659 | 1660 | .ion-ios-pricetags:before { 1661 | content: ""; 1662 | } 1663 | 1664 | .ion-ios-print:before { 1665 | content: ""; 1666 | } 1667 | 1668 | .ion-ios-pulse:before { 1669 | content: ""; 1670 | } 1671 | 1672 | .ion-ios-qr-scanner:before { 1673 | content: ""; 1674 | } 1675 | 1676 | .ion-ios-quote:before { 1677 | content: ""; 1678 | } 1679 | 1680 | .ion-ios-radio:before { 1681 | content: ""; 1682 | } 1683 | 1684 | .ion-ios-radio-button-off:before { 1685 | content: ""; 1686 | } 1687 | 1688 | .ion-ios-radio-button-on:before { 1689 | content: ""; 1690 | } 1691 | 1692 | .ion-ios-rainy:before { 1693 | content: ""; 1694 | } 1695 | 1696 | .ion-ios-recording:before { 1697 | content: ""; 1698 | } 1699 | 1700 | .ion-ios-redo:before { 1701 | content: ""; 1702 | } 1703 | 1704 | .ion-ios-refresh:before { 1705 | content: ""; 1706 | } 1707 | 1708 | .ion-ios-refresh-circle:before { 1709 | content: ""; 1710 | } 1711 | 1712 | .ion-ios-remove:before { 1713 | content: ""; 1714 | } 1715 | 1716 | .ion-ios-remove-circle:before { 1717 | content: ""; 1718 | } 1719 | 1720 | .ion-ios-remove-circle-outline:before { 1721 | content: ""; 1722 | } 1723 | 1724 | .ion-ios-reorder:before { 1725 | content: ""; 1726 | } 1727 | 1728 | .ion-ios-repeat:before { 1729 | content: ""; 1730 | } 1731 | 1732 | .ion-ios-resize:before { 1733 | content: ""; 1734 | } 1735 | 1736 | .ion-ios-restaurant:before { 1737 | content: ""; 1738 | } 1739 | 1740 | .ion-ios-return-left:before { 1741 | content: ""; 1742 | } 1743 | 1744 | .ion-ios-return-right:before { 1745 | content: ""; 1746 | } 1747 | 1748 | .ion-ios-reverse-camera:before { 1749 | content: ""; 1750 | } 1751 | 1752 | .ion-ios-rewind:before { 1753 | content: ""; 1754 | } 1755 | 1756 | .ion-ios-ribbon:before { 1757 | content: ""; 1758 | } 1759 | 1760 | .ion-ios-rocket:before { 1761 | content: ""; 1762 | } 1763 | 1764 | .ion-ios-rose:before { 1765 | content: ""; 1766 | } 1767 | 1768 | .ion-ios-sad:before { 1769 | content: ""; 1770 | } 1771 | 1772 | .ion-ios-save:before { 1773 | content: ""; 1774 | } 1775 | 1776 | .ion-ios-school:before { 1777 | content: ""; 1778 | } 1779 | 1780 | .ion-ios-search:before { 1781 | content: ""; 1782 | } 1783 | 1784 | .ion-ios-send:before { 1785 | content: ""; 1786 | } 1787 | 1788 | .ion-ios-settings:before { 1789 | content: ""; 1790 | } 1791 | 1792 | .ion-ios-share:before { 1793 | content: ""; 1794 | } 1795 | 1796 | .ion-ios-share-alt:before { 1797 | content: ""; 1798 | } 1799 | 1800 | .ion-ios-shirt:before { 1801 | content: ""; 1802 | } 1803 | 1804 | .ion-ios-shuffle:before { 1805 | content: ""; 1806 | } 1807 | 1808 | .ion-ios-skip-backward:before { 1809 | content: ""; 1810 | } 1811 | 1812 | .ion-ios-skip-forward:before { 1813 | content: ""; 1814 | } 1815 | 1816 | .ion-ios-snow:before { 1817 | content: ""; 1818 | } 1819 | 1820 | .ion-ios-speedometer:before { 1821 | content: ""; 1822 | } 1823 | 1824 | .ion-ios-square:before { 1825 | content: ""; 1826 | } 1827 | 1828 | .ion-ios-square-outline:before { 1829 | content: ""; 1830 | } 1831 | 1832 | .ion-ios-star:before { 1833 | content: ""; 1834 | } 1835 | 1836 | .ion-ios-star-half:before { 1837 | content: ""; 1838 | } 1839 | 1840 | .ion-ios-star-outline:before { 1841 | content: ""; 1842 | } 1843 | 1844 | .ion-ios-stats:before { 1845 | content: ""; 1846 | } 1847 | 1848 | .ion-ios-stopwatch:before { 1849 | content: ""; 1850 | } 1851 | 1852 | .ion-ios-subway:before { 1853 | content: ""; 1854 | } 1855 | 1856 | .ion-ios-sunny:before { 1857 | content: ""; 1858 | } 1859 | 1860 | .ion-ios-swap:before { 1861 | content: ""; 1862 | } 1863 | 1864 | .ion-ios-switch:before { 1865 | content: ""; 1866 | } 1867 | 1868 | .ion-ios-sync:before { 1869 | content: ""; 1870 | } 1871 | 1872 | .ion-ios-tablet-landscape:before { 1873 | content: ""; 1874 | } 1875 | 1876 | .ion-ios-tablet-portrait:before { 1877 | content: ""; 1878 | } 1879 | 1880 | .ion-ios-tennisball:before { 1881 | content: ""; 1882 | } 1883 | 1884 | .ion-ios-text:before { 1885 | content: ""; 1886 | } 1887 | 1888 | .ion-ios-thermometer:before { 1889 | content: ""; 1890 | } 1891 | 1892 | .ion-ios-thumbs-down:before { 1893 | content: ""; 1894 | } 1895 | 1896 | .ion-ios-thumbs-up:before { 1897 | content: ""; 1898 | } 1899 | 1900 | .ion-ios-thunderstorm:before { 1901 | content: ""; 1902 | } 1903 | 1904 | .ion-ios-time:before { 1905 | content: ""; 1906 | } 1907 | 1908 | .ion-ios-timer:before { 1909 | content: ""; 1910 | } 1911 | 1912 | .ion-ios-today:before { 1913 | content: ""; 1914 | } 1915 | 1916 | .ion-ios-train:before { 1917 | content: ""; 1918 | } 1919 | 1920 | .ion-ios-transgender:before { 1921 | content: ""; 1922 | } 1923 | 1924 | .ion-ios-trash:before { 1925 | content: ""; 1926 | } 1927 | 1928 | .ion-ios-trending-down:before { 1929 | content: ""; 1930 | } 1931 | 1932 | .ion-ios-trending-up:before { 1933 | content: ""; 1934 | } 1935 | 1936 | .ion-ios-trophy:before { 1937 | content: ""; 1938 | } 1939 | 1940 | .ion-ios-tv:before { 1941 | content: ""; 1942 | } 1943 | 1944 | .ion-ios-umbrella:before { 1945 | content: ""; 1946 | } 1947 | 1948 | .ion-ios-undo:before { 1949 | content: ""; 1950 | } 1951 | 1952 | .ion-ios-unlock:before { 1953 | content: ""; 1954 | } 1955 | 1956 | .ion-ios-videocam:before { 1957 | content: ""; 1958 | } 1959 | 1960 | .ion-ios-volume-high:before { 1961 | content: ""; 1962 | } 1963 | 1964 | .ion-ios-volume-low:before { 1965 | content: ""; 1966 | } 1967 | 1968 | .ion-ios-volume-mute:before { 1969 | content: ""; 1970 | } 1971 | 1972 | .ion-ios-volume-off:before { 1973 | content: ""; 1974 | } 1975 | 1976 | .ion-ios-walk:before { 1977 | content: ""; 1978 | } 1979 | 1980 | .ion-ios-wallet:before { 1981 | content: ""; 1982 | } 1983 | 1984 | .ion-ios-warning:before { 1985 | content: ""; 1986 | } 1987 | 1988 | .ion-ios-watch:before { 1989 | content: ""; 1990 | } 1991 | 1992 | .ion-ios-water:before { 1993 | content: ""; 1994 | } 1995 | 1996 | .ion-ios-wifi:before { 1997 | content: ""; 1998 | } 1999 | 2000 | .ion-ios-wine:before { 2001 | content: ""; 2002 | } 2003 | 2004 | .ion-ios-woman:before { 2005 | content: ""; 2006 | } 2007 | 2008 | .ion-logo-android:before { 2009 | content: ""; 2010 | } 2011 | 2012 | .ion-logo-angular:before { 2013 | content: ""; 2014 | } 2015 | 2016 | .ion-logo-apple:before { 2017 | content: ""; 2018 | } 2019 | 2020 | .ion-logo-bitbucket:before { 2021 | content: ""; 2022 | } 2023 | 2024 | .ion-logo-bitcoin:before { 2025 | content: ""; 2026 | } 2027 | 2028 | .ion-logo-buffer:before { 2029 | content: ""; 2030 | } 2031 | 2032 | .ion-logo-chrome:before { 2033 | content: ""; 2034 | } 2035 | 2036 | .ion-logo-closed-captioning:before { 2037 | content: ""; 2038 | } 2039 | 2040 | .ion-logo-codepen:before { 2041 | content: ""; 2042 | } 2043 | 2044 | .ion-logo-css3:before { 2045 | content: ""; 2046 | } 2047 | 2048 | .ion-logo-designernews:before { 2049 | content: ""; 2050 | } 2051 | 2052 | .ion-logo-dribbble:before { 2053 | content: ""; 2054 | } 2055 | 2056 | .ion-logo-dropbox:before { 2057 | content: ""; 2058 | } 2059 | 2060 | .ion-logo-euro:before { 2061 | content: ""; 2062 | } 2063 | 2064 | .ion-logo-facebook:before { 2065 | content: ""; 2066 | } 2067 | 2068 | .ion-logo-flickr:before { 2069 | content: ""; 2070 | } 2071 | 2072 | .ion-logo-foursquare:before { 2073 | content: ""; 2074 | } 2075 | 2076 | .ion-logo-freebsd-devil:before { 2077 | content: ""; 2078 | } 2079 | 2080 | .ion-logo-game-controller-a:before { 2081 | content: ""; 2082 | } 2083 | 2084 | .ion-logo-game-controller-b:before { 2085 | content: ""; 2086 | } 2087 | 2088 | .ion-logo-github:before { 2089 | content: ""; 2090 | } 2091 | 2092 | .ion-logo-google:before { 2093 | content: ""; 2094 | } 2095 | 2096 | .ion-logo-googleplus:before { 2097 | content: ""; 2098 | } 2099 | 2100 | .ion-logo-hackernews:before { 2101 | content: ""; 2102 | } 2103 | 2104 | .ion-logo-html5:before { 2105 | content: ""; 2106 | } 2107 | 2108 | .ion-logo-instagram:before { 2109 | content: ""; 2110 | } 2111 | 2112 | .ion-logo-ionic:before { 2113 | content: ""; 2114 | } 2115 | 2116 | .ion-logo-ionitron:before { 2117 | content: ""; 2118 | } 2119 | 2120 | .ion-logo-javascript:before { 2121 | content: ""; 2122 | } 2123 | 2124 | .ion-logo-linkedin:before { 2125 | content: ""; 2126 | } 2127 | 2128 | .ion-logo-markdown:before { 2129 | content: ""; 2130 | } 2131 | 2132 | .ion-logo-model-s:before { 2133 | content: ""; 2134 | } 2135 | 2136 | .ion-logo-no-smoking:before { 2137 | content: ""; 2138 | } 2139 | 2140 | .ion-logo-nodejs:before { 2141 | content: ""; 2142 | } 2143 | 2144 | .ion-logo-npm:before { 2145 | content: ""; 2146 | } 2147 | 2148 | .ion-logo-octocat:before { 2149 | content: ""; 2150 | } 2151 | 2152 | .ion-logo-pinterest:before { 2153 | content: ""; 2154 | } 2155 | 2156 | .ion-logo-playstation:before { 2157 | content: ""; 2158 | } 2159 | 2160 | .ion-logo-polymer:before { 2161 | content: ""; 2162 | } 2163 | 2164 | .ion-logo-python:before { 2165 | content: ""; 2166 | } 2167 | 2168 | .ion-logo-reddit:before { 2169 | content: ""; 2170 | } 2171 | 2172 | .ion-logo-rss:before { 2173 | content: ""; 2174 | } 2175 | 2176 | .ion-logo-sass:before { 2177 | content: ""; 2178 | } 2179 | 2180 | .ion-logo-skype:before { 2181 | content: ""; 2182 | } 2183 | 2184 | .ion-logo-slack:before { 2185 | content: ""; 2186 | } 2187 | 2188 | .ion-logo-snapchat:before { 2189 | content: ""; 2190 | } 2191 | 2192 | .ion-logo-steam:before { 2193 | content: ""; 2194 | } 2195 | 2196 | .ion-logo-tumblr:before { 2197 | content: ""; 2198 | } 2199 | 2200 | .ion-logo-tux:before { 2201 | content: ""; 2202 | } 2203 | 2204 | .ion-logo-twitch:before { 2205 | content: ""; 2206 | } 2207 | 2208 | .ion-logo-twitter:before { 2209 | content: ""; 2210 | } 2211 | 2212 | .ion-logo-usd:before { 2213 | content: ""; 2214 | } 2215 | 2216 | .ion-logo-vimeo:before { 2217 | content: ""; 2218 | } 2219 | 2220 | .ion-logo-vk:before { 2221 | content: ""; 2222 | } 2223 | 2224 | .ion-logo-whatsapp:before { 2225 | content: ""; 2226 | } 2227 | 2228 | .ion-logo-windows:before { 2229 | content: ""; 2230 | } 2231 | 2232 | .ion-logo-wordpress:before { 2233 | content: ""; 2234 | } 2235 | 2236 | .ion-logo-xbox:before { 2237 | content: ""; 2238 | } 2239 | 2240 | .ion-logo-xing:before { 2241 | content: ""; 2242 | } 2243 | 2244 | .ion-logo-yahoo:before { 2245 | content: ""; 2246 | } 2247 | 2248 | .ion-logo-yen:before { 2249 | content: ""; 2250 | } 2251 | 2252 | .ion-logo-youtube:before { 2253 | content: ""; 2254 | } 2255 | 2256 | .ion-md-add:before { 2257 | content: ""; 2258 | } 2259 | 2260 | .ion-md-add-circle:before { 2261 | content: ""; 2262 | } 2263 | 2264 | .ion-md-add-circle-outline:before { 2265 | content: ""; 2266 | } 2267 | 2268 | .ion-md-airplane:before { 2269 | content: ""; 2270 | } 2271 | 2272 | .ion-md-alarm:before { 2273 | content: ""; 2274 | } 2275 | 2276 | .ion-md-albums:before { 2277 | content: ""; 2278 | } 2279 | 2280 | .ion-md-alert:before { 2281 | content: ""; 2282 | } 2283 | 2284 | .ion-md-american-football:before { 2285 | content: ""; 2286 | } 2287 | 2288 | .ion-md-analytics:before { 2289 | content: ""; 2290 | } 2291 | 2292 | .ion-md-aperture:before { 2293 | content: ""; 2294 | } 2295 | 2296 | .ion-md-apps:before { 2297 | content: ""; 2298 | } 2299 | 2300 | .ion-md-appstore:before { 2301 | content: ""; 2302 | } 2303 | 2304 | .ion-md-archive:before { 2305 | content: ""; 2306 | } 2307 | 2308 | .ion-md-arrow-back:before { 2309 | content: ""; 2310 | } 2311 | 2312 | .ion-md-arrow-down:before { 2313 | content: ""; 2314 | } 2315 | 2316 | .ion-md-arrow-dropdown:before { 2317 | content: ""; 2318 | } 2319 | 2320 | .ion-md-arrow-dropdown-circle:before { 2321 | content: ""; 2322 | } 2323 | 2324 | .ion-md-arrow-dropleft:before { 2325 | content: ""; 2326 | } 2327 | 2328 | .ion-md-arrow-dropleft-circle:before { 2329 | content: ""; 2330 | } 2331 | 2332 | .ion-md-arrow-dropright:before { 2333 | content: ""; 2334 | } 2335 | 2336 | .ion-md-arrow-dropright-circle:before { 2337 | content: ""; 2338 | } 2339 | 2340 | .ion-md-arrow-dropup:before { 2341 | content: ""; 2342 | } 2343 | 2344 | .ion-md-arrow-dropup-circle:before { 2345 | content: ""; 2346 | } 2347 | 2348 | .ion-md-arrow-forward:before { 2349 | content: ""; 2350 | } 2351 | 2352 | .ion-md-arrow-round-back:before { 2353 | content: ""; 2354 | } 2355 | 2356 | .ion-md-arrow-round-down:before { 2357 | content: ""; 2358 | } 2359 | 2360 | .ion-md-arrow-round-forward:before { 2361 | content: ""; 2362 | } 2363 | 2364 | .ion-md-arrow-round-up:before { 2365 | content: ""; 2366 | } 2367 | 2368 | .ion-md-arrow-up:before { 2369 | content: ""; 2370 | } 2371 | 2372 | .ion-md-at:before { 2373 | content: ""; 2374 | } 2375 | 2376 | .ion-md-attach:before { 2377 | content: ""; 2378 | } 2379 | 2380 | .ion-md-backspace:before { 2381 | content: ""; 2382 | } 2383 | 2384 | .ion-md-barcode:before { 2385 | content: ""; 2386 | } 2387 | 2388 | .ion-md-baseball:before { 2389 | content: ""; 2390 | } 2391 | 2392 | .ion-md-basket:before { 2393 | content: ""; 2394 | } 2395 | 2396 | .ion-md-basketball:before { 2397 | content: ""; 2398 | } 2399 | 2400 | .ion-md-battery-charging:before { 2401 | content: ""; 2402 | } 2403 | 2404 | .ion-md-battery-dead:before { 2405 | content: ""; 2406 | } 2407 | 2408 | .ion-md-battery-full:before { 2409 | content: ""; 2410 | } 2411 | 2412 | .ion-md-beaker:before { 2413 | content: ""; 2414 | } 2415 | 2416 | .ion-md-bed:before { 2417 | content: ""; 2418 | } 2419 | 2420 | .ion-md-beer:before { 2421 | content: ""; 2422 | } 2423 | 2424 | .ion-md-bicycle:before { 2425 | content: ""; 2426 | } 2427 | 2428 | .ion-md-bluetooth:before { 2429 | content: ""; 2430 | } 2431 | 2432 | .ion-md-boat:before { 2433 | content: ""; 2434 | } 2435 | 2436 | .ion-md-body:before { 2437 | content: ""; 2438 | } 2439 | 2440 | .ion-md-bonfire:before { 2441 | content: ""; 2442 | } 2443 | 2444 | .ion-md-book:before { 2445 | content: ""; 2446 | } 2447 | 2448 | .ion-md-bookmark:before { 2449 | content: ""; 2450 | } 2451 | 2452 | .ion-md-bookmarks:before { 2453 | content: ""; 2454 | } 2455 | 2456 | .ion-md-bowtie:before { 2457 | content: ""; 2458 | } 2459 | 2460 | .ion-md-briefcase:before { 2461 | content: ""; 2462 | } 2463 | 2464 | .ion-md-browsers:before { 2465 | content: ""; 2466 | } 2467 | 2468 | .ion-md-brush:before { 2469 | content: ""; 2470 | } 2471 | 2472 | .ion-md-bug:before { 2473 | content: ""; 2474 | } 2475 | 2476 | .ion-md-build:before { 2477 | content: ""; 2478 | } 2479 | 2480 | .ion-md-bulb:before { 2481 | content: ""; 2482 | } 2483 | 2484 | .ion-md-bus:before { 2485 | content: ""; 2486 | } 2487 | 2488 | .ion-md-business:before { 2489 | content: ""; 2490 | } 2491 | 2492 | .ion-md-cafe:before { 2493 | content: ""; 2494 | } 2495 | 2496 | .ion-md-calculator:before { 2497 | content: ""; 2498 | } 2499 | 2500 | .ion-md-calendar:before { 2501 | content: ""; 2502 | } 2503 | 2504 | .ion-md-call:before { 2505 | content: ""; 2506 | } 2507 | 2508 | .ion-md-camera:before { 2509 | content: ""; 2510 | } 2511 | 2512 | .ion-md-car:before { 2513 | content: ""; 2514 | } 2515 | 2516 | .ion-md-card:before { 2517 | content: ""; 2518 | } 2519 | 2520 | .ion-md-cart:before { 2521 | content: ""; 2522 | } 2523 | 2524 | .ion-md-cash:before { 2525 | content: ""; 2526 | } 2527 | 2528 | .ion-md-cellular:before { 2529 | content: ""; 2530 | } 2531 | 2532 | .ion-md-chatboxes:before { 2533 | content: ""; 2534 | } 2535 | 2536 | .ion-md-chatbubbles:before { 2537 | content: ""; 2538 | } 2539 | 2540 | .ion-md-checkbox:before { 2541 | content: ""; 2542 | } 2543 | 2544 | .ion-md-checkbox-outline:before { 2545 | content: ""; 2546 | } 2547 | 2548 | .ion-md-checkmark:before { 2549 | content: ""; 2550 | } 2551 | 2552 | .ion-md-checkmark-circle:before { 2553 | content: ""; 2554 | } 2555 | 2556 | .ion-md-checkmark-circle-outline:before { 2557 | content: ""; 2558 | } 2559 | 2560 | .ion-md-clipboard:before { 2561 | content: ""; 2562 | } 2563 | 2564 | .ion-md-clock:before { 2565 | content: ""; 2566 | } 2567 | 2568 | .ion-md-close:before { 2569 | content: ""; 2570 | } 2571 | 2572 | .ion-md-close-circle:before { 2573 | content: ""; 2574 | } 2575 | 2576 | .ion-md-close-circle-outline:before { 2577 | content: ""; 2578 | } 2579 | 2580 | .ion-md-cloud:before { 2581 | content: ""; 2582 | } 2583 | 2584 | .ion-md-cloud-circle:before { 2585 | content: ""; 2586 | } 2587 | 2588 | .ion-md-cloud-done:before { 2589 | content: ""; 2590 | } 2591 | 2592 | .ion-md-cloud-download:before { 2593 | content: ""; 2594 | } 2595 | 2596 | .ion-md-cloud-outline:before { 2597 | content: ""; 2598 | } 2599 | 2600 | .ion-md-cloud-upload:before { 2601 | content: ""; 2602 | } 2603 | 2604 | .ion-md-cloudy:before { 2605 | content: ""; 2606 | } 2607 | 2608 | .ion-md-cloudy-night:before { 2609 | content: ""; 2610 | } 2611 | 2612 | .ion-md-code:before { 2613 | content: ""; 2614 | } 2615 | 2616 | .ion-md-code-download:before { 2617 | content: ""; 2618 | } 2619 | 2620 | .ion-md-code-working:before { 2621 | content: ""; 2622 | } 2623 | 2624 | .ion-md-cog:before { 2625 | content: ""; 2626 | } 2627 | 2628 | .ion-md-color-fill:before { 2629 | content: ""; 2630 | } 2631 | 2632 | .ion-md-color-filter:before { 2633 | content: ""; 2634 | } 2635 | 2636 | .ion-md-color-palette:before { 2637 | content: ""; 2638 | } 2639 | 2640 | .ion-md-color-wand:before { 2641 | content: ""; 2642 | } 2643 | 2644 | .ion-md-compass:before { 2645 | content: ""; 2646 | } 2647 | 2648 | .ion-md-construct:before { 2649 | content: ""; 2650 | } 2651 | 2652 | .ion-md-contact:before { 2653 | content: ""; 2654 | } 2655 | 2656 | .ion-md-contacts:before { 2657 | content: ""; 2658 | } 2659 | 2660 | .ion-md-contract:before { 2661 | content: ""; 2662 | } 2663 | 2664 | .ion-md-contrast:before { 2665 | content: ""; 2666 | } 2667 | 2668 | .ion-md-copy:before { 2669 | content: ""; 2670 | } 2671 | 2672 | .ion-md-create:before { 2673 | content: ""; 2674 | } 2675 | 2676 | .ion-md-crop:before { 2677 | content: ""; 2678 | } 2679 | 2680 | .ion-md-cube:before { 2681 | content: ""; 2682 | } 2683 | 2684 | .ion-md-cut:before { 2685 | content: ""; 2686 | } 2687 | 2688 | .ion-md-desktop:before { 2689 | content: ""; 2690 | } 2691 | 2692 | .ion-md-disc:before { 2693 | content: ""; 2694 | } 2695 | 2696 | .ion-md-document:before { 2697 | content: ""; 2698 | } 2699 | 2700 | .ion-md-done-all:before { 2701 | content: ""; 2702 | } 2703 | 2704 | .ion-md-download:before { 2705 | content: ""; 2706 | } 2707 | 2708 | .ion-md-easel:before { 2709 | content: ""; 2710 | } 2711 | 2712 | .ion-md-egg:before { 2713 | content: ""; 2714 | } 2715 | 2716 | .ion-md-exit:before { 2717 | content: ""; 2718 | } 2719 | 2720 | .ion-md-expand:before { 2721 | content: ""; 2722 | } 2723 | 2724 | .ion-md-eye:before { 2725 | content: ""; 2726 | } 2727 | 2728 | .ion-md-eye-off:before { 2729 | content: ""; 2730 | } 2731 | 2732 | .ion-md-fastforward:before { 2733 | content: ""; 2734 | } 2735 | 2736 | .ion-md-female:before { 2737 | content: ""; 2738 | } 2739 | 2740 | .ion-md-filing:before { 2741 | content: ""; 2742 | } 2743 | 2744 | .ion-md-film:before { 2745 | content: ""; 2746 | } 2747 | 2748 | .ion-md-finger-print:before { 2749 | content: ""; 2750 | } 2751 | 2752 | .ion-md-fitness:before { 2753 | content: ""; 2754 | } 2755 | 2756 | .ion-md-flag:before { 2757 | content: ""; 2758 | } 2759 | 2760 | .ion-md-flame:before { 2761 | content: ""; 2762 | } 2763 | 2764 | .ion-md-flash:before { 2765 | content: ""; 2766 | } 2767 | 2768 | .ion-md-flash-off:before { 2769 | content: ""; 2770 | } 2771 | 2772 | .ion-md-flashlight:before { 2773 | content: ""; 2774 | } 2775 | 2776 | .ion-md-flask:before { 2777 | content: ""; 2778 | } 2779 | 2780 | .ion-md-flower:before { 2781 | content: ""; 2782 | } 2783 | 2784 | .ion-md-folder:before { 2785 | content: ""; 2786 | } 2787 | 2788 | .ion-md-folder-open:before { 2789 | content: ""; 2790 | } 2791 | 2792 | .ion-md-football:before { 2793 | content: ""; 2794 | } 2795 | 2796 | .ion-md-funnel:before { 2797 | content: ""; 2798 | } 2799 | 2800 | .ion-md-gift:before { 2801 | content: ""; 2802 | } 2803 | 2804 | .ion-md-git-branch:before { 2805 | content: ""; 2806 | } 2807 | 2808 | .ion-md-git-commit:before { 2809 | content: ""; 2810 | } 2811 | 2812 | .ion-md-git-compare:before { 2813 | content: ""; 2814 | } 2815 | 2816 | .ion-md-git-merge:before { 2817 | content: ""; 2818 | } 2819 | 2820 | .ion-md-git-network:before { 2821 | content: ""; 2822 | } 2823 | 2824 | .ion-md-git-pull-request:before { 2825 | content: ""; 2826 | } 2827 | 2828 | .ion-md-glasses:before { 2829 | content: ""; 2830 | } 2831 | 2832 | .ion-md-globe:before { 2833 | content: ""; 2834 | } 2835 | 2836 | .ion-md-grid:before { 2837 | content: ""; 2838 | } 2839 | 2840 | .ion-md-hammer:before { 2841 | content: ""; 2842 | } 2843 | 2844 | .ion-md-hand:before { 2845 | content: ""; 2846 | } 2847 | 2848 | .ion-md-happy:before { 2849 | content: ""; 2850 | } 2851 | 2852 | .ion-md-headset:before { 2853 | content: ""; 2854 | } 2855 | 2856 | .ion-md-heart:before { 2857 | content: ""; 2858 | } 2859 | 2860 | .ion-md-heart-dislike:before { 2861 | content: ""; 2862 | } 2863 | 2864 | .ion-md-heart-empty:before { 2865 | content: ""; 2866 | } 2867 | 2868 | .ion-md-heart-half:before { 2869 | content: ""; 2870 | } 2871 | 2872 | .ion-md-help:before { 2873 | content: ""; 2874 | } 2875 | 2876 | .ion-md-help-buoy:before { 2877 | content: ""; 2878 | } 2879 | 2880 | .ion-md-help-circle:before { 2881 | content: ""; 2882 | } 2883 | 2884 | .ion-md-help-circle-outline:before { 2885 | content: ""; 2886 | } 2887 | 2888 | .ion-md-home:before { 2889 | content: ""; 2890 | } 2891 | 2892 | .ion-md-hourglass:before { 2893 | content: ""; 2894 | } 2895 | 2896 | .ion-md-ice-cream:before { 2897 | content: ""; 2898 | } 2899 | 2900 | .ion-md-image:before { 2901 | content: ""; 2902 | } 2903 | 2904 | .ion-md-images:before { 2905 | content: ""; 2906 | } 2907 | 2908 | .ion-md-infinite:before { 2909 | content: ""; 2910 | } 2911 | 2912 | .ion-md-information:before { 2913 | content: ""; 2914 | } 2915 | 2916 | .ion-md-information-circle:before { 2917 | content: ""; 2918 | } 2919 | 2920 | .ion-md-information-circle-outline:before { 2921 | content: ""; 2922 | } 2923 | 2924 | .ion-md-jet:before { 2925 | content: ""; 2926 | } 2927 | 2928 | .ion-md-journal:before { 2929 | content: ""; 2930 | } 2931 | 2932 | .ion-md-key:before { 2933 | content: ""; 2934 | } 2935 | 2936 | .ion-md-keypad:before { 2937 | content: ""; 2938 | } 2939 | 2940 | .ion-md-laptop:before { 2941 | content: ""; 2942 | } 2943 | 2944 | .ion-md-leaf:before { 2945 | content: ""; 2946 | } 2947 | 2948 | .ion-md-link:before { 2949 | content: ""; 2950 | } 2951 | 2952 | .ion-md-list:before { 2953 | content: ""; 2954 | } 2955 | 2956 | .ion-md-list-box:before { 2957 | content: ""; 2958 | } 2959 | 2960 | .ion-md-locate:before { 2961 | content: ""; 2962 | } 2963 | 2964 | .ion-md-lock:before { 2965 | content: ""; 2966 | } 2967 | 2968 | .ion-md-log-in:before { 2969 | content: ""; 2970 | } 2971 | 2972 | .ion-md-log-out:before { 2973 | content: ""; 2974 | } 2975 | 2976 | .ion-md-magnet:before { 2977 | content: ""; 2978 | } 2979 | 2980 | .ion-md-mail:before { 2981 | content: ""; 2982 | } 2983 | 2984 | .ion-md-mail-open:before { 2985 | content: ""; 2986 | } 2987 | 2988 | .ion-md-mail-unread:before { 2989 | content: ""; 2990 | } 2991 | 2992 | .ion-md-male:before { 2993 | content: ""; 2994 | } 2995 | 2996 | .ion-md-man:before { 2997 | content: ""; 2998 | } 2999 | 3000 | .ion-md-map:before { 3001 | content: ""; 3002 | } 3003 | 3004 | .ion-md-medal:before { 3005 | content: ""; 3006 | } 3007 | 3008 | .ion-md-medical:before { 3009 | content: ""; 3010 | } 3011 | 3012 | .ion-md-medkit:before { 3013 | content: ""; 3014 | } 3015 | 3016 | .ion-md-megaphone:before { 3017 | content: ""; 3018 | } 3019 | 3020 | .ion-md-menu:before { 3021 | content: ""; 3022 | } 3023 | 3024 | .ion-md-mic:before { 3025 | content: ""; 3026 | } 3027 | 3028 | .ion-md-mic-off:before { 3029 | content: ""; 3030 | } 3031 | 3032 | .ion-md-microphone:before { 3033 | content: ""; 3034 | } 3035 | 3036 | .ion-md-moon:before { 3037 | content: ""; 3038 | } 3039 | 3040 | .ion-md-more:before { 3041 | content: ""; 3042 | } 3043 | 3044 | .ion-md-move:before { 3045 | content: ""; 3046 | } 3047 | 3048 | .ion-md-musical-note:before { 3049 | content: ""; 3050 | } 3051 | 3052 | .ion-md-musical-notes:before { 3053 | content: ""; 3054 | } 3055 | 3056 | .ion-md-navigate:before { 3057 | content: ""; 3058 | } 3059 | 3060 | .ion-md-notifications:before { 3061 | content: ""; 3062 | } 3063 | 3064 | .ion-md-notifications-off:before { 3065 | content: ""; 3066 | } 3067 | 3068 | .ion-md-notifications-outline:before { 3069 | content: ""; 3070 | } 3071 | 3072 | .ion-md-nuclear:before { 3073 | content: ""; 3074 | } 3075 | 3076 | .ion-md-nutrition:before { 3077 | content: ""; 3078 | } 3079 | 3080 | .ion-md-open:before { 3081 | content: ""; 3082 | } 3083 | 3084 | .ion-md-options:before { 3085 | content: ""; 3086 | } 3087 | 3088 | .ion-md-outlet:before { 3089 | content: ""; 3090 | } 3091 | 3092 | .ion-md-paper:before { 3093 | content: ""; 3094 | } 3095 | 3096 | .ion-md-paper-plane:before { 3097 | content: ""; 3098 | } 3099 | 3100 | .ion-md-partly-sunny:before { 3101 | content: ""; 3102 | } 3103 | 3104 | .ion-md-pause:before { 3105 | content: ""; 3106 | } 3107 | 3108 | .ion-md-paw:before { 3109 | content: ""; 3110 | } 3111 | 3112 | .ion-md-people:before { 3113 | content: ""; 3114 | } 3115 | 3116 | .ion-md-person:before { 3117 | content: ""; 3118 | } 3119 | 3120 | .ion-md-person-add:before { 3121 | content: ""; 3122 | } 3123 | 3124 | .ion-md-phone-landscape:before { 3125 | content: ""; 3126 | } 3127 | 3128 | .ion-md-phone-portrait:before { 3129 | content: ""; 3130 | } 3131 | 3132 | .ion-md-photos:before { 3133 | content: ""; 3134 | } 3135 | 3136 | .ion-md-pie:before { 3137 | content: ""; 3138 | } 3139 | 3140 | .ion-md-pin:before { 3141 | content: ""; 3142 | } 3143 | 3144 | .ion-md-pint:before { 3145 | content: ""; 3146 | } 3147 | 3148 | .ion-md-pizza:before { 3149 | content: ""; 3150 | } 3151 | 3152 | .ion-md-planet:before { 3153 | content: ""; 3154 | } 3155 | 3156 | .ion-md-play:before { 3157 | content: ""; 3158 | } 3159 | 3160 | .ion-md-play-circle:before { 3161 | content: ""; 3162 | } 3163 | 3164 | .ion-md-podium:before { 3165 | content: ""; 3166 | } 3167 | 3168 | .ion-md-power:before { 3169 | content: ""; 3170 | } 3171 | 3172 | .ion-md-pricetag:before { 3173 | content: ""; 3174 | } 3175 | 3176 | .ion-md-pricetags:before { 3177 | content: ""; 3178 | } 3179 | 3180 | .ion-md-print:before { 3181 | content: ""; 3182 | } 3183 | 3184 | .ion-md-pulse:before { 3185 | content: ""; 3186 | } 3187 | 3188 | .ion-md-qr-scanner:before { 3189 | content: ""; 3190 | } 3191 | 3192 | .ion-md-quote:before { 3193 | content: ""; 3194 | } 3195 | 3196 | .ion-md-radio:before { 3197 | content: ""; 3198 | } 3199 | 3200 | .ion-md-radio-button-off:before { 3201 | content: ""; 3202 | } 3203 | 3204 | .ion-md-radio-button-on:before { 3205 | content: ""; 3206 | } 3207 | 3208 | .ion-md-rainy:before { 3209 | content: ""; 3210 | } 3211 | 3212 | .ion-md-recording:before { 3213 | content: ""; 3214 | } 3215 | 3216 | .ion-md-redo:before { 3217 | content: ""; 3218 | } 3219 | 3220 | .ion-md-refresh:before { 3221 | content: ""; 3222 | } 3223 | 3224 | .ion-md-refresh-circle:before { 3225 | content: ""; 3226 | } 3227 | 3228 | .ion-md-remove:before { 3229 | content: ""; 3230 | } 3231 | 3232 | .ion-md-remove-circle:before { 3233 | content: ""; 3234 | } 3235 | 3236 | .ion-md-remove-circle-outline:before { 3237 | content: ""; 3238 | } 3239 | 3240 | .ion-md-reorder:before { 3241 | content: ""; 3242 | } 3243 | 3244 | .ion-md-repeat:before { 3245 | content: ""; 3246 | } 3247 | 3248 | .ion-md-resize:before { 3249 | content: ""; 3250 | } 3251 | 3252 | .ion-md-restaurant:before { 3253 | content: ""; 3254 | } 3255 | 3256 | .ion-md-return-left:before { 3257 | content: ""; 3258 | } 3259 | 3260 | .ion-md-return-right:before { 3261 | content: ""; 3262 | } 3263 | 3264 | .ion-md-reverse-camera:before { 3265 | content: ""; 3266 | } 3267 | 3268 | .ion-md-rewind:before { 3269 | content: ""; 3270 | } 3271 | 3272 | .ion-md-ribbon:before { 3273 | content: ""; 3274 | } 3275 | 3276 | .ion-md-rocket:before { 3277 | content: ""; 3278 | } 3279 | 3280 | .ion-md-rose:before { 3281 | content: ""; 3282 | } 3283 | 3284 | .ion-md-sad:before { 3285 | content: ""; 3286 | } 3287 | 3288 | .ion-md-save:before { 3289 | content: ""; 3290 | } 3291 | 3292 | .ion-md-school:before { 3293 | content: ""; 3294 | } 3295 | 3296 | .ion-md-search:before { 3297 | content: ""; 3298 | } 3299 | 3300 | .ion-md-send:before { 3301 | content: ""; 3302 | } 3303 | 3304 | .ion-md-settings:before { 3305 | content: ""; 3306 | } 3307 | 3308 | .ion-md-share:before { 3309 | content: ""; 3310 | } 3311 | 3312 | .ion-md-share-alt:before { 3313 | content: ""; 3314 | } 3315 | 3316 | .ion-md-shirt:before { 3317 | content: ""; 3318 | } 3319 | 3320 | .ion-md-shuffle:before { 3321 | content: ""; 3322 | } 3323 | 3324 | .ion-md-skip-backward:before { 3325 | content: ""; 3326 | } 3327 | 3328 | .ion-md-skip-forward:before { 3329 | content: ""; 3330 | } 3331 | 3332 | .ion-md-snow:before { 3333 | content: ""; 3334 | } 3335 | 3336 | .ion-md-speedometer:before { 3337 | content: ""; 3338 | } 3339 | 3340 | .ion-md-square:before { 3341 | content: ""; 3342 | } 3343 | 3344 | .ion-md-square-outline:before { 3345 | content: ""; 3346 | } 3347 | 3348 | .ion-md-star:before { 3349 | content: ""; 3350 | } 3351 | 3352 | .ion-md-star-half:before { 3353 | content: ""; 3354 | } 3355 | 3356 | .ion-md-star-outline:before { 3357 | content: ""; 3358 | } 3359 | 3360 | .ion-md-stats:before { 3361 | content: ""; 3362 | } 3363 | 3364 | .ion-md-stopwatch:before { 3365 | content: ""; 3366 | } 3367 | 3368 | .ion-md-subway:before { 3369 | content: ""; 3370 | } 3371 | 3372 | .ion-md-sunny:before { 3373 | content: ""; 3374 | } 3375 | 3376 | .ion-md-swap:before { 3377 | content: ""; 3378 | } 3379 | 3380 | .ion-md-switch:before { 3381 | content: ""; 3382 | } 3383 | 3384 | .ion-md-sync:before { 3385 | content: ""; 3386 | } 3387 | 3388 | .ion-md-tablet-landscape:before { 3389 | content: ""; 3390 | } 3391 | 3392 | .ion-md-tablet-portrait:before { 3393 | content: ""; 3394 | } 3395 | 3396 | .ion-md-tennisball:before { 3397 | content: ""; 3398 | } 3399 | 3400 | .ion-md-text:before { 3401 | content: ""; 3402 | } 3403 | 3404 | .ion-md-thermometer:before { 3405 | content: ""; 3406 | } 3407 | 3408 | .ion-md-thumbs-down:before { 3409 | content: ""; 3410 | } 3411 | 3412 | .ion-md-thumbs-up:before { 3413 | content: ""; 3414 | } 3415 | 3416 | .ion-md-thunderstorm:before { 3417 | content: ""; 3418 | } 3419 | 3420 | .ion-md-time:before { 3421 | content: ""; 3422 | } 3423 | 3424 | .ion-md-timer:before { 3425 | content: ""; 3426 | } 3427 | 3428 | .ion-md-today:before { 3429 | content: ""; 3430 | } 3431 | 3432 | .ion-md-train:before { 3433 | content: ""; 3434 | } 3435 | 3436 | .ion-md-transgender:before { 3437 | content: ""; 3438 | } 3439 | 3440 | .ion-md-trash:before { 3441 | content: ""; 3442 | } 3443 | 3444 | .ion-md-trending-down:before { 3445 | content: ""; 3446 | } 3447 | 3448 | .ion-md-trending-up:before { 3449 | content: ""; 3450 | } 3451 | 3452 | .ion-md-trophy:before { 3453 | content: ""; 3454 | } 3455 | 3456 | .ion-md-tv:before { 3457 | content: ""; 3458 | } 3459 | 3460 | .ion-md-umbrella:before { 3461 | content: ""; 3462 | } 3463 | 3464 | .ion-md-undo:before { 3465 | content: ""; 3466 | } 3467 | 3468 | .ion-md-unlock:before { 3469 | content: ""; 3470 | } 3471 | 3472 | .ion-md-videocam:before { 3473 | content: ""; 3474 | } 3475 | 3476 | .ion-md-volume-high:before { 3477 | content: ""; 3478 | } 3479 | 3480 | .ion-md-volume-low:before { 3481 | content: ""; 3482 | } 3483 | 3484 | .ion-md-volume-mute:before { 3485 | content: ""; 3486 | } 3487 | 3488 | .ion-md-volume-off:before { 3489 | content: ""; 3490 | } 3491 | 3492 | .ion-md-walk:before { 3493 | content: ""; 3494 | } 3495 | 3496 | .ion-md-wallet:before { 3497 | content: ""; 3498 | } 3499 | 3500 | .ion-md-warning:before { 3501 | content: ""; 3502 | } 3503 | 3504 | .ion-md-watch:before { 3505 | content: ""; 3506 | } 3507 | 3508 | .ion-md-water:before { 3509 | content: ""; 3510 | } 3511 | 3512 | .ion-md-wifi:before { 3513 | content: ""; 3514 | } 3515 | 3516 | .ion-md-wine:before { 3517 | content: ""; 3518 | } 3519 | 3520 | .ion-md-woman:before { 3521 | content: ""; 3522 | } --------------------------------------------------------------------------------