feat(shortcut): add shortcut setting for toggle show

This commit is contained in:
EnixCoda 2018-05-28 23:41:46 +08:00
parent 2c6de05b17
commit 9f7fc11997
5 changed files with 231 additions and 41 deletions

View file

@ -3,70 +3,126 @@ import preact from 'preact'
import Icon from './Icon'
import storageHelper from '../utils/storageHelper'
import keyHelper from '../utils/keyHelper'
const ACCESS_TOKEN_REGEXP = /^[0-9a-f]{40}$/
const OperatingSystems = {
Windows: 'Windows',
macOS: 'Macintosh',
others: 'unknown',
}
function detectOS() {
const {
navigator: { userAgent },
} = window
if (userAgent.indexOf(OperatingSystems.Windows) !== -1) return OperatingSystems.Windows
else if (userAgent.indexOf(OperatingSystems.macOS) !== -1) return OperatingSystems.macOS
return OperatingSystems.others
}
function friendlyFormatShortcut(shortcut) {
const OS = detectOS()
if (OS === OperatingSystems.Windows) {
return shortcut.replace(/meta/, 'win')
} else if (OS === OperatingSystems.macOS) {
return shortcut
.replace(/meta/, '⌘')
.replace(/ctrl/, '⌃')
.replace(/shift/, '⇧')
.replace(/alt/, '⌥')
} else {
return shortcut
}
}
export default class SettingsBar extends preact.Component {
state = {
hint: null,
hasAccessToken: false,
accessTokenHint: null,
accessToken: '',
shortcutHint: null,
toggleShowSideBarShortcut: '',
}
componentWillMount() {
const { hasAccessToken } = this.props
this.setState({ hasAccessToken })
const { toggleShowSideBarShortcut } = this.props
this.setState({ toggleShowSideBarShortcut })
}
componentWillReceiveProps({ hasAccessToken }) {
this.setState({ hasAccessToken })
componentWillReceiveProps({ toggleShowSideBarShortcut }) {
this.setState({ toggleShowSideBarShortcut })
}
onInputAccessToken = event => {
const value = event.target.value
this.setState({ accessToken: value })
this.setState({
hint: ACCESS_TOKEN_REGEXP.test(value)
? ''
: 'This token is in unknown format.'
accessTokenHint: ACCESS_TOKEN_REGEXP.test(value) ? '' : 'This token is in unknown format.',
})
}
saveToken = () => {
saveToken = async () => {
const { onHasAccessTokenChange } = this.props
const { accessToken } = this.state
if (accessToken) {
storageHelper.setAccessToken(accessToken)
await storageHelper.setAccessToken(accessToken)
onHasAccessTokenChange(true)
this.setState({
hasAccessToken: true,
hint: 'Your token is saved, will work after reloading the page!',
accessTokenHint: 'Your token is saved, will work after reloading the page!',
})
}
}
clearToken = () => {
storageHelper.setAccessToken('')
this.setState({ accessToken: '', hasAccessToken: false })
clearToken = async () => {
const { onHasAccessTokenChange } = this.props
await storageHelper.setAccessToken('')
onHasAccessTokenChange(false)
this.setState({ accessToken: '' })
}
saveShortcut = async () => {
const { onShortcutChange } = this.props
const { toggleShowSideBarShortcut } = this.state
await storageHelper.setShortcut(toggleShowSideBarShortcut)
onShortcutChange(toggleShowSideBarShortcut)
this.setState({
shortcutHint: 'Shortcut is saved!',
})
}
/**
* @param {KeyboardEvent} e
*/
onShortCutInputKeyDown = e => {
e.preventDefault()
const shortcut = keyHelper.parseEvent(e)
this.setState({ toggleShowSideBarShortcut: shortcut })
}
render() {
const { hint, accessToken, hasAccessToken } = this.state
const { toggleShowSettings, activated} = this.props
const { accessTokenHint, accessToken, toggleShowSideBarShortcut, shortcutHint } = this.state
const { hasAccessToken } = this.props
const { toggleShowSettings, activated } = this.props
return (
<div className={'gitako-settings-bar'}>
<div className={'placeholder-row'}>
<h3>{activated ? 'Settings' : ''}</h3>
{activated
? <Icon type={'chevron-down'} className={'hide-settings-icon'} onClick={toggleShowSettings} />
: <Icon type={'gear'} className={'show-settings-icon'} onClick={toggleShowSettings} />
}
{activated ? (
<Icon
type={'chevron-down'}
className={'hide-settings-icon'}
onClick={toggleShowSettings}
/>
) : (
<Icon type={'gear'} className={'show-settings-icon'} onClick={toggleShowSettings} />
)}
</div>
{activated && (
<div className={'gitako-settings-bar-content'}>
<div className={'gitako-settings-bar-content-section access-token'}>
<h4>Access Token</h4>
<span>
With access token provided, Gitako can access more repositories.
</span>
<span>With access token provided, Gitako can access more repositories.</span>
<br />
<a href="https://github.com/blog/1509-personal-api-tokens" target="_blank">
Help: how to create access token?
@ -84,21 +140,39 @@ export default class SettingsBar extends preact.Component {
<input
className={'access-token-input form-control'}
disabled={hasAccessToken}
placeholder={
hasAccessToken
? 'Your token is saved'
: 'Input your token here'
}
placeholder={hasAccessToken ? 'Your token is saved' : 'Input your token here'}
value={accessToken}
onInput={this.onInputAccessToken}
/>
{
hasAccessToken
? <button className={'btn'} onClick={this.clearToken}>Clear</button>
: <button className={'btn'} onClick={this.saveToken}>Save</button>
}
{hasAccessToken ? (
<button className={'btn'} onClick={this.clearToken}>
Clear
</button>
) : (
<button className={'btn'} onClick={this.saveToken}>
Save
</button>
)}
</div>
{hint && <span className={'hint'}>{hint}</span>}
{accessTokenHint && <span className={'hint'}>{accessTokenHint}</span>}
</div>
<div className={'gitako-settings-bar-content-section toggle-shortcut'}>
<h4>Toggle shortcut</h4>
<span>Set a combination of keys for toggling Gitako sidebar.</span>
<br />
<div className={'toggle-shortcut-input-control'}>
<input
className={'toggle-shortcut-input form-control'}
placeholder={'focus here and press the shortcut keys'}
default={''}
value={friendlyFormatShortcut(toggleShowSideBarShortcut)}
onKeyDown={this.onShortCutInputKeyDown}
/>
<button className={'btn'} onClick={this.saveShortcut}>
Save
</button>
</div>
{shortcutHint && <span className={'hint'}>{shortcutHint}</span>}
</div>
<div className={'gitako-settings-bar-content-section position'}>
<h5>Position of Gitako (WIP)</h5>

View file

@ -13,6 +13,7 @@ import DOMHelper, { REPO_TYPE_PRIVATE } from '../utils/DOMHelper'
import GitHubHelper, { NOT_FOUND, BAD_CREDENTIALS } from '../utils/GitHubHelper'
import storageHelper from '../utils/storageHelper'
import URLHelper from '../utils/URLHelper'
import keyHelper from '../utils/keyHelper'
export default class SideBar extends preact.Component {
state = {
@ -23,7 +24,9 @@ export default class SideBar extends preact.Component {
// whether failed loading the repo due to it is private
errorDueToAuth: false,
// got access token for GitHub
hasAccessToken: null,
hasAccessToken: false,
// the shortcut string for toggle sidebar
toggleShowSideBarShortcut: '',
// meta data for the repository
metaData: null,
// file tree data
@ -35,8 +38,11 @@ export default class SideBar extends preact.Component {
const metaDataFromUrl = URLHelper.parse()
this.setState({ metaData: metaDataFromUrl })
DOMHelper.decorateGitHubPageContent()
const accessToken = await storageHelper.getAccessToken()
this.setState({ hasAccessToken: Boolean(accessToken) })
const [accessToken, shortcut] = await Promise.all([
storageHelper.getAccessToken(),
storageHelper.getShortcut(),
])
this.setState({ hasAccessToken: Boolean(accessToken), toggleShowSideBarShortcut: shortcut })
const metaDataFromAPI = await GitHubHelper.getRepoMeta({ ...metaDataFromUrl, accessToken })
const branchName = metaDataFromUrl.branchName || metaDataFromAPI['default_branch']
const metaData = { ...metaDataFromUrl, branchName, api: metaDataFromAPI }
@ -48,6 +54,7 @@ export default class SideBar extends preact.Component {
window.addEventListener('pjax:send', this.onPJAXStart)
window.addEventListener('pjax:complete', this.onPJAXEnd)
window.addEventListener('keydown', this.onKeyDown)
} catch (err) {
// TODO: detect request time exceeds limit
if (err.message === NOT_FOUND || err.message === BAD_CREDENTIALS) {
@ -99,6 +106,24 @@ export default class SideBar extends preact.Component {
this.setState({ showSettings: !showSettings })
}
onHasAccessTokenChange = hasAccessToken => {
this.setState({ hasAccessToken })
}
onKeyDown = e => {
const { toggleShowSideBarShortcut } = this.state
if (toggleShowSideBarShortcut) {
const keys = keyHelper.parseEvent(e)
if (keys === toggleShowSideBarShortcut) {
this.toggleShowSideBar()
}
}
}
onShortcutChange = shortcut => {
this.setState({ toggleShowSideBarShortcut: shortcut })
}
renderAccessDeniedError() {
return (
<div className={'description'}>
@ -126,7 +151,13 @@ export default class SideBar extends preact.Component {
}
render() {
const { shouldShow, showSettings, hasAccessToken, loading } = this.state
const {
shouldShow,
showSettings,
hasAccessToken,
toggleShowSideBarShortcut,
loading,
} = this.state
return (
<div className={cx('gitako', { hidden: !shouldShow })}>
<Portal into={this.logoContainerElement}>
@ -136,8 +167,11 @@ export default class SideBar extends preact.Component {
{this.renderContent()}
<SettingsBar
toggleShowSettings={this.toggleShowSettings}
onShortcutChange={this.onShortcutChange}
onHasAccessTokenChange={this.onHasAccessTokenChange}
activated={showSettings}
hasAccessToken={hasAccessToken}
toggleShowSideBarShortcut={toggleShowSideBarShortcut}
/>
</div>
</div>

View file

@ -274,6 +274,19 @@
}
}
}
.toggle-shortcut {
.hint {
color: #6a737d;
}
}
.toggle-shortcut-input-control {
display: flex;
margin-top: 8px;
.toggle-shortcut-input {
flex: 1;
box-shadow: none;
}
}
.placeholder-row {
display: flex;
justify-content: space-between;

58
src/utils/keyHelper.js Normal file
View file

@ -0,0 +1,58 @@
const keyCodeArray = [
...'1234567890abcdefghijklmnopqrstuvwxyz'.split(''),
...'`[]\\;\',./'.split(''),
'alt',
'shift',
'control',
'meta',
]
const validKeyCodes = new Set(keyCodeArray)
function isValidKey(key) {
return validKeyCodes.has(key)
}
/**
* parse a string representation of key combination
*
* @param {string} keysString
* @returns {string}
*/
function parse(keysString) {
return (
keysString
.split('+')
/* when trying to set a combination includes '+',
input should be 'shift + =' instead of 'shift + +',
thus a valid key string won't contain '++' */
.map(_ => _.trim().toLowerCase())
.filter(isValidKey)
.sort((a, b) => keyCodeArray.indexOf(b) - keyCodeArray.indexOf(a))
.join('+')
)
}
function parseKeyCode(code) {
let c = code.toLowerCase()
c = c.replace(/(left|right)$/, '')
c = c.replace(/^digit/, '')
c = c.replace(/^key/, '')
return c
}
function parseEvent(e) {
const { altKey: alt, shiftKey: shift, metaKey: meta, ctrlKey: ctrl } = e
const code = parseKeyCode(e.code)
const keys = { meta, ctrl, shift, alt, [code]: true }
const combination = parse(
Object.entries(keys)
.filter(([key, pressed]) => pressed)
.map(([key, pressed]) => key)
.join('+')
)
return combination
}
export default {
parseEvent,
}

View file

@ -1,5 +1,6 @@
const localStorage = chrome.storage.local
const ACCESS_TOKEN_KEY = 'access_token'
const SHORTCUT_KEY = 'shortcut'
function get(key) {
return new Promise(resolve => localStorage.get(key, items => resolve(items[key])))
@ -17,9 +18,19 @@ function setAccessToken(accessToken) {
return set(ACCESS_TOKEN_KEY, accessToken)
}
function getShortcut() {
return get(SHORTCUT_KEY)
}
function setShortcut(shortcut) {
return set(SHORTCUT_KEY, shortcut)
}
export default {
get,
set,
getAccessToken,
setAccessToken,
getShortcut,
setShortcut,
}