Skip to content
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

Sync's UX fixes #1189

Merged
merged 10 commits into from
Oct 30, 2024
Merged

Sync's UX fixes #1189

merged 10 commits into from
Oct 30, 2024

Conversation

mdmohsin7
Copy link
Collaborator

@mdmohsin7 mdmohsin7 commented Oct 27, 2024

sync_all_test_with_new_states.mp4
single_sync_with_better_states.mov

Summary by CodeRabbit

Release Notes

  • New Features

    • Enhanced user experience during memory syncing with updated status indicators, including a loading indicator and a message "Finalizing your memories."
    • Introduced a reprocessing option for discarded memories in the memory list.
    • Added a new function for converting seconds into a human-readable format.
    • Improved the UI for syncing memories with a more structured display of device type, ETA, and a sync button.
  • Bug Fixes

    • Improved handling of memory fetching states, ensuring accurate loading indicators.
  • Refactor

    • Streamlined memory list handling by removing unnecessary null checks.
    • Updated string handling methods for better data presentation.
    • Enhanced control flow for sync status determination and display.
    • Improved organization of code related to displaying and grouping memory items.

These changes aim to provide a smoother and more intuitive experience while managing memories in the app.

@mdmohsin7 mdmohsin7 requested a review from beastoin October 27, 2024 16:00
Copy link
Contributor

coderabbitai bot commented Oct 27, 2024

Walkthrough

The pull request introduces several modifications across multiple files related to memory synchronization features. Key changes include updates to the SyncPage class to allow navigation during syncing, enhancements to the MemoriesListWidget for improved null handling, and modifications to the SyncedMemoryListItem for better string handling and reprocessing logic. Additionally, a new property for tracking memory fetching state is added to the MemoryProvider, improving state management during asynchronous operations. The secondsToHumanReadable function is removed from MemoriesPage, with its functionality replaced by a new implementation in a utility file.

Changes

File Path Change Summary
app/lib/pages/memories/sync_page.dart - Added calculateProgress method in _WalListItemState.
- Restructured WalListItem UI with ListTile.
- Introduced WalsListWidget class.
- Updated SyncPage to manage FAB visibility with AnimationController.
- Removed dialog for waiting during sync.
app/lib/pages/memories/synced_memories_page.dart - Changed memories parameter from nullable to non-nullable list.
- Simplified null-checking logic in build method.
- Updated itemBuilder and itemCount without null-safe operator.
app/lib/pages/memories/widgets/synced_memory_list_item.dart - Added import for string extension utility.
- Updated string handling using decodeString for memory.transcriptSegments.first.text and memory.structured.title.
- Modified reprocessing button logic to display under new conditions.
app/lib/providers/memory_provider.dart - Added isFetchingMemories boolean property.
- Introduced setIsFetchingMemories(bool value) method.
- Updated syncWals and getSyncedMemoriesData methods for state management.
- Added syncWal(Wal wal) method for individual sync operations.
app/lib/pages/memories/page.dart - Removed secondsToHumanReadable function.
app/lib/pages/memories/widgets/local_sync.dart - Removed import for memories/page.dart.
- Added import for sdcard_transfer_progress.dart.
- Updated _status logic and how _missSeconds is handled.
app/lib/utils/other/time_utils.dart - Added new secondsToHumanReadable function for converting seconds to a human-readable format.
- Added secondsToHMS function for formatting seconds as "hh:mm:ss".
app/lib/backend/preferences.dart - Updated localSyncEnabled getter to return true by default if not found in shared preferences.
app/lib/services/wals.dart - Added syncWal method to IWalSync interface and its implementations for individual Wal synchronization.
- Updated syncAll method to return synchronization results.

Possibly related PRs

Suggested reviewers

  • beastoin

🐇 In the meadow where memories play,
The sync page now leads the way.
With buttons to pop and strings to decode,
Fetching memories lightens the load.
A hop and a skip, all's well in the end,
For memories shared, our hearts will mend! 🌸


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@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: 3

🧹 Outside diff range and nitpick comments (3)
app/lib/pages/memories/synced_memories_page.dart (1)

Line range hint 17-23: Consider handling loading states

Given that MemoryProvider now tracks memory fetching state with isFetchingMemories, consider showing a loading indicator when memories are being fetched. This would provide better feedback to users during data loading.

   return SingleChildScrollView(
     child: Column(
       mainAxisSize: MainAxisSize.min,
       children: [
+        if (memoryProvider.isFetchingMemories)
+          const Padding(
+            padding: EdgeInsets.all(16.0),
+            child: CircularProgressIndicator(color: Colors.white),
+          ),
+        if (!memoryProvider.isFetchingMemories) ...[
           MemoriesListWidget(
             memories: memoryProvider.syncedMemoriesPointers
                 .where((e) => e.type == SyncedMemoryType.updatedMemory)
                 .toList(),
             title: 'Updated Memories',
             showReprocess: true,
           ),
           MemoriesListWidget(
             memories: memoryProvider.syncedMemoriesPointers
                 .where((e) => e.type == SyncedMemoryType.newMemory)
                 .toList(),
             title: 'New Memories',
             showReprocess: false,
           ),
+        ],
       ],
     ),
   );
app/lib/providers/memory_provider.dart (1)

Line range hint 377-392: Improve error handling in getSyncedMemoriesData

While the fetching state management is good, the error handling could be improved:

  1. Use proper logging instead of print
  2. Consider propagating the error to allow upstream handling
  3. Add more specific error handling for different types of failures

Consider this improvement:

 Future getSyncedMemoriesData(SyncLocalFilesResponse syncResult) async {
   List<dynamic> newMemories = syncResult.newMemoryIds;
   List<dynamic> updatedMemories = syncResult.updatedMemoryIds;
   setIsFetchingMemories(true);
   List<Future<ServerMemory?>> newMemoriesFutures = newMemories.map((item) => getMemoryDetails(item)).toList();
   List<Future<ServerMemory?>> updatedMemoriesFutures = updatedMemories.map((item) => getMemoryDetails(item)).toList();
   var syncedMemories = {'new_memories': [], 'updated_memories': []};
   try {
     final newMemoriesResponses = await Future.wait(newMemoriesFutures);
     syncedMemories['new_memories'] = newMemoriesResponses;
     final updatedMemoriesResponses = await Future.wait(updatedMemoriesFutures);
     syncedMemories['updated_memories'] = updatedMemoriesResponses;
     addSyncedMemoriesToGroupedMemories(syncedMemories);
     setIsFetchingMemories(false);
-  } catch (e) {
-    print('Error during API calls: $e');
+  } catch (e, stackTrace) {
+    debugPrint('Error during memory sync: $e\n$stackTrace');
+    // Consider showing a user-friendly error message
+    rethrow; // Allow upstream error handling
   } finally {
     setIsFetchingMemories(false);
   }
 }
app/lib/pages/memories/sync_page.dart (1)

341-347: Consider increasing loading indicator size

The current 20x20 size might be too small for optimal visibility. Consider increasing it to match common Flutter loading indicator sizes (24x24 or 28x28).

-                                            height: 20,
-                                            width: 20,
+                                            height: 24,
+                                            width: 24,
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between e6bcaa9 and 857d1b2.

📒 Files selected for processing (4)
  • app/lib/pages/memories/sync_page.dart (3 hunks)
  • app/lib/pages/memories/synced_memories_page.dart (3 hunks)
  • app/lib/pages/memories/widgets/synced_memory_list_item.dart (2 hunks)
  • app/lib/providers/memory_provider.dart (4 hunks)
🔇 Additional comments (5)
app/lib/pages/memories/synced_memories_page.dart (1)

48-48: LGTM: Type safety improvement

The change from nullable to non-nullable list is safe and improves type safety, as the parameter is required and the parent widget always provides a non-null list.

app/lib/pages/memories/widgets/synced_memory_list_item.dart (2)

8-8: LGTM: Import added for string extension utility

The import is correctly placed and supports the new string decoding functionality.


103-104: Verify business logic: Reprocessing now available for discarded memories

The condition has been expanded to allow reprocessing of discarded memories. Please confirm if this is the intended behavior:

  • Previously: Only showed when showReprocess was true
  • Now: Shows when either showReprocess is true OR memory.discarded is true
#!/bin/bash
# Look for related logic around discarded memories and reprocessing
rg "discarded.*reprocess|reprocess.*discarded" --type dart

# Check for any tests related to this functionality
rg -l "discarded.*test|test.*reprocess" --type dart
app/lib/providers/memory_provider.dart (1)

40-40: LGTM: Clean implementation of fetching state management

The new property and setter method follow Flutter/Dart conventions and properly notify listeners of state changes.

Also applies to: 469-472

app/lib/pages/memories/sync_page.dart (1)

283-303: Verify the safety of allowing navigation during sync

While improving UX by allowing users to navigate away during sync, we should ensure:

  1. The sync process continues in the background
  2. No data inconsistencies occur if the user navigates away
  3. The sync state is properly managed at the application level
✅ Verification successful

Navigation during sync is safe to allow

The code analysis reveals that sync state is properly managed at the application level:

  1. The sync process is handled by WalService and WalSyncs which operate independently of the UI
  2. The sync state (isSyncing) is managed in multiple layers:
    • MemoryProvider for UI state updates
    • WalService for actual sync operations
  3. The sync process continues in the background through:
    • Independent service architecture
    • Proper state management with listeners
    • Robust error handling and progress tracking
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check if sync state is properly managed at app level
# Look for sync state management in providers and services

# Check for sync state management in providers
rg -l "isSyncing" --type dart

# Check for background sync handling
rg "sync.*background|background.*sync" --type dart

Length of output: 186


Script:

#!/bin/bash
# Check implementation of isSyncing in memory provider
ast-grep --pattern 'class MemoryProvider $_$ {
  $$$
  bool isSyncing = false;
  $$$
}'

# Check how isSyncing is set/managed
rg "isSyncing\s*=\s*" -A 2 --type dart

# Check sync implementation in wals service
rg "sync" -A 5 app/lib/services/wals.dart

# Look for any sync completion handlers
rg "onSync(Complete|Finished|Done)" --type dart

Length of output: 12907

Comment on lines +55 to 57
if (memories.isEmpty) {
return const SizedBox();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider improving empty state feedback

Instead of showing nothing when there are no memories, consider displaying a message to inform users about the empty state. This would improve user experience by making it clear that there are no memories rather than leaving users wondering if something went wrong.

   if (memories.isEmpty) {
-    return const SizedBox();
+    return Padding(
+      padding: const EdgeInsets.symmetric(vertical: 8.0),
+      child: Text(
+        'No $title available',
+        style: const TextStyle(
+          color: Colors.white70,
+          fontSize: 16,
+        ),
+      ),
+    );
   }

Committable suggestion was skipped due to low confidence.

Comment on lines +90 to 97
memory.transcriptSegments.first.text.decodeString,
style: Theme.of(context).textTheme.titleLarge,
maxLines: 1,
)
: Text(
memory.structured.title,
memory.structured.title.decodeString,
style: Theme.of(context).textTheme.titleLarge,
maxLines: 1,
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Codebase verification

String decoding implementation is consistent but needs standardization

The verification reveals:

  • decodeString extension method is properly implemented in app/lib/widgets/extensions/string.dart using UTF-8 decoding
  • Most text display instances use decodeString consistently for memory titles, transcripts, and content
  • However, there's an inconsistent pattern where tryDecodeText is used in ai_message.dart that implements the same functionality

Action items:

  • Replace tryDecodeText in app/lib/pages/chat/widgets/ai_message.dart with the standard decodeString extension
  • Add decodeString to these missing instances:
    • app/lib/pages/memory_detail/share.dart: structured.title usage
    • app/lib/pages/memory_detail/widgets.dart: One instance of provider.memory.structured.title
    • app/lib/pages/chat/widgets/ai_message.dart: modifiedDetails.structured.title
🔗 Analysis chain

Verify consistent string decoding across the application

The addition of decodeString suggests there might be encoded characters in the memory content. Let's verify this pattern is consistently applied where needed.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for other instances of memory transcript or title usage that might need decoding
rg -l "transcriptSegments.*text|structured\.title" --type dart

# Check the string extension implementation
rg -l "decodeString" --type dart

Length of output: 114


Script:

#!/bin/bash
# Let's try a more focused search for transcript and title usage
rg "transcriptSegments.*?text|structured\.title" --type dart -A 2

# Search for the decodeString extension method definition
ast-grep --pattern 'extension $_ on String { $$$decodeString$$$ }'

Length of output: 3785


Script:

#!/bin/bash
# Let's find the decodeString implementation since we see it's being used
rg "decodeString" --type dart -B 2 -A 2

# Also check for tryDecodeText since we saw it in the results
rg "tryDecodeText" --type dart -B 2 -A 2

Length of output: 10229

Comment on lines 335 to 364
? memoryProvider.isFetchingMemories
? const Padding(
padding: EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
color: Colors.white,
),
),
SizedBox(width: 12),
Text(
'Finalizing your memories',
style: TextStyle(color: Colors.white, fontSize: 16),
textAlign: TextAlign.center,
),
],
),
)
: const Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'Syncing Memories\nPlease don\'t close the app',
style: TextStyle(color: Colors.white, fontSize: 16),
textAlign: TextAlign.center,
),
)
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Extract text styles to constants

Consider extracting the text styles to constants for better maintainability and consistency. This applies to both the "Finalizing your memories" and "Syncing Memories" text styles.

// Add to a styles.dart file or similar
class SyncPageStyles {
  static const TextStyle statusText = TextStyle(
    color: Colors.white,
    fontSize: 16,
  );
}

Then update the usage:

-                                            style: TextStyle(color: Colors.white, fontSize: 16),
+                                            style: SyncPageStyles.statusText,

@@ -280,26 +280,27 @@ class _SyncPageState extends State<SyncPage> {
}

return PopScope(
canPop: !Provider.of<MemoryProvider>(context, listen: false).isSyncing,
canPop: true,
onPopInvoked: (didPop) {
var provider = Provider.of<MemoryProvider>(context, listen: false);
Copy link
Collaborator

Choose a reason for hiding this comment

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

We allow pop, btw the sync process should be stop if users exited the sync page. We haven't had the background syncs yet 🫣

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The sync state is handled by provider and provider isn't disposed until the user closes the app. So the sync should keep running even if the user is on another screen. That is way changed the text as well (to only mention not to close the app)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Lmk if I should revert it

Copy link
Collaborator

Choose a reason for hiding this comment

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

would be great if we could indicate it somewhere that the sync is running, and when the provider closes, we could gratefully stop the sync(e.g notify sd card to stop sync) @mdmohsin7 any idea?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We can definitely have some sync in progress indicator maybe through a notification without cluttering the existing UI. What do you think? @beastoin

Provider is only disposed when the app is restarted or the app is closed completely

@beastoin beastoin changed the title Sync fixes Sync's UX fixes Oct 28, 2024
Copy link
Contributor

@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: 9

🧹 Outside diff range and nitpick comments (4)
app/lib/utils/other/time_utils.dart (1)

1-29: Consider adding unit tests.

Given this is a utility function that handles various time ranges and formatting cases, it would be beneficial to have comprehensive unit tests.

Would you like me to generate unit tests covering various scenarios including edge cases?

app/lib/pages/memories/widgets/local_sync.dart (2)

Line range hint 64-71: Consider extracting magic numbers as named constants.

The threshold of 120 seconds should be extracted as a named constant at the class level for better maintainability and clarity.

class _LocalSyncWidgetState extends State<LocalSyncWidget> {
+  static const int _SYNC_THRESHOLD_SECONDS = 120;
  LocalSyncStatus? _status;
  Timer? _missSecondsInEstTimer;
  bool _missSecondsInEstTimerEnabled = false;
  int _missSeconds = 0;

  @override
  void build(BuildContext context) {
    return Consumer2<MemoryProvider, CaptureProvider>(builder: (context, provider, captureProvider, child) {
      var previousStatus = _status;
-      if (provider.missingWalsInSeconds >= 120) {
+      if (provider.missingWalsInSeconds >= _SYNC_THRESHOLD_SECONDS) {

Line range hint 32-42: Optimize timer management.

The timer is created in initState and only cancelled in dispose. Consider pausing/resuming the timer based on the widget's status to improve performance.

  @override
  void initState() {
    super.initState();
+    _startTimerIfNeeded();
+  }

+  void _startTimerIfNeeded() {
+    _missSecondsInEstTimer?.cancel();
    _missSecondsInEstTimer = Timer.periodic(const Duration(seconds: 1), (t) {
      if (_missSecondsInEstTimerEnabled) {
        setState(() {
          _missSeconds++;
        });
      }
    });
  }

+  void _stopTimer() {
+    _missSecondsInEstTimer?.cancel();
+    _missSecondsInEstTimer = null;
+  }
app/lib/pages/memories/sync_page.dart (1)

289-291: Remove commented out code

The commented out _toggleAnimation calls should be removed as they're no longer used.

-                          // _toggleAnimation();
                           await memoryProvider.syncWals();
-                          // _toggleAnimation();
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 857d1b2 and a1ac7ce.

📒 Files selected for processing (4)
  • app/lib/pages/memories/page.dart (0 hunks)
  • app/lib/pages/memories/sync_page.dart (3 hunks)
  • app/lib/pages/memories/widgets/local_sync.dart (2 hunks)
  • app/lib/utils/other/time_utils.dart (1 hunks)
💤 Files with no reviewable changes (1)
  • app/lib/pages/memories/page.dart
🔇 Additional comments (3)
app/lib/pages/memories/widgets/local_sync.dart (1)

118-118: Verify type conversion in secondsToHumanReadable call.

Converting _missSeconds to string before passing to secondsToHumanReadable seems unnecessary and might indicate a type mismatch.

#!/bin/bash
# Check the function signature in the codebase
rg "secondsToHumanReadable" --type dart
app/lib/pages/memories/sync_page.dart (2)

209-222: LGTM: Clean state management implementation

The state management implementation follows Flutter best practices with proper initialization and disposal of the animation controller.


440-467: LGTM: Well-implemented list widget

The WalsListWidget implementation is clean and efficient:

  • Uses SliverList for better performance
  • Properly handles empty states
  • Clean separation of grouping logic

Comment on lines 2 to 11
if (seconds < 60) {
return '$seconds secs';
} else if (seconds < 3600) {
var minutes = (seconds / 60).floor();
var remainingSeconds = seconds % 60;
if (remainingSeconds == 0) {
return '$minutes mins';
} else {
return '$minutes mins $remainingSeconds secs';
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix singular/plural forms for minutes and seconds.

The current implementation doesn't handle singular forms correctly (e.g., "1 mins" should be "1 min").

Apply this diff to fix the grammar:

   if (seconds < 60) {
-    return '$seconds secs';
+    return '$seconds ${seconds == 1 ? 'sec' : 'secs'}';
   } else if (seconds < 3600) {
     var minutes = (seconds / 60).floor();
     var remainingSeconds = seconds % 60;
     if (remainingSeconds == 0) {
-      return '$minutes mins';
+      return '$minutes ${minutes == 1 ? 'min' : 'mins'}';
     } else {
-      return '$minutes mins $remainingSeconds secs';
+      return '$minutes ${minutes == 1 ? 'min' : 'mins'} $remainingSeconds ${remainingSeconds == 1 ? 'sec' : 'secs'}';
     }
📝 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
if (seconds < 60) {
return '$seconds secs';
} else if (seconds < 3600) {
var minutes = (seconds / 60).floor();
var remainingSeconds = seconds % 60;
if (remainingSeconds == 0) {
return '$minutes mins';
} else {
return '$minutes mins $remainingSeconds secs';
}
if (seconds < 60) {
return '$seconds ${seconds == 1 ? 'sec' : 'secs'}';
} else if (seconds < 3600) {
var minutes = (seconds / 60).floor();
var remainingSeconds = seconds % 60;
if (remainingSeconds == 0) {
return '$minutes ${minutes == 1 ? 'min' : 'mins'}';
} else {
return '$minutes ${minutes == 1 ? 'min' : 'mins'} $remainingSeconds ${remainingSeconds == 1 ? 'sec' : 'secs'}';
}

@@ -0,0 +1,29 @@
String secondsToHumanReadable(int seconds) {
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add input validation and documentation.

The function should validate its input and include documentation explaining its purpose and usage.

Apply this diff to add validation and documentation:

+/// Converts a duration in seconds to a human-readable string format.
+/// 
+/// Examples:
+/// - 30 -> "30 secs"
+/// - 90 -> "1 min 30 secs"
+/// - 3600 -> "1 hour"
+/// - 90000 -> "1 day 1 hour"
+/// 
+/// @param seconds The number of seconds to convert
+/// @throws ArgumentError if seconds is negative
 String secondsToHumanReadable(int seconds) {
+  if (seconds < 0) {
+    throw ArgumentError('Seconds cannot be negative');
+  }
📝 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
String secondsToHumanReadable(int seconds) {
/// Converts a duration in seconds to a human-readable string format.
///
/// Examples:
/// - 30 -> "30 secs"
/// - 90 -> "1 min 30 secs"
/// - 3600 -> "1 hour"
/// - 90000 -> "1 day 1 hour"
///
/// @param seconds The number of seconds to convert
/// @throws ArgumentError if seconds is negative
String secondsToHumanReadable(int seconds) {
if (seconds < 0) {
throw ArgumentError('Seconds cannot be negative');
}

Comment on lines 12 to 20
} else if (seconds < 86400) {
var hours = (seconds / 3600).floor();
var remainingMinutes = (seconds % 3600 / 60).floor();
if (remainingMinutes == 0) {
return '$hours hours';
} else {
return '$hours hours $remainingMinutes mins';
}
} else {
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix singular/plural forms for hours.

Similar to the previous issue, hour/hours needs proper handling.

Apply this diff to fix the grammar:

   } else if (seconds < 86400) {
     var hours = (seconds / 3600).floor();
     var remainingMinutes = (seconds % 3600 / 60).floor();
     if (remainingMinutes == 0) {
-      return '$hours hours';
+      return '$hours ${hours == 1 ? 'hour' : 'hours'}';
     } else {
-      return '$hours hours $remainingMinutes mins';
+      return '$hours ${hours == 1 ? 'hour' : 'hours'} $remainingMinutes ${remainingMinutes == 1 ? 'min' : 'mins'}';
     }
📝 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
} else if (seconds < 86400) {
var hours = (seconds / 3600).floor();
var remainingMinutes = (seconds % 3600 / 60).floor();
if (remainingMinutes == 0) {
return '$hours hours';
} else {
return '$hours hours $remainingMinutes mins';
}
} else {
} else if (seconds < 86400) {
var hours = (seconds / 3600).floor();
var remainingMinutes = (seconds % 3600 / 60).floor();
if (remainingMinutes == 0) {
return '$hours ${hours == 1 ? 'hour' : 'hours'}';
} else {
return '$hours ${hours == 1 ? 'hour' : 'hours'} $remainingMinutes ${remainingMinutes == 1 ? 'min' : 'mins'}';
}
} else {

Comment on lines 21 to 28
var days = (seconds / 86400).floor();
var remainingHours = (seconds % 86400 / 3600).floor();
if (remainingHours == 0) {
return '$days days';
} else {
return '$days days $remainingHours hours';
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix singular/plural forms for days.

Days also needs proper singular/plural handling.

Apply this diff to fix the grammar:

     var days = (seconds / 86400).floor();
     var remainingHours = (seconds % 86400 / 3600).floor();
     if (remainingHours == 0) {
-      return '$days days';
+      return '$days ${days == 1 ? 'day' : 'days'}';
     } else {
-      return '$days days $remainingHours hours';
+      return '$days ${days == 1 ? 'day' : 'days'} $remainingHours ${remainingHours == 1 ? 'hour' : 'hours'}';
     }
📝 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
var days = (seconds / 86400).floor();
var remainingHours = (seconds % 86400 / 3600).floor();
if (remainingHours == 0) {
return '$days days';
} else {
return '$days days $remainingHours hours';
}
}
var days = (seconds / 86400).floor();
var remainingHours = (seconds % 86400 / 3600).floor();
if (remainingHours == 0) {
return '$days ${days == 1 ? 'day' : 'days'}';
} else {
return '$days ${days == 1 ? 'day' : 'days'} $remainingHours ${remainingHours == 1 ? 'hour' : 'hours'}';
}
}

import 'package:friend_private/pages/memories/sync_page.dart';
import 'package:friend_private/pages/sdcard/sdcard_transfer_progress.dart';
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Codebase verification

Remove unused import of sdcard_transfer_progress.dart

The sdcard_transfer_progress.dart import is not being used in local_sync.dart. The secondsToHumanReadable function that's being used in local_sync.dart appears to be defined in multiple places, but based on the import statements, it's likely coming from utils/other/time_utils.dart which should already be available through other imports.

🔗 Analysis chain

Verify usage of sdcard_transfer_progress import.

The newly added import appears to be unused in the current implementation.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check if the imported file's components are used
rg "SdcardTransferProgress" app/lib/pages/memories/widgets/local_sync.dart

Length of output: 775


Script:

#!/bin/bash
# Let's check the full content of local_sync.dart to understand the usage
cat app/lib/pages/memories/widgets/local_sync.dart

Length of output: 4316


Script:

#!/bin/bash
# Let's check the content of sdcard_transfer_progress.dart to see what it exports
cat app/lib/pages/sdcard/sdcard_transfer_progress.dart

# Also check where secondsToHumanReadable is defined
rg "secondsToHumanReadable" --type dart

Length of output: 6569

Comment on lines +274 to +282
border: const GradientBoxBorder(
gradient: LinearGradient(colors: [
Color.fromARGB(127, 208, 208, 208),
Color.fromARGB(127, 188, 99, 121),
Color.fromARGB(127, 86, 101, 182),
Color.fromARGB(127, 126, 190, 236)
]),
width: 2,
),
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Extract duplicate gradient border decoration

The gradient border decoration is duplicated. Consider extracting it to a reusable constant or widget.

// Add to a constants file or within the class
const _gradientBorderDecoration = GradientBoxBorder(
  gradient: LinearGradient(
    colors: [
      Color.fromARGB(127, 208, 208, 208),
      Color.fromARGB(127, 188, 99, 121),
      Color.fromARGB(127, 86, 101, 182),
      Color.fromARGB(127, 126, 190, 236)
    ]
  ),
  width: 2,
);

Also applies to: 385-393

Comment on lines +287 to +300
onPressed: () async {
if (context.read<ConnectivityProvider>().isConnected) {
// _toggleAnimation();
await memoryProvider.syncWals();
// _toggleAnimation();
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Internet connection is required to sync memories'),
backgroundColor: Colors.red,
),
);
}
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add error handling for sync operation

The sync operation should handle potential errors and show appropriate error messages to the user.

                      onPressed: () async {
                        if (context.read<ConnectivityProvider>().isConnected) {
-                          await memoryProvider.syncWals();
+                          try {
+                            await memoryProvider.syncWals();
+                          } catch (e) {
+                            if (mounted) {
+                              ScaffoldMessenger.of(context).showSnackBar(
+                                SnackBar(
+                                  content: Text('Failed to sync memories: ${e.toString()}'),
+                                  backgroundColor: Colors.red,
+                                ),
+                              );
+                            }
+                          }
                        } else {
📝 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
onPressed: () async {
if (context.read<ConnectivityProvider>().isConnected) {
// _toggleAnimation();
await memoryProvider.syncWals();
// _toggleAnimation();
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Internet connection is required to sync memories'),
backgroundColor: Colors.red,
),
);
}
},
onPressed: () async {
if (context.read<ConnectivityProvider>().isConnected) {
// _toggleAnimation();
try {
await memoryProvider.syncWals();
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to sync memories: ${e.toString()}'),
backgroundColor: Colors.red,
),
);
}
}
// _toggleAnimation();
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Internet connection is required to sync memories'),
backgroundColor: Colors.red,
),
);
}
},

Comment on lines +224 to +245
bool _handleScrollNotification(ScrollNotification notification) {
if (notification.depth == 0) {
if (notification is UserScrollNotification) {
final UserScrollNotification userScroll = notification;
switch (userScroll.direction) {
case ScrollDirection.forward:
if (userScroll.metrics.maxScrollExtent != userScroll.metrics.minScrollExtent) {
_hideFabAnimation.forward();
}

var date = keys[idx];
List<Wal> wals = groupedWals[date] ?? [];
return Column(
mainAxisSize: MainAxisSize.min,
children: [
if (index == 0) const SizedBox(height: 16),
SyncWalGroupWidget(
isFirst: index == 0,
wals: wals,
date: date,
),
],
);
},
),
);
break;
case ScrollDirection.reverse:
if (userScroll.metrics.maxScrollExtent != userScroll.metrics.minScrollExtent) {
_hideFabAnimation.reverse();
}
break;
case ScrollDirection.idle:
break;
}
}
}
return false;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider simplifying scroll notification handler

The scroll handling logic can be simplified by flattening conditions and extracting the scroll extent check.

  bool _handleScrollNotification(ScrollNotification notification) {
-    if (notification.depth == 0) {
-      if (notification is UserScrollNotification) {
-        final UserScrollNotification userScroll = notification;
-        switch (userScroll.direction) {
-          case ScrollDirection.forward:
-            if (userScroll.metrics.maxScrollExtent != userScroll.metrics.minScrollExtent) {
-              _hideFabAnimation.forward();
-            }
-            break;
-          case ScrollDirection.reverse:
-            if (userScroll.metrics.maxScrollExtent != userScroll.metrics.minScrollExtent) {
-              _hideFabAnimation.reverse();
-            }
-            break;
-          case ScrollDirection.idle:
-            break;
-        }
-      }
-    }
+    if (notification.depth != 0 || notification is! UserScrollNotification) {
+      return false;
+    }
+    
+    final hasScrollExtent = notification.metrics.maxScrollExtent != notification.metrics.minScrollExtent;
+    if (!hasScrollExtent) {
+      return false;
+    }
+    
+    switch (notification.direction) {
+      case ScrollDirection.forward:
+        _hideFabAnimation.forward();
+      case ScrollDirection.reverse:
+        _hideFabAnimation.reverse();
+      case ScrollDirection.idle:
+        break;
+    }
     return false;
   }
📝 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
bool _handleScrollNotification(ScrollNotification notification) {
if (notification.depth == 0) {
if (notification is UserScrollNotification) {
final UserScrollNotification userScroll = notification;
switch (userScroll.direction) {
case ScrollDirection.forward:
if (userScroll.metrics.maxScrollExtent != userScroll.metrics.minScrollExtent) {
_hideFabAnimation.forward();
}
var date = keys[idx];
List<Wal> wals = groupedWals[date] ?? [];
return Column(
mainAxisSize: MainAxisSize.min,
children: [
if (index == 0) const SizedBox(height: 16),
SyncWalGroupWidget(
isFirst: index == 0,
wals: wals,
date: date,
),
],
);
},
),
);
break;
case ScrollDirection.reverse:
if (userScroll.metrics.maxScrollExtent != userScroll.metrics.minScrollExtent) {
_hideFabAnimation.reverse();
}
break;
case ScrollDirection.idle:
break;
}
}
}
return false;
}
bool _handleScrollNotification(ScrollNotification notification) {
if (notification.depth != 0 || notification is! UserScrollNotification) {
return false;
}
final hasScrollExtent = notification.metrics.maxScrollExtent != notification.metrics.minScrollExtent;
if (!hasScrollExtent) {
return false;
}
switch (notification.direction) {
case ScrollDirection.forward:
_hideFabAnimation.forward();
case ScrollDirection.reverse:
_hideFabAnimation.reverse();
case ScrollDirection.idle:
break;
}
return false;
}

Comment on lines 424 to 438
Map<DateTime, List<Wal>> _groupWalsByDate(List<Wal> wals) {
var groupedWals = <DateTime, List<Wal>>{};
for (var wal in wals) {
var createdAt = DateTime.fromMillisecondsSinceEpoch(wal.timerStart * 1000).toLocal();
var date = DateTime(createdAt.year, createdAt.month, createdAt.day, createdAt.hour);
if (!groupedWals.containsKey(date)) {
groupedWals[date] = [];
}
groupedWals[date]?.add(wal);
}
for (final date in groupedWals.keys) {
groupedWals[date]?.sort((a, b) => b.timerStart.compareTo(a.timerStart));
}
return groupedWals;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider moving grouping logic to a utility file

The _groupWalsByDate function is a utility function that could be reused across the app. Consider:

  1. Moving it to a utility file
  2. Adding documentation to explain the grouping logic
+/// Groups a list of Wal objects by their creation date, using hour-level precision.
+/// The resulting map contains DateTime keys (truncated to hour) and sorted lists of Wals.
+/// The Wals in each group are sorted in descending order by their timer start time.
 Map<DateTime, List<Wal>> _groupWalsByDate(List<Wal> wals) {

Committable suggestion was skipped due to low confidence.

Copy link
Contributor

@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: 9

🧹 Outside diff range and nitpick comments (4)
app/lib/providers/memory_provider.dart (3)

40-41: Consider adding documentation for new state properties.

The new state properties look good, but would benefit from documentation explaining their purpose:

  • multipleSyncs: Tracks concurrent sync operations
  • isFetchingMemories: Tracks memory fetching state
+  /// Stack to track multiple concurrent sync operations
   List<bool> multipleSyncs = [];
+  /// Indicates whether memories are currently being fetched
   bool isFetchingMemories = false;

375-389: Use debugPrint consistently for logging.

The implementation looks good, but for consistency with line 376, replace print with debugPrint:

-        print('Synced memories: ${res.newMemoryIds} ${res.updatedMemoryIds}');
+        debugPrint('Synced memories: ${res.newMemoryIds} ${res.updatedMemoryIds}');

Line range hint 399-414: Use debugPrint for error logging.

The fetching state management looks good, but for consistency:

-      print('Error during API calls: $e');
+      debugPrint('Error during API calls: $e');
app/lib/pages/memories/sync_page.dart (1)

309-324: Enhance the warning message for better user guidance.

The current warning message could be more informative about the sync process and its implications.

-                              child: const ListTile(
-                                leading: Icon(
-                                  Icons.warning,
-                                  color: Colors.yellow,
-                                ),
-                                title: Text('Please do not close the app while sync is in progress'),
-                              ),
+                              child: const ListTile(
+                                leading: Icon(
+                                  Icons.warning,
+                                  color: Colors.yellow,
+                                ),
+                                title: Text('Please keep the app open'),
+                                subtitle: Text('Closing the app will interrupt the sync process and may result in incomplete data'),
+                              ),
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 273637d and 2b2cae5.

📒 Files selected for processing (5)
  • app/lib/backend/preferences.dart (1 hunks)
  • app/lib/pages/memories/sync_page.dart (4 hunks)
  • app/lib/providers/memory_provider.dart (5 hunks)
  • app/lib/services/wals.dart (4 hunks)
  • app/lib/utils/other/time_utils.dart (1 hunks)
🔇 Additional comments (8)
app/lib/utils/other/time_utils.dart (1)

1-38: Add input validation and documentation.

The previous review comment about adding input validation and documentation is still valid and should be addressed.

app/lib/backend/preferences.dart (1)

126-126: Verify the impact of changing the default sync behavior.

Changing the default value of localSyncEnabled from false to true is a breaking change that affects the default synchronization behavior for new installations or reset preferences. This change needs careful consideration:

  1. Ensure this aligns with the broader sync-related changes mentioned in the PR
  2. Verify the impact on the MemoryProvider and sync functionality
  3. Consider documenting this change in the migration guide if applicable

Let's verify the usage and dependencies:

✅ Verification successful

Based on the search results, I can see that localSyncEnabled is used in several key places:

  1. In CaptureProvider to determine if local sync should be enabled during capture
  2. In DeveloperModeProvider as a toggle setting in developer mode
  3. The default value change affects new installations and reset preferences

Let's verify the impact by checking if there are any conditional behaviors or dependencies on this setting:


Let's check one more thing to understand how this setting affects the actual sync behavior:


Let's check one final thing to understand where this setting is actually used in the sync logic:


Based on the comprehensive analysis of the codebase, I can now provide a final response:

The default value change for localSyncEnabled is safe and intentional

The change from false to true as the default value for localSyncEnabled is safe because:

  1. The setting is only used in a very specific scenario - it enables local sync for opus codec audio after 15 seconds of device connection
  2. It's primarily a developer mode setting that can be toggled in the developer settings UI
  3. The actual sync behavior has proper guards and checks beyond just this flag:
    • Requires opus codec
    • Device must be connected for >15 seconds
    • WAL (Write-Ahead Logging) support must be verified
  4. Existing installations won't be affected as they already have the preference set

The change aligns with making local sync enabled by default for better data reliability, while still allowing it to be disabled through developer settings if needed.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for direct usage of localSyncEnabled
echo "=== Direct usage of localSyncEnabled ==="
rg "localSyncEnabled" -B 2 -A 2

# Search for sync-related functionality that might be affected
echo "=== Related sync functionality ==="
rg "sync|synchroniz(e|ation)" --type dart -B 2 -A 2

Length of output: 245924


Script:

#!/bin/bash
# Search for conditional logic using localSyncEnabled
echo "=== Conditional logic using localSyncEnabled ==="
rg "if.*localSyncEnabled" -B 2 -A 2

# Search for any assignments to localSyncEnabled
echo "=== Assignments to localSyncEnabled ==="
rg "localSyncEnabled\s*=\s*" -B 2 -A 2

Length of output: 2366


Script:

#!/bin/bash
# Search for where localSyncEnabled is used in conditional logic
echo "=== Usage of localSyncEnabled in sync logic ==="
rg -B 5 -A 5 "localSyncEnabled.*\?" 

# Search for sync-related methods that might use this flag
echo "=== Sync methods and localSyncEnabled ==="
rg -B 5 -A 5 "sync.*local.*enabled|local.*sync.*enabled"

Length of output: 1056


Script:

#!/bin/bash
# Search for files containing both sync and localSyncEnabled references
echo "=== Files with both sync and localSyncEnabled ==="
rg -l "sync" | xargs rg -l "localSyncEnabled"

# Look for actual usage of localSyncEnabled in capture logic
echo "=== Usage in capture logic ==="
rg -B 10 -A 10 "localSyncEnabled" app/lib/providers/capture_provider.dart

Length of output: 2148

app/lib/providers/memory_provider.dart (2)

Line range hint 360-373: LGTM! Improved sync state management.

The sync state management has been enhanced with proper state transitions using setSyncCompleted.


391-394: LGTM! Clean state management implementation.

The method properly updates the sync completion state and notifies listeners.

app/lib/pages/memories/sync_page.dart (2)

187-200: LGTM: Animation controller setup is correct.

The initialization and disposal of the animation controller follow Flutter best practices.


435-463: LGTM: WalsListWidget implementation is clean and efficient.

The widget properly uses SliverList for efficient list rendering and correctly handles the grouping of Wals.

app/lib/services/wals.dart (2)

34-34: Addition of syncWal method to IWalSync interface

The syncWal method has been added to the IWalSync interface, allowing synchronization of individual Wal instances. This change enhances the flexibility and granularity of the synchronization process.


898-904: Ensure all WalStorage types are properly handled

The syncWal method delegates to _sdcardSync if wal.storage is WalStorage.sdcard and to _phoneSync otherwise. Since WalStorage includes mem, disk, and sdcard, ensure that _phoneSync correctly handles both WalStorage.mem and WalStorage.disk, or consider explicitly handling each storage type to prevent unexpected behaviors.

}
return '$minutes mins';
} else {
return '$minutes mins $remainingSeconds secs';
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Optimize string concatenation using interpolation.

Consider using string interpolation with conditional expressions for more concise and maintainable code.

Example improvement for line 13:

-      return '$minutes mins $remainingSeconds secs';
+      return '$minutes ${minutes == 1 ? 'min' : 'mins'} $remainingSeconds ${remainingSeconds == 1 ? 'sec' : 'secs'}';

Also applies to: 24-24, 35-35

Comment on lines +40 to +46
// convert seconds to hh:mm:ss format
String secondsToHMS(int seconds) {
var hours = (seconds / 3600).floor();
var minutes = (seconds % 3600 / 60).floor();
var remainingSeconds = seconds % 60;
return '$hours:$minutes:$remainingSeconds';
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix formatting and add validation in secondsToHMS.

The function has several issues:

  1. Missing input validation
  2. Numbers aren't zero-padded (e.g., "9:5:0" instead of "09:05:00")
  3. Comment should be in dartdoc format

Apply this diff to fix the issues:

-// convert seconds to hh:mm:ss format
+/// Converts seconds to HH:MM:SS format.
+/// 
+/// Example: 3665 seconds becomes "01:01:05"
+/// 
+/// @param seconds The number of seconds to convert
+/// @throws ArgumentError if seconds is negative
 String secondsToHMS(int seconds) {
+  if (seconds < 0) {
+    throw ArgumentError('Seconds cannot be negative');
+  }
   var hours = (seconds / 3600).floor();
   var minutes = (seconds % 3600 / 60).floor();
   var remainingSeconds = seconds % 60;
-  return '$hours:$minutes:$remainingSeconds';
+  return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${remainingSeconds.toString().padLeft(2, '0')}';
 }
📝 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
// convert seconds to hh:mm:ss format
String secondsToHMS(int seconds) {
var hours = (seconds / 3600).floor();
var minutes = (seconds % 3600 / 60).floor();
var remainingSeconds = seconds % 60;
return '$hours:$minutes:$remainingSeconds';
}
/// Converts seconds to HH:MM:SS format.
///
/// Example: 3665 seconds becomes "01:01:05"
///
/// @param seconds The number of seconds to convert
/// @throws ArgumentError if seconds is negative
String secondsToHMS(int seconds) {
if (seconds < 0) {
throw ArgumentError('Seconds cannot be negative');
}
var hours = (seconds / 3600).floor();
var minutes = (seconds % 3600 / 60).floor();
var remainingSeconds = seconds % 60;
return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${remainingSeconds.toString().padLeft(2, '0')}';
}

Comment on lines +491 to +515
void appendMultipleSyncs(bool value) {
setIsSyncing(true);
multipleSyncs.add(value);
notifyListeners();
}

void removeMultipleSyncs() {
if (multipleSyncs.isNotEmpty) {
multipleSyncs.removeLast();
} else {
setIsSyncing(false);
setSyncCompleted(true);
}
notifyListeners();
}

void clearMultipleSyncs() {
multipleSyncs.clear();
notifyListeners();
}

void setIsFetchingMemories(bool value) {
isFetchingMemories = value;
notifyListeners();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix potential race condition in removeMultipleSyncs.

The sync state management methods look good overall, but there's a potential race condition in removeMultipleSyncs. When removing the last sync operation, it doesn't verify if there are other ongoing syncs before setting isSyncing to false.

   void removeMultipleSyncs() {
     if (multipleSyncs.isNotEmpty) {
       multipleSyncs.removeLast();
+      if (multipleSyncs.isEmpty) {
+        setIsSyncing(false);
+        setSyncCompleted(true);
+      }
-    } else {
-      setIsSyncing(false);
-      setSyncCompleted(true);
     }
     notifyListeners();
   }
📝 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
void appendMultipleSyncs(bool value) {
setIsSyncing(true);
multipleSyncs.add(value);
notifyListeners();
}
void removeMultipleSyncs() {
if (multipleSyncs.isNotEmpty) {
multipleSyncs.removeLast();
} else {
setIsSyncing(false);
setSyncCompleted(true);
}
notifyListeners();
}
void clearMultipleSyncs() {
multipleSyncs.clear();
notifyListeners();
}
void setIsFetchingMemories(bool value) {
isFetchingMemories = value;
notifyListeners();
}
void appendMultipleSyncs(bool value) {
setIsSyncing(true);
multipleSyncs.add(value);
notifyListeners();
}
void removeMultipleSyncs() {
if (multipleSyncs.isNotEmpty) {
multipleSyncs.removeLast();
if (multipleSyncs.isEmpty) {
setIsSyncing(false);
setSyncCompleted(true);
}
}
notifyListeners();
}
void clearMultipleSyncs() {
multipleSyncs.clear();
notifyListeners();
}
void setIsFetchingMemories(bool value) {
isFetchingMemories = value;
notifyListeners();
}

Comment on lines +32 to +42
double calculateProgress(DateTime? startedAt, int eta) {
if (startedAt == null) {
return 0.0;
}
if (eta == 0) {
return 0.01;
}
final elapsed = DateTime.now().difference(startedAt).inSeconds;
final progress = elapsed / eta;
return progress.clamp(0.0, 1.0);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider optimizing the progress calculation method.

The progress calculation logic is sound, but could be improved:

  1. Make the method static since it doesn't use instance state
  2. Use milliseconds for more precise progress calculation
-  double calculateProgress(DateTime? startedAt, int eta) {
+  static double calculateProgress(DateTime? startedAt, int eta) {
     if (startedAt == null) {
       return 0.0;
     }
     if (eta == 0) {
       return 0.01;
     }
-    final elapsed = DateTime.now().difference(startedAt).inSeconds;
+    final elapsed = DateTime.now().difference(startedAt).inMilliseconds / 1000.0;
     final progress = elapsed / eta;
     return progress.clamp(0.0, 1.0);
   }
📝 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
double calculateProgress(DateTime? startedAt, int eta) {
if (startedAt == null) {
return 0.0;
}
if (eta == 0) {
return 0.01;
}
final elapsed = DateTime.now().difference(startedAt).inSeconds;
final progress = elapsed / eta;
return progress.clamp(0.0, 1.0);
}
static double calculateProgress(DateTime? startedAt, int eta) {
if (startedAt == null) {
return 0.0;
}
if (eta == 0) {
return 0.01;
}
final elapsed = DateTime.now().difference(startedAt).inMilliseconds / 1000.0;
final progress = elapsed / eta;
return progress.clamp(0.0, 1.0);
}

Comment on lines +418 to +433
Map<DateTime, List<Wal>> _groupWalsByDate(List<Wal> wals) {
var groupedWals = <DateTime, List<Wal>>{};
wals.sort((a, b) => b.timerStart.compareTo(a.timerStart));
for (var wal in wals) {
var createdAt = DateTime.fromMillisecondsSinceEpoch(wal.timerStart * 1000).toLocal();
var date = DateTime(createdAt.year, createdAt.month, createdAt.day, createdAt.hour);
if (!groupedWals.containsKey(date)) {
groupedWals[date] = [];
}
groupedWals[date]?.add(wal);
}
for (final date in groupedWals.keys) {
groupedWals[date]?.sort((a, b) => b.timerStart.compareTo(a.timerStart));
}
return groupedWals;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider optimizing the grouping operation.

The current implementation performs sorting twice: once for the entire list and then for each group. This could be optimized by sorting only once.

 Map<DateTime, List<Wal>> _groupWalsByDate(List<Wal> wals) {
   var groupedWals = <DateTime, List<Wal>>{};
-  wals.sort((a, b) => b.timerStart.compareTo(a.timerStart));
   for (var wal in wals) {
     var createdAt = DateTime.fromMillisecondsSinceEpoch(wal.timerStart * 1000).toLocal();
     var date = DateTime(createdAt.year, createdAt.month, createdAt.day, createdAt.hour);
     if (!groupedWals.containsKey(date)) {
       groupedWals[date] = [];
     }
     groupedWals[date]?.add(wal);
   }
-  for (final date in groupedWals.keys) {
-    groupedWals[date]?.sort((a, b) => b.timerStart.compareTo(a.timerStart));
+  // Sort all groups at once using a more efficient approach
+  groupedWals.values.forEach((list) {
+    list.sort((a, b) => b.timerStart.compareTo(a.timerStart));
+  });
   }
   return groupedWals;
 }

Committable suggestion was skipped due to low confidence.

Comment on lines +482 to +484
walToSync.syncEtaSeconds = DateTime.now().difference(walToSync.syncStartedAt!).inSeconds *
(walToSync.storageTotalBytes - wal.storageOffset) ~/
(walToSync.storageOffset - storageOffsetStarts);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Potential division by zero in ETA calculation

In the calculation of syncEtaSeconds, if walToSync.storageOffset equals storageOffsetStarts, the denominator becomes zero, leading to a division by zero exception.

Please add a check to prevent division by zero:

  var offsetProgress = walToSync.storageOffset - storageOffsetStarts;
+ if (offsetProgress > 0) {
    walToSync.syncEtaSeconds = DateTime.now().difference(walToSync.syncStartedAt!).inSeconds *
        (walToSync.storageTotalBytes - walToSync.storageOffset) ~/
-       (walToSync.storageOffset - storageOffsetStarts);
+       offsetProgress;
+ } else {
+   walToSync.syncEtaSeconds = null; // or handle accordingly
+ }
📝 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
walToSync.syncEtaSeconds = DateTime.now().difference(walToSync.syncStartedAt!).inSeconds *
(walToSync.storageTotalBytes - wal.storageOffset) ~/
(walToSync.storageOffset - storageOffsetStarts);
var offsetProgress = walToSync.storageOffset - storageOffsetStarts;
if (offsetProgress > 0) {
walToSync.syncEtaSeconds = DateTime.now().difference(walToSync.syncStartedAt!).inSeconds *
(walToSync.storageTotalBytes - walToSync.storageOffset) ~/
offsetProgress;
} else {
walToSync.syncEtaSeconds = null; // or handle accordingly
}


@override
Future<SyncLocalFilesResponse?> syncWal({required Wal wal, IWalSyncProgressListener? progress}) async {
var walToSync = _wals.where((w) => w == wal).toList().first;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Possible exception when retrieving Wal from list

Using first on a list without checking if it is empty can throw a StateError if wal is not found in _wals. It's safer to check if the list is not empty before accessing first.

Consider modifying the code to handle the case where the wal is not found:

- var walToSync = _wals.where((w) => w == wal).toList().first;
+ var walToSyncList = _wals.where((w) => w == wal).toList();
+ if (walToSyncList.isNotEmpty) {
+   var walToSync = walToSyncList.first;
+   // proceed with synchronization
+ } else {
+   // handle the case when wal is not found
+   return null; // or take appropriate action
+ }

Committable suggestion was skipped due to low confidence.

Future<SyncLocalFilesResponse?> syncWal({required Wal wal, IWalSyncProgressListener? progress}) async {
await _flush();

var walToSync = _wals.where((w) => w == wal).toList().first;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Possible exception when retrieving Wal from list

Using first without checking if the list is empty can throw a StateError if the wal is not found in _wals.

Modify the code to handle the case where the wal is not found:

- var walToSync = _wals.where((w) => w == wal).toList().first;
+ var walToSyncList = _wals.where((w) => w == wal).toList();
+ if (walToSyncList.isNotEmpty) {
+   var walToSync = walToSyncList.first;
+   // proceed with synchronization
+ } else {
+   // handle the case when wal is not found
+   return null; // or take appropriate action
+ }

Committable suggestion was skipped due to low confidence.

Comment on lines +789 to +806
late File walFile;
if (wal.filePath == null) {
debugPrint("file path is not found. wal id ${wal.id}");
wal.status = WalStatus.corrupted;
}
try {
File file = File(wal.filePath!);
if (!file.existsSync()) {
debugPrint("file ${wal.filePath} is not exists");
wal.status = WalStatus.corrupted;
} else {
walFile = file;
wal.isSyncing = true;
}
} catch (e) {
wal.status = WalStatus.corrupted;
debugPrint(e.toString());
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Uninitialized variable walFile may cause runtime errors

The variable walFile is only assigned if the file exists. If the file does not exist, walFile remains uninitialized, which can lead to a runtime error when it's used later.

Consider initializing walFile appropriately or returning early when the file is not found:

  var resp = SyncLocalFilesResponse(newMemoryIds: [], updatedMemoryIds: []);

- late File walFile;
+ File? walFile;

  if (wal.filePath == null) {
    debugPrint("file path is not found. wal id ${wal.id}");
    wal.status = WalStatus.corrupted;
+   return resp; // or handle accordingly
  }
  try {
    File file = File(wal.filePath!);
    if (!file.existsSync()) {
      debugPrint("file ${wal.filePath} is not exists");
      wal.status = WalStatus.corrupted;
+     return resp; // or handle accordingly
    } else {
      walFile = file;
      wal.isSyncing = true;
    }
  } catch (e) {
    wal.status = WalStatus.corrupted;
    debugPrint(e.toString());
+   return resp; // or handle accordingly
  }

+ if (walFile == null) {
+   // Handle the case appropriately
+   return resp; // or take necessary action
+ }
📝 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
late File walFile;
if (wal.filePath == null) {
debugPrint("file path is not found. wal id ${wal.id}");
wal.status = WalStatus.corrupted;
}
try {
File file = File(wal.filePath!);
if (!file.existsSync()) {
debugPrint("file ${wal.filePath} is not exists");
wal.status = WalStatus.corrupted;
} else {
walFile = file;
wal.isSyncing = true;
}
} catch (e) {
wal.status = WalStatus.corrupted;
debugPrint(e.toString());
}
File? walFile;
if (wal.filePath == null) {
debugPrint("file path is not found. wal id ${wal.id}");
wal.status = WalStatus.corrupted;
return resp;
}
try {
File file = File(wal.filePath!);
if (!file.existsSync()) {
debugPrint("file ${wal.filePath} is not exists");
wal.status = WalStatus.corrupted;
return resp;
} else {
walFile = file;
wal.isSyncing = true;
}
} catch (e) {
wal.status = WalStatus.corrupted;
debugPrint(e.toString());
return resp;
}
if (walFile == null) {
return resp;
}

@beastoin
Copy link
Collaborator

lgtm 🥳

@beastoin beastoin merged commit d8b4d0e into main Oct 30, 2024
1 check passed
@beastoin beastoin deleted the sync-fixes branch October 30, 2024 23:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

2 participants