mirror of
https://github.com/EnixCoda/Gitako.git
synced 2026-03-11 08:54:44 +00:00
changes are out of control
This commit is contained in:
parent
f677d7df5a
commit
ddb86fa4ef
11 changed files with 569 additions and 633 deletions
|
|
@ -1,129 +1,91 @@
|
|||
import { LoadingIndicator } from 'components/LoadingIndicator'
|
||||
import { Node } from 'components/Node'
|
||||
import { SearchBar } from 'components/SearchBar'
|
||||
import { useConfigs } from 'containers/ConfigsContext'
|
||||
import { connect } from 'driver/connect'
|
||||
import { FileExplorerCore } from 'driver/core'
|
||||
import { ConnectorState, Props } from 'driver/core/FileExplorer'
|
||||
import * as React from 'react'
|
||||
import { FixedSizeList as List, ListChildComponentProps } from 'react-window'
|
||||
import { FixedSizeList as List, ListChildComponentProps, ListProps } from 'react-window'
|
||||
import { cx } from 'utils/cx'
|
||||
import { usePrevious } from 'utils/hooks'
|
||||
import { TreeNode, VisibleNodes } from 'utils/VisibleNodesGenerator'
|
||||
import { Icon } from './Icon'
|
||||
import { SizeObserver } from './SizeObserver'
|
||||
|
||||
class RawFileExplorer extends React.Component<Props & ConnectorState> {
|
||||
static defaultProps: Partial<Props & ConnectorState> = {
|
||||
freeze: false,
|
||||
searchKey: '',
|
||||
visibleNodes: null,
|
||||
}
|
||||
const VisibleNodesContext = React.createContext<VisibleNodes | null>(null)
|
||||
|
||||
componentDidMount() {
|
||||
const { init, setUpTree, treeData, metaData, compressSingletonFolder, accessToken } = this.props
|
||||
const RawFileExplorer: React.FC<Props & ConnectorState> = function RawFileExplorer(props) {
|
||||
const { visibleNodes, freeze, onNodeClick, searchKey } = props
|
||||
const {
|
||||
val: { access_token: accessToken, compressSingletonFolder },
|
||||
} = useConfigs()
|
||||
|
||||
React.useEffect(() => {
|
||||
const { init } = props
|
||||
init()
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
const { setUpTree, treeData, metaData } = props
|
||||
setUpTree({ treeData, metaData, compressSingletonFolder, accessToken })
|
||||
const { execAfterRender } = this.props
|
||||
}, [props.setUpTree, props.treeData, props.metaData, compressSingletonFolder, accessToken])
|
||||
|
||||
React.useEffect(() => {
|
||||
const { execAfterRender } = props
|
||||
execAfterRender()
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props & ConnectorState) {
|
||||
if (this.props.treeData !== prevProps.treeData) {
|
||||
const { setUpTree, treeData, metaData, compressSingletonFolder, accessToken } = this.props
|
||||
setUpTree({ treeData, metaData, compressSingletonFolder, accessToken })
|
||||
}
|
||||
const { execAfterRender } = this.props
|
||||
execAfterRender()
|
||||
}
|
||||
|
||||
renderFiles(visibleNodes: VisibleNodes) {
|
||||
const { nodes, focusedNode } = visibleNodes
|
||||
const { searchKey } = this.props
|
||||
const inSearch = searchKey !== ''
|
||||
if (inSearch && nodes.length === 0) {
|
||||
return <label className={'no-results'}>No results found.</label>
|
||||
}
|
||||
return (
|
||||
<SizeObserver className={'files'}>
|
||||
{({ width = 0, height = 0 }) => (
|
||||
<this.ListV focusedNode={focusedNode} nodes={nodes} height={height} width={width} />
|
||||
)}
|
||||
</SizeObserver>
|
||||
)
|
||||
}
|
||||
|
||||
ListV = React.memo<{
|
||||
nodes: TreeNode[]
|
||||
height: number
|
||||
width: number
|
||||
focusedNode: TreeNode | null
|
||||
}>(({ nodes, width, height, focusedNode }) => {
|
||||
const listRef = React.useRef<List>(null)
|
||||
React.useEffect(() => {
|
||||
if (focusedNode && listRef.current) {
|
||||
listRef.current.scrollToItem(nodes.indexOf(focusedNode), 'smart')
|
||||
}
|
||||
}, [listRef.current, focusedNode])
|
||||
|
||||
const lastNodeLength = usePrevious(nodes.length)
|
||||
React.useEffect(() => {
|
||||
if (listRef.current && !focusedNode && lastNodeLength !== nodes.length) {
|
||||
listRef.current.scrollTo(0)
|
||||
}
|
||||
}, [listRef.current, focusedNode, nodes.length])
|
||||
return (
|
||||
<List
|
||||
ref={listRef}
|
||||
itemKey={(index, { nodes }) => {
|
||||
const node = nodes[index]
|
||||
return node && node.path
|
||||
}}
|
||||
itemData={{ nodes }}
|
||||
itemCount={nodes.length}
|
||||
itemSize={35}
|
||||
height={height}
|
||||
width={width}
|
||||
>
|
||||
{this.VirtualNode}
|
||||
</List>
|
||||
)
|
||||
})
|
||||
|
||||
VirtualNode = React.memo<ListChildComponentProps>(({ index, style }) => {
|
||||
const { visibleNodes, onNodeClick } = this.props
|
||||
if (!visibleNodes) return null
|
||||
const { nodes, depths, focusedNode, expandedNodes } = visibleNodes
|
||||
const node = nodes[index]
|
||||
return (
|
||||
<Node
|
||||
style={style}
|
||||
key={node.path}
|
||||
node={node}
|
||||
depth={depths.get(node) || 0}
|
||||
focused={focusedNode === node}
|
||||
expanded={expandedNodes.has(node)}
|
||||
onClick={onNodeClick}
|
||||
renderActions={this.renderActions}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
private renderActions: React.ComponentProps<typeof Node>['renderActions'] = node => {
|
||||
const { searchKey, goTo } = this.props
|
||||
return (
|
||||
searchKey && (
|
||||
const renderActions: React.ComponentProps<typeof Node>['renderActions'] = React.useCallback(
|
||||
node =>
|
||||
searchKey ? (
|
||||
<button
|
||||
title={'Reveal in file tree'}
|
||||
className={'go-to-button'}
|
||||
onClick={this.revealNode(goTo, node)}
|
||||
onClick={revealNode(props.goTo, node)}
|
||||
>
|
||||
<Icon type="go-to" />
|
||||
</button>
|
||||
)
|
||||
)
|
||||
}
|
||||
) : null,
|
||||
[searchKey, props.goTo],
|
||||
)
|
||||
|
||||
revealNode(
|
||||
const renderNode = React.useCallback(
|
||||
({ index, style }: ListChildComponentProps) => (
|
||||
<VirtualNode
|
||||
index={index}
|
||||
style={style}
|
||||
onNodeClick={onNodeClick}
|
||||
renderActions={renderActions}
|
||||
/>
|
||||
),
|
||||
[renderActions, onNodeClick],
|
||||
)
|
||||
|
||||
const renderFiles = React.useCallback(
|
||||
({ nodes, focusedNode }: VisibleNodes) => {
|
||||
const inSearch = searchKey !== ''
|
||||
if (inSearch && nodes.length === 0) {
|
||||
return <label className={'no-results'}>No results found.</label>
|
||||
}
|
||||
return (
|
||||
<SizeObserver className={'files'}>
|
||||
{({ width = 0, height = 0 }) => (
|
||||
<ListView
|
||||
renderNode={renderNode}
|
||||
focusedNode={focusedNode}
|
||||
nodes={nodes}
|
||||
height={height}
|
||||
width={width}
|
||||
/>
|
||||
)}
|
||||
</SizeObserver>
|
||||
)
|
||||
},
|
||||
[searchKey, ListView, renderNode],
|
||||
)
|
||||
|
||||
const revealNode = React.useCallback(function revealNode(
|
||||
goTo: (path: string[]) => void,
|
||||
node: TreeNode,
|
||||
): (event: React.MouseEvent<HTMLElement, MouseEvent>) => void {
|
||||
|
|
@ -132,39 +94,113 @@ class RawFileExplorer extends React.Component<Props & ConnectorState> {
|
|||
e.preventDefault()
|
||||
goTo(node.path.split('/'))
|
||||
}
|
||||
}
|
||||
},
|
||||
[])
|
||||
|
||||
render() {
|
||||
const {
|
||||
stateText,
|
||||
visibleNodes,
|
||||
freeze,
|
||||
handleKeyDown,
|
||||
search,
|
||||
toggleShowSettings,
|
||||
onFocusSearchBar,
|
||||
searchKey,
|
||||
} = this.props
|
||||
return (
|
||||
return (
|
||||
<VisibleNodesContext.Provider value={visibleNodes}>
|
||||
<div
|
||||
className={cx(`file-explorer`, { freeze })}
|
||||
tabIndex={-1}
|
||||
onKeyDown={handleKeyDown}
|
||||
onClick={freeze ? toggleShowSettings : undefined}
|
||||
onKeyDown={props.handleKeyDown}
|
||||
onClick={freeze ? props.toggleShowSettings : undefined}
|
||||
>
|
||||
{stateText ? (
|
||||
<LoadingIndicator text={stateText} />
|
||||
{props.stateText ? (
|
||||
<LoadingIndicator text={props.stateText} />
|
||||
) : (
|
||||
visibleNodes && (
|
||||
<React.Fragment>
|
||||
<SearchBar searchKey={searchKey} onSearch={search} onFocus={onFocusSearchBar} />
|
||||
{this.renderFiles(visibleNodes)}
|
||||
</React.Fragment>
|
||||
<>
|
||||
<SearchBar
|
||||
searchKey={searchKey}
|
||||
onSearch={props.search}
|
||||
onFocus={props.onFocusSearchBar}
|
||||
/>
|
||||
{renderFiles(visibleNodes)}
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</VisibleNodesContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
RawFileExplorer.defaultProps = {
|
||||
freeze: false,
|
||||
searchKey: '',
|
||||
visibleNodes: null,
|
||||
}
|
||||
|
||||
export const FileExplorer = connect(FileExplorerCore)(RawFileExplorer)
|
||||
|
||||
function VirtualNode({
|
||||
index,
|
||||
style,
|
||||
onNodeClick,
|
||||
renderActions,
|
||||
}: {
|
||||
index: number
|
||||
style: React.CSSProperties
|
||||
onNodeClick: (treeNode: TreeNode) => void
|
||||
renderActions: ((node: TreeNode) => React.ReactNode) | undefined
|
||||
}) {
|
||||
const visibleNodes = React.useContext(VisibleNodesContext)
|
||||
if (!visibleNodes) return null
|
||||
const { nodes, depths, focusedNode, expandedNodes } = visibleNodes
|
||||
const node = nodes[index]
|
||||
return (
|
||||
<Node
|
||||
style={style}
|
||||
key={node.path}
|
||||
node={node}
|
||||
depth={depths.get(node) || 0}
|
||||
focused={focusedNode === node}
|
||||
expanded={expandedNodes.has(node)}
|
||||
onClick={onNodeClick}
|
||||
renderActions={renderActions}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function ListView({
|
||||
nodes,
|
||||
width,
|
||||
height,
|
||||
focusedNode,
|
||||
renderNode,
|
||||
}: {
|
||||
nodes: TreeNode[]
|
||||
height: number
|
||||
width: number
|
||||
focusedNode: TreeNode | null
|
||||
renderNode: ListProps['children']
|
||||
}) {
|
||||
const listRef = React.useRef<List>(null)
|
||||
React.useEffect(() => {
|
||||
if (focusedNode && listRef.current) {
|
||||
listRef.current.scrollToItem(nodes.indexOf(focusedNode), 'smart')
|
||||
}
|
||||
}, [listRef.current, focusedNode])
|
||||
|
||||
const lastNodeLength = usePrevious(nodes.length)
|
||||
React.useEffect(() => {
|
||||
if (listRef.current && !focusedNode && lastNodeLength !== nodes.length) {
|
||||
listRef.current.scrollTo(0)
|
||||
}
|
||||
}, [listRef.current, focusedNode, nodes.length])
|
||||
return (
|
||||
<List
|
||||
ref={listRef}
|
||||
itemKey={(index, { nodes }) => {
|
||||
const node = nodes[index]
|
||||
return node && node.path
|
||||
}}
|
||||
itemData={{ nodes }}
|
||||
itemCount={nodes.length}
|
||||
itemSize={35}
|
||||
height={height}
|
||||
width={width}
|
||||
>
|
||||
{renderNode}
|
||||
</List>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
52
src/components/MoreOption.tsx
Normal file
52
src/components/MoreOption.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { useConfigs } from 'containers/ConfigsContext'
|
||||
import * as React from 'react'
|
||||
import { Config } from 'utils/configHelper'
|
||||
|
||||
export type SimpleField = {
|
||||
key: keyof Config
|
||||
label: string
|
||||
wikiLink?: string
|
||||
description?: string
|
||||
overwrite?: Props['overwrite']
|
||||
}
|
||||
|
||||
type Props = {
|
||||
field: SimpleField
|
||||
onChange?(): void
|
||||
overwrite?: {
|
||||
value: <T>(value: T) => boolean
|
||||
onChange: (checked: boolean) => any
|
||||
}
|
||||
}
|
||||
|
||||
export function SimpleFieldInput({ field, overwrite, onChange }: Props) {
|
||||
const configContext = useConfigs()
|
||||
const value = configContext.val[field.key]
|
||||
return (
|
||||
<label htmlFor={field.key}>
|
||||
<input
|
||||
id={field.key}
|
||||
name={field.key}
|
||||
type={'checkbox'}
|
||||
onChange={async e => {
|
||||
const enabled = e.currentTarget.checked
|
||||
configContext.set({ [field.key]: overwrite ? overwrite.onChange(enabled) : enabled })
|
||||
if (onChange) onChange()
|
||||
}}
|
||||
checked={overwrite ? overwrite.value(value) : Boolean(value)}
|
||||
/>
|
||||
{field.label}
|
||||
{field.wikiLink ? (
|
||||
<a href={field.wikiLink} target={'_blank'}>
|
||||
(?)
|
||||
</a>
|
||||
) : (
|
||||
field.description && (
|
||||
<span className={'description'} title={field.description}>
|
||||
(?)
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
</label>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
import { raiseError } from 'analytics'
|
||||
import { Icon } from 'components/Icon'
|
||||
import { useConfigs } from 'containers/ConfigsContext'
|
||||
import { oauth, VERSION } from 'env'
|
||||
import * as React from 'react'
|
||||
import { Config, configKeys, setOne } from 'utils/configHelper'
|
||||
import { friendlyFormatShortcut, JSONRequest, parseURLSearch } from 'utils/general'
|
||||
import { useStates } from 'utils/hooks'
|
||||
import * as keyHelper from 'utils/keyHelper'
|
||||
import { SimpleField, SimpleFieldInput } from './MoreOption'
|
||||
|
||||
const WIKI_HOME_LINK = 'https://github.com/EnixCoda/Gitako/wiki'
|
||||
const wikiLinks = {
|
||||
|
|
@ -18,348 +20,268 @@ const wikiLinks = {
|
|||
const ACCESS_TOKEN_REGEXP = /^[0-9a-f]{40}$/
|
||||
|
||||
type Props = {
|
||||
accessToken?: string
|
||||
activated: boolean
|
||||
onAccessTokenChange: (accessToken: string) => void
|
||||
onShortcutChange: (shortcut: string) => void
|
||||
setCopyFile: (copyFileButton: Props['copyFileButton']) => void
|
||||
setCopySnippet: (copySnippetButton: Props['copySnippetButton']) => void
|
||||
setCompressSingleton: (compressSingletonFolder: Props['compressSingletonFolder']) => void
|
||||
setIntelligentToggle: (intelligentToggle: Props['intelligentToggle']) => void
|
||||
toggleShowSettings: () => void
|
||||
toggleShowSideBarShortcut?: string
|
||||
} & Pick<
|
||||
Config,
|
||||
'compressSingletonFolder' | 'copyFileButton' | 'copySnippetButton' | 'intelligentToggle'
|
||||
>
|
||||
|
||||
type State = {
|
||||
accessToken?: string
|
||||
accessTokenHint: React.ReactNode
|
||||
shortcutHint: string
|
||||
toggleShowSideBarShortcut?: string
|
||||
reloadHint: React.ReactNode
|
||||
varyOptions: {
|
||||
key: string
|
||||
label: string
|
||||
onChange: (e: React.FormEvent<HTMLInputElement>) => Promise<void> | void
|
||||
getValue: () => boolean
|
||||
wikiLink?: string
|
||||
description?: string
|
||||
}[]
|
||||
}
|
||||
|
||||
export class SettingsBar extends React.PureComponent<Props, State> {
|
||||
state = {
|
||||
accessToken: '',
|
||||
accessTokenHint: '',
|
||||
shortcutHint: '',
|
||||
toggleShowSideBarShortcut: this.props.toggleShowSideBarShortcut,
|
||||
reloadHint: '',
|
||||
varyOptions: [
|
||||
{
|
||||
key: 'compress-singleton',
|
||||
label: 'Compress singleton folder',
|
||||
onChange: this.createOnToggleChecked(
|
||||
configKeys.compressSingletonFolder,
|
||||
this.props.setCompressSingleton,
|
||||
),
|
||||
getValue: () => this.props.compressSingletonFolder,
|
||||
wikiLink: wikiLinks.compressSingletonFolder,
|
||||
},
|
||||
{
|
||||
key: 'copy-file',
|
||||
label: 'Copy File Shortcut',
|
||||
onChange: this.createOnToggleChecked(configKeys.copyFileButton, this.props.setCopyFile),
|
||||
getValue: () => this.props.copyFileButton,
|
||||
wikiLink: wikiLinks.copyFileButton,
|
||||
},
|
||||
{
|
||||
key: 'copy-snippet',
|
||||
label: 'Copy Snippet Shortcut',
|
||||
onChange: this.createOnToggleChecked(
|
||||
configKeys.copySnippetButton,
|
||||
this.props.setCopySnippet,
|
||||
),
|
||||
getValue: () => this.props.copySnippetButton,
|
||||
wikiLink: wikiLinks.copySnippet,
|
||||
},
|
||||
{
|
||||
key: 'intelligent-toggle',
|
||||
label: 'Intelligent Toggle',
|
||||
onChange: async (e: React.FormEvent<HTMLInputElement>) => {
|
||||
const { checked } = e.currentTarget
|
||||
const intelligentToggle = checked ? null : true
|
||||
await setOne(configKeys.intelligentToggle, intelligentToggle)
|
||||
this.props.setIntelligentToggle(intelligentToggle)
|
||||
},
|
||||
getValue: () => this.props.intelligentToggle === null,
|
||||
description: `Gitako will open/close automatically according to page content when this is enabled.`,
|
||||
},
|
||||
],
|
||||
}
|
||||
const moreFields: SimpleField[] = [
|
||||
{
|
||||
key: 'compressSingletonFolder',
|
||||
label: 'Compress singleton folder',
|
||||
wikiLink: wikiLinks.compressSingletonFolder,
|
||||
},
|
||||
{
|
||||
key: 'copyFileButton',
|
||||
label: 'Copy File Shortcut',
|
||||
wikiLink: wikiLinks.copyFileButton,
|
||||
},
|
||||
{
|
||||
key: 'copySnippetButton',
|
||||
label: 'Copy Snippet Shortcut',
|
||||
wikiLink: wikiLinks.copySnippet,
|
||||
},
|
||||
{
|
||||
key: 'intelligentToggle',
|
||||
label: 'Intelligent Toggle',
|
||||
description: `Gitako will open/close automatically according to page content when this is enabled.`,
|
||||
overwrite: {
|
||||
value: enabled => enabled === null,
|
||||
onChange: checked => (checked ? null : true),
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.accessToken) this.trySetUpAccessTokenWithCode()
|
||||
}
|
||||
function SettingsBarContent() {
|
||||
const configContext = useConfigs()
|
||||
const hasAccessToken = Boolean(configContext.val.access_token)
|
||||
const useAccessToken = useStates('')
|
||||
const useAccessTokenHint = useStates<React.ReactNode>('')
|
||||
const useShortcutHint = useStates('')
|
||||
const useToggleShowSideBarShortcut = useStates(configContext.val.shortcut)
|
||||
const useReloadHint = useStates<React.ReactNode>('')
|
||||
|
||||
componentDidUpdate({ toggleShowSideBarShortcut }: Props) {
|
||||
if (toggleShowSideBarShortcut !== this.props.toggleShowSideBarShortcut) {
|
||||
this.setState({ toggleShowSideBarShortcut: this.props.toggleShowSideBarShortcut })
|
||||
React.useEffect(() => {
|
||||
if (!configContext.val.access_token) {
|
||||
trySetUpAccessTokenWithCode().then(accessToken => {
|
||||
useAccessToken.set(accessToken)
|
||||
saveToken('')
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
private async trySetUpAccessTokenWithCode() {
|
||||
try {
|
||||
const search = parseURLSearch()
|
||||
if ('code' in search) {
|
||||
const res = await JSONRequest('https://github.com/login/oauth/access_token', {
|
||||
code: search.code,
|
||||
client_id: oauth.clientId,
|
||||
client_secret: oauth.clientSecret,
|
||||
})
|
||||
const { access_token: accessToken, scope } = res
|
||||
if (scope !== 'repo' || !accessToken) {
|
||||
throw new Error(`Cannot resolve token response: '${JSON.stringify(res)}'`)
|
||||
}
|
||||
window.history.pushState({}, 'removed code', window.location.pathname.replace(/#.*$/, ''))
|
||||
this.setState({ accessToken }, () => this.saveToken(''))
|
||||
}
|
||||
} catch (err) {
|
||||
raiseError(err)
|
||||
}
|
||||
}
|
||||
React.useEffect(() => {
|
||||
useToggleShowSideBarShortcut.set(configContext.val.shortcut)
|
||||
}, [configContext.val.shortcut])
|
||||
|
||||
onInputAccessToken = (event: React.FormEvent<HTMLInputElement>) => {
|
||||
const { value } = event.currentTarget
|
||||
this.setState({
|
||||
accessToken: value,
|
||||
accessTokenHint: ACCESS_TOKEN_REGEXP.test(value) ? '' : 'This token is in unknown format.',
|
||||
})
|
||||
}
|
||||
const onInputAccessToken = React.useCallback(
|
||||
({ currentTarget: { value } }: React.FormEvent<HTMLInputElement>) => {
|
||||
useAccessToken.set(value)
|
||||
useAccessTokenHint.set(
|
||||
ACCESS_TOKEN_REGEXP.test(value) ? '' : 'This token is in unknown format.',
|
||||
)
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
onPressAccessToken = (event: React.KeyboardEvent) => {
|
||||
const { key } = event
|
||||
if (key === 'Enter') {
|
||||
this.saveToken()
|
||||
}
|
||||
}
|
||||
const onPressAccessToken = React.useCallback(({ key }: React.KeyboardEvent) => {
|
||||
if (key === 'Enter') saveToken()
|
||||
}, [])
|
||||
|
||||
saveToken = async (
|
||||
hint: State['accessTokenHint'] = (
|
||||
<span>
|
||||
<a href="#" onClick={() => window.location.reload()}>
|
||||
Reload
|
||||
</a>{' '}
|
||||
to activate!
|
||||
</span>
|
||||
),
|
||||
) => {
|
||||
const { onAccessTokenChange } = this.props
|
||||
const { accessToken } = this.state
|
||||
const saveToken = React.useCallback(async (hint?: typeof useAccessTokenHint.val) => {
|
||||
const { val: accessToken } = useAccessToken
|
||||
if (accessToken) {
|
||||
await setOne(configKeys.accessToken, accessToken)
|
||||
onAccessTokenChange(accessToken)
|
||||
this.setState({
|
||||
accessToken: '',
|
||||
accessTokenHint: hint,
|
||||
})
|
||||
configContext.set({ access_token: accessToken })
|
||||
useAccessToken.set('')
|
||||
useAccessTokenHint.set(
|
||||
hint || (
|
||||
<span>
|
||||
<a href="#" onClick={() => window.location.reload()}>
|
||||
Reload
|
||||
</a>{' '}
|
||||
to activate!
|
||||
</span>
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
clearToken = async () => {
|
||||
const { onAccessTokenChange } = this.props
|
||||
await setOne(configKeys.accessToken, '')
|
||||
onAccessTokenChange('')
|
||||
this.setState({ accessToken: '' })
|
||||
}
|
||||
const clearToken = React.useCallback(async () => {
|
||||
configContext.set({ access_token: '' })
|
||||
useAccessToken.set('')
|
||||
}, [])
|
||||
|
||||
saveShortcut = async () => {
|
||||
const { onShortcutChange } = this.props
|
||||
const { toggleShowSideBarShortcut } = this.state
|
||||
await setOne(configKeys.shortcut, toggleShowSideBarShortcut)
|
||||
const saveShortcut = React.useCallback(async () => {
|
||||
const { val: toggleShowSideBarShortcut } = useToggleShowSideBarShortcut
|
||||
configContext.set({ shortcut: toggleShowSideBarShortcut })
|
||||
if (typeof toggleShowSideBarShortcut === 'string') {
|
||||
onShortcutChange(toggleShowSideBarShortcut)
|
||||
this.setState({
|
||||
shortcutHint: 'Shortcut is saved!',
|
||||
})
|
||||
useShortcutHint.set('Shortcut is saved!')
|
||||
}
|
||||
}
|
||||
}, [useToggleShowSideBarShortcut.val])
|
||||
|
||||
onShortCutInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
const onShortCutInputKeyDown = React.useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
// Clear shortcut with backspace
|
||||
const shortcut = e.key === 'Backspace' ? '' : keyHelper.parseEvent(e)
|
||||
this.setState({ toggleShowSideBarShortcut: shortcut })
|
||||
}
|
||||
useToggleShowSideBarShortcut.set(shortcut)
|
||||
}, [])
|
||||
|
||||
showReloadHint = () => {
|
||||
this.setState({
|
||||
reloadHint: (
|
||||
const showReloadHint = React.useCallback(
|
||||
() =>
|
||||
useReloadHint.set(
|
||||
<span>
|
||||
Saved,{' '}
|
||||
<a href="#" onClick={() => window.location.reload()}>
|
||||
reload
|
||||
</a>{' '}
|
||||
to apply.
|
||||
</span>
|
||||
</span>,
|
||||
),
|
||||
})
|
||||
}
|
||||
[],
|
||||
)
|
||||
|
||||
createOnToggleChecked(
|
||||
configKey: configKeys,
|
||||
set: (value: boolean) => void,
|
||||
): (e: React.FormEvent<HTMLInputElement>) => Promise<void> {
|
||||
return async e => {
|
||||
const enabled = e.currentTarget.checked
|
||||
await setOne(configKey, enabled)
|
||||
set(enabled)
|
||||
this.showReloadHint()
|
||||
}
|
||||
}
|
||||
const { val: accessTokenHint } = useAccessTokenHint
|
||||
const { val: toggleShowSideBarShortcut } = useToggleShowSideBarShortcut
|
||||
const { val: shortcutHint } = useShortcutHint
|
||||
const { val: accessToken } = useAccessToken
|
||||
const { val: reloadHint } = useReloadHint
|
||||
|
||||
render() {
|
||||
const {
|
||||
accessTokenHint,
|
||||
toggleShowSideBarShortcut,
|
||||
shortcutHint,
|
||||
accessToken,
|
||||
reloadHint,
|
||||
varyOptions,
|
||||
} = this.state
|
||||
const { toggleShowSettings, activated } = this.props
|
||||
const hasAccessToken = Boolean(this.props.accessToken)
|
||||
return (
|
||||
<div className={'gitako-settings-bar'}>
|
||||
{activated && (
|
||||
<React.Fragment>
|
||||
<h3 className={'gitako-settings-bar-title'}>Settings</h3>
|
||||
<div className={'gitako-settings-bar-content'}>
|
||||
<div className={'shadow-shelter'} />
|
||||
<div className={'gitako-settings-bar-content-section access-token'}>
|
||||
<h4>
|
||||
Access Token
|
||||
<a href={wikiLinks.createAccessToken} target="_blank">
|
||||
(?)
|
||||
</a>
|
||||
</h4>
|
||||
{!hasAccessToken && (
|
||||
<a
|
||||
href="#"
|
||||
onClick={() => {
|
||||
// use js here to make sure redirect_uri is latest url
|
||||
const url = `https://github.com/login/oauth/authorize?client_id=${
|
||||
oauth.clientId
|
||||
}&scope=repo&redirect_uri=${encodeURIComponent(window.location.href)}`
|
||||
window.location.href = url
|
||||
}}
|
||||
>
|
||||
Create with OAuth (recommended)
|
||||
</a>
|
||||
)}
|
||||
<div className={'access-token-input-control'}>
|
||||
<input
|
||||
className={'access-token-input form-control'}
|
||||
disabled={hasAccessToken}
|
||||
placeholder={hasAccessToken ? 'Your token is saved' : 'Or input here manually'}
|
||||
value={accessToken}
|
||||
onChange={this.onInputAccessToken}
|
||||
onKeyPress={this.onPressAccessToken}
|
||||
/>
|
||||
{hasAccessToken && !accessToken ? (
|
||||
<button className={'btn'} onClick={this.clearToken}>
|
||||
Clear
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className={'btn'}
|
||||
onClick={() => this.saveToken()}
|
||||
disabled={!accessToken}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{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'}
|
||||
value={friendlyFormatShortcut(toggleShowSideBarShortcut)}
|
||||
onKeyDown={this.onShortCutInputKeyDown}
|
||||
readOnly
|
||||
/>
|
||||
<button className={'btn'} onClick={this.saveShortcut}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
{shortcutHint && <span className={'hint'}>{shortcutHint}</span>}
|
||||
</div>
|
||||
<div className={'gitako-settings-bar-content-section others'}>
|
||||
<h4>More Options</h4>
|
||||
{varyOptions.map(option => (
|
||||
<React.Fragment key={option.key}>
|
||||
<label htmlFor={option.key}>
|
||||
<input
|
||||
id={option.key}
|
||||
name={option.key}
|
||||
type={'checkbox'}
|
||||
onChange={option.onChange}
|
||||
checked={option.getValue()}
|
||||
/>
|
||||
{option.label}
|
||||
{option.wikiLink ? (
|
||||
<a href={option.wikiLink} target={'_blank'}>
|
||||
(?)
|
||||
</a>
|
||||
) : (
|
||||
option.description && (
|
||||
<span className={'description'} title={option.description}>
|
||||
(?)
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
</label>
|
||||
<br />
|
||||
</React.Fragment>
|
||||
))}
|
||||
{reloadHint && <div className={'hint'}>{reloadHint}</div>}
|
||||
</div>
|
||||
<div className={'gitako-settings-bar-content-section issue'}>
|
||||
<h4>Contact</h4>
|
||||
<a href="https://github.com/EnixCoda/Gitako/issues" target="_blank">
|
||||
Bug report / feature request.
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
<div className={'placeholder-row'}>
|
||||
<a
|
||||
className={'version'}
|
||||
href={wikiLinks.changeLog}
|
||||
target={'_blank'}
|
||||
title={'Check out new features!'}
|
||||
>
|
||||
{VERSION}
|
||||
</a>
|
||||
{activated ? (
|
||||
<Icon
|
||||
type={'chevron-down'}
|
||||
className={'hide-settings-icon'}
|
||||
onClick={toggleShowSettings}
|
||||
/>
|
||||
) : (
|
||||
<Icon type={'gear'} className={'show-settings-icon'} onClick={toggleShowSettings} />
|
||||
return (
|
||||
<>
|
||||
<h3 className={'gitako-settings-bar-title'}>Settings</h3>
|
||||
<div className={'gitako-settings-bar-content'}>
|
||||
<div className={'shadow-shelter'} />
|
||||
<div className={'gitako-settings-bar-content-section access-token'}>
|
||||
<h4>
|
||||
Access Token{' '}
|
||||
<a href={wikiLinks.createAccessToken} target="_blank">
|
||||
(?)
|
||||
</a>
|
||||
</h4>
|
||||
{!hasAccessToken && (
|
||||
<a
|
||||
href="#"
|
||||
onClick={() => {
|
||||
// use js here to make sure redirect_uri is latest url
|
||||
const url = `https://github.com/login/oauth/authorize?client_id=${
|
||||
oauth.clientId
|
||||
}&scope=repo&redirect_uri=${encodeURIComponent(window.location.href)}`
|
||||
window.location.href = url
|
||||
}}
|
||||
>
|
||||
Create with OAuth (recommended)
|
||||
</a>
|
||||
)}
|
||||
<div className={'access-token-input-control'}>
|
||||
<input
|
||||
className={'access-token-input form-control'}
|
||||
disabled={hasAccessToken}
|
||||
placeholder={hasAccessToken ? 'Your token is saved' : 'Or input here manually'}
|
||||
value={accessToken}
|
||||
onChange={onInputAccessToken}
|
||||
onKeyPress={onPressAccessToken}
|
||||
/>
|
||||
{hasAccessToken && !accessToken ? (
|
||||
<button className={'btn'} onClick={clearToken}>
|
||||
Clear
|
||||
</button>
|
||||
) : (
|
||||
<button className={'btn'} onClick={() => saveToken()} disabled={!accessToken}>
|
||||
Save
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{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'}
|
||||
value={friendlyFormatShortcut(toggleShowSideBarShortcut)}
|
||||
onKeyDown={onShortCutInputKeyDown}
|
||||
readOnly
|
||||
/>
|
||||
<button className={'btn'} onClick={saveShortcut}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
{shortcutHint && <span className={'hint'}>{shortcutHint}</span>}
|
||||
</div>
|
||||
<div className={'gitako-settings-bar-content-section others'}>
|
||||
<h4>More Options</h4>
|
||||
{moreFields.map(field => (
|
||||
<React.Fragment key={field.key}>
|
||||
<SimpleFieldInput
|
||||
field={field}
|
||||
overwrite={field.overwrite}
|
||||
onChange={showReloadHint}
|
||||
/>
|
||||
<br />
|
||||
</React.Fragment>
|
||||
))}
|
||||
|
||||
{reloadHint && <div className={'hint'}>{reloadHint}</div>}
|
||||
</div>
|
||||
<div className={'gitako-settings-bar-content-section issue'}>
|
||||
<h4>Contact</h4>
|
||||
<a href="https://github.com/EnixCoda/Gitako/issues" target="_blank">
|
||||
Bug report / feature request.
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function SettingsBar(props: Props) {
|
||||
const { toggleShowSettings, activated } = props
|
||||
return (
|
||||
<div className={'gitako-settings-bar'}>
|
||||
{activated && <SettingsBarContent />}
|
||||
<div className={'header-row'}>
|
||||
<a
|
||||
className={'version'}
|
||||
href={wikiLinks.changeLog}
|
||||
target={'_blank'}
|
||||
title={'Check out new features!'}
|
||||
>
|
||||
{VERSION}
|
||||
</a>
|
||||
{activated ? (
|
||||
<Icon
|
||||
type={'chevron-down'}
|
||||
className={'hide-settings-icon'}
|
||||
onClick={toggleShowSettings}
|
||||
/>
|
||||
) : (
|
||||
<Icon type={'gear'} className={'show-settings-icon'} onClick={toggleShowSettings} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
async function trySetUpAccessTokenWithCode() {
|
||||
try {
|
||||
const search = parseURLSearch()
|
||||
if ('code' in search) {
|
||||
const res = await JSONRequest('https://github.com/login/oauth/access_token', {
|
||||
code: search.code,
|
||||
client_id: oauth.clientId,
|
||||
client_secret: oauth.clientSecret,
|
||||
})
|
||||
const { access_token: accessToken, scope } = res
|
||||
if (scope !== 'repo' || !accessToken) {
|
||||
throw new Error(`Cannot resolve token response: '${JSON.stringify(res)}'`)
|
||||
}
|
||||
window.history.pushState({}, 'removed code', window.location.pathname.replace(/#.*$/, ''))
|
||||
return accessToken
|
||||
}
|
||||
} catch (err) {
|
||||
raiseError(err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,135 +10,92 @@ import { ConnectorState, Props } from 'driver/core/SideBar'
|
|||
import * as React from 'react'
|
||||
import { cx } from 'utils/cx'
|
||||
|
||||
class RawGitako extends React.PureComponent<Props & ConnectorState> {
|
||||
static defaultProps: Partial<Props & ConnectorState> = {
|
||||
baseSize: 260,
|
||||
shouldShow: false,
|
||||
showSettings: false,
|
||||
errorDueToAuth: false,
|
||||
accessToken: '',
|
||||
toggleShowSideBarShortcut: '',
|
||||
compressSingletonFolder: true,
|
||||
copyFileButton: true,
|
||||
copySnippetButton: true,
|
||||
disabled: false,
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { init, useListeners } = this.props
|
||||
const RawGitako: React.FC<Props & ConnectorState> = function RawGitako(props) {
|
||||
React.useEffect(() => {
|
||||
const { init, useListeners } = props
|
||||
init()
|
||||
useListeners(true)
|
||||
}
|
||||
return () => useListeners(false)
|
||||
}, [])
|
||||
|
||||
componentWillUnmount() {
|
||||
const { useListeners } = this.props
|
||||
useListeners(false)
|
||||
}
|
||||
const accessToken = props.configContext.val.access_token
|
||||
React.useEffect(() => {
|
||||
if (accessToken) {
|
||||
// reload when setting new accessToken
|
||||
if (accessToken) props.init()
|
||||
}
|
||||
}, [accessToken, props.init])
|
||||
|
||||
renderAccessDeniedError() {
|
||||
return (
|
||||
<div className={'description'}>
|
||||
<h5>Access Denied</h5>
|
||||
<p>
|
||||
Due to{' '}
|
||||
<a target="_blank" href="https://developer.github.com/v3/#rate-limiting">
|
||||
limitation of GitHub
|
||||
</a>{' '}
|
||||
or{' '}
|
||||
<a target="_blank" href="https://developer.github.com/v3/#authentication">
|
||||
auth needs
|
||||
</a>
|
||||
, Gitako needs access token to continue. Please follow the instructions in the settings
|
||||
panel below.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
const {
|
||||
errorDueToAuth,
|
||||
metaData,
|
||||
treeData,
|
||||
showSettings,
|
||||
toggleShowSettings,
|
||||
compressSingletonFolder,
|
||||
accessToken,
|
||||
} = this.props
|
||||
return (
|
||||
<div className={'gitako-side-bar-content'}>
|
||||
{metaData && <MetaBar metaData={metaData} />}
|
||||
{errorDueToAuth
|
||||
? this.renderAccessDeniedError()
|
||||
: metaData && (
|
||||
<FileExplorer
|
||||
compressSingletonFolder={compressSingletonFolder}
|
||||
toggleShowSettings={toggleShowSettings}
|
||||
metaData={metaData}
|
||||
treeData={treeData}
|
||||
freeze={showSettings}
|
||||
accessToken={accessToken}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
baseSize,
|
||||
error,
|
||||
shouldShow,
|
||||
showSettings,
|
||||
accessToken,
|
||||
compressSingletonFolder,
|
||||
copyFileButton,
|
||||
copySnippetButton,
|
||||
intelligentToggle,
|
||||
toggleShowSideBarShortcut,
|
||||
logoContainerElement,
|
||||
toggleShowSideBar,
|
||||
toggleShowSettings,
|
||||
onShortcutChange,
|
||||
onAccessTokenChange,
|
||||
setCompressSingleton,
|
||||
setCopyFile,
|
||||
setCopySnippet,
|
||||
setIntelligentToggle,
|
||||
} = this.props
|
||||
return (
|
||||
<div className={'gitako-side-bar'}>
|
||||
<Portal into={logoContainerElement}>
|
||||
<ToggleShowButton
|
||||
error={error}
|
||||
shouldShow={shouldShow}
|
||||
toggleShowSideBar={toggleShowSideBar}
|
||||
/>
|
||||
</Portal>
|
||||
<Resizable className={cx({ hidden: error || !shouldShow })} baseSize={baseSize}>
|
||||
<div className={'gitako-side-bar-body'}>
|
||||
{this.renderContent()}
|
||||
<SettingsBar
|
||||
toggleShowSettings={toggleShowSettings}
|
||||
onShortcutChange={onShortcutChange}
|
||||
onAccessTokenChange={onAccessTokenChange}
|
||||
activated={showSettings}
|
||||
accessToken={accessToken}
|
||||
toggleShowSideBarShortcut={toggleShowSideBarShortcut}
|
||||
compressSingletonFolder={compressSingletonFolder}
|
||||
copyFileButton={copyFileButton}
|
||||
copySnippetButton={copySnippetButton}
|
||||
intelligentToggle={intelligentToggle}
|
||||
setCompressSingleton={setCompressSingleton}
|
||||
setCopyFile={setCopyFile}
|
||||
setCopySnippet={setCopySnippet}
|
||||
setIntelligentToggle={setIntelligentToggle}
|
||||
/>
|
||||
const {
|
||||
errorDueToAuth,
|
||||
metaData,
|
||||
treeData,
|
||||
baseSize,
|
||||
error,
|
||||
shouldShow,
|
||||
showSettings,
|
||||
logoContainerElement,
|
||||
toggleShowSideBar,
|
||||
toggleShowSettings,
|
||||
} = props
|
||||
return (
|
||||
<div className={'gitako-side-bar'}>
|
||||
<Portal into={logoContainerElement}>
|
||||
<ToggleShowButton
|
||||
error={error}
|
||||
shouldShow={shouldShow}
|
||||
toggleShowSideBar={toggleShowSideBar}
|
||||
/>
|
||||
</Portal>
|
||||
<Resizable className={cx({ hidden: error || !shouldShow })} baseSize={baseSize}>
|
||||
<div className={'gitako-side-bar-body'}>
|
||||
<div className={'gitako-side-bar-content'}>
|
||||
{metaData && <MetaBar metaData={metaData} />}
|
||||
{errorDueToAuth
|
||||
? renderAccessDeniedError()
|
||||
: metaData && (
|
||||
<FileExplorer
|
||||
toggleShowSettings={toggleShowSettings}
|
||||
metaData={metaData}
|
||||
treeData={treeData}
|
||||
freeze={showSettings}
|
||||
accessToken={accessToken}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Resizable>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<SettingsBar toggleShowSettings={toggleShowSettings} activated={showSettings} />
|
||||
</div>
|
||||
</Resizable>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
RawGitako.defaultProps = {
|
||||
baseSize: 260,
|
||||
shouldShow: false,
|
||||
showSettings: false,
|
||||
errorDueToAuth: false,
|
||||
disabled: false,
|
||||
}
|
||||
|
||||
export const SideBar = connect(SideBarCore)(RawGitako)
|
||||
|
||||
function renderAccessDeniedError() {
|
||||
return (
|
||||
<div className={'description'}>
|
||||
<h5>Access Denied</h5>
|
||||
<p>
|
||||
Due to{' '}
|
||||
<a target="_blank" href="https://developer.github.com/v3/#rate-limiting">
|
||||
limitation of GitHub
|
||||
</a>{' '}
|
||||
or{' '}
|
||||
<a target="_blank" href="https://developer.github.com/v3/#authentication">
|
||||
auth needs
|
||||
</a>
|
||||
, Gitako needs access token to continue. Please follow the instructions in the settings
|
||||
panel below.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,11 +15,12 @@ export function ConfigsContextWrapper(props: React.PropsWithChildren<Props>) {
|
|||
configsHelper.get().then(setConfigs)
|
||||
}, [])
|
||||
const set = React.useCallback(
|
||||
() => (configs: Config) => {
|
||||
configsHelper.set(configs)
|
||||
setConfigs(configs)
|
||||
(updatedConfigs: Partial<Config>) => {
|
||||
const mergedConfigs = { ...configs, ...updatedConfigs } as Config
|
||||
configsHelper.set(mergedConfigs)
|
||||
setConfigs(mergedConfigs)
|
||||
},
|
||||
[setConfigs],
|
||||
[configs, setConfigs],
|
||||
)
|
||||
if (configs === null) return null
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -473,7 +473,7 @@
|
|||
color: #6a737d;
|
||||
}
|
||||
}
|
||||
.placeholder-row {
|
||||
.header-row {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
|
|||
|
|
@ -94,10 +94,9 @@ function link<P, S>(instance: React.Component<P, S>, sources: Sources<P, S>): Wr
|
|||
}
|
||||
|
||||
export function connect<BaseP, ExtraP>(mapping: Sources<BaseP, ExtraP>) {
|
||||
return function linkComponent<
|
||||
State,
|
||||
ComponentClass extends React.ComponentClass<BaseP & ExtraP, State>
|
||||
>(Component: ComponentClass) {
|
||||
return function linkComponent<State, ComponentType extends React.ComponentType<BaseP & ExtraP>>(
|
||||
Component: ComponentType,
|
||||
) {
|
||||
return class ConnectedComponent extends React.PureComponent<BaseP, ExtraP, State> {
|
||||
static displayName = `Connected(${Component.displayName || Component.name})`
|
||||
static defaultProps = Component.defaultProps
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { GetCreatedMethod, MethodCreator } from 'driver/connect'
|
||||
import * as ini from 'ini'
|
||||
import { Base64 } from 'js-base64'
|
||||
import { Config } from 'utils/configHelper'
|
||||
import * as DOMHelper from 'utils/DOMHelper'
|
||||
import { findNode, searchKeyToRegexps } from 'utils/general'
|
||||
import * as GitHubHelper from 'utils/GitHubHelper'
|
||||
|
|
@ -13,7 +14,6 @@ export type Props = {
|
|||
treeData?: GitHubHelper.TreeData
|
||||
metaData: GitHubHelper.MetaData
|
||||
freeze: boolean
|
||||
compressSingletonFolder: boolean
|
||||
accessToken: string | undefined
|
||||
toggleShowSettings: React.MouseEventHandler
|
||||
}
|
||||
|
|
@ -137,7 +137,7 @@ function handleParsed(root: TreeNode, parsed: Parsed) {
|
|||
}
|
||||
|
||||
export const setUpTree: BoundMethodCreator<
|
||||
[Pick<Props, 'treeData' | 'metaData' | 'compressSingletonFolder' | 'accessToken'>]
|
||||
[Pick<Props, 'treeData' | 'metaData' | 'accessToken'> & Pick<Config, 'compressSingletonFolder'>]
|
||||
> = dispatch => async ({ treeData, metaData, compressSingletonFolder, accessToken }) => {
|
||||
if (!treeData) return
|
||||
dispatch.call(setStateText, 'Rendering File List...')
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { ConfigsContextShape } from 'containers/ConfigsContext'
|
||||
import { GetCreatedMethod, MethodCreator } from 'driver/connect'
|
||||
import { Config } from 'utils/configHelper'
|
||||
import * as DOMHelper from 'utils/DOMHelper'
|
||||
import * as GitHubHelper from 'utils/GitHubHelper'
|
||||
import { MetaData, TreeData } from 'utils/GitHubHelper'
|
||||
|
|
@ -34,20 +33,9 @@ export type ConnectorState = {
|
|||
toggleShowSideBar: GetCreatedMethod<typeof toggleShowSideBar>
|
||||
toggleShowSettings: GetCreatedMethod<typeof toggleShowSettings>
|
||||
useListeners: GetCreatedMethod<typeof useListeners>
|
||||
onAccessTokenChange: GetCreatedMethod<typeof onAccessTokenChange>
|
||||
onShortcutChange: GetCreatedMethod<typeof onShortcutChange>
|
||||
setCopyFile: GetCreatedMethod<typeof setCopyFile>
|
||||
setCopySnippet: GetCreatedMethod<typeof setCopySnippet>
|
||||
setCompressSingleton: GetCreatedMethod<typeof setCompressSingleton>
|
||||
setIntelligentToggle: GetCreatedMethod<typeof setIntelligentToggle>
|
||||
} & {
|
||||
baseSize: number
|
||||
toggleShowSideBarShortcut?: string
|
||||
accessToken?: string
|
||||
} & Pick<
|
||||
Config,
|
||||
'compressSingletonFolder' | 'copyFileButton' | 'copySnippetButton' | 'intelligentToggle'
|
||||
>
|
||||
}
|
||||
|
||||
type BoundMethodCreator<Args extends any[] = []> = MethodCreator<Props, ConnectorState, Args>
|
||||
|
||||
|
|
@ -83,8 +71,6 @@ export const init: BoundMethodCreator = dispatch => async () => {
|
|||
const {
|
||||
sideBarWidth,
|
||||
access_token: accessToken,
|
||||
shortcut,
|
||||
compressSingletonFolder,
|
||||
copyFileButton,
|
||||
copySnippetButton,
|
||||
intelligentToggle,
|
||||
|
|
@ -92,12 +78,6 @@ export const init: BoundMethodCreator = dispatch => async () => {
|
|||
DOMHelper.decorateGitHubPageContent({ copyFileButton, copySnippetButton })
|
||||
dispatch.set({
|
||||
baseSize: sideBarWidth,
|
||||
accessToken,
|
||||
toggleShowSideBarShortcut: shortcut,
|
||||
compressSingletonFolder,
|
||||
copyFileButton,
|
||||
copySnippetButton,
|
||||
intelligentToggle,
|
||||
})
|
||||
|
||||
if (!metaData.branchName || !metaData.userName) return
|
||||
|
|
@ -178,7 +158,14 @@ export const handleError: BoundMethodCreator<[Error]> = dispatch => async err =>
|
|||
}
|
||||
|
||||
export const onPJAXEnd: BoundMethodCreator = dispatch => () => {
|
||||
const [{ metaData, copyFileButton, copySnippetButton, intelligentToggle }] = dispatch.get()
|
||||
const [
|
||||
{ metaData },
|
||||
{
|
||||
configContext: {
|
||||
val: { intelligentToggle, copyFileButton, copySnippetButton },
|
||||
},
|
||||
},
|
||||
] = dispatch.get()
|
||||
DOMHelper.unmountTopProgressBar()
|
||||
DOMHelper.decorateGitHubPageContent({ copyFileButton, copySnippetButton })
|
||||
const mergedMetaData = { ...metaData, ...URLHelper.parse() }
|
||||
|
|
@ -190,22 +177,25 @@ export const onPJAXEnd: BoundMethodCreator = dispatch => () => {
|
|||
}
|
||||
|
||||
export const onKeyDown: BoundMethodCreator<[KeyboardEvent]> = dispatch => e => {
|
||||
const [{ toggleShowSideBarShortcut }] = dispatch.get()
|
||||
if (toggleShowSideBarShortcut) {
|
||||
const [
|
||||
,
|
||||
{
|
||||
configContext: {
|
||||
val: { shortcut },
|
||||
},
|
||||
},
|
||||
] = dispatch.get()
|
||||
if (shortcut) {
|
||||
const keys = keyHelper.parseEvent(e)
|
||||
if (keys === toggleShowSideBarShortcut) {
|
||||
if (keys === shortcut) {
|
||||
dispatch.call(toggleShowSideBar)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const toggleShowSideBar: BoundMethodCreator = dispatch => () => {
|
||||
const [{ intelligentToggle, shouldShow }] = dispatch.get()
|
||||
const [{ shouldShow }] = dispatch.get()
|
||||
dispatch.call(setShouldShow, !shouldShow)
|
||||
|
||||
if (intelligentToggle !== null) {
|
||||
dispatch.call(setIntelligentToggle, shouldShow)
|
||||
}
|
||||
}
|
||||
|
||||
export const setShouldShow: BoundMethodCreator<
|
||||
|
|
@ -229,43 +219,9 @@ export const setShowSettings: BoundMethodCreator<
|
|||
[ConnectorState['showSettings']]
|
||||
> = dispatch => showSettings => dispatch.set({ showSettings })
|
||||
|
||||
export const onAccessTokenChange: BoundMethodCreator<
|
||||
[ConnectorState['accessToken']]
|
||||
> = dispatch => accessToken => {
|
||||
dispatch.set({ accessToken })
|
||||
// reload when setting new accessToken
|
||||
if (accessToken) {
|
||||
dispatch.call(init)
|
||||
}
|
||||
}
|
||||
|
||||
export const onShortcutChange: BoundMethodCreator<
|
||||
[ConnectorState['toggleShowSideBarShortcut']]
|
||||
> = dispatch => shortcut => dispatch.set({ toggleShowSideBarShortcut: shortcut })
|
||||
|
||||
export const setMetaData: BoundMethodCreator<[ConnectorState['metaData']]> = dispatch => metaData =>
|
||||
dispatch.set({ metaData })
|
||||
|
||||
export const setCompressSingleton: BoundMethodCreator<
|
||||
[ConnectorState['compressSingletonFolder']]
|
||||
> = dispatch => compressSingletonFolder => dispatch.set({ compressSingletonFolder })
|
||||
|
||||
export const setCopyFile: BoundMethodCreator<
|
||||
[ConnectorState['copyFileButton']]
|
||||
> = dispatch => copyFileButton => dispatch.set({ copyFileButton })
|
||||
|
||||
export const setCopySnippet: BoundMethodCreator<
|
||||
[ConnectorState['copySnippetButton']]
|
||||
> = dispatch => copySnippetButton => dispatch.set({ copySnippetButton })
|
||||
|
||||
export const setIntelligentToggle: BoundMethodCreator<
|
||||
[ConnectorState['intelligentToggle']]
|
||||
> = dispatch => intelligentToggle => {
|
||||
const [, { configContext }] = dispatch.get()
|
||||
configContext.set({ intelligentToggle })
|
||||
dispatch.set({ intelligentToggle })
|
||||
}
|
||||
|
||||
export const useListeners: BoundMethodCreator<[boolean]> = dispatch => {
|
||||
const $onPJAXEnd = () => dispatch.call(onPJAXEnd)
|
||||
const $onKeyDown = (e: KeyboardEvent) => dispatch.call(onKeyDown, e)
|
||||
|
|
|
|||
|
|
@ -54,3 +54,17 @@ export function useStates<S>(
|
|||
const [val, set] = React.useState(initialState)
|
||||
return { val, set }
|
||||
}
|
||||
|
||||
export function useAsyncMemo<T, D extends any[] | readonly any[]>(
|
||||
factory: (dependencies: D) => T | Promise<T>,
|
||||
deps: D,
|
||||
initialValue: T,
|
||||
): T {
|
||||
const firstTime = React.useRef(true)
|
||||
const state = useStates<T>(() => initialValue)
|
||||
React.useEffect(() => {
|
||||
if (firstTime.current) firstTime.current = false
|
||||
Promise.resolve(factory(deps)).then(consumed => state.set(() => consumed))
|
||||
}, deps)
|
||||
return state.val
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
{
|
||||
"files": ["src/content.tsx"],
|
||||
"compilerOptions": {
|
||||
"target": "es2016",
|
||||
"outDir": "dist",
|
||||
|
|
|
|||
Loading…
Reference in a new issue