返回 AiToEarn
1 /**
2 * PluginPublishingFloatButton - global plugin publishing entry
3 * Shows a floating entry while plugin publish tasks are running.
4 */
5
6 'use client'
7
8 import type { PlatformPublishTask, PublishTask } from '@/store/plugin/types/baseTypes'
9 import { Loader2 } from 'lucide-react'
10 import { useEffect, useMemo, useRef, useState } from 'react'
11 import { useTranslation } from 'react-i18next'
12 import { useShallow } from 'zustand/shallow'
13 import { OssImage } from '@/components/common/OssImage'
14 import { Button } from '@/components/ui/button'
15
16 import { getPlatformInfoSync } from '@/store/platformMetadata'
17 import { usePluginStore } from '@/store/plugin'
18 import { PlatformTaskStatus, PLUGIN_SUPPORTED_PLATFORMS } from '@/store/plugin/types/baseTypes'
19 import { cn } from '@/utils/className'
20 import { notification } from '@/utils/ui/notification'
21 import { PublishDetailModal } from '../PublishDetailModal'
22 import styles from './PluginPublishingFloatButton.module.scss'
23
24 const notifiedPublishTaskIds = new Set<string>()
25 const pluginSupportedPlatformSet = new Set<PlatformPublishTask['platform']>(PLUGIN_SUPPORTED_PLATFORMS)
26
27 function isPluginPlatformTask(platformTask: PlatformPublishTask) {
28 return pluginSupportedPlatformSet.has(platformTask.platform)
29 }
30
31 function isPublishingPluginPlatformTask(platformTask: PlatformPublishTask) {
32 return isPluginPlatformTask(platformTask) && platformTask.status === PlatformTaskStatus.PUBLISHING
33 }
34
35 function isActionRequiredPlatformTask(platformTask: PlatformPublishTask) {
36 return platformTask.publishMode === 'user_action' && platformTask.status === PlatformTaskStatus.PENDING
37 }
38
39 function isActivePlatformTask(platformTask: PlatformPublishTask) {
40 return platformTask.status === PlatformTaskStatus.PUBLISHING || isActionRequiredPlatformTask(platformTask)
41 }
42
43 function hasPublishingPluginTask(task: PublishTask) {
44 return task.platformTasks.some(isPublishingPluginPlatformTask)
45 }
46
47 function hasActivePublishTask(task: PublishTask) {
48 return task.platformTasks.some(isActivePlatformTask)
49 }
50
51 function getPluginFinalStatus(task: PublishTask): 'success' | 'error' | null {
52 const pluginTasks = task.platformTasks.filter(isPluginPlatformTask)
53 if (pluginTasks.length === 0)
54 return null
55
56 const hasRunningTask = pluginTasks.some(platformTask => (
57 platformTask.status === PlatformTaskStatus.PENDING
58 || platformTask.status === PlatformTaskStatus.PUBLISHING
59 ))
60 if (hasRunningTask)
61 return null
62
63 if (pluginTasks.every(platformTask => platformTask.status === PlatformTaskStatus.CANCELED))
64 return null
65
66 if (pluginTasks.some(platformTask => platformTask.status === PlatformTaskStatus.ERROR))
67 return 'error'
68
69 return pluginTasks.every(platformTask => platformTask.status === PlatformTaskStatus.COMPLETED) ? 'success' : null
70 }
71
72 function getActivePlatformTask(task?: PublishTask) {
73 return task?.platformTasks.find(platformTask => platformTask.status === PlatformTaskStatus.PUBLISHING)
74 || task?.platformTasks.find(isActionRequiredPlatformTask)
75 }
76
77 export function PluginPublishingFloatButton() {
78 const { t, ready } = useTranslation('plugin')
79 const [detailVisible, setDetailVisible] = useState(false)
80 const [activeTaskId, setActiveTaskId] = useState<string | undefined>()
81 const taskStatusRef = useRef<Map<string, { notified: boolean, wasPublishing: boolean }>>(new Map())
82
83 const { publishTasks, publishDetailModalOpenCount } = usePluginStore(
84 useShallow(state => ({
85 publishTasks: state.publishTasks,
86 publishDetailModalOpenCount: state.publishDetailModalOpenCount,
87 })),
88 )
89
90 const activeProgressTask = useMemo(
91 () => publishTasks.find(hasActivePublishTask),
92 [publishTasks],
93 )
94 const activePlatformTask = useMemo(
95 () => getActivePlatformTask(activeProgressTask),
96 [activeProgressTask],
97 )
98 const isActionRequiredActive = activePlatformTask ? isActionRequiredPlatformTask(activePlatformTask) : false
99 const activePlatInfo = activePlatformTask
100 ? getPlatformInfoSync(activePlatformTask.platform)
101 : undefined
102
103 useEffect(() => {
104 const nextTaskStatus = new Map<string, { notified: boolean, wasPublishing: boolean }>()
105
106 publishTasks.forEach((task) => {
107 const previousStatus = taskStatusRef.current.get(task.id)
108 const isPublishing = hasPublishingPluginTask(task)
109 const finalStatus = getPluginFinalStatus(task)
110 const notified = previousStatus?.notified || false
111 const finishedAfterPublishing = !!previousStatus?.wasPublishing && !!finalStatus
112 const notificationKey = `plugin-publish-finished-${task.id}`
113 const hasNotified = notified || notifiedPublishTaskIds.has(notificationKey)
114 const shouldNotify = ready && finishedAfterPublishing && !hasNotified
115
116 if (shouldNotify) {
117 notifiedPublishTaskIds.add(notificationKey)
118 const notify = finalStatus === 'success' ? notification.success : notification.error
119 notify(finalStatus === 'success' ? t('publishFloating.completed') : t('publishFloating.failed'), {
120 key: notificationKey,
121 duration: 5,
122 })
123 }
124
125 const nextNotified = hasNotified || shouldNotify || (!!finalStatus && !previousStatus)
126 nextTaskStatus.set(task.id, {
127 notified: nextNotified,
128 wasPublishing: !nextNotified && (isPublishing || finishedAfterPublishing),
129 })
130 })
131
132 taskStatusRef.current = nextTaskStatus
133 }, [publishTasks, ready, t])
134
135 useEffect(() => {
136 if (!detailVisible || !activeTaskId)
137 return
138
139 const taskExists = publishTasks.some(task => task.id === activeTaskId)
140 if (!taskExists) {
141 setDetailVisible(false)
142 setActiveTaskId(undefined)
143 }
144 }, [activeTaskId, detailVisible, publishTasks])
145
146 const showButton = ready && !!activeProgressTask && publishDetailModalOpenCount === 0
147
148 const handleOpenDetail = () => {
149 if (!activeProgressTask)
150 return
151
152 setActiveTaskId(activeProgressTask.id)
153 setDetailVisible(true)
154 }
155
156 const handleCloseDetail = () => {
157 setDetailVisible(false)
158 setActiveTaskId(undefined)
159 }
160
161 return (
162 <>
163 {showButton && (
164 <Button
165 type="button"
166 className={cn(
167 styles.floatButton,
168 'h-auto border-0 bg-transparent p-0 shadow-none cursor-pointer hover:bg-transparent',
169 )}
170 variant="ghost"
171 onClick={handleOpenDetail}
172 aria-label={t('publishFloating.openDetail')}
173 >
174 <span className={styles.floatButton_wave} aria-hidden="true" />
175 <span className={cn(styles.floatButton_wave, styles.floatButton_waveDelay)} aria-hidden="true" />
176 <span className={styles.floatButton_glow} aria-hidden="true" />
177 <span className={styles.floatButton_surface}>
178 <span className={styles.floatButton_iconWrap}>
179 {activePlatInfo?.icon ? (
180 <OssImage src={activePlatInfo.icon} alt={activePlatInfo.name} width={24} height={24} />
181 ) : (
182 <Loader2 className="h-5 w-5 animate-spin" />
183 )}
184 {!isActionRequiredActive && (
185 <span className={styles.floatButton_spinnerBadge}>
186 <Loader2 className="h-3 w-3 animate-spin" />
187 </span>
188 )}
189 </span>
190 <span className={styles.floatButton_textWrap}>
191 <span className={styles.floatButton_title}>
192 {isActionRequiredActive ? t('publishDetail.actionRequired') : t('publishFloating.pluginPublishing')}
193 </span>
194 <span className={styles.floatButton_subtitle}>{t('publishFloating.openDetail')}</span>
195 </span>
196 </span>
197 </Button>
198 )}
199
200 <PublishDetailModal
201 visible={detailVisible}
202 onClose={handleCloseDetail}
203 taskId={activeTaskId}
204 />
205 </>
206 )
207 }
208
208 lines Plain Text