mirror of
https://github.com/EnixCoda/Gitako.git
synced 2026-03-11 08:54:44 +00:00
feat(shortcut): add shortcut setting for toggle show
This commit is contained in:
parent
2c6de05b17
commit
9f7fc11997
5 changed files with 231 additions and 41 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
58
src/utils/keyHelper.js
Normal 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,
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue