feat(ui): add real-time download progress to instance creation flow#122
feat(ui): add real-time download progress to instance creation flow#122RockChinQ wants to merge 1 commit intoHydroRoll-Team:mainfrom
Conversation
Reviewer's GuideImplements a global, throttled download progress store wired to Tauri events and integrates it into the instance creation flow by adding an Installing step with a phase-aware progress UI and safer dialog behavior. Sequence diagram for instance creation with real-time download progresssequenceDiagram
actor User
participant InstanceCreationModal
participant DownloadStore
participant DownloadProgress
participant TauriRuntime
participant RustDownloader
participant InstancesStore
User->>InstanceCreationModal: Open modal (open=true)
InstanceCreationModal->>DownloadStore: init()
DownloadStore->>TauriRuntime: listen download-start
DownloadStore->>TauriRuntime: listen download-progress
DownloadStore->>TauriRuntime: listen download-complete
User->>InstanceCreationModal: Fill steps 1-3
User->>InstanceCreationModal: Click Create
InstanceCreationModal->>InstanceCreationModal: setStep(4)
InstanceCreationModal->>DownloadStore: reset()
InstanceCreationModal->>DownloadStore: setPhase(preparing, label)
InstanceCreationModal->>RustDownloader: installVersion(instanceId, versionId)
RustDownloader-->>TauriRuntime: emit download-start(totalFiles)
TauriRuntime-->>DownloadStore: download-start event
DownloadStore->>DownloadStore: set phase=downloading
loop Throttled progress updates
RustDownloader-->>TauriRuntime: emit download-progress(ProgressEvent)
TauriRuntime-->>DownloadStore: download-progress event
DownloadStore->>DownloadStore: buffer ProgressEvent
DownloadStore->>DownloadStore: flush buffered progress (every 50ms)
end
RustDownloader-->>TauriRuntime: emit download-complete
TauriRuntime-->>DownloadStore: download-complete event
DownloadStore->>DownloadStore: phase=finalizing
DownloadProgress->>DownloadStore: read phase and counters
DownloadStore-->>DownloadProgress: state(preparing/downloading/finalizing)
DownloadProgress->>DownloadProgress: render phase-aware UI
alt Mod loader selected
InstanceCreationModal->>DownloadStore: setPhase(installing-mod-loader, label)
InstanceCreationModal->>RustDownloader: installFabric or installForge
RustDownloader-->>InstanceCreationModal: return success
else No mod loader
Note over InstanceCreationModal,RustDownloader: Skip mod loader install
end
InstancesStore->>InstancesStore: refresh()
InstanceCreationModal->>DownloadStore: setPhase(completed, "Installation complete")
InstanceCreationModal->>InstanceCreationModal: setInstallFinished(true)
DownloadProgress->>DownloadStore: read phase=completed
User->>InstanceCreationModal: Click Close or Done
InstanceCreationModal->>DownloadStore: cleanup() on unmount
DownloadStore->>TauriRuntime: unlisten all events
Class diagram for DownloadStore state and related UI componentsclassDiagram
class DownloadPhase {
<<enumeration>>
idle
preparing
downloading
finalizing
installing-mod-loader
completed
error
}
class DownloadState {
+DownloadPhase phase
+number totalFiles
+number completedFiles
+string currentFile
+string currentFileStatus
+number currentFileDownloaded
+number currentFileTotal
+number totalDownloadedBytes
+string errorMessage
+string phaseLabel
+Promise~void~ init()
+void cleanup()
+void reset()
+void setPhase(phase, label)
+void setError(message)
}
class DownloadStore {
<<zustand_store>>
+useDownloadStore(selector)
}
class ProgressEventPayload {
+string file
+string status
+number downloaded
+number total
+number completedFiles
+number totalFiles
+number totalDownloadedBytes
}
class InstanceCreationModal {
<<ReactComponent>>
+boolean open
+function onOpenChange(newOpen)
+number step
+boolean creating
+boolean installFinished
+string instanceName
+string modLoaderType
+handleNext()
+handleCreate()
}
class DownloadProgress {
<<ReactComponent>>
+render() ReactNode
}
class TauriEventSystem {
<<framework>>
+listen(eventName, handler)
+emit(eventName, payload)
}
DownloadStore --> DownloadState : holds
DownloadState --> DownloadPhase : uses
TauriEventSystem --> ProgressEventPayload : emits
TauriEventSystem --> DownloadStore : download-start
TauriEventSystem --> DownloadStore : download-progress
TauriEventSystem --> DownloadStore : download-complete
DownloadStore --> ProgressEventPayload : updates from
InstanceCreationModal --> DownloadStore : init() cleanup() reset() setPhase() setError()
DownloadProgress --> DownloadStore : reads state via useDownloadStore
InstanceCreationModal --> DownloadProgress : embeds in step4_installing
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
@RockChinQ is attempting to deploy a commit to the retrofor Team on Vercel. A member of the Team first needs to authorize it. |
There was a problem hiding this comment.
Hey - I've found 2 issues, and left some high level feedback:
- The
useEffectthat callsdownloadStore.init()/cleanup()is usingopeninside the cleanup closure in a way that prevents cleanup from running when the modal closes (the cleanup for theopen=truepass seesopenas true), so you may want to remove theif (!open)guard and instead always calldownloadStore.cleanup()in the cleanup function to ensure listeners are released when the modal closes. - Right now
downloadStore.reset()is only called when the modal (re)opens or when you start creating an instance; consider also resetting in thecleanuppath when the modal is closed mid-download so that any stale progress/error state doesn’t bleed into the next open.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The `useEffect` that calls `downloadStore.init()`/`cleanup()` is using `open` inside the cleanup closure in a way that prevents cleanup from running when the modal closes (the cleanup for the `open=true` pass sees `open` as true), so you may want to remove the `if (!open)` guard and instead always call `downloadStore.cleanup()` in the cleanup function to ensure listeners are released when the modal closes.
- Right now `downloadStore.reset()` is only called when the modal (re)opens or when you start creating an instance; consider also resetting in the `cleanup` path when the modal is closed mid-download so that any stale progress/error state doesn’t bleed into the next open.
## Individual Comments
### Comment 1
<location path="packages/ui/src/components/instance-creation-modal.tsx" line_range="126-135" />
<code_context>
return list;
}, [gameStore.versions, versionFilter, versionSearch]);
+ // Initialize download store event listeners when modal opens
+ useEffect(() => {
+ if (open) {
+ downloadStore.init();
+ }
+ return () => {
+ if (!open) {
+ downloadStore.cleanup();
+ }
+ };
+ }, [open, downloadStore.init, downloadStore.cleanup]);
+
// Reset when opened/closed
</code_context>
<issue_to_address>
**issue (bug_risk):** Download-store cleanup never runs when the modal is closed while still mounted.
Because `open` is closed over from the render that created this effect, the cleanup from the `open === true` render runs *before* `open` flips to `false`. In that cleanup `!open` is still `false`, so `downloadStore.cleanup()` is never called; the next effect with `open === false` has an empty cleanup. That means listeners/timers from `init()` can leak across modal close.
I’d either:
- Always clean up in the effect teardown:
```ts
useEffect(() => {
if (open) {
downloadStore.init();
}
return () => {
downloadStore.cleanup();
};
}, [open, downloadStore.init, downloadStore.cleanup]);
```
- Or add a separate effect that calls `cleanup()` when `open` transitions to `false`.
As written, the logic can leave subscriptions active after the modal closes.
</issue_to_address>
### Comment 2
<location path="packages/ui/src/components/download-monitor.tsx" line_range="4-9" />
<code_context>
-export function DownloadMonitor() {
- const [isVisible, setIsVisible] = useState(true);
+function formatBytes(bytes: number): string {
+ if (bytes === 0) return "0 B";
+ const units = ["B", "KB", "MB", "GB"];
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
+ const value = bytes / 1024 ** i;
+ return `${value.toFixed(value < 10 ? 1 : 0)} ${units[i]}`;
+}
+
</code_context>
<issue_to_address>
**suggestion:** formatBytes can produce `undefined` units for extremely large downloads.
Because `units` stops at `GB`, any size ≥ 1 TB yields an index ≥ 4, so `units[i]` becomes `undefined` and you get outputs like `"1024.0 undefined"`.
You can cap the index and/or extend the units:
```ts
const units = ["B", "KB", "MB", "GB", "TB"]; // or more
const i = Math.min(units.length - 1, Math.floor(Math.log(bytes) / Math.log(1024)));
```
This avoids invalid unit strings as asset sizes grow.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
There was a problem hiding this comment.
Pull request overview
Adds a phase-aware, real-time download/install progress UI to the instance creation wizard, backed by a global Zustand store that listens to Tauri download events.
Changes:
- Introduces a
download-storewith throttled handling ofdownload-start/progress/completeevents. - Reworks the download monitor into an inline
DownloadProgresscomponent with phase-aware rendering. - Extends the instance creation modal with an “Installing” step (Step 4) + live progress and reduced layout jitter from long filenames.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| packages/ui/src/stores/download-store.ts | Adds a global download progress store with throttled progress updates and lifecycle helpers. |
| packages/ui/src/components/instance-creation-modal.tsx | Adds Step 4 “Installing”, wires store phases/errors, and changes close behavior during install. |
| packages/ui/src/components/download-monitor.tsx | Replaces prior monitor UI with a phase-aware inline progress component that reads from the store. |
| if (!open) { | ||
| downloadStore.cleanup(); | ||
| } |
There was a problem hiding this comment.
The effect cleanup condition is inverted relative to how React effect cleanups work: when open changes from true → false, the cleanup runs with the previous open value (true), so downloadStore.cleanup() will not execute. This leaves event listeners active unintentionally. Make the cleanup unconditional (or cleanup when open was true), or explicitly cleanup in an effect branch when open becomes false.
| if (!open) { | |
| downloadStore.cleanup(); | |
| } | |
| downloadStore.cleanup(); |
- Add download-store with throttled event handling for download-start/progress/complete - Rewrite download-monitor with phase-aware progress display (preparing/downloading/finalizing/installing-mod-loader/completed/error) - Add Step 4 (Installing) to instance creation modal with live progress bar - Fix dialog width jittering caused by long filenames (overflow-hidden + w-0 grow)
50949cc to
5f93323
Compare
| @@ -0,0 +1,194 @@ | |||
| import { listen, type UnlistenFn } from "@tauri-apps/api/event"; | |||
There was a problem hiding this comment.
Move this file to models please, all stores placed at stores are legacy codes.
| setError: (message: string) => void; | ||
| } | ||
|
|
||
| let unlisteners: UnlistenFn[] = []; |
There was a problem hiding this comment.
This may cause unexpected memory leak, consider handle unlisteners and cleanup callbacks into your zustand store instead of using global vars.
|
@RockChinQ check |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Summary by Sourcery
Add an installing step with real-time download progress to the instance creation flow, backed by a shared download store and updated download monitor UI.
New Features:
Enhancements: