feat: Support for Multiple LED Badge sizes#1347
feat: Support for Multiple LED Badge sizes#1347samruddhi-Rahegaonkar wants to merge 40 commits intofossasia:developmentfrom
Conversation
Reviewer's GuideThis PR adds dynamic support for multiple LED badge sizes by introducing a ScreenSize model and replacing all hard-coded grid dimensions with runtime-configurable values, propagating the selected size through UI widgets, providers, converters, custom painters, utilities, and tests. Sequence diagram for badge size selection and propagationsequenceDiagram
actor User
participant HomeScreen
participant AnimationBadge
participant DrawBadge
participant SaveBadgeDialog
participant SaveBadgeProvider
User->>HomeScreen: Selects badge size from dropdown
HomeScreen->>AnimationBadge: Passes selected ScreenSize
User->>DrawBadge: Navigates to draw badge
DrawBadge->>DrawBadge: Initializes grid with selected ScreenSize
User->>SaveBadgeDialog: Opens save dialog
SaveBadgeDialog->>SaveBadgeProvider: Saves badge with selected ScreenSize
SaveBadgeProvider->>SaveBadgeProvider: Stores badge data with size info
Class diagram for ScreenSize and supportedScreenSizesclassDiagram
class ScreenSize {
+int width
+int height
+String name
+operator ==(Object other)
+int hashCode
}
class supportedScreenSizes {
<<constant>>
+List<ScreenSize>
}
Class diagram for updated AnimationBadgeProvider and DrawBadgeProviderclassDiagram
class AnimationBadgeProvider {
-List<List<bool>> _paintGrid
-List<List<bool>> _newGrid
-List<List<List<bool>>> _frames
-int _currentFrame
+void initGrids(ScreenSize size)
+void badgeAnimation(String, Converters, bool, ScreenSize)
+List<List<bool>> getPaintGrid()
+List<List<bool>> getNewGrid()
}
class DrawBadgeProvider {
-List<List<bool>> _drawViewGrid
-ScreenSize _currentSize
+void initGridWithSize(ScreenSize size)
+List<List<bool>> getDrawViewGrid()
+ScreenSize getCurrentSize()
}
Class diagram for updated UI widgets with ScreenSize supportclassDiagram
class AnimationBadge {
+ScreenSize selectedSize
}
class BMBadge {
+ScreenSize selectedSize
}
class EffectContainer {
+ScreenSize selectedSize
}
class EffectTab {
+ScreenSize selectedSize
}
class SaveBadgeCard {
+ScreenSize selectedSize
}
class BadgeListView {
+ScreenSize selectedSize
}
class SavedClipartListView {
+ScreenSize selectedSize
}
class SaveBadgeDialog {
+ScreenSize selectedSize
}
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
fc0ac96 to
74a1dda
Compare
There was a problem hiding this comment.
Hey @samruddhi-Rahegaonkar - I've reviewed your changes - here's some feedback:
- This change spreads the new ScreenSize parameters into almost every widget and provider method; consider using a single Provider or InheritedWidget to hold the selected badge size so you don’t have to modify every signature and widget.
- The Converters.messageTohex method has become extremely large with mixed responsibilities (emoji tags, char scaling, bitmap scaling); refactor by extracting emoji, character, and bitmap conversion into separate helper classes or private services to improve readability and testability.
- BadgePaint (and other rendering code) still uses magic scaling factors like 0.93 and 0.5—extract these into named constants or compute them dynamically from width/height ratios to avoid unexpected layout issues on non-standard sizes.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- This change spreads the new ScreenSize parameters into almost every widget and provider method; consider using a single Provider or InheritedWidget to hold the selected badge size so you don’t have to modify every signature and widget.
- The Converters.messageTohex method has become extremely large with mixed responsibilities (emoji tags, char scaling, bitmap scaling); refactor by extracting emoji, character, and bitmap conversion into separate helper classes or private services to improve readability and testability.
- BadgePaint (and other rendering code) still uses magic scaling factors like 0.93 and 0.5—extract these into named constants or compute them dynamically from width/height ratios to avoid unexpected layout issues on non-standard sizes.
## Individual Comments
### Comment 1
<location> `lib/providers/animation_badge_provider.dart:110` </location>
<code_context>
}
void startTimer() {
+ if (_newGrid.isEmpty || _newGrid[0].isEmpty) {
+ logger.w("Cannot start animation timer: _newGrid is empty");
+ return;
</code_context>
<issue_to_address>
startTimer silently returns if _newGrid is empty, which may mask upstream issues.
Consider surfacing this condition as an error or notifying the caller, so they are aware the animation did not start.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
Hey @sourcery-ai ,
Isn't It ? |
1038c33 to
e334dbe
Compare
4d3d10a to
856c79e
Compare
This comment was marked as outdated.
This comment was marked as outdated.
There was a problem hiding this comment.
Hey @samruddhi-Rahegaonkar - I've reviewed your changes - here's some feedback:
- Consider centralizing the selected ScreenSize in a provider or InheritedWidget so you don’t have to prop-drill it through every widget and method call.
- The Converters class now mixes text parsing, bitmap scaling, emoji handling, and hex formatting—splitting it into focused services (e.g. TextConverter, ImageConverter) will improve readability and testability.
- There are multiple bitmap scaling and trimming routines (_scaleBitmapToBadgeSize, _scaleTextCharacterToBadgeSize, convertBitmapToLEDHex, etc.); consolidating them into shared utilities will reduce duplication and ensure consistent behavior.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Consider centralizing the selected ScreenSize in a provider or InheritedWidget so you don’t have to prop-drill it through every widget and method call.
- The Converters class now mixes text parsing, bitmap scaling, emoji handling, and hex formatting—splitting it into focused services (e.g. TextConverter, ImageConverter) will improve readability and testability.
- There are multiple bitmap scaling and trimming routines (_scaleBitmapToBadgeSize, _scaleTextCharacterToBadgeSize, convertBitmapToLEDHex, etc.); consolidating them into shared utilities will reduce duplication and ensure consistent behavior.
## Individual Comments
### Comment 1
<location> `lib/providers/draw_badge_provider.dart:25` </location>
<code_context>
- }
+ final rows = _drawViewGrid.length;
+ final cols = _drawViewGrid.isNotEmpty ? _drawViewGrid[0].length : 0;
+ for (int i = 0; i < rows && i < badgeData.length; i++) {
+ for (int j = 0; j < cols && j < badgeData[0].length; j++) {
+ _drawViewGrid[i][j] = badgeData[i][j];
</code_context>
<issue_to_address>
updateDrawViewGrid only copies overlapping regions, which may leave parts of the grid unchanged.
If badgeData is smaller than the grid, leftover cells may show outdated data. Please clear or reset all grid cells before copying new values.
</issue_to_address>
### Comment 2
<location> `lib/virtualbadge/view/badge_paint.dart:50` </location>
<code_context>
+ final int rows = grid.length;
+
+ // Adjust cell size to fit all pixels inside the badge area
+ final double cellWidth = badgeWidth / (cols * 0.93);
+ final double cellHeight = badgeHeight / rows;
+ final double cellSize = math.min(cellWidth, cellHeight);
</code_context>
<issue_to_address>
The cell width uses a 0.93 scaling factor, which may not be intuitive.
Please document the choice of 0.93 or make it a configurable parameter to improve clarity and adaptability.
</issue_to_address>
### Comment 3
<location> `lib/virtualbadge/view/badge_paint.dart:97` </location>
<code_context>
- bool shouldRepaint(covariant CustomPainter oldDelegate) {
- return true;
- }
+ bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
</code_context>
<issue_to_address>
shouldRepaint always returns true, which may cause unnecessary repaints.
Compare the old and new grid values in shouldRepaint to avoid unnecessary repaints and improve performance.
Suggested implementation:
```
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
if (oldDelegate is! YourPainterClassName) return true;
return oldDelegate.grid != grid;
}
}
```
- Replace `YourPainterClassName` with the actual name of your CustomPainter subclass.
- Ensure that `grid` is a field of your painter class and that it implements `==` properly (e.g., use `ListEquality` from `collection` if it's a List).
- If `grid` is a complex object, you may need to implement or use a deep equality check.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
|
hey @samruddhi-Rahegaonkar can you resolve the conflicts. |
|
@nope3472 I will resolve it by end of the day. |
|
@nope3472 will you please test font style feature on this it is not properly showing on my side. |
|
@sourcery-ai review again |
|
Sorry @mariobehling, your pull request is larger than the review limit of 150000 diff characters |
There was a problem hiding this comment.
Pull Request Overview
This PR introduces comprehensive support for multiple LED badge screen sizes by creating a dynamic sizing system that replaces hardcoded dimensions throughout the application. The changes enable users to select from multiple badge configurations (44x11, 64x16, 128x32) through dropdown interfaces.
- Introduced
ScreenSizemodel with predefined badge dimensions and size selection UI components - Refactored core providers and utilities to accept dynamic width/height parameters instead of hardcoded values
- Updated converters, byte array utilities, and rendering logic to handle arbitrary badge dimensions with proper scaling
Reviewed Changes
Copilot reviewed 34 out of 34 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| lib/bademagic_module/models/screen_size.dart | Defines ScreenSize model and supported badge dimensions |
| test/data_to_bytearray_converter_test.dart | Updates test methods to include badgeHeight parameter for new converter signature |
| test/converters_test.dart | Adds ScreenSize parameter and expected hex values for converter testing |
| lib/providers/animation_badge_provider.dart | Adds grid initialization with ScreenSize and dynamic dimension handling |
| lib/view/homescreen.dart | Integrates ScreenSize selection dropdown and propagates size throughout animation pipeline |
| lib/virtualbadge/view/draw_badge.dart | Updates drawing logic to use dynamic badge dimensions from ScreenSize |
| lib/providers/saved_badge_provider.dart | Adds height/width parameters to badge saving and loading methods |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
|
Hey @samruddhi-Rahegaonkar this PR has too many unrelated changes. Ideally adding support to multiple badge sizes should be tied to the badge unique identifier and allowing users to select a new badge which changes it's height and width accordingly. I don't know why these many changes are involved. |
|
@adityastic I will make fresh PR for this issue. |
|
Thank you! Closing this PR and looking forward to a fresh PR. Small process note. We have automatic Copilot PR reviews enabled on this repository. These reviews are only triggered if the contributor has GitHub Copilot enabled and an active license on their own account. Please enable Copilot in your GitHub settings if you have access. In many regions, free licenses are available through educational institutions or developer programs. Enabling Copilot helps us speed up the auto review process and reduces manual review overhead for the core team. The team will review your PR. Thank you for your contribution and cooperation. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 34 out of 34 changed files in this pull request and generated 15 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| @override | ||
| void initState() { | ||
| super.initState(); |
There was a problem hiding this comment.
HomeScreen implements WidgetsBindingObserver and defines didChangeAppLifecycleState, but initState no longer calls WidgetsBinding.instance.addObserver(this). As a result, lifecycle callbacks won’t run while dispose still removes the observer. Add the observer registration in initState (or remove the observer mixin/logic).
| super.initState(); | |
| super.initState(); | |
| WidgetsBinding.instance.addObserver(this); |
| TransitionTab(selectedSize: _selectedSize), | ||
| AnimationTab(selectedSize: _selectedSize), |
There was a problem hiding this comment.
TabBar labels don’t match the TabBarView child order: the tab labeled “Animation” shows TransitionTab and the tab labeled “Transition” shows AnimationTab. Reorder either the TabBar tabs or the TabBarView children so indices align.
| TransitionTab(selectedSize: _selectedSize), | |
| AnimationTab(selectedSize: _selectedSize), | |
| AnimationTab(selectedSize: _selectedSize), | |
| TransitionTab(selectedSize: _selectedSize), |
| if (state == AppLifecycleState.resumed) { | ||
| inlineimagecontroller.clear(); | ||
| previousText = ''; | ||
| animationProvider.stopAllAnimations(); | ||
| animationProvider.initializeAnimation(); | ||
| if (mounted) setState(() {}); | ||
| } else if (state == AppLifecycleState.paused || | ||
| state == AppLifecycleState.inactive) { | ||
| animationProvider.stopAnimation(); | ||
| } |
There was a problem hiding this comment.
On app resume, didChangeAppLifecycleState clears the text controller and resets previousText, which will discard the user’s in-progress message when returning from background. Consider restoring the previous controller text (or avoiding mutation) and just restarting animations as needed.
| // ignore_for_file: invalid_use_of_visible_for_testing_member | ||
|
|
||
| import 'package:badgemagic/bademagic_module/models/screen_size.dart'; |
There was a problem hiding this comment.
This file disables invalid_use_of_visible_for_testing_member, but the code is actually calling a protected member (notifyListeners) directly. Prefer removing the file-level ignore and exposing a public method on DrawBadgeProvider to trigger updates, rather than calling notifyListeners externally.
| int badgeWidth, | ||
| int badgeHeight) async { |
There was a problem hiding this comment.
updateBadgeData’s parameters are ordered as (badgeWidth, badgeHeight), but most other call sites and Data serialization treat them as (height, width). This mismatch is already causing swapped arguments from callers. Consider changing the method signature to (badgeHeight, badgeWidth) and updating all calls to keep ordering consistent across the codebase.
| int badgeWidth, | |
| int badgeHeight) async { | |
| int badgeHeight, | |
| int badgeWidth) async { |
| void commitGridUpdate() { | ||
| _pushToUndoStack(); | ||
| for (int i = 0; i < rows; i++) { | ||
| for (int j = 0; j < cols; j++) { | ||
| _drawViewGrid[i][j] = | ||
| (j < badgeData[0].length) ? badgeData[i][j] : false; | ||
| if (_previewGrid[i][j]) { | ||
| _drawViewGrid[i][j] = true; | ||
| } | ||
| } | ||
| } | ||
| clearPreviewGrid(); | ||
| notifyListeners(); |
There was a problem hiding this comment.
commitGridUpdate currently sets affected cells to true whenever _previewGrid is true. Because preview cells are set to isDrawing, erase previews write false and will never be applied (erasing with shapes won’t work). Apply the preview value to _drawViewGrid (or apply isDrawing) instead of forcing true.
| for (int i = 0; i < text.length; i++) { | ||
| String char = text[i]; | ||
| bool hasDescender = "ypgqj".contains(char); | ||
| final matrixData = await renderTextToMatrix(char, style, | ||
| rows: 11, hasDescender: hasDescender); | ||
| targetWidth: size.width, | ||
| targetHeight: size.height, | ||
| hasDescender: hasDescender); | ||
| List<List<bool>> charMatrix = matrixData['matrix']; | ||
| for (int row = 0; row < 11; row++) { | ||
| for (int row = 0; row < size.height; row++) { | ||
| combinedMatrix[row].addAll(charMatrix[row]); | ||
| } |
There was a problem hiding this comment.
Custom font rendering renders each character using targetWidth = size.width and then concatenates the full-width matrices. This makes every character as wide as the whole badge and causes the combinedMatrix to explode in width. renderTextToMatrix should render each character at its natural width (or a fixed glyph width) and only scale to targetHeight; alternatively render the entire message once and then segment to 8px columns.
| _selectedSize.height, | ||
| _selectedSize.width, |
There was a problem hiding this comment.
updateBadgeData is declared as (badgeWidth, badgeHeight) in SavedBadgeProvider, but this call passes (_selectedSize.height, _selectedSize.width). This will swap stored width/height for updated badges. Pass width first, height second (or adjust the provider signature to match the rest of the codebase).
| _selectedSize.height, | |
| _selectedSize.width, | |
| _selectedSize.width, | |
| _selectedSize.height, |
| builder: (context) => AlertDialog( | ||
| title: const Text('Edit Badge'), | ||
| content: const Text( | ||
| 'Do you want to edit this badge?'), | ||
| actions: [ | ||
| TextButton( | ||
| onPressed: () => | ||
| Navigator.pop(context, false), | ||
| child: const Text('No'), | ||
| ), | ||
| TextButton( | ||
| onPressed: () => | ||
| Navigator.pop(context, true), | ||
| child: const Text('Yes'), | ||
| ), |
There was a problem hiding this comment.
The edit confirmation dialog uses hard-coded English strings ('Edit Badge', 'Do you want to edit this badge?', 'No', 'Yes'). These should be localized via l10n (and ideally reused across the app) to avoid i18n regressions.
| ANDROID_EMULATOR_API: ${{ env.ANDROID_EMULATOR_API }} | ||
| ANDROID_EMULATOR_ARCH: ${{ env.ANDROID_EMULATOR_ARCH }} | ||
|
|
||
| screenshots-iphone: | ||
| name: Screenshots (iPhone) | ||
| screenshots-ios: | ||
| name: Screenshots (iOS) | ||
| runs-on: macos-latest | ||
| timeout-minutes: 30 | ||
| steps: | ||
| - name: Set up Xcode | ||
| uses: maxim-lobanov/setup-xcode@v1.6.0 | ||
| with: | ||
| xcode-version: latest-stable | ||
|
|
||
| - uses: actions/checkout@v5 | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - name: iPhone Screenshot Workflow | ||
| uses: ./.github/actions/screenshot-iphone | ||
| with: | ||
| IPHONE_DEVICE_MODEL: ${{ env.IPHONE_DEVICE_MODEL }} | ||
|
|
||
| screenshots-ipad: | ||
| name: Screenshots (iPad) | ||
| runs-on: macos-latest | ||
| timeout-minutes: 30 | ||
| steps: | ||
| - name: Set up Xcode | ||
| uses: maxim-lobanov/setup-xcode@v1.6.0 | ||
| with: | ||
| xcode-version: latest-stable | ||
|
|
||
| - uses: actions/checkout@v5 | ||
|
|
||
| - name: iPad Screenshot Workflow | ||
| uses: ./.github/actions/screenshot-ipad | ||
| with: | ||
| IPAD_DEVICE_MODEL: ${{ env.IPAD_DEVICE_MODEL }} | ||
|
|
||
| debian: | ||
| name: Debian Flutter Build | ||
| needs: common | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v5 | ||
|
|
||
| - name: Debian Workflow | ||
| uses: ./.github/actions/debian | ||
| with: | ||
| VERSION_NAME: ${{ needs.common.outputs.VERSION_NAME }} | ||
| VERSION_CODE: ${{ needs.common.outputs.VERSION_CODE }} | ||
|
|
||
| - name: Upload Debian Build | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: Debian Build | ||
| path: build/linux/x64/release/bundle/ | ||
|
|
||
| - name: Push Debian Build to app branch | ||
| shell: bash | ||
| run: | | ||
| git config --global user.name "github-actions[bot]" | ||
| git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" | ||
|
|
||
| git clone --branch=app https://${{ github.repository_owner }}:${{ github.token }}@github.com/${{ github.repository }} app | ||
| cd app | ||
| branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}} | ||
| cp -r ../build/linux/x64/release/bundle/* . | ||
| git checkout --orphan temporary | ||
| git add --all | ||
| git commit -m "[Auto] Update Debian build from $branch ($(date +%Y-%m-%d.%H:%M:%S))" | ||
| git branch -D app | ||
| git branch -m app | ||
| git push --force origin app | ||
|
|
||
| macos: | ||
| name: macOS Flutter Build | ||
| needs: common | ||
| runs-on: macos-latest | ||
| steps: | ||
| - uses: actions/checkout@v5 | ||
|
|
||
| - name: macOS Workflow | ||
| uses: ./.github/actions/macos | ||
| with: | ||
| VERSION_NAME: ${{ needs.common.outputs.VERSION_NAME }} | ||
| VERSION_CODE: ${{ needs.common.outputs.VERSION_CODE }} | ||
|
|
||
| - name: Upload macOS Build | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: macOS Build | ||
| path: build/macos/Build/Products/Release/ | ||
|
|
||
| - name: Push macOS Build to app branch | ||
| shell: bash | ||
| run: | | ||
| git config --global user.name "github-actions[bot]" | ||
| git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" | ||
|
|
||
| git clone --branch=app https://${{ github.repository_owner }}:${{ github.token }}@github.com/${{ github.repository }} app | ||
| cd app | ||
| branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}} | ||
|
|
||
| # Copy all macOS build artifacts to root | ||
| cp -r ../build/macos/Build/Products/Release/* . | ||
|
|
||
| git checkout --orphan temporary | ||
| git add --all | ||
| git commit -m "[Auto] Update macOS build from $branch ($(date +%Y-%m-%d.%H:%M:%S))" | ||
| git branch -D app | ||
| git branch -m app | ||
| git push --force origin app | ||
|
|
||
|
|
||
| windows: | ||
| name: Windows Flutter Build | ||
| needs: common | ||
| runs-on: windows-latest | ||
| steps: | ||
| - uses: actions/checkout@v5 | ||
|
|
||
| - name: Windows Workflow | ||
| uses: ./.github/actions/windows | ||
| - name: iOS Screenshot Workflow | ||
| uses: ./.github/actions/screenshot-ios | ||
| with: | ||
| VERSION_NAME: ${{ needs.common.outputs.VERSION_NAME }} | ||
| VERSION_CODE: ${{ needs.common.outputs.VERSION_CODE }} | ||
|
|
||
| - name: Upload Windows Build | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: Windows Build | ||
| path: build/windows/x64/runner/Release/ | ||
|
|
||
| - name: Push Windows Build to app branch | ||
| shell: bash | ||
| run: | | ||
| git config --global user.name "github-actions[bot]" | ||
| git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" | ||
|
|
||
| git clone --branch=app https://${{ github.repository_owner }}:${{ github.token }}@github.com/${{ github.repository }} app | ||
| cd app | ||
| branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}} | ||
|
|
||
| # Copy all Windows build artifacts to root | ||
| cp -r ../build/windows/x64/runner/Release/* . | ||
|
|
||
| git checkout --orphan temporary | ||
| git add --all | ||
| git commit -m "[Auto] Update Windows build from $branch ($(date +%Y-%m-%d.%H:%M:%S))" | ||
| git branch -D app | ||
| git branch -m app | ||
| git push --force origin app | ||
|
|
||
| IPHONE_DEVICE_MODEL: ${{ env.IPHONE_DEVICE_MODEL }} | ||
| IPAD_DEVICE_MODEL: ${{ env.IPAD_DEVICE_MODEL }} No newline at end of file |
There was a problem hiding this comment.
This workflow refactor consolidates the iOS screenshot jobs (and removes several previously defined jobs in this file). If those removed jobs (e.g., separate iPhone/iPad screenshots and/or desktop build jobs) are still required for releases/CI coverage, they should be restored; otherwise please update the PR description to explicitly call out the CI job removals to avoid surprises.
Fixes #1314
Changes
Screenshots / Recordings
Screen.Recording.2025-06-26.at.6.49.21.PM.mov
Checklist:
constants.dartwithout hard coding any value.Summary by Sourcery
Introduce dynamic support for multiple LED badge sizes by adding a ScreenSize model and propagating variable dimensions throughout the UI, providers, converters, painting logic, and tests, while enabling users to select their badge size at runtime.
New Features:
Enhancements:
CI:
Tests:
Summary by Sourcery
Support multiple LED badge sizes by introducing a ScreenSize model and wiring dynamic width/height parameters throughout the app to enable scalable layouts in all drawing, animation, effect, saving, and transfer features.
New Features:
Enhancements:
CI:
Tests: