Skip to content

fix: Incorrect animation screen options #1325

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: development
Choose a base branch
from

Conversation

samruddhi-Rahegaonkar
Copy link
Member

@samruddhi-Rahegaonkar samruddhi-Rahegaonkar commented Jun 13, 2025

Fixes #1248
Fixes #1370

Changes

-Updated Animation according to proprietary app.

Screenshots / Recordings

Checklist:

  • No hard coding: I have used resources from constants.dart without hard coding any value.
  • No end of file edits: No modifications done at end of resource files.
  • Code reformatting: I have reformatted code and fixed indentation in every file included in this pull request.
  • Code analyzation: My code passes analyzations run in flutter analyze and tests run in flutter test.

Summary by Sourcery

Improve Bluetooth handling in BadgeMessageProvider by automating enablement on Android, adding platform-specific prompts, timeouts, and error logging, and streamline the data transfer flow; refactor SnowFlakeAnimation to simplify frame computation and prevent rendering glitches by aligning animation slices and bounding checks.

Enhancements:

  • Automate Bluetooth enabling on Android with error handling and timeouts, and add platform-specific prompts for iOS
  • Restructure BadgeMessageProvider to unify adapter state checks and streamline data generation and transfer
  • Refactor snowflake animation to compute frame count once, align frames to the left, and guard against out-of-bounds grid accesses

Copy link
Contributor

sourcery-ai bot commented Jun 13, 2025

Reviewer's Guide

This PR enhances the Bluetooth activation and data-transfer flow in BadgeMessageProvider with robust error handling, platform-specific enable routines, and logging, and refactors the SnowflakeAnimation module by consolidating frame calculations and simplifying grid updates for smoother rendering.

Class Diagram for Modified BadgeMessageProvider and SnowFlakeAnimation

classDiagram
    class BadgeMessageProvider {
      +sendMessage(String text, bool flash, bool marq, bool isInverted, String speed, String mode, Map<String, dynamic>? jsonData, bool isSavedBadge) async void
    }
    class BadgeAnimation {
      +processFrame(int animationIndex, List<List<bool>> processGrid, List<List<bool>> canvas) void
    }
    class SnowFlakeAnimation {
      +processFrame(int animationIndex, List<List<bool>> processGrid, List<List<bool>> canvas) void
    }
    SnowFlakeAnimation --|> BadgeAnimation : extends
    BadgeMessageProvider ..> FlutterBluePlus : uses
    BadgeMessageProvider ..> ToastUtils : uses
Loading

File-Level Changes

Change Details Files
Robust Bluetooth state handling and data transfer orchestration
  • Rephrased toast message for unsupported Bluetooth
  • Restructured adapterState check to branch on platform
  • Wrapped FlutterBluePlus.turnOn() calls in try/catch with error toasts and logging
  • Added timeout-based wait for adapter to turn on on Android and iOS
  • Moved data generation and transfer logic after successful Bluetooth enablement
lib/providers/badge_message_provider.dart
Refactored SnowflakeAnimation frame computation and canvas update
  • Moved frameCount and currentFrame calculations outside nested loops
  • Renamed variables for clarity (e.g., currentcountFrame → currentFrame)
  • Simplified grid boundary check with a single ternary assignment
  • Always left-align frames by recalculating startCol per frame
  • Removed redundant comments and nested branching
lib/badge_animation/ani_snowflake.dart

Assessment against linked issues

Issue Objective Addressed Explanation
#1248 Restore the original behavior of the 'Snowflake' animation where text that overflows the display area starts rendering from the leftmost pixel of the screen in each cycle, creating a 'blinking' effect for characters at the edge.

Possibly linked issues


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
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey @samruddhi-Rahegaonkar - I've reviewed your changes and they look great!

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

### Comment 1
<location> `lib/providers/badge_message_provider.dart:111` </location>
<code_context>
+    BluetoothAdapterState adapterState =
+        await FlutterBluePlus.adapterState.first;
+
+    if (adapterState != BluetoothAdapterState.on) {
       if (Platform.isAndroid) {
-        ToastUtils().showToast('Turning on Bluetooth...');
</code_context>

<issue_to_address>
Consider extracting Bluetooth enable logic into a helper

Extract the repeated Bluetooth enable logic for Android and iOS into a shared helper method to reduce duplication and improve code clarity.

Suggested implementation:

```
    if (!await _ensureBluetoothEnabled()) {
      return;
    }

```

```
Future<bool> _ensureBluetoothEnabled() async {
  BluetoothAdapterState adapterState =
      await FlutterBluePlus.adapterState.first;

  if (adapterState == BluetoothAdapterState.on) {
    return true;
  }

  if (Platform.isAndroid) {
    ToastUtils().showToast('Please enable Bluetooth...');
    try {
      await FlutterBluePlus.turnOn();
      return true;
    } catch (e) {
      ToastUtils().showErrorToast('Failed to enable Bluetooth: $e');
      logger.e('Bluetooth turnOn() failed: $e');
      return false;
    }
  }
  // Add iOS logic here if needed in the future
  return false;
}

```
</issue_to_address>

### Comment 2
<location> `lib/providers/badge_message_provider.dart:126` </location>
<code_context>
+          adapterState = await FlutterBluePlus.adapterState
+              .where((state) => state == BluetoothAdapterState.on)
+              .first
+              .timeout(
+            const Duration(seconds: 10),
+            onTimeout: () {
</code_context>

<issue_to_address>
Extract timeout duration to a constant

Defining a named constant improves clarity and makes future adjustments easier.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
          adapterState = await FlutterBluePlus.adapterState
              .where((state) => state == BluetoothAdapterState.on)
              .first
              .timeout(
            const Duration(seconds: 10),
            onTimeout: () {
              ToastUtils().showErrorToast('Bluetooth did not turn on in time.');
              throw Exception('Bluetooth enable timeout');
            },
          );
=======
const Duration _bluetoothEnableTimeout = Duration(seconds: 10);

          adapterState = await FlutterBluePlus.adapterState
              .where((state) => state == BluetoothAdapterState.on)
              .first
              .timeout(
            _bluetoothEnableTimeout,
            onTimeout: () {
              ToastUtils().showErrorToast('Bluetooth did not turn on in time.');
              throw Exception('Bluetooth enable timeout');
            },
          );
>>>>>>> REPLACE

</suggested_fix>

### Comment 3
<location> `lib/badge_animation/ani_snowflake.dart:12` </location>
<code_context>
-    for (int i = 0; i < badgeHeight; i++) {
-      for (int j = 0; j < badgeWidth; j++) {
-        // Calculate the total number of frames that fit the badge width
-        int framesCount = (newGridWidth / badgeWidth).ceil();
-
-        // Determine the current frame based on the animation value
</code_context>

<issue_to_address>
Guard against zero framesCount to avoid divide/modulo by zero

Clamping framesCount to at least 1 ensures animationIndex % framesCount won't cause a crash when newGridWidth is 0.
</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.

BluetoothAdapterState adapterState =
await FlutterBluePlus.adapterState.first;

if (adapterState != BluetoothAdapterState.on) {
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: Consider extracting Bluetooth enable logic into a helper

Extract the repeated Bluetooth enable logic for Android and iOS into a shared helper method to reduce duplication and improve code clarity.

Suggested implementation:

    if (!await _ensureBluetoothEnabled()) {
      return;
    }

Future<bool> _ensureBluetoothEnabled() async {
  BluetoothAdapterState adapterState =
      await FlutterBluePlus.adapterState.first;

  if (adapterState == BluetoothAdapterState.on) {
    return true;
  }

  if (Platform.isAndroid) {
    ToastUtils().showToast('Please enable Bluetooth...');
    try {
      await FlutterBluePlus.turnOn();
      return true;
    } catch (e) {
      ToastUtils().showErrorToast('Failed to enable Bluetooth: $e');
      logger.e('Bluetooth turnOn() failed: $e');
      return false;
    }
  }
  // Add iOS logic here if needed in the future
  return false;
}

Comment on lines 123 to 132
adapterState = await FlutterBluePlus.adapterState
.where((state) => state == BluetoothAdapterState.on)
.first
.timeout(
const Duration(seconds: 10),
onTimeout: () {
ToastUtils().showErrorToast('Bluetooth did not turn on in time.');
throw Exception('Bluetooth enable timeout');
},
);
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: Extract timeout duration to a constant

Defining a named constant improves clarity and makes future adjustments easier.

Suggested change
adapterState = await FlutterBluePlus.adapterState
.where((state) => state == BluetoothAdapterState.on)
.first
.timeout(
const Duration(seconds: 10),
onTimeout: () {
ToastUtils().showErrorToast('Bluetooth did not turn on in time.');
throw Exception('Bluetooth enable timeout');
},
);
const Duration _bluetoothEnableTimeout = Duration(seconds: 10);
adapterState = await FlutterBluePlus.adapterState
.where((state) => state == BluetoothAdapterState.on)
.first
.timeout(
_bluetoothEnableTimeout,
onTimeout: () {
ToastUtils().showErrorToast('Bluetooth did not turn on in time.');
throw Exception('Bluetooth enable timeout');
},
);

// Calculate the starting column for the current frame in newGrid
int startCol = currentcountFrame * badgeWidth;
// Calculate the total number of frames based on overflow
int framesCount = (newGridWidth / badgeWidth).ceil();

Copy link
Contributor

Choose a reason for hiding this comment

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

issue: Guard against zero framesCount to avoid divide/modulo by zero

Clamping framesCount to at least 1 ensures animationIndex % framesCount won't cause a crash when newGridWidth is 0.

@samruddhi-Rahegaonkar samruddhi-Rahegaonkar changed the title Issue#1248 fix: Behaviour change in the Snowflake animation Jun 13, 2025
Copy link
Contributor

github-actions bot commented Jun 13, 2025

Build Status

Build successful. APKs to test: https://github.com/fossasia/badgemagic-app/actions/runs/16652499339/artifacts/3659326012.

Screenshots (Android)

Screenshots (iPhone)

Screenshots (iPad)

Copy link
Contributor

@nope3472 nope3472 left a comment

Choose a reason for hiding this comment

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

The frame calculation now uses animationIndex % framesCount instead of animationIndex ~/ badgeWidth % framesCount, which ignores badgeWidth and skews the animation timing/sequencing.

@samruddhi-Rahegaonkar
Copy link
Member Author

@adityastic @Jhalakupadhyay Please review this too.

@samruddhi-Rahegaonkar
Copy link
Member Author

samruddhi-Rahegaonkar commented Jun 24, 2025

@nope3472 Will you please review it i will add recording of output?

Copy link
Contributor

@nope3472 nope3472 left a comment

Choose a reason for hiding this comment

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

The function processAnimation lacks input validation for the size and structure of processGrid and canvas. This may cause runtime errors if the input arrays do not match the expected dimensions.

@samruddhi-Rahegaonkar samruddhi-Rahegaonkar force-pushed the issue#1248 branch 2 times, most recently from 7f25deb to bddc6f8 Compare June 26, 2025 12:11
@hpdang
Copy link
Member

hpdang commented Jul 8, 2025

@samruddhi-Rahegaonkar what is the status here?

@samruddhi-Rahegaonkar
Copy link
Member Author

@hpdang This feature has been completed from my side also added recordings regarding it and updated the codes according to the review.

@samruddhi-Rahegaonkar
Copy link
Member Author

@nope3472 Review the Latest changes!

@hpdang
Copy link
Member

hpdang commented Jul 15, 2025

@samruddhi-Rahegaonkar the Animation effect works as expected on the physical LED badge. However, the in-app preview (what users see on their phone screen before sending it to the badge) does not accurately reflect the actual animation that plays on the badge. This is confusing for users, because they expect the preview to show exactly what will appear on the badge. Please test it again and fix it.

@samruddhi-Rahegaonkar
Copy link
Member Author

samruddhi-Rahegaonkar commented Jul 15, 2025

@hpdang we could only show in app preview if user set three messages in textfield and saved as animation. if we saved each word as individual animation then in saved badge preview it will show still image because we cannot create animation with one word.

@samruddhi-Rahegaonkar
Copy link
Member Author

@hpdang in the Homescreen preview they see what animation looks like.

@hpdang
Copy link
Member

hpdang commented Jul 15, 2025

@samruddhi-Rahegaonkar users should be able to understand how their text will appear on the badge without having to save anything first.

If the user’s input is just a single word that fits within one screen, the animation remains fixed on that screen, and the preview should reflect this behavior.

In short: the in-app preview must always accurately represent what will actually happen on the badge, regardless of the input length or animation type.

Maybe you can try:

  • Test with multiple text lengths 1) Very short text (fewer characters than the badge can display in one screen), 2) Text that exactly fits one full screen, 3) Longer text that requires scrolling across multiple screens.
  • Detect when the text fits on a single screen and show a static animation in the preview.
  • Ensure the preview mimics the badge output frame by frame.

@mariobehling mariobehling changed the title fix: Behaviour change in the Snowflake animation fix: Incorrect animation screen options Jul 15, 2025
fix: Guard against zero framesCount to avoid modulo by zero in SnowFlakeAnimation

fix: blicking illusion back

fix: added input validation

fix: fixed the interchanges animations

fix: animation and picture swapped with correct demo animation

style: format animation_badge_provider

fix: fixed animation preview and usage

fix: shows selected animation in preview of saved badges

fix: animation transfers to the badge succesfully as animation

fix: test fail

fix: failed flutter test fixed

fix: sync the actual badge and preview
@hpdang
Copy link
Member

hpdang commented Jul 17, 2025

@samruddhi-Rahegaonkar the preview doesnt not correctly reflect what is on the physical badge.
@nope3472 please review this PR, and share your idea how this should be done

video6325424209149302949.mp4
video6325424209149302947.mp4

@hpdang
Copy link
Member

hpdang commented Jul 23, 2025

@samruddhi-Rahegaonkar what is the status here?

@samruddhi-Rahegaonkar
Copy link
Member Author

@hpdang this is the most technically correct solution that we can write for this issue. isn't it @nope3472 ?

@samruddhi-Rahegaonkar
Copy link
Member Author

@hpdang The badge and preview use completely separate rendering systems, and unless both are driven by the exact same data stream and logic, 100% visual sync in full-width word-switching is not feasible.
Even if the preview and badge share the same data stream, they still won’t match in full-width word-switching, because they interpret and animate that data differently.
The badge reads the byte array directly and animates based on its own firmware logic.
The badge does not use our animationIndex or frame slicing. So even after giving the same processGrid, the frame 2 is not necessarily badge's frame 2.

@hpdang hpdang requested a review from CloudyPadmal July 24, 2025 14:08
@samruddhi-Rahegaonkar
Copy link
Member Author

@nope3472 I have done with this feature. Please review and if there are any changes suggest the changes or please aprove it. so i can move to another issue.

@samruddhi-Rahegaonkar
Copy link
Member Author

@nope3472

Copy link
Contributor

@nope3472 nope3472 left a comment

Choose a reason for hiding this comment

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

-There is repeated logic for mapping animation indices to modes and vice versa across multiple files (providers/animation_badge_provider.dart, providers/badge_message_provider.dart, providers/saved_badge_provider.dart).
-Suggestion: Create a central utility/helper for animation/mode/index mapping. This makes future changes less error-prone and the codebase easier to maintain.

APART FROM THIS THE LOGIC IS TECHNICALLY CORRECT.

@samruddhi-Rahegaonkar
Copy link
Member Author

@nope3472 I think all have there seperate working functionality making single utility will be hard to manage isn't it ?

AnimationBadgeProvider handles grid updates, animation rendering, timer lifecycle.
BadgeMessageProvider handles BLE data packaging and transmission.
SavedBadgeProvider handles saved state logic and restoring animations from JSON.

@samruddhi-Rahegaonkar
Copy link
Member Author

@nope3472

@samruddhi-Rahegaonkar
Copy link
Member Author

@nope3472 Please Aprove the changes as discussed.

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

Successfully merging this pull request may close these issues.

Animation screen options incorrect Behaviour change in the "Snowflake" animation
4 participants