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 ReactMarkdown from 'react-markdown'
|
||||||
import remarkGfm from 'remark-gfm'
|
import remarkGfm from 'remark-gfm'
|
||||||
import { DownloadCloud, LoaderCircle, RefreshCw } from 'lucide-react'
|
import { DownloadCloud, LoaderCircle, RefreshCw } from 'lucide-react'
|
||||||
import Modal from './ui/Modal'
|
import Modal from './ui/Modal'
|
||||||
|
import { useUpdateDialogState } from '../hooks/useUpdateDialogState'
|
||||||
import type { UserType } from '../../../main/types/user.types'
|
import type { UserType } from '../../../main/types/user.types'
|
||||||
import type {
|
import type {
|
||||||
DownloadReleaseRequest,
|
DownloadReleaseRequest,
|
||||||
@@ -22,10 +23,6 @@ interface UpdateDialogProps {
|
|||||||
onRefreshCatalog: () => Promise<void>
|
onRefreshCatalog: () => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
function releaseKey(release: UpdateRelease): string {
|
|
||||||
return `${release.channel}:${release.version}`
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function UpdateDialog({
|
export default function UpdateDialog({
|
||||||
isOpen,
|
isOpen,
|
||||||
userType,
|
userType,
|
||||||
@@ -36,54 +33,8 @@ export default function UpdateDialog({
|
|||||||
onDownloadAndInstallAdminRelease,
|
onDownloadAndInstallAdminRelease,
|
||||||
onRefreshCatalog
|
onRefreshCatalog
|
||||||
}: UpdateDialogProps): React.JSX.Element {
|
}: UpdateDialogProps): React.JSX.Element {
|
||||||
const [selectedRelease, setSelectedRelease] = useState<UpdateRelease | undefined>(undefined)
|
const { selectedRelease, setSelectedRelease, changelog, isLoadingChangelog, isSelectedRelease } =
|
||||||
const [changelog, setChangelog] = useState<string>('')
|
useUpdateDialogState(isOpen, catalog)
|
||||||
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 isBusy =
|
const isBusy =
|
||||||
status?.phase === 'downloading' ||
|
status?.phase === 'downloading' ||
|
||||||
@@ -103,11 +54,15 @@ export default function UpdateDialog({
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="text-xs font-semibold uppercase tracking-wide text-slate-500">{title}</div>
|
<div className="text-xs font-semibold uppercase tracking-wide text-slate-500">{title}</div>
|
||||||
{releases.map((release) => {
|
{releases.map((release) => {
|
||||||
const selected = selectedRelease && releaseKey(selectedRelease) === releaseKey(release)
|
const selected = isSelectedRelease(release)
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={releaseKey(release)}
|
key={`${release.channel}:${release.version}`}
|
||||||
onClick={() => setSelectedRelease(release)}
|
onClick={() => {
|
||||||
|
startTransition(() => {
|
||||||
|
setSelectedRelease(release)
|
||||||
|
})
|
||||||
|
}}
|
||||||
className={`w-full rounded-lg border px-3 py-3 text-left transition ${
|
className={`w-full rounded-lg border px-3 py-3 text-left transition ${
|
||||||
selected
|
selected
|
||||||
? 'border-blue-500 bg-blue-50 text-blue-900'
|
? 'border-blue-500 bg-blue-50 text-blue-900'
|
||||||
|
|||||||
@@ -232,9 +232,9 @@ export function useAppBootstrap() {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const openUpdateDialog = useCallback(async () => {
|
const openUpdateDialog = useCallback(async () => {
|
||||||
await refreshUpdateCatalog()
|
await Promise.all([refreshUpdateCatalog(), refreshUpdateState()])
|
||||||
setShowUpdateDialog(true)
|
setShowUpdateDialog(true)
|
||||||
}, [refreshUpdateCatalog])
|
}, [refreshUpdateCatalog, refreshUpdateState])
|
||||||
|
|
||||||
const handleInstallUserRelease = useCallback(async () => {
|
const handleInstallUserRelease = useCallback(async () => {
|
||||||
if (!updateCatalog?.recommendedRelease) {
|
if (!updateCatalog?.recommendedRelease) {
|
||||||
@@ -276,8 +276,7 @@ export function useAppBootstrap() {
|
|||||||
|
|
||||||
const refreshUpdateDialogState = useCallback(async () => {
|
const refreshUpdateDialogState = useCallback(async () => {
|
||||||
await window.electron.update.checkNow()
|
await window.electron.update.checkNow()
|
||||||
await refreshUpdateCatalog()
|
await Promise.all([refreshUpdateCatalog(), refreshUpdateState()])
|
||||||
await refreshUpdateState()
|
|
||||||
}, [refreshUpdateCatalog, refreshUpdateState])
|
}, [refreshUpdateCatalog, refreshUpdateState])
|
||||||
|
|
||||||
return {
|
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 { Download, Play, CheckCircle } from 'lucide-react'
|
||||||
import OrderNumberInput from '../components/OrderNumberInput'
|
import OrderNumberInput from '../components/OrderNumberInput'
|
||||||
import { useExtractor } from '../hooks/useExtractor'
|
import { useExtractor } from '../hooks/useExtractor'
|
||||||
|
import { usePersistentTextState } from '../hooks/usePersistentTextState'
|
||||||
|
import { useSharedProductionIds } from '../hooks/useSharedProductionIds'
|
||||||
import LogPanel from '../components/ui/LogPanel'
|
import LogPanel from '../components/ui/LogPanel'
|
||||||
import { SegmentedProgressBar } from '../components/ui/SegmentedProgressBar'
|
import { SegmentedProgressBar } from '../components/ui/SegmentedProgressBar'
|
||||||
|
|
||||||
const ExtractorPage: React.FC = () => {
|
const ExtractorPage: React.FC = () => {
|
||||||
const [orderNumbers, setOrderNumbers] = useState(() => {
|
const [orderNumbers, setOrderNumbers] = usePersistentTextState('extractor_orderNumbers')
|
||||||
return sessionStorage.getItem('extractor_orderNumbers') || ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isRunning,
|
isRunning,
|
||||||
@@ -22,19 +22,7 @@ const ExtractorPage: React.FC = () => {
|
|||||||
setComplete
|
setComplete
|
||||||
} = useExtractor()
|
} = useExtractor()
|
||||||
|
|
||||||
useEffect(() => {
|
const { clearSharedProductionIdsNow } = useSharedProductionIds(orderNumbers)
|
||||||
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 handleExtract = () => {
|
const handleExtract = () => {
|
||||||
startExtraction(orderNumbers)
|
startExtraction(orderNumbers)
|
||||||
@@ -42,6 +30,7 @@ const ExtractorPage: React.FC = () => {
|
|||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
setOrderNumbers('')
|
setOrderNumbers('')
|
||||||
|
void clearSharedProductionIdsNow()
|
||||||
setError(null)
|
setError(null)
|
||||||
setComplete(false)
|
setComplete(false)
|
||||||
clearLogs()
|
clearLogs()
|
||||||
|
|||||||
Reference in New Issue
Block a user