Skip to content

feat(instance): support listing world zips with extract - #1894

Open
icgnos wants to merge 5 commits into
mainfrom
feat/issue-1170
Open

feat(instance): support listing world zips with extract#1894
icgnos wants to merge 5 commits into
mainfrom
feat/issue-1170

Conversation

@icgnos

@icgnos icgnos commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Checklist

  • Changes have been tested locally and work as expected.
  • All tests in workflows pass successfully.
  • Documentation has been updated if necessary.
  • Code formatting and commit messages align with the project's conventions.
  • Comments have been added for any complex logic or functionality if possible.

This PR is a ..

  • 🆕 New feature
  • 🐞 Bug fix
  • 🛠 Refactoring
  • ⚡️ Performance improvement
  • 🌐 Internationalization
  • 📄 Documentation improvement
  • 🎨 Code style optimization
  • ❓ Other (Please specify below)

Related Issues

Description

rt,顺便修复了两个问题:

  • 没有正确处理嵌套文件夹(解压后是一个包含地图文件夹的文件夹)
  • 中文乱码

Additional Context

  • Add any other relevant information or screenshots here.
    image

    解压后不会自动删除,这是等待之后为存档添加删除按钮

Summary by Sourcery

Add support for listing and extracting zipped Minecraft worlds within instances, including proper handling of nested folders and encoded names.

New Features:

  • Allow retrieving world list and world details from .zip saves alongside directory-based worlds.
  • Expose an extract action in the instance worlds UI for zipped saves, showing a ZIP badge and rendering in-memory icons for zip-based worlds.

Bug Fixes:

  • Fix handling of archives that contain an extra nested world directory when importing resources.
  • Resolve garbled Chinese filenames in imported zip archives by decoding names with appropriate character encoding.

Enhancements:

  • Extend toast context to support programmatic closing and show a loading toast during long extract operations.
  • Generalize unique filename generation to optionally ignore file extensions, enabling cleaner extracted world directory names.

Build:

  • Add encoding_rs as a dependency for handling non-UTF-8 zip entry names.

@sourcery-ai

sourcery-ai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Adds support for listing and extracting world zip files in instances, fixes handling of nested world directories and Chinese filenames during extraction, and updates toast and world models/UI to accommodate these behaviors.

Sequence diagram for extracting a world zip into instance saves

sequenceDiagram
  actor User
  participant InstanceWorldsPage
  participant InstanceContext
  participant InstanceService
  participant InstanceCommands
  participant Fs

  User->>InstanceWorldsPage: click world extract (ZIP)
  InstanceWorldsPage->>InstanceContext: handleImportResources({ paths: [save.dirPath], decompress: true })
  InstanceContext->>InstanceContext: toast({ title: General.extracting, status: loading })
  InstanceContext->>InstanceService: copyResourcesToInstances(selectedPaths, [instanceId], InstanceSubdirType.Saves, true)
  InstanceService->>InstanceCommands: copyResourcesToInstances

  loop for each resource
    InstanceCommands->>Fs: generate_unique_filename(tgt_path, base_name, false)
    InstanceCommands->>Fs: ZipArchive::new + archive.extract(dest_path)
    alt dest_path has single inner dir
      InstanceCommands->>Fs: read_dir(dest_path)
      InstanceCommands->>InstanceCommands: decode_zip_name(first_component)
      InstanceCommands->>Fs: generate_unique_filename(tgt_path, .sjmcl_extract_decoded_name, false)
      InstanceCommands->>Fs: rename(inner_dir, tmp_path)
      InstanceCommands->>Fs: remove_dir_all(dest_path)
      InstanceCommands->>Fs: generate_unique_filename(tgt_path, decoded_name, false)
      InstanceCommands->>Fs: rename(tmp_path, final_path)
    end
  end

  InstanceCommands-->>InstanceService: SJMCLResult<CopyResourcesResponse>
  InstanceService-->>InstanceContext: response
  InstanceContext->>InstanceContext: toast.close(toastId)
  InstanceContext->>InstanceContext: toast({ title: response.message, status: success })
  InstanceContext->>InstanceWorldsPage: onSuccessCallback() -> getWorldListWrapper(true)
Loading

Sequence diagram for retrieving world list including zip saves

sequenceDiagram
  actor User
  participant InstanceWorldsPage
  participant InstanceService
  participant InstanceCommands
  participant WorldHelpers
  participant Fs

  User->>InstanceWorldsPage: open instance worlds page
  InstanceWorldsPage->>InstanceService: retrieveWorldList(instanceId)
  InstanceService->>InstanceCommands: retrieve_world_list

  InstanceCommands->>Fs: get_subdirectories(worlds_dir)
  loop for each world folder
    InstanceCommands->>WorldHelpers: load_world_info_from_dir(path, has_difficulty_support)
    WorldHelpers->>Fs: load_level_data_from_nbt
    WorldHelpers-->>InstanceCommands: WorldInfo { is_zip: false, icon_src: dir path string }
  end

  InstanceCommands->>Fs: get_files_with_regex(worlds_dir, zip_pattern)
  loop for each world zip
    InstanceCommands->>WorldHelpers: load_world_info_from_zip(path, has_difficulty_support)
    WorldHelpers->>Fs: File::open + ZipArchive::new
    WorldHelpers->>Fs: read level.dat + icon.png
    WorldHelpers->>WorldHelpers: deserialize::<Level>
    WorldHelpers-->>InstanceCommands: WorldInfo { is_zip: true, icon_src: base64 image string }
  end

  InstanceCommands-->>InstanceService: Vec<WorldInfo>
  InstanceService-->>InstanceWorldsPage: worldList
  InstanceWorldsPage->>InstanceWorldsPage: render OptionItem
  InstanceWorldsPage->>InstanceWorldsPage: show Tag ZIP when world.isZip
  InstanceWorldsPage->>InstanceWorldsPage: use base64ImgSrc(world.iconSrc) for ZIP, convertFileSrc for folders
Loading

File-Level Changes

Change Details Files
Support loading world metadata and level data directly from zipped world saves.
  • Introduce load_world_info_from_zip to scan zip entries for level.dat and icon.png, deserialize level data, and compress the icon image into a string field.
  • Introduce load_world_data_from_zip to read level.dat from a world zip and return LevelData.
  • Update WorldInfo model so icon_src is a String and add is_zip flag to distinguish zipped worlds.
src-tauri/src/instance/helpers/world.rs
src-tauri/src/instance/models/world/base.rs
src/models/instance/world.ts
Enable instances to list world zip files alongside directories and view their details.
  • In retrieve_world_list, collect .zip files in the saves directory and append WorldInfo objects from load_world_info_from_zip.
  • In retrieve_world_details, fall back to reading level.dat from a corresponding world zip when the world directory is missing.
  • Wire new world helpers into instance commands via updated imports.
src-tauri/src/instance/commands.rs
Improve resource import/extraction to handle nested folders and non-UTF-8 (e.g., GBK) zip entry names while preventing unwanted extensions in generated folder names.
  • Extend generate_unique_filename with a keep_extension flag and update all call sites to preserve extensions only for files, not extracted directories.
  • After extracting a world zip, detect the single nested directory case, decode its name using raw zip entry bytes with UTF-8/GBK fallback, and rename/move it to a flattened directory under the instance saves path.
  • Add decode_zip_name helper using encoding_rs to decode non-UTF-8 zip names and include encoding_rs in Cargo.toml workspace dependencies.
src-tauri/src/utils/fs.rs
src-tauri/src/instance/commands.rs
src-tauri/Cargo.toml
src-tauri/Cargo.lock
Update the UI to represent zipped worlds and allow in-place extraction from the world list.
  • Add an extract action (using LuArchiveRestore icon) for zipped saves that calls handleImportResources with decompress=true and then refreshes the world list.
  • Retain the quick play launch action for non-zip saves.
  • Render a ZIP tag next to zipped worlds and adjust icon rendering to use base64ImgSrc for zip-based icons versus convertFileSrc for directory-based icons.
src/pages/instances/details/[id]/worlds.tsx
Enhance toast handling to support programmatic closing, enabling progress indication during long-running operations like extraction.
  • Change ToastContextType to be a callable that also exposes a close(id) method.
  • Refactor ToastContextProvider to build the toast function via useMemo and attach a close method that proxies to chakraToast.close.
  • Use the new toast.close API in instance context to show a loading toast while decompressing resources and close it on completion; also integrate i18n for the extracting label.
src/contexts/toast.tsx
src/contexts/instance.tsx
src/locales/en.json
src/locales/zh-Hans.json

Assessment against linked issues

Issue Objective Addressed Explanation
#1170 Support listing both folder-based and zip-based world saves, with helper functions to read basic world data (including level.dat and icon) from directories and zip files.
#1170 Enhance the instance world list UI/UX to clearly distinguish zip saves and provide dedicated actions (extract and delete) for zip-based worlds. The PR adds an isZip flag, shows a ZIP tag for zip entries, and provides an extract action that uses the import/decompress flow, but it explicitly does not implement a delete action for zip saves yet (the description notes deletion will be added later). Therefore only part of the requested UX (extract) is implemented, not the full extract-and-delete functionality.

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

@github-actions github-actions Bot added the size/L Denotes a PR that changes 100-499 lines, ignoring generated files. label Aug 7, 2026

@sourcery-ai sourcery-ai Bot left a comment

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.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src-tauri/src/utils/fs.rs" line_range="53-62" />
<code_context>
 /// ```
-pub fn generate_unique_filename(base_path: &Path, filename: &OsStr) -> PathBuf {
-  let (name, extension) = split_filename(filename);
+pub fn generate_unique_filename(
+  base_path: &Path,
+  filename: &OsStr,
+  keep_extension: bool,
+) -> PathBuf {
+  let (name, extension) = if keep_extension {
+    split_filename(filename)
+  } else {
+    (filename.to_string_lossy().into_owned(), String::new())
+  };
   let mut dest_path = base_path.join(filename);
   let mut counter = 1;

</code_context>
<issue_to_address>
**issue (bug_risk):** The `keep_extension` flag isn't fully honored for the initial destination path.

When `keep_extension` is `false`, `dest_path` is still built from the original `filename` (including extension), and only numbered variants omit the extension. This makes the flag’s behavior inconsistent and may let an unwanted extension (e.g. `.zip`) slip through on the first path. Please build the initial `dest_path` from `name`/`extension` so `keep_extension` consistently applies to both the initial and numbered paths.
</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.

Comment thread src-tauri/src/utils/fs.rs
Comment on lines +53 to +62
pub fn generate_unique_filename(
base_path: &Path,
filename: &OsStr,
keep_extension: bool,
) -> PathBuf {
let (name, extension) = if keep_extension {
split_filename(filename)
} else {
(filename.to_string_lossy().into_owned(), String::new())
};

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.

issue (bug_risk): The keep_extension flag isn't fully honored for the initial destination path.

When keep_extension is false, dest_path is still built from the original filename (including extension), and only numbered variants omit the extension. This makes the flag’s behavior inconsistent and may let an unwanted extension (e.g. .zip) slip through on the first path. Please build the initial dest_path from name/extension so keep_extension consistently applies to both the initial and numbered paths.

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

Labels

size/L Denotes a PR that changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] 存档下载相关 UX 提升

1 participant