From 113f5d39433ef3a8be094ab88078c940712095f4 Mon Sep 17 00:00:00 2001 From: NimBold Date: Tue, 16 Jun 2026 11:21:20 +0330 Subject: [PATCH] perf(frontend): architect transient react progress state with zustand --- src/App.tsx | 17 +--- src/components/DownloadItem.tsx | 146 +++++++++++++++++++++++++++++++ src/components/DownloadTable.tsx | 92 +++---------------- src/store/downloadStore.ts | 41 +++++++++ 4 files changed, 200 insertions(+), 96 deletions(-) create mode 100644 src/components/DownloadItem.tsx create mode 100644 src/store/downloadStore.ts diff --git a/src/App.tsx b/src/App.tsx index 4de2c25..93d6d56 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,7 @@ import { PropertiesModal } from "./components/PropertiesModal"; import { DeleteConfirmationModal } from "./components/DeleteConfirmationModal"; import { listenEvent as listen, invokeCommand as invoke } from "./ipc"; import { useDownloadStore, MAIN_QUEUE_ID } from './store/useDownloadStore'; +import { initDownloadListener } from './store/downloadStore'; import { useSettingsStore } from "./store/useSettingsStore"; import { isPermissionGranted, requestPermission, sendNotification } from '@tauri-apps/plugin-notification'; import SchedulerView from "./components/SchedulerView"; @@ -204,20 +205,7 @@ function App() { }, [theme]); useEffect(() => { - const unlistenProgress = listen('download-progress', (event: any) => { - const { id, fraction, speed, eta, size } = event.payload; - const state = useDownloadStore.getState(); - const current = state.downloads.find(d => d.id === id); - - const updates: any = { fraction, speed, eta }; - if (size) updates.size = size; - - if (current && current.status === 'queued') { - updates.status = 'downloading'; - } - - updateDownload(id, updates); - }); + initDownloadListener(); const unlistenComplete = listen('download-complete', (event) => { updateDownload(event.payload, { status: 'completed', fraction: 1.0, speed: '-', eta: '-' }); @@ -255,7 +243,6 @@ function App() { return () => { invoke('set_extension_frontend_ready', { ready: false }).catch(() => {}); - unlistenProgress.then(f => f()); unlistenComplete.then(f => f()); unlistenFailed.then(f => f()); unlistenExtension.then(f => f()); diff --git a/src/components/DownloadItem.tsx b/src/components/DownloadItem.tsx new file mode 100644 index 0000000..e20c293 --- /dev/null +++ b/src/components/DownloadItem.tsx @@ -0,0 +1,146 @@ +import React, { useEffect, useRef } from 'react'; +import { useDownloadStore } from '../store/useDownloadStore'; +import { useDownloadProgressStore } from '../store/downloadStore'; +import { Play, Pause, MoreVertical } from 'lucide-react'; +import type { DownloadItem as DownloadItemType } from '../bindings/DownloadItem'; + +interface DownloadItemProps { + downloadId: string; + index: number; + tableGridTemplate: string; + setContextMenu: (menu: { x: number; y: number; id: string }) => void; + handlePause: (id: string) => void; + handleResume: (item: DownloadItemType) => void; + getCategoryIcon: (category: string) => React.ReactNode; +} + +export const DownloadItem = React.memo(({ + downloadId, + index, + tableGridTemplate, + setContextMenu, + handlePause, + handleResume, + getCategoryIcon, +}) => { + const download = useDownloadStore(state => state.downloads.find(d => d.id === downloadId)); + + const progressBarRef = useRef(null); + const statusTextRef = useRef(null); + const speedTextRef = useRef(null); + const etaTextRef = useRef(null); + + useEffect(() => { + // We only need transient updates while it's actively downloading + if (!download || download.status !== 'downloading') return; + + const unsubscribe = useDownloadProgressStore.subscribe((state) => { + const progress = state.progressMap[downloadId]; + if (!progress) return; + + if (progressBarRef.current) { + progressBarRef.current.style.width = `${progress.fraction * 100}%`; + } + if (statusTextRef.current) { + statusTextRef.current.innerText = `${(progress.fraction * 100).toFixed(0)}%`; + } + if (speedTextRef.current) { + speedTextRef.current.innerText = progress.speed; + } + if (etaTextRef.current) { + etaTextRef.current.innerText = progress.eta; + } + }); + + return () => unsubscribe(); + }, [downloadId, download?.status]); + + if (!download) return null; + + return ( +
{ + e.preventDefault(); + setContextMenu({ x: e.clientX, y: e.clientY, id: download.id }); + }} + > +
+ + {getCategoryIcon(download.category)} + + + {download.fileName} + +
+ +
+ + {download.size && download.size !== '-' ? download.size : 'Unknown'} + +
+ +
+ {download.status === 'completed' ? ( + Completed + ) : ( + <> +
+
+
+ + {download.status === 'downloading' + ? `${((download.fraction || 0) * 100).toFixed(0)}%` + : download.status.charAt(0).toUpperCase() + download.status.slice(1)} + + + )} +
+ +
+ {download.status === 'downloading' ? download.speed : '-'} +
+ +
+ {download.status === 'downloading' ? download.eta : '-'} +
+ +
+ + {download.dateAdded ? new Date(download.dateAdded).toLocaleDateString() : '-'} + + +
+ {download.status === 'downloading' && ( + + )} + {download.status === 'paused' && ( + + )} + +
+
+
+ ); +}); diff --git a/src/components/DownloadTable.tsx b/src/components/DownloadTable.tsx index 3dc4b7f..5fa1a38 100644 --- a/src/components/DownloadTable.tsx +++ b/src/components/DownloadTable.tsx @@ -2,7 +2,8 @@ import React, { useState, useEffect } from 'react'; import { useDownloadStore, DownloadItem } from '../store/useDownloadStore'; import { useSettingsStore } from '../store/useSettingsStore'; import { SidebarFilter } from './Sidebar'; -import { Play, Pause, Plus, FileText, Image as ImageIcon, Music, Film, Box, Archive, FileQuestion, MoreVertical, PanelLeft, ArrowDownCircle, Command } from 'lucide-react'; +import { Play, Pause, Plus, FileText, Image as ImageIcon, Music, Film, Box, Archive, FileQuestion, PanelLeft, ArrowDownCircle, Command } from 'lucide-react'; +import { DownloadItem as DownloadItemComponent } from './DownloadItem'; import { invokeCommand as invoke } from '../ipc'; import { homeDir } from '@tauri-apps/api/path'; @@ -210,87 +211,16 @@ export const DownloadTable: React.FC = ({ filter }) => {
{filteredDownloads.map((d, index) => ( -
{ - e.preventDefault(); - setContextMenu({ x: e.clientX, y: e.clientY, id: d.id }); - }} - > -
- - {getCategoryIcon(d.category)} - - - {d.fileName} - -
- -
- - {d.size && d.size !== '-' ? d.size : 'Unknown'} - -
- -
- {d.status === 'completed' ? ( - Completed - ) : ( - <> -
-
-
- - {d.status === 'downloading' - ? `${((d.fraction || 0) * 100).toFixed(0)}%` - : d.status.charAt(0).toUpperCase() + d.status.slice(1)} - - - )} -
- -
- {d.status === 'downloading' ? d.speed : '-'} -
- -
- {d.status === 'downloading' ? d.eta : '-'} -
- -
- - {d.dateAdded ? new Date(d.dateAdded).toLocaleDateString() : '-'} - - -
- {d.status === 'downloading' && ( - - )} - {d.status === 'paused' && ( - - )} - -
-
-
+ downloadId={d.id} + index={index} + tableGridTemplate={tableGridTemplate} + setContextMenu={setContextMenu} + handlePause={handlePause} + handleResume={handleResume} + getCategoryIcon={getCategoryIcon} + /> ))} {Array.from({ length: Math.max(0, 50 - filteredDownloads.length) }).map((_, i) => { const globalIndex = filteredDownloads.length + i; diff --git a/src/store/downloadStore.ts b/src/store/downloadStore.ts new file mode 100644 index 0000000..2975a5d --- /dev/null +++ b/src/store/downloadStore.ts @@ -0,0 +1,41 @@ +import { create } from 'zustand'; +import { listen, UnlistenFn } from '@tauri-apps/api/event'; +import type { DownloadProgressEvent } from '../bindings/DownloadProgressEvent'; + +interface DownloadProgressState { + progressMap: Record; + updateDownloadProgress: (id: string, payload: DownloadProgressEvent) => void; +} + +import { useDownloadStore } from './useDownloadStore'; + +export const useDownloadProgressStore = create((set) => ({ + progressMap: {}, + updateDownloadProgress: (id, payload) => + set((state) => ({ + progressMap: { + ...state.progressMap, + [id]: payload, + }, + })), +})); + +let unlistenProgress: UnlistenFn | null = null; + +export async function initDownloadListener() { + if (unlistenProgress) return; + unlistenProgress = await listen('download-progress', (event) => { + const payload = event.payload; + useDownloadProgressStore.getState().updateDownloadProgress(payload.id, payload); + + const mainStore = useDownloadStore.getState(); + const current = mainStore.downloads.find(d => d.id === payload.id); + if (current && current.status === 'queued') { + const updates: any = { status: 'downloading' }; + if (payload.size && current.size !== payload.size) updates.size = payload.size; + mainStore.updateDownload(payload.id, updates); + } else if (current && payload.size && current.size !== payload.size) { + mainStore.updateDownload(payload.id, { size: payload.size }); + } + }); +}