Skip to content

feat: Enable editing of saved badges (fixes #1305) #1311

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 31 commits into
base: development
Choose a base branch
from

Conversation

nope3472
Copy link
Contributor

@nope3472 nope3472 commented Jun 2, 2025

🎯 What does this PR do?

This PR implements in-place editing of saved badges so that updates modify the existing badge file and cache, instead of creating duplicates.

πŸ› Problem Solved

Fixes #1305 – previously, editing a saved badge would spawn a new badge, cluttering the list with duplicates. Now the original badge is updated directly.

recording

Screen_Recording_20250716_200307.mp4
Screen_Recording_20250716_200758.mp4

✨ Changes Made

  1. Saving a New Badge
    Where:

    • lib/providers/saved_badge_provider.dart

      • saveBadgeData() method: Serializes badge data and writes it to disk.
    • lib/bademagic_module/utils/file_helper.dart

      • saveBadgeData() method: Handles writing the JSON file and updating the cache.
    • lib/bademagic_module/utils/badge_text_storage.dart

      • saveOriginalText() method: Stores the original text for later restoration.
  2. Editing an Existing Badge
    Where:

    • lib/view/widgets/save_badge_card.dart

      • Edit button’s onPressed: Navigates to HomeScreen, passing only the badge filename.
    • lib/view/homescreen.dart

      • _loadBadgeDataFromDisk() method:

        • Loads the badge JSON and original text from disk using the filename.
        • Populates all UI controllers/providers (text field, speed, effects, animation mode, etc.) with the loaded data.
  3. Saving an Edited Badge
    Where:

    • lib/view/homescreen.dart

      • Save button handler: If editing, calls updateBadgeData() instead of creating a new badge.
    • lib/providers/saved_badge_provider.dart

      • updateBadgeData() method:

        • Overwrites the existing badge file with updated data.
        • Updates the in-memory cache entry (not appending).
        • Refreshes the stored original text.
    • lib/bademagic_module/utils/file_helper.dart

      • saveBadgeData() method: Handles the actual file overwrite and cache update logic.
  4. Robustness & User Experience
    Where:

    • lib/view/homescreen.dart

      • _loadBadgeDataFromDisk() method:

        • Wraps disk reads and JSON parsing in try/catch, showing user-friendly error messages on failure.
        • Ensures UI always reflects the on-disk state.
    • lib/bademagic_module/utils/file_helper.dart

      • Adds defensive checks around file I/O and cache operations.
    • lib/providers/saved_badge_provider.dart

      • Adds guards for missing files and malformed data, falling back gracefully.

πŸ§ͺ How to Test

  1. Create and save a new badge.
  2. Open it for editing from the saved badges list.
  3. Modify text, speed, effects, animation mode, etc.
  4. Save your changes.
  5. Confirm the original badge file updates in place (no duplicate appears) and the UI reflects the edits.

πŸ“Έ Before vs After

Before: Editing a saved badge created a duplicate.
After: Editing a saved badge updates the original in-place.

Copy link
Contributor

sourcery-ai bot commented Jun 2, 2025

Reviewer's Guide

Implements in-place editing of saved badges by extending HomeScreen to accept saved data, loading that data into input fields and providers, and invoking a new update flow in SavedBadgeProvider to overwrite existing badge files (and cache) rather than creating duplicates.

Sequence Diagram for Updating an Existing Badge

sequenceDiagram
    actor User
    participant HomeScreen
    participant SavedBadgeProvider
    participant BadgeTextStorage
    participant FileSystemCache as "File System / Cache"

    User->>HomeScreen: Clicks 'Save' (for an existing badge)
    HomeScreen->>HomeScreen: if editing (savedBadgeFilename is not null)
    HomeScreen->>SavedBadgeProvider: updateBadgeData(filename, newText, effects, speed, mode)
    SavedBadgeProvider->>SavedBadgeProvider: Create/Prepare Data object for badge
    SavedBadgeProvider->>FileSystemCache: Write updated badge data to existing file (e.g., filename.json)
    SavedBadgeProvider->>FileSystemCache: Update FileHelper.imageCacheProvider.savedBadgeCache
    SavedBadgeProvider->>BadgeTextStorage: saveOriginalText(filename.json, newText)
    BadgeTextStorage->>FileSystemCache: Update/Write badge_original_texts.json file
    BadgeTextStorage-->>SavedBadgeProvider: Original text saved
    SavedBadgeProvider-->>HomeScreen: Badge update complete
    HomeScreen->>User: Show "Badge Updated Successfully" toast
Loading

Class Diagram for Badge Editing Feature Changes

classDiagram
    class HomeScreen {
        +savedBadgeData: Map~String, dynamic~? (new)
        +savedBadgeFilename: String? (new)
        #_applySavedBadgeData(): void (new)
        +handleSave(): void (modified: calls updateBadgeData if editing)
    }

    class SavedBadgeProvider {
        +updateBadgeData(String filename, String message, bool isFlash, bool isMarquee, bool isInvert, int? speed, int animation): Future~void~ (new)
        +saveBadgeData(String filename, String message, ...): Future~void~ (modified: calls BadgeTextStorage.saveOriginalText)
    }

    class BadgeTextStorage {
        <<Utility>>
        +TEXT_STORAGE_FILENAME: String (new)
        +saveOriginalText(String badgeFilename, String originalText): Future~void~ (new)
        +getOriginalText(String badgeFilename): Future~String~ (new)
        +deleteOriginalText(String badgeFilename): Future~void~ (new)
    }

    class FileHelper {
        +saveBadgeData(Data data, String filename, bool isInvert): Future~void~ (modified: updates cache entry if exists)
        +jsonToData(Map~String, dynamic~ jsonData): Data (modified: handles missing 'messages' key)
    }

    class SaveBadgeCard {
        +handleEdit(): void (modified: navigates to HomeScreen with badge data)
        #_safeGetFlashValue(Map~String,dynamic~ data): bool (new)
        #_safeGetMarqueeValue(Map~String,dynamic~ data): bool (new)
        #_safeGetInvertValue(Map~String,dynamic~ data): bool (new)
    }

    HomeScreen "1" o-- "1" SavedBadgeProvider : uses
    SaveBadgeCard ..> HomeScreen : navigates to / passes data to
    SavedBadgeProvider "1" o-- "1" FileHelper : uses
    SavedBadgeProvider "1" o-- "1" BadgeTextStorage : uses
Loading

File-Level Changes

Change Details Files
Enhanced badge editing workflow in HomeScreen
  • Added optional savedBadgeData and savedBadgeFilename parameters to constructor
  • Invoked _applySavedBadgeData in initState to prefill fields and effects
  • Moved save button logic to choose between updateBadgeData or SaveBadgeDialog
  • Refactored initState and post-frame callback to set up editing context
lib/view/homescreen.dart
Introduced BadgeTextStorage utility
  • Created badge_text_storage.dart for persisting/retrieving original badge text
  • Integrated saveOriginalText in save and update flows
  • Used getOriginalText when loading saved badges for editing
lib/bademagic_module/utils/badge_text_storage.dart
lib/providers/saved_badge_provider.dart
Extended SavedBadgeProvider to support updates
  • Added updateBadgeData method to overwrite existing JSON file and update cache
  • Enhanced saveBadgeData to call BadgeTextStorage
  • Retained fallback to saveBadgeData on update errors
lib/providers/saved_badge_provider.dart
Improved FileHelper caching and JSON resilience
  • Switched cache update to replace existing entries or append new ones
  • Wrapped jsonToData in try/catch to inject default message structure when missing
  • Logged cache operations for debugging
lib/bademagic_module/utils/file_helper.dart
Refined SaveBadgeCard for edit navigation and safety
  • Introduced safe getters for flash/marquee/invert properties
  • Replaced DrawBadge navigation with pushAndRemoveUntil to HomeScreen with edit params
  • Removed direct grid conversion in favor of HomeScreen loader
lib/view/widgets/save_badge_card.dart
Simplified speed value logic
  • Changed Speed.getIntValue to return index+1 instead of parsing hex
  • Removed obsolete string/substring code
lib/bademagic_module/models/speed.dart

Assessment against linked issues

Issue Objective Addressed Explanation
#1305 Implement an editing mode for saved badges, allowing users to modify existing badges instead of creating duplicates. βœ…

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 @nope3472 - I've reviewed your changes and found some issues that need to be addressed.

Blocking issues:

  • fileHelper is undefined in this scope (link)

General comments:

  • HomeScreen has grown very large with UI and business logic mixedβ€”consider extracting the badge-editing setup (applying savedBadgeData, parsing mode/speed, etc.) into a dedicated controller or service to improve readability and maintainability.
  • The enum-to-int mapping logic for modes and speeds (string parsing, switch statements) is duplicated and verbose; centralize that in your enums or a shared utility to avoid inconsistencies.
  • SaveBadgeCard uses pushAndRemoveUntil to navigate back into HomeScreen which clears the entire stack; consider using a normal push or preserving previous routes to avoid unexpected UX/navigation side effects.
Here's what I looked at during the review
  • πŸ”΄ General issues: 1 blocking issue, 5 other issues
  • 🟒 Security: all looks good
  • 🟒 Testing: all looks good
  • 🟒 Documentation: all looks good

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click πŸ‘ or πŸ‘Ž on each comment and I'll use the feedback to improve your reviews.

Comment on lines 77 to 81
_startImageCaching();
speedDialProvider = SpeedDialProvider(animationProvider);
super.initState();

_tabController = TabController(length: 3, vsync: this);
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: Call super.initState() before other initializations

Invoke super.initState() at the start of initState() to ensure proper initialization of inherited behavior.

Suggested change
_startImageCaching();
speedDialProvider = SpeedDialProvider(animationProvider);
super.initState();
_tabController = TabController(length: 3, vsync: this);
super.initState();
_startImageCaching();
speedDialProvider = SpeedDialProvider(animationProvider);
_tabController = TabController(length: 3, vsync: this);


// Update the cache
final cacheKey = '$cleanFilename.json';
final cache = fileHelper.imageCacheProvider.savedBadgeCache;
Copy link
Contributor

Choose a reason for hiding this comment

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

issue (bug_risk): fileHelper is undefined in this scope

Instantiate fileHelper in updateBadgeData or use a shared instance before referencing fileHelper.imageCacheProvider.

// If we're editing an existing badge, update it instead of showing save dialog
if (widget.savedBadgeFilename != null) {
// Update the existing badge file using the new updateBadgeData method
SavedBadgeProvider savedBadgeProvider =
Copy link
Contributor

Choose a reason for hiding this comment

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

issue (bug_risk): Instantiating SavedBadgeProvider in widget build

Use Provider.of(context, listen: false) to access the existing provider instead of creating a new instance.

Copy link
Contributor

Choose a reason for hiding this comment

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

@nope3472 look at this

String badgeFilename = badgeData.key;

// Navigate to HomeScreen and replace the current route
Navigator.of(context).pushAndRemoveUntil(
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: pushAndRemoveUntil clears entire navigation stack

Using pushAndRemoveUntil removes all previous routes, which may not be intended. Consider pushReplacement to preserve navigation history.

/// @param isInvert Whether invert effect is enabled
/// @param speed The speed value for the animation
/// @param animation The animation mode index
Future<void> updateBadgeData(String filename, String message, bool isFlash,
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (bug_risk): Missing notifyListeners() after updating badge data

Call notifyListeners() after updating the badge file and cache to ensure UI updates correctly.

Suggested implementation:

  Future<void> updateBadgeData(String filename, String message, bool isFlash,
      bool isMarquee, bool isInvert, int? speed, int animation) async {
    // Make sure filename doesn't have .json extension
    String cleanFilename = filename;
    if (cleanFilename.endsWith('.json')) {
      cleanFilename = cleanFilename.substring(0, cleanFilename.length - 5);
    }

    logger.i('Updating existing badge: $cleanFilename');

    // Create the updated badge data
    Data data = await getBadgeData(
      message,

    // Create the updated badge data
    Data data = await getBadgeData(
      message,
    );

    // ... (rest of your update logic, e.g. writing to file, updating cache, etc.)

    notifyListeners();

Place the notifyListeners(); call at the end of the function, after all badge data and cache updates are complete, to ensure the UI is notified of changes. If there is additional logic after the shown code (such as writing to file or updating an in-memory cache), ensure notifyListeners(); is called after those operations.

Copy link
Contributor

github-actions bot commented Jun 2, 2025

Build Status

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

Screenshots (Android)

Screenshots (iPhone)

Screenshots (iPad)

@nope3472
Copy link
Contributor Author

nope3472 commented Jun 4, 2025

@mariobehling can you review this pr

@mariobehling
Copy link
Member

Please check sourcery comments and resolve where reasonable first. @nope3472

@nope3472
Copy link
Contributor Author

nope3472 commented Jun 5, 2025

sure @mariobehling will make these minor improvements

Choose a reason for hiding this comment

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

Is it necessary to change the getIntValue method to return speed.index + 1 instead of parsing it from the hex value for resolving the current issue?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I’m using this so the mapping’s simplerβ€”otherwise we’d have to mess with the hex value and convert it back just to end up with the same integer.

Choose a reason for hiding this comment

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

  1. Every get, save, or delete operation reads/writes the whole JSON file. For a large number of badges, this could become slow.
  2. If two badges are saved simultaneously (race condition), one could overwrite the other's changes, leading to lost data.

Copy link
Contributor

Choose a reason for hiding this comment

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

we are saving it in local storage its not in the same DB and at an instance only one badge will be saved or edited user cannot be doing two things at a time.
Json we are saving is very snall so that will not be an issue.

@nope3472
Copy link
Contributor Author

hey @mariobehling @Jhalakupadhyay can you guys finalise this pr and any other possible issues that you think should be resolved for this

Copy link
Contributor

Choose a reason for hiding this comment

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

why are we storing the data why wen are not directly assigning the data in the controllers and route the user to the homescreen. Everything that the user needs to edit is bieng saved already in the device storage we can use that to update the controllers rather than saving the data in the cache for sometime.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Jhalakupadhyay i have now Refactored the edit flow so that only the badge filename is passed to HomeScreen.
Refactored HomeScreen to load badge data from disk and populate controllers/providers.

@adityastic
Copy link
Collaborator

@samruddhi-Rahegaonkar
Copy link
Member

samruddhi-Rahegaonkar commented Jul 26, 2025

@nope3472 You have updated the test or Navigation?

@nope3472
Copy link
Contributor Author

Hey @samruddhi-Rahegaonkar β€” I’ve changed the flow slightly:
Saving a badge from the Home screen now ends the edit session instead of leaving the badge in β€œediting” mode.
If you want to edit that badge again, go to Saved Badges β†’ Edit and open it from there.

@samruddhi-Rahegaonkar
Copy link
Member

@nope3472 after clicking on overwrite or overwriting a badge it should go to Saved badge Screen. isn't it ?

@samruddhi-Rahegaonkar
Copy link
Member

@nope3472 After saving or editing a badge, the text field isn't clearing when navigating back to the Home screen.

@samruddhi-Rahegaonkar
Copy link
Member

@nope3472 Should badge names be treated as case-insensitive like (one, One, ONE) ?

@samruddhi-Rahegaonkar
Copy link
Member

samruddhi-Rahegaonkar commented Jul 26, 2025

Screenshot 2025-07-26 at 7 25 35β€―PM

Its asking for Overwrite but its not updating the name of the original one instead creating duplicate this is resulting in the wrong JSON sharing where both the badges having different characterstics but after sharing treating same.

@samruddhi-Rahegaonkar
Copy link
Member

samruddhi-Rahegaonkar commented Jul 26, 2025

this is development (Having Marquee = true) :
{"messages":[{"text":["001C0C0C7CCCCCCCCC7600","000000007CC6FEC0C67C00","00000000C6C6C66C381000","000000007CC6FEC0C67C00","0038181818181818183C00","000000007CC6C6C6C67C00","000000DC6666667C6060F0","00000000ECFED6D6D6C600","000000007CC6FEC0C67C00","00000000DC666666666600","00103030FC303030341800"],"flash":false,"marquee":false,"speed":"0x70","mode":"0x01","invert":false}]}

this is Development (having Marquee=false) :
{"messages":[{"text":["001C0C0C7CCCCCCCCC7600","000000007CC6FEC0C67C00","00000000C6C6C66C381000","000000007CC6FEC0C67C00","0038181818181818183C00","000000007CC6C6C6C67C00","000000DC6666667C6060F0","00000000ECFED6D6D6C600","000000007CC6FEC0C67C00","00000000DC666666666600","00103030FC303030341800"],"flash":false,"marquee":false,"speed":"0x70","mode":"0x01","invert":false}]}

@samruddhi-Rahegaonkar samruddhi-Rahegaonkar self-requested a review July 26, 2025 14:01
Copy link
Member

@samruddhi-Rahegaonkar samruddhi-Rahegaonkar left a comment

Choose a reason for hiding this comment

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

@nope3472 Please Address all the issue which i have commented. I guess then we are good to go @hpdang !

@nope3472
Copy link
Contributor Author

@samruddhi-Rahegaonkar thanks for pointing this out will do it

Copy link
Member

@samruddhi-Rahegaonkar samruddhi-Rahegaonkar left a comment

Choose a reason for hiding this comment

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

@nope3472 Why always there a pre-created empty badge named as badge_original_texts Please fix this ?
Second thing After saving or editing a badge, the text field isn't clearing when navigating back to the Home screen.

Screenshot 2025-07-27 at 6 43 52β€―PM Screenshot 2025-07-27 at 6 46 04β€―PM

@nope3472
Copy link
Contributor Author

@samruddhi-Rahegaonkar i will look for the empty badge but for clearing the text the original code of the app reatins the text after save so i have also followed that same rule

@samruddhi-Rahegaonkar
Copy link
Member

@nope3472 After overwriting its creating another again
Screenshot 2025-07-27 at 6 49 49β€―PM

Copy link
Member

@samruddhi-Rahegaonkar samruddhi-Rahegaonkar left a comment

Choose a reason for hiding this comment

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

@nope3472 Have you tested this ? because in my machine the same file names are still do not overwriting each other instead it is creating new badge as previous.

@samruddhi-Rahegaonkar samruddhi-Rahegaonkar self-requested a review July 31, 2025 16:52
Copy link
Member

@samruddhi-Rahegaonkar samruddhi-Rahegaonkar left a comment

Choose a reason for hiding this comment

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

LGTM! We are Good to go πŸ‘

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.

Make saved badges editable
7 participants