├── .eslintignore ├── demo.gif ├── app ├── popup │ ├── root.jsx │ ├── img │ │ ├── stars.png │ │ ├── offline-icon.png │ │ ├── jellyfish-blur.png │ │ └── jellyfish-large.png │ ├── fonts │ │ ├── dripicons.ttf │ │ ├── maven_pro_bold-webfont.ttf │ │ ├── maven_pro_medium-webfont.ttf │ │ └── maven_pro_regular-webfont.ttf │ ├── styles │ │ ├── common.less │ │ ├── animations.less │ │ ├── file-drop.less │ │ ├── menu.less │ │ ├── loader.less │ │ └── fonts.less │ └── js │ │ ├── screens │ │ ├── README.md │ │ ├── menu │ │ │ ├── start.js │ │ │ └── profile.js │ │ └── menu.js │ │ └── components │ │ └── view │ │ ├── icon.js │ │ ├── ipfs-logo.js │ │ ├── header.js │ │ ├── loader.js │ │ ├── README.md │ │ ├── details.js │ │ ├── icon-button.js │ │ └── simple-stat.js └── options │ ├── root.jsx │ └── components │ ├── menu.jsx │ └── settings.jsx ├── .npmignore ├── chrome ├── assets │ └── img │ │ ├── icon-16.png │ │ ├── icon-48.png │ │ └── icon-128.png ├── app │ ├── background │ │ ├── index.js │ │ ├── storage-defaults.js │ │ ├── badge.js │ │ ├── interceptor.js │ │ └── api.js │ ├── popup │ │ └── index.js │ └── options │ │ └── index.js ├── views │ ├── background.pug │ ├── options.pug │ └── popup.pug ├── manifest.prod.json └── manifest.dev.json ├── .babelrc ├── .gitignore ├── LICENSE ├── webpack ├── replace │ ├── log-apply-result.js │ └── JsonpMainTemplate.runtime.js ├── prod.config.js └── dev.config.js ├── package.json ├── README.md └── gulpfile.babel.js /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | dev 4 | webpack/replace -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fbaiodias/ipfs-chrome-station/HEAD/demo.gif -------------------------------------------------------------------------------- /app/popup/root.jsx: -------------------------------------------------------------------------------- 1 | import Menu from './js/screens/menu' 2 | 3 | export default Menu 4 | -------------------------------------------------------------------------------- /app/options/root.jsx: -------------------------------------------------------------------------------- 1 | import Menu from './components/menu' 2 | 3 | export default Menu 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .DS_Store 4 | 5 | build/ 6 | dev/ 7 | 8 | *.zip -------------------------------------------------------------------------------- /app/popup/img/stars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fbaiodias/ipfs-chrome-station/HEAD/app/popup/img/stars.png -------------------------------------------------------------------------------- /app/popup/fonts/dripicons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fbaiodias/ipfs-chrome-station/HEAD/app/popup/fonts/dripicons.ttf -------------------------------------------------------------------------------- /app/popup/styles/common.less: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | font-family: 'Maven Pro', sans-serif; 4 | overflow: hidden; 5 | } 6 | -------------------------------------------------------------------------------- /chrome/assets/img/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fbaiodias/ipfs-chrome-station/HEAD/chrome/assets/img/icon-16.png -------------------------------------------------------------------------------- /chrome/assets/img/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fbaiodias/ipfs-chrome-station/HEAD/chrome/assets/img/icon-48.png -------------------------------------------------------------------------------- /app/popup/img/offline-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fbaiodias/ipfs-chrome-station/HEAD/app/popup/img/offline-icon.png -------------------------------------------------------------------------------- /chrome/assets/img/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fbaiodias/ipfs-chrome-station/HEAD/chrome/assets/img/icon-128.png -------------------------------------------------------------------------------- /app/popup/img/jellyfish-blur.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fbaiodias/ipfs-chrome-station/HEAD/app/popup/img/jellyfish-blur.png -------------------------------------------------------------------------------- /app/popup/img/jellyfish-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fbaiodias/ipfs-chrome-station/HEAD/app/popup/img/jellyfish-large.png -------------------------------------------------------------------------------- /chrome/app/background/index.js: -------------------------------------------------------------------------------- 1 | require('./storage-defaults') 2 | require('./badge') 3 | require('./interceptor') 4 | require('./api') 5 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "es2015", "stage-0", "react" ], 3 | "plugins": [ "add-module-exports", "transform-decorators-legacy" ] 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .DS_Store 4 | 5 | build/ 6 | dev/ 7 | 8 | *.zip 9 | *.swp 10 | 11 | *.crx 12 | *.pem 13 | -------------------------------------------------------------------------------- /app/popup/fonts/maven_pro_bold-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fbaiodias/ipfs-chrome-station/HEAD/app/popup/fonts/maven_pro_bold-webfont.ttf -------------------------------------------------------------------------------- /app/popup/fonts/maven_pro_medium-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fbaiodias/ipfs-chrome-station/HEAD/app/popup/fonts/maven_pro_medium-webfont.ttf -------------------------------------------------------------------------------- /app/popup/fonts/maven_pro_regular-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fbaiodias/ipfs-chrome-station/HEAD/app/popup/fonts/maven_pro_regular-webfont.ttf -------------------------------------------------------------------------------- /chrome/views/background.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | 3 | html 4 | head 5 | script(src=env == 'prod' ? '/background.bundle.js' : 'http://localhost:3000/js/background.bundle.js') 6 | -------------------------------------------------------------------------------- /chrome/app/popup/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import Root from '../../../app/popup/root' 4 | 5 | ReactDOM.render( 6 | , 7 | document.querySelector('#root') 8 | ) 9 | -------------------------------------------------------------------------------- /chrome/app/options/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import Root from '../../../app/options/root' 4 | 5 | ReactDOM.render( 6 | , 7 | document.querySelector('#root') 8 | ) 9 | -------------------------------------------------------------------------------- /app/popup/js/screens/README.md: -------------------------------------------------------------------------------- 1 | # Screens 2 | 3 | A screen is the everything visible at a given point in time inside a single window. 4 | These are implemented as react components, but differ in so far as they are an aggregation 5 | of multiple small components to one larger, complete picture. 6 | -------------------------------------------------------------------------------- /chrome/views/options.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | 3 | html 4 | head 5 | meta(charset='UTF-8') 6 | title IPFS Station Options 7 | style. 8 | body { width: 400px; min-height: 220px } 9 | body 10 | #root 11 | script(src=env == 'prod' ? '/options.bundle.js' : 'http://localhost:3000/js/options.bundle.js') 12 | -------------------------------------------------------------------------------- /app/popup/styles/animations.less: -------------------------------------------------------------------------------- 1 | .fade-enter { 2 | opacity: 0.01; 3 | } 4 | 5 | .fade-enter.fade-enter-active { 6 | opacity: 1; 7 | transition: opacity .3s ease-in; 8 | } 9 | 10 | .fade-leave { 11 | opacity: 1; 12 | } 13 | 14 | .fade-leave.fade-leave-active { 15 | opacity: 0.01; 16 | transition: opacity .2s ease-in; 17 | } 18 | -------------------------------------------------------------------------------- /chrome/views/popup.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | 3 | html 4 | head 5 | meta(charset='UTF-8') 6 | title IPFS Station 7 | style. 8 | body { width: 450px; min-height: 250px; background-color: #19b5fe; } 9 | 10 | body 11 | #root 12 | script(src=env == 'prod' ? '/popup.bundle.js' : 'http://localhost:3000/js/popup.bundle.js') 13 | -------------------------------------------------------------------------------- /app/popup/styles/file-drop.less: -------------------------------------------------------------------------------- 1 | .file-drop { 2 | position: absolute; 3 | top: 40px; 4 | left: 0; 5 | right: 0; 6 | bottom: 70px; 7 | } 8 | 9 | .file-drop .file-drop-target.file-drop-dragging-over-frame, 10 | .file-drop .file-drop-target.file-drop-dragging-over-target { 11 | height: 100%; 12 | width: 100%; 13 | background: rgba(255, 255, 255, 0.8); 14 | } -------------------------------------------------------------------------------- /app/popup/js/components/view/icon.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react' 2 | import Radium from 'radium' 3 | 4 | @Radium 5 | export default class Icon extends Component { 6 | static propTypes = { 7 | name: PropTypes.string.isRequired 8 | }; 9 | 10 | render () { 11 | return ( 12 |
13 | ) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /chrome/app/background/storage-defaults.js: -------------------------------------------------------------------------------- 1 | /* global chrome */ 2 | 3 | const defaultValues = { 4 | redirecting: false, 5 | host: 'localhost', 6 | port: 8080, 7 | apiPort: 5001, 8 | apiInterval: 5000 9 | } 10 | 11 | chrome.storage.local.set({ 12 | running: false, 13 | peersCount: 0 14 | }) 15 | 16 | chrome.storage.sync.get(Object.keys(defaultValues), (result) => { 17 | const next = { 18 | ...defaultValues, 19 | ...result 20 | } 21 | 22 | chrome.storage.sync.set(next) 23 | }) 24 | -------------------------------------------------------------------------------- /app/popup/js/components/view/ipfs-logo.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const styles = { 4 | wrapper: { 5 | display: 'flex' 6 | }, 7 | image: { 8 | marginRight: '5px' 9 | } 10 | } 11 | 12 | const logoPath = require('ipfs-logo/raster-generated/ipfs-logo-128-white-outline.png') 13 | 14 | export default function IPFSLogo () { 15 | return ( 16 |
17 | 23 | IPFS 24 |
25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /chrome/app/background/badge.js: -------------------------------------------------------------------------------- 1 | /* global chrome */ 2 | 3 | chrome.storage.onChanged.addListener(function (changes, namespace) { 4 | Object.keys(changes).forEach(key => { 5 | const storageChange = changes[key] 6 | 7 | if (key === 'running' && storageChange.newValue === false) { 8 | chrome.browserAction.setBadgeBackgroundColor({ color: '#ff0000' }) 9 | chrome.browserAction.setBadgeText({ text: 'off' }) 10 | } else if (key === 'peersCount') { 11 | chrome.browserAction.setBadgeBackgroundColor({ color: '#0000ff' }) 12 | chrome.browserAction.setBadgeText({ text: storageChange.newValue.toString() }) 13 | } 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /app/popup/js/components/view/header.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react' 2 | import Radium from 'radium' 3 | 4 | import IPFSLogo from './ipfs-logo' 5 | 6 | @Radium 7 | export default class Header extends Component { 8 | static propTypes = { 9 | style: PropTypes.object 10 | }; 11 | 12 | render () { 13 | const styles = { 14 | wrapper: { 15 | display: 'flex', 16 | height: '40px' 17 | }, 18 | text: { 19 | alignSelf: 'center', 20 | flex: '1', 21 | paddingTop: '4px' 22 | } 23 | } 24 | 25 | return ( 26 |
27 |
28 | 29 |
30 |
31 | ) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/popup/styles/menu.less: -------------------------------------------------------------------------------- 1 | html { 2 | width: 240px; 3 | overflow: hidden; 4 | } 5 | 6 | .padding { 7 | padding: 8px; 8 | } 9 | 10 | .status { 11 | text-align: right; 12 | } 13 | 14 | .value { 15 | text-align: right; 16 | } 17 | 18 | .panel { 19 | padding: 4px; 20 | margin-top: 10px; 21 | margin-bottom: 0; 22 | } 23 | 24 | #logo { 25 | display: table; 26 | width: 100%; 27 | } 28 | 29 | .control { 30 | display: block; 31 | margin-top: 6px; 32 | } 33 | 34 | .toggle { 35 | height: 32px !important; 36 | width: 64px !important; 37 | } 38 | 39 | .cell { 40 | width: 33%; 41 | display: table-cell; 42 | text-align: center; 43 | padding-bottom: 6px; 44 | } 45 | 46 | .version { 47 | text-align: center; 48 | margin-bottom: 4px; 49 | } 50 | 51 | .nomarginbottom { 52 | margin-bottom: 0; 53 | } -------------------------------------------------------------------------------- /chrome/manifest.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.2", 3 | "name": "IPFS Station", 4 | "manifest_version": 2, 5 | "description": "This extension let's you access IPFS urls seamlessly from your local IPFS node, and take a look at its stats.", 6 | "browser_action": { 7 | "default_title": "IPFS Station", 8 | "default_popup": "popup.html" 9 | }, 10 | "icons": { 11 | "16": "img/icon-16.png", 12 | "48": "img/icon-48.png", 13 | "128": "img/icon-128.png" 14 | }, 15 | "background": { 16 | "page": "background.html" 17 | }, 18 | "options_ui": { 19 | "page": "options.html", 20 | "chrome_style": true 21 | }, 22 | "permissions": [ 23 | "webRequest", 24 | "webRequestBlocking", 25 | "tabs", 26 | "storage", 27 | "" 28 | ], 29 | "content_security_policy": "default-src 'self'; script-src 'self' 'unsafe-eval'; connect-src *; style-src * 'unsafe-inline'; img-src 'self' data:;" 30 | } 31 | -------------------------------------------------------------------------------- /app/popup/js/components/view/loader.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | // TODO: Extract from less file into here 4 | import '../../../styles/loader.less' 5 | 6 | export default function Loader () { 7 | return ( 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /app/options/components/menu.jsx: -------------------------------------------------------------------------------- 1 | /* global chrome */ 2 | import React, {Component} from 'react' 3 | import Settings from './settings' 4 | 5 | const settingsKeys = ['redirecting', 'host', 'port', 'apiPort', 'apiInterval'] 6 | 7 | export default class Menu extends Component { 8 | state = {}; 9 | 10 | constructor (props) { 11 | super(props) 12 | 13 | this.handleSubmit = this.handleSubmit.bind(this) 14 | 15 | chrome.storage.sync.get(settingsKeys, (settings) => { 16 | console.log('got settings', settings) 17 | this.setState(settings) 18 | }) 19 | } 20 | 21 | handleSubmit (values) { 22 | chrome.storage.sync.set(values, () => { 23 | console.log('saved settings', values) 24 | window.close() 25 | }) 26 | } 27 | 28 | render () { 29 | return ( 30 | 35 | ) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /chrome/manifest.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.2", 3 | "name": "IPFS Station", 4 | "manifest_version": 2, 5 | "description": "This extension let's you access IPFS urls seamlessly from your local IPFS node, and take a look at its stats.", 6 | "browser_action": { 7 | "default_title": "IPFS Station", 8 | "default_popup": "popup.html" 9 | }, 10 | "icons": { 11 | "16": "img/icon-16.png", 12 | "48": "img/icon-48.png", 13 | "128": "img/icon-128.png" 14 | }, 15 | "background": { 16 | "page": "background.html" 17 | }, 18 | "options_ui": { 19 | "page": "options.html", 20 | "chrome_style": true 21 | }, 22 | "permissions": [ 23 | "webRequest", 24 | "webRequestBlocking", 25 | "tabs", 26 | "storage", 27 | "" 28 | ], 29 | "content_security_policy": "default-src 'self' http://localhost:3000; script-src 'self' http://localhost:3000 'unsafe-eval'; connect-src *; style-src * 'unsafe-inline'; img-src 'self' http://localhost:3000 data:;" 30 | } 31 | -------------------------------------------------------------------------------- /app/popup/js/components/view/README.md: -------------------------------------------------------------------------------- 1 | # View Components 2 | 3 | In this directory only components that have no own state are stored. 4 | Having no state on themselves means they are only responsible for 5 | displaying the data they are given. 6 | 7 | They are usually written as *stateless functional components*, 8 | 9 | ```js 10 | export default function MyComponent ({name}) { 11 | return ( 12 |
Hello my name is, {name}.
13 | ) 14 | } 15 | ``` 16 | 17 | As a lot of styling does require the library [Radium](https://github.com/FormidableLabs/radium) 18 | for now these components have to be written like with the `class` notation, 19 | 20 | ```js 21 | @Radium 22 | export default class MyComponent extends Component { 23 | render () { 24 | const stlyes = { 25 | name: { 26 | color: 'read' 27 | } 28 | } 29 | 30 | return ( 31 |
Hello my name is, {name}.
32 | ) 33 | } 34 | } 35 | ``` 36 | 37 | until [FormidableLabs/radium#353](https://github.com/FormidableLabs/radium/issues/353) is 38 | resolved. 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Francisco Dias 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /webpack/replace/log-apply-result.js: -------------------------------------------------------------------------------- 1 | /* global chrome */ 2 | /* 3 | MIT License http://www.opensource.org/licenses/mit-license.php 4 | Author Tobias Koppers @sokra 5 | */ 6 | module.exports = function (updatedModules, renewedModules) { 7 | var unacceptedModules = updatedModules.filter(function (moduleId) { 8 | return renewedModules && renewedModules.indexOf(moduleId) < 0 9 | }) 10 | 11 | if (unacceptedModules.length > 0) { 12 | console.warn('[HMR] The following modules couldn\'t be hot updated: (They would need a full reload!)') 13 | unacceptedModules.forEach(function (moduleId) { 14 | console.warn('[HMR] - ' + moduleId) 15 | }) 16 | 17 | if (chrome && chrome.runtime && chrome.runtime.reload) { 18 | console.warn('[HMR] extension reload') 19 | chrome.runtime.reload() 20 | } else { 21 | console.warn('[HMR] Can\'t extension reload. not found chrome.runtime.reload.') 22 | } 23 | } 24 | 25 | if (!renewedModules || renewedModules.length === 0) { 26 | console.log('[HMR] Nothing hot updated.') 27 | } else { 28 | console.log('[HMR] Updated modules:') 29 | renewedModules.forEach(function (moduleId) { 30 | console.log('[HMR] - ' + moduleId) 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/popup/js/components/view/details.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react' 2 | import Radium from 'radium' 3 | 4 | @Radium 5 | export default class Details extends Component { 6 | static propTypes = { 7 | agentVersion: PropTypes.string, 8 | protocolVersion: PropTypes.string, 9 | host: PropTypes.string, 10 | port: PropTypes.number 11 | }; 12 | 13 | static defaultProps = { 14 | peer: {} 15 | }; 16 | 17 | render () { 18 | const styles = { 19 | wrapper: { 20 | display: 'flex', 21 | flexDirection: 'column', 22 | justifyContent: 'center', 23 | textAlign: 'center' 24 | }, 25 | entry: { 26 | color: 'rgba(0, 0, 0, 0.5)', 27 | padding: '2px 0', 28 | fontSize: '13px', 29 | fontWeight: '500' 30 | }, 31 | title: { 32 | fontWeight: 'bold', 33 | fontSize: '20px' 34 | } 35 | } 36 | 37 | return ( 38 |
39 |
40 | Details 41 |
42 |
43 | Agent: {this.props.agentVersion} 44 |
45 |
46 | Protocol: {this.props.protocolVersion} 47 |
48 |
49 | Gateway: {`${this.props.host}:${this.props.port}`} 50 |
51 |
52 | ) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/popup/js/components/view/icon-button.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react' 2 | import Radium from 'radium' 3 | 4 | @Radium 5 | export default class IconButton extends Component { 6 | static propTypes = { 7 | name: PropTypes.string, 8 | icon: PropTypes.string, 9 | onClick: PropTypes.func, 10 | style: PropTypes.object, 11 | iconStyle: PropTypes.object 12 | }; 13 | 14 | static defaultProps = { 15 | name: null, 16 | icon: '', 17 | onClick () {}, 18 | style: {}, 19 | iconStyle: {} 20 | }; 21 | 22 | render () { 23 | const styles = { 24 | button: { 25 | opacity: 0.8, 26 | background: 'none', 27 | border: 'none', 28 | flex: '1', 29 | padding: '0 10px', 30 | textAlign: 'center', 31 | fontSize: '12px', 32 | textTransform: 'uppercase', 33 | transition: 'color 0.3s ease-in-out', 34 | ':focus': { 35 | outline: 'none' 36 | }, 37 | ':hover': { 38 | opacity: 1, 39 | cursor: 'pointer' 40 | }, 41 | ...this.props.style 42 | } 43 | } 44 | 45 | return ( 46 | 53 | ) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/popup/js/components/view/simple-stat.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react' 2 | import Radium from 'radium' 3 | 4 | @Radium 5 | export default class SimpleStat extends Component { 6 | static propTypes = { 7 | name: PropTypes.string.isRequired, 8 | value: PropTypes.number.isRequired, 9 | color: PropTypes.string 10 | }; 11 | 12 | static defaultProps = { 13 | color: '#50d2c2' 14 | }; 15 | 16 | render () { 17 | const styles = { 18 | wrapper: { 19 | display: 'flex', 20 | flexDirection: 'column', 21 | justifyContent: 'center' 22 | }, 23 | ring: { 24 | width: '16px', 25 | height: '16px', 26 | borderRadius: '8px', 27 | border: `3px solid ${this.props.color}`, 28 | margin: '0 auto' 29 | }, 30 | label: { 31 | textTransform: 'uppercase', 32 | color: 'rgba(0, 0, 0, 0.5)', 33 | padding: '10px 0 5px', 34 | fontSize: '13px', 35 | fontWeight: '500' 36 | }, 37 | value: { 38 | textAlign: 'center', 39 | fontWeight: 'bold', 40 | fontSize: '20px' 41 | } 42 | } 43 | 44 | return ( 45 |
46 |
47 |
48 | {this.props.name} 49 |
50 |
51 | {this.props.value} 52 |
53 |
54 | ) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/popup/js/screens/menu/start.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react' 2 | import Radium from 'radium' 3 | 4 | import Header from '../../components/view/header' 5 | 6 | import 'normalize.css' 7 | import 'css-box-sizing-border-box/index.css' 8 | import '../../../styles/common.less' 9 | import '../../../styles/fonts.less' 10 | 11 | @Radium 12 | export default class StartScreen extends Component { 13 | static propTypes = { 14 | onReportProblemClick: PropTypes.func 15 | }; 16 | 17 | static defaultProps = { 18 | onReportProblemClick () {} 19 | }; 20 | 21 | render () { 22 | const styles = { 23 | wrapper: { 24 | display: 'flex', 25 | width: '100%', 26 | height: '100%', 27 | backgroundColor: '#19b5fe', 28 | color: '#FFFFFF', 29 | flexDirection: 'column', 30 | alignItems: 'center' 31 | }, 32 | content: { 33 | display: 'flex', 34 | flex: '1', 35 | margin: '40px 0', 36 | flexDirection: 'column' 37 | }, 38 | text: { 39 | padding: '40px 0', 40 | textAlign: 'center' 41 | }, 42 | link: { 43 | cursor: 'pointer', 44 | textAlign: 'center', 45 | textDecoration: 'underline' 46 | } 47 | } 48 | 49 | return ( 50 |
51 |
52 |
53 | 59 |
60 |

Oh snap, it looks like your node is not running yet.

61 |

Please start it by running ipfs daemon on your terminal.

62 |

63 | Also, please make sure you have CORS enabled, by running
64 | {`ipfs config --json API.HTTPHeaders '{"Access-Control-Allow-Origin": ["*"]}'`} 65 |

66 |
67 | Report a problem 68 |
69 |
70 | ) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /chrome/app/background/interceptor.js: -------------------------------------------------------------------------------- 1 | /* global chrome */ 2 | import url from 'url' 3 | import { url as isIPFSUrl } from 'is-ipfs' 4 | 5 | const settingsKeys = ['redirecting', 'host', 'port'] 6 | let settings = {} 7 | 8 | function interceptor (details) { 9 | var parsedUrl = url.parse(details.url) 10 | if (isIPFSUrl(details.url) && parsedUrl.host.indexOf(settings.host) === -1) { 11 | const node = `${settings.host}:${settings.port}` 12 | 13 | parsedUrl.protocol = 'http:' 14 | parsedUrl.host = node 15 | parsedUrl.hostname = node 16 | const localUrl = url.format(parsedUrl) 17 | console.log('redirected', details.url, 'to', node) 18 | return { redirectUrl: localUrl } 19 | } 20 | } 21 | 22 | function startInterceptor () { 23 | console.log('starting interceptor') 24 | chrome.webRequest.onBeforeRequest.addListener(interceptor, 25 | { urls: ['*://*/ipfs/*', '*://*/ipns/*'] }, 26 | ['blocking'] 27 | ) 28 | } 29 | 30 | function stopInterceptor () { 31 | console.log('stopping interceptor') 32 | chrome.webRequest.onBeforeRequest.removeListener(interceptor) 33 | } 34 | 35 | chrome.storage.onChanged.addListener(function (changes, namespace) { 36 | Object.keys(changes).forEach(key => { 37 | var storageChange = changes[key] 38 | 39 | if (settingsKeys.indexOf(key) !== -1) { 40 | settings[key] = storageChange.newValue 41 | } 42 | 43 | if (key === 'redirecting') { 44 | if (storageChange.newValue === true) { 45 | startInterceptor() 46 | } else { 47 | stopInterceptor() 48 | } 49 | } else if (key === 'running') { 50 | if (storageChange.newValue === true) { 51 | chrome.storage.sync.get('redirecting', function (result) { 52 | if (result.redirecting === true) { 53 | startInterceptor() 54 | } 55 | }) 56 | } else { 57 | stopInterceptor() 58 | } 59 | } 60 | }) 61 | }) 62 | 63 | chrome.storage.sync.get(settingsKeys, (result) => { 64 | settings = result 65 | 66 | const { redirecting } = settings 67 | if (redirecting === true) { 68 | chrome.storage.local.get('running', ({ running }) => { 69 | if (running === true) { 70 | startInterceptor() 71 | } 72 | }) 73 | } 74 | }) 75 | -------------------------------------------------------------------------------- /webpack/prod.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import webpack from 'webpack' 3 | 4 | export default { 5 | entry: { 6 | popup: [ path.join(__dirname, '../chrome/app/popup/index') ], 7 | background: [ path.join(__dirname, '../chrome/app/background/index') ], 8 | options: [ path.join(__dirname, '../chrome/app/options/index') ] 9 | }, 10 | output: { 11 | path: path.join(__dirname, '../build'), 12 | filename: '[name].bundle.js', 13 | chunkFilename: '[id].chunk.js' 14 | }, 15 | plugins: [ 16 | new webpack.DefinePlugin({ 17 | 'process.env': { 18 | NODE_ENV: '"production"' 19 | } 20 | }), 21 | new webpack.IgnorePlugin(/[^/]+\/[\S]+.dev$/), 22 | new webpack.optimize.DedupePlugin() 23 | // new webpack.optimize.UglifyJsPlugin({ 24 | // comments: false, 25 | // compressor: { 26 | // warnings: false 27 | // } 28 | // }) 29 | ], 30 | resolve: { 31 | modulesDirectories: [ 32 | 'node_modules' 33 | ], 34 | alias: { 35 | http: 'stream-http', 36 | https: 'https-browserify' 37 | }, 38 | extensions: ['', '.js', '.jsx', '.json'] 39 | }, 40 | module: { 41 | loaders: [ 42 | { 43 | test: /\.(js|jsx)$/, 44 | loader: 'babel', 45 | exclude: /node_modules/ 46 | }, 47 | { 48 | test: /\.js$/, 49 | include: /node_modules\/(hoek|qs|wreck|boom)/, 50 | loader: 'babel' 51 | }, 52 | { 53 | test: /\.json$/, 54 | loader: 'json-loader' 55 | }, 56 | { 57 | test: /\.css$/, 58 | loaders: ['style', 'css'] 59 | }, 60 | { 61 | test: /\.less$/, 62 | loaders: ['style', 'css', 'less'] 63 | }, 64 | { 65 | test: /\.woff(2)?(\?v=[0-9].[0-9].[0-9])?$/, 66 | loader: 'file-loader?name=[name].[ext]' 67 | }, 68 | { 69 | test: /\.(ttf|eot|svg)(\?v=[0-9].[0-9].[0-9])?$/, 70 | loader: 'file-loader?name=[name].[ext]' 71 | }, 72 | { 73 | test: /\.(png|gif|jpg|jpeg)$/, 74 | loader: 'file-loader?name=[name].[ext]' 75 | } 76 | ] 77 | }, 78 | externals: { 79 | net: '{}', 80 | fs: '{}', 81 | tls: '{}', 82 | console: '{}', 83 | 'require-dir': '{}' 84 | }, 85 | timeout: 60000 86 | } 87 | -------------------------------------------------------------------------------- /chrome/app/background/api.js: -------------------------------------------------------------------------------- 1 | /* global chrome */ 2 | import ipfsAPI from 'ipfs-api' 3 | import { get } from 'lodash' 4 | 5 | const settingsKeys = ['host', 'apiPort', 'apiInterval'] 6 | let settings = {} 7 | 8 | let ipfs 9 | let updateInterval 10 | 11 | function connectToAPI () { 12 | ipfs = ipfsAPI(settings.host, settings.apiPort) 13 | console.log('changed api to', settings.host, settings.apiPort) 14 | 15 | ipfs.id((err, peer) => { 16 | if (err) return 17 | 18 | chrome.storage.local.set({ 19 | id: peer.id, 20 | agentVersion: peer.agentVersion, 21 | protocolVersion: peer.protocolVersion 22 | }) 23 | }) 24 | } 25 | 26 | function updatePeersCount () { 27 | ipfs.swarm.peers((err, res) => { 28 | if (err) { 29 | chrome.storage.local.set({ 30 | peersCount: 0, 31 | running: false 32 | }) 33 | return 34 | } 35 | 36 | chrome.storage.local.set({ 37 | peersCount: res.length, 38 | running: true 39 | }) 40 | }) 41 | } 42 | 43 | chrome.storage.onChanged.addListener(function (changes, namespace) { 44 | let needsRestart = false 45 | Object.keys(changes).forEach(key => { 46 | var storageChange = changes[key] 47 | 48 | if (settingsKeys.indexOf(key) !== -1) { 49 | settings[key] = storageChange.newValue 50 | } 51 | 52 | needsRestart = (key === 'host' || key === 'apiPort') 53 | 54 | if (key === 'apiInterval' && updateInterval) { 55 | clearInterval(updateInterval) 56 | updateInterval = setInterval(updatePeersCount, settings.apiInterval) 57 | console.log('changed update interval to', settings.apiInterval) 58 | } 59 | }) 60 | 61 | if (needsRestart) { 62 | connectToAPI() 63 | } 64 | }) 65 | 66 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 67 | if (request.method) { 68 | const method = get(ipfs, request.method) 69 | const args = request.args || [] 70 | 71 | args.push(function () { 72 | console.log(request.method, 'result', arguments) 73 | sendResponse(arguments) 74 | }) 75 | 76 | console.log('executing', request.method) 77 | method.apply(null, args) 78 | 79 | return true 80 | } 81 | }) 82 | 83 | chrome.storage.sync.get(settingsKeys, (result) => { 84 | settings = result 85 | 86 | connectToAPI() 87 | 88 | updatePeersCount() 89 | updateInterval = setInterval(updatePeersCount, settings.apiInterval) 90 | }) 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ipfs-chrome-station", 3 | "version": "0.1.2", 4 | "description": "Chrome extension that let's you access IPFS urls seamlessly from your local IPFS node, and take a look at its stats.", 5 | "scripts": { 6 | "lint": "standard --verbose | snazzy", 7 | "dev": "gulp", 8 | "build": "gulp build", 9 | "compress": "zip -r build.zip build", 10 | "clean": "rimraf build/ dev/ *.zip *.crx" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/xicombd/ipfs-chrome-station" 15 | }, 16 | "homepage": "https://github.com/xicombd/ipfs-chrome-station", 17 | "author": "Francisco Dias ", 18 | "license": "MIT", 19 | "devDependencies": { 20 | "babel-core": "^6.3.15", 21 | "babel-eslint": "^7.1.1", 22 | "babel-loader": "^6.2.0", 23 | "babel-plugin-add-module-exports": "^0.1.1", 24 | "babel-plugin-transform-decorators-legacy": "^1.2.0", 25 | "babel-plugin-transform-runtime": "^6.4.3", 26 | "babel-polyfill": "^6.3.14", 27 | "babel-preset-es2015": "^6.3.13", 28 | "babel-preset-react": "^6.3.13", 29 | "babel-preset-react-hmre": "^1.0.0", 30 | "babel-preset-stage-0": "^6.3.13", 31 | "css-loader": "^0.27.3", 32 | "file-loader": "^0.8.5", 33 | "gulp": "^3.9.0", 34 | "gulp-pug": "^3.3.0", 35 | "gulp-rename": "^1.2.2", 36 | "gulp-util": "^3.0.5", 37 | "https-browserify": "0.0.1", 38 | "jsdom": "^6.5.1", 39 | "json-loader": "^0.5.4", 40 | "less": "^2.7.2", 41 | "less-loader": "^3.0.0", 42 | "merge-stream": "^1.0.0", 43 | "pre-commit": "^1.1.2", 44 | "raw-loader": "^0.5.1", 45 | "rimraf": "^2.4.3", 46 | "snazzy": "^2.0.1", 47 | "standard": "^9.0.2", 48 | "style-loader": "^0.12.3", 49 | "webpack": "^1.12.9", 50 | "webpack-dev-server": "^1.10.1" 51 | }, 52 | "dependencies": { 53 | "belle": "^2.0.3", 54 | "css-box-sizing-border-box": "^0.1.0", 55 | "ipfs-api": "^12.1.7", 56 | "ipfs-logo": "github:ipfs/logo", 57 | "is-ipfs": "0.0.4", 58 | "lodash": "^4.2.0", 59 | "normalize.css": "^3.0.3", 60 | "radium": "^0.15.3", 61 | "react": "^0.14.2", 62 | "react-addons-css-transition-group": "^0.14.2", 63 | "react-dom": "^0.14.2", 64 | "request": "^2.65.0", 65 | "stream-http": "^2.1.0" 66 | }, 67 | "standard": { 68 | "parser": "babel-eslint" 69 | }, 70 | "pre-commit": [ 71 | "lint" 72 | ] 73 | } 74 | -------------------------------------------------------------------------------- /webpack/replace/JsonpMainTemplate.runtime.js: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License http://www.opensource.org/licenses/mit-license.php 3 | Author Tobias Koppers @sokra 4 | */ 5 | /* global hotAddUpdateChunk parentHotUpdateCallback document XMLHttpRequest hotCurrentHash $require$ $hotMainFilename$ __webpack_require__ */ 6 | module.exports = function () { 7 | function webpackHotUpdateCallback (chunkId, moreModules) { // eslint-disable-line no-unused-vars 8 | hotAddUpdateChunk(chunkId, moreModules) 9 | if (parentHotUpdateCallback) parentHotUpdateCallback(chunkId, moreModules) 10 | } 11 | 12 | var context = this 13 | function evalCode (code, context) { 14 | return (function () { return eval(code) }.call(context)) // eslint-disable-line no-eval 15 | } 16 | 17 | context.hotDownloadUpdateChunk = function (chunkId) { // eslint-disable-line no-unused-vars 18 | var src = __webpack_require__.p + '' + chunkId + '.' + hotCurrentHash + '.hot-update.js' 19 | var request = new XMLHttpRequest() 20 | 21 | request.onload = function () { 22 | evalCode(this.responseText, context) 23 | } 24 | request.open('get', src, true) 25 | request.send() 26 | } 27 | 28 | function hotDownloadManifest (callback) { // eslint-disable-line no-unused-vars 29 | if (typeof XMLHttpRequest === 'undefined') { 30 | return callback(new Error('No browser support')) 31 | } 32 | try { 33 | var request = new XMLHttpRequest() 34 | var requestPath = $require$.p + $hotMainFilename$ 35 | request.open('GET', requestPath, true) 36 | request.timeout = 10000 37 | request.send(null) 38 | } catch (err) { 39 | return callback(err) 40 | } 41 | request.onreadystatechange = function () { 42 | if (request.readyState !== 4) return 43 | if (request.status === 0) { 44 | // timeout 45 | callback(new Error('Manifest request to ' + requestPath + ' timed out.')) 46 | } else if (request.status === 404) { 47 | // no update available 48 | callback() 49 | } else if (request.status !== 200 && request.status !== 304) { 50 | // other failure 51 | callback(new Error('Manifest request to ' + requestPath + ' failed.')) 52 | } else { 53 | // success 54 | try { 55 | var update = JSON.parse(request.responseText) 56 | } catch (e) { 57 | callback(e) 58 | return 59 | } 60 | callback(null, update) 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ipfs-chrome-station 2 | === 3 | 4 | Chrome extension that let's you access [IPFS](https://ipfs.io) urls seamlessly from your local [IPFS node](https://ipfs.io/docs/install/), and take a look at its stats. 5 | 6 | [![js-standard-style](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard) 7 | [![Available in the Chrome Web Store](https://developer.chrome.com/webstore/images/ChromeWebStore_BadgeWBorder_v2_206x58.png)](https://chrome.google.com/webstore/detail/ipfs-station/kckhgoigikkadogfdiojcblegfhdnjei) 8 | 9 | Based on [react-chrome-extension-boilerplate](https://github.com/jhen0409/react-chrome-extension-boilerplate), [ipfs-station](https://github.com/ipfs/station/) and [ipfs-firefox-addon](https://github.com/lidel/ipfs-firefox-addon) *(if you're using Firefox, make sure to check it out)*. 10 | 11 | ### Demo 12 | 13 | ![demo](https://raw.githubusercontent.com/xicombd/ipfs-chrome-station/master/demo.gif) 14 | 15 | ### Features 16 | 17 | - Icon with badge that shows if the node is running, and how many peers are connected to it 18 | - Clicking on the icon opens popup menu with useful operations: 19 | - See stats of the IPFS node 20 | - Toggle redirection to the IPFS node 21 | - Open IPFS node WebUI 22 | - Open extension options (more about this bellow) 23 | - Additionally, on pages loaded from IPFS: 24 | - Copy canonical IPFS address 25 | - Copy shareable URL to resource at a default public gateway (https://ipfs.io) 26 | - Pin/unpin IPFS Resource 27 | - When redirection is on, requests to `https?://*/(ipfs|ipns)/$RESOURCE` are replaced with `http://localhost:8080/(ipfs|ipns)/$RESOURCE` 28 | - Options menu that let's you customize several parameters: 29 | - IPFS node host 30 | - IPFS node port 31 | - IPFS node API port 32 | - API stats polling interval 33 | - Toggle redirection 34 | 35 | ### Installation 36 | 37 | ```bash 38 | # git clone ... 39 | 40 | npm install 41 | ``` 42 | 43 | ### Development 44 | 45 | * Run script 46 | ```bash 47 | # build files to './dev' 48 | # start webpack dev server 49 | npm run dev 50 | ``` 51 | * Go to [chrome://extensions/](chrome://extensions/) and check `Developer Mode` box 52 | * Click `Load unpacked extension...` and select the `dev` folder 53 | 54 | ### Build 55 | 56 | ```bash 57 | # build files to './build' 58 | npm run build 59 | ``` 60 | 61 | ### Compress ZIP file 62 | 63 | ```bash 64 | # compress build folder to build.zip 65 | npm run compress 66 | ``` 67 | 68 | ### LICENSE 69 | 70 | [MIT](LICENSE) 71 | -------------------------------------------------------------------------------- /webpack/dev.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import webpack from 'webpack' 3 | 4 | const port = 3000 5 | const entry = [ 6 | `webpack-dev-server/client?http://localhost:${port}`, 7 | 'webpack/hot/only-dev-server' 8 | ] 9 | 10 | export default { 11 | devtool: 'source-map', 12 | entry: { 13 | popup: [ path.join(__dirname, '../chrome/app/popup/index'), ...entry ], 14 | background: [ path.join(__dirname, '../chrome/app/background/index'), ...entry ], 15 | options: [ path.join(__dirname, '../chrome/app/options/index'), ...entry ] 16 | }, 17 | output: { 18 | path: path.join(__dirname, '../dev/js'), 19 | filename: '[name].bundle.js', 20 | chunkFilename: '[id].chunk.js', 21 | publicPath: `http://localhost:${port}/js/` 22 | }, 23 | plugins: [ 24 | new webpack.HotModuleReplacementPlugin(), 25 | new webpack.NoErrorsPlugin(), 26 | new webpack.IgnorePlugin(/[^/]+\/[\S]+.prod$/), 27 | new webpack.DefinePlugin({ 28 | 'process.env': { 29 | DEVTOOLS: !!process.env.DEVTOOLS || true, 30 | DEVTOOLS_EXT: !!process.env.DEVTOOLS_EXT 31 | } 32 | }) 33 | ], 34 | resolve: { 35 | modulesDirectories: [ 36 | 'node_modules' 37 | ], 38 | alias: { 39 | http: 'stream-http', 40 | https: 'https-browserify' 41 | }, 42 | extensions: ['', '.js', '.jsx', '.json'] 43 | }, 44 | module: { 45 | loaders: [ 46 | { 47 | test: /\.(js|jsx)$/, 48 | exclude: /node_modules/, 49 | loader: 'babel', 50 | query: { 51 | presets: ['react-hmre'], 52 | plugins: ['transform-runtime'] 53 | } 54 | }, 55 | { 56 | test: /\.js$/, 57 | include: /node_modules\/(hoek|qs|wreck|boom)/, 58 | loader: 'babel', 59 | query: { 60 | presets: ['es2015'], 61 | plugins: ['transform-runtime'] 62 | } 63 | }, 64 | { 65 | test: /\.json$/, 66 | loader: 'json' 67 | }, 68 | { 69 | test: /\.css$/, 70 | loaders: ['style', 'css'] 71 | }, 72 | { 73 | test: /\.less$/, 74 | loaders: ['style', 'css', 'less'] 75 | }, 76 | { 77 | test: /\.woff(2)?(\?v=[0-9].[0-9].[0-9])?$/, 78 | loader: 'file-loader?name=[name].[ext]' 79 | }, 80 | { 81 | test: /\.(ttf|eot|svg)(\?v=[0-9].[0-9].[0-9])?$/, 82 | loader: 'file-loader?name=[name].[ext]' 83 | }, 84 | { 85 | test: /\.(png|gif|jpg|jpeg)$/, 86 | loader: 'file-loader?name=[name].[ext]' 87 | } 88 | ] 89 | }, 90 | externals: { 91 | net: '{}', 92 | fs: '{}', 93 | tls: '{}', 94 | console: '{}', 95 | 'require-dir': '{}' 96 | }, 97 | timeout: 60000 98 | } 99 | -------------------------------------------------------------------------------- /app/options/components/settings.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { Toggle, TextInput, Button } from 'belle' 3 | 4 | export default class Settings extends Component { 5 | state = {}; 6 | 7 | static propTypes = { 8 | settings: PropTypes.object, 9 | onSubmit: PropTypes.func 10 | }; 11 | 12 | constructor (props) { 13 | super(props) 14 | 15 | this.state = props.settings 16 | } 17 | 18 | componentWillReceiveProps (nextProps) { 19 | const { 20 | redirecting = this.state.redirecting, 21 | host = this.state.host, 22 | port = this.state.port, 23 | apiPort = this.state.apiPort, 24 | apiInterval = this.state.apiInterval 25 | } = nextProps.settings 26 | 27 | this.setState({ redirecting, host, port, apiPort, apiInterval }) 28 | } 29 | 30 | handleChange (field, isNumber, { value }) { 31 | const nextState = { 32 | [field]: isNumber ? parseInt(value, 10) : value 33 | } 34 | 35 | console.log('change', nextState) 36 | 37 | this.setState(nextState) 38 | } 39 | 40 | handleSubmit () { 41 | this.props.onSubmit(this.state) 42 | } 43 | 44 | render () { 45 | const { 46 | redirecting, 47 | host, 48 | port, 49 | apiPort, 50 | apiInterval 51 | } = this.state 52 | 53 | const style = { 54 | toggle: { 55 | base: { 56 | transform: 'scale(0.7)', 57 | verticalAlign: 'middle', 58 | marginLeft: '-8px' 59 | }, 60 | label: { 61 | fontSize: '14px', 62 | verticalAlign: 'sub' 63 | } 64 | }, 65 | node: { 66 | paddingLeft: '10px' 67 | }, 68 | button: { 69 | marginTop: '20px' 70 | } 71 | } 72 | 73 | return ( 74 |
75 |
76 | 81 | 82 |
83 | 84 |

IPFS node details

85 |
86 | 91 | 96 | 101 | 106 |
107 | 108 | 115 |
116 | 117 | ) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /app/popup/styles/loader.less: -------------------------------------------------------------------------------- 1 | // Loading animation taken from 2 | // http://codepen.io/thiesbrake/pen/bvLcI 3 | 4 | // 1. Variables 5 | 6 | @floatTime: 4s; 7 | @inital: 1; 8 | @downstate: 0.6; 9 | @originOne: 22.5px; 10 | @originTwo: 22.5px; 11 | @strokeDash: 142; 12 | 13 | // 2. Animation-Setup 14 | 15 | .spinner_svg{ 16 | position: relative; 17 | z-index: 400; 18 | top: -50%; 19 | left: -50%; 20 | } 21 | 22 | #Group{ 23 | stroke-dasharray: @strokeDash; 24 | stroke-dashoffset: @strokeDash; 25 | animation: dash 8s cubic-bezier(.02,.61,.85,.98) infinite; 26 | } 27 | 28 | #Oval-1{ 29 | fill: none; 30 | transform-origin: @originOne @originTwo; 31 | animation: pulsate 8s infinite; 32 | } 33 | 34 | #Oval-2, #Oval-3, #Oval-4{ 35 | fill: none; 36 | transform-origin: @originOne @originTwo; 37 | opacity: 0; 38 | } 39 | 40 | #Oval-2{ 41 | animation: float 8s ease 3s infinite; 42 | } 43 | 44 | #Oval-3{ 45 | animation: float2 8s ease 4s infinite; 46 | } 47 | 48 | #Oval-4{ 49 | animation: float3 8s ease 5s infinite; 50 | } 51 | 52 | .pulsateInital(@inital){ 53 | transform: scale(@inital) ; 54 | animation-timing-function: cubic-bezier(0,.96,.56,.97); 55 | } 56 | 57 | .pulsateDownstate(){ 58 | transform: scale(@downstate); 59 | animation-timing-function:ease-in; 60 | } 61 | 62 | // 2. Animations ================= 63 | 64 | @keyframes dash { 65 | 25% {stroke-dashoffset: 0; opacity: inherit;} 66 | 75% {opacity: 1;stroke-dashoffset: 0;} 67 | 99% {opacity: 0;stroke-dashoffset: 0;} 68 | 100%{stroke-dashoffset: @strokeDash;} 69 | } 70 | 71 | @keyframes pulsate { 72 | 0% {opacity:1} 73 | 25% {.pulsateInital(1);} 74 | 28% {.pulsateDownstate();} 75 | 37.5% {.pulsateInital(0.95);} 76 | 40.5% {.pulsateDownstate();} 77 | 50% {.pulsateInital(0.9);} 78 | 53% {.pulsateDownstate();} 79 | 62.5% {.pulsateInital(0.85);} 80 | 65.5% {.pulsateDownstate();} 81 | 75% {.pulsateInital(0.85);opacity:1;fill:none;} 82 | 100% {.pulsateDownstate();opacity:0;fill:white;} 83 | } 84 | 85 | @keyframes float { 86 | 0% {opacity: 0} 87 | 0.1% {transform: scale(@inital);opacity: 0.6;} 88 | 40% {transform: scale(2);opacity: 0;} 89 | 100% {transform: scale(@inital);opacity: 0;} 90 | } 91 | 92 | @keyframes float2 { 93 | 0% {opacity: 0} 94 | 0.1% {transform: scale(@inital);opacity: 0.6;} 95 | 30% {transform: scale(2);opacity: 0;} 96 | 100% {transform: scale(@inital);opacity: 0;} 97 | } 98 | 99 | @keyframes float3 { 100 | 0% {opacity: 0} 101 | 0.1% {transform: scale(@inital);opacity: 0.6;} 102 | 30% {transform: scale(2);opacity: 0;} 103 | 100% {transform: scale(@inital);opacity: 0;} 104 | } 105 | 106 | // Presentation ================= 107 | 108 | body { 109 | background: #19b5fe; 110 | } 111 | 112 | .spinner{ 113 | position: absolute; 114 | z-index: 200; 115 | top: 50%; 116 | left: 50%; 117 | width: 100px; 118 | height: 100px; 119 | background: #19b5fe; 120 | 121 | &:after{ 122 | content: ""; 123 | position: absolute; 124 | z-index: 300; 125 | width: 300px; 126 | height: 200px; 127 | top: 0; 128 | left: 0; 129 | opacity: 0.3; 130 | background-size: 100%; 131 | } 132 | } -------------------------------------------------------------------------------- /gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill' 2 | import fs from 'fs' 3 | import gulp from 'gulp' 4 | import merge from 'merge-stream' 5 | import gutil from 'gulp-util' 6 | import pug from 'gulp-pug' 7 | import rename from 'gulp-rename' 8 | import webpack from 'webpack' 9 | import WebpackDevServer from 'webpack-dev-server' 10 | import prodConfig from './webpack/prod.config' 11 | import devConfig from './webpack/dev.config' 12 | 13 | const port = 3000 14 | 15 | /* 16 | * common tasks 17 | */ 18 | gulp.task('replace-webpack-code', () => { 19 | const replaceTasks = [ { 20 | from: './webpack/replace/JsonpMainTemplate.runtime.js', 21 | to: './node_modules/webpack/lib/JsonpMainTemplate.runtime.js' 22 | }, { 23 | from: './webpack/replace/log-apply-result.js', 24 | to: './node_modules/webpack/hot/log-apply-result.js' 25 | } ] 26 | replaceTasks.forEach(task => fs.writeFileSync(task.to, fs.readFileSync(task.from))) 27 | }) 28 | 29 | /* 30 | * dev tasks 31 | */ 32 | 33 | gulp.task('webpack-dev-server', () => { 34 | let myConfig = Object.create(devConfig) 35 | new WebpackDevServer(webpack(myConfig, (err, stats) => { 36 | if (err) throw new gutil.PluginError('webpack', err) 37 | gutil.log('Please load the unpacked extension with `./dev` folder.\n (see https://developer.chrome.com/extensions/getstarted#unpacked)') 38 | }), { 39 | publicPath: myConfig.output.publicPath, 40 | stats: {colors: true}, 41 | noInfo: true, 42 | hot: true, 43 | historyApiFallback: true 44 | // https: true 45 | }).listen(port, 'localhost', (err) => { 46 | if (err) throw new gutil.PluginError('webpack-dev-server', err) 47 | gutil.log('[webpack-dev-server]', `listening at port ${port}`) 48 | }) 49 | }) 50 | 51 | gulp.task('views:dev', () => { 52 | return gulp.src('./chrome/views/*.pug') 53 | .pipe(pug({ 54 | locals: { 55 | env: 'dev', 56 | devToolsExt: !!process.env.DEVTOOLS_EXT || true 57 | } 58 | })) 59 | .pipe(gulp.dest('./dev')) 60 | }) 61 | 62 | gulp.task('copy:dev', () => { 63 | const manifest = gulp.src('./chrome/manifest.dev.json') 64 | .pipe(rename('manifest.json')) 65 | .pipe(gulp.dest('./dev')) 66 | const assets = gulp.src('./chrome/assets/**/*').pipe(gulp.dest('./dev')) 67 | return merge(manifest, assets) 68 | }) 69 | 70 | /* 71 | * build tasks 72 | */ 73 | 74 | gulp.task('webpack:build', (callback) => { 75 | let myConfig = Object.create(prodConfig) 76 | webpack(myConfig, (err, stats) => { 77 | if (err) { 78 | throw new gutil.PluginError('webpack:build', err) 79 | } 80 | gutil.log('[webpack:build]', stats.toString({ colors: true })) 81 | callback() 82 | }) 83 | }) 84 | 85 | gulp.task('views:build', () => { 86 | return gulp.src('./chrome/views/*.pug') 87 | .pipe(pug({ 88 | locals: { env: 'prod' } 89 | })) 90 | .pipe(gulp.dest('./build')) 91 | }) 92 | 93 | gulp.task('copy:build', () => { 94 | const manifest = gulp.src('./chrome/manifest.prod.json') 95 | .pipe(rename('manifest.json')) 96 | .pipe(gulp.dest('./build')) 97 | const assets = gulp.src('./chrome/assets/**/*').pipe(gulp.dest('./build')) 98 | return merge(manifest, assets) 99 | }) 100 | 101 | gulp.task('default', [ 'replace-webpack-code', 'webpack-dev-server', 'views:dev', 'copy:dev' ]) 102 | gulp.task('build', [ 'replace-webpack-code', 'webpack:build', 'views:build', 'copy:build' ]) 103 | -------------------------------------------------------------------------------- /app/popup/js/screens/menu/profile.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react' 2 | import Radium from 'radium' 3 | 4 | import SimpleStat from '../../components/view/simple-stat' 5 | import IconButton from '../../components/view/icon-button' 6 | import Header from '../../components/view/header' 7 | import Icon from '../../components/view/icon' 8 | import Details from '../../components/view/details' 9 | 10 | import 'normalize.css' 11 | import 'css-box-sizing-border-box/index.css' 12 | import '../../../styles/common.less' 13 | import '../../../styles/fonts.less' 14 | import '../../../styles/file-drop.less' 15 | 16 | @Radium 17 | export default class ProfileScreen extends Component { 18 | static propTypes = { 19 | peers: PropTypes.number, 20 | location: PropTypes.string, 21 | redirecting: PropTypes.bool, 22 | agentVersion: PropTypes.string, 23 | protocolVersion: PropTypes.string, 24 | host: PropTypes.string, 25 | port: PropTypes.number, 26 | isIpfsPage: PropTypes.bool, 27 | pageUrl: PropTypes.string, 28 | pinned: PropTypes.bool, 29 | onRedirectClick: PropTypes.func, 30 | onWebUIClick: PropTypes.func, 31 | onOptionsClick: PropTypes.func, 32 | onCopyToClipboard: PropTypes.func, 33 | onPinClick: PropTypes.func 34 | }; 35 | 36 | static defaultProps = { 37 | peers: 0, 38 | location: '', 39 | onRedirectClick () {}, 40 | onWebUIClick () {}, 41 | onOptionsClick () {}, 42 | onCopyToClipboard () {}, 43 | onPinClick () {} 44 | }; 45 | 46 | render () { 47 | const styles = { 48 | wrapper: { 49 | display: 'flex', 50 | width: '100%', 51 | height: '100%', 52 | backgroundImage: `url(${require('../../../img/stars.png')})`, 53 | backgroundSize: 'cover', 54 | backgroundPosition: '0 10%', 55 | color: '#FFFFFF', 56 | flexDirection: 'column', 57 | alignItems: 'center' 58 | }, 59 | header: { 60 | height: this.props.location ? '40px' : '60px' 61 | }, 62 | location: { 63 | display: 'flex', 64 | flex: '1', 65 | color: '#FFFFFF', 66 | width: '100%', 67 | minHeight: '30px', 68 | alignItems: 'center', 69 | justifyContent: 'center', 70 | flexDirection: 'column', 71 | padding: '10px 0' 72 | }, 73 | stats: { 74 | padding: '20px', 75 | display: 'flex', 76 | flex: '1', 77 | backgroundColor: '#FFFFFF', 78 | color: '#000000', 79 | width: '100%', 80 | height: '30%', 81 | justifyContent: 'space-around' 82 | }, 83 | actions: { 84 | display: 'flex', 85 | height: '70px', 86 | justifyContent: 'space-around', 87 | backgroundColor: '#19b5fe', 88 | width: '100%' 89 | }, 90 | pageActions: { 91 | wrapper: { 92 | width: '100%', 93 | backgroundImage: `url(${require('../../../img/stars.png')})`, 94 | backgroundSize: 'cover', 95 | backgroundPosition: '0 90%' 96 | }, 97 | header: { 98 | textAlign: 'center', 99 | fontSize: '16px', 100 | padding: '10px' 101 | }, 102 | buttons: { 103 | display: 'flex', 104 | height: '70px', 105 | justifyContent: 'space-around', 106 | width: '100%' 107 | } 108 | } 109 | } 110 | 111 | const pageActions = this.props.isIpfsPage ? ( 112 |
113 |
114 | Current address actions 115 |
116 |
117 | 122 | 127 | 132 |
133 |
134 | ) : null 135 | 136 | const location = this.props.location ? ( 137 |
138 | 139 |
140 | {this.props.location} 141 |
142 |
143 | ) : null 144 | 145 | return ( 146 |
147 |
148 | {location} 149 |
150 |
156 | 161 |
162 |
163 | 168 | 173 | 178 |
179 | {pageActions} 180 |
181 | ) 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /app/popup/js/screens/menu.js: -------------------------------------------------------------------------------- 1 | /* global chrome */ 2 | import React, {Component} from 'react' 3 | import CSSTransitionGroup from 'react-addons-css-transition-group' 4 | import { url as isIPFSUrl, urlPattern } from 'is-ipfs' 5 | 6 | import StartScreen from './menu/start' 7 | import ProfileScreen from './menu/profile' 8 | import Loader from '../components/view/loader' 9 | 10 | import '../../styles/animations.less' 11 | 12 | const INITIALIZING = 'initializing' 13 | const RUNNING = 'running' 14 | const STOPPED = 'stopped' 15 | 16 | function copyToClipboard (str) { 17 | document.oncopy = (event) => { 18 | event.clipboardData.setData('Text', str) 19 | event.preventDefault() 20 | } 21 | 22 | document.execCommand('Copy') 23 | document.oncopy = undefined 24 | } 25 | 26 | function parseUrl (url) { 27 | const matches = url.match(urlPattern) 28 | const hash = matches[1] === 'ipfs' ? matches[4] : null 29 | const path = matches[3] 30 | const address = `/${matches[1]}/${path}` 31 | const publicUrl = `https://ipfs.io${address}` 32 | 33 | return { 34 | hash, 35 | path, 36 | address, 37 | publicUrl 38 | } 39 | } 40 | 41 | export default class Menu extends Component { 42 | state = { 43 | status: INITIALIZING, 44 | connected: false, 45 | version: null, 46 | stats: {} 47 | }; 48 | 49 | constructor (props) { 50 | super(props) 51 | 52 | this.handleStorageChange = this.handleStorageChange.bind(this) 53 | this.handleRedirectClick = this.handleRedirectClick.bind(this) 54 | this.handleWebUIClick = this.handleWebUIClick.bind(this) 55 | this.handleOptionsClick = this.handleOptionsClick.bind(this) 56 | this.handleCopyToClipboard = this.handleCopyToClipboard.bind(this) 57 | this.handlePinClick = this.handlePinClick.bind(this) 58 | this.handleReportProblemClick = this.handleReportProblemClick.bind(this) 59 | 60 | chrome.storage.onChanged.addListener(this.handleStorageChange) 61 | 62 | chrome.storage.local.get(null, (result) => { 63 | this.setState({ 64 | ...result, 65 | status: result.running ? RUNNING : STOPPED 66 | }) 67 | }) 68 | 69 | chrome.storage.sync.get(null, (result) => { 70 | this.setState(result) 71 | }) 72 | 73 | chrome.tabs.query({ currentWindow: true, active: true }, (tabs) => { 74 | if (!isIPFSUrl(tabs[0].url)) { 75 | return 76 | } 77 | 78 | const pageUrl = tabs[0].url 79 | 80 | const { hash } = parseUrl(pageUrl) 81 | 82 | this.setState({ 83 | isIpfsPage: true, 84 | pageUrl, 85 | pageHash: hash 86 | }) 87 | 88 | chrome.runtime.sendMessage({ method: 'pin.list' }, (response) => { 89 | const err = response[0] 90 | const res = response[1] 91 | if (err) { 92 | console.error('error on pin.list', err) 93 | return 94 | } 95 | 96 | this.setState({ 97 | pinned: !!res.Keys[hash] 98 | }) 99 | }) 100 | }) 101 | } 102 | 103 | handleStorageChange (changes, namespace) { 104 | const nextState = {} 105 | Object.keys(changes).forEach(key => { 106 | const storageChange = changes[key] 107 | 108 | nextState[key] = storageChange.newValue 109 | 110 | if (key === 'running') { 111 | nextState.status = storageChange.newValue ? RUNNING : STOPPED 112 | } 113 | }) 114 | 115 | console.log('nextState', nextState) 116 | 117 | this.setState(nextState) 118 | } 119 | 120 | handleRedirectClick () { 121 | const redirecting = !this.state.redirecting 122 | 123 | chrome.storage.sync.set({ 124 | redirecting 125 | }) 126 | } 127 | 128 | handleWebUIClick () { 129 | chrome.tabs.create({ url: `http://${this.state.host}:${this.state.apiPort}/webui` }) 130 | } 131 | 132 | handleOptionsClick () { 133 | chrome.runtime.openOptionsPage() 134 | } 135 | 136 | handleCopyToClipboard (type) { 137 | const { address, publicUrl } = parseUrl(this.state.pageUrl) 138 | 139 | switch (type) { 140 | case 'public-address': 141 | return copyToClipboard(publicUrl) 142 | case 'address': 143 | return copyToClipboard(address) 144 | } 145 | } 146 | 147 | handlePinClick () { 148 | const method = this.state.pinned ? 'pin.remove' : 'pin.add' 149 | 150 | const args = [this.state.pageHash] 151 | 152 | chrome.runtime.sendMessage({ method, args }, (response) => { 153 | const err = response[0] 154 | if (err) { 155 | console.error('error on', method, err) 156 | return 157 | } 158 | 159 | this.setState({ 160 | pinned: !this.state.pinned 161 | }) 162 | }) 163 | } 164 | 165 | handleReportProblemClick () { 166 | chrome.tabs.create({url: 'https://github.com/xicombd/ipfs-chrome-station/issues'}) 167 | } 168 | 169 | getScreen () { 170 | switch (this.state.status) { 171 | case RUNNING: 172 | return ( 173 | 191 | ) 192 | case INITIALIZING: 193 | return 194 | default: 195 | return 196 | } 197 | } 198 | 199 | render () { 200 | return ( 201 | 206 | {this.getScreen()} 207 | 208 | ) 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /app/popup/styles/fonts.less: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | @font-face { 4 | font-family: 'Maven Pro'; 5 | src: url('../fonts/maven_pro_bold-webfont.ttf') format('truetype'); 6 | font-weight: bold; 7 | font-style: normal; 8 | } 9 | 10 | @font-face { 11 | font-family: 'Maven Pro'; 12 | src: url('../fonts/maven_pro_medium-webfont.ttf') format('truetype'); 13 | font-weight: 500; 14 | font-style: normal; 15 | 16 | } 17 | 18 | @font-face { 19 | font-family: 'Maven Pro'; 20 | src: url('../fonts/maven_pro_regular-webfont.ttf') format('truetype'); 21 | font-weight: normal; 22 | font-style: normal; 23 | } 24 | 25 | @font-face { 26 | font-family: "dripicons"; 27 | src: url("../fonts/dripicons.ttf") format("truetype"); 28 | font-weight: normal; 29 | font-style: normal; 30 | 31 | } 32 | 33 | [data-icon]:before { 34 | font-family: "dripicons"; 35 | content: attr(data-icon); 36 | font-style: normal; 37 | font-weight: normal; 38 | font-variant: normal; 39 | text-transform: none !important; 40 | speak: none; 41 | display: inline-block; 42 | text-decoration: none; 43 | width: 1em; 44 | line-height: 1em; 45 | -webkit-font-smoothing: antialiased; 46 | } 47 | 48 | [class^="icon-"]:before, 49 | [class*=" icon-"]:before { 50 | font-family: "dripicons"; 51 | font-style: normal; 52 | font-weight: normal; 53 | font-variant: normal; 54 | text-transform: none !important; 55 | speak: none; 56 | display: inline-block; 57 | text-decoration: none; 58 | width: 1em; 59 | line-height: 1em; 60 | -webkit-font-smoothing: antialiased; 61 | } 62 | 63 | 64 | .icon-align-center:before { 65 | content: "\e000"; 66 | } 67 | 68 | .icon-align-justify:before { 69 | content: "\e001"; 70 | } 71 | 72 | .icon-align-left:before { 73 | content: "\e002"; 74 | } 75 | 76 | .icon-align-right:before { 77 | content: "\e003"; 78 | } 79 | 80 | .icon-arrow-down:before { 81 | content: "\e004"; 82 | } 83 | 84 | .icon-arrow-left:before { 85 | content: "\e005"; 86 | } 87 | 88 | .icon-arrow-thin-down:before { 89 | content: "\e006"; 90 | } 91 | 92 | .icon-arrow-right:before { 93 | content: "\e007"; 94 | } 95 | 96 | .icon-arrow-thin-left:before { 97 | content: "\e008"; 98 | } 99 | 100 | .icon-arrow-thin-up:before { 101 | content: "\e009"; 102 | } 103 | 104 | .icon-arrow-up:before { 105 | content: "\e010"; 106 | } 107 | 108 | .icon-attachment:before { 109 | content: "\e011"; 110 | } 111 | 112 | .icon-arrow-thin-right:before { 113 | content: "\e012"; 114 | } 115 | 116 | .icon-code:before { 117 | content: "\e013"; 118 | } 119 | 120 | .icon-cloud:before { 121 | content: "\e014"; 122 | } 123 | 124 | .icon-chevron-right:before { 125 | content: "\e015"; 126 | } 127 | 128 | .icon-chevron-up:before { 129 | content: "\e016"; 130 | } 131 | 132 | .icon-chevron-down:before { 133 | content: "\e017"; 134 | } 135 | 136 | .icon-chevron-left:before { 137 | content: "\e018"; 138 | } 139 | 140 | .icon-camera:before { 141 | content: "\e019"; 142 | } 143 | 144 | .icon-checkmark:before { 145 | content: "\e020"; 146 | } 147 | 148 | .icon-calendar:before { 149 | content: "\e021"; 150 | } 151 | 152 | .icon-clockwise:before { 153 | content: "\e022"; 154 | } 155 | 156 | .icon-conversation:before { 157 | content: "\e023"; 158 | } 159 | 160 | .icon-direction:before { 161 | content: "\e024"; 162 | } 163 | 164 | .icon-cross:before { 165 | content: "\e025"; 166 | } 167 | 168 | .icon-graph-line:before { 169 | content: "\e026"; 170 | } 171 | 172 | .icon-gear:before { 173 | content: "\e027"; 174 | } 175 | 176 | .icon-graph-bar:before { 177 | content: "\e028"; 178 | } 179 | 180 | .icon-export:before { 181 | content: "\e029"; 182 | } 183 | 184 | .icon-feed:before { 185 | content: "\e030"; 186 | } 187 | 188 | .icon-folder:before { 189 | content: "\e031"; 190 | } 191 | 192 | .icon-forward:before { 193 | content: "\e032"; 194 | } 195 | 196 | .icon-folder-open:before { 197 | content: "\e033"; 198 | } 199 | 200 | .icon-download:before { 201 | content: "\e034"; 202 | } 203 | 204 | .icon-document-new:before { 205 | content: "\e035"; 206 | } 207 | 208 | .icon-document-edit:before { 209 | content: "\e036"; 210 | } 211 | 212 | .icon-document:before { 213 | content: "\e037"; 214 | } 215 | 216 | .icon-gaming:before { 217 | content: "\e038"; 218 | } 219 | 220 | .icon-graph-pie:before { 221 | content: "\e039"; 222 | } 223 | 224 | .icon-heart:before { 225 | content: "\e040"; 226 | } 227 | 228 | .icon-headset:before { 229 | content: "\e041"; 230 | } 231 | 232 | .icon-help:before { 233 | content: "\e042"; 234 | } 235 | 236 | .icon-information:before { 237 | content: "\e043"; 238 | } 239 | 240 | .icon-loading:before { 241 | content: "\e044"; 242 | } 243 | 244 | .icon-lock:before { 245 | content: "\e045"; 246 | } 247 | 248 | .icon-location:before { 249 | content: "\e046"; 250 | } 251 | 252 | .icon-lock-open:before { 253 | content: "\e047"; 254 | } 255 | 256 | .icon-mail:before { 257 | content: "\e048"; 258 | } 259 | 260 | .icon-map:before { 261 | content: "\e049"; 262 | } 263 | 264 | .icon-media-loop:before { 265 | content: "\e050"; 266 | } 267 | 268 | .icon-mobile-portrait:before { 269 | content: "\e051"; 270 | } 271 | 272 | .icon-mobile-landscape:before { 273 | content: "\e052"; 274 | } 275 | 276 | .icon-microphone:before { 277 | content: "\e053"; 278 | } 279 | 280 | .icon-minus:before { 281 | content: "\e054"; 282 | } 283 | 284 | .icon-message:before { 285 | content: "\e055"; 286 | } 287 | 288 | .icon-menu:before { 289 | content: "\e056"; 290 | } 291 | 292 | .icon-media-stop:before { 293 | content: "\e057"; 294 | } 295 | 296 | .icon-media-shuffle:before { 297 | content: "\e058"; 298 | } 299 | 300 | .icon-media-previous:before { 301 | content: "\e059"; 302 | } 303 | 304 | .icon-media-play:before { 305 | content: "\e060"; 306 | } 307 | 308 | .icon-media-next:before { 309 | content: "\e061"; 310 | } 311 | 312 | .icon-media-pause:before { 313 | content: "\e062"; 314 | } 315 | 316 | .icon-monitor:before { 317 | content: "\e063"; 318 | } 319 | 320 | .icon-move:before { 321 | content: "\e064"; 322 | } 323 | 324 | .icon-plus:before { 325 | content: "\e065"; 326 | } 327 | 328 | .icon-phone:before { 329 | content: "\e066"; 330 | } 331 | 332 | .icon-preview:before { 333 | content: "\e067"; 334 | } 335 | 336 | .icon-print:before { 337 | content: "\e068"; 338 | } 339 | 340 | .icon-media-record:before { 341 | content: "\e069"; 342 | } 343 | 344 | .icon-music:before { 345 | content: "\e070"; 346 | } 347 | 348 | .icon-home:before { 349 | content: "\e071"; 350 | } 351 | 352 | .icon-question:before { 353 | content: "\e072"; 354 | } 355 | 356 | .icon-reply:before { 357 | content: "\e073"; 358 | } 359 | 360 | .icon-reply-all:before { 361 | content: "\e074"; 362 | } 363 | 364 | .icon-return:before { 365 | content: "\e075"; 366 | } 367 | 368 | .icon-retweet:before { 369 | content: "\e076"; 370 | } 371 | 372 | .icon-search:before { 373 | content: "\e077"; 374 | } 375 | 376 | .icon-view-thumb:before { 377 | content: "\e078"; 378 | } 379 | 380 | .icon-view-list-large:before { 381 | content: "\e079"; 382 | } 383 | 384 | .icon-view-list:before { 385 | content: "\e080"; 386 | } 387 | 388 | .icon-upload:before { 389 | content: "\e081"; 390 | } 391 | 392 | .icon-user-group:before { 393 | content: "\e082"; 394 | } 395 | 396 | .icon-trash:before { 397 | content: "\e083"; 398 | } 399 | 400 | .icon-user:before { 401 | content: "\e084"; 402 | } 403 | 404 | .icon-thumbs-up:before { 405 | content: "\e085"; 406 | } 407 | 408 | .icon-thumbs-down:before { 409 | content: "\e086"; 410 | } 411 | 412 | .icon-tablet-portrait:before { 413 | content: "\e087"; 414 | } 415 | 416 | .icon-tablet-landscape:before { 417 | content: "\e088"; 418 | } 419 | 420 | .icon-tag:before { 421 | content: "\e089"; 422 | } 423 | 424 | .icon-star:before { 425 | content: "\e090"; 426 | } 427 | 428 | .icon-volume-full:before { 429 | content: "\e091"; 430 | } 431 | 432 | .icon-volume-off:before { 433 | content: "\e092"; 434 | } 435 | 436 | .icon-warning:before { 437 | content: "\e093"; 438 | } 439 | 440 | .icon-window:before { 441 | content: "\e094"; 442 | } 443 | --------------------------------------------------------------------------------