Skip to content

feat(ui): add real-time download progress to instance creation flow#122

Open
RockChinQ wants to merge 1 commit intoHydroRoll-Team:mainfrom
RockChinQ:feat/download-progress-ui
Open

feat(ui): add real-time download progress to instance creation flow#122
RockChinQ wants to merge 1 commit intoHydroRoll-Team:mainfrom
RockChinQ:feat/download-progress-ui

Conversation

@RockChinQ
Copy link
Copy Markdown

@RockChinQ RockChinQ commented Mar 20, 2026

  • 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)

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:

  • Introduce a global download store wired to Tauri download events to track multi-file download progress and phases.
  • Add an installing step to the instance creation modal that shows live download and installation details for the new instance, including mod loader installation status.
  • Provide an inline download progress component that displays phase-aware overall and per-file progress inside dialogs.

Enhancements:

  • Redesign the download monitor UI into a phase-aware progress view with byte formatting, file counts, and error/completion messaging.
  • Prevent closing the instance creation modal while installation is in progress and adjust footer actions to reflect installing/completed states.
  • Tweak dialog content layout to avoid width jitter from long filenames by constraining and truncating file paths.

Copilot AI review requested due to automatic review settings March 20, 2026 16:12
@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai bot commented Mar 20, 2026

Reviewer's Guide

Implements 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 progress

sequenceDiagram
  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
Loading

Class diagram for DownloadStore state and related UI components

classDiagram
  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
Loading

File-Level Changes

Change Details Files
Add a global download store backed by Tauri events with throttled progress updates.
  • Create a zustand-based download store to track phase, file counts, byte counts, and errors.
  • Subscribe to Tauri download-start, download-progress, and download-complete events and map them into store state updates.
  • Throttle download-progress handling via a timer and buffered event to reduce React re-renders.
  • Expose init, cleanup, reset, setPhase, and setError actions and manage listener lifecycle and global timers.
packages/ui/src/stores/download-store.ts
Refactor the instance creation modal to include an Installing step driven by the download store and improve step UX.
  • Inject the download store into the modal, initializing listeners when opened and cleaning up/resetting on close or restart of creation.
  • Introduce a fourth Installing step with corresponding titles/descriptions and prevent closing while installation is in progress.
  • Update the creation flow to advance to step 4, drive download phases (preparing, installing-mod-loader, completed, error), and mark installation completion for button enablement.
  • Embed a new DownloadProgress component in step 4 alongside a summary of the instance, version, and mod loader being installed.
  • Adjust navigation buttons, cancel/close behavior, and success/error handling to respect the new step and installFinished state.
  • Refactor the step indicator to derive segments from a stepKeys array and tweak layout (overflow-hidden on content).
packages/ui/src/components/instance-creation-modal.tsx
Replace the old static DownloadMonitor with a phase-aware DownloadProgress component bound to the download store.
  • Remove local visibility state and hardcoded example downloads in favor of live data from useDownloadStore.
  • Render different icons and texts based on phase (idle, preparing, downloading, finalizing, installing-mod-loader, completed, error).
  • Compute and display overall progress percentage and per-file progress with byte count formatting and filename shortening, minimizing layout shifts with fixed-height containers.
  • Handle error and completion messages based on store state.
packages/ui/src/components/download-monitor.tsx

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 20, 2026

@RockChinQ is attempting to deploy a commit to the retrofor Team on Vercel.

A member of the Team first needs to authorize it.

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • 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.
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>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-store with throttled handling of download-start/progress/complete events.
  • Reworks the download monitor into an inline DownloadProgress component 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.

Comment on lines +132 to +134
if (!open) {
downloadStore.cleanup();
}
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The effect cleanup condition is inverted relative to how React effect cleanups work: when open changes from truefalse, 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.

Suggested change
if (!open) {
downloadStore.cleanup();
}
downloadStore.cleanup();

Copilot uses AI. Check for mistakes.
- 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)
@RockChinQ RockChinQ force-pushed the feat/download-progress-ui branch from 50949cc to 5f93323 Compare March 20, 2026 16:34
Copy link
Copy Markdown
Member

@HsiangNianian HsiangNianian left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fu050409 check

@HsiangNianian HsiangNianian enabled auto-merge (squash) March 21, 2026 06:16
@@ -0,0 +1,194 @@
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this file to models please, all stores placed at stores are legacy codes.

setError: (message: string) => void;
}

let unlisteners: UnlistenFn[] = [];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may cause unexpected memory leak, consider handle unlisteners and cleanup callbacks into your zustand store instead of using global vars.

@fu050409 fu050409 disabled auto-merge March 28, 2026 19:03
@HsiangNianian
Copy link
Copy Markdown
Member

@RockChinQ check

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 1, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
drop-out-docs Ready Ready Preview, Comment Apr 1, 2026 6:17am

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants