mirror of
https://github.com/EnixCoda/Gitako.git
synced 2026-03-11 08:54:44 +00:00
Merge branch 'refactor/simplify' into develop
This commit is contained in:
commit
fad5c268e1
26 changed files with 390 additions and 362 deletions
|
|
@ -32,12 +32,20 @@ const RawFileExplorer: React.FC<Props & ConnectorState> = function RawFileExplor
|
|||
treeRoot,
|
||||
defer,
|
||||
} = props
|
||||
const { val: config } = useConfigs()
|
||||
const { value: config } = useConfigs()
|
||||
|
||||
React.useEffect(() => {
|
||||
const { setUpTree, treeRoot, metaData } = props
|
||||
setUpTree({ treeRoot, metaData, config })
|
||||
}, [setUpTree, treeRoot, config.compressSingletonFolder, config.accessToken])
|
||||
if (treeRoot) {
|
||||
setUpTree({
|
||||
treeRoot,
|
||||
metaData,
|
||||
config: {
|
||||
compressSingletonFolder: config.compressSingletonFolder,
|
||||
accessToken: config.accessToken,
|
||||
},
|
||||
})
|
||||
}
|
||||
}, [setUpTree, treeRoot, metaData, config.compressSingletonFolder, config.accessToken])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (visibleNodes?.focusedNode) focusFileExplorer()
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ const NodeItemIcon = React.memo(function NodeItemIcon({
|
|||
loading?: boolean
|
||||
}) {
|
||||
const {
|
||||
val: { icons },
|
||||
value: { icons },
|
||||
} = useConfigs()
|
||||
|
||||
const src = React.useMemo(
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export function Resizable({ baseSize, className, children }: React.PropsWithChil
|
|||
}, [width, size])
|
||||
|
||||
useCSSVariable('--gitako-width', `${size}px`)
|
||||
useDebounce(() => configContext.set({ sideBarWidth: size }), 100, [size])
|
||||
useDebounce(() => configContext.onChange({ sideBarWidth: size }), 100, [size])
|
||||
|
||||
const onResize = React.useCallback((size: number) => {
|
||||
// do NOT merge this with the above similar effect, side bar will jump otherwise
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { Portal } from 'components/Portal'
|
|||
import { Resizable } from 'components/Resizable'
|
||||
import { SettingsBar } from 'components/settings/SettingsBar'
|
||||
import { ToggleShowButton } from 'components/ToggleShowButton'
|
||||
import { useConfigs } from 'containers/ConfigsContext'
|
||||
import { connect } from 'driver/connect'
|
||||
import { SideBarCore } from 'driver/core'
|
||||
import { ConnectorState, Props } from 'driver/core/SideBar'
|
||||
|
|
@ -16,9 +15,8 @@ import {
|
|||
useGitHubAttachCopySnippetButton,
|
||||
} from 'platforms/GitHub'
|
||||
import * as React from 'react'
|
||||
import { useUpdateEffect } from 'react-use'
|
||||
import { cx } from 'utils/cx'
|
||||
import { parseURLSearch } from 'utils/general'
|
||||
import { parseURLSearch, run } from 'utils/general'
|
||||
import { loadWithPJAX, useOnPJAXDone, usePJAX } from 'utils/hooks/usePJAX'
|
||||
import { useProgressBar } from 'utils/hooks/useProgressBar'
|
||||
import * as keyHelper from 'utils/keyHelper'
|
||||
|
|
@ -26,83 +24,10 @@ import { Icon } from './Icon'
|
|||
import { Theme } from './Theme'
|
||||
|
||||
const RawGitako: React.FC<Props & ConnectorState> = function RawGitako(props) {
|
||||
const configContext = useConfigs()
|
||||
const accessToken = props.configContext.val.accessToken
|
||||
const [baseSize] = React.useState(() => configContext.val.sideBarWidth)
|
||||
|
||||
const { shrinkGitHubHeader } = configContext.val
|
||||
React.useEffect(() => {
|
||||
if (platform === GitHub) {
|
||||
const ele = document.body
|
||||
if (shrinkGitHubHeader) {
|
||||
ele.classList.add('shrink-github-header')
|
||||
} else {
|
||||
ele.classList.remove('shrink-github-header')
|
||||
}
|
||||
}
|
||||
}, [shrinkGitHubHeader])
|
||||
|
||||
const intelligentToggle = configContext.val.intelligentToggle
|
||||
React.useEffect(() => {
|
||||
const shouldShow = intelligentToggle === null ? platform.shouldShow() : intelligentToggle
|
||||
props.setShouldShow(shouldShow)
|
||||
}, [intelligentToggle, props.metaData])
|
||||
|
||||
React.useEffect(() => {
|
||||
const { init } = props
|
||||
;(async function () {
|
||||
if (!accessToken) {
|
||||
const accessToken = (await trySetUpAccessTokenWithCode()) || undefined
|
||||
configContext.set({ accessToken })
|
||||
}
|
||||
init()
|
||||
})()
|
||||
}, [])
|
||||
|
||||
React.useEffect(
|
||||
function attachKeyDown() {
|
||||
if (props.disabled || !configContext.val.shortcut) return
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
const keys = keyHelper.parseEvent(e)
|
||||
if (keys === configContext.val.shortcut) {
|
||||
props.toggleShowSideBar()
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', onKeyDown)
|
||||
return () => window.removeEventListener('keydown', onKeyDown)
|
||||
},
|
||||
[props.disabled, configContext.val.shortcut],
|
||||
)
|
||||
|
||||
const updateSideBarVisibility = React.useCallback(
|
||||
function updateSideBarVisibility() {
|
||||
if (configContext.val.intelligentToggle === null) {
|
||||
props.setShouldShow(platform.shouldShow())
|
||||
}
|
||||
},
|
||||
[props.metaData?.branchName, configContext.val.intelligentToggle],
|
||||
)
|
||||
useOnPJAXDone(updateSideBarVisibility)
|
||||
|
||||
const copyFileButton = configContext.val.copyFileButton
|
||||
useGitHubAttachCopyFileButton(copyFileButton)
|
||||
|
||||
const copySnippetButton = configContext.val.copySnippetButton
|
||||
useGitHubAttachCopySnippetButton(copySnippetButton)
|
||||
|
||||
// init again when setting new accessToken
|
||||
useUpdateEffect(() => {
|
||||
props.init()
|
||||
}, [accessToken || '']) // '' prevents duplicated requests
|
||||
|
||||
usePJAX()
|
||||
useProgressBar()
|
||||
|
||||
const {
|
||||
errorDueToAuth,
|
||||
metaData,
|
||||
treeData: treeRoot,
|
||||
treeData,
|
||||
defer,
|
||||
error,
|
||||
shouldShow,
|
||||
|
|
@ -110,7 +35,65 @@ const RawGitako: React.FC<Props & ConnectorState> = function RawGitako(props) {
|
|||
logoContainerElement,
|
||||
toggleShowSideBar,
|
||||
toggleShowSettings,
|
||||
configContext,
|
||||
} = props
|
||||
|
||||
const accessToken = configContext.value.accessToken || ''
|
||||
const [baseSize] = React.useState(() => configContext.value.sideBarWidth)
|
||||
|
||||
useShrinkGitHubHeader(configContext.value.shrinkGitHubHeader)
|
||||
|
||||
React.useEffect(() => {
|
||||
run(async function () {
|
||||
if (!accessToken) {
|
||||
const accessToken = await trySetUpAccessTokenWithCode()
|
||||
if (accessToken) configContext.onChange({ accessToken })
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
props.init()
|
||||
}, [accessToken])
|
||||
|
||||
React.useEffect(
|
||||
function attachKeyDown() {
|
||||
if (props.disabled || !configContext.value.shortcut) return
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
const keys = keyHelper.parseEvent(e)
|
||||
if (keys === configContext.value.shortcut) {
|
||||
toggleShowSideBar()
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', onKeyDown)
|
||||
return () => window.removeEventListener('keydown', onKeyDown)
|
||||
},
|
||||
[toggleShowSideBar, props.disabled, configContext.value.shortcut],
|
||||
)
|
||||
|
||||
const intelligentToggle = configContext.value.intelligentToggle
|
||||
React.useEffect(() => {
|
||||
const shouldShow = intelligentToggle === null ? platform.shouldShow() : intelligentToggle
|
||||
props.setShouldShow(shouldShow)
|
||||
}, [intelligentToggle, props.metaData])
|
||||
|
||||
const updateSideBarVisibility = React.useCallback(
|
||||
function updateSideBarVisibility() {
|
||||
if (intelligentToggle === null) {
|
||||
props.setShouldShow(platform.shouldShow())
|
||||
}
|
||||
},
|
||||
[props.metaData?.branchName, intelligentToggle],
|
||||
)
|
||||
useOnPJAXDone(updateSideBarVisibility)
|
||||
|
||||
useGitHubAttachCopyFileButton(configContext.value.copyFileButton)
|
||||
useGitHubAttachCopySnippetButton(configContext.value.copySnippetButton)
|
||||
|
||||
usePJAX()
|
||||
useProgressBar()
|
||||
|
||||
return (
|
||||
<Theme>
|
||||
<div className={'gitako-side-bar'}>
|
||||
|
|
@ -121,15 +104,13 @@ const RawGitako: React.FC<Props & ConnectorState> = function RawGitako(props) {
|
|||
</Portal>
|
||||
<Resizable className={cx({ hidden: error || !shouldShow })} baseSize={baseSize}>
|
||||
<div className={'gitako-side-bar-body'}>
|
||||
<div className={'close-side-bar-button-position'}>
|
||||
<button className={'close-side-bar-button'} onClick={toggleShowSideBar}>
|
||||
<Icon className={'action-icon'} type={'x'} />
|
||||
</button>
|
||||
</div>
|
||||
<div className={'gitako-side-bar-content'}>
|
||||
<div className={'header'}>
|
||||
{metaData ? <MetaBar metaData={metaData} /> : <div />}
|
||||
<div className={'close-side-bar-button-position'}>
|
||||
<button className={'close-side-bar-button'} onClick={toggleShowSideBar}>
|
||||
<Icon className={'action-icon'} type={'x'} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={'header'}>{metaData ? <MetaBar metaData={metaData} /> : <div />}</div>
|
||||
{errorDueToAuth ? (
|
||||
<AccessDeniedError hasToken={Boolean(accessToken)} />
|
||||
) : (
|
||||
|
|
@ -137,11 +118,11 @@ const RawGitako: React.FC<Props & ConnectorState> = function RawGitako(props) {
|
|||
<FileExplorer
|
||||
toggleShowSettings={toggleShowSettings}
|
||||
metaData={metaData}
|
||||
treeRoot={treeRoot}
|
||||
treeRoot={treeData}
|
||||
freeze={showSettings}
|
||||
accessToken={accessToken}
|
||||
loadWithPJAX={loadWithPJAX}
|
||||
config={configContext.val}
|
||||
config={configContext.value}
|
||||
defer={defer}
|
||||
/>
|
||||
)
|
||||
|
|
@ -168,6 +149,19 @@ RawGitako.defaultProps = {
|
|||
|
||||
export const SideBar = connect(SideBarCore)(RawGitako)
|
||||
|
||||
function useShrinkGitHubHeader(shrinkGitHubHeader: boolean) {
|
||||
React.useEffect(() => {
|
||||
if (platform === GitHub) {
|
||||
const target = document.body
|
||||
if (shrinkGitHubHeader) {
|
||||
target.classList.add('shrink-github-header')
|
||||
} else {
|
||||
target.classList.remove('shrink-github-header')
|
||||
}
|
||||
}
|
||||
}, [shrinkGitHubHeader])
|
||||
}
|
||||
|
||||
function AccessDeniedError({ hasToken }: { hasToken: boolean }) {
|
||||
return <AccessDeniedDescription hasToken={hasToken} />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ type Props = {
|
|||
export function SimpleToggleField({ field, onChange }: Props) {
|
||||
const { overwrite } = field
|
||||
const configContext = useConfigs()
|
||||
const value = configContext.val[field.key]
|
||||
const value = configContext.value[field.key]
|
||||
return (
|
||||
<Field
|
||||
id={field.key}
|
||||
|
|
@ -57,7 +57,7 @@ export function SimpleToggleField({ field, onChange }: Props) {
|
|||
type={'checkbox'}
|
||||
onChange={async e => {
|
||||
const enabled = e.currentTarget.checked
|
||||
configContext.set({ [field.key]: overwrite ? overwrite.onChange(enabled) : enabled })
|
||||
configContext.onChange({ [field.key]: overwrite ? overwrite.onChange(enabled) : enabled })
|
||||
if (onChange) onChange()
|
||||
}}
|
||||
checked={overwrite ? overwrite.value(value) : Boolean(value)}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ type Props = {
|
|||
export function ToggleShowButton({ error, onClick }: Props) {
|
||||
const ref = React.useRef<HTMLDivElement>(null)
|
||||
const config = useConfigs()
|
||||
const [distance, setDistance] = React.useState(config.val.toggleButtonVerticalDistance)
|
||||
const [distance, setDistance] = React.useState(config.value.toggleButtonVerticalDistance)
|
||||
const { height } = useWindowSize()
|
||||
const buttonHeight = 42
|
||||
React.useEffect(() => {
|
||||
|
|
@ -28,12 +28,12 @@ export function ToggleShowButton({ error, onClick }: Props) {
|
|||
|
||||
// updating context
|
||||
useDebounce(
|
||||
() => config.set({ toggleButtonVerticalDistance: distance }), // too slow
|
||||
() => config.onChange({ toggleButtonVerticalDistance: distance }), // too slow
|
||||
100,
|
||||
[distance],
|
||||
)
|
||||
|
||||
const toggleIconMode = config.val.toggleButtonContent
|
||||
const toggleIconMode = config.value.toggleButtonContent
|
||||
return (
|
||||
<div ref={ref} className={'gitako-toggle-show-button-wrapper'}>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { platform } from 'platforms'
|
|||
import { Gitea } from 'platforms/Gitea'
|
||||
import { Gitee } from 'platforms/Gitee'
|
||||
import * as React from 'react'
|
||||
import { useStates } from 'utils/hooks/useStates'
|
||||
import { useStateIO } from 'utils/hooks/useStateIO'
|
||||
import { SettingsSection } from './SettingsSection'
|
||||
|
||||
const ACCESS_TOKEN_REGEXP = /^[0-9a-f]+$/
|
||||
|
|
@ -14,23 +14,23 @@ type Props = {}
|
|||
|
||||
export function AccessTokenSettings(props: React.PropsWithChildren<Props>) {
|
||||
const configContext = useConfigs()
|
||||
const hasAccessToken = Boolean(configContext.val.accessToken)
|
||||
const useAccessToken = useStates('')
|
||||
const useAccessTokenHint = useStates<React.ReactNode>('')
|
||||
const focusInput = useStates(false)
|
||||
const hasAccessToken = Boolean(configContext.value.accessToken)
|
||||
const useAccessToken = useStateIO('')
|
||||
const useAccessTokenHint = useStateIO<React.ReactNode>('')
|
||||
const focusInput = useStateIO(false)
|
||||
|
||||
const { val: accessTokenHint } = useAccessTokenHint
|
||||
const { val: accessToken } = useAccessToken
|
||||
const { value: accessTokenHint } = useAccessTokenHint
|
||||
const { value: accessToken } = useAccessToken
|
||||
|
||||
React.useEffect(() => {
|
||||
// clear input when access token updates
|
||||
useAccessToken.set('')
|
||||
}, [configContext.val.accessToken])
|
||||
useAccessToken.onChange('')
|
||||
}, [configContext.value.accessToken])
|
||||
|
||||
const onInputAccessToken = React.useCallback(
|
||||
({ currentTarget: { value } }: React.FormEvent<HTMLInputElement>) => {
|
||||
useAccessToken.set(value)
|
||||
useAccessTokenHint.set(
|
||||
useAccessToken.onChange(value)
|
||||
useAccessTokenHint.onChange(
|
||||
ACCESS_TOKEN_REGEXP.test(value) ? '' : 'Gitako does not recognize the token.',
|
||||
)
|
||||
},
|
||||
|
|
@ -42,11 +42,11 @@ export function AccessTokenSettings(props: React.PropsWithChildren<Props>) {
|
|||
}, [])
|
||||
|
||||
const saveToken = React.useCallback(
|
||||
async (hint?: typeof useAccessTokenHint.val) => {
|
||||
async (hint?: typeof useAccessTokenHint.value) => {
|
||||
if (accessToken) {
|
||||
configContext.set({ accessToken })
|
||||
useAccessToken.set('')
|
||||
useAccessTokenHint.set(
|
||||
configContext.onChange({ accessToken })
|
||||
useAccessToken.onChange('')
|
||||
useAccessTokenHint.onChange(
|
||||
hint || (
|
||||
<span>
|
||||
<a href="#" onClick={() => window.location.reload()}>
|
||||
|
|
@ -79,7 +79,7 @@ export function AccessTokenSettings(props: React.PropsWithChildren<Props>) {
|
|||
{hasAccessToken ? (
|
||||
<div>
|
||||
<Text as="p">Your token has been saved.</Text>
|
||||
<Button onClick={() => configContext.set({ accessToken: '' })}>Clear</Button>
|
||||
<Button onClick={() => configContext.onChange({ accessToken: '' })}>Clear</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
|
|
@ -110,8 +110,8 @@ export function AccessTokenSettings(props: React.PropsWithChildren<Props>) {
|
|||
className={'access-token-input'}
|
||||
value={accessToken}
|
||||
placeholder="Or input here manually"
|
||||
onFocus={() => focusInput.set(true)}
|
||||
onBlur={() => focusInput.set(false)}
|
||||
onFocus={() => focusInput.onChange(true)}
|
||||
onBlur={() => focusInput.onChange(false)}
|
||||
onChange={onInputAccessToken}
|
||||
onKeyPress={onPressAccessToken}
|
||||
/>
|
||||
|
|
@ -121,7 +121,7 @@ export function AccessTokenSettings(props: React.PropsWithChildren<Props>) {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
{accessTokenHint && !focusInput.val && <span className={'hint'}>{accessTokenHint}</span>}
|
||||
{accessTokenHint && !focusInput.value && <span className={'hint'}>{accessTokenHint}</span>}
|
||||
</SettingsSection>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,11 +49,11 @@ export function FileTreeSettings(props: React.PropsWithChildren<Props>) {
|
|||
id="recursive-toggle-folder"
|
||||
options={recursiveToggleFolderOptions}
|
||||
onChange={v => {
|
||||
configContext.set({
|
||||
configContext.onChange({
|
||||
recursiveToggleFolder: v,
|
||||
})
|
||||
}}
|
||||
value={configContext.val.recursiveToggleFolder}
|
||||
value={configContext.value.recursiveToggleFolder}
|
||||
></SelectInput>
|
||||
</Field>
|
||||
<Field title="Icons" id="file-tree-icons">
|
||||
|
|
@ -61,11 +61,11 @@ export function FileTreeSettings(props: React.PropsWithChildren<Props>) {
|
|||
id="file-tree-icons"
|
||||
options={iconOptions}
|
||||
onChange={v => {
|
||||
configContext.set({
|
||||
configContext.onChange({
|
||||
icons: v,
|
||||
})
|
||||
}}
|
||||
value={configContext.val.icons}
|
||||
value={configContext.value.icons}
|
||||
/>
|
||||
</Field>
|
||||
<SimpleToggleField
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { VERSION } from 'env'
|
|||
import { platform } from 'platforms'
|
||||
import { GitHub } from 'platforms/GitHub'
|
||||
import * as React from 'react'
|
||||
import { useStates } from 'utils/hooks/useStates'
|
||||
import { useStateIO } from 'utils/hooks/useStateIO'
|
||||
import { SimpleField, SimpleToggleField } from '../SimpleToggleField'
|
||||
import { AccessTokenSettings } from './AccessTokenSettings'
|
||||
import { FileTreeSettings } from './FileTreeSettings'
|
||||
|
|
@ -27,8 +27,8 @@ type Props = {
|
|||
}
|
||||
|
||||
function SettingsBarContent() {
|
||||
const useReloadHint = useStates<React.ReactNode>('')
|
||||
const { val: reloadHint } = useReloadHint
|
||||
const useReloadHint = useStateIO<React.ReactNode>('')
|
||||
const { value: reloadHint } = useReloadHint
|
||||
|
||||
const moreFields: SimpleField[] =
|
||||
platform === GitHub
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { useConfigs } from 'containers/ConfigsContext'
|
|||
import * as React from 'react'
|
||||
import { Config } from 'utils/configHelper'
|
||||
import { friendlyFormatShortcut } from 'utils/general'
|
||||
import { useStates } from 'utils/hooks/useStates'
|
||||
import { useStateIO } from 'utils/hooks/useStateIO'
|
||||
import * as keyHelper from 'utils/keyHelper'
|
||||
import { Field } from './Field'
|
||||
import { SettingsSection } from './SettingsSection'
|
||||
|
|
@ -27,13 +27,13 @@ const toggleButtonContentOptions: Option<Config['toggleButtonContent']>[] = [
|
|||
|
||||
export function SidebarSettings(props: React.PropsWithChildren<Props>) {
|
||||
const configContext = useConfigs()
|
||||
const useToggleShowSideBarShortcut = useStates(configContext.val.shortcut)
|
||||
const { val: toggleShowSideBarShortcut } = useToggleShowSideBarShortcut
|
||||
const focused = useStates(false)
|
||||
const useToggleShowSideBarShortcut = useStateIO(configContext.value.shortcut)
|
||||
const { value: toggleShowSideBarShortcut } = useToggleShowSideBarShortcut
|
||||
const focused = useStateIO(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
useToggleShowSideBarShortcut.set(configContext.val.shortcut)
|
||||
}, [configContext.val.shortcut])
|
||||
useToggleShowSideBarShortcut.onChange(configContext.value.shortcut)
|
||||
}, [configContext.value.shortcut])
|
||||
|
||||
return (
|
||||
<SettingsSection title={'Sidebar'}>
|
||||
|
|
@ -43,24 +43,24 @@ export function SidebarSettings(props: React.PropsWithChildren<Props>) {
|
|||
id="toggle-sidebar-shortcut"
|
||||
marginRight={1}
|
||||
className={'toggle-shortcut-input'}
|
||||
onFocus={() => focused.set(true)}
|
||||
onBlur={() => focused.set(false)}
|
||||
placeholder={focused.val ? 'Press key combination' : 'Click here to set'}
|
||||
onFocus={() => focused.onChange(true)}
|
||||
onBlur={() => focused.onChange(false)}
|
||||
placeholder={focused.value ? 'Press key combination' : 'Click here to set'}
|
||||
value={friendlyFormatShortcut(toggleShowSideBarShortcut)}
|
||||
onKeyDown={React.useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
// Clear shortcut with backspace
|
||||
const shortcut = e.key === 'Backspace' ? '' : keyHelper.parseEvent(e)
|
||||
useToggleShowSideBarShortcut.set(shortcut)
|
||||
useToggleShowSideBarShortcut.onChange(shortcut)
|
||||
}, [])}
|
||||
readOnly
|
||||
/>
|
||||
{configContext.val.shortcut === toggleShowSideBarShortcut ? (
|
||||
{configContext.value.shortcut === toggleShowSideBarShortcut ? (
|
||||
<Button
|
||||
disabled={!configContext.val.shortcut}
|
||||
disabled={!configContext.value.shortcut}
|
||||
onClick={() => {
|
||||
configContext.set({ shortcut: '' })
|
||||
configContext.onChange({ shortcut: '' })
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
|
|
@ -68,9 +68,9 @@ export function SidebarSettings(props: React.PropsWithChildren<Props>) {
|
|||
) : (
|
||||
<Button
|
||||
onClick={() => {
|
||||
const { val: toggleShowSideBarShortcut } = useToggleShowSideBarShortcut
|
||||
const { value: toggleShowSideBarShortcut } = useToggleShowSideBarShortcut
|
||||
if (typeof toggleShowSideBarShortcut !== 'string') return
|
||||
configContext.set({ shortcut: toggleShowSideBarShortcut })
|
||||
configContext.onChange({ shortcut: toggleShowSideBarShortcut })
|
||||
}}
|
||||
>
|
||||
Save
|
||||
|
|
@ -83,11 +83,11 @@ export function SidebarSettings(props: React.PropsWithChildren<Props>) {
|
|||
id="toggle-button-content"
|
||||
options={toggleButtonContentOptions}
|
||||
onChange={v => {
|
||||
configContext.set({
|
||||
configContext.onChange({
|
||||
toggleButtonContent: v,
|
||||
})
|
||||
}}
|
||||
value={configContext.val.toggleButtonContent}
|
||||
value={configContext.value.toggleButtonContent}
|
||||
></SelectInput>
|
||||
</Field>
|
||||
<SimpleToggleField
|
||||
|
|
|
|||
|
|
@ -4,12 +4,7 @@ import { Config } from 'utils/configHelper'
|
|||
|
||||
type Props = {}
|
||||
|
||||
type PartialValSet<T> = {
|
||||
val: T
|
||||
set: (val: Partial<T>) => void
|
||||
}
|
||||
|
||||
type ContextShape = PartialValSet<Config>
|
||||
type ContextShape = IO<Config, Partial<Config>>
|
||||
export type ConfigsContextShape = ContextShape
|
||||
|
||||
export const ConfigsContext = React.createContext<ContextShape | null>(null)
|
||||
|
|
@ -19,7 +14,7 @@ export function ConfigsContextWrapper(props: React.PropsWithChildren<Props>) {
|
|||
React.useEffect(() => {
|
||||
configsHelper.get().then(setConfigs)
|
||||
}, [])
|
||||
const set = React.useCallback(
|
||||
const onChange = React.useCallback(
|
||||
(updatedConfigs: Partial<Config>) => {
|
||||
const mergedConfigs = { ...configs, ...updatedConfigs } as Config
|
||||
configsHelper.set(mergedConfigs)
|
||||
|
|
@ -29,7 +24,7 @@ export function ConfigsContextWrapper(props: React.PropsWithChildren<Props>) {
|
|||
)
|
||||
if (configs === null) return null
|
||||
return (
|
||||
<ConfigsContext.Provider value={{ val: configs, set }}>
|
||||
<ConfigsContext.Provider value={{ value: configs, onChange: onChange }}>
|
||||
{props.children}
|
||||
</ConfigsContext.Provider>
|
||||
)
|
||||
|
|
@ -37,10 +32,10 @@ export function ConfigsContextWrapper(props: React.PropsWithChildren<Props>) {
|
|||
|
||||
export const useConfigs = useNonNullContext(ConfigsContext)
|
||||
|
||||
function useNonNullContext<T, R extends Exclude<T, null>>(theContext: React.Context<T>): () => R {
|
||||
function useNonNullContext<T>(theContext: React.Context<T | null>): () => T {
|
||||
return () => {
|
||||
const context = React.useContext(theContext)
|
||||
if (context === null) throw new Error(`Empty context`)
|
||||
return context as R
|
||||
return context
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,13 +9,6 @@ import './content.scss'
|
|||
if (platform.resolveMeta()) {
|
||||
addMiddleware(withErrorLog)
|
||||
|
||||
async function init() {
|
||||
await injectStyles(browser.extension.getURL('content.css'))
|
||||
const SideBarElement = document.createElement('div')
|
||||
document.body.appendChild(SideBarElement)
|
||||
ReactDOM.render(<Gitako />, SideBarElement)
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init)
|
||||
} else {
|
||||
|
|
@ -23,6 +16,13 @@ if (platform.resolveMeta()) {
|
|||
}
|
||||
}
|
||||
|
||||
async function init() {
|
||||
await injectStyles(browser.extension.getURL('content.css'))
|
||||
const SideBarElement = document.createElement('div')
|
||||
document.body.appendChild(SideBarElement)
|
||||
ReactDOM.render(<Gitako />, SideBarElement)
|
||||
}
|
||||
|
||||
// injects a copy of stylesheets so that other extensions(e.g. dark reader) could read
|
||||
// resolves when style is loaded to prevent render without proper styles
|
||||
async function injectStyles(url: string) {
|
||||
|
|
|
|||
|
|
@ -40,22 +40,22 @@ function getVisibleParentNode(nodes: TreeNode[], focusedNode: TreeNode) {
|
|||
}
|
||||
}
|
||||
|
||||
type Task = () => void
|
||||
let visibleNodesGenerator: VisibleNodesGenerator
|
||||
|
||||
type BoundMethodCreator<Args extends any[] = []> = MethodCreator<Props, ConnectorState, Args>
|
||||
|
||||
export const setUpTree: BoundMethodCreator<
|
||||
[Pick<Props, 'treeRoot' | 'metaData'> & { config: Config }]
|
||||
[
|
||||
Required<Pick<Props, 'treeRoot' | 'metaData'>> & {
|
||||
config: Pick<Config, 'compressSingletonFolder' | 'accessToken'>
|
||||
},
|
||||
]
|
||||
> = dispatch => async ({ treeRoot, metaData, config }) => {
|
||||
if (!treeRoot) return
|
||||
dispatch.set({ state: 'rendering' })
|
||||
|
||||
const { compressSingletonFolder } = config
|
||||
|
||||
visibleNodesGenerator = new VisibleNodesGenerator({
|
||||
root: treeRoot,
|
||||
compress: compressSingletonFolder,
|
||||
compress: config.compressSingletonFolder,
|
||||
async getTreeData(path) {
|
||||
const { root } = await platform.getTreeData(metaData, path, false, config.accessToken)
|
||||
return root
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { ConfigsContextShape } from 'containers/ConfigsContext'
|
|||
import { GetCreatedMethod, MethodCreator } from 'driver/connect'
|
||||
import { errors, platform, platformName } from 'platforms'
|
||||
import * as DOMHelper from 'utils/DOMHelper'
|
||||
import { createPromiseQueue } from 'utils/general'
|
||||
|
||||
export type Props = {
|
||||
configContext: ConfigsContextShape
|
||||
|
|
@ -26,7 +27,6 @@ export type ConnectorState = {
|
|||
initializingPromise: Promise<void> | null
|
||||
} & {
|
||||
init: GetCreatedMethod<typeof init>
|
||||
setMetaData: GetCreatedMethod<typeof setMetaData>
|
||||
setShouldShow: GetCreatedMethod<typeof setShouldShow>
|
||||
toggleShowSideBar: GetCreatedMethod<typeof toggleShowSideBar>
|
||||
toggleShowSettings: GetCreatedMethod<typeof toggleShowSettings>
|
||||
|
|
@ -34,18 +34,10 @@ export type ConnectorState = {
|
|||
|
||||
type BoundMethodCreator<Args extends any[] = []> = MethodCreator<Props, ConnectorState, Args>
|
||||
|
||||
export const init: BoundMethodCreator = dispatch => async () => {
|
||||
const {
|
||||
state: { initializingPromise },
|
||||
} = dispatch.get()
|
||||
if (initializingPromise) await initializingPromise
|
||||
const promiseQueue = createPromiseQueue()
|
||||
|
||||
let done: any = null // cannot use type `(() => void) | null` here
|
||||
dispatch.set({
|
||||
initializingPromise: new Promise(resolve => {
|
||||
done = () => resolve()
|
||||
}),
|
||||
})
|
||||
export const init: BoundMethodCreator = dispatch => async () => {
|
||||
const leave = await promiseQueue.enter()
|
||||
|
||||
try {
|
||||
const metaData = platform.resolveMeta()
|
||||
|
|
@ -53,90 +45,85 @@ export const init: BoundMethodCreator = dispatch => async () => {
|
|||
dispatch.set({ disabled: true })
|
||||
return
|
||||
}
|
||||
const { userName, repoName, branchName } = metaData
|
||||
|
||||
DOMHelper.markGitakoReadyState(true)
|
||||
dispatch.set({
|
||||
errorDueToAuth: false,
|
||||
showSettings: false,
|
||||
logoContainerElement: DOMHelper.insertLogoMountPoint(),
|
||||
})
|
||||
dispatch.call(setMetaData, metaData)
|
||||
|
||||
const {
|
||||
props: { configContext },
|
||||
} = dispatch.get()
|
||||
const { accessToken } = configContext.val
|
||||
const { accessToken } = configContext.value
|
||||
|
||||
if (!metaData.userName || !metaData.repoName) return
|
||||
const guessDefaultBranch = 'master'
|
||||
const getTreeDataAggressively = platform.getTreeData(
|
||||
const guessDefaultBranch = 'master' // when to switch to 'main'?
|
||||
let getTreeData = platform.getTreeData(
|
||||
{
|
||||
branchName: metaData.branchName || guessDefaultBranch,
|
||||
userName: metaData.userName,
|
||||
repoName: metaData.repoName,
|
||||
branchName: branchName || guessDefaultBranch,
|
||||
userName,
|
||||
repoName,
|
||||
},
|
||||
'/',
|
||||
true,
|
||||
accessToken,
|
||||
)
|
||||
const caughtAggressiveError = getTreeDataAggressively?.catch(error => {
|
||||
// 1. the repo has no master branch
|
||||
// 2. detect branch name from DOM failed
|
||||
// 3. not very possible...
|
||||
// not handle this error immediately
|
||||
return error
|
||||
})
|
||||
let getTreeData = getTreeDataAggressively
|
||||
const metaDataFromAPI = await platform.getMetaData(
|
||||
{
|
||||
userName: metaData.userName,
|
||||
repoName: metaData.repoName,
|
||||
},
|
||||
accessToken,
|
||||
)
|
||||
const projectDefaultBranchName = metaDataFromAPI?.defaultBranchName
|
||||
const detectedBranchName = metaData.branchName
|
||||
if (
|
||||
!detectedBranchName &&
|
||||
projectDefaultBranchName &&
|
||||
projectDefaultBranchName !== metaData.branchName &&
|
||||
metaData.type !== 'pull'
|
||||
) {
|
||||
// Accessing repository's non-homepage(no branch name in URL, nor in DOM)
|
||||
// We predicted its default branch to be 'master' and sent aggressive request
|
||||
// Throw that request due to the repo do not use {defaultBranchName} as default branch
|
||||
metaData.branchName = projectDefaultBranchName
|
||||
getTreeData = platform.getTreeData(
|
||||
{
|
||||
branchName: metaData.branchName,
|
||||
userName: metaData.userName,
|
||||
repoName: metaData.repoName,
|
||||
},
|
||||
'/',
|
||||
true,
|
||||
accessToken,
|
||||
)
|
||||
getTreeData.catch(error => error) // catch it early to prevent the error being raised higher
|
||||
|
||||
const metaDataFromAPI = await platform.getMetaData({ userName, repoName }, accessToken)
|
||||
|
||||
if (branchName) {
|
||||
const safeMetaData = {
|
||||
...metaDataFromAPI,
|
||||
userName,
|
||||
repoName,
|
||||
branchName,
|
||||
}
|
||||
dispatch.set({ metaData: safeMetaData })
|
||||
getTreeData.catch(error => {
|
||||
dispatch.call(handleError, error)
|
||||
})
|
||||
} else {
|
||||
caughtAggressiveError.then(error => {
|
||||
// aggressive requested correct branch but ends in failure (e.g. project is empty)
|
||||
if (error instanceof Error) {
|
||||
dispatch.call(handleError, error)
|
||||
}
|
||||
})
|
||||
const { defaultBranchName } = metaDataFromAPI
|
||||
|
||||
if (!defaultBranchName) {
|
||||
throw new Error(`Failed resolving default branch name`)
|
||||
}
|
||||
|
||||
const safeMetaData = {
|
||||
...metaDataFromAPI,
|
||||
userName,
|
||||
repoName,
|
||||
branchName: defaultBranchName,
|
||||
}
|
||||
dispatch.set({ metaData: safeMetaData })
|
||||
|
||||
if (defaultBranchName !== guessDefaultBranch && metaData.type !== 'pull') {
|
||||
// Accessing repository's non-homepage(no branch name in URL, nor in DOM)
|
||||
// We predicted its default branch to be 'master' and sent aggressive request
|
||||
// Throw that request due to the repo do not use {defaultBranchName} as default branch
|
||||
getTreeData = platform.getTreeData(
|
||||
{
|
||||
branchName: defaultBranchName,
|
||||
userName,
|
||||
repoName,
|
||||
},
|
||||
'/',
|
||||
true,
|
||||
accessToken,
|
||||
)
|
||||
}
|
||||
}
|
||||
getTreeData
|
||||
.then(async ({ root: treeData, defer }) => {
|
||||
if (treeData) {
|
||||
dispatch.set({ treeData, defer })
|
||||
}
|
||||
})
|
||||
.catch(err => dispatch.call(handleError, err))
|
||||
Object.assign(metaData, metaDataFromAPI)
|
||||
dispatch.call(setMetaData, metaData)
|
||||
|
||||
const { root: treeData, defer } = await getTreeData
|
||||
dispatch.set({ treeData, defer })
|
||||
} catch (err) {
|
||||
dispatch.call(handleError, err)
|
||||
} finally {
|
||||
if (done) done()
|
||||
}
|
||||
|
||||
leave()
|
||||
}
|
||||
|
||||
export const handleError: BoundMethodCreator<[Error]> = dispatch => async err => {
|
||||
|
|
@ -152,7 +139,7 @@ export const handleError: BoundMethodCreator<[Error]> = dispatch => async err =>
|
|||
dispatch.set({ errorDueToAuth: true })
|
||||
} else if (err.message === errors.CONNECTION_BLOCKED) {
|
||||
const { props } = dispatch.get()
|
||||
if (props.configContext.val.accessToken) {
|
||||
if (props.configContext.value.accessToken) {
|
||||
dispatch.call(setError, `Cannot connect to ${platformName}.`)
|
||||
} else {
|
||||
dispatch.set({ errorDueToAuth: true })
|
||||
|
|
@ -174,10 +161,10 @@ export const toggleShowSideBar: BoundMethodCreator = dispatch => () => {
|
|||
dispatch.call(setShouldShow, !shouldShow)
|
||||
|
||||
const {
|
||||
val: { intelligentToggle },
|
||||
value: { intelligentToggle },
|
||||
} = configContext
|
||||
if (intelligentToggle !== null) {
|
||||
configContext.set({ intelligentToggle: !shouldShow })
|
||||
configContext.onChange({ intelligentToggle: !shouldShow })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -197,6 +184,3 @@ export const toggleShowSettings: BoundMethodCreator = dispatch => () =>
|
|||
dispatch.set(({ showSettings }) => ({
|
||||
showSettings: !showSettings,
|
||||
}))
|
||||
|
||||
export const setMetaData: BoundMethodCreator<[ConnectorState['metaData']]> = dispatch => metaData =>
|
||||
dispatch.set({ metaData })
|
||||
|
|
|
|||
8
src/global.d.ts
vendored
8
src/global.d.ts
vendored
|
|
@ -18,12 +18,16 @@ type TreeNode = {
|
|||
accessDenied?: boolean
|
||||
}
|
||||
|
||||
type IO<T> = {
|
||||
type IO<T, ChangeT = T> = {
|
||||
value: T
|
||||
onChange(value: T): void
|
||||
onChange(value: ChangeT): void
|
||||
}
|
||||
|
||||
type Override<Original, Incoming> = Omit<Original, keyof Incoming> & Incoming
|
||||
type MakeOptional<Original, keys extends keyof Original> = Override<
|
||||
Original,
|
||||
Partial<Pick<Original, keys>>
|
||||
>
|
||||
|
||||
type VoidFN<T> = (payload: T) => void
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { raiseError } from 'analytics'
|
||||
|
||||
export function parse(): Partial<MetaData> & { path: string[] } {
|
||||
export function parse(): Partial<Pick<MetaData, 'userName' | 'repoName' | 'type'>> & {
|
||||
path: string[]
|
||||
} {
|
||||
const { pathname } = window.location
|
||||
let [
|
||||
,
|
||||
|
|
@ -13,12 +15,13 @@ export function parse(): Partial<MetaData> & { path: string[] } {
|
|||
return {
|
||||
userName,
|
||||
repoName,
|
||||
branchName: undefined,
|
||||
type,
|
||||
path,
|
||||
}
|
||||
}
|
||||
|
||||
// not working well with non-branch blob
|
||||
// cannot handle '/' split branch name, should not use when possibly in branch page
|
||||
export function parseSHA() {
|
||||
const { type, path } = parse()
|
||||
return type === 'blob' || type === 'tree' ? path[0] : undefined
|
||||
|
|
@ -30,11 +33,15 @@ export function isInPullPage() {
|
|||
}
|
||||
|
||||
function isCommitPath(path: string[]) {
|
||||
return isCompleteCommitSHA(path[0])
|
||||
return path[0] ? isCompleteCommitSHA(path[0]) : false
|
||||
}
|
||||
|
||||
function isCompleteCommitSHA(sha?: string) {
|
||||
return typeof sha === 'string' && /^[abcdef0-9]{40}$/i.test(sha)
|
||||
function isCompleteCommitSHA(sha: string) {
|
||||
return /^[abcdef0-9]{40}$/i.test(sha)
|
||||
}
|
||||
|
||||
function isPossiblyCommitSHA(sha: string) {
|
||||
return /^[abcdef0-9]+$/i.test(sha)
|
||||
}
|
||||
|
||||
export function getCurrentPath(branchName = '') {
|
||||
|
|
@ -49,22 +56,29 @@ export function getCurrentPath(branchName = '') {
|
|||
if (path[0] === 'HEAD') path.shift()
|
||||
else {
|
||||
const splitBranchName = branchName.split('/')
|
||||
while (splitBranchName.length) {
|
||||
if (
|
||||
splitBranchName[0] === path[0] ||
|
||||
// Keep consuming as their heads are same
|
||||
(splitBranchName.length === 1 && splitBranchName[0].startsWith(path[0]))
|
||||
// This happens when visiting URLs like /blob/{commitSHA}/path/to/file
|
||||
// and {commitSHA} is shorter than we got from DOM
|
||||
) {
|
||||
splitBranchName.shift()
|
||||
path.shift()
|
||||
} else {
|
||||
raiseError(new Error(`branch name and path prefix not match`), {
|
||||
branchName,
|
||||
path: parse().path,
|
||||
})
|
||||
return []
|
||||
if (
|
||||
splitBranchName.length === 1 &&
|
||||
path.length > 0 &&
|
||||
isPossiblyCommitSHA(splitBranchName[0]) &&
|
||||
isPossiblyCommitSHA(path[0]) &&
|
||||
(splitBranchName[0].startsWith(path[0]) || path[0].startsWith(splitBranchName[0]))
|
||||
// This happens when visiting URLs like /blob/{commitSHA}/path/to/file
|
||||
// and {commitSHA} does not match the one got from DOM
|
||||
) {
|
||||
splitBranchName.shift()
|
||||
path.shift()
|
||||
} else {
|
||||
while (splitBranchName.length) {
|
||||
if (splitBranchName[0] === path[0]) {
|
||||
splitBranchName.shift()
|
||||
path.shift()
|
||||
} else {
|
||||
raiseError(new Error(`branch name and path prefix not match`), {
|
||||
branchName,
|
||||
path: parse().path,
|
||||
})
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,26 +91,30 @@ export const GitHub: Platform = {
|
|||
return null
|
||||
}
|
||||
|
||||
let detectedBranchName
|
||||
let branchName
|
||||
if (URLHelper.isInPullPage()) {
|
||||
detectedBranchName = DOMHelper.getIssueTitle()
|
||||
branchName = DOMHelper.getIssueTitle()
|
||||
} else if (
|
||||
DOMHelper.isInCodePage() &&
|
||||
!['releases', 'tags'].includes(URLHelper.parse().type || '') // resolve sentry issue #-CK
|
||||
) {
|
||||
// not working well with non-branch blob
|
||||
// cannot handle '/' split branch name, should not use when possibly on branch page
|
||||
detectedBranchName = DOMHelper.getCurrentBranch() || URLHelper.parseSHA()
|
||||
branchName = DOMHelper.getCurrentBranch() || URLHelper.parseSHA()
|
||||
}
|
||||
|
||||
const { userName, repoName, type } = URLHelper.parse()
|
||||
if (!userName || !repoName) {
|
||||
return null
|
||||
}
|
||||
|
||||
const metaData = {
|
||||
...URLHelper.parse(),
|
||||
branchName: detectedBranchName,
|
||||
} as MetaData
|
||||
userName,
|
||||
repoName,
|
||||
type,
|
||||
branchName,
|
||||
}
|
||||
return metaData
|
||||
},
|
||||
async getMetaData(partialMetaData, accessToken) {
|
||||
const { userName, repoName } = partialMetaData
|
||||
async getMetaData({ userName, repoName }, accessToken) {
|
||||
const data = await API.getRepoMeta(userName, repoName, accessToken)
|
||||
return {
|
||||
userUrl: data?.owner?.html_url,
|
||||
|
|
@ -156,9 +160,9 @@ export const GitHub: Platform = {
|
|||
path: item.filename || '',
|
||||
type: 'blob',
|
||||
name: item.filename?.replace(/^.*\//, '') || '',
|
||||
url: `https://${window.location.host}/${metaData.userName}/${
|
||||
metaData.repoName
|
||||
}/pull/${pullId}/files${window.location.search}#${creator(item.filename) || ''}`,
|
||||
url: `https://${window.location.host}/${userName}/${repoName}/pull/${pullId}/files${
|
||||
window.location.search
|
||||
}#${creator(item.filename) || ''}`,
|
||||
sha: item.sha,
|
||||
}))
|
||||
|
||||
|
|
@ -198,32 +202,21 @@ export const GitHub: Platform = {
|
|||
name: item.path?.replace(/^.*\//, '') || '',
|
||||
url:
|
||||
item.url && item.type && item.path
|
||||
? getUrlForRedirect(
|
||||
metaData.userName,
|
||||
metaData.repoName,
|
||||
metaData.branchName,
|
||||
item.type,
|
||||
item.path,
|
||||
)
|
||||
? getUrlForRedirect(userName, repoName, branchName, item.type, item.path)
|
||||
: undefined,
|
||||
contents: item.type === 'tree' ? [] : undefined,
|
||||
sha: item.sha,
|
||||
})),
|
||||
)
|
||||
|
||||
const gitModules = root.contents?.find(item => item.name === '.gitmodules')
|
||||
if (gitModules) {
|
||||
if (metaData.userName && metaData.repoName && gitModules.sha) {
|
||||
const blobData = await API.getBlobData(
|
||||
metaData.userName,
|
||||
metaData.repoName,
|
||||
gitModules.sha,
|
||||
accessToken,
|
||||
)
|
||||
const gitModules = root.contents?.find(
|
||||
item => item.type === 'blob' && item.name === '.gitmodules',
|
||||
)
|
||||
if (gitModules?.sha) {
|
||||
const blobData = await API.getBlobData(userName, repoName, gitModules.sha, accessToken)
|
||||
|
||||
if (blobData && blobData.encoding === 'base64' && blobData.content) {
|
||||
await resolveGitModules(root, Base64.decode(blobData.content))
|
||||
}
|
||||
if (blobData && blobData.encoding === 'base64' && blobData.content) {
|
||||
await resolveGitModules(root, Base64.decode(blobData.content))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -82,15 +82,23 @@ export const Gitea: Platform = {
|
|||
return null
|
||||
}
|
||||
|
||||
let detectedBranchName
|
||||
let branchName
|
||||
if (DOMHelper.isInCodePage()) {
|
||||
detectedBranchName = DOMHelper.getCurrentBranch() || URLHelper.parseSHA()
|
||||
branchName = DOMHelper.getCurrentBranch() || URLHelper.parseSHA()
|
||||
}
|
||||
|
||||
const { userName, repoName, type } = URLHelper.parse()
|
||||
if (!userName || !repoName) {
|
||||
return null
|
||||
}
|
||||
|
||||
const metaData = {
|
||||
...URLHelper.parse(),
|
||||
branchName: detectedBranchName,
|
||||
} as MetaData
|
||||
userName,
|
||||
repoName,
|
||||
type,
|
||||
branchName,
|
||||
}
|
||||
|
||||
return metaData
|
||||
},
|
||||
async getMetaData(partialMetaData, accessToken) {
|
||||
|
|
|
|||
|
|
@ -78,17 +78,24 @@ export const Gitee: Platform = {
|
|||
return null
|
||||
}
|
||||
|
||||
let detectedBranchName
|
||||
let branchName
|
||||
if (DOMHelper.isInCodePage()) {
|
||||
// not working well with non-branch blob
|
||||
// cannot handle '/' split branch name, should not use when possibly on branch page
|
||||
detectedBranchName = DOMHelper.getCurrentBranch() || URLHelper.parseSHA()
|
||||
branchName = DOMHelper.getCurrentBranch() || URLHelper.parseSHA()
|
||||
}
|
||||
|
||||
const { userName, repoName, type } = URLHelper.parse()
|
||||
if (!userName || !repoName) {
|
||||
return null
|
||||
}
|
||||
|
||||
const metaData = {
|
||||
...URLHelper.parse(),
|
||||
branchName: detectedBranchName,
|
||||
} as MetaData
|
||||
userName,
|
||||
repoName,
|
||||
type,
|
||||
branchName,
|
||||
}
|
||||
return metaData
|
||||
},
|
||||
async getMetaData(partialMetaData, accessToken) {
|
||||
|
|
|
|||
3
src/platforms/platform.d.ts
vendored
3
src/platforms/platform.d.ts
vendored
|
|
@ -1,6 +1,7 @@
|
|||
type Platform = {
|
||||
isEnterprise(): boolean
|
||||
resolveMeta(): MetaData | null
|
||||
// branch name might not be available when resolving from DOM and URL
|
||||
resolveMeta(): MakeOptional<MetaData, 'branchName'> | null
|
||||
getMetaData(
|
||||
metaData: Pick<MetaData, 'userName' | 'repoName'>,
|
||||
accessToken?: string,
|
||||
|
|
|
|||
|
|
@ -303,7 +303,9 @@ $minimal-z-index: max($github-header-z-index, $github-pull-request-float-header-
|
|||
font-size: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.#{$name}-side-bar-body {
|
||||
$button-size: 32px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
|
|
@ -334,6 +336,37 @@ $minimal-z-index: max($github-header-z-index, $github-pull-request-float-header-
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.close-side-bar-button-position {
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
top: 6px;
|
||||
z-index: 1; // prevent being covered by following elements
|
||||
|
||||
.close-side-bar-button {
|
||||
@include icon-button;
|
||||
@include button-color;
|
||||
width: $button-size;
|
||||
height: $button-size;
|
||||
border-radius: $button-size;
|
||||
|
||||
// feedback to click should be instant
|
||||
&:not(:active) {
|
||||
transition: background linear 0.3s;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
color: var(--gitako-icon-tertiary);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
text-align: center;
|
||||
.octicon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.#{$name}-side-bar-content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
|
|
@ -342,7 +375,6 @@ $minimal-z-index: max($github-header-z-index, $github-pull-request-float-header-
|
|||
min-height: 0; // make content shrinkable
|
||||
|
||||
.header {
|
||||
$button-size: 32px;
|
||||
position: relative;
|
||||
|
||||
.meta-bar {
|
||||
|
|
@ -364,38 +396,6 @@ $minimal-z-index: max($github-header-z-index, $github-pull-request-float-header-
|
|||
background-color: var(--gitako-branch-name-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.close-side-bar-button-position {
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
.close-side-bar-button {
|
||||
@include icon-button;
|
||||
@include button-color;
|
||||
width: $button-size;
|
||||
height: $button-size;
|
||||
border-radius: $button-size;
|
||||
|
||||
// feedback to click should be instant
|
||||
&:not(:active) {
|
||||
transition: background linear 0.3s;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
color: var(--gitako-icon-tertiary);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
text-align: center;
|
||||
.octicon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
|
|
@ -478,9 +478,9 @@ $minimal-z-index: max($github-header-z-index, $github-pull-request-float-header-
|
|||
}
|
||||
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
color: var(--gitako-text-disabled);
|
||||
}
|
||||
pointer-events: none;
|
||||
color: var(--gitako-text-disabled);
|
||||
}
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export enum configKeys {
|
|||
const defaultConfigs: Config = {
|
||||
sideBarWidth: 260,
|
||||
shortcut: undefined,
|
||||
accessToken: undefined,
|
||||
accessToken: '',
|
||||
compressSingletonFolder: true,
|
||||
copyFileButton: true,
|
||||
copySnippetButton: true,
|
||||
|
|
|
|||
|
|
@ -183,3 +183,23 @@ export function withEffect<Method extends (...args: any[]) => any>(
|
|||
return returnValue
|
||||
}
|
||||
}
|
||||
|
||||
export function run<T>(fn: () => T) {
|
||||
return fn()
|
||||
}
|
||||
|
||||
export function createPromiseQueue() {
|
||||
let promise: Promise<void>
|
||||
return {
|
||||
async enter() {
|
||||
let leave: () => void
|
||||
const current = new Promise<void>(resolve => (leave = () => resolve()))
|
||||
|
||||
const lastPromise = promise
|
||||
promise = current!
|
||||
if (lastPromise) await lastPromise
|
||||
|
||||
return leave!
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import * as React from 'react'
|
||||
import { useStates } from './useStates'
|
||||
import { useStateIO } from './useStateIO'
|
||||
|
||||
export function useAsyncMemo<T, D extends any[] | readonly any[]>(
|
||||
factory: (dependencies: D) => T | Promise<T>,
|
||||
|
|
@ -7,10 +7,10 @@ export function useAsyncMemo<T, D extends any[] | readonly any[]>(
|
|||
initialValue: T,
|
||||
): T {
|
||||
const firstTime = React.useRef(true)
|
||||
const state = useStates<T>(() => initialValue)
|
||||
const state = useStateIO<T>(() => initialValue)
|
||||
React.useEffect(() => {
|
||||
if (firstTime.current) firstTime.current = false
|
||||
Promise.resolve(factory(deps)).then(consumed => state.set(() => consumed))
|
||||
Promise.resolve(factory(deps)).then(consumed => state.onChange(() => consumed))
|
||||
}, deps)
|
||||
return state.val
|
||||
return state.value
|
||||
}
|
||||
|
|
|
|||
11
src/utils/hooks/useStateIO.ts
Normal file
11
src/utils/hooks/useStateIO.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import * as React from 'react'
|
||||
|
||||
export function useStateIO<S>(
|
||||
initialState: S | (() => S),
|
||||
): {
|
||||
value: S
|
||||
onChange: React.Dispatch<React.SetStateAction<S>>
|
||||
} {
|
||||
const [value, onChange] = React.useState(initialState)
|
||||
return { value, onChange }
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import * as React from 'react'
|
||||
|
||||
export function useStates<S>(
|
||||
initialState: S | (() => S),
|
||||
): {
|
||||
val: S
|
||||
set: React.Dispatch<React.SetStateAction<S>>
|
||||
} {
|
||||
const [val, set] = React.useState(initialState)
|
||||
return { val, set }
|
||||
}
|
||||
Loading…
Reference in a new issue