Initial work to get working in Firefox

This commit is contained in:
Nathaniel van Diepen
2016-12-14 13:55:43 -07:00
parent 628d6eb2f1
commit 9823acc03f
81 changed files with 5501 additions and 5496 deletions

View File

@@ -1,3 +1,3 @@
{ {
"directory": "vendor/bower_components" "directory": "vendor/bower_components"
} }

68
.gitignore vendored
View File

@@ -1,34 +1,34 @@
# Logs # Logs
logs logs
*.log *.log
# Runtime data # Runtime data
pids pids
*.pid *.pid
*.seed *.seed
# Directory for instrumented libs generated by jscoverage/JSCover # Directory for instrumented libs generated by jscoverage/JSCover
lib-cov lib-cov
# Coverage directory used by tools like istanbul # Coverage directory used by tools like istanbul
coverage coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt .grunt
# Compiled binary addons (http://nodejs.org/api/addons.html) # Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release build/Release
# Dependency directory # Dependency directory
# Deployed apps should consider commenting this line out: # Deployed apps should consider commenting this line out:
# see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git
node_modules node_modules
.DS_Store .DS_Store
vendor/ vendor/
.idea .idea
# Generated chrome extension # Generated chrome extension
public/ public/

View File

@@ -1,5 +1,5 @@
coverage/ coverage/
node_modules/ node_modules/
public/ public/
vendor/ vendor/
tests/ tests/

View File

@@ -1,10 +1,10 @@
{ {
"node": true, "node": true,
"curly": true, "curly": true,
"latedef": true, "latedef": true,
"quotmark": true, "quotmark": true,
"undef": true, "undef": true,
"unused": true, "unused": true,
"trailing": true, "trailing": true,
"predef": [ "chrome" ] "predef": [ "chrome" ]
} }

View File

@@ -1,6 +1,6 @@
{ {
"scripts": { "scripts": {
"lint": "jshint ." "lint": "jshint ."
}, },
"pre-commit": ["lint", "validate", "test"] "pre-commit": ["lint", "validate", "test"]
} }

28
AUTHORS
View File

@@ -1,14 +1,14 @@
WakaTime is written and maintained by Alan Hamlett and WakaTime is written and maintained by Alan Hamlett and
various contributors: various contributors:
Development Lead Development Lead
---------------- ----------------
- Alan Hamlett <alan.hamlett@gmail.com> - Alan Hamlett <alan.hamlett@gmail.com>
- Mario Bašić <mario.basic@outlook.com> - Mario Bašić <mario.basic@outlook.com>
Patches and Suggestions Patches and Suggestions
----------------------- -----------------------

View File

@@ -1,24 +1,24 @@
History History
------- -------
1.0.2 (2016-06-30) 1.0.2 (2016-06-30)
++++++++++++++++++ ++++++++++++++++++
- Fix bug preventing options from saving. #42 - Fix bug preventing options from saving. #42
1.0.1 (2016-06-29) 1.0.1 (2016-06-29)
++++++++++++++++++ ++++++++++++++++++
- Fix blacklist setting. - Fix blacklist setting.
- Misc bug fixes related to icon popup. - Misc bug fixes related to icon popup.
- Send plugin name and version with heartbeat data. #41 - Send plugin name and version with heartbeat data. #41
1.0.0 (2016-06-29) 1.0.0 (2016-06-29)
++++++++++++++++++ ++++++++++++++++++
- Birth - Birth

62
LICENSE
View File

@@ -1,31 +1,31 @@
BSD 3-Clause License BSD 3-Clause License
Copyright (c) 2014 by the respective authors (see AUTHORS file). Copyright (c) 2014 by the respective authors (see AUTHORS file).
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met: modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright * Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer. notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright * Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided in the documentation and/or other materials provided
with the distribution. with the distribution.
* Neither the names of WakaTime, nor the names of its * Neither the names of WakaTime, nor the names of its
contributors may be used to endorse or promote products derived contributors may be used to endorse or promote products derived
from this software without specific prior written permission. from this software without specific prior written permission.
THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

150
README.md
View File

@@ -1,75 +1,75 @@
chrome-wakatime chrome-wakatime
=============== ===============
Automatic time tracking for stats about your website debugging, research, documentation, etc. Automatic time tracking for stats about your website debugging, research, documentation, etc.
##Installation ##Installation
1. Install the extension: 1. Install the extension:
[![Chrome Web Store](https://wakatime.com/static/img/chrome-web-store.png)](https://chrome.google.com/webstore/detail/wakatime/jnbbnacmeggbgdjgaoojpmhdlkkpblgi) [![Chrome Web Store](https://wakatime.com/static/img/chrome-web-store.png)](https://chrome.google.com/webstore/detail/wakatime/jnbbnacmeggbgdjgaoojpmhdlkkpblgi)
2. Login to [WakaTime](https://wakatime.com/). 2. Login to [WakaTime](https://wakatime.com/).
3. Use Chrome like you normally do and your time will be tracked for you automatically. 3. Use Chrome like you normally do and your time will be tracked for you automatically.
4. Visit https://wakatime.com to see your logged time. 4. Visit https://wakatime.com to see your logged time.
5. Use in conjunction with [other WakaTime plugins](https://wakatime.com/plugins). 5. Use in conjunction with [other WakaTime plugins](https://wakatime.com/plugins).
## Screenshots ## Screenshots
![SC open](./screenshots/sc_6-green.png) ![SC open](./screenshots/sc_6-green.png)
![SC open](./screenshots/sc_6-open.png) ![SC open](./screenshots/sc_6-open.png)
![Options SC](./screenshots/sc_8-options.png) ![Options SC](./screenshots/sc_8-options.png)
## Development instructions ## Development instructions
> For development purposes only. > For development purposes only.
To get started, install NPM and Bower dependencies, and do an initial build with Gulp: To get started, install NPM and Bower dependencies, and do an initial build with Gulp:
``` ```
npm start npm start
``` ```
To build the extension once: To build the extension once:
``` ```
npm run gulp npm run gulp
``` ```
To monitor changes: To monitor changes:
``` ```
npm run watch npm run watch
``` ```
Run tests: Run tests:
``` ```
npm test npm test
``` ```
Lint code *(Both JS and JSX)*: Lint code *(Both JS and JSX)*:
``` ```
jsxhint --jsx-only . jsxhint --jsx-only .
``` ```
### Automatic code linting ### Automatic code linting
There is a precommit hook that lints the code before commiting the changes. There is a precommit hook that lints the code before commiting the changes.
### Load unpacked in Chrome ### Load unpacked in Chrome
1. Clone repository to disk 1. Clone repository to disk
2. Go to `Settings` -> `Extensions` 2. Go to `Settings` -> `Extensions`
3. Enable `Developer mode` 3. Enable `Developer mode`
4. Click `Load unpacked extension...` 4. Click `Load unpacked extension...`
5. Select repository directory 5. Select repository directory

View File

@@ -1,15 +1,15 @@
/* This is a fix for Bootstrap requiring jQuery */ /* This is a fix for Bootstrap requiring jQuery */
global.jQuery = require('jquery'); global.jQuery = require('jquery');
require('bootstrap'); require('bootstrap');
var React = require('react'); var React = require('react');
var ReactDOM = require('react-dom'); var ReactDOM = require('react-dom');
// React components // React components
var WakaTime = require('./components/WakaTime.jsx'); var WakaTime = require('./components/WakaTime.jsx');
ReactDOM.render( ReactDOM.render(
<WakaTime />, <WakaTime />,
document.getElementById('wakatime') document.getElementById('wakatime')
); );

View File

@@ -1,19 +1,19 @@
var React = require('react'); var React = require('react');
var classNames = require('classnames'); var classNames = require('classnames');
var Alert = React.createClass({ var Alert = React.createClass({
propTypes: { propTypes: {
type: React.PropTypes.string.isRequired, type: React.PropTypes.string.isRequired,
text: React.PropTypes.string.isRequired text: React.PropTypes.string.isRequired
}, },
render: function() { render: function() {
return( return(
<div className={classNames('alert', 'alert-' + this.props.type)}>{this.props.text}</div> <div className={classNames('alert', 'alert-' + this.props.type)}>{this.props.text}</div>
); );
} }
}); });
module.exports = Alert; module.exports = Alert;

View File

@@ -1,106 +1,106 @@
/* global chrome */ /* global browser */
var React = require('react'); var React = require('react');
var MainList = React.createClass({ var MainList = React.createClass({
_openOptionsPage: function() { _openOptionsPage: function() {
if (chrome.runtime.openOptionsPage) { if (browser.runtime.openOptionsPage) {
// New way to open options pages, if supported (Chrome 42+). // New way to open options pages, if supported (Chrome 42+).
chrome.runtime.openOptionsPage(); browser.runtime.openOptionsPage();
} else { } else {
// Reasonable fallback. // Reasonable fallback.
window.open(chrome.runtime.getURL('options.html')); window.open(browser.runtime.getURL('options.html'));
} }
}, },
render: function() { render: function() {
var that = this; var that = this;
var loginLogoutButton = function() { var loginLogoutButton = function() {
if (that.props.loggedIn === true) { if (that.props.loggedIn === true) {
return ( return (
<div> <div>
<a href="#" className="list-group-item" onClick={that.props.logoutUser}> <a href="#" className="list-group-item" onClick={that.props.logoutUser}>
<i className="fa fa-fw fa-sign-out"></i> <i className="fa fa-fw fa-sign-out"></i>
Logout Logout
</a> </a>
</div> </div>
); );
} }
return ( return (
<a target="_blank" href="https://wakatime.com/login" className="list-group-item"> <a target="_blank" href="https://wakatime.com/login" className="list-group-item">
<i className="fa fa-fw fa-sign-in"></i> <i className="fa fa-fw fa-sign-in"></i>
Login Login
</a> </a>
); );
}; };
// If logging is enabled, display that info to user // If logging is enabled, display that info to user
var loggingStatus = function() { var loggingStatus = function() {
if(that.props.loggingEnabled === true && that.props.loggedIn === true) if(that.props.loggingEnabled === true && that.props.loggedIn === true)
{ {
return ( return (
<div className="row"> <div className="row">
<div className="col-xs-12"> <div className="col-xs-12">
<p> <p>
<a href="#" onClick={that.props.disableLogging} className="btn btn-danger btn-block">Disable logging</a> <a href="#" onClick={that.props.disableLogging} className="btn btn-danger btn-block">Disable logging</a>
</p> </p>
</div> </div>
</div> </div>
); );
} }
else if(that.props.loggingEnabled === false && that.props.loggedIn === true) else if(that.props.loggingEnabled === false && that.props.loggedIn === true)
{ {
return ( return (
<div className="row"> <div className="row">
<div className="col-xs-12"> <div className="col-xs-12">
<p> <p>
<a href="#" onClick={that.props.enableLogging} className="btn btn-success btn-block">Enable logging</a> <a href="#" onClick={that.props.enableLogging} className="btn btn-success btn-block">Enable logging</a>
</p> </p>
</div> </div>
</div> </div>
); );
} }
}; };
var totalTimeLoggedToday = function() { var totalTimeLoggedToday = function() {
if (that.props.loggedIn === true) { if (that.props.loggedIn === true) {
return ( return (
<div className="row"> <div className="row">
<div className="col-xs-12"> <div className="col-xs-12">
<blockquote> <blockquote>
<p>{that.props.totalTimeLoggedToday}</p> <p>{that.props.totalTimeLoggedToday}</p>
<small><cite>TOTAL TIME LOGGED TODAY</cite></small> <small><cite>TOTAL TIME LOGGED TODAY</cite></small>
</blockquote> </blockquote>
</div> </div>
</div> </div>
); );
} }
}; };
return ( return (
<div> <div>
{totalTimeLoggedToday()} {totalTimeLoggedToday()}
{loggingStatus()} {loggingStatus()}
<div className="list-group"> <div className="list-group">
<a href="#" className="list-group-item" onClick={this._openOptionsPage}> <a href="#" className="list-group-item" onClick={this._openOptionsPage}>
<i className="fa fa-fw fa-cogs"></i> <i className="fa fa-fw fa-cogs"></i>
Options Options
</a> </a>
{loginLogoutButton()} {loginLogoutButton()}
</div> </div>
</div> </div>
); );
} }
}); });
module.exports = MainList; module.exports = MainList;

View File

@@ -1,89 +1,89 @@
var React = require('react'); var React = require('react');
var NavBar = React.createClass({ var NavBar = React.createClass({
render: function() { render: function() {
var that = this; var that = this;
var signedInAs = function() { var signedInAs = function() {
if (that.props.loggedIn === true) { if (that.props.loggedIn === true) {
return ( return (
<p className="navbar-text">Signed in as <b>{that.props.user.full_name}</b></p> <p className="navbar-text">Signed in as <b>{that.props.user.full_name}</b></p>
); );
} }
}; };
var dashboard = function() { var dashboard = function() {
if (that.props.loggedIn === true) { if (that.props.loggedIn === true) {
return ( return (
<li> <li>
<a target="_blank" href="https://wakatime.com/dashboard"> <a target="_blank" href="https://wakatime.com/dashboard">
<i className="fa fa-fw fa-tachometer"></i> <i className="fa fa-fw fa-tachometer"></i>
Dashboard Dashboard
</a> </a>
</li> </li>
); );
} }
}; };
var customRules = function() { var customRules = function() {
if (that.props.loggedIn === true) { if (that.props.loggedIn === true) {
return ( return (
<li> <li>
<a target="_blank" href="https://wakatime.com/settings/rules"> <a target="_blank" href="https://wakatime.com/settings/rules">
<i className="fa fa-fw fa-filter"></i> <i className="fa fa-fw fa-filter"></i>
Custom Rules Custom Rules
</a> </a>
</li> </li>
); );
} }
}; };
return ( return (
<nav className="navbar navbar-default" role="navigation"> <nav className="navbar navbar-default" role="navigation">
<div className="container-fluid"> <div className="container-fluid">
<div className="navbar-header"> <div className="navbar-header">
<button type="button" className="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1"> <button type="button" className="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
<span className="sr-only">Toggle navigation</span> <span className="sr-only">Toggle navigation</span>
<i className="fa fa-fw fa-cogs"></i> <i className="fa fa-fw fa-cogs"></i>
</button> </button>
<a target="_blank" className="navbar-brand" href="https://wakatime.com"> <a target="_blank" className="navbar-brand" href="https://wakatime.com">
WakaTime WakaTime
<img src="graphics/wakatime-logo-48.png" /> <img src="graphics/wakatime-logo-48.png" />
</a> </a>
</div> </div>
<div className="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> <div className="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
{signedInAs()} {signedInAs()}
<ul className="nav navbar-nav"> <ul className="nav navbar-nav">
{customRules()} {customRules()}
{dashboard()} {dashboard()}
<li className="dropdown"> <li className="dropdown">
<a href="#" className="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"> <a href="#" className="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">
<i className="fa fa-fw fa-info"></i> <i className="fa fa-fw fa-info"></i>
About About
<span className="caret"></span> <span className="caret"></span>
</a> </a>
<ul className="dropdown-menu" role="menu"> <ul className="dropdown-menu" role="menu">
<li> <li>
<a target="_blank" href="https://github.com/wakatime/chrome-wakatime/issues"> <a target="_blank" href="https://github.com/wakatime/chrome-wakatime/issues">
<i className="fa fa-fw fa-bug"></i> <i className="fa fa-fw fa-bug"></i>
Report an Issue</a> Report an Issue</a>
</li> </li>
<li> <li>
<a target="_blank" href="https://github.com/wakatime/chrome-wakatime"> <a target="_blank" href="https://github.com/wakatime/chrome-wakatime">
<i className="fa fa-fw fa-github"></i> <i className="fa fa-fw fa-github"></i>
View on GitHub</a> View on GitHub</a>
</li> </li>
</ul> </ul>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
</nav> </nav>
); );
} }
}); });
module.exports = NavBar; module.exports = NavBar;

View File

@@ -1,214 +1,214 @@
/* global chrome */ /* global browser */
var React = require('react'); var React = require('react');
var ReactCSSTransitionGroup = require('react-addons-css-transition-group'); var ReactCSSTransitionGroup = require('react-addons-css-transition-group');
var config = require('../config'); var config = require('../config');
// React components // React components
var Alert = require('./Alert.jsx'); var Alert = require('./Alert.jsx');
var SitesList = require('./SitesList.jsx'); var SitesList = require('./SitesList.jsx');
/** /**
* One thing to keep in mind is that you cannot use this.refs.blacklist if * One thing to keep in mind is that you cannot use this.refs.blacklist if
* the blacklist select box is not being rendered on the form. * the blacklist select box is not being rendered on the form.
* *
* @type {*|Function} * @type {*|Function}
*/ */
var Options = React.createClass({ var Options = React.createClass({
getInitialState: function () { getInitialState: function () {
return { return {
theme: config.theme, theme: config.theme,
blacklist: '', blacklist: '',
whitelist: '', whitelist: '',
loggingType: config.loggingType, loggingType: config.loggingType,
loggingStyle: config.loggingStyle, loggingStyle: config.loggingStyle,
displayAlert: false, displayAlert: false,
alertType: config.alert.success.type, alertType: config.alert.success.type,
alertText: config.alert.success.text alertText: config.alert.success.text
}; };
}, },
componentDidMount: function () { componentDidMount: function () {
this.restoreSettings(); this.restoreSettings();
}, },
restoreSettings: function () { restoreSettings: function () {
var that = this; var that = this;
chrome.storage.sync.get({ browser.storage.sync.get({
theme: config.theme, theme: config.theme,
blacklist: '', blacklist: '',
whitelist: '', whitelist: '',
loggingType: config.loggingType, loggingType: config.loggingType,
loggingStyle: config.loggingStyle loggingStyle: config.loggingStyle
}, function (items) { }).then(function (items) {
that.setState({ that.setState({
theme: items.theme, theme: items.theme,
blacklist: items.blacklist, blacklist: items.blacklist,
whitelist: items.whitelist, whitelist: items.whitelist,
loggingType: items.loggingType, loggingType: items.loggingType,
loggingStyle: items.loggingStyle loggingStyle: items.loggingStyle
}); });
that.refs.theme.value = items.theme; that.refs.theme.value = items.theme;
that.refs.loggingType.value = items.loggingType; that.refs.loggingType.value = items.loggingType;
that.refs.loggingStyle.value = items.loggingStyle; that.refs.loggingStyle.value = items.loggingStyle;
}); });
}, },
_handleSubmit: function (e) { _handleSubmit: function (e) {
e.preventDefault(); e.preventDefault();
this.saveSettings(); this.saveSettings();
}, },
saveSettings: function () { saveSettings: function () {
var that = this; var that = this;
var theme = this.refs.theme.value.trim(); var theme = this.refs.theme.value.trim();
var loggingType = this.refs.loggingType.value.trim(); var loggingType = this.refs.loggingType.value.trim();
var loggingStyle = this.refs.loggingStyle.value.trim(); var loggingStyle = this.refs.loggingStyle.value.trim();
// Trimming blacklist and whitelist removes blank lines and spaces. // Trimming blacklist and whitelist removes blank lines and spaces.
var blacklist = that.state.blacklist.trim(); var blacklist = that.state.blacklist.trim();
var whitelist = that.state.whitelist.trim(); var whitelist = that.state.whitelist.trim();
// Sync options with google storage. // Sync options with google storage.
chrome.storage.sync.set({ browser.storage.sync.set({
theme: theme, theme: theme,
blacklist: blacklist, blacklist: blacklist,
whitelist: whitelist, whitelist: whitelist,
loggingType: loggingType, loggingType: loggingType,
loggingStyle: loggingStyle loggingStyle: loggingStyle
}, function () { }).then(function () {
// Set state to be newly entered values. // Set state to be newly entered values.
that.setState({ that.setState({
theme: theme, theme: theme,
blacklist: blacklist, blacklist: blacklist,
whitelist: whitelist, whitelist: whitelist,
loggingType: loggingType, loggingType: loggingType,
loggingStyle: loggingStyle, loggingStyle: loggingStyle,
displayAlert: true displayAlert: true
}); });
}); });
}, },
_displayBlackOrWhiteList: function () { _displayBlackOrWhiteList: function () {
var loggingStyle = this.refs.loggingStyle.value.trim(); var loggingStyle = this.refs.loggingStyle.value.trim();
this.setState({loggingStyle: loggingStyle}); this.setState({loggingStyle: loggingStyle});
}, },
_updateBlacklistState: function(sites){ _updateBlacklistState: function(sites){
this.setState({ this.setState({
blacklist: sites blacklist: sites
}); });
}, },
_updateWhitelistState: function(sites){ _updateWhitelistState: function(sites){
this.setState({ this.setState({
whitelist: sites whitelist: sites
}); });
}, },
render: function () { render: function () {
var that = this; var that = this;
var alert = function() { var alert = function() {
if(that.state.displayAlert === true){ if(that.state.displayAlert === true){
setTimeout(function () { setTimeout(function () {
that.setState({displayAlert:false}); that.setState({displayAlert:false});
}, 2000); }, 2000);
return( return(
<Alert key={that.state.alertText} type={that.state.alertType} text={that.state.alertText} /> <Alert key={that.state.alertText} type={that.state.alertType} text={that.state.alertText} />
); );
} }
}; };
var loggingStyle = function () { var loggingStyle = function () {
if (that.state.loggingStyle == 'blacklist') { if (that.state.loggingStyle == 'blacklist') {
return ( return (
<SitesList <SitesList
handleChange={that._updateBlacklistState} handleChange={that._updateBlacklistState}
label="Blacklist" label="Blacklist"
sites={that.state.blacklist} sites={that.state.blacklist}
helpText="Sites that you don't want to show in your reports." /> helpText="Sites that you don't want to show in your reports." />
); );
} }
return ( return (
<SitesList <SitesList
handleChange={that._updateWhitelistState} handleChange={that._updateWhitelistState}
label="Whitelist" label="Whitelist"
sites={that.state.whitelist} sites={that.state.whitelist}
helpText="Sites that you want to show in your reports." /> helpText="Sites that you want to show in your reports." />
); );
}; };
return ( return (
<div className="container"> <div className="container">
<div className="row"> <div className="row">
<div className="col-md-12"> <div className="col-md-12">
<ReactCSSTransitionGroup transitionName="alert" transitionEnterTimeout={500} transitionLeaveTimeout={300}> <ReactCSSTransitionGroup transitionName="alert" transitionEnterTimeout={500} transitionLeaveTimeout={300}>
{alert()} {alert()}
</ReactCSSTransitionGroup> </ReactCSSTransitionGroup>
<form className="form-horizontal" onSubmit={this._handleSubmit}> <form className="form-horizontal" onSubmit={this._handleSubmit}>
<div className="form-group"> <div className="form-group">
<label className="col-lg-2 control-label">Logging style</label> <label className="col-lg-2 control-label">Logging style</label>
<div className="col-lg-10"> <div className="col-lg-10">
<select className="form-control" ref="loggingStyle" defaultValue="blacklist" onChange={this._displayBlackOrWhiteList}> <select className="form-control" ref="loggingStyle" defaultValue="blacklist" onChange={this._displayBlackOrWhiteList}>
<option value="blacklist">All except blacklisted sites</option> <option value="blacklist">All except blacklisted sites</option>
<option value="whitelist">Only whitelisted sites</option> <option value="whitelist">Only whitelisted sites</option>
</select> </select>
</div> </div>
</div> </div>
{loggingStyle()} {loggingStyle()}
<div className="form-group"> <div className="form-group">
<label className="col-lg-2 control-label">Logging type</label> <label className="col-lg-2 control-label">Logging type</label>
<div className="col-lg-10"> <div className="col-lg-10">
<select className="form-control" ref="loggingType" defaultValue="domain"> <select className="form-control" ref="loggingType" defaultValue="domain">
<option value="domain">Only the domain</option> <option value="domain">Only the domain</option>
<option value="url">Entire URL</option> <option value="url">Entire URL</option>
</select> </select>
</div> </div>
</div> </div>
<div className="form-group"> <div className="form-group">
<label htmlFor="theme" className="col-lg-2 control-label">Theme</label> <label htmlFor="theme" className="col-lg-2 control-label">Theme</label>
<div className="col-lg-10"> <div className="col-lg-10">
<select className="form-control" ref="theme" defaultValue="light"> <select className="form-control" ref="theme" defaultValue="light">
<option value="light">Light</option> <option value="light">Light</option>
<option value="dark">Dark</option> <option value="dark">Dark</option>
</select> </select>
</div> </div>
</div> </div>
<div className="form-group"> <div className="form-group">
<div className="col-lg-10 col-lg-offset-2"> <div className="col-lg-10 col-lg-offset-2">
<button type="submit" className="btn btn-primary">Save</button> <button type="submit" className="btn btn-primary">Save</button>
</div> </div>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
); );
} }
}); });
module.exports = Options; module.exports = Options;

View File

@@ -1,35 +1,35 @@
var React = require('react'); var React = require('react');
var SitesList = React.createClass({ var SitesList = React.createClass({
getDefaultProps: function () { getDefaultProps: function () {
return { return {
placeholder: 'http://google.com' placeholder: 'http://google.com'
}; };
}, },
_handleChange: function (event) { _handleChange: function (event) {
var sites = event.target.value; var sites = event.target.value;
this.props.handleChange(sites); this.props.handleChange(sites);
}, },
render: function () { render: function () {
return ( return (
<div className="form-group"> <div className="form-group">
<label htmlFor="sites" className="col-lg-2 control-label">{this.props.label}</label> <label htmlFor="sites" className="col-lg-2 control-label">{this.props.label}</label>
<div className="col-lg-10"> <div className="col-lg-10">
<textarea className="form-control" rows="3" ref="sites" onChange={this._handleChange} <textarea className="form-control" rows="3" ref="sites" onChange={this._handleChange}
placeholder={this.props.placeholder} value={this.props.sites}></textarea> placeholder={this.props.placeholder} value={this.props.sites}></textarea>
<span className="help-block">{this.props.helpText} <span className="help-block">{this.props.helpText}
<br/> <br/>
One line per site.</span> One line per site.</span>
</div> </div>
</div> </div>
); );
} }
}); });
module.exports = SitesList; module.exports = SitesList;

View File

@@ -1,173 +1,173 @@
/* global chrome */ /* global browser */
var React = require("react"); var React = require("react");
var $ = require('jquery'); var $ = require('jquery');
var config = require('../config'); var config = require('../config');
// React components // React components
var NavBar = require('./NavBar.jsx'); var NavBar = require('./NavBar.jsx');
var MainList = require('./MainList.jsx'); var MainList = require('./MainList.jsx');
// Core // Core
var WakaTimeCore = require('../core/WakaTimeCore').default; var WakaTimeCore = require('../core/WakaTimeCore').default;
// Helpers // Helpers
var changeExtensionState = require('../helpers/changeExtensionState'); var changeExtensionState = require('../helpers/changeExtensionState');
var Wakatime = React.createClass({ var Wakatime = React.createClass({
getInitialState: function() { getInitialState: function() {
return { return {
user: { user: {
full_name: null, full_name: null,
email: null, email: null,
photo: null photo: null
}, },
loggedIn: false, loggedIn: false,
loggingEnabled: config.loggingEnabled, loggingEnabled: config.loggingEnabled,
totalTimeLoggedToday: '0 minutes' totalTimeLoggedToday: '0 minutes'
}; };
}, },
componentDidMount: function() { componentDidMount: function() {
var wakatime = new WakaTimeCore(); var wakatime = new WakaTimeCore();
var that = this; var that = this;
wakatime.checkAuth().done(function(data) { wakatime.checkAuth().done(function(data) {
if (data !== false) { if (data !== false) {
chrome.storage.sync.get({ browser.storage.sync.get({
loggingEnabled: config.loggingEnabled loggingEnabled: config.loggingEnabled
}, function(items) { }).then(function(items) {
that.setState({loggingEnabled: items.loggingEnabled}); that.setState({loggingEnabled: items.loggingEnabled});
if (items.loggingEnabled === true) { if (items.loggingEnabled === true) {
changeExtensionState('allGood'); changeExtensionState('allGood');
} }
else { else {
changeExtensionState('notLogging'); changeExtensionState('notLogging');
} }
}); });
that.setState({ that.setState({
user: { user: {
full_name: data.full_name, full_name: data.full_name,
email: data.email, email: data.email,
photo: data.photo photo: data.photo
}, },
loggedIn: true loggedIn: true
}); });
wakatime.getTotalTimeLoggedToday().done(function(grand_total) { wakatime.getTotalTimeLoggedToday().done(function(grand_total) {
that.setState({ that.setState({
totalTimeLoggedToday: grand_total.text totalTimeLoggedToday: grand_total.text
}); });
}); });
} }
else { else {
changeExtensionState('notSignedIn'); changeExtensionState('notSignedIn');
} }
}); });
}, },
logoutUser: function() { logoutUser: function() {
var deferredObject = $.Deferred(); var deferredObject = $.Deferred();
var that = this; var that = this;
$.ajax({ $.ajax({
url: config.logoutUserUrl, url: config.logoutUserUrl,
method: 'GET', method: 'GET',
success: function() { success: function() {
deferredObject.resolve(that); deferredObject.resolve(that);
}, },
error: function(xhr, status, err) { error: function(xhr, status, err) {
console.error(config.logoutUserUrl, status, err.toString()); console.error(config.logoutUserUrl, status, err.toString());
deferredObject.resolve(that); deferredObject.resolve(that);
} }
}); });
return deferredObject.promise(); return deferredObject.promise();
}, },
_logoutUser: function() { _logoutUser: function() {
var that = this; var that = this;
this.logoutUser().done(function(){ this.logoutUser().done(function(){
that.setState({ that.setState({
user: { user: {
full_name: null, full_name: null,
email: null, email: null,
photo: null photo: null
}, },
loggedIn: false, loggedIn: false,
loggingEnabled: false loggingEnabled: false
}); });
changeExtensionState('notSignedIn'); changeExtensionState('notSignedIn');
}); });
}, },
_disableLogging: function() { _disableLogging: function() {
this.setState({ this.setState({
loggingEnabled: false loggingEnabled: false
}); });
changeExtensionState('notLogging'); changeExtensionState('notLogging');
chrome.storage.sync.set({ browser.storage.sync.set({
loggingEnabled: false loggingEnabled: false
}); });
}, },
_enableLogging: function() { _enableLogging: function() {
this.setState({ this.setState({
loggingEnabled: true loggingEnabled: true
}); });
changeExtensionState('allGood'); changeExtensionState('allGood');
chrome.storage.sync.set({ browser.storage.sync.set({
loggingEnabled: true loggingEnabled: true
}); });
}, },
render: function() { render: function() {
return ( return (
<div> <div>
<NavBar <NavBar
user={this.state.user} user={this.state.user}
loggedIn={this.state.loggedIn} /> loggedIn={this.state.loggedIn} />
<div className="container"> <div className="container">
<div className="row"> <div className="row">
<div className="col-md-12"> <div className="col-md-12">
<MainList <MainList
disableLogging={this._disableLogging} disableLogging={this._disableLogging}
enableLogging={this._enableLogging} enableLogging={this._enableLogging}
loggingEnabled={this.state.loggingEnabled} loggingEnabled={this.state.loggingEnabled}
user={this.state.user} user={this.state.user}
totalTimeLoggedToday={this.state.totalTimeLoggedToday} totalTimeLoggedToday={this.state.totalTimeLoggedToday}
logoutUser={this._logoutUser} logoutUser={this._logoutUser}
loggedIn={this.state.loggedIn} /> loggedIn={this.state.loggedIn} />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
); );
} }
}); });
module.exports = Wakatime; module.exports = Wakatime;

View File

@@ -1,67 +1,67 @@
/* global chrome */ /* global browser */
//jshint esnext:true //jshint esnext:true
var config = { var config = {
// Extension name // Extension name
name: 'WakaTime', name: 'WakaTime',
// Extension version // Extension version
version: chrome.app.getDetails().version, version: browser.runtime.getManifest().version,
// Time for idle state of the browser // Time for idle state of the browser
// The user is considered idle if there was // The user is considered idle if there was
// no activity in the browser for x seconds // no activity in the browser for x seconds
detectionIntervalInSeconds: 60, detectionIntervalInSeconds: 60,
// Default logging style // Default logging style
// Log all except blacklisted sites // Log all except blacklisted sites
// or log only the white listed sites. // or log only the white listed sites.
loggingStyle: 'blacklist', loggingStyle: 'blacklist',
// Default logging type // Default logging type
loggingType: 'domain', loggingType: 'domain',
// By default logging is enabled // By default logging is enabled
loggingEnabled: true, loggingEnabled: true,
// Url to which to send the heartbeat // Url to which to send the heartbeat
heartbeatApiUrl: 'https://api.wakatime.com/api/v1/users/current/heartbeats', heartbeatApiUrl: 'https://api.wakatime.com/api/v1/users/current/heartbeats',
// Url from which to detect if the user is logged in // Url from which to detect if the user is logged in
currentUserApiUrl: 'https://api.wakatime.com/api/v1/users/current', currentUserApiUrl: 'https://api.wakatime.com/api/v1/users/current',
// The url to logout the user from wakatime // The url to logout the user from wakatime
logoutUserUrl: 'https://wakatime.com/logout', logoutUserUrl: 'https://wakatime.com/logout',
// Gets stats from the WakaTime API // Gets stats from the WakaTime API
summariesApiUrl: 'https://api.wakatime.com/api/v1/users/current/summaries', summariesApiUrl: 'https://api.wakatime.com/api/v1/users/current/summaries',
// Different colors for different states of the extension // Different colors for different states of the extension
colors: { colors: {
allGood: '', allGood: '',
notLogging: 'gray', notLogging: 'gray',
notSignedIn: 'red', notSignedIn: 'red',
lightTheme: 'white' lightTheme: 'white'
}, },
// Tooltips for each of the extension states // Tooltips for each of the extension states
tooltips: { tooltips: {
allGood: '', allGood: '',
notLogging: 'Not logging', notLogging: 'Not logging',
notSignedIn: 'Not signed In', notSignedIn: 'Not signed In',
blacklisted: 'This URL is blacklisted', blacklisted: 'This URL is blacklisted',
whitelisted: 'This URL is not on your whitelist' whitelisted: 'This URL is not on your whitelist'
}, },
// Default theme // Default theme
theme: 'light', theme: 'light',
// Valid extension states // Valid extension states
states: [ states: [
'allGood', 'allGood',
'notLogging', 'notLogging',
'notSignedIn', 'notSignedIn',
'blacklisted', 'blacklisted',
'whitelisted' 'whitelisted'
], ],
// Predefined alert type and text for success and failure. // Predefined alert type and text for success and failure.
alert: { alert: {
success: { success: {
type: 'success', type: 'success',
text: 'Options have been saved!' text: 'Options have been saved!'
}, },
failure: { failure: {
type: 'danger', type: 'danger',
text: 'There was an error while saving the options!' text: 'There was an error while saving the options!'
} }
} }
}; };
module.exports = config; module.exports = config;

View File

@@ -1,256 +1,256 @@
/* global chrome */ /* global browser */
//jshint esnext:true //jshint esnext:true
var $ = require('jquery'); var $ = require('jquery');
var moment = require('moment'); var moment = require('moment');
var config = require('./../config'); var config = require('./../config');
// Helpers // Helpers
var getDomainFromUrl = require('./../helpers/getDomainFromUrl'); var getDomainFromUrl = require('./../helpers/getDomainFromUrl');
var changeExtensionState = require('../helpers/changeExtensionState'); var changeExtensionState = require('../helpers/changeExtensionState');
var in_array = require('./../helpers/in_array'); var in_array = require('./../helpers/in_array');
var contains = require('./../helpers/contains'); var contains = require('./../helpers/contains');
class WakaTimeCore { class WakaTimeCore {
constructor() { constructor() {
this.tabsWithDevtoolsOpen = []; this.tabsWithDevtoolsOpen = [];
} }
/** /**
* Settter for tabsWithDevtoolsOpen * Settter for tabsWithDevtoolsOpen
* *
* @param tabs * @param tabs
*/ */
setTabsWithDevtoolsOpen(tabs) { setTabsWithDevtoolsOpen(tabs) {
this.tabsWithDevtoolsOpen = tabs; this.tabsWithDevtoolsOpen = tabs;
} }
getTotalTimeLoggedToday() { getTotalTimeLoggedToday() {
var deferredObject = $.Deferred(); var deferredObject = $.Deferred();
var today = moment().format('YYYY-MM-DD'); var today = moment().format('YYYY-MM-DD');
$.ajax({ $.ajax({
url: config.summariesApiUrl + '?start=' + today + '&end=' + today, url: config.summariesApiUrl + '?start=' + today + '&end=' + today,
dataType: 'json', dataType: 'json',
success: (data) => { success: (data) => {
deferredObject.resolve(data.data[0].grand_total); deferredObject.resolve(data.data[0].grand_total);
}, },
error: (xhr, status, err) => { error: (xhr, status, err) => {
console.error(config.summariesApiUrl, status, err.toString()); console.error(config.summariesApiUrl, status, err.toString());
deferredObject.resolve(false); deferredObject.resolve(false);
} }
}); });
return deferredObject.promise(); return deferredObject.promise();
} }
/** /**
* Checks if the user is logged in. * Checks if the user is logged in.
* *
* @returns {*} * @returns {*}
*/ */
checkAuth() { checkAuth() {
var deferredObject = $.Deferred(); var deferredObject = $.Deferred();
$.ajax({ $.ajax({
url: config.currentUserApiUrl, url: config.currentUserApiUrl,
dataType: 'json', dataType: 'json',
success: (data) => { success: (data) => {
deferredObject.resolve(data.data); deferredObject.resolve(data.data);
}, },
error: (xhr, status, err) => { error: (xhr, status, err) => {
console.error(config.currentUserApiUrl, status, err.toString()); console.error(config.currentUserApiUrl, status, err.toString());
deferredObject.resolve(false); deferredObject.resolve(false);
} }
}); });
return deferredObject.promise(); return deferredObject.promise();
} }
/** /**
* Depending on various factors detects the current active tab URL or domain, * Depending on various factors detects the current active tab URL or domain,
* and sends it to WakaTime for logging. * and sends it to WakaTime for logging.
*/ */
recordHeartbeat() { recordHeartbeat() {
chrome.storage.sync.get({ browser.storage.sync.get({
loggingEnabled: config.loggingEnabled, loggingEnabled: config.loggingEnabled,
loggingStyle: config.loggingStyle, loggingStyle: config.loggingStyle,
blacklist: '', blacklist: '',
whitelist: '' whitelist: ''
}, (items) => { }).then((items) => {
if (items.loggingEnabled === true) { if (items.loggingEnabled === true) {
changeExtensionState('allGood'); changeExtensionState('allGood');
chrome.idle.queryState(config.detectionIntervalInSeconds, (newState) => { browser.idle.queryState(config.detectionIntervalInSeconds).then((newState) => {
if (newState === 'active') { if (newState === 'active') {
// Get current tab URL. // Get current tab URL.
chrome.tabs.query({active: true}, (tabs) => { browser.tabs.query({active: true}).then((tabs) => {
var currentActiveTab = tabs[0]; var currentActiveTab = tabs[0];
var debug = false; var debug = false;
// If the current active tab has devtools open // If the current active tab has devtools open
if (in_array(currentActiveTab.id, this.tabsWithDevtoolsOpen)) debug = true; if (in_array(currentActiveTab.id, this.tabsWithDevtoolsOpen)) debug = true;
if (items.loggingStyle == 'blacklist') { if (items.loggingStyle == 'blacklist') {
if (! contains(currentActiveTab.url, items.blacklist)) { if (! contains(currentActiveTab.url, items.blacklist)) {
this.sendHeartbeat(currentActiveTab.url, debug); this.sendHeartbeat(currentActiveTab.url, debug);
} }
else { else {
changeExtensionState('blacklisted'); changeExtensionState('blacklisted');
console.log(currentActiveTab.url + ' is on a blacklist.'); console.log(currentActiveTab.url + ' is on a blacklist.');
} }
} }
if (items.loggingStyle == 'whitelist') { if (items.loggingStyle == 'whitelist') {
if (contains(currentActiveTab.url, items.whitelist)) { if (contains(currentActiveTab.url, items.whitelist)) {
this.sendHeartbeat(currentActiveTab.url, debug); this.sendHeartbeat(currentActiveTab.url, debug);
} }
else { else {
changeExtensionState('whitelisted'); changeExtensionState('whitelisted');
console.log(currentActiveTab.url + ' is not on a whitelist.'); console.log(currentActiveTab.url + ' is not on a whitelist.');
} }
} }
}); });
} }
}); });
} }
else { else {
changeExtensionState('notLogging'); changeExtensionState('notLogging');
} }
}); });
} }
/** /**
* Creates payload for the heartbeat and returns it as JSON. * Creates payload for the heartbeat and returns it as JSON.
* *
* @param entity * @param entity
* @param type * @param type
* @param debug * @param debug
* @returns {*} * @returns {*}
* @private * @private
*/ */
_preparePayload(entity, type, debug = false) { _preparePayload(entity, type, debug = false) {
return JSON.stringify({ return JSON.stringify({
entity: entity, entity: entity,
type: type, type: type,
time: moment().format('X'), time: moment().format('X'),
project: '<<LAST_PROJECT>>', project: '<<LAST_PROJECT>>',
is_debugging: debug, is_debugging: debug,
plugin: 'chrome-wakatime/' + config.version plugin: 'browser-wakatime/' + config.version
}); });
} }
/** /**
* Returns a promise with logging type variable. * Returns a promise with logging type variable.
* *
* @returns {*} * @returns {*}
* @private * @private
*/ */
_getLoggingType() { _getLoggingType() {
var deferredObject = $.Deferred(); var deferredObject = $.Deferred();
chrome.storage.sync.get({ browser.storage.sync.get({
loggingType: config.loggingType loggingType: config.loggingType
}, function (items) { }).then(function (items) {
deferredObject.resolve(items.loggingType); deferredObject.resolve(items.loggingType);
}); });
return deferredObject.promise(); return deferredObject.promise();
} }
/** /**
* Given the entity and logging type it creates a payload and * Given the entity and logging type it creates a payload and
* sends an ajax post request to the API. * sends an ajax post request to the API.
* *
* @param entity * @param entity
* @param debug * @param debug
*/ */
sendHeartbeat(entity, debug) { sendHeartbeat(entity, debug) {
var payload = null; var payload = null;
this._getLoggingType().done((loggingType) => { this._getLoggingType().done((loggingType) => {
// Get only the domain from the entity. // Get only the domain from the entity.
// And send that in heartbeat // And send that in heartbeat
if (loggingType == 'domain') { if (loggingType == 'domain') {
var domain = getDomainFromUrl(entity); var domain = getDomainFromUrl(entity);
payload = this._preparePayload(domain, 'domain', debug); payload = this._preparePayload(domain, 'domain', debug);
console.log(payload); console.log(payload);
this.sendAjaxRequestToApi(payload); this.sendAjaxRequestToApi(payload);
} }
// Send entity in heartbeat // Send entity in heartbeat
else if (loggingType == 'url') { else if (loggingType == 'url') {
payload = this._preparePayload(entity, 'url', debug); payload = this._preparePayload(entity, 'url', debug);
console.log(payload); console.log(payload);
this.sendAjaxRequestToApi(payload); this.sendAjaxRequestToApi(payload);
} }
}); });
} }
/** /**
* Sends AJAX request with payload to the heartbeat API as JSON. * Sends AJAX request with payload to the heartbeat API as JSON.
* *
* @param payload * @param payload
* @param method * @param method
* @returns {*} * @returns {*}
*/ */
sendAjaxRequestToApi(payload, method = 'POST') { sendAjaxRequestToApi(payload, method = 'POST') {
var deferredObject = $.Deferred(); var deferredObject = $.Deferred();
$.ajax({ $.ajax({
url: config.heartbeatApiUrl, url: config.heartbeatApiUrl,
dataType: 'json', dataType: 'json',
contentType: 'application/json', contentType: 'application/json',
method: method, method: method,
data: payload, data: payload,
statusCode: { statusCode: {
401: function () { 401: function () {
changeExtensionState('notSignedIn'); changeExtensionState('notSignedIn');
}, },
201: function () { 201: function () {
// nothing to do here // nothing to do here
} }
}, },
success: (response) => { success: (response) => {
deferredObject.resolve(this); deferredObject.resolve(this);
}, },
error: (xhr, status, err) => { error: (xhr, status, err) => {
console.error(config.heartbeatApiUrl, status, err.toString()); console.error(config.heartbeatApiUrl, status, err.toString());
deferredObject.resolve(this); deferredObject.resolve(this);
} }
}); });
return deferredObject.promise(); return deferredObject.promise();
} }
} }
export default WakaTimeCore; export default WakaTimeCore;

View File

@@ -1,12 +1,12 @@
/* global chrome */ /* global browser */
// Create a connection to the background page // Create a connection to the background page
var backgroundPageConnection = chrome.runtime.connect({ var backgroundPageConnection = browser.runtime.connect({
name: "devtools-page" name: "devtools-page"
}); });
// Send a message to background page with the current active tabId // Send a message to background page with the current active tabId
backgroundPageConnection.postMessage({ backgroundPageConnection.postMessage({
name: 'init', name: 'init',
tabId: chrome.devtools.inspectedWindow.tabId tabId: browser.devtools.inspectedWindow.tabId
}); });

View File

@@ -1,100 +1,100 @@
/* global chrome */ /* global browser */
// Core // Core
var WakaTimeCore = require("./core/WakaTimeCore").default; var WakaTimeCore = require("./core/WakaTimeCore").default;
// initialize class // initialize class
var wakatime = new WakaTimeCore(); var wakatime = new WakaTimeCore();
// Holds currently open connections (ports) with devtools // Holds currently open connections (ports) with devtools
// Uses tabId as index key. // Uses tabId as index key.
var connections = {}; var connections = {};
// Add a listener to resolve alarms // Add a listener to resolve alarms
chrome.alarms.onAlarm.addListener(function (alarm) { browser.alarms.onAlarm.addListener(function (alarm) {
// |alarm| can be undefined because onAlarm also gets called from // |alarm| can be undefined because onAlarm also gets called from
// window.setTimeout on old chrome versions. // window.setTimeout on old chrome versions.
if (alarm && alarm.name == 'heartbeatAlarm') { if (alarm && alarm.name == 'heartbeatAlarm') {
console.log('recording a heartbeat - alarm triggered'); console.log('recording a heartbeat - alarm triggered');
wakatime.recordHeartbeat(); wakatime.recordHeartbeat();
} }
}); });
// Create a new alarm for heartbeats. // Create a new alarm for heartbeats.
chrome.alarms.create('heartbeatAlarm', {periodInMinutes: 2}); browser.alarms.create('heartbeatAlarm', {periodInMinutes: 2});
/** /**
* Whenever a active tab is changed it records a heartbeat with that tab url. * Whenever a active tab is changed it records a heartbeat with that tab url.
*/ */
chrome.tabs.onActivated.addListener(function (activeInfo) { browser.tabs.onActivated.addListener(function (activeInfo) {
chrome.tabs.get(activeInfo.tabId, function (tab) { browser.tabs.get(activeInfo.tabId).then(function (tab) {
console.log('recording a heartbeat - active tab changed'); console.log('recording a heartbeat - active tab changed');
wakatime.recordHeartbeat(); wakatime.recordHeartbeat();
}); });
}); });
/** /**
* Whenever any tab is updated it checks if the updated tab is the tab that is * Whenever any tab is updated it checks if the updated tab is the tab that is
* currently active and if it is, then it records a heartbeat. * currently active and if it is, then it records a heartbeat.
*/ */
chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) { browser.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {
if (changeInfo.status === 'complete') { if (changeInfo.status === 'complete') {
// Get current tab URL. // Get current tab URL.
chrome.tabs.query({active: true}, function(tabs) { browser.tabs.query({active: true}).then(function(tabs) {
// If tab updated is the same as active tab // If tab updated is the same as active tab
if (tabId == tabs[0].id) { if (tabId == tabs[0].id) {
console.log('recording a heartbeat - tab updated'); console.log('recording a heartbeat - tab updated');
wakatime.recordHeartbeat(); wakatime.recordHeartbeat();
} }
}); });
} }
}); });
/** /**
* This is in charge of detecting if devtools are opened or closed * This is in charge of detecting if devtools are opened or closed
* and sending a heartbeat depending on that. * and sending a heartbeat depending on that.
*/ */
chrome.runtime.onConnect.addListener(function (port) { browser.runtime.onConnect.addListener(function (port) {
if (port.name == "devtools-page") { if (port.name == "devtools-page") {
// Listen to messages sent from the DevTools page // Listen to messages sent from the DevTools page
port.onMessage.addListener(function (message, sender, sendResponse) { port.onMessage.addListener(function (message, sender, sendResponse) {
if (message.name == "init") { if (message.name == "init") {
connections[message.tabId] = port; connections[message.tabId] = port;
wakatime.setTabsWithDevtoolsOpen(Object.keys(connections)); wakatime.setTabsWithDevtoolsOpen(Object.keys(connections));
wakatime.recordHeartbeat(); wakatime.recordHeartbeat();
} }
}); });
port.onDisconnect.addListener(function (port) { port.onDisconnect.addListener(function (port) {
var tabs = Object.keys(connections); var tabs = Object.keys(connections);
for (var i = 0, len = tabs.length; i < len; i ++) { for (var i = 0, len = tabs.length; i < len; i ++) {
if (connections[tabs[i]] == port) { if (connections[tabs[i]] == port) {
delete connections[tabs[i]]; delete connections[tabs[i]];
break; break;
} }
} }
wakatime.setTabsWithDevtoolsOpen(Object.keys(connections)); wakatime.setTabsWithDevtoolsOpen(Object.keys(connections));
wakatime.recordHeartbeat(); wakatime.recordHeartbeat();
}); });
} }
}); });

View File

@@ -1,50 +1,50 @@
/* global chrome */ /* global browser */
var config = require('../config'); var config = require('../config');
/** /**
* It changes the extension icon color. * It changes the extension icon color.
* Supported values are: 'red', 'white', 'gray' and ''. * Supported values are: 'red', 'white', 'gray' and ''.
* *
* @param color * @param color
*/ */
function changeExtensionIcon(color) { function changeExtensionIcon(color) {
color = color ? color : ''; color = color ? color : '';
var path = null; var path = null;
if (color !== '') { if (color !== '') {
color = '-' + color; color = '-' + color;
path = './graphics/wakatime-logo-38' + color + '.png'; path = './graphics/wakatime-logo-38' + color + '.png';
chrome.browserAction.setIcon({ browser.browserAction.setIcon({
path: path path: path
}); });
} }
if (color === '') { if (color === '') {
chrome.storage.sync.get({ browser.storage.sync.get({
theme: config.theme theme: config.theme
}, function (items) { }).then(function (items) {
if (items.theme == config.theme) { if (items.theme == config.theme) {
path = './graphics/wakatime-logo-38.png'; path = './graphics/wakatime-logo-38.png';
chrome.browserAction.setIcon({ browser.browserAction.setIcon({
path: path path: path
}); });
} }
else { else {
path = './graphics/wakatime-logo-38-white.png'; path = './graphics/wakatime-logo-38-white.png';
chrome.browserAction.setIcon({ browser.browserAction.setIcon({
path: path path: path
}); });
} }
}); });
} }
} }
module.exports = changeExtensionIcon; module.exports = changeExtensionIcon;

View File

@@ -1,42 +1,42 @@
var config = require('../config'); var config = require('../config');
// Helpers // Helpers
var changeExtensionIcon = require('./changeExtensionIcon'); var changeExtensionIcon = require('./changeExtensionIcon');
var changeExtensionTooltip = require('./changeExtensionTooltip'); var changeExtensionTooltip = require('./changeExtensionTooltip');
var in_array = require('./in_array'); var in_array = require('./in_array');
/** /**
* Sets the current state of the extension. * Sets the current state of the extension.
* *
* @param state * @param state
*/ */
function changeExtensionState(state) { function changeExtensionState(state) {
if (! in_array(state, config.states)) { if (! in_array(state, config.states)) {
throw new Error('Not a valid state!'); throw new Error('Not a valid state!');
} }
switch (state) { switch (state) {
case 'allGood': case 'allGood':
changeExtensionIcon(config.colors.allGood); changeExtensionIcon(config.colors.allGood);
changeExtensionTooltip(config.tooltips.allGood); changeExtensionTooltip(config.tooltips.allGood);
break; break;
case 'notLogging': case 'notLogging':
changeExtensionIcon(config.colors.notLogging); changeExtensionIcon(config.colors.notLogging);
changeExtensionTooltip(config.tooltips.notLogging); changeExtensionTooltip(config.tooltips.notLogging);
break; break;
case 'notSignedIn': case 'notSignedIn':
changeExtensionIcon(config.colors.notSignedIn); changeExtensionIcon(config.colors.notSignedIn);
changeExtensionTooltip(config.tooltips.notSignedIn); changeExtensionTooltip(config.tooltips.notSignedIn);
break; break;
case 'blacklisted': case 'blacklisted':
changeExtensionIcon(config.colors.notLogging); changeExtensionIcon(config.colors.notLogging);
changeExtensionTooltip(config.tooltips.blacklisted); changeExtensionTooltip(config.tooltips.blacklisted);
break; break;
case 'whitelisted': case 'whitelisted':
changeExtensionIcon(config.colors.notLogging); changeExtensionIcon(config.colors.notLogging);
changeExtensionTooltip(config.tooltips.whitelisted); changeExtensionTooltip(config.tooltips.whitelisted);
break; break;
} }
} }
module.exports = changeExtensionState; module.exports = changeExtensionState;

View File

@@ -1,22 +1,22 @@
/* global chrome */ /* global browser */
var config = require('../config'); var config = require('../config');
/** /**
* It changes the extension title * It changes the extension title
* *
* @param text * @param text
*/ */
function changeExtensionTooltip(text) { function changeExtensionTooltip(text) {
if (text === '') { if (text === '') {
text = config.name; text = config.name;
} }
else { else {
text = config.name + ' - ' + text; text = config.name + ' - ' + text;
} }
chrome.browserAction.setTitle({title: text}); browser.browserAction.setTitle({title: text});
} }
module.exports = changeExtensionTooltip; module.exports = changeExtensionTooltip;

View File

@@ -1,29 +1,29 @@
/** /**
* Creates an array from list using \n as delimiter * Creates an array from list using \n as delimiter
* and checks if any element in list is contained in the url. * and checks if any element in list is contained in the url.
* *
* @param url * @param url
* @param list * @param list
* @returns {boolean} * @returns {boolean}
*/ */
function contains(url, list) { function contains(url, list) {
var lines = list.split('\n'); var lines = list.split('\n');
for (var i = 0; i < lines.length; i ++) { for (var i = 0; i < lines.length; i ++) {
// Trim all lines from the list one by one // Trim all lines from the list one by one
var cleanLine = lines[i].trim(); var cleanLine = lines[i].trim();
// If by any chance one line in the list is empty, ignore it // If by any chance one line in the list is empty, ignore it
if(cleanLine === '') continue; if(cleanLine === '') continue;
// If url contains the current line return true // If url contains the current line return true
if (url.indexOf(cleanLine) > -1) { if (url.indexOf(cleanLine) > -1) {
return true; return true;
} }
} }
return false; return false;
} }
module.exports = contains; module.exports = contains;

View File

@@ -1,13 +1,13 @@
/** /**
* Returns domain from given URL. * Returns domain from given URL.
* *
* @param url * @param url
* @returns {string} * @returns {string}
*/ */
function getDomainFromUrl(url) { function getDomainFromUrl(url) {
var parts = url.split('/'); var parts = url.split('/');
return parts[0] + "//" + parts[2]; return parts[0] + "//" + parts[2];
} }
module.exports = getDomainFromUrl; module.exports = getDomainFromUrl;

View File

@@ -1,18 +1,18 @@
/** /**
* Returns boolean if needle is found in haystack or not. * Returns boolean if needle is found in haystack or not.
* *
* @param needle * @param needle
* @param haystack * @param haystack
* @returns {boolean} * @returns {boolean}
*/ */
function in_array(needle, haystack) { function in_array(needle, haystack) {
for (var i = 0; i < haystack.length; i ++) { for (var i = 0; i < haystack.length; i ++) {
if (needle == haystack[i]) { if (needle == haystack[i]) {
return true; return true;
} }
} }
return false; return false;
} }
module.exports = in_array; module.exports = in_array;

View File

@@ -1,16 +1,16 @@
/* global chrome */ /* global browser */
/* This is a fix for Bootstrap requiring jQuery */ /* This is a fix for Bootstrap requiring jQuery */
global.jQuery = require('jquery'); global.jQuery = require('jquery');
require('bootstrap'); require('bootstrap');
var React = require('react'); var React = require('react');
var ReactDOM = require('react-dom'); var ReactDOM = require('react-dom');
// React components // React components
var Options = require('./components/Options.jsx'); var Options = require('./components/Options.jsx');
ReactDOM.render( ReactDOM.render(
<Options />, <Options />,
document.getElementById('wakatime-options') document.getElementById('wakatime-options')
); );

View File

@@ -1,30 +1,30 @@
@import "bootstrap/bootstrap"; @import "bootstrap/bootstrap";
@import "font-awesome/font-awesome"; @import "font-awesome/font-awesome";
@import "bootswatch/paper/bootswatch"; @import "bootswatch/paper/bootswatch";
@import "bootswatch/paper/variables"; @import "bootswatch/paper/variables";
@import "variables"; @import "variables";
@import "partials/_animations"; @import "partials/_animations";
body { body {
min-width: 357px; min-width: 357px;
} }
a.navbar-brand { a.navbar-brand {
img { img {
margin-top: -12px; margin-top: -12px;
float: left; float: left;
margin-right: 7px; margin-right: 7px;
} }
} }
div.container { div.container {
margin-top: 20px; margin-top: 20px;
} }
canvas#icon { canvas#icon {
display: none; display: none;
} }
div#status { div#status {
display: none; display: none;
} }

View File

@@ -1,304 +1,304 @@
{ {
"always-semicolon": true, "always-semicolon": true,
"block-indent": 2, "block-indent": 2,
"color-case": "lower", "color-case": "lower",
"color-shorthand": true, "color-shorthand": true,
"element-case": "lower", "element-case": "lower",
"eof-newline": true, "eof-newline": true,
"leading-zero": false, "leading-zero": false,
"remove-empty-rulesets": true, "remove-empty-rulesets": true,
"space-after-colon": 1, "space-after-colon": 1,
"space-after-combinator": 1, "space-after-combinator": 1,
"space-before-selector-delimiter": 0, "space-before-selector-delimiter": 0,
"space-between-declarations": "\n", "space-between-declarations": "\n",
"space-after-opening-brace": "\n", "space-after-opening-brace": "\n",
"space-before-closing-brace": "\n", "space-before-closing-brace": "\n",
"space-before-colon": 0, "space-before-colon": 0,
"space-before-combinator": 1, "space-before-combinator": 1,
"space-before-opening-brace": 1, "space-before-opening-brace": 1,
"strip-spaces": true, "strip-spaces": true,
"unitless-zero": true, "unitless-zero": true,
"vendor-prefix-align": true, "vendor-prefix-align": true,
"sort-order": [ "sort-order": [
[ [
"position", "position",
"top", "top",
"right", "right",
"bottom", "bottom",
"left", "left",
"z-index", "z-index",
"display", "display",
"float", "float",
"width", "width",
"min-width", "min-width",
"max-width", "max-width",
"height", "height",
"min-height", "min-height",
"max-height", "max-height",
"-webkit-box-sizing", "-webkit-box-sizing",
"-moz-box-sizing", "-moz-box-sizing",
"box-sizing", "box-sizing",
"-webkit-appearance", "-webkit-appearance",
"padding", "padding",
"padding-top", "padding-top",
"padding-right", "padding-right",
"padding-bottom", "padding-bottom",
"padding-left", "padding-left",
"margin", "margin",
"margin-top", "margin-top",
"margin-right", "margin-right",
"margin-bottom", "margin-bottom",
"margin-left", "margin-left",
"overflow", "overflow",
"overflow-x", "overflow-x",
"overflow-y", "overflow-y",
"-webkit-overflow-scrolling", "-webkit-overflow-scrolling",
"-ms-overflow-x", "-ms-overflow-x",
"-ms-overflow-y", "-ms-overflow-y",
"-ms-overflow-style", "-ms-overflow-style",
"clip", "clip",
"clear", "clear",
"font", "font",
"font-family", "font-family",
"font-size", "font-size",
"font-style", "font-style",
"font-weight", "font-weight",
"font-variant", "font-variant",
"font-size-adjust", "font-size-adjust",
"font-stretch", "font-stretch",
"font-effect", "font-effect",
"font-emphasize", "font-emphasize",
"font-emphasize-position", "font-emphasize-position",
"font-emphasize-style", "font-emphasize-style",
"font-smooth", "font-smooth",
"-webkit-hyphens", "-webkit-hyphens",
"-moz-hyphens", "-moz-hyphens",
"hyphens", "hyphens",
"line-height", "line-height",
"color", "color",
"text-align", "text-align",
"-webkit-text-align-last", "-webkit-text-align-last",
"-moz-text-align-last", "-moz-text-align-last",
"-ms-text-align-last", "-ms-text-align-last",
"text-align-last", "text-align-last",
"text-emphasis", "text-emphasis",
"text-emphasis-color", "text-emphasis-color",
"text-emphasis-style", "text-emphasis-style",
"text-emphasis-position", "text-emphasis-position",
"text-decoration", "text-decoration",
"text-indent", "text-indent",
"text-justify", "text-justify",
"text-outline", "text-outline",
"-ms-text-overflow", "-ms-text-overflow",
"text-overflow", "text-overflow",
"text-overflow-ellipsis", "text-overflow-ellipsis",
"text-overflow-mode", "text-overflow-mode",
"text-shadow", "text-shadow",
"text-transform", "text-transform",
"text-wrap", "text-wrap",
"-webkit-text-size-adjust", "-webkit-text-size-adjust",
"-ms-text-size-adjust", "-ms-text-size-adjust",
"letter-spacing", "letter-spacing",
"-ms-word-break", "-ms-word-break",
"word-break", "word-break",
"word-spacing", "word-spacing",
"-ms-word-wrap", "-ms-word-wrap",
"word-wrap", "word-wrap",
"-moz-tab-size", "-moz-tab-size",
"-o-tab-size", "-o-tab-size",
"tab-size", "tab-size",
"white-space", "white-space",
"vertical-align", "vertical-align",
"list-style", "list-style",
"list-style-position", "list-style-position",
"list-style-type", "list-style-type",
"list-style-image", "list-style-image",
"pointer-events", "pointer-events",
"-ms-touch-action", "-ms-touch-action",
"touch-action", "touch-action",
"cursor", "cursor",
"visibility", "visibility",
"zoom", "zoom",
"flex-direction", "flex-direction",
"flex-order", "flex-order",
"flex-pack", "flex-pack",
"flex-align", "flex-align",
"table-layout", "table-layout",
"empty-cells", "empty-cells",
"caption-side", "caption-side",
"border-spacing", "border-spacing",
"border-collapse", "border-collapse",
"content", "content",
"quotes", "quotes",
"counter-reset", "counter-reset",
"counter-increment", "counter-increment",
"resize", "resize",
"-webkit-user-select", "-webkit-user-select",
"-moz-user-select", "-moz-user-select",
"-ms-user-select", "-ms-user-select",
"-o-user-select", "-o-user-select",
"user-select", "user-select",
"nav-index", "nav-index",
"nav-up", "nav-up",
"nav-right", "nav-right",
"nav-down", "nav-down",
"nav-left", "nav-left",
"background", "background",
"background-color", "background-color",
"background-image", "background-image",
"-ms-filter:\\'progid:DXImageTransform.Microsoft.gradient", "-ms-filter:\\'progid:DXImageTransform.Microsoft.gradient",
"filter:progid:DXImageTransform.Microsoft.gradient", "filter:progid:DXImageTransform.Microsoft.gradient",
"filter:progid:DXImageTransform.Microsoft.AlphaImageLoader", "filter:progid:DXImageTransform.Microsoft.AlphaImageLoader",
"filter", "filter",
"background-repeat", "background-repeat",
"background-attachment", "background-attachment",
"background-position", "background-position",
"background-position-x", "background-position-x",
"background-position-y", "background-position-y",
"-webkit-background-clip", "-webkit-background-clip",
"-moz-background-clip", "-moz-background-clip",
"background-clip", "background-clip",
"background-origin", "background-origin",
"-webkit-background-size", "-webkit-background-size",
"-moz-background-size", "-moz-background-size",
"-o-background-size", "-o-background-size",
"background-size", "background-size",
"border", "border",
"border-color", "border-color",
"border-style", "border-style",
"border-width", "border-width",
"border-top", "border-top",
"border-top-color", "border-top-color",
"border-top-style", "border-top-style",
"border-top-width", "border-top-width",
"border-right", "border-right",
"border-right-color", "border-right-color",
"border-right-style", "border-right-style",
"border-right-width", "border-right-width",
"border-bottom", "border-bottom",
"border-bottom-color", "border-bottom-color",
"border-bottom-style", "border-bottom-style",
"border-bottom-width", "border-bottom-width",
"border-left", "border-left",
"border-left-color", "border-left-color",
"border-left-style", "border-left-style",
"border-left-width", "border-left-width",
"border-radius", "border-radius",
"border-top-left-radius", "border-top-left-radius",
"border-top-right-radius", "border-top-right-radius",
"border-bottom-right-radius", "border-bottom-right-radius",
"border-bottom-left-radius", "border-bottom-left-radius",
"-webkit-border-image", "-webkit-border-image",
"-moz-border-image", "-moz-border-image",
"-o-border-image", "-o-border-image",
"border-image", "border-image",
"-webkit-border-image-source", "-webkit-border-image-source",
"-moz-border-image-source", "-moz-border-image-source",
"-o-border-image-source", "-o-border-image-source",
"border-image-source", "border-image-source",
"-webkit-border-image-slice", "-webkit-border-image-slice",
"-moz-border-image-slice", "-moz-border-image-slice",
"-o-border-image-slice", "-o-border-image-slice",
"border-image-slice", "border-image-slice",
"-webkit-border-image-width", "-webkit-border-image-width",
"-moz-border-image-width", "-moz-border-image-width",
"-o-border-image-width", "-o-border-image-width",
"border-image-width", "border-image-width",
"-webkit-border-image-outset", "-webkit-border-image-outset",
"-moz-border-image-outset", "-moz-border-image-outset",
"-o-border-image-outset", "-o-border-image-outset",
"border-image-outset", "border-image-outset",
"-webkit-border-image-repeat", "-webkit-border-image-repeat",
"-moz-border-image-repeat", "-moz-border-image-repeat",
"-o-border-image-repeat", "-o-border-image-repeat",
"border-image-repeat", "border-image-repeat",
"outline", "outline",
"outline-width", "outline-width",
"outline-style", "outline-style",
"outline-color", "outline-color",
"outline-offset", "outline-offset",
"-webkit-box-shadow", "-webkit-box-shadow",
"-moz-box-shadow", "-moz-box-shadow",
"box-shadow", "box-shadow",
"filter:progid:DXImageTransform.Microsoft.Alpha(Opacity", "filter:progid:DXImageTransform.Microsoft.Alpha(Opacity",
"-ms-filter:\\'progid:DXImageTransform.Microsoft.Alpha", "-ms-filter:\\'progid:DXImageTransform.Microsoft.Alpha",
"opacity", "opacity",
"-ms-interpolation-mode", "-ms-interpolation-mode",
"-webkit-transition", "-webkit-transition",
"-moz-transition", "-moz-transition",
"-ms-transition", "-ms-transition",
"-o-transition", "-o-transition",
"transition", "transition",
"-webkit-transition-delay", "-webkit-transition-delay",
"-moz-transition-delay", "-moz-transition-delay",
"-ms-transition-delay", "-ms-transition-delay",
"-o-transition-delay", "-o-transition-delay",
"transition-delay", "transition-delay",
"-webkit-transition-timing-function", "-webkit-transition-timing-function",
"-moz-transition-timing-function", "-moz-transition-timing-function",
"-ms-transition-timing-function", "-ms-transition-timing-function",
"-o-transition-timing-function", "-o-transition-timing-function",
"transition-timing-function", "transition-timing-function",
"-webkit-transition-duration", "-webkit-transition-duration",
"-moz-transition-duration", "-moz-transition-duration",
"-ms-transition-duration", "-ms-transition-duration",
"-o-transition-duration", "-o-transition-duration",
"transition-duration", "transition-duration",
"-webkit-transition-property", "-webkit-transition-property",
"-moz-transition-property", "-moz-transition-property",
"-ms-transition-property", "-ms-transition-property",
"-o-transition-property", "-o-transition-property",
"transition-property", "transition-property",
"-webkit-transform", "-webkit-transform",
"-moz-transform", "-moz-transform",
"-ms-transform", "-ms-transform",
"-o-transform", "-o-transform",
"transform", "transform",
"-webkit-transform-origin", "-webkit-transform-origin",
"-moz-transform-origin", "-moz-transform-origin",
"-ms-transform-origin", "-ms-transform-origin",
"-o-transform-origin", "-o-transform-origin",
"transform-origin", "transform-origin",
"-webkit-animation", "-webkit-animation",
"-moz-animation", "-moz-animation",
"-ms-animation", "-ms-animation",
"-o-animation", "-o-animation",
"animation", "animation",
"-webkit-animation-name", "-webkit-animation-name",
"-moz-animation-name", "-moz-animation-name",
"-ms-animation-name", "-ms-animation-name",
"-o-animation-name", "-o-animation-name",
"animation-name", "animation-name",
"-webkit-animation-duration", "-webkit-animation-duration",
"-moz-animation-duration", "-moz-animation-duration",
"-ms-animation-duration", "-ms-animation-duration",
"-o-animation-duration", "-o-animation-duration",
"animation-duration", "animation-duration",
"-webkit-animation-play-state", "-webkit-animation-play-state",
"-moz-animation-play-state", "-moz-animation-play-state",
"-ms-animation-play-state", "-ms-animation-play-state",
"-o-animation-play-state", "-o-animation-play-state",
"animation-play-state", "animation-play-state",
"-webkit-animation-timing-function", "-webkit-animation-timing-function",
"-moz-animation-timing-function", "-moz-animation-timing-function",
"-ms-animation-timing-function", "-ms-animation-timing-function",
"-o-animation-timing-function", "-o-animation-timing-function",
"animation-timing-function", "animation-timing-function",
"-webkit-animation-delay", "-webkit-animation-delay",
"-moz-animation-delay", "-moz-animation-delay",
"-ms-animation-delay", "-ms-animation-delay",
"-o-animation-delay", "-o-animation-delay",
"animation-delay", "animation-delay",
"-webkit-animation-iteration-count", "-webkit-animation-iteration-count",
"-moz-animation-iteration-count", "-moz-animation-iteration-count",
"-ms-animation-iteration-count", "-ms-animation-iteration-count",
"-o-animation-iteration-count", "-o-animation-iteration-count",
"animation-iteration-count", "animation-iteration-count",
"-webkit-animation-direction", "-webkit-animation-direction",
"-moz-animation-direction", "-moz-animation-direction",
"-ms-animation-direction", "-ms-animation-direction",
"-o-animation-direction", "-o-animation-direction",
"animation-direction" "animation-direction"
] ]
] ]
} }

View File

@@ -1,19 +1,19 @@
{ {
"adjoining-classes": false, "adjoining-classes": false,
"box-sizing": false, "box-sizing": false,
"box-model": false, "box-model": false,
"compatible-vendor-prefixes": false, "compatible-vendor-prefixes": false,
"floats": false, "floats": false,
"font-sizes": false, "font-sizes": false,
"gradients": false, "gradients": false,
"important": false, "important": false,
"known-properties": false, "known-properties": false,
"outline-none": false, "outline-none": false,
"qualified-headings": false, "qualified-headings": false,
"regex-selectors": false, "regex-selectors": false,
"shorthand": false, "shorthand": false,
"text-indent": false, "text-indent": false,
"unique-headings": false, "unique-headings": false,
"universal-selector": false, "universal-selector": false,
"unqualified-attributes": false "unqualified-attributes": false
} }

View File

@@ -1,6 +1,6 @@
/*! /*!
* Bootstrap v3.3.6 (http://getbootstrap.com) * Bootstrap v3.3.7 (http://getbootstrap.com)
* Copyright 2011-2015 Twitter, Inc. * Copyright 2011-2016 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/ */

View File

@@ -59,7 +59,7 @@
.border-right-radius(0); .border-right-radius(0);
} }
} }
// Need .dropdown-toggle since :last-child doesn't apply given a .dropdown-menu immediately after it // Need .dropdown-toggle since :last-child doesn't apply, given that a .dropdown-menu is used immediately after it
.btn-group > .btn:last-child:not(:first-child), .btn-group > .btn:last-child:not(:first-child),
.btn-group > .dropdown-toggle:not(:first-child) { .btn-group > .dropdown-toggle:not(:first-child) {
.border-left-radius(0); .border-left-radius(0);

View File

@@ -181,7 +181,7 @@ input[type="search"] {
// set a pixel line-height that matches the given height of the input, but only // set a pixel line-height that matches the given height of the input, but only
// for Safari. See https://bugs.webkit.org/show_bug.cgi?id=139848 // for Safari. See https://bugs.webkit.org/show_bug.cgi?id=139848
// //
// Note that as of 8.3, iOS doesn't support `datetime` or `week`. // Note that as of 9.3, iOS doesn't support `week`.
@media screen and (-webkit-min-device-pixel-ratio: 0) { @media screen and (-webkit-min-device-pixel-ratio: 0) {
input[type="date"], input[type="date"],

View File

@@ -29,7 +29,7 @@
width: 100%; width: 100%;
margin-bottom: 0; margin-bottom: 0;
&:focus { &:focus {
z-index: 3; z-index: 3;
} }

View File

@@ -1,9 +1,9 @@
// WebKit-style focus // WebKit-style focus
.tab-focus() { .tab-focus() {
// Default // WebKit-specific. Other browsers will keep their default outline style.
outline: thin dotted; // (Initially tried to also force default via `outline: initial`,
// WebKit // but that seems to erroneously remove the outline in Firefox altogether.)
outline: 5px auto -webkit-focus-ring-color; outline: 5px auto -webkit-focus-ring-color;
outline-offset: -2px; outline-offset: -2px;
} }

View File

@@ -214,7 +214,7 @@
} }
// Collapsable panels (aka, accordion) // Collapsible panels (aka, accordion)
// //
// Wrap a series of panels in `.panel-group` to turn them into an accordion with // Wrap a series of panels in `.panel-group` to turn them into an accordion with
// the help of our collapse JavaScript plugin. // the help of our collapse JavaScript plugin.

View File

@@ -120,7 +120,7 @@ hr {
// Only display content to screen readers // Only display content to screen readers
// //
// See: http://a11yproject.com/posts/how-to-hide-content/ // See: http://a11yproject.com/posts/how-to-hide-content
.sr-only { .sr-only {
position: absolute; position: absolute;

View File

@@ -1,6 +1,6 @@
/*! /*!
* Bootstrap v3.3.6 (http://getbootstrap.com) * Bootstrap v3.3.7 (http://getbootstrap.com)
* Copyright 2011-2015 Twitter, Inc. * Copyright 2011-2016 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/ */

View File

@@ -111,7 +111,7 @@
//** Global background color for active items (e.g., navs or dropdowns). //** Global background color for active items (e.g., navs or dropdowns).
@component-active-bg: @brand-primary; @component-active-bg: @brand-primary;
//** Width of the `border` for generating carets that indicator dropdowns. //** Width of the `border` for generating carets that indicate dropdowns.
@caret-width-base: 4px; @caret-width-base: 4px;
//** Carets increase slightly in size for larger components. //** Carets increase slightly in size for larger components.
@caret-width-large: 5px; @caret-width-large: 5px;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,34 +1,34 @@
// Animated Icons // Animated Icons
// -------------------------- // --------------------------
.@{fa-css-prefix}-spin { .@{fa-css-prefix}-spin {
-webkit-animation: fa-spin 2s infinite linear; -webkit-animation: fa-spin 2s infinite linear;
animation: fa-spin 2s infinite linear; animation: fa-spin 2s infinite linear;
} }
.@{fa-css-prefix}-pulse { .@{fa-css-prefix}-pulse {
-webkit-animation: fa-spin 1s infinite steps(8); -webkit-animation: fa-spin 1s infinite steps(8);
animation: fa-spin 1s infinite steps(8); animation: fa-spin 1s infinite steps(8);
} }
@-webkit-keyframes fa-spin { @-webkit-keyframes fa-spin {
0% { 0% {
-webkit-transform: rotate(0deg); -webkit-transform: rotate(0deg);
transform: rotate(0deg); transform: rotate(0deg);
} }
100% { 100% {
-webkit-transform: rotate(359deg); -webkit-transform: rotate(359deg);
transform: rotate(359deg); transform: rotate(359deg);
} }
} }
@keyframes fa-spin { @keyframes fa-spin {
0% { 0% {
-webkit-transform: rotate(0deg); -webkit-transform: rotate(0deg);
transform: rotate(0deg); transform: rotate(0deg);
} }
100% { 100% {
-webkit-transform: rotate(359deg); -webkit-transform: rotate(359deg);
transform: rotate(359deg); transform: rotate(359deg);
} }
} }

View File

@@ -1,25 +1,25 @@
// Bordered & Pulled // Bordered & Pulled
// ------------------------- // -------------------------
.@{fa-css-prefix}-border { .@{fa-css-prefix}-border {
padding: .2em .25em .15em; padding: .2em .25em .15em;
border: solid .08em @fa-border-color; border: solid .08em @fa-border-color;
border-radius: .1em; border-radius: .1em;
} }
.@{fa-css-prefix}-pull-left { float: left; } .@{fa-css-prefix}-pull-left { float: left; }
.@{fa-css-prefix}-pull-right { float: right; } .@{fa-css-prefix}-pull-right { float: right; }
.@{fa-css-prefix} { .@{fa-css-prefix} {
&.@{fa-css-prefix}-pull-left { margin-right: .3em; } &.@{fa-css-prefix}-pull-left { margin-right: .3em; }
&.@{fa-css-prefix}-pull-right { margin-left: .3em; } &.@{fa-css-prefix}-pull-right { margin-left: .3em; }
} }
/* Deprecated as of 4.4.0 */ /* Deprecated as of 4.4.0 */
.pull-right { float: right; } .pull-right { float: right; }
.pull-left { float: left; } .pull-left { float: left; }
.@{fa-css-prefix} { .@{fa-css-prefix} {
&.pull-left { margin-right: .3em; } &.pull-left { margin-right: .3em; }
&.pull-right { margin-left: .3em; } &.pull-right { margin-left: .3em; }
} }

View File

@@ -1,12 +1,12 @@
// Base Class Definition // Base Class Definition
// ------------------------- // -------------------------
.@{fa-css-prefix} { .@{fa-css-prefix} {
display: inline-block; display: inline-block;
font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration
font-size: inherit; // can't have font-size inherit on line above, so need to override font-size: inherit; // can't have font-size inherit on line above, so need to override
text-rendering: auto; // optimizelegibility throws things off #1094 text-rendering: auto; // optimizelegibility throws things off #1094
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }

View File

@@ -1,6 +1,6 @@
// Fixed Width Icons // Fixed Width Icons
// ------------------------- // -------------------------
.@{fa-css-prefix}-fw { .@{fa-css-prefix}-fw {
width: (18em / 14); width: (18em / 14);
text-align: center; text-align: center;
} }

View File

@@ -1,18 +1,18 @@
/*! /*!
* Font Awesome 4.6.3 by @davegandy - http://fontawesome.io - @fontawesome * Font Awesome 4.6.3 by @davegandy - http://fontawesome.io - @fontawesome
* License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
*/ */
@import "variables.less"; @import "variables.less";
@import "mixins.less"; @import "mixins.less";
@import "path.less"; @import "path.less";
@import "core.less"; @import "core.less";
@import "larger.less"; @import "larger.less";
@import "fixed-width.less"; @import "fixed-width.less";
@import "list.less"; @import "list.less";
@import "bordered-pulled.less"; @import "bordered-pulled.less";
@import "animated.less"; @import "animated.less";
@import "rotated-flipped.less"; @import "rotated-flipped.less";
@import "stacked.less"; @import "stacked.less";
@import "icons.less"; @import "icons.less";
@import "screen-reader.less"; @import "screen-reader.less";

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,13 @@
// Icon Sizes // Icon Sizes
// ------------------------- // -------------------------
/* makes the font 33% larger relative to the icon container */ /* makes the font 33% larger relative to the icon container */
.@{fa-css-prefix}-lg { .@{fa-css-prefix}-lg {
font-size: (4em / 3); font-size: (4em / 3);
line-height: (3em / 4); line-height: (3em / 4);
vertical-align: -15%; vertical-align: -15%;
} }
.@{fa-css-prefix}-2x { font-size: 2em; } .@{fa-css-prefix}-2x { font-size: 2em; }
.@{fa-css-prefix}-3x { font-size: 3em; } .@{fa-css-prefix}-3x { font-size: 3em; }
.@{fa-css-prefix}-4x { font-size: 4em; } .@{fa-css-prefix}-4x { font-size: 4em; }
.@{fa-css-prefix}-5x { font-size: 5em; } .@{fa-css-prefix}-5x { font-size: 5em; }

View File

@@ -1,19 +1,19 @@
// List Icons // List Icons
// ------------------------- // -------------------------
.@{fa-css-prefix}-ul { .@{fa-css-prefix}-ul {
padding-left: 0; padding-left: 0;
margin-left: @fa-li-width; margin-left: @fa-li-width;
list-style-type: none; list-style-type: none;
> li { position: relative; } > li { position: relative; }
} }
.@{fa-css-prefix}-li { .@{fa-css-prefix}-li {
position: absolute; position: absolute;
left: -@fa-li-width; left: -@fa-li-width;
width: @fa-li-width; width: @fa-li-width;
top: (2em / 14); top: (2em / 14);
text-align: center; text-align: center;
&.@{fa-css-prefix}-lg { &.@{fa-css-prefix}-lg {
left: (-@fa-li-width + (4em / 14)); left: (-@fa-li-width + (4em / 14));
} }
} }

View File

@@ -1,60 +1,60 @@
// Mixins // Mixins
// -------------------------- // --------------------------
.fa-icon() { .fa-icon() {
display: inline-block; display: inline-block;
font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration
font-size: inherit; // can't have font-size inherit on line above, so need to override font-size: inherit; // can't have font-size inherit on line above, so need to override
text-rendering: auto; // optimizelegibility throws things off #1094 text-rendering: auto; // optimizelegibility throws things off #1094
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.fa-icon-rotate(@degrees, @rotation) { .fa-icon-rotate(@degrees, @rotation) {
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation})"; -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation})";
-webkit-transform: rotate(@degrees); -webkit-transform: rotate(@degrees);
-ms-transform: rotate(@degrees); -ms-transform: rotate(@degrees);
transform: rotate(@degrees); transform: rotate(@degrees);
} }
.fa-icon-flip(@horiz, @vert, @rotation) { .fa-icon-flip(@horiz, @vert, @rotation) {
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation}, mirror=1)"; -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation}, mirror=1)";
-webkit-transform: scale(@horiz, @vert); -webkit-transform: scale(@horiz, @vert);
-ms-transform: scale(@horiz, @vert); -ms-transform: scale(@horiz, @vert);
transform: scale(@horiz, @vert); transform: scale(@horiz, @vert);
} }
// Only display content to screen readers. A la Bootstrap 4. // Only display content to screen readers. A la Bootstrap 4.
// //
// See: http://a11yproject.com/posts/how-to-hide-content/ // See: http://a11yproject.com/posts/how-to-hide-content/
.sr-only() { .sr-only() {
position: absolute; position: absolute;
width: 1px; width: 1px;
height: 1px; height: 1px;
padding: 0; padding: 0;
margin: -1px; margin: -1px;
overflow: hidden; overflow: hidden;
clip: rect(0,0,0,0); clip: rect(0,0,0,0);
border: 0; border: 0;
} }
// Use in conjunction with .sr-only to only display content when it's focused. // Use in conjunction with .sr-only to only display content when it's focused.
// //
// Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 // Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1
// //
// Credit: HTML5 Boilerplate // Credit: HTML5 Boilerplate
.sr-only-focusable() { .sr-only-focusable() {
&:active, &:active,
&:focus { &:focus {
position: static; position: static;
width: auto; width: auto;
height: auto; height: auto;
margin: 0; margin: 0;
overflow: visible; overflow: visible;
clip: auto; clip: auto;
} }
} }

View File

@@ -1,15 +1,15 @@
/* FONT PATH /* FONT PATH
* -------------------------- */ * -------------------------- */
@font-face { @font-face {
font-family: 'FontAwesome'; font-family: 'FontAwesome';
src: url('@{fa-font-path}/fontawesome-webfont.eot?v=@{fa-version}'); src: url('@{fa-font-path}/fontawesome-webfont.eot?v=@{fa-version}');
src: url('@{fa-font-path}/fontawesome-webfont.eot?#iefix&v=@{fa-version}') format('embedded-opentype'), src: url('@{fa-font-path}/fontawesome-webfont.eot?#iefix&v=@{fa-version}') format('embedded-opentype'),
url('@{fa-font-path}/fontawesome-webfont.woff2?v=@{fa-version}') format('woff2'), url('@{fa-font-path}/fontawesome-webfont.woff2?v=@{fa-version}') format('woff2'),
url('@{fa-font-path}/fontawesome-webfont.woff?v=@{fa-version}') format('woff'), url('@{fa-font-path}/fontawesome-webfont.woff?v=@{fa-version}') format('woff'),
url('@{fa-font-path}/fontawesome-webfont.ttf?v=@{fa-version}') format('truetype'), url('@{fa-font-path}/fontawesome-webfont.ttf?v=@{fa-version}') format('truetype'),
url('@{fa-font-path}/fontawesome-webfont.svg?v=@{fa-version}#fontawesomeregular') format('svg'); url('@{fa-font-path}/fontawesome-webfont.svg?v=@{fa-version}#fontawesomeregular') format('svg');
// src: url('@{fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts // src: url('@{fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }

View File

@@ -1,20 +1,20 @@
// Rotated & Flipped Icons // Rotated & Flipped Icons
// ------------------------- // -------------------------
.@{fa-css-prefix}-rotate-90 { .fa-icon-rotate(90deg, 1); } .@{fa-css-prefix}-rotate-90 { .fa-icon-rotate(90deg, 1); }
.@{fa-css-prefix}-rotate-180 { .fa-icon-rotate(180deg, 2); } .@{fa-css-prefix}-rotate-180 { .fa-icon-rotate(180deg, 2); }
.@{fa-css-prefix}-rotate-270 { .fa-icon-rotate(270deg, 3); } .@{fa-css-prefix}-rotate-270 { .fa-icon-rotate(270deg, 3); }
.@{fa-css-prefix}-flip-horizontal { .fa-icon-flip(-1, 1, 0); } .@{fa-css-prefix}-flip-horizontal { .fa-icon-flip(-1, 1, 0); }
.@{fa-css-prefix}-flip-vertical { .fa-icon-flip(1, -1, 2); } .@{fa-css-prefix}-flip-vertical { .fa-icon-flip(1, -1, 2); }
// Hook for IE8-9 // Hook for IE8-9
// ------------------------- // -------------------------
:root .@{fa-css-prefix}-rotate-90, :root .@{fa-css-prefix}-rotate-90,
:root .@{fa-css-prefix}-rotate-180, :root .@{fa-css-prefix}-rotate-180,
:root .@{fa-css-prefix}-rotate-270, :root .@{fa-css-prefix}-rotate-270,
:root .@{fa-css-prefix}-flip-horizontal, :root .@{fa-css-prefix}-flip-horizontal,
:root .@{fa-css-prefix}-flip-vertical { :root .@{fa-css-prefix}-flip-vertical {
filter: none; filter: none;
} }

View File

@@ -1,5 +1,5 @@
// Screen Readers // Screen Readers
// ------------------------- // -------------------------
.sr-only { .sr-only(); } .sr-only { .sr-only(); }
.sr-only-focusable { .sr-only-focusable(); } .sr-only-focusable { .sr-only-focusable(); }

View File

@@ -1,20 +1,20 @@
// Stacked Icons // Stacked Icons
// ------------------------- // -------------------------
.@{fa-css-prefix}-stack { .@{fa-css-prefix}-stack {
position: relative; position: relative;
display: inline-block; display: inline-block;
width: 2em; width: 2em;
height: 2em; height: 2em;
line-height: 2em; line-height: 2em;
vertical-align: middle; vertical-align: middle;
} }
.@{fa-css-prefix}-stack-1x, .@{fa-css-prefix}-stack-2x { .@{fa-css-prefix}-stack-1x, .@{fa-css-prefix}-stack-2x {
position: absolute; position: absolute;
left: 0; left: 0;
width: 100%; width: 100%;
text-align: center; text-align: center;
} }
.@{fa-css-prefix}-stack-1x { line-height: inherit; } .@{fa-css-prefix}-stack-1x { line-height: inherit; }
.@{fa-css-prefix}-stack-2x { font-size: 2em; } .@{fa-css-prefix}-stack-2x { font-size: 2em; }
.@{fa-css-prefix}-inverse { color: @fa-inverse; } .@{fa-css-prefix}-inverse { color: @fa-inverse; }

File diff suppressed because it is too large Load Diff

View File

@@ -1,17 +1,17 @@
.alert-enter { .alert-enter {
opacity: 0.01; opacity: 0.01;
transition: opacity 1s ease-in; transition: opacity 1s ease-in;
} }
.alert-enter.alert-enter-active { .alert-enter.alert-enter-active {
opacity: 1; opacity: 1;
} }
.alert-leave { .alert-leave {
opacity: 1; opacity: 1;
transition: opacity 1s ease-in; transition: opacity 1s ease-in;
} }
.alert-leave.alert-leave-active { .alert-leave.alert-leave-active {
opacity: 0.01; opacity: 0.01;
} }

View File

@@ -1,14 +1,14 @@
::-webkit-scrollbar { ::-webkit-scrollbar {
height: 12px; height: 12px;
width: 12px; width: 12px;
background: @body-bg; background: @body-bg;
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: @brand-primary; background: @brand-primary;
-webkit-border-radius: 0; -webkit-border-radius: 0;
} }
::-webkit-scrollbar-corner { ::-webkit-scrollbar-corner {
background: #000; background: #000;
} }

View File

@@ -1,2 +1,2 @@
@navbar-margin-bottom: 0px; @navbar-margin-bottom: 0px;
@navbar-border-radius: 0px; @navbar-border-radius: 0px;

View File

@@ -1,14 +1,14 @@
{ {
"name": "WakaTime", "name": "WakaTime",
"ignore": [ "ignore": [
"**/.*", "**/.*",
"node_modules", "node_modules",
"bower_components", "bower_components",
"test", "test",
"tests" "tests"
], ],
"dependencies": { "dependencies": {
"font-awesome": "~4.6.3", "font-awesome": "~4.6.3",
"bootstrap": "~3.3.4" "bootstrap": "~3.3.4"
} }
} }

View File

@@ -1,2 +1,2 @@
<!DOCTYPE html> <!DOCTYPE html>
<script src="public/js/devtools.js"></script> <script src="public/js/devtools.js"></script>

View File

@@ -1,43 +1,43 @@
var del = require('del'); var del = require('del');
var gulp = require('gulp'); var gulp = require('gulp');
var elixir = require('laravel-elixir'); var elixir = require('laravel-elixir');
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Pre-defined Gulp Tasks | Pre-defined Gulp Tasks
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| |
| Tasks outside the scope of Elixir can be predefined before setting it up. | Tasks outside the scope of Elixir can be predefined before setting it up.
| |
*/ */
gulp.task('postinstall', function (cb) { gulp.task('postinstall', function (cb) {
// .pem files cause Chrome to show a bunch of warnings // .pem files cause Chrome to show a bunch of warnings
//so we remove them on postinstall //so we remove them on postinstall
del('node_modules/**/*.pem', cb); del('node_modules/**/*.pem', cb);
}); });
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Elixir Asset Management | Elixir Asset Management
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| |
| Elixir provides a clean, fluent API for defining some basic Gulp tasks | Elixir provides a clean, fluent API for defining some basic Gulp tasks
| for your Laravel application. By default, we are compiling the Less | for your Laravel application. By default, we are compiling the Less
| file for our application, as well as publishing vendor resources. | file for our application, as well as publishing vendor resources.
| |
*/ */
elixir.config.assetsPath = 'assets/'; elixir.config.assetsPath = 'assets/';
elixir(function (mix) { elixir(function (mix) {
mix.copy('vendor/bower_components/bootstrap/less', 'assets/less/bootstrap'); mix.copy('vendor/bower_components/bootstrap/less', 'assets/less/bootstrap');
mix.copy('vendor/bower_components/bootstrap/fonts', 'public/fonts'); mix.copy('vendor/bower_components/bootstrap/fonts', 'public/fonts');
mix.copy('vendor/bower_components/font-awesome/less', 'assets/less/font-awesome'); mix.copy('vendor/bower_components/font-awesome/less', 'assets/less/font-awesome');
mix.copy('vendor/bower_components/font-awesome/fonts', 'public/fonts'); mix.copy('vendor/bower_components/font-awesome/fonts', 'public/fonts');
mix.less('app.less'); mix.less('app.less');
mix.browserify('app.jsx', 'public/js/app.js', 'assets/js'); mix.browserify('app.jsx', 'public/js/app.js', 'assets/js');
mix.browserify('events.js', 'public/js/events.js', 'assets/js'); mix.browserify('events.js', 'public/js/events.js', 'assets/js');
mix.browserify('options.jsx', 'public/js/options.js', 'assets/js'); mix.browserify('options.jsx', 'public/js/options.js', 'assets/js');
mix.browserify('devtools.js', 'public/js/devtools.js', 'assets/js'); mix.browserify('devtools.js', 'public/js/devtools.js', 'assets/js');
}); });

View File

@@ -1,39 +1,44 @@
{ {
"manifest_version": 2, "manifest_version": 2,
"name": "WakaTime", "name": "WakaTime",
"version": "1.0.2", "version": "1.0.2",
"description": "Automatic time tracking for Chrome.", "description": "Automatic time tracking for Chrome.",
"homepage_url": "https://wakatime.com", "homepage_url": "https://wakatime.com",
"devtools_page": "devtools.html", "devtools_page": "devtools.html",
"icons": { "icons": {
"16": "graphics/wakatime-logo-16.png", "16": "graphics/wakatime-logo-16.png",
"48": "graphics/wakatime-logo-48.png", "48": "graphics/wakatime-logo-48.png",
"128": "graphics/wakatime-logo-128.png" "128": "graphics/wakatime-logo-128.png"
}, },
"permissions": [ "permissions": [
"https://api.wakatime.com/*", "https://api.wakatime.com/*",
"https://wakatime.com/*", "https://wakatime.com/*",
"alarms", "alarms",
"tabs", "tabs",
"storage", "storage",
"idle" "idle"
], ],
"background": { "background": {
"scripts": [ "scripts": [
"public/js/events.js" "public/js/events.js"
], ],
"persistent": false "persistent": false
}, },
"browser_action": { "browser_action": {
"default_icon": { "default_icon": {
"19": "graphics/wakatime-logo-19.png", "19": "graphics/wakatime-logo-19.png",
"38": "graphics/wakatime-logo-38.png" "38": "graphics/wakatime-logo-38.png"
}, },
"default_title": "WakaTime", "default_title": "WakaTime",
"default_popup": "popup.html" "default_popup": "popup.html"
}, },
"options_ui": { "options_ui": {
"page": "options.html", "page": "options.html",
"chrome_style": false "chrome_style": false
} },
} "applicaitons": {
"gecko": {
"id": "addon@wakatime.com"
}
}
}

View File

@@ -1,17 +1,17 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>WakaTime options</title> <title>WakaTime options</title>
<link href="public/css/app.css" rel="stylesheet"> <link href="public/css/app.css" rel="stylesheet">
</head> </head>
<body> <body>
<div id="wakatime-options"></div> <div id="wakatime-options"></div>
<script src="public/js/options.js"></script> <script src="public/js/options.js"></script>
</body> </body>
</html> </html>

View File

@@ -1,67 +1,67 @@
{ {
"scripts": { "scripts": {
"test": "jest --verbose --coverage && mocha --compilers js:mocha-traceur tests/**/*.spec.js", "test": "jest --verbose --coverage && mocha --compilers js:mocha-traceur tests/**/*.spec.js",
"test-react": "jest --verbose --coverage", "test-react": "jest --verbose --coverage",
"test-js": "node_modules/.bin/phantomjs tests/run.js", "test-js": "node_modules/.bin/phantomjs tests/run.js",
"start": "npm install && bower install && gulp", "start": "npm install && bower install && gulp",
"gulp": "gulp", "gulp": "gulp",
"watch": "gulp watch", "watch": "gulp watch",
"lint": "jsxhint --jsx-only .", "lint": "jsxhint --jsx-only .",
"postinstall": "gulp postinstall", "postinstall": "gulp postinstall",
"validate": "npm ls" "validate": "npm ls"
}, },
"pre-commit": [ "pre-commit": [
"lint" "lint"
], ],
"jest": { "jest": {
"testFileExtensions": [ "testFileExtensions": [
"jest.js" "jest.js"
], ],
"scriptPreprocessor": "<rootDir>/node_modules/babel-jest", "scriptPreprocessor": "<rootDir>/node_modules/babel-jest",
"testDirectoryName": "tests", "testDirectoryName": "tests",
"unmockedModulePathPatterns": [ "unmockedModulePathPatterns": [
"<rootDir>/node_modules/react" "<rootDir>/node_modules/react"
] ]
}, },
"private": true, "private": true,
"devDependencies": { "devDependencies": {
"babel-jest": "^13.0.0", "babel-jest": "^13.0.0",
"bower": "^1.7.9", "bower": "^1.7.9",
"chai": "^3.5.0", "chai": "^3.5.0",
"del": "^2.2.1", "del": "^2.2.1",
"gulp": "^3.9.1", "gulp": "^3.9.1",
"jest-cli": "^13.0.0", "jest-cli": "^13.0.0",
"jshint": "^2.9.2", "jshint": "^2.9.2",
"jsxhint": "^0.15.1", "jsxhint": "^0.15.1",
"laravel-elixir": "^5.0.0", "laravel-elixir": "^5.0.0",
"mocha": "^2.5.3", "mocha": "^2.5.3",
"mocha-sinon": "^1.1.5", "mocha-sinon": "^1.1.5",
"mocha-traceur": "^2.1.0", "mocha-traceur": "^2.1.0",
"precommit-hook": "^3.0.0", "precommit-hook": "^3.0.0",
"sinon": "^1.17.4", "sinon": "^1.17.4",
"sinon-chai": "^2.8.0", "sinon-chai": "^2.8.0",
"sinon-chrome": "^1.1.2", "sinon-chrome": "^1.1.2",
"traceur": "^0.0.111" "traceur": "^0.0.111"
}, },
"dependencies": { "dependencies": {
"bootstrap": "^3.3.6", "bootstrap": "^3.3.6",
"classnames": "^2.2.5", "classnames": "^2.2.5",
"jquery": "^3.0.0", "jquery": "^3.0.0",
"moment": "^2.13.0", "moment": "^2.13.0",
"react": "^15.1.0", "react": "^15.1.0",
"react-addons-css-transition-group": "^15.1.0", "react-addons-css-transition-group": "^15.1.0",
"react-dom": "^15.1.0" "react-dom": "^15.1.0"
}, },
"jshintConfig": { "jshintConfig": {
"asi": false, "asi": false,
"browser": true, "browser": true,
"curly": false, "curly": false,
"expr": true, "expr": true,
"indent": 4, "indent": 4,
"loopfunc": true, "loopfunc": true,
"node": true, "node": true,
"trailing": true, "trailing": true,
"undef": true, "undef": true,
"white": true "white": true
} }
} }

View File

@@ -1,17 +1,17 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>WakaTime</title> <title>WakaTime</title>
<link href="public/css/app.css" rel="stylesheet"> <link href="public/css/app.css" rel="stylesheet">
</head> </head>
<body> <body>
<div id="wakatime"></div> <div id="wakatime"></div>
<script src="public/js/app.js"></script> <script src="public/js/app.js"></script>
</body> </body>
</html> </html>

View File

@@ -1,40 +1,40 @@
var fs = require('fs'); var fs = require('fs');
var page; var page;
var beforeLoadFn; var beforeLoadFn;
beforeEach(function() { beforeEach(function() {
page = require('webpage').create(); page = require('webpage').create();
page.onConsoleMessage = function(msg) { console.log(msg); }; page.onConsoleMessage = function(msg) { console.log(msg); };
page.onError = function(msg, trace) { page.onError = function(msg, trace) {
var msgStack = [msg]; var msgStack = [msg];
if (trace && trace.length) { if (trace && trace.length) {
msgStack.push('TRACE:'); msgStack.push('TRACE:');
trace.forEach(function(t) { trace.forEach(function(t) {
msgStack.push(' -> ' + t.file + ': ' + t.line + (t.function ? ' (in function "' + t.function +'")' : '')); msgStack.push(' -> ' + t.file + ': ' + t.line + (t.function ? ' (in function "' + t.function +'")' : ''));
}); });
} }
// we need try..catch here as mocha throws error that catched by phantom.onError // we need try..catch here as mocha throws error that catched by phantom.onError
try { try {
mocha.throwError(msgStack.join('\n')); mocha.throwError(msgStack.join('\n'));
} catch(e) { } } catch(e) { }
}; };
page.onInitialized = function() { page.onInitialized = function() {
page.injectJs(node_modules + 'chai/chai.js'); page.injectJs(node_modules + 'chai/chai.js');
page.injectJs(node_modules + 'sinon/pkg/sinon.js'); page.injectJs(node_modules + 'sinon/pkg/sinon.js');
page.injectJs(node_modules + 'sinon-chrome/chrome.js'); page.injectJs(node_modules + 'sinon-chrome/chrome.js');
page.injectJs(node_modules + 'sinon-chrome/src/phantom-tweaks.js'); page.injectJs(node_modules + 'sinon-chrome/src/phantom-tweaks.js');
page.injectJs(node_modules + 'require-stub/index.js'); page.injectJs(node_modules + 'require-stub/index.js');
// call additional function defined in tests // call additional function defined in tests
if (beforeLoadFn) { if (beforeLoadFn) {
beforeLoadFn(); beforeLoadFn();
} }
}; };
}); });
afterEach(function() { afterEach(function() {
page.close(); page.close();
beforeLoadFn = null; beforeLoadFn = null;
}); });

View File

@@ -1,17 +1,17 @@
jest.dontMock('../../assets/js/components/Alert.jsx'); jest.dontMock('../../assets/js/components/Alert.jsx');
describe('Alert', function() { describe('Alert', function() {
var React, Alert, TestUtils, Component; var React, Alert, TestUtils, Component;
beforeEach(function() { beforeEach(function() {
// Setup our tools // Setup our tools
React = require('react/addons'); React = require('react/addons');
Alert = require('../../assets/js/components/Alert.jsx'); Alert = require('../../assets/js/components/Alert.jsx');
TestUtils = React.addons.TestUtils; TestUtils = React.addons.TestUtils;
// Create the React component here using TestUtils and store into Component // Create the React component here using TestUtils and store into Component
}); });
it('should work', function() { it('should work', function() {
expect(2 + 2).toEqual(4); expect(2 + 2).toEqual(4);
}); });
}); });

View File

@@ -1,17 +1,17 @@
jest.dontMock('../../assets/js/components/MainList.jsx'); jest.dontMock('../../assets/js/components/MainList.jsx');
describe('MainList', function() { describe('MainList', function() {
var React, MainList, TestUtils, Component; var React, MainList, TestUtils, Component;
beforeEach(function() { beforeEach(function() {
// Setup our tools // Setup our tools
React = require('react/addons'); React = require('react/addons');
MainList = require('../../assets/js/components/MainList.jsx'); MainList = require('../../assets/js/components/MainList.jsx');
TestUtils = React.addons.TestUtils; TestUtils = React.addons.TestUtils;
// Create the React component here using TestUtils and store into Component // Create the React component here using TestUtils and store into Component
}); });
it('should work', function() { it('should work', function() {
expect(2 + 2).toEqual(4); expect(2 + 2).toEqual(4);
}); });
}); });

View File

@@ -1,17 +1,17 @@
jest.dontMock('../../assets/js/components/Navbar.jsx'); jest.dontMock('../../assets/js/components/Navbar.jsx');
describe('Navbar', function() { describe('Navbar', function() {
var React, Navbar, TestUtils, Component; var React, Navbar, TestUtils, Component;
beforeEach(function() { beforeEach(function() {
// Setup our tools // Setup our tools
React = require('react/addons'); React = require('react/addons');
Navbar = require('../../assets/js/components/Navbar.jsx'); Navbar = require('../../assets/js/components/Navbar.jsx');
TestUtils = React.addons.TestUtils; TestUtils = React.addons.TestUtils;
// Create the React component here using TestUtils and store into Component // Create the React component here using TestUtils and store into Component
}); });
it('should work', function() { it('should work', function() {
expect(2 + 2).toEqual(4); expect(2 + 2).toEqual(4);
}); });
}); });

View File

@@ -1,17 +1,17 @@
jest.dontMock('../../assets/js/components/Options.jsx'); jest.dontMock('../../assets/js/components/Options.jsx');
describe('Options', function() { describe('Options', function() {
var React, Options, TestUtils, Component; var React, Options, TestUtils, Component;
beforeEach(function() { beforeEach(function() {
// Setup our tools // Setup our tools
React = require('react/addons'); React = require('react/addons');
Options = require('../../assets/js/components/Options.jsx'); Options = require('../../assets/js/components/Options.jsx');
TestUtils = React.addons.TestUtils; TestUtils = React.addons.TestUtils;
// Create the React component here using TestUtils and store into Component // Create the React component here using TestUtils and store into Component
}); });
it('should work', function() { it('should work', function() {
expect(2 + 2).toEqual(4); expect(2 + 2).toEqual(4);
}); });
}); });

View File

@@ -1,17 +1,17 @@
jest.dontMock('../../assets/js/components/SitesList.jsx'); jest.dontMock('../../assets/js/components/SitesList.jsx');
describe('SitesList', function() { describe('SitesList', function() {
var React, SitesList, TestUtils, Component; var React, SitesList, TestUtils, Component;
beforeEach(function() { beforeEach(function() {
// Setup our tools // Setup our tools
React = require('react/addons'); React = require('react/addons');
SitesList = require('../../assets/js/components/SitesList.jsx'); SitesList = require('../../assets/js/components/SitesList.jsx');
TestUtils = React.addons.TestUtils; TestUtils = React.addons.TestUtils;
// Create the React component here using TestUtils and store into Component // Create the React component here using TestUtils and store into Component
}); });
it('should work', function() { it('should work', function() {
expect(2 + 2).toEqual(4); expect(2 + 2).toEqual(4);
}); });
}); });

View File

@@ -1,17 +1,17 @@
jest.dontMock('../../assets/js/components/Wakatime.jsx'); jest.dontMock('../../assets/js/components/Wakatime.jsx');
describe('Wakatime', function() { describe('Wakatime', function() {
var React, Wakatime, TestUtils, Component; var React, Wakatime, TestUtils, Component;
beforeEach(function() { beforeEach(function() {
// Setup our tools // Setup our tools
React = require('react/addons'); React = require('react/addons');
Wakatime = require('../../assets/js/components/Wakatime.jsx'); Wakatime = require('../../assets/js/components/Wakatime.jsx');
TestUtils = React.addons.TestUtils; TestUtils = React.addons.TestUtils;
// Create the React component here using TestUtils and store into Component // Create the React component here using TestUtils and store into Component
}); });
it('should work', function() { it('should work', function() {
expect(2 + 2).toEqual(4); expect(2 + 2).toEqual(4);
}); });
}); });

View File

@@ -1,10 +1,10 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title></title> <title></title>
</head> </head>
<body> <body>
</body> </body>
</html> </html>

View File

@@ -1,11 +1,11 @@
var chai = require('chai'); var chai = require('chai');
var sinon = require('sinon'); var sinon = require('sinon');
var chrome = require('sinon-chrome'); var chrome = require('sinon-chrome');
var expect = chai.expect; var expect = chai.expect;
describe('Chrome Dev Tools', function() { describe('Chrome Dev Tools', function() {
it('should work', function() { it('should work', function() {
chrome.browserAction.setTitle({title: 'hello'}); chrome.browserAction.setTitle({title: 'hello'});
sinon.assert.calledOnce(chrome.browserAction.setTitle); sinon.assert.calledOnce(chrome.browserAction.setTitle);
}); });
}); });

View File

@@ -1,10 +1,10 @@
var chai = require('chai'); var chai = require('chai');
var expect = chai.expect; var expect = chai.expect;
import changeExtensionIcon from '../../assets/js/helpers/changeExtensionIcon'; import changeExtensionIcon from '../../assets/js/helpers/changeExtensionIcon';
describe('changeExtensionIcon', function() { describe('changeExtensionIcon', function() {
it('should be a function', function() { it('should be a function', function() {
expect(changeExtensionIcon).to.be.a('function'); expect(changeExtensionIcon).to.be.a('function');
}); });
}); });

View File

@@ -1,10 +1,10 @@
var chai = require('chai'); var chai = require('chai');
var expect = chai.expect; var expect = chai.expect;
import changeExtensionState from '../../assets/js/helpers/changeExtensionState'; import changeExtensionState from '../../assets/js/helpers/changeExtensionState';
describe('changeExtensionState', function() { describe('changeExtensionState', function() {
it('should be a function', function() { it('should be a function', function() {
expect(changeExtensionState).to.be.a('function'); expect(changeExtensionState).to.be.a('function');
}); });
}); });

View File

@@ -1,18 +1,18 @@
var chai = require('chai'); var chai = require('chai');
var sinon = require('sinon-chai'); var sinon = require('sinon-chai');
var chrome = require('sinon-chrome'); var chrome = require('sinon-chrome');
var expect = chai.expect; var expect = chai.expect;
import changeExtensionTooltip from '../../assets/js/helpers/changeExtensionTooltip'; import changeExtensionTooltip from '../../assets/js/helpers/changeExtensionTooltip';
describe('changeExtensionTooltip', function() { describe('changeExtensionTooltip', function() {
it('should be a function', function() { it('should be a function', function() {
expect(changeExtensionTooltip).to.be.a('function'); expect(changeExtensionTooltip).to.be.a('function');
}); });
// it('should change the extension tooltip', function() { // it('should change the extension tooltip', function() {
// changeExtensionTooltip('WakaTime'); // changeExtensionTooltip('WakaTime');
// expect(chrome.browserAction.setTitle).toHaveBeenCalledWith({title: 'Wakatime'}); // expect(chrome.browserAction.setTitle).toHaveBeenCalledWith({title: 'Wakatime'});
// sinon.assert.calledWithMatch(chrome.browserAction.setTitle, {title: 'WakaTime'}); // sinon.assert.calledWithMatch(chrome.browserAction.setTitle, {title: 'WakaTime'});
// }); // });
}); });

View File

@@ -1,26 +1,26 @@
var chai = require('chai'); var chai = require('chai');
var expect = chai.expect; var expect = chai.expect;
import contains from '../../assets/js/helpers/contains'; import contains from '../../assets/js/helpers/contains';
describe('contains', function() { describe('contains', function() {
it('should be a function', function() { it('should be a function', function() {
expect(contains).to.be.a('function'); expect(contains).to.be.a('function');
}); });
it('should match url against blacklist and return true', function() { it('should match url against blacklist and return true', function() {
var list = "localhost\ntest.com"; var list = "localhost\ntest.com";
var url = 'http://localhost/fooapp'; var url = 'http://localhost/fooapp';
expect(contains(url, list)).to.equal(true); expect(contains(url, list)).to.equal(true);
}); });
it('should not match url against blacklist and return false', function() { it('should not match url against blacklist and return false', function() {
var list = "localhost2\ntest.com"; var list = "localhost2\ntest.com";
var url = 'http://localhost/fooapp'; var url = 'http://localhost/fooapp';
expect(contains(url, list)).to.equal(false); expect(contains(url, list)).to.equal(false);
}); });
}); });

View File

@@ -1,19 +1,19 @@
var chai = require('chai'); var chai = require('chai');
var expect = chai.expect; var expect = chai.expect;
import getDomainFromUrl from '../../assets/js/helpers/getDomainFromUrl'; import getDomainFromUrl from '../../assets/js/helpers/getDomainFromUrl';
describe('getDomainFromUrl', function() { describe('getDomainFromUrl', function() {
it('should be a function', function() { it('should be a function', function() {
expect(getDomainFromUrl).to.be.a('function'); expect(getDomainFromUrl).to.be.a('function');
}); });
it('should return the domain', function() { it('should return the domain', function() {
expect(getDomainFromUrl('http://google.com/something/very/secret')).to.equal('http://google.com'); expect(getDomainFromUrl('http://google.com/something/very/secret')).to.equal('http://google.com');
expect(getDomainFromUrl('http://www.google.com/something/very/secret')).to.equal('http://www.google.com'); expect(getDomainFromUrl('http://www.google.com/something/very/secret')).to.equal('http://www.google.com');
// This is not how it was imaged to work, but let's leave it here as a warning. // This is not how it was imaged to work, but let's leave it here as a warning.
expect(getDomainFromUrl('google.com/something/very/secret')).to.equal('google.com//very'); expect(getDomainFromUrl('google.com/something/very/secret')).to.equal('google.com//very');
}); });
}); });

View File

@@ -1,18 +1,18 @@
var chai = require('chai'); var chai = require('chai');
var expect = chai.expect; var expect = chai.expect;
import in_array from '../../assets/js/helpers/in_array'; import in_array from '../../assets/js/helpers/in_array';
describe('in_array', function() { describe('in_array', function() {
it('should be a function', function() { it('should be a function', function() {
expect(in_array).to.be.a('function'); expect(in_array).to.be.a('function');
}); });
it('should find the needle and return true', function() { it('should find the needle and return true', function() {
expect(in_array('4', ['4', '3', '2', '1'])).to.equal(true); expect(in_array('4', ['4', '3', '2', '1'])).to.equal(true);
}); });
it('should not find the needle and it should return false', function() { it('should not find the needle and it should return false', function() {
expect(in_array('5', ['4', '3', '2', '1'])).to.equal(false); expect(in_array('5', ['4', '3', '2', '1'])).to.equal(false);
}); });
}); });

View File

@@ -1,24 +1,24 @@
/** /**
* Test Runner for Mocha tests * Test Runner for Mocha tests
* Using phantomjs to render page and execute scripts * Using phantomjs to render page and execute scripts
*/ */
var node_modules = '../node_modules/'; var node_modules = '../node_modules/';
phantom.injectJs(node_modules + 'mocha/mocha.js'); phantom.injectJs(node_modules + 'mocha/mocha.js');
phantom.injectJs(node_modules + 'sinon-chrome/src/phantom-tweaks.js'); phantom.injectJs(node_modules + 'sinon-chrome/src/phantom-tweaks.js');
mocha.setup({ui: 'bdd', reporter: 'spec'}); mocha.setup({ui: 'bdd', reporter: 'spec'});
// Setup // Setup
phantom.injectJs('beforeeach.js'); phantom.injectJs('beforeeach.js');
// Tests // Tests
phantom.injectJs('helpers/changeExtensionTooltip.spec.js'); phantom.injectJs('helpers/changeExtensionTooltip.spec.js');
// Execute // Execute
mocha.run(function(failures) { mocha.run(function(failures) {
// setTimeout is needed to supress "Unsafe JavaScript attempt to access..." // setTimeout is needed to supress "Unsafe JavaScript attempt to access..."
// see https://github.com/ariya/phantomjs/issues/12697 // see https://github.com/ariya/phantomjs/issues/12697
setTimeout(function() { setTimeout(function() {
phantom.exit(failures); phantom.exit(failures);
}, 0); }, 0);
}); });