Skip to content

feat(sticker): 支持自定义底图#30

Merged
xiaocaoooo merged 2 commits into
mainfrom
dev
May 2, 2026
Merged

feat(sticker): 支持自定义底图#30
xiaocaoooo merged 2 commits into
mainfrom
dev

Conversation

@xiaocaoooo
Copy link
Copy Markdown
Member

@xiaocaoooo xiaocaoooo commented May 2, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Users can now select custom background images for stickers
    • Supports PNG, JPEG, GIF, and WebP formats (max 5MB file size)
    • Option to clear custom backgrounds
    • Theme colors automatically adapt based on custom background selection
  • Chores

    • Added cross-platform image selection support for Windows, macOS, and Linux
    • Updated translations across English, Japanese, and Chinese languages

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 2, 2026

Warning

Rate limit exceeded

@xiaocaoooo has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 27 minutes and 22 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a71c8073-d651-4a02-848e-97f3e3eecdae

📥 Commits

Reviewing files that changed from the base of the PR and between 5360574 and 605af85.

📒 Files selected for processing (1)
  • lib/sticker.dart
📝 Walkthrough

Walkthrough

Custom background image support is added to the sticker generator. Users can select gallery images (with size/format validation), save them to app storage, and regenerate stickers with these backgrounds. Localization strings, platform plugins, preference persistence, and UI components enable the full workflow.

Changes

Custom Background Feature

Layer / File(s) Summary
Localization
lib/l10n/app_en.arb, app_ja.arb, app_zh.arb, app_zh_Hant.arb
New localization keys added: customBackground, customBackgroundHint, clearCustomBackground, invalidImageFormat, fileTooLarge across four language variants.
Dependencies & Platform Plugins
pubspec.yaml, linux/flutter/generated_plugin_registrant.cc, linux/flutter/generated_plugins.cmake, macos/Flutter/GeneratedPluginRegistrant.swift, windows/flutter/generated_plugin_registrant.cc, windows/flutter/generated_plugins.cmake
image_picker dependency added; file_selector plugin registered for Linux, macOS, and Windows platforms.
Core Sticker Generation
lib/sticker.dart
PjskGenerator.pjsk method signature extended with optional customImageBytes parameter; custom image decoding with fallback to default asset when invalid.
State & Preference Management
lib/pages/sticker/sticker_page_logic.dart
Preference loading/saving for custom background path; new _pickCustomBackground method validates image format (PNG/JPEG/GIF/WebP) via magic bytes, enforces 5MB size limit, and saves to app documents; _isValidImageBytes validates format; _clearCustomBackground removes persisted background; _getThemeColor returns primary color scheme when custom background is active.
Layer Coordination
lib/pages/sticker/sticker_page_layers.dart
_resetPreferences now asynchronously deletes custom background file and removes persisted preference before recreating sticker.
UI Components
lib/pages/sticker.dart, lib/pages/sticker/sticker_page_picker.dart
Character selection ListTile displays custom background image preview with clear button when active; _buildCustomBackgroundButton added to picker sheet for initiating background selection.

Sequence Diagram

sequenceDiagram
    actor User
    participant UI as Sticker Page UI
    participant Picker as Image Picker
    participant FileSystem as File System
    participant Validator as Image Validator
    participant Prefs as SharedPreferences
    participant Generator as Sticker Generator

    User->>UI: Tap "Custom Background"
    UI->>Picker: Open gallery
    Picker-->>User: Select image
    Picker-->>UI: Return image file
    UI->>Validator: Validate format & size
    alt Invalid format or too large
        Validator-->>UI: Show error (invalidImageFormat/fileTooLarge)
    else Valid
        Validator-->>UI: Format/size OK
        UI->>FileSystem: Copy image to app docs
        FileSystem-->>UI: File saved
        UI->>Prefs: Persist customBgPath
        Prefs-->>UI: Preference saved
        UI->>Generator: Generate sticker with customImageBytes
        Generator-->>UI: Sticker bytes
        UI-->>User: Display sticker with custom background
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~85 minutes

Possibly related PRs

  • PR #22: Introduced or modified the same lib/l10n/app_*.arb localization files that this PR extends with new custom background feature strings.
  • PR #21: Previously modified lib/sticker.dart and the PjskGenerator.pjsk method, which this PR extends with customImageBytes parameter support.

Poem

A rabbit hops through gardens bright,
With custom backdrops in sight!
Pick an image, trim its size,
Watch your sticker mesmerize—
Backgrounds bloom at last! 🐰✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding support for custom background images in the sticker feature, which is the primary objective across all modified files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 27 minutes and 22 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (1)
lib/pages/sticker/sticker_page_layers.dart (1)

111-115: 💤 Low value

Async function called without await in dialog callback.

_resetPreferences() is now async, but it's called without await at line 113. While the synchronous state update in _update() executes immediately, Navigator.pop(context) runs before the SharedPreferences cleanup and _createSticker() complete. This is mostly safe due to the generation ID pattern in _createSticker(), but could cause subtle issues if the user quickly takes another action.

Consider either awaiting the call or documenting that fire-and-forget is intentional:

Proposed fix
             TextButton(
-              onPressed: () {
-                _resetPreferences();
+              onPressed: () async {
+                await _resetPreferences();
                 Navigator.pop(context);
               },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/pages/sticker/sticker_page_layers.dart` around lines 111 - 115, The
dialog callback calls the now-async _resetPreferences() without awaiting it, so
Navigator.pop(context) can run before preference cleanup and _createSticker()
complete; make the onPressed handler asynchronous and await _resetPreferences()
(and any subsequent async work like _createSticker() if invoked there) before
calling Navigator.pop(context), or explicitly document/fire-and-forget intent if
awaiting is not desired; update the TextButton onPressed to async and await
_resetPreferences() (or chain a .then(...) to call Navigator.pop only after
completion) and ensure related methods _update() and _createSticker() behavior
remains consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/l10n/app_zh.arb`:
- Line 161: Replace the half-width ASCII comma in the localization value for the
key "invalidImageFormat" with a full-width Chinese comma (,); update the string
value for invalidImageFormat so it reads using the full-width comma between
clauses to match Simplified Chinese punctuation standards.

In `@lib/pages/sticker.dart`:
- Around line 146-161: The UI uses Image.file(File(_customBgPath!)) and shows
the custom background button unconditionally, which will crash on web; update
the code to guard native-only file operations with kIsWeb checks: 1) In the
picker button builder (_buildCustomBackgroundButton) only render the custom
background button when !kIsWeb so the button that calls _pickCustomBackground()
is hidden on web; 2) In the widget tree where _customBgPath is used (the leading
Image.file usage) wrap or replace that branch with a kIsWeb conditional so
Image.file(File(...)) is only executed on non-web platforms (or ensure
_customBgPath is never set on web), and keep the errorBuilder/fallback for
web-safe rendering. Ensure references: _customBgPath,
_buildCustomBackgroundButton(), _pickCustomBackground(), and Image.file are the
points you modify.

In `@lib/pages/sticker/sticker_page_logic.dart`:
- Around line 238-246: Wrap the file I/O around
getApplicationDocumentsDirectory() and customBgFile.writeAsBytes(bytes) in a
try-catch so disk/permission/platform errors cannot crash the app; on success
update state with _update(() { _customBgPath = customBgFile.path; }) and call
_createSticker(), but on failure catch the exception, log it, and show
user-facing feedback via ScaffoldMessenger (e.g.,
ScaffoldMessenger.of(context).showSnackBar) and ensure you do not set
_customBgPath or call _createSticker() when the write fails; also consider
rethrowing or returning after handling if the calling flow expects it.
- Around line 12-18: Wrap the file existence check for customBgPath in a
try-catch inside the same block that creates File(customBgPath) to catch
FileSystemException (and general exceptions); on error set customBgPath = null,
call await prefs.remove('customBgPath'), and show user feedback via
ScaffoldMessenger (e.g., a SnackBar) explaining the background could not be
loaded; ensure the catch does not rethrow and that normal exists() logic remains
in the try body so File IO is protected.

In `@lib/pages/sticker/sticker_page_picker.dart`:
- Around line 155-159: The post-await route pop uses the widget state check
`mounted`, which refers to the parent `_StickerPageState` and can wrongly pop
the app route if the bottom sheet was dismissed; update the async handler so it
checks the BuildContext's mounted flag (`ctx.mounted`) before calling
`nav.pop()` after awaiting `_pickCustomBackground()`, keeping the existing
`Navigator.of(ctx)`/`nav.pop()` usage and ensuring the guard uses `ctx.mounted`.

In `@lib/sticker.dart`:
- Around line 1102-1103: The if statement using kDebugMode at the print call
must use braces to satisfy the curly_braces_in_flow_control_structures lint;
update the conditional that currently reads the single-line call to
print("Invalid custom image, falling back to default: $e") so the body is
wrapped in a block (e.g., if (kDebugMode) { ... }), locating the occurrence
around the print referencing the invalid custom image fallback.
- Around line 1095-1109: The try/catch around
ui.instantiateImageCodec/customImageBytes can leak the codec if getNextFrame()
throws; ensure the ImageCodec (variable codec) is disposed in a finally block so
codec.dispose() always runs and only dispose frame.image when frame was
successfully obtained; adjust the logic around ui.instantiateImageCodec,
codec.getNextFrame, frame.image.dispose and codec.dispose so bgImageBytes is set
to customImageBytes only after successful decode, and on failure fall back to
rootBundle.load(...) as before.

---

Nitpick comments:
In `@lib/pages/sticker/sticker_page_layers.dart`:
- Around line 111-115: The dialog callback calls the now-async
_resetPreferences() without awaiting it, so Navigator.pop(context) can run
before preference cleanup and _createSticker() complete; make the onPressed
handler asynchronous and await _resetPreferences() (and any subsequent async
work like _createSticker() if invoked there) before calling
Navigator.pop(context), or explicitly document/fire-and-forget intent if
awaiting is not desired; update the TextButton onPressed to async and await
_resetPreferences() (or chain a .then(...) to call Navigator.pop only after
completion) and ensure related methods _update() and _createSticker() behavior
remains consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2fb4f4d1-e9c8-4091-8cb4-5e1d9e822a2c

📥 Commits

Reviewing files that changed from the base of the PR and between f8faa8f and 5360574.

⛔ Files ignored due to path filters (1)
  • pubspec.lock is excluded by !**/*.lock
📒 Files selected for processing (15)
  • lib/l10n/app_en.arb
  • lib/l10n/app_ja.arb
  • lib/l10n/app_zh.arb
  • lib/l10n/app_zh_Hant.arb
  • lib/pages/sticker.dart
  • lib/pages/sticker/sticker_page_layers.dart
  • lib/pages/sticker/sticker_page_logic.dart
  • lib/pages/sticker/sticker_page_picker.dart
  • lib/sticker.dart
  • linux/flutter/generated_plugin_registrant.cc
  • linux/flutter/generated_plugins.cmake
  • macos/Flutter/GeneratedPluginRegistrant.swift
  • pubspec.yaml
  • windows/flutter/generated_plugin_registrant.cc
  • windows/flutter/generated_plugins.cmake

Comment thread lib/l10n/app_zh.arb
"customBackground": "自定义底图",
"customBackgroundHint": "从相册选择图片作为底图",
"clearCustomBackground": "清除自定义底图",
"invalidImageFormat": "不支持的图片格式,仅支持 PNG、JPEG、GIF 和 WebP",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use full-width Chinese comma in invalidImageFormat.

"不支持的图片格式,仅支持..." uses a half-width ASCII comma without a space. The Traditional Chinese locale uses (full-width comma), which is the standard in Simplified Chinese copy as well.

✏️ Proposed fix
-  "invalidImageFormat": "不支持的图片格式,仅支持 PNG、JPEG、GIF 和 WebP",
+  "invalidImageFormat": "不支持的图片格式,仅支持 PNG、JPEG、GIF 和 WebP",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"invalidImageFormat": "不支持的图片格式,仅支持 PNG、JPEG、GIF 和 WebP",
"invalidImageFormat": "不支持的图片格式仅支持 PNG、JPEG、GIF 和 WebP",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/l10n/app_zh.arb` at line 161, Replace the half-width ASCII comma in the
localization value for the key "invalidImageFormat" with a full-width Chinese
comma (,); update the string value for invalidImageFormat so it reads using the
full-width comma between clauses to match Simplified Chinese punctuation
standards.

Comment thread lib/pages/sticker.dart
Comment on lines +146 to +161
leading:
_customBgPath != null
? ClipRRect(
borderRadius: BorderRadius.circular(4),
child: Image.file(
File(_customBgPath!),
key: ValueKey(_customBgPath),
width: 24,
height: 24,
fit: BoxFit.cover,
cacheWidth: 48,
cacheHeight: 48,
errorBuilder:
(ctx, err, st) =>
const SizedBox(width: 24, height: 24),
),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if _pickCustomBackground guards against web
rg -n "kIsWeb\|Platform\.is" --type=dart -A2 -B2

Repository: Parallel-SEKAI/PJSK-Sticker

Length of output: 53


🏁 Script executed:

# First, verify the actual code at lines 146-161 in sticker.dart
head -n 161 lib/pages/sticker.dart | tail -n 16

Repository: Parallel-SEKAI/PJSK-Sticker

Length of output: 869


🏁 Script executed:

# Find sticker_page_logic.dart and check _pickCustomBackground
fd "sticker_page_logic" --type f

Repository: Parallel-SEKAI/PJSK-Sticker

Length of output: 113


🏁 Script executed:

# Check imports at the top of sticker.dart
head -n 20 lib/pages/sticker.dart

Repository: Parallel-SEKAI/PJSK-Sticker

Length of output: 895


🏁 Script executed:

# Find _pickCustomBackground function in sticker_page_logic.dart
rg -n "_pickCustomBackground" lib/pages/sticker/sticker_page_logic.dart -A 20

Repository: Parallel-SEKAI/PJSK-Sticker

Length of output: 688


🏁 Script executed:

# Also check if there are any web guards in sticker_page_logic.dart
rg -n "kIsWeb\|Platform\.is" lib/pages/sticker/sticker_page_logic.dart -B 2 -A 2

Repository: Parallel-SEKAI/PJSK-Sticker

Length of output: 53


🏁 Script executed:

# Get more lines from _pickCustomBackground to see where _customBgPath is set
sed -n '207,270p' lib/pages/sticker/sticker_page_logic.dart

Repository: Parallel-SEKAI/PJSK-Sticker

Length of output: 1749


🏁 Script executed:

# Search for where _pickCustomBackground is called
rg -n "_pickCustomBackground\(\)" lib/pages/sticker/ -B 5 -A 2

Repository: Parallel-SEKAI/PJSK-Sticker

Length of output: 1320


🏁 Script executed:

# Get more context around the button call at line 157
sed -n '140,170p' lib/pages/sticker/sticker_page_picker.dart

Repository: Parallel-SEKAI/PJSK-Sticker

Length of output: 846


🏁 Script executed:

# Search for calls to _buildCustomBackgroundButton
rg -n "_buildCustomBackgroundButton" lib/pages/sticker/ -B 2 -A 2

Repository: Parallel-SEKAI/PJSK-Sticker

Length of output: 868


🏁 Script executed:

# Get context around line 86 where button is called
sed -n '75,100p' lib/pages/sticker/sticker_page_picker.dart

Repository: Parallel-SEKAI/PJSK-Sticker

Length of output: 1077


Add kIsWeb guard before Image.file(File(_customBgPath!)) and gate the custom background button on native platforms — coding guideline violation.

dart:io's File, getApplicationDocumentsDirectory(), and Image.file() are unsupported on web. The _pickCustomBackground() function in sticker_page_logic.dart (lines 237–239) uses getApplicationDocumentsDirectory() and File.writeAsBytes(), which will crash on web. The button that triggers this function (line 86 in sticker_page_picker.dart) is shown unconditionally. The coding guidelines require kIsWeb/Platform.is... checks to differentiate file operations between web and native.

The fix requires two changes:

  1. Gate the custom background button visibility on !kIsWeb in _buildCustomBackgroundButton().
  2. Add a kIsWeb check in the UI render at lines 150–161, or ensure _customBgPath is never set on web.
🛡️ Proposed fix for the thumbnail
                    leading:
                        _customBgPath != null
                            ? ClipRRect(
                              borderRadius: BorderRadius.circular(4),
-                             child: Image.file(
-                               File(_customBgPath!),
-                               key: ValueKey(_customBgPath),
-                               width: 24,
-                               height: 24,
-                               fit: BoxFit.cover,
-                               cacheWidth: 48,
-                               cacheHeight: 48,
-                               errorBuilder:
-                                   (ctx, err, st) =>
-                                       const SizedBox(width: 24, height: 24),
-                             ),
+                             child: !kIsWeb
+                                 ? Image.file(
+                                     File(_customBgPath!),
+                                     key: ValueKey(_customBgPath),
+                                     width: 24,
+                                     height: 24,
+                                     fit: BoxFit.cover,
+                                     cacheWidth: 48,
+                                     cacheHeight: 48,
+                                     errorBuilder:
+                                         (ctx, err, st) =>
+                                             const SizedBox(width: 24, height: 24),
+                                   )
+                                 : const Icon(Icons.image, size: 24),
                            )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
leading:
_customBgPath != null
? ClipRRect(
borderRadius: BorderRadius.circular(4),
child: Image.file(
File(_customBgPath!),
key: ValueKey(_customBgPath),
width: 24,
height: 24,
fit: BoxFit.cover,
cacheWidth: 48,
cacheHeight: 48,
errorBuilder:
(ctx, err, st) =>
const SizedBox(width: 24, height: 24),
),
leading:
_customBgPath != null
? ClipRRect(
borderRadius: BorderRadius.circular(4),
child: !kIsWeb
? Image.file(
File(_customBgPath!),
key: ValueKey(_customBgPath),
width: 24,
height: 24,
fit: BoxFit.cover,
cacheWidth: 48,
cacheHeight: 48,
errorBuilder:
(ctx, err, st) =>
const SizedBox(width: 24, height: 24),
)
: const Icon(Icons.image, size: 24),
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/pages/sticker.dart` around lines 146 - 161, The UI uses
Image.file(File(_customBgPath!)) and shows the custom background button
unconditionally, which will crash on web; update the code to guard native-only
file operations with kIsWeb checks: 1) In the picker button builder
(_buildCustomBackgroundButton) only render the custom background button when
!kIsWeb so the button that calls _pickCustomBackground() is hidden on web; 2) In
the widget tree where _customBgPath is used (the leading Image.file usage) wrap
or replace that branch with a kIsWeb conditional so Image.file(File(...)) is
only executed on non-web platforms (or ensure _customBgPath is never set on
web), and keep the errorBuilder/fallback for web-safe rendering. Ensure
references: _customBgPath, _buildCustomBackgroundButton(),
_pickCustomBackground(), and Image.file are the points you modify.

Comment on lines +12 to +18
if (customBgPath != null) {
final file = File(customBgPath);
if (!await file.exists()) {
customBgPath = null;
await prefs.remove('customBgPath');
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing try-catch around file existence check.

file.exists() can throw FileSystemException on invalid paths or permission issues. As per coding guidelines, file I/O operations should be wrapped in try-catch blocks.

Proposed fix
     if (customBgPath != null) {
-      final file = File(customBgPath);
-      if (!await file.exists()) {
+      try {
+        final file = File(customBgPath);
+        if (!await file.exists()) {
+          customBgPath = null;
+          await prefs.remove('customBgPath');
+        }
+      } catch (e) {
+        if (kDebugMode) print('Error checking custom background: $e');
         customBgPath = null;
         await prefs.remove('customBgPath');
       }
     }

As per coding guidelines: "Wrap all asset loading and file I/O operations in try-catch blocks with ScaffoldMessenger feedback to users"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/pages/sticker/sticker_page_logic.dart` around lines 12 - 18, Wrap the
file existence check for customBgPath in a try-catch inside the same block that
creates File(customBgPath) to catch FileSystemException (and general
exceptions); on error set customBgPath = null, call await
prefs.remove('customBgPath'), and show user feedback via ScaffoldMessenger
(e.g., a SnackBar) explaining the background could not be loaded; ensure the
catch does not rethrow and that normal exists() logic remains in the try body so
File IO is protected.

Comment on lines +238 to +246
// 复制到应用私有目录
final appDir = await getApplicationDocumentsDirectory();
final customBgFile = File('${appDir.path}/custom_background.png');
await customBgFile.writeAsBytes(bytes);

_update(() {
_customBgPath = customBgFile.path;
});
_createSticker();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing try-catch for file write operation.

getApplicationDocumentsDirectory() and writeAsBytes() can throw exceptions (disk full, permissions, platform unsupported). This could crash the app or leave the UI in an inconsistent state.

Proposed fix
-    // 复制到应用私有目录
-    final appDir = await getApplicationDocumentsDirectory();
-    final customBgFile = File('${appDir.path}/custom_background.png');
-    await customBgFile.writeAsBytes(bytes);
-
-    _update(() {
-      _customBgPath = customBgFile.path;
-    });
-    _createSticker();
+    // 复制到应用私有目录
+    try {
+      final appDir = await getApplicationDocumentsDirectory();
+      final customBgFile = File('${appDir.path}/custom_background.png');
+      await customBgFile.writeAsBytes(bytes);
+
+      _update(() {
+        _customBgPath = customBgFile.path;
+      });
+      _createSticker();
+    } catch (e) {
+      if (kDebugMode) print('Failed to save custom background: $e');
+      if (mounted) {
+        ScaffoldMessenger.of(context).showSnackBar(
+          SnackBar(content: Text(S.of(context).saveFailed)),
+        );
+      }
+    }

As per coding guidelines: "Wrap all asset loading and file I/O operations in try-catch blocks with ScaffoldMessenger feedback to users"

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 复制到应用私有目录
final appDir = await getApplicationDocumentsDirectory();
final customBgFile = File('${appDir.path}/custom_background.png');
await customBgFile.writeAsBytes(bytes);
_update(() {
_customBgPath = customBgFile.path;
});
_createSticker();
// 复制到应用私有目录
try {
final appDir = await getApplicationDocumentsDirectory();
final customBgFile = File('${appDir.path}/custom_background.png');
await customBgFile.writeAsBytes(bytes);
_update(() {
_customBgPath = customBgFile.path;
});
_createSticker();
} catch (e) {
if (kDebugMode) print('Failed to save custom background: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(S.of(context).saveFailed)),
);
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/pages/sticker/sticker_page_logic.dart` around lines 238 - 246, Wrap the
file I/O around getApplicationDocumentsDirectory() and
customBgFile.writeAsBytes(bytes) in a try-catch so disk/permission/platform
errors cannot crash the app; on success update state with _update(() {
_customBgPath = customBgFile.path; }) and call _createSticker(), but on failure
catch the exception, log it, and show user-facing feedback via ScaffoldMessenger
(e.g., ScaffoldMessenger.of(context).showSnackBar) and ensure you do not set
_customBgPath or call _createSticker() when the write fails; also consider
rethrowing or returning after handling if the calling flow expects it.

Comment on lines +155 to +159
onPressed: () async {
final nav = Navigator.of(ctx);
await _pickCustomBackground();
if (mounted) nav.pop();
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use ctx.mounted instead of mounted to guard nav.pop() after the async gap.

mounted checks the parent _StickerPageState, not the bottom sheet's route. If the sheet is dismissed by a swipe while _pickCustomBackground() is running, nav.pop() fires after the sheet is already gone and pops the wrong route. Since the minimum SDK is Dart 3.7, BuildContext.mounted is available and is the correct guard here.

🐛 Proposed fix
-        onPressed: () async {
-          final nav = Navigator.of(ctx);
-          await _pickCustomBackground();
-          if (mounted) nav.pop();
-        },
+        onPressed: () async {
+          await _pickCustomBackground();
+          if (mounted && ctx.mounted) Navigator.of(ctx).pop();
+        },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/pages/sticker/sticker_page_picker.dart` around lines 155 - 159, The
post-await route pop uses the widget state check `mounted`, which refers to the
parent `_StickerPageState` and can wrongly pop the app route if the bottom sheet
was dismissed; update the async handler so it checks the BuildContext's mounted
flag (`ctx.mounted`) before calling `nav.pop()` after awaiting
`_pickCustomBackground()`, keeping the existing `Navigator.of(ctx)`/`nav.pop()`
usage and ensuring the guard uses `ctx.mounted`.

Comment thread lib/sticker.dart
Comment on lines +1095 to +1109
try {
final codec = await ui.instantiateImageCodec(customImageBytes);
final frame = await codec.getNextFrame();
frame.image.dispose();
codec.dispose();
bgImageBytes = customImageBytes;
} catch (e) {
if (kDebugMode)
print("Invalid custom image, falling back to default: $e");
// 回退到 miku1
final bgImageData = await rootBundle.load(
"assets/characters/miku/miku1.png",
);
bgImageBytes = bgImageData.buffer.asUint8List();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Codec resource leak when getNextFrame() throws.

If codec.getNextFrame() raises, execution jumps to the catch block and codec.dispose() is never called. Move disposal to finally:

🛡️ Proposed fix
-      try {
-        final codec = await ui.instantiateImageCodec(customImageBytes);
-        final frame = await codec.getNextFrame();
-        frame.image.dispose();
-        codec.dispose();
-        bgImageBytes = customImageBytes;
-      } catch (e) {
-        if (kDebugMode)
-          print("Invalid custom image, falling back to default: $e");
-        // 回退到 miku1
-        final bgImageData = await rootBundle.load(
-          "assets/characters/miku/miku1.png",
-        );
-        bgImageBytes = bgImageData.buffer.asUint8List();
-      }
+      ui.Codec? codec;
+      try {
+        codec = await ui.instantiateImageCodec(customImageBytes);
+        final frame = await codec.getNextFrame();
+        frame.image.dispose();
+        bgImageBytes = customImageBytes;
+      } catch (e) {
+        if (kDebugMode) {
+          print("Invalid custom image, falling back to default: $e");
+        }
+        // 回退到 miku1
+        final bgImageData = await rootBundle.load(
+          "assets/characters/miku/miku1.png",
+        );
+        bgImageBytes = bgImageData.buffer.asUint8List();
+      } finally {
+        codec?.dispose();
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
final codec = await ui.instantiateImageCodec(customImageBytes);
final frame = await codec.getNextFrame();
frame.image.dispose();
codec.dispose();
bgImageBytes = customImageBytes;
} catch (e) {
if (kDebugMode)
print("Invalid custom image, falling back to default: $e");
// 回退到 miku1
final bgImageData = await rootBundle.load(
"assets/characters/miku/miku1.png",
);
bgImageBytes = bgImageData.buffer.asUint8List();
}
ui.Codec? codec;
try {
codec = await ui.instantiateImageCodec(customImageBytes);
final frame = await codec.getNextFrame();
frame.image.dispose();
bgImageBytes = customImageBytes;
} catch (e) {
if (kDebugMode) {
print("Invalid custom image, falling back to default: $e");
}
// 回退到 miku1
final bgImageData = await rootBundle.load(
"assets/characters/miku/miku1.png",
);
bgImageBytes = bgImageData.buffer.asUint8List();
} finally {
codec?.dispose();
}
🧰 Tools
🪛 GitHub Actions: Flutter CI Build

[error] 1103-1103: flutter analyze: info • Statements in an if should be enclosed in a block (curly_braces_in_flow_control_structures)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/sticker.dart` around lines 1095 - 1109, The try/catch around
ui.instantiateImageCodec/customImageBytes can leak the codec if getNextFrame()
throws; ensure the ImageCodec (variable codec) is disposed in a finally block so
codec.dispose() always runs and only dispose frame.image when frame was
successfully obtained; adjust the logic around ui.instantiateImageCodec,
codec.getNextFrame, frame.image.dispose and codec.dispose so bgImageBytes is set
to customImageBytes only after successful decode, and on failure fall back to
rootBundle.load(...) as before.

Comment thread lib/sticker.dart Outdated
@xiaocaoooo xiaocaoooo merged commit d1bb753 into main May 2, 2026
15 checks passed
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.

1 participant