refactor: isolate extractor and update dialog state
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React, { startTransition } from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { DownloadCloud, LoaderCircle, RefreshCw } from 'lucide-react'
|
||||
import Modal from './ui/Modal'
|
||||
import { useUpdateDialogState } from '../hooks/useUpdateDialogState'
|
||||
import type { UserType } from '../../../main/types/user.types'
|
||||
import type {
|
||||
DownloadReleaseRequest,
|
||||
@@ -22,10 +23,6 @@ interface UpdateDialogProps {
|
||||
onRefreshCatalog: () => Promise<void>
|
||||
}
|
||||
|
||||
function releaseKey(release: UpdateRelease): string {
|
||||
return `${release.channel}:${release.version}`
|
||||
}
|
||||
|
||||
export default function UpdateDialog({
|
||||
isOpen,
|
||||
userType,
|
||||
@@ -36,54 +33,8 @@ export default function UpdateDialog({
|
||||
onDownloadAndInstallAdminRelease,
|
||||
onRefreshCatalog
|
||||
}: UpdateDialogProps): React.JSX.Element {
|
||||
const [selectedRelease, setSelectedRelease] = useState<UpdateRelease | undefined>(undefined)
|
||||
const [changelog, setChangelog] = useState<string>('')
|
||||
const [isLoadingChangelog, setIsLoadingChangelog] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen || !catalog) return
|
||||
|
||||
if (catalog.mode === 'user') {
|
||||
setSelectedRelease(catalog.recommendedRelease)
|
||||
return
|
||||
}
|
||||
|
||||
if (catalog.mode === 'admin') {
|
||||
setSelectedRelease(
|
||||
catalog.recommendedRelease ?? catalog.channels?.stable[0] ?? catalog.channels?.preview[0]
|
||||
)
|
||||
}
|
||||
}, [catalog, isOpen])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen || !selectedRelease) {
|
||||
setChangelog('')
|
||||
return
|
||||
}
|
||||
|
||||
let active = true
|
||||
setIsLoadingChangelog(true)
|
||||
|
||||
window.electron.update
|
||||
.getChangelog(selectedRelease)
|
||||
.then((result) => {
|
||||
if (!active) return
|
||||
setChangelog(result.success && result.data ? result.data : '暂无更新说明。')
|
||||
})
|
||||
.catch(() => {
|
||||
if (!active) return
|
||||
setChangelog('暂无更新说明。')
|
||||
})
|
||||
.finally(() => {
|
||||
if (active) {
|
||||
setIsLoadingChangelog(false)
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
active = false
|
||||
}
|
||||
}, [isOpen, selectedRelease])
|
||||
const { selectedRelease, setSelectedRelease, changelog, isLoadingChangelog, isSelectedRelease } =
|
||||
useUpdateDialogState(isOpen, catalog)
|
||||
|
||||
const isBusy =
|
||||
status?.phase === 'downloading' ||
|
||||
@@ -103,11 +54,15 @@ export default function UpdateDialog({
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs font-semibold uppercase tracking-wide text-slate-500">{title}</div>
|
||||
{releases.map((release) => {
|
||||
const selected = selectedRelease && releaseKey(selectedRelease) === releaseKey(release)
|
||||
const selected = isSelectedRelease(release)
|
||||
return (
|
||||
<button
|
||||
key={releaseKey(release)}
|
||||
onClick={() => setSelectedRelease(release)}
|
||||
key={`${release.channel}:${release.version}`}
|
||||
onClick={() => {
|
||||
startTransition(() => {
|
||||
setSelectedRelease(release)
|
||||
})
|
||||
}}
|
||||
className={`w-full rounded-lg border px-3 py-3 text-left transition ${
|
||||
selected
|
||||
? 'border-blue-500 bg-blue-50 text-blue-900'
|
||||
|
||||
@@ -232,9 +232,9 @@ export function useAppBootstrap() {
|
||||
}, [])
|
||||
|
||||
const openUpdateDialog = useCallback(async () => {
|
||||
await refreshUpdateCatalog()
|
||||
await Promise.all([refreshUpdateCatalog(), refreshUpdateState()])
|
||||
setShowUpdateDialog(true)
|
||||
}, [refreshUpdateCatalog])
|
||||
}, [refreshUpdateCatalog, refreshUpdateState])
|
||||
|
||||
const handleInstallUserRelease = useCallback(async () => {
|
||||
if (!updateCatalog?.recommendedRelease) {
|
||||
@@ -276,8 +276,7 @@ export function useAppBootstrap() {
|
||||
|
||||
const refreshUpdateDialogState = useCallback(async () => {
|
||||
await window.electron.update.checkNow()
|
||||
await refreshUpdateCatalog()
|
||||
await refreshUpdateState()
|
||||
await Promise.all([refreshUpdateCatalog(), refreshUpdateState()])
|
||||
}, [refreshUpdateCatalog, refreshUpdateState])
|
||||
|
||||
return {
|
||||
|
||||
15
src/renderer/src/hooks/usePersistentTextState.ts
Normal file
15
src/renderer/src/hooks/usePersistentTextState.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { getStoredText, setStoredText } from '../lib/session-storage/text'
|
||||
|
||||
export function usePersistentTextState(
|
||||
key: string,
|
||||
fallback = ''
|
||||
): [string, React.Dispatch<React.SetStateAction<string>>] {
|
||||
const [value, setValue] = useState(() => getStoredText(key, fallback))
|
||||
|
||||
useEffect(() => {
|
||||
setStoredText(key, value)
|
||||
}, [key, value])
|
||||
|
||||
return [value, setValue]
|
||||
}
|
||||
36
src/renderer/src/hooks/useSharedProductionIds.ts
Normal file
36
src/renderer/src/hooks/useSharedProductionIds.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useEffect } from 'react'
|
||||
|
||||
function parseProductionIds(input: string): string[] {
|
||||
return input
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0)
|
||||
}
|
||||
|
||||
async function syncSharedProductionIds(input: string): Promise<void> {
|
||||
const productionIds = parseProductionIds(input)
|
||||
|
||||
if (productionIds.length === 0) {
|
||||
await window.electron.validation.clearSharedProductionIds()
|
||||
return
|
||||
}
|
||||
|
||||
await window.electron.validation.setSharedProductionIds(productionIds)
|
||||
}
|
||||
|
||||
export function useSharedProductionIds(orderNumbers: string, debounceMs = 300) {
|
||||
useEffect(() => {
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
void syncSharedProductionIds(orderNumbers)
|
||||
}, debounceMs)
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(timeoutId)
|
||||
}
|
||||
}, [debounceMs, orderNumbers])
|
||||
|
||||
return {
|
||||
clearSharedProductionIdsNow: () => window.electron.validation.clearSharedProductionIds(),
|
||||
syncSharedProductionIdsNow: () => syncSharedProductionIds(orderNumbers)
|
||||
}
|
||||
}
|
||||
89
src/renderer/src/hooks/useUpdateDialogState.ts
Normal file
89
src/renderer/src/hooks/useUpdateDialogState.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { startTransition, useEffect, useRef, useState } from 'react'
|
||||
import type { UpdateDialogCatalog, UpdateRelease } from '../../../main/types/update.types'
|
||||
|
||||
function releaseKey(release: UpdateRelease): string {
|
||||
return `${release.channel}:${release.version}`
|
||||
}
|
||||
|
||||
function getInitialSelectedRelease(catalog: UpdateDialogCatalog | null): UpdateRelease | undefined {
|
||||
if (!catalog || catalog.mode === 'disabled') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (catalog.mode === 'user') {
|
||||
return catalog.recommendedRelease
|
||||
}
|
||||
|
||||
return catalog.recommendedRelease ?? catalog.channels?.stable[0] ?? catalog.channels?.preview[0]
|
||||
}
|
||||
|
||||
export function useUpdateDialogState(
|
||||
isOpen: boolean,
|
||||
catalog: UpdateDialogCatalog | null
|
||||
): {
|
||||
selectedRelease: UpdateRelease | undefined
|
||||
setSelectedRelease: React.Dispatch<React.SetStateAction<UpdateRelease | undefined>>
|
||||
changelog: string
|
||||
isLoadingChangelog: boolean
|
||||
isSelectedRelease: (release: UpdateRelease) => boolean
|
||||
} {
|
||||
const [selectedRelease, setSelectedRelease] = useState<UpdateRelease | undefined>(undefined)
|
||||
const [changelog, setChangelog] = useState('')
|
||||
const [isLoadingChangelog, setIsLoadingChangelog] = useState(false)
|
||||
const changelogRequestIdRef = useRef(0)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setSelectedRelease(undefined)
|
||||
setChangelog('')
|
||||
setIsLoadingChangelog(false)
|
||||
return
|
||||
}
|
||||
|
||||
startTransition(() => {
|
||||
setSelectedRelease(getInitialSelectedRelease(catalog))
|
||||
})
|
||||
}, [catalog, isOpen])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen || !selectedRelease) {
|
||||
setChangelog('')
|
||||
setIsLoadingChangelog(false)
|
||||
return
|
||||
}
|
||||
|
||||
const requestId = ++changelogRequestIdRef.current
|
||||
setIsLoadingChangelog(true)
|
||||
|
||||
void window.electron.update
|
||||
.getChangelog(selectedRelease)
|
||||
.then((result) => {
|
||||
if (changelogRequestIdRef.current !== requestId) {
|
||||
return
|
||||
}
|
||||
|
||||
setChangelog(result.success && result.data ? result.data : '暂无更新说明。')
|
||||
})
|
||||
.catch(() => {
|
||||
if (changelogRequestIdRef.current !== requestId) {
|
||||
return
|
||||
}
|
||||
|
||||
setChangelog('暂无更新说明。')
|
||||
})
|
||||
.finally(() => {
|
||||
if (changelogRequestIdRef.current === requestId) {
|
||||
setIsLoadingChangelog(false)
|
||||
}
|
||||
})
|
||||
}, [isOpen, selectedRelease])
|
||||
|
||||
return {
|
||||
selectedRelease,
|
||||
setSelectedRelease,
|
||||
changelog,
|
||||
isLoadingChangelog,
|
||||
isSelectedRelease: (release) =>
|
||||
selectedRelease ? releaseKey(selectedRelease) === releaseKey(release) : false
|
||||
}
|
||||
}
|
||||
8
src/renderer/src/lib/session-storage/text.ts
Normal file
8
src/renderer/src/lib/session-storage/text.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export function getStoredText(key: string, fallback = ''): string {
|
||||
const stored = sessionStorage.getItem(key)
|
||||
return stored ?? fallback
|
||||
}
|
||||
|
||||
export function setStoredText(key: string, value: string): void {
|
||||
sessionStorage.setItem(key, value)
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import React from 'react'
|
||||
import { Download, Play, CheckCircle } from 'lucide-react'
|
||||
import OrderNumberInput from '../components/OrderNumberInput'
|
||||
import { useExtractor } from '../hooks/useExtractor'
|
||||
import { usePersistentTextState } from '../hooks/usePersistentTextState'
|
||||
import { useSharedProductionIds } from '../hooks/useSharedProductionIds'
|
||||
import LogPanel from '../components/ui/LogPanel'
|
||||
import { SegmentedProgressBar } from '../components/ui/SegmentedProgressBar'
|
||||
|
||||
const ExtractorPage: React.FC = () => {
|
||||
const [orderNumbers, setOrderNumbers] = useState(() => {
|
||||
return sessionStorage.getItem('extractor_orderNumbers') || ''
|
||||
})
|
||||
const [orderNumbers, setOrderNumbers] = usePersistentTextState('extractor_orderNumbers')
|
||||
|
||||
const {
|
||||
isRunning,
|
||||
@@ -22,19 +22,7 @@ const ExtractorPage: React.FC = () => {
|
||||
setComplete
|
||||
} = useExtractor()
|
||||
|
||||
useEffect(() => {
|
||||
sessionStorage.setItem('extractor_orderNumbers', orderNumbers)
|
||||
if (orderNumbers.trim()) {
|
||||
const orderNumberList = orderNumbers
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0)
|
||||
window.electron.validation.setSharedProductionIds(orderNumberList)
|
||||
} else {
|
||||
// Clear shared Production IDs when input is cleared
|
||||
window.electron.validation.clearSharedProductionIds()
|
||||
}
|
||||
}, [orderNumbers])
|
||||
const { clearSharedProductionIdsNow } = useSharedProductionIds(orderNumbers)
|
||||
|
||||
const handleExtract = () => {
|
||||
startExtraction(orderNumbers)
|
||||
@@ -42,6 +30,7 @@ const ExtractorPage: React.FC = () => {
|
||||
|
||||
const handleReset = () => {
|
||||
setOrderNumbers('')
|
||||
void clearSharedProductionIdsNow()
|
||||
setError(null)
|
||||
setComplete(false)
|
||||
clearLogs()
|
||||
|
||||
Reference in New Issue
Block a user