diff --git a/src/components/SettingsBar.js b/src/components/SettingsBar.js index 6494257..ceb29d1 100644 --- a/src/components/SettingsBar.js +++ b/src/components/SettingsBar.js @@ -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 (

{activated ? 'Settings' : ''}

- {activated - ? - : - } + {activated ? ( + + ) : ( + + )}
{activated && (

Access Token

- - With access token provided, Gitako can access more repositories. - + With access token provided, Gitako can access more repositories.
Help: how to create access token? @@ -84,21 +140,39 @@ export default class SettingsBar extends preact.Component { - { - hasAccessToken - ? - : - } + {hasAccessToken ? ( + + ) : ( + + )}
- {hint && {hint}} + {accessTokenHint && {accessTokenHint}} +
+
+

Toggle shortcut

+ Set a combination of keys for toggling Gitako sidebar. +
+
+ + +
+ {shortcutHint && {shortcutHint}}
Position of Gitako (WIP)
diff --git a/src/components/SideBar.js b/src/components/SideBar.js index 236cefb..b1de9cc 100644 --- a/src/components/SideBar.js +++ b/src/components/SideBar.js @@ -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 (
@@ -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 (
@@ -136,8 +167,11 @@ export default class SideBar extends preact.Component { {this.renderContent()}
diff --git a/src/content.less b/src/content.less index 2524d52..7d5a4a2 100644 --- a/src/content.less +++ b/src/content.less @@ -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; diff --git a/src/utils/keyHelper.js b/src/utils/keyHelper.js new file mode 100644 index 0000000..8d26bcb --- /dev/null +++ b/src/utils/keyHelper.js @@ -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, +} diff --git a/src/utils/storageHelper.js b/src/utils/storageHelper.js index 7f3195d..a5f1f19 100644 --- a/src/utils/storageHelper.js +++ b/src/utils/storageHelper.js @@ -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, }