refactor: isolate extractor and update dialog state

This commit is contained in:
Misaka
2026-03-21 19:28:01 +08:00
parent 1fc2eab816
commit 16ac892c93
7 changed files with 168 additions and 77 deletions

View File

@@ -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'

View File

@@ -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 {

View 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]
}

View 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)
}
}

View 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
}
}

View 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)
}

View File

@@ -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()