Skip to content

Commit 9556417

Browse files
authored
Merge pull request #2923 from LuisReinoso/master
Add Wakatime integration
2 parents 60fbb7d + 8ede1a4 commit 9556417

File tree

9 files changed

+323
-5
lines changed

9 files changed

+323
-5
lines changed

browser/components/CodeEditor.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ const buildEditorContextMenu = require('browser/lib/contextMenuBuilder')
2121
import { createTurndownService } from '../lib/turndown'
2222
import { languageMaps } from '../lib/CMLanguageList'
2323
import snippetManager from '../lib/SnippetManager'
24+
import { findStorage } from 'browser/lib/findStorage'
25+
import { sendWakatimeHeartBeat } from 'browser/lib/wakatime-plugin'
2426
import {
2527
generateInEditor,
2628
tocExistsInEditor
@@ -113,6 +115,16 @@ export default class CodeEditor extends React.Component {
113115
this.editorActivityHandler = () => this.handleEditorActivity()
114116

115117
this.turndownService = createTurndownService()
118+
119+
// wakatime
120+
const { storageKey, noteKey } = this.props
121+
const storage = findStorage(storageKey)
122+
if (storage)
123+
sendWakatimeHeartBeat(storage.path, noteKey, storage.name, {
124+
isWrite: false,
125+
hasFileChanges: false,
126+
isFileChange: true
127+
})
116128
}
117129

118130
handleSearch(msg) {
@@ -793,9 +805,23 @@ export default class CodeEditor extends React.Component {
793805
this.updateHighlight(editor, changeObject)
794806

795807
this.value = editor.getValue()
808+
809+
const { storageKey, noteKey } = this.props
810+
const storage = findStorage(storageKey)
796811
if (this.props.onChange) {
797812
this.props.onChange(editor)
798813
}
814+
815+
const isWrite = !!this.props.onChange
816+
const hasFileChanges = isWrite
817+
818+
if (storage) {
819+
sendWakatimeHeartBeat(storage.path, noteKey, storage.name, {
820+
isWrite,
821+
hasFileChanges,
822+
isFileChange: false
823+
})
824+
}
799825
}
800826

801827
linePossibleContainsHeadline(currentLine) {
@@ -923,6 +949,16 @@ export default class CodeEditor extends React.Component {
923949
this.restartHighlighting()
924950
this.editor.on('change', this.changeHandler)
925951
this.editor.refresh()
952+
953+
// wakatime
954+
const { storageKey, noteKey } = this.props
955+
const storage = findStorage(storageKey)
956+
if (storage)
957+
sendWakatimeHeartBeat(storage.path, noteKey, storage.name, {
958+
isWrite: false,
959+
hasFileChanges: false,
960+
isFileChange: true
961+
})
926962
}
927963

928964
setValue(value) {

browser/lib/wakatime-plugin.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import config from 'browser/main/lib/ConfigManager'
2+
const exec = require('child_process').exec
3+
const path = require('path')
4+
let lastHeartbeat = 0
5+
6+
function sendWakatimeHeartBeat(
7+
storagePath,
8+
noteKey,
9+
storageName,
10+
{ isWrite, hasFileChanges, isFileChange }
11+
) {
12+
if (
13+
config.get().wakatime.isActive &&
14+
!!config.get().wakatime.key &&
15+
(new Date().getTime() - lastHeartbeat > 120000 || isFileChange)
16+
) {
17+
const notePath = path.join(storagePath, 'notes', noteKey + '.cson')
18+
19+
if (!isWrite && !hasFileChanges && !isFileChange) {
20+
return
21+
}
22+
23+
lastHeartbeat = new Date()
24+
const wakatimeKey = config.get().wakatime.key
25+
if (wakatimeKey) {
26+
exec(
27+
`wakatime --file ${notePath} --project '${storageName}' --key ${wakatimeKey} --plugin Boostnote-wakatime`,
28+
(error, stdOut, stdErr) => {
29+
if (error) {
30+
console.log(error)
31+
lastHeartbeat = 0
32+
} else {
33+
console.log(
34+
'wakatime',
35+
'isWrite',
36+
isWrite,
37+
'hasChanges',
38+
hasFileChanges,
39+
'isFileChange',
40+
isFileChange
41+
)
42+
}
43+
}
44+
)
45+
}
46+
}
47+
}
48+
49+
export { sendWakatimeHeartBeat }

browser/main/Detail/SnippetNoteDetail.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -870,6 +870,8 @@ class SnippetNoteDetail extends React.Component {
870870
enableSmartPaste={config.editor.enableSmartPaste}
871871
hotkey={config.hotkey}
872872
autoDetect={autoDetect}
873+
storageKey={storageKey}
874+
noteKey={note.key}
873875
/>
874876
)}
875877
</div>

browser/main/lib/ConfigManager.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,10 @@ export const DEFAULT_CONFIG = {
138138
username: '',
139139
password: ''
140140
},
141-
coloredTags: {}
141+
coloredTags: {},
142+
wakatime: {
143+
key: null
144+
}
142145
}
143146

144147
function validate(config) {
@@ -254,6 +257,12 @@ function assignConfigValues(originalConfig, rcConfig) {
254257
originalConfig.hotkey,
255258
rcConfig.hotkey
256259
)
260+
config.wakatime = Object.assign(
261+
{},
262+
DEFAULT_CONFIG.wakatime,
263+
originalConfig.wakatime,
264+
rcConfig.wakatime
265+
)
257266
config.blog = Object.assign(
258267
{},
259268
DEFAULT_CONFIG.blog,
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import PropTypes from 'prop-types'
2+
import React from 'react'
3+
import CSSModules from 'browser/lib/CSSModules'
4+
import styles from './ConfigTab.styl'
5+
import ConfigManager from 'browser/main/lib/ConfigManager'
6+
import { store } from 'browser/main/store'
7+
import _ from 'lodash'
8+
import i18n from 'browser/lib/i18n'
9+
import { sync as commandExists } from 'command-exists'
10+
const electron = require('electron')
11+
const ipc = electron.ipcRenderer
12+
const { remote } = electron
13+
const { dialog } = remote
14+
class PluginsTab extends React.Component {
15+
constructor(props) {
16+
super(props)
17+
18+
this.state = {
19+
config: props.config
20+
}
21+
}
22+
23+
componentDidMount() {
24+
this.handleSettingDone = () => {
25+
this.setState({
26+
pluginsAlert: {
27+
type: 'success',
28+
message: i18n.__('Successfully applied!')
29+
}
30+
})
31+
}
32+
this.handleSettingError = err => {
33+
this.setState({
34+
pluginsAlert: {
35+
type: 'error',
36+
message:
37+
err.message != null ? err.message : i18n.__('An error occurred!')
38+
}
39+
})
40+
}
41+
this.oldWakatimeConfig = this.state.config.wakatime
42+
ipc.addListener('APP_SETTING_DONE', this.handleSettingDone)
43+
ipc.addListener('APP_SETTING_ERROR', this.handleSettingError)
44+
}
45+
46+
componentWillUnmount() {
47+
ipc.removeListener('APP_SETTING_DONE', this.handleSettingDone)
48+
ipc.removeListener('APP_SETTING_ERROR', this.handleSettingError)
49+
}
50+
51+
checkWakatimePluginRequirement() {
52+
const { wakatime } = this.state.config
53+
if (wakatime.isActive && !commandExists('wakatime')) {
54+
this.setState({
55+
wakatimePluginAlert: {
56+
type: i18n.__('Warning'),
57+
message: i18n.__('Missing wakatime cli')
58+
}
59+
})
60+
61+
const alertConfig = {
62+
type: 'warning',
63+
message: i18n.__('Missing Wakatime CLI'),
64+
detail: i18n.__(
65+
`Please install Wakatime CLI to use Wakatime tracker feature.`
66+
),
67+
buttons: [i18n.__('OK')]
68+
}
69+
dialog.showMessageBox(remote.getCurrentWindow(), alertConfig)
70+
} else {
71+
this.setState({
72+
wakatimePluginAlert: null
73+
})
74+
}
75+
}
76+
77+
handleSaveButtonClick(e) {
78+
const newConfig = {
79+
wakatime: {
80+
isActive: this.state.config.wakatime.isActive,
81+
key: this.state.config.wakatime.key
82+
}
83+
}
84+
85+
ConfigManager.set(newConfig)
86+
87+
store.dispatch({
88+
type: 'SET_CONFIG',
89+
config: newConfig
90+
})
91+
this.clearMessage()
92+
this.props.haveToSave()
93+
this.checkWakatimePluginRequirement()
94+
}
95+
96+
handleIsWakatimePluginActiveChange(e) {
97+
const { config } = this.state
98+
config.wakatime.isActive = !config.wakatime.isActive
99+
this.setState({
100+
config
101+
})
102+
if (_.isEqual(this.oldWakatimeConfig.isActive, config.wakatime.isActive)) {
103+
this.props.haveToSave()
104+
} else {
105+
this.props.haveToSave({
106+
tab: 'Plugins',
107+
type: 'warning',
108+
message: i18n.__('Unsaved Changes!')
109+
})
110+
}
111+
}
112+
113+
handleWakatimeKeyChange(e) {
114+
const { config } = this.state
115+
config.wakatime = {
116+
isActive: true,
117+
key: this.refs.wakatimeKey.value
118+
}
119+
this.setState({
120+
config
121+
})
122+
if (_.isEqual(this.oldWakatimeConfig.key, config.wakatime.key)) {
123+
this.props.haveToSave()
124+
} else {
125+
this.props.haveToSave({
126+
tab: 'Plugins',
127+
type: 'warning',
128+
message: i18n.__('Unsaved Changes!')
129+
})
130+
}
131+
}
132+
133+
clearMessage() {
134+
_.debounce(() => {
135+
this.setState({
136+
pluginsAlert: null
137+
})
138+
}, 2000)()
139+
}
140+
141+
render() {
142+
const pluginsAlert = this.state.pluginsAlert
143+
const pluginsAlertElement =
144+
pluginsAlert != null ? (
145+
<p className={`alert ${pluginsAlert.type}`}>{pluginsAlert.message}</p>
146+
) : null
147+
148+
const wakatimeAlert = this.state.wakatimePluginAlert
149+
const wakatimePluginAlertElement =
150+
wakatimeAlert != null ? (
151+
<p className={`alert ${wakatimeAlert.type}`}>{wakatimeAlert.message}</p>
152+
) : null
153+
154+
const { config } = this.state
155+
156+
return (
157+
<div styleName='root'>
158+
<div styleName='group'>
159+
<div styleName='group-header'>{i18n.__('Plugins')}</div>
160+
<div styleName='group-header2'>{i18n.__('Wakatime')}</div>
161+
<div styleName='group-checkBoxSection'>
162+
<label>
163+
<input
164+
onChange={e => this.handleIsWakatimePluginActiveChange(e)}
165+
checked={config.wakatime.isActive}
166+
ref='wakatimeIsActive'
167+
type='checkbox'
168+
/>
169+
&nbsp;
170+
{i18n.__('Enable Wakatime')}
171+
</label>
172+
</div>
173+
<div styleName='group-section'>
174+
<div styleName='group-section-label'>{i18n.__('Wakatime key')}</div>
175+
<div styleName='group-section-control'>
176+
<input
177+
styleName='group-section-control-input'
178+
onChange={e => this.handleWakatimeKeyChange(e)}
179+
disabled={!config.wakatime.isActive}
180+
ref='wakatimeKey'
181+
value={config.wakatime.key}
182+
type='text'
183+
/>
184+
{wakatimePluginAlertElement}
185+
</div>
186+
</div>
187+
<div styleName='group-control'>
188+
<button
189+
styleName='group-control-rightButton'
190+
onClick={e => this.handleSaveButtonClick(e)}
191+
>
192+
{i18n.__('Save')}
193+
</button>
194+
{pluginsAlertElement}
195+
</div>
196+
</div>
197+
</div>
198+
)
199+
}
200+
}
201+
202+
PluginsTab.propTypes = {
203+
dispatch: PropTypes.func,
204+
haveToSave: PropTypes.func
205+
}
206+
207+
export default CSSModules(PluginsTab, styles)

browser/main/modals/PreferencesModal/index.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import InfoTab from './InfoTab'
77
import Crowdfunding from './Crowdfunding'
88
import StoragesTab from './StoragesTab'
99
import SnippetTab from './SnippetTab'
10+
import PluginsTab from './PluginsTab'
1011
import Blog from './Blog'
1112
import ModalEscButton from 'browser/components/ModalEscButton'
1213
import CSSModules from 'browser/lib/CSSModules'
@@ -82,6 +83,14 @@ class Preferences extends React.Component {
8283
)
8384
case 'SNIPPET':
8485
return <SnippetTab dispatch={dispatch} config={config} data={data} />
86+
case 'PLUGINS':
87+
return (
88+
<PluginsTab
89+
dispatch={dispatch}
90+
config={config}
91+
haveToSave={alert => this.setState({ PluginsAlert: alert })}
92+
/>
93+
)
8594
case 'STORAGES':
8695
default:
8796
return (
@@ -122,7 +131,8 @@ class Preferences extends React.Component {
122131
{ target: 'INFO', label: i18n.__('About') },
123132
{ target: 'CROWDFUNDING', label: i18n.__('Crowdfunding') },
124133
{ target: 'BLOG', label: i18n.__('Blog'), Blog: this.state.BlogAlert },
125-
{ target: 'SNIPPET', label: i18n.__('Snippets') }
134+
{ target: 'SNIPPET', label: i18n.__('Snippets') },
135+
{ target: 'PLUGINS', label: i18n.__('Plugins') }
126136
]
127137

128138
const navButtons = tabs.map(tab => {

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"chart.js": "^2.7.2",
6262
"codemirror": "^5.40.2",
6363
"codemirror-mode-elixir": "^1.1.1",
64+
"command-exists": "^1.2.9",
6465
"connected-react-router": "^6.4.0",
6566
"electron-config": "^1.0.0",
6667
"electron-gh-releases": "^2.0.4",

0 commit comments

Comments
 (0)