This commit is contained in:
2025-12-01 17:21:38 +08:00
parent 32fee2b8ab
commit fab8c13cb3
7511 changed files with 996300 additions and 0 deletions

View File

@@ -0,0 +1,235 @@
'use client'
import React, { useCallback, useState } from 'react'
import Modal from '@/app/components/base/modal'
import type { Item } from '@/app/components/base/select'
import type { InstallState } from '@/app/components/plugins/types'
import { useGitHubReleases } from '../hooks'
import { convertRepoToUrl, parseGitHubUrl } from '../utils'
import type { PluginDeclaration, UpdateFromGitHubPayload } from '../../types'
import { InstallStepFromGitHub } from '../../types'
import Toast from '@/app/components/base/toast'
import SetURL from './steps/setURL'
import SelectPackage from './steps/selectPackage'
import Installed from '../base/installed'
import Loaded from './steps/loaded'
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
import { useTranslation } from 'react-i18next'
import useRefreshPluginList from '../hooks/use-refresh-plugin-list'
import cn from '@/utils/classnames'
import useHideLogic from '../hooks/use-hide-logic'
const i18nPrefix = 'plugin.installFromGitHub'
type InstallFromGitHubProps = {
updatePayload?: UpdateFromGitHubPayload
onClose: () => void
onSuccess: () => void
}
const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, onClose, onSuccess }) => {
const { t } = useTranslation()
const { getIconUrl } = useGetIcon()
const { fetchReleases } = useGitHubReleases()
const { refreshPluginList } = useRefreshPluginList()
const {
modalClassName,
foldAnimInto,
setIsInstalling,
handleStartToInstall,
} = useHideLogic(onClose)
const [state, setState] = useState<InstallState>({
step: updatePayload ? InstallStepFromGitHub.selectPackage : InstallStepFromGitHub.setUrl,
repoUrl: updatePayload?.originalPackageInfo?.repo
? convertRepoToUrl(updatePayload.originalPackageInfo.repo)
: '',
selectedVersion: '',
selectedPackage: '',
releases: updatePayload ? updatePayload.originalPackageInfo.releases : [],
})
const [uniqueIdentifier, setUniqueIdentifier] = useState<string | null>(null)
const [manifest, setManifest] = useState<PluginDeclaration | null>(null)
const [errorMsg, setErrorMsg] = useState<string | null>(null)
const versions: Item[] = state.releases.map(release => ({
value: release.tag_name,
name: release.tag_name,
}))
const packages: Item[] = state.selectedVersion
? (state.releases
.find(release => release.tag_name === state.selectedVersion)
?.assets
.map(asset => ({
value: asset.name,
name: asset.name,
})) || [])
: []
const getTitle = useCallback(() => {
if (state.step === InstallStepFromGitHub.installed)
return t(`${i18nPrefix}.installedSuccessfully`)
if (state.step === InstallStepFromGitHub.installFailed)
return t(`${i18nPrefix}.installFailed`)
return updatePayload ? t(`${i18nPrefix}.updatePlugin`) : t(`${i18nPrefix}.installPlugin`)
}, [state.step, t, updatePayload])
const handleUrlSubmit = async () => {
const { isValid, owner, repo } = parseGitHubUrl(state.repoUrl)
if (!isValid || !owner || !repo) {
Toast.notify({
type: 'error',
message: t('plugin.error.inValidGitHubUrl'),
})
return
}
try {
const fetchedReleases = await fetchReleases(owner, repo)
if (fetchedReleases.length > 0) {
setState(prevState => ({
...prevState,
releases: fetchedReleases,
step: InstallStepFromGitHub.selectPackage,
}))
}
else {
Toast.notify({
type: 'error',
message: t('plugin.error.noReleasesFound'),
})
}
}
catch {
Toast.notify({
type: 'error',
message: t('plugin.error.fetchReleasesError'),
})
}
}
const handleError = (e: any, isInstall: boolean) => {
const message = e?.response?.message || t('plugin.installModal.installFailedDesc')
setErrorMsg(message)
setState(prevState => ({ ...prevState, step: isInstall ? InstallStepFromGitHub.installFailed : InstallStepFromGitHub.uploadFailed }))
}
const handleUploaded = async (GitHubPackage: any) => {
try {
const icon = await getIconUrl(GitHubPackage.manifest.icon)
setManifest({
...GitHubPackage.manifest,
icon,
})
setUniqueIdentifier(GitHubPackage.uniqueIdentifier)
setState(prevState => ({ ...prevState, step: InstallStepFromGitHub.readyToInstall }))
}
catch (e) {
handleError(e, false)
}
}
const handleUploadFail = useCallback((errorMsg: string) => {
setErrorMsg(errorMsg)
setState(prevState => ({ ...prevState, step: InstallStepFromGitHub.uploadFailed }))
}, [])
const handleInstalled = useCallback((notRefresh?: boolean) => {
setState(prevState => ({ ...prevState, step: InstallStepFromGitHub.installed }))
if (!notRefresh)
refreshPluginList(manifest)
setIsInstalling(false)
onSuccess()
}, [manifest, onSuccess, refreshPluginList, setIsInstalling])
const handleFailed = useCallback((errorMsg?: string) => {
setState(prevState => ({ ...prevState, step: InstallStepFromGitHub.installFailed }))
setIsInstalling(false)
if (errorMsg)
setErrorMsg(errorMsg)
}, [setIsInstalling])
const handleBack = () => {
setState((prevState) => {
switch (prevState.step) {
case InstallStepFromGitHub.selectPackage:
return { ...prevState, step: InstallStepFromGitHub.setUrl }
case InstallStepFromGitHub.readyToInstall:
return { ...prevState, step: InstallStepFromGitHub.selectPackage }
default:
return prevState
}
})
}
return (
<Modal
isShow={true}
onClose={foldAnimInto}
className={cn(modalClassName, `shadows-shadow-xl flex min-w-[560px] flex-col items-start rounded-2xl border-[0.5px]
border-components-panel-border bg-components-panel-bg p-0`)}
closable
>
<div className='flex items-start gap-2 self-stretch pb-3 pl-6 pr-14 pt-6'>
<div className='flex grow flex-col items-start gap-1'>
<div className='title-2xl-semi-bold self-stretch text-text-primary'>
{getTitle()}
</div>
<div className='system-xs-regular self-stretch text-text-tertiary'>
{!([InstallStepFromGitHub.uploadFailed, InstallStepFromGitHub.installed, InstallStepFromGitHub.installFailed].includes(state.step)) && t('plugin.installFromGitHub.installNote')}
</div>
</div>
</div>
{([InstallStepFromGitHub.uploadFailed, InstallStepFromGitHub.installed, InstallStepFromGitHub.installFailed].includes(state.step))
? <Installed
payload={manifest}
isFailed={[InstallStepFromGitHub.uploadFailed, InstallStepFromGitHub.installFailed].includes(state.step)}
errMsg={errorMsg}
onCancel={onClose}
/>
: <div className={`flex flex-col items-start justify-center self-stretch px-6 py-3 ${state.step === InstallStepFromGitHub.installed ? 'gap-2' : 'gap-4'}`}>
{state.step === InstallStepFromGitHub.setUrl && (
<SetURL
repoUrl={state.repoUrl}
onChange={value => setState(prevState => ({ ...prevState, repoUrl: value }))}
onNext={handleUrlSubmit}
onCancel={onClose}
/>
)}
{state.step === InstallStepFromGitHub.selectPackage && (
<SelectPackage
updatePayload={updatePayload!}
repoUrl={state.repoUrl}
selectedVersion={state.selectedVersion}
versions={versions}
onSelectVersion={item => setState(prevState => ({ ...prevState, selectedVersion: item.value as string }))}
selectedPackage={state.selectedPackage}
packages={packages}
onSelectPackage={item => setState(prevState => ({ ...prevState, selectedPackage: item.value as string }))}
onUploaded={handleUploaded}
onFailed={handleUploadFail}
onBack={handleBack}
/>
)}
{state.step === InstallStepFromGitHub.readyToInstall && (
<Loaded
updatePayload={updatePayload!}
uniqueIdentifier={uniqueIdentifier!}
payload={manifest as any}
repoUrl={state.repoUrl}
selectedVersion={state.selectedVersion}
selectedPackage={state.selectedPackage}
onBack={handleBack}
onStartToInstall={handleStartToInstall}
onInstalled={handleInstalled}
onFailed={handleFailed}
/>
)}
</div>}
</Modal>
)
}
export default InstallFromGitHub

View File

@@ -0,0 +1,179 @@
'use client'
import React, { useEffect } from 'react'
import Button from '@/app/components/base/button'
import { type Plugin, type PluginDeclaration, TaskStatus, type UpdateFromGitHubPayload } from '../../../types'
import Card from '../../../card'
import { pluginManifestToCardPluginProps } from '../../utils'
import { useTranslation } from 'react-i18next'
import { updateFromGitHub } from '@/service/plugins'
import { useInstallPackageFromGitHub } from '@/service/use-plugins'
import { RiLoader2Line } from '@remixicon/react'
import { usePluginTaskList } from '@/service/use-plugins'
import checkTaskStatus from '../../base/check-task-status'
import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed'
import { parseGitHubUrl } from '../../utils'
import Version from '../../base/version'
type LoadedProps = {
updatePayload: UpdateFromGitHubPayload
uniqueIdentifier: string
payload: PluginDeclaration | Plugin
repoUrl: string
selectedVersion: string
selectedPackage: string
onBack: () => void
onStartToInstall?: () => void
onInstalled: (notRefresh?: boolean) => void
onFailed: (message?: string) => void
}
const i18nPrefix = 'plugin.installModal'
const Loaded: React.FC<LoadedProps> = ({
updatePayload,
uniqueIdentifier,
payload,
repoUrl,
selectedVersion,
selectedPackage,
onBack,
onStartToInstall,
onInstalled,
onFailed,
}) => {
const { t } = useTranslation()
const toInstallVersion = payload.version
const pluginId = (payload as Plugin).plugin_id
const { installedInfo, isLoading } = useCheckInstalled({
pluginIds: [pluginId],
enabled: !!pluginId,
})
const installedInfoPayload = installedInfo?.[pluginId]
const installedVersion = installedInfoPayload?.installedVersion
const hasInstalled = !!installedVersion
const [isInstalling, setIsInstalling] = React.useState(false)
const { mutateAsync: installPackageFromGitHub } = useInstallPackageFromGitHub()
const { handleRefetch } = usePluginTaskList(payload.category)
const { check } = checkTaskStatus()
useEffect(() => {
if (hasInstalled && uniqueIdentifier === installedInfoPayload.uniqueIdentifier)
onInstalled()
}, [hasInstalled])
const handleInstall = async () => {
if (isInstalling) return
setIsInstalling(true)
onStartToInstall?.()
try {
const { owner, repo } = parseGitHubUrl(repoUrl)
let taskId
let isInstalled
if (updatePayload) {
const { all_installed, task_id } = await updateFromGitHub(
`${owner}/${repo}`,
selectedVersion,
selectedPackage,
updatePayload.originalPackageInfo.id,
uniqueIdentifier,
)
taskId = task_id
isInstalled = all_installed
}
else {
if (hasInstalled) {
const {
all_installed,
task_id,
} = await updateFromGitHub(
`${owner}/${repo}`,
selectedVersion,
selectedPackage,
installedInfoPayload.uniqueIdentifier,
uniqueIdentifier,
)
taskId = task_id
isInstalled = all_installed
}
else {
const { all_installed, task_id } = await installPackageFromGitHub({
repoUrl: `${owner}/${repo}`,
selectedVersion,
selectedPackage,
uniqueIdentifier,
})
taskId = task_id
isInstalled = all_installed
}
}
if (isInstalled) {
onInstalled()
return
}
handleRefetch()
const { status, error } = await check({
taskId,
pluginUniqueIdentifier: uniqueIdentifier,
})
if (status === TaskStatus.failed) {
onFailed(error)
return
}
onInstalled(true)
}
catch (e) {
if (typeof e === 'string') {
onFailed(e)
return
}
onFailed()
}
finally {
setIsInstalling(false)
}
}
return (
<>
<div className='system-md-regular text-text-secondary'>
<p>{t(`${i18nPrefix}.readyToInstall`)}</p>
</div>
<div className='flex flex-wrap content-start items-start gap-1 self-stretch rounded-2xl bg-background-section-burn p-2'>
<Card
className='w-full'
payload={pluginManifestToCardPluginProps(payload as PluginDeclaration)}
titleLeft={!isLoading && <Version
hasInstalled={hasInstalled}
installedVersion={installedVersion}
toInstallVersion={toInstallVersion}
/>}
/>
</div>
<div className='mt-4 flex items-center justify-end gap-2 self-stretch'>
{!isInstalling && (
<Button variant='secondary' className='min-w-[72px]' onClick={onBack}>
{t('plugin.installModal.back')}
</Button>
)}
<Button
variant='primary'
className='flex min-w-[72px] space-x-0.5'
onClick={handleInstall}
disabled={isInstalling || isLoading}
>
{isInstalling && <RiLoader2Line className='h-4 w-4 animate-spin-slow' />}
<span>{t(`${i18nPrefix}.${isInstalling ? 'installing' : 'install'}`)}</span>
</Button>
</div>
</>
)
}
export default Loaded

View File

@@ -0,0 +1,127 @@
'use client'
import React from 'react'
import type { Item } from '@/app/components/base/select'
import { PortalSelect } from '@/app/components/base/select'
import Button from '@/app/components/base/button'
import type { PluginDeclaration, UpdateFromGitHubPayload } from '../../../types'
import { useTranslation } from 'react-i18next'
import { useGitHubUpload } from '../../hooks'
const i18nPrefix = 'plugin.installFromGitHub'
type SelectPackageProps = {
updatePayload: UpdateFromGitHubPayload
repoUrl: string
selectedVersion: string
versions: Item[]
onSelectVersion: (item: Item) => void
selectedPackage: string
packages: Item[]
onSelectPackage: (item: Item) => void
onUploaded: (result: {
uniqueIdentifier: string
manifest: PluginDeclaration
}) => void
onFailed: (errorMsg: string) => void
onBack: () => void
}
const SelectPackage: React.FC<SelectPackageProps> = ({
updatePayload,
repoUrl,
selectedVersion,
versions,
onSelectVersion,
selectedPackage,
packages,
onSelectPackage,
onUploaded,
onFailed,
onBack,
}) => {
const { t } = useTranslation()
const isEdit = Boolean(updatePayload)
const [isUploading, setIsUploading] = React.useState(false)
const { handleUpload } = useGitHubUpload()
const handleUploadPackage = async () => {
if (isUploading) return
setIsUploading(true)
try {
const repo = repoUrl.replace('https://github.com/', '')
await handleUpload(repo, selectedVersion, selectedPackage, (GitHubPackage) => {
onUploaded({
uniqueIdentifier: GitHubPackage.unique_identifier,
manifest: GitHubPackage.manifest,
})
})
}
catch (e: any) {
if (e.response?.message)
onFailed(e.response?.message)
else
onFailed(t(`${i18nPrefix}.uploadFailed`))
}
finally {
setIsUploading(false)
}
}
return (
<>
<label
htmlFor='version'
className='flex flex-col items-start justify-center self-stretch text-text-secondary'
>
<span className='system-sm-semibold'>{t(`${i18nPrefix}.selectVersion`)}</span>
</label>
<PortalSelect
value={selectedVersion}
onSelect={onSelectVersion}
items={versions}
installedValue={updatePayload?.originalPackageInfo.version}
placeholder={t(`${i18nPrefix}.selectVersionPlaceholder`) || ''}
popupClassName='w-[512px] z-[1001]'
triggerClassName='text-components-input-text-filled'
/>
<label
htmlFor='package'
className='flex flex-col items-start justify-center self-stretch text-text-secondary'
>
<span className='system-sm-semibold'>{t(`${i18nPrefix}.selectPackage`)}</span>
</label>
<PortalSelect
value={selectedPackage}
onSelect={onSelectPackage}
items={packages}
readonly={!selectedVersion}
placeholder={t(`${i18nPrefix}.selectPackagePlaceholder`) || ''}
popupClassName='w-[512px] z-[1001]'
triggerClassName='text-components-input-text-filled'
/>
<div className='mt-4 flex items-center justify-end gap-2 self-stretch'>
{!isEdit
&& <Button
variant='secondary'
className='min-w-[72px]'
onClick={onBack}
disabled={isUploading}
>
{t('plugin.installModal.back')}
</Button>
}
<Button
variant='primary'
className='min-w-[72px]'
onClick={handleUploadPackage}
disabled={!selectedVersion || !selectedPackage || isUploading}
>
{t('plugin.installModal.next')}
</Button>
</div>
</>
)
}
export default SelectPackage

View File

@@ -0,0 +1,56 @@
'use client'
import React from 'react'
import Button from '@/app/components/base/button'
import { useTranslation } from 'react-i18next'
type SetURLProps = {
repoUrl: string
onChange: (value: string) => void
onNext: () => void
onCancel: () => void
}
const SetURL: React.FC<SetURLProps> = ({ repoUrl, onChange, onNext, onCancel }) => {
const { t } = useTranslation()
return (
<>
<label
htmlFor='repoUrl'
className='flex flex-col items-start justify-center self-stretch text-text-secondary'
>
<span className='system-sm-semibold'>{t('plugin.installFromGitHub.gitHubRepo')}</span>
</label>
<input
type='url'
id='repoUrl'
name='repoUrl'
value={repoUrl}
onChange={e => onChange(e.target.value)}
className='shadows-shadow-xs system-sm-regular flex grow items-center gap-[2px]
self-stretch overflow-hidden text-ellipsis rounded-lg border border-components-input-border-active
bg-components-input-bg-active p-2 text-components-input-text-filled'
placeholder='Please enter GitHub repo URL'
/>
<div className='mt-4 flex items-center justify-end gap-2 self-stretch'>
<Button
variant='secondary'
className='min-w-[72px]'
onClick={onCancel}
>
{t('plugin.installModal.cancel')}
</Button>
<Button
variant='primary'
className='min-w-[72px]'
onClick={onNext}
disabled={!repoUrl.trim()}
>
{t('plugin.installModal.next')}
</Button>
</div>
</>
)
}
export default SetURL