Skip to content

improve: 优化图片加载#2608

Merged
LinQingYuu merged 8 commits intodevfrom
improve/image-load
Mar 19, 2026
Merged

improve: 优化图片加载#2608
LinQingYuu merged 8 commits intodevfrom
improve/image-load

Conversation

@tangge233
Copy link
Copy Markdown
Contributor

@tangge233 tangge233 commented Mar 15, 2026

Close #1615

Summary by Sourcery

改进启动器中的图像加载鲁棒性和外观。

Bug 修复:

  • 通过以共享读/写访问方式打开位图文件流,防止图像文件被锁定的问题。
  • 当未设置或为负数时,避免对 MyImage 的圆角半径进行无效裁剪,从而防止渲染异常。

增强:

  • 重构在线图片加载逻辑,集中管理下载逻辑、复用并发下载任务,并改进错误处理和日志记录。
  • 调整 MyImage 默认圆角半径的行为,并在初始化时分发图像加载,以更好地与 XAML 属性初始化顺序对齐。
  • 为模组列表条目的 Logo 添加圆角和设备像素对齐样式,使外观更加整洁。
Original summary in English

Summary by Sourcery

Improve image loading robustness and appearance across the launcher.

Bug Fixes:

  • Prevent image file locking issues by opening bitmap file streams with shared read/write access.
  • Avoid invalid clipping when MyImage corner radius is unset or negative, preventing rendering glitches.

Enhancements:

  • Refactor online image loading to centralize download logic, reuse concurrent download tasks, and improve error handling and logging.
  • Adjust default MyImage corner radius behavior and dispatch image loading on initialization to better align with XAML property initialization order.
  • Style mod list item logos with rounded corners and device-pixel snapping for a cleaner appearance.

@pcl-ce-automation pcl-ce-automation bot added 🛠️ 等待审查 Pull Request 已完善,等待维护者或负责人进行代码审查 size: XL PR 大小评估:超大型 labels Mar 15, 2026
@tangge233
Copy link
Copy Markdown
Contributor Author

@sourcery-ai review

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Mar 16, 2026

审查者指南

重构 MyImage 的加载与裁剪行为,以改进异步图片下载与缓存;增加共享下载任务去重与更安全的临时文件处理;微调列表项徽标的渲染效果;并在读取图片时放宽位图文件共享限制。

MyImage 异步下载、缓存与回退的时序图

sequenceDiagram
    actor WPF as WPFFramework
    participant MI as MyImage
    participant Disp as Dispatcher
    participant Tasks as DownloadTasks
    participant Http as HttpRequestBuilder

    WPF->>MI: Initialized event
    MI->>MI: Load
    MI->>Disp: BeginInvoke(async delegate)
    activate Disp
    Disp->>MI: Async lambda execution
    MI->>MI: ActualSource = LoadingSource

    MI->>Tasks: GetOrAdd(Url, DownloadImage)
    alt New task for Url
        Tasks->>MI: Returns new Task for Url
        MI->>Http: Create(Url).SendAsync()
        Http-->>MI: HttpResponseMessage
        MI->>MI: Save stream to temp file
        MI->>MI: Move to cache path
        MI-->>Tasks: Return temp path
    else Existing task for Url
        Tasks-->>MI: Existing Task for Url
    end

    MI->>Tasks: await Task for Url
    alt Url download succeeded
        MI->>MI: ActualSource = urlTempPath
        Disp-->>MI: Async lambda returns
    else Url download failed or returned empty
        MI->>Tasks: GetOrAdd(FallbackSource, DownloadImage)
        alt New task for FallbackSource
            Tasks->>MI: Returns new Task for FallbackSource
            MI->>Http: Create(FallbackSource).SendAsync()
            Http-->>MI: HttpResponseMessage
            MI->>MI: Save stream to temp file
            MI->>MI: Move to fallback cache path
            MI-->>Tasks: Return fallback temp path
        else Existing task for FallbackSource
            Tasks-->>MI: Existing Task for FallbackSource
        end

        MI->>Tasks: await Task for FallbackSource
        alt Fallback succeeded
            MI->>MI: ActualSource = fallbackTempPath
        else Fallback failed or returned empty
            MI->>MI: Try load from local cache for Url
        end
    end

    Disp-->>WPF: UI updated with ActualSource
Loading

MyImage、MyBitmap 与 MyListItem 交互的更新类图

classDiagram
    class MyImage {
        +CornerRadius CornerRadius
        +String Source
        +String LoadingSource
        +String FallbackSource
        +Boolean EnableCache
        -String _ActualSource
        +String ActualSource
        -Shared ConcurrentDictionary~String, Task~String~~ _downloadTasks
        +Sub Load()
        +Sub UpdateClip()
        +Shared Function GetTempPath(Url As String) String
        -Shared Async Function DownloadImage(url As String) Task~String~
    }

    class MyBitmap {
        +Sub New()
        +Sub LoadFromPath(FilePathOrResourceName As String)
    }

    class MyListItem {
        +String Title
        +String Info
        +String Logo
        +Object Tags
        +Object Tag
        +Object PathLogo
        +Sub New()
    }

    MyListItem "1" o-- "1" MyImage : PathLogo
    MyBitmap ..> MyImage : loads image files for
    MyImage ..> ConcurrentDictionary : uses
    class ConcurrentDictionary {
        +Function GetOrAdd(key As String, valueFactory As Func~String, Task~String~~) Task~String~
        +Function Remove(key As String, value As Task~String~) Boolean
    }
Loading

DownloadImage 临时文件处理与清理的流程图

flowchart TD
    A[Start DownloadImage url] --> B[Compute tempPath = GetTempPath url]
    B --> C[Compute TempDownloadingPath = tempPath + random]
    C --> D[CreateDirectory GetPathFromFullPath tempPath]
    D --> E[Create FileStream TempDownloadingPath]
    E --> F[SendAsync HttpRequestBuilder for url]
    F --> G{response.EnsureSuccessStatusCode}
    G -->|Success| H[Get response.AsStreamAsync]
    H --> I[Copy network stream to FileStream]
    I --> J[Dispose streams]
    J --> K[File.Move TempDownloadingPath to tempPath]
    K --> L[Return tempPath]

    G -->|Throws exception| X[Catch Exception]
    X --> M{File.Exists tempPath}
    M -->|Yes| N[File.Delete tempPath]
    M -->|No| O[Skip delete tempPath]
    N --> P{File.Exists TempDownloadingPath}
    O --> P
    P -->|Yes| Q[File.Delete TempDownloadingPath]
    P -->|No| R[Skip delete TempDownloadingPath]
    Q --> S[Log failure with url and tempPath]
    R --> S
    S --> T[Return String.Empty]
    T --> U[End]
Loading

文件级变更

Change Details Files
调整 MyImage 的圆角默认值和裁剪逻辑,使圆角为可选,仅在进行配置时才应用。
  • 将默认 CornerRadius 从 0 改为 -1,以表示“未设置”状态。
  • 在 UpdateClip 中增加保护,仅当控件已有尺寸且顶部圆角半径为非负值时才应用裁剪几何。
Plain Craft Launcher 2/Controls/MyImage.vb
重构 MyImage 图片加载逻辑,使用调度器安排的异步下载、共享下载任务和更清晰的缓存处理。
  • 将 Load 事件处理程序从 Async Sub 改为普通 Sub,并在初始化时通过 dispatcher 派发一个异步 lambda。
  • 在开始网络请求之前立即显示 LoadingSource。
  • 引入以 URL 为键的下载任务 ConcurrentDictionary,用于共享正在进行中的下载。
  • 将 HTTP 下载与缓存逻辑抽取到 DownloadImage:写入临时文件,再移动到缓存路径,并返回最终路径;失败时返回空字符串。
  • 在下载失败时记录错误日志,并回退为从现有缓存文件加载,同时遵守缓存过期规则。
Plain Craft Launcher 2/Controls/MyImage.vb
改进位图文件加载方式,通过允许共享读取访问来避免图片文件被锁定。
  • 打开图片 FileStream 时使用 FileAccess.Read 和 FileShare.ReadWrite,而不是默认的共享模式,以减少当其他进程正在使用图片时的文件锁竞争。
Plain Craft Launcher 2/Modules/Base/MyBitmap.vb
使用针对 MyImage 的专门配置改进列表项中模组条目的徽标展示效果。
  • 在 ModComp.ToListItem 中构造 MyListItem 时使用对象初始化器语法,以提升可读性。
  • 将列表项的 PathLogo 配置为一个 MyImage,并设置 6px 的 CornerRadius,启用 SnapsToDevicePixels,以获得更清晰、带圆角的缩略图。
Plain Craft Launcher 2/Modules/Minecraft/ModComp.vb
对多处文件进行小型整理。
  • 在数个 VB/XAML 文件中规范 BOM/编码,不涉及行为变更。
Plain Craft Launcher 2/Modules/Minecraft/ModComp.vb
Plain Craft Launcher 2/Modules/Base/MyBitmap.vb
Plain Craft Launcher 2/Controls/MyListItem.xaml.vb
Plain Craft Launcher 2/Pages/PageInstance/MyLocalModItem.xaml
Plain Craft Launcher 2/Pages/PageSetup/PageSetupAbout.xaml

针对关联 Issue 的评估

Issue Objective Addressed Explanation
#1615 为下载/收藏列表中的图标添加圆角(样式与实例资源管理器一致)。
#1615 提升下载/收藏图标的渲染质量,使其看起来具有抗锯齿/更平滑。

提示与命令

与 Sourcery 交互

  • 触发新的审查: 在 pull request 上评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的审查评论。
  • 从审查评论生成 GitHub issue: 在回复某条审查评论时让 Sourcery 从该评论创建 issue。你也可以在审查评论下回复 @sourcery-ai issue,以从该评论创建一个 issue。
  • 生成 pull request 标题: 在 pull request 标题中的任意位置写入 @sourcery-ai,即可随时生成标题。你也可以在 pull request 中评论 @sourcery-ai title 来(重新)生成标题。
  • 生成 pull request 摘要: 在 pull request 正文中的任意位置写入 @sourcery-ai summary,即可在该位置生成 PR 摘要。你也可以在 pull request 中评论 @sourcery-ai summary 来(重新)生成摘要。
  • 生成审查者指南: 在 pull request 上评论 @sourcery-ai guide,即可随时(重新)生成审查者指南。
  • 解决所有 Sourcery 评论: 在 pull request 上评论 @sourcery-ai resolve,以标记解决所有 Sourcery 评论。如果你已经处理完所有评论且不希望再看到它们,这会很有用。
  • 撤销所有 Sourcery 审查: 在 pull request 上评论 @sourcery-ai dismiss,以撤销所有现有的 Sourcery 审查。如果你希望从一个全新的审查开始,尤其有用——别忘了随后再评论 @sourcery-ai review 触发新的审查!

自定义你的使用体验

访问你的 dashboard 以:

  • 启用或禁用审查功能,例如 Sourcery 生成的 pull request 摘要、审查者指南等。
  • 更改审查语言。
  • 添加、移除或编辑自定义审查指令。
  • 调整其他审查设置。

获取帮助

Original review guide in English

Reviewer's Guide

Refactors MyImage’s loading and clipping behavior to improve async image downloading and caching, adds shared download task de-duplication and safer temp file handling, tweaks list item logo rendering, and relaxes bitmap file sharing when reading images.

Sequence diagram for MyImage async download, caching, and fallback

sequenceDiagram
    actor WPF as WPFFramework
    participant MI as MyImage
    participant Disp as Dispatcher
    participant Tasks as DownloadTasks
    participant Http as HttpRequestBuilder

    WPF->>MI: Initialized event
    MI->>MI: Load
    MI->>Disp: BeginInvoke(async delegate)
    activate Disp
    Disp->>MI: Async lambda execution
    MI->>MI: ActualSource = LoadingSource

    MI->>Tasks: GetOrAdd(Url, DownloadImage)
    alt New task for Url
        Tasks->>MI: Returns new Task for Url
        MI->>Http: Create(Url).SendAsync()
        Http-->>MI: HttpResponseMessage
        MI->>MI: Save stream to temp file
        MI->>MI: Move to cache path
        MI-->>Tasks: Return temp path
    else Existing task for Url
        Tasks-->>MI: Existing Task for Url
    end

    MI->>Tasks: await Task for Url
    alt Url download succeeded
        MI->>MI: ActualSource = urlTempPath
        Disp-->>MI: Async lambda returns
    else Url download failed or returned empty
        MI->>Tasks: GetOrAdd(FallbackSource, DownloadImage)
        alt New task for FallbackSource
            Tasks->>MI: Returns new Task for FallbackSource
            MI->>Http: Create(FallbackSource).SendAsync()
            Http-->>MI: HttpResponseMessage
            MI->>MI: Save stream to temp file
            MI->>MI: Move to fallback cache path
            MI-->>Tasks: Return fallback temp path
        else Existing task for FallbackSource
            Tasks-->>MI: Existing Task for FallbackSource
        end

        MI->>Tasks: await Task for FallbackSource
        alt Fallback succeeded
            MI->>MI: ActualSource = fallbackTempPath
        else Fallback failed or returned empty
            MI->>MI: Try load from local cache for Url
        end
    end

    Disp-->>WPF: UI updated with ActualSource
Loading

Updated class diagram for MyImage, MyBitmap, and MyListItem interactions

classDiagram
    class MyImage {
        +CornerRadius CornerRadius
        +String Source
        +String LoadingSource
        +String FallbackSource
        +Boolean EnableCache
        -String _ActualSource
        +String ActualSource
        -Shared ConcurrentDictionary~String, Task~String~~ _downloadTasks
        +Sub Load()
        +Sub UpdateClip()
        +Shared Function GetTempPath(Url As String) String
        -Shared Async Function DownloadImage(url As String) Task~String~
    }

    class MyBitmap {
        +Sub New()
        +Sub LoadFromPath(FilePathOrResourceName As String)
    }

    class MyListItem {
        +String Title
        +String Info
        +String Logo
        +Object Tags
        +Object Tag
        +Object PathLogo
        +Sub New()
    }

    MyListItem "1" o-- "1" MyImage : PathLogo
    MyBitmap ..> MyImage : loads image files for
    MyImage ..> ConcurrentDictionary : uses
    class ConcurrentDictionary {
        +Function GetOrAdd(key As String, valueFactory As Func~String, Task~String~~) Task~String~
        +Function Remove(key As String, value As Task~String~) Boolean
    }
Loading

Flow diagram for DownloadImage temp file handling and cleanup

flowchart TD
    A[Start DownloadImage url] --> B[Compute tempPath = GetTempPath url]
    B --> C[Compute TempDownloadingPath = tempPath + random]
    C --> D[CreateDirectory GetPathFromFullPath tempPath]
    D --> E[Create FileStream TempDownloadingPath]
    E --> F[SendAsync HttpRequestBuilder for url]
    F --> G{response.EnsureSuccessStatusCode}
    G -->|Success| H[Get response.AsStreamAsync]
    H --> I[Copy network stream to FileStream]
    I --> J[Dispose streams]
    J --> K[File.Move TempDownloadingPath to tempPath]
    K --> L[Return tempPath]

    G -->|Throws exception| X[Catch Exception]
    X --> M{File.Exists tempPath}
    M -->|Yes| N[File.Delete tempPath]
    M -->|No| O[Skip delete tempPath]
    N --> P{File.Exists TempDownloadingPath}
    O --> P
    P -->|Yes| Q[File.Delete TempDownloadingPath]
    P -->|No| R[Skip delete TempDownloadingPath]
    Q --> S[Log failure with url and tempPath]
    R --> S
    S --> T[Return String.Empty]
    T --> U[End]
Loading

File-Level Changes

Change Details Files
Adjust MyImage corner radius defaults and clipping so rounded corners are optional and only applied when configured.
  • Change default CornerRadius from 0 to -1 to represent an unset state.
  • Guard UpdateClip so it only applies a clipping geometry when control has size and non-negative top corner radii.
Plain Craft Launcher 2/Controls/MyImage.vb
Rework MyImage image loading to use dispatcher-scheduled async downloads, shared download tasks, and clearer cache handling.
  • Change Load handler from Async Sub to a regular Sub that dispatches an async lambda on initialization.
  • Show LoadingSource immediately before starting network requests.
  • Introduce a ConcurrentDictionary of download tasks keyed by URL to share ongoing downloads.
  • Extract the HTTP download/caching logic into DownloadImage, which writes to a temp file, moves it to the cache path, and returns the final path or empty on failure.
  • On download failure, log the error and fall back to loading from any existing cache file, respecting cache expiration.
Plain Craft Launcher 2/Controls/MyImage.vb
Improve bitmap file loading to avoid locking image files by allowing shared read access.
  • Open image FileStream with FileAccess.Read and FileShare.ReadWrite instead of the default sharing mode to reduce file lock contention when images are in use by other processes.
Plain Craft Launcher 2/Modules/Base/MyBitmap.vb
Improve list item logo presentation using MyImage-specific configuration for mod items.
  • Use object initializer syntax when constructing MyListItem in ModComp.ToListItem for clarity.
  • Configure the list item’s PathLogo as a MyImage with a 6px CornerRadius and SnapsToDevicePixels enabled for sharper, rounded thumbnails.
Plain Craft Launcher 2/Modules/Minecraft/ModComp.vb
Minor housekeeping changes in various files.
  • Normalize BOM/encoding in several VB/XAML files without behavioral changes.
Plain Craft Launcher 2/Modules/Minecraft/ModComp.vb
Plain Craft Launcher 2/Modules/Base/MyBitmap.vb
Plain Craft Launcher 2/Controls/MyListItem.xaml.vb
Plain Craft Launcher 2/Pages/PageInstance/MyLocalModItem.xaml
Plain Craft Launcher 2/Pages/PageSetup/PageSetupAbout.xaml

Assessment against linked issues

Issue Objective Addressed Explanation
#1615 Add rounded corners to the icons shown in the download/favorites lists (same style as instance resource manager).
#1615 Improve the rendering quality of the download/favorites icons so that they appear anti-aliased/smooth.

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

Copy link
Copy Markdown

@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 - 我已经留下了一些整体上的反馈:

  • _downloadTasks.GetOrAdd 的调用中,ContinueWith 移除的是 FallbackSource 作为键,而不是真正的任务键(例如 Url/key),这会导致字典中主 URL 对应的条目无限期保留,并且错误地移除后备任务。
  • DownloadImage 中,File.Move(TempDownloadingPath, tempPath) 不再使用覆盖标志;如果缓存文件已经存在,这里会抛出异常并不必要地触发错误路径,实际上会导致无法刷新已有的缓存图片。
面向 AI 代理的提示词
Please address the comments from this code review:

## Overall Comments
- In the `_downloadTasks.GetOrAdd` calls, the `ContinueWith` removes the `FallbackSource` key instead of the actual task key (e.g., `Url`/`key`), which can leave entries for the primary URL in the dictionary indefinitely and incorrectly remove fallback tasks.
- In `DownloadImage`, `File.Move(TempDownloadingPath, tempPath)` no longer uses the overwrite flag; if a cache file already exists this will throw and trigger the error path unnecessarily, effectively making it impossible to refresh an existing cached image.

Sourcery 对开源项目永久免费——如果你觉得我们的代码评审有帮助,请考虑分享给更多人 ✨
帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进后续的评审。
Original comment in English

Hey - I've left some high level feedback:

  • In the _downloadTasks.GetOrAdd calls, the ContinueWith removes the FallbackSource key instead of the actual task key (e.g., Url/key), which can leave entries for the primary URL in the dictionary indefinitely and incorrectly remove fallback tasks.
  • In DownloadImage, File.Move(TempDownloadingPath, tempPath) no longer uses the overwrite flag; if a cache file already exists this will throw and trigger the error path unnecessarily, effectively making it impossible to refresh an existing cached image.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In the `_downloadTasks.GetOrAdd` calls, the `ContinueWith` removes the `FallbackSource` key instead of the actual task key (e.g., `Url`/`key`), which can leave entries for the primary URL in the dictionary indefinitely and incorrectly remove fallback tasks.
- In `DownloadImage`, `File.Move(TempDownloadingPath, tempPath)` no longer uses the overwrite flag; if a cache file already exists this will throw and trigger the error path unnecessarily, effectively making it impossible to refresh an existing cached image.

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.

@LinQingYuu LinQingYuu merged commit 3dfbd43 into dev Mar 19, 2026
3 checks passed
@pcl-ce-automation pcl-ce-automation bot added 👌 完成 相关问题已修复或功能已实现,计划在下次版本更新时正式上线 and removed 🛠️ 等待审查 Pull Request 已完善,等待维护者或负责人进行代码审查 labels Mar 19, 2026
@LinQingYuu LinQingYuu deleted the improve/image-load branch March 19, 2026 01:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size: XL PR 大小评估:超大型 👌 完成 相关问题已修复或功能已实现,计划在下次版本更新时正式上线

Projects

None yet

Development

Successfully merging this pull request may close these issues.

下载/收藏夹内的图标没有抗锯齿和圆角

3 participants