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

Album support #106

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open

Album support #106

wants to merge 26 commits into from

Conversation

cp-pratik-k
Copy link
Collaborator

@cp-pratik-k cp-pratik-k commented Jan 3, 2025

Summary by CodeRabbit

Release Notes

  • New Features

    • Added album management functionality.
    • Introduced media selection from device, Google Drive, and Dropbox.
    • Enhanced navigation with a new tab-based main screen.
    • Implemented a customizable text input field and radio button selection interface.
    • Added new screens for adding and managing albums and media lists.
    • Introduced a new OnVisibleCallback widget for executing callbacks when a widget becomes visible.
    • Enhanced media thumbnail display with interactive features.
    • Introduced a new MediaSelectionScreen for selecting media from various sources.
    • Added functionality to create, update, and delete albums in Dropbox and Google Drive.
    • Enhanced user interface with new selection menu components.
  • Improvements

    • Refined app routing and navigation structure.
    • Updated UI components for better user experience.
    • Enhanced theme customization options for app bar and background colors.
    • Improved state management using Riverpod.
    • Improved error messages and prompts for media access issues.
    • Enhanced layout and appearance of navigation components.
    • Streamlined action handling in media selection.
  • Bug Fixes

    • Enhanced error handling across various services.
    • Fixed minor UI inconsistencies.
    • Resolved issues with media loading and selection.
  • Performance

    • Optimized media loading and album management.
    • Improved unique ID generation for resources.
    • Streamlined the loading process for media previews.

Copy link

coderabbitai bot commented Jan 3, 2025

Walkthrough

This pull request introduces a comprehensive update to the Cloud Gallery application, focusing on album management, media selection, and navigation improvements. The changes span multiple components, including UI screens, state management, routing, and service integrations. Key additions include new screens for albums and media selection, enhanced routing structure, and improved state management using Riverpod and Freezed packages.

Changes

File/Group Change Summary
.idea/libraries/Flutter_Plugins.xml Updated plugin entries for firebase, permission_handler, path_provider, and others, removing older versions.
app/assets/locales/app_en.arb Added localization entries for album management and media selection.
app/lib/components/app_media_thumbnail.dart Introduced AppMediaThumbnail class for displaying media thumbnails with interactive features.
app/lib/components/app_page.dart Updated resizeToAvoidBottomInset to non-nullable and modified AppBar and CupertinoNavigationBar properties.
app/lib/components/app_sheet.dart Added useRootNavigator parameter to showAppSheet method.
app/lib/ui/app.dart Modified CloudGalleryApp to initialize GoRouter with navigatorKey.
app/lib/ui/flow/albums/* New screens and state management for album creation, listing, and media management.
app/lib/ui/flow/media_selection/* New screen and state notifier for media selection from various sources.
app/lib/ui/navigation/app_route.dart Restructured routing with new routes for albums and media selection.
data/lib/models/album/* New Album model with Freezed implementation for immutable data.
data/lib/services/* Added album management methods for Dropbox, Google Drive, and local media services.
style/lib/buttons/* New radio selection and action buttons with enhanced functionality.

Possibly Related PRs

  • Improve-image-loading #55: Improve-image-loading - This PR involves changes to the .idea/libraries/Dart_Packages.xml file, which may relate to the updates in plugin references in the main PR's .idea/libraries/Flutter_Plugins.xml.
  • Improve preview #82: Improve preview - This PR modifies the AppMediaImage class in thumbnail_builder.dart, which is relevant to the changes in the main PR that also involve updates to image handling in the .idea/libraries/Flutter_Plugins.xml.

Poem

🐰 Hop, hop, hooray! Albums are here to stay!
With media from near and far,
Dropbox, Drive, and local star
Our gallery's got a brand new way! 🌈📸


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 generate docstrings to generate docstrings for this PR. (Beta)
  • @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

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

🔭 Outside diff range comments (1)
data/lib/services/google_drive_service.dart (1)

Line range hint 95-132: Filter expression correctness.
In getAllMedias and getPaginatedMedias, confirm that 'name!="album.json"' is properly escaped in the queries. A missing quote might lead to unexpected results.

- "'$folder' in parents and trashed=false and name!='${ProviderConstants.albumFileName}"
+ "'$folder' in parents and trashed=false and name!='${ProviderConstants.albumFileName}'"
🧹 Nitpick comments (56)
data/lib/domain/config.dart (1)

10-16: Consider consolidating databases if feasible.

The introduction of two separate databases (cloud-gallery.db and cloud-gallery-album.db) may increase integration overhead and complexity. Evaluate whether merging these into a single database with multiple tables would simplify potential migrations and data integrity.

Do you want me to open a new GitHub issue to address this and explore a unified database approach?

data/lib/apis/google_drive/google_drive_endpoint.dart (1)

219-242: Consider aligning default fields with typical file retrieval
GoogleDriveGetEndpoint defaults to returning nextPageToken and files(...) fields, which are more relevant for listing rather than retrieving a single file’s metadata. If this endpoint is fetching details of a single file, consider tailoring the default fields or clarifying the rationale in code comments.

app/lib/ui/flow/albums/albums_view_notifier.dart (1)

85-112: Improve error handling for partial failures from one or more media services.
Currently, if any of the Future.wait calls fails, the entire call stack is aborted. Consider wrapping each call in a try/catch and combining partial results so that a failure in one source (e.g., Google Drive) doesn't discard successful album data from the others.

app/lib/ui/navigation/app_route.dart (2)

43-83: Consider dynamic path segments for more granular navigation.
The TypedStatefulShellRoute is a powerful approach for structuring multi-branch navigation. However, consider whether certain routes (e.g., showing a specific album) might benefit from path parameters (albums/:albumId) rather than always relying on nested typed routes.


185-194: Good separation of media selection route.
The new MediaSelectionRoute is clearly isolated, which will simplify navigation logic further. If extended, consider whether to encode source as a URL parameter for deep-linking.

app/lib/ui/flow/albums/add/add_album_state_notifier.dart (2)

81-153: Rename method for clarity and handle potential partial failures.
Though createAlbum is correct for new albums, it also updates existing ones. Consider a more neutral name, like saveAlbum. Also, if multiple external calls are introduced in future expansions, you may want to catch partial failures separately.


155-166: Validate album names for potential special characters.
Current name validation checks only if the field is empty. Consider more robust checks (length, allowed characters, etc.) to prevent storage or naming issues.

app/lib/ui/flow/media_selection/media_selection_state_notifier.dart (1)

183-206: Add a robust null check for ID references.
When toggling selections for Google Drive or Dropbox, you rely on driveMediaRefId! or dropboxMediaRefId!. A defensive check or assertion for unexpected null references may be safer.

app/lib/ui/flow/media_selection/media_selection_screen.dart (1)

88-205: Consider breaking down _body into smaller widgets.
Multiple nested conditions for loading, errors, and placeholders may benefit from extraction into distinct widgets or methods to keep the build logic simpler. The usage of FadeInSwitcher is a nice touch for transitions.

app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart (6)

15-24: Consider naming the provider for debugging clarity.
Using .family with autoDispose is fine, but adding a providerName parameter (if provided by Riverpod) can make debugging easier in large codebases.


32-42: Evaluate the constructor’s immediate call to loadMedia().
Invoking loadMedia() within the constructor can trigger network requests before the caller fully configures the notifier. Consider deferring this call until after certain UI or logical conditions are met.


46-78: Set loading state before network fetch.
The loadAlbum() method sets actionError to null but never indicates that the album is loading until after the retrieval. Consider toggling a loading flag to true to communicate ongoing work to the UI.

46   Future<void> loadAlbum() async {
47     if (state.loading) return;

+    state = state.copyWith(loading: true, actionError: null);
49     List<Album> albums = [];

85-113: Consider a loading flag for deleteAlbum.
Deletion is a significant operation. Setting and clearing a loading state can help the UI reflect the progress of album deletion.


162-235: Ensure concurrency safety in loadMedia.
This method guards against concurrent loads with if (state.loading) return;, but consider a queue or cancellation strategy if multiple load requests may be triggered rapidly (e.g., via UI interactions).


238-248: Clarify or unify error fields.
Two separate fields, error and actionError, may confuse consumers. Consider using more descriptive property names or merging them if roles overlap.

app/lib/ui/navigation/app_route.g.dart (2)

39-86: Use a consistent naming scheme for shell branches.
The newly introduced $mainShellRoute organizes branches for home, albums, transfer, and accounts. Verify that each branch’s path does not conflict with older paths or cause 404s.


258-283: Sanity-check the new /select path.
$mediaSelectionRoute might need to handle multiple optional parameters or subpaths in the future. Plan for expandability to avoid route naming collisions.

data/lib/services/google_drive_service.dart (4)

30-64: Simplify conditional folder creation logic.
In getBackUpFolderId, you fetch or create the folder in a nested if block. Consider extracting the creation logic into a helper for clarity.


173-179: Success check for deleteMedia might be incomplete.
Currently, only status codes 200 or 204 are considered success. If the Drive API returns additional success codes or a no-content scenario (e.g., 202), consider including them.


314-408: Avoid re-downloading album file repeatedly.
createAlbum downloads the file on each addition. For performance, consider caching or partial updates.


492-564: Validate concurrency in removeAlbum.
Like the createAlbum logic, removing an album re-downloads the entire file. If multiple updates are triggered concurrently, consider the possibility of race conditions.

style/lib/callback/on_visible_callback.dart (2)

3-16: Widget naming clarity.
The term OnVisibleCallback is somewhat ambiguous. Consider naming it OnWidgetInitCallback or similar to reflect that the callback triggers on widget creation, not actual visibility on the screen.


17-28: Consider a post-frame callback for UI readiness.
Calling widget.onVisible() in initState() is immediate, which may skip waiting for the first frame. If the callback depends on rendering, consider using a WidgetsBinding.instance.addPostFrameCallback.

data/lib/models/album/album.dart (1)

1-2: Omit unnecessary ignore rule.
// ignore_for_file: non_constant_identifier_names might not be needed if your fields follow normal naming.

style/lib/buttons/action_button.dart (1)

7-7: Validate null callback handling for onPressed.

By making onPressed nullable, calls to this field must be guarded. Optionally disable the button or provide an alternative visual state when onPressed == null, if appropriate.

data/lib/handlers/unique_id_generator.dart (2)

5-7: Check provider scope usage.

A single global provider is fine if you're comfortable using one global generator instance. If you need unique behavior per scope, consider creating a scoped provider.


39-48: Consider uppercase formatting for conventional UUID representation.

While this method is correct, uppercase hex output is more common in certain contexts. If that matters, you can modify .toRadixString(16) with .toUpperCase().

style/lib/buttons/radio_selection_button.dart (1)

6-18: Define a more descriptive callback name.
“onTab” might be confused with “onTap”. Consider using a clearer name like “onPressed” or “onSelection” to better align with Flutter conventions.

- final void Function() onTab;
+ final void Function() onPressed;
app/lib/components/app_media_thumbnail.dart (1)

25-67: Animate selection transitions for better UX.
The AnimatedOpacity is a nice touch. Consider also adding a subtle border or scale transform for more pronounced visual distinction when selected is true, if user tests suggest it's helpful.

- opacity: selected ? 0.6 : 1,
+ opacity: selected ? 0.7 : 1,
+ transform: selected
+     ? Matrix4.identity()..scale(0.95)
+     : Matrix4.identity(),
app/lib/ui/flow/albums/add/add_album_screen.dart (2)

16-23: Consider adding an explicit “edit mode” concept.
The constructor indicates optional edit-based initialization. For clarity, add a boolean isEditing property or explicitly name it “editAlbum” vs. “albumToEdit,” as it can streamline usage in the UI.

- final Album? editAlbum;
+ final Album? albumToEdit;
+ bool get isEditing => albumToEdit != null;

25-35: Use a single “init” method for clarity.
Currently, _provider and _notifier initialization logic is in initState. If you need more logic later, consolidating them into a private _init() method might improve readability.

app/lib/ui/flow/main/main_screen.dart (3)

9-19: Consider adding documentation for MainScreen.
It can be helpful for future maintainers to understand this widget’s responsibilities and how to use it properly.


47-50: Avoid repeated platform checks.
You can unify the platform check logic by extracting it into a helper method or by using a more standardized approach (e.g., checking using a single condition for kIsWeb and a single condition for mobile/desktop). This can enhance code readability.

-(!kIsWeb && Platform.isIOS)
+isCupertinoPlatform

129-147: Use a default switch case for maintainability.
If you ever add more items to tabs and forget to update the switch, it could cause an unexpected behavior. Consider adding a default: or an assert(false) to guard against future expansions.

 switch (index) {
   case 0:
     HomeRoute().go(context);
     break;
   ...
   case 3:
     AccountRoute().go(context);
     break;
+  default:
+    assert(false, 'Unexpected navigation index: $index');
 }
app/lib/components/app_page.dart (1)

53-53: Clarify enableBackgroundFilterBlur usage.
Disabling the blur can affect the iOS-style translucency effect. Documenting or referencing design considerations helps future devs understand why it’s disabled.

data/lib/services/local_media_service.dart (3)

108-124: Consider using a single open connection for your album database.
Repeatedly opening and closing the DB can lead to overhead and potential concurrency issues. Using a shared connection or a database manager pattern can improve performance and stability.


126-139: Consider adding basic error handling or conflict checks.
For instance, if you attempt to create an album with a duplicate ID, the insert will fail. Logging or capturing the error can help debug album creation issues.


141-155: Wrap updates in a transaction or add concurrency checks.
While not strictly required, for more robust concurrency you can wrap updates in a transaction. Additionally, check that the album still exists when updating.

app/lib/ui/flow/albums/media_list/album_media_list_screen.dart (2)

22-30: Add documentation to explain how AlbumMediaListScreen uses Riverpod.
Explicitly stating how the screen is re-built and how the albumMediaListStateNotifierProvider is created will help future devs quickly understand the data flow.


57-105: Consider grouping album actions with separators.
In a long action sheet, grouping related items (e.g., album-specific vs. media-specific) improves clarity for users. This is optional but can enhance user experience.

style/lib/text/app_text_field.dart (3)

16-16: Use a more descriptive default for borderType.
While outline is a popular default, consider explicitly documenting why outline is used most frequently or consider requiring a border type to encourage explicit usage.


58-59: Consider clarifying isDense vs isCollapsed.
Having both isDense and isCollapsed might cause confusion among consumers of this widget. Add documentation or rename if their usage patterns differ significantly.


136-158: Consider customizing the error text style or color.
If the text field is invalid, the error text is displayed. Verify that the design system meets your aesthetic or accessibility needs (e.g., color contrast with the background).

data/lib/apis/dropbox/dropbox_content_endpoints.dart (1)

Line range hint 28-44: Document why appPropertyTemplateId is optional.
The new optional parameter can lead to different request bodies. Clearly document how and when it's expected to be null for consistent usage across the codebase.

app/assets/locales/app_en.arb (1)

134-140: Confirm cloud sign-in error text.
The message instructs the user to check their sign in with the cloud. Consider clarifying which steps the user should take if they're not signed in.

app/lib/ui/flow/albums/albums_screen.dart (2)

59-63: Refresh album list only if necessary.
After successfully creating an album, the code reloads the entire list. This is fine, but if performance is paramount, consider a more incremental approach (e.g., appending the new album).

🧰 Tools
🪛 GitHub Actions: Analyze

[warning] File was automatically formatted, indicating it did not meet the required formatting standards


203-203: Optimize image fetch size.
Loading a 300x300 sized image may be large for certain devices. Consider dynamic sizing or passing a scale factor from the parent's constraints for improved performance.

🧰 Tools
🪛 GitHub Actions: Analyze

[warning] File was automatically formatted, indicating it did not meet the required formatting standards

data/lib/models/album/album.freezed.dart (2)

21-37: Clarify purpose of error-throwing getters
Each property (name, id, medias, source, created_at) throws _privateConstructorUsedError when accessed directly. This behavior is typical of Freezed’s boilerplate, but it might be confusing to newcomers. Consider adding a brief comment in the docstrings clarifying that these getters are intended solely for generated code usage.


157-188: Ensure custom logic in constructor if needed
The _$AlbumImpl constructor sets _medias for the unmodifiable list view. If you require additional validations or transformations (e.g., trimming album names, filtering out invalid media paths), consider adding them in the constructor or a separate factory.

app/lib/ui/flow/albums/albums_view_notifier.freezed.dart (2)

17-31: Avoid confusion around error fields
error and actionError both store Object?. This is valid, but consider more specific types (e.g., custom error structures) if you need to parse or handle them differently.


60-107: Evaluate concurrency impact
The copyWith usage in a UI-based state can be frequent. Ensure that no heavy I/O or side effects occur within property getters or copyWith calls. If so, refactor side effects into a separate method.

app/lib/ui/flow/albums/add/add_album_state_notifier.freezed.dart (2)

17-34: Revisit naming for clarity
Naming the state mixin $AddAlbumsState can be slightly confusing. If the state is specifically for "AddAlbum" flows, ensuring the name matches the domain might help new contributors quickly grasp its scope.


207-284: Check concurrency & disposal
The _AddAlbumsStateImpl holds a TextEditingController. If multiple screen rebuilds or concurrency flows happen, ensure that the controller is properly disposed. Typically, StateNotifier doesn’t handle lifecycle. Evaluate if this is managed elsewhere.

data/lib/services/dropbox_services.dart (3)

228-242: Check performance for frequent media lookups
getMedia calls DropboxGetFileMetadata(id: id). If you frequently call this for large numbers of media, performance might degrade. Consider caching or a bulk metadata fetch if feasible.


381-408: Handle malformed JSON in Albums.json
When reading Albums.json, the code handles a valid list but might throw if the JSON is incorrectly structured (not just an empty list). Consider adding user-friendly error handling for parse errors.


436-459: Improve error feedback
deleteAlbum returns silently on status 200, but if the album is missing or already removed, there’s no explicit user-facing feedback. Consider returning a boolean or a more descriptive result (e.g., “Album not found” or “Successfully deleted”).

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2b12c34 and 3b2e47f.

📒 Files selected for processing (42)
  • .idea/libraries/Flutter_Plugins.xml (1 hunks)
  • app/assets/locales/app_en.arb (2 hunks)
  • app/lib/components/app_media_thumbnail.dart (1 hunks)
  • app/lib/components/app_page.dart (6 hunks)
  • app/lib/components/app_sheet.dart (1 hunks)
  • app/lib/ui/app.dart (1 hunks)
  • app/lib/ui/flow/albums/add/add_album_screen.dart (1 hunks)
  • app/lib/ui/flow/albums/add/add_album_state_notifier.dart (1 hunks)
  • app/lib/ui/flow/albums/add/add_album_state_notifier.freezed.dart (1 hunks)
  • app/lib/ui/flow/albums/albums_screen.dart (1 hunks)
  • app/lib/ui/flow/albums/albums_view_notifier.dart (1 hunks)
  • app/lib/ui/flow/albums/albums_view_notifier.freezed.dart (1 hunks)
  • app/lib/ui/flow/albums/media_list/album_media_list_screen.dart (1 hunks)
  • app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart (1 hunks)
  • app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.freezed.dart (1 hunks)
  • app/lib/ui/flow/home/home_screen.dart (0 hunks)
  • app/lib/ui/flow/main/main_screen.dart (1 hunks)
  • app/lib/ui/flow/media_preview/components/video_player_components/video_duration_slider.dart (1 hunks)
  • app/lib/ui/flow/media_selection/media_selection_screen.dart (1 hunks)
  • app/lib/ui/flow/media_selection/media_selection_state_notifier.dart (1 hunks)
  • app/lib/ui/flow/media_selection/media_selection_state_notifier.freezed.dart (1 hunks)
  • app/lib/ui/navigation/app_route.dart (4 hunks)
  • app/lib/ui/navigation/app_route.g.dart (6 hunks)
  • data/.flutter-plugins-dependencies (1 hunks)
  • data/lib/apis/dropbox/dropbox_content_endpoints.dart (7 hunks)
  • data/lib/apis/google_drive/google_drive_endpoint.dart (2 hunks)
  • data/lib/domain/config.dart (1 hunks)
  • data/lib/handlers/unique_id_generator.dart (1 hunks)
  • data/lib/models/album/album.dart (1 hunks)
  • data/lib/models/album/album.freezed.dart (1 hunks)
  • data/lib/models/album/album.g.dart (1 hunks)
  • data/lib/repositories/media_process_repository.dart (0 hunks)
  • data/lib/services/dropbox_services.dart (4 hunks)
  • data/lib/services/google_drive_service.dart (7 hunks)
  • data/lib/services/local_media_service.dart (4 hunks)
  • style/lib/buttons/action_button.dart (2 hunks)
  • style/lib/buttons/radio_selection_button.dart (1 hunks)
  • style/lib/callback/on_visible_callback.dart (1 hunks)
  • style/lib/text/app_text_field.dart (1 hunks)
  • style/lib/theme/app_theme_builder.dart (2 hunks)
  • style/lib/theme/colors.dart (0 hunks)
  • style/lib/theme/theme.dart (0 hunks)
💤 Files with no reviewable changes (4)
  • app/lib/ui/flow/home/home_screen.dart
  • data/lib/repositories/media_process_repository.dart
  • style/lib/theme/colors.dart
  • style/lib/theme/theme.dart
✅ Files skipped from review due to trivial changes (5)
  • app/lib/ui/flow/media_preview/components/video_player_components/video_duration_slider.dart
  • data/.flutter-plugins-dependencies
  • data/lib/models/album/album.g.dart
  • app/lib/ui/flow/media_selection/media_selection_state_notifier.freezed.dart
  • app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.freezed.dart
🧰 Additional context used
🪛 GitHub Actions: Analyze
app/lib/ui/flow/albums/albums_screen.dart

[warning] File was automatically formatted, indicating it did not meet the required formatting standards

🔇 Additional comments (57)
data/lib/domain/config.dart (1)

2-2: Consistency with naming conventions.

Defining albumFileName in ProviderConstants appears consistent with existing naming patterns across the class. Looks good.

data/lib/apis/google_drive/google_drive_endpoint.dart (2)

137-137: Good flexibility with optional save location
Allowing saveLocation to be null is a beneficial change. Just ensure any downstream code checks if saveLocation is non-null before attempting to write.


145-145: Validate fallback logic for missing save location
As saveLocation is optional, confirm that no fallback logic is needed (e.g., a temporary directory) if saveLocation is left unspecified.

app/lib/ui/flow/albums/albums_view_notifier.dart (3)

18-35: Good usage of autoDispose and listeners on provider changes.
This approach ensures that the notifier updates or filters albums whenever there is an account change, preventing stale data.


55-70: Consider handling concurrent calls to onGoogleDriveAccountChange.
If the user rapidly signs out and in again, you could introduce a mechanism that cancels or ignores redundant calls to avoid potential race conditions when loading albums.


114-143: Great logic for deleting albums across multiple sources.
Clear, source-specific removal steps are implemented, followed by state updates with appropriate error logging and feedback. This looks maintainable.

app/lib/ui/navigation/app_route.dart (2)

21-30: Path constants look consistent.
The introduction of albums, add, mediaList, and mediaSelection routes is well-organized. Keep an eye on future route naming consistency when new routes are added.


108-119: Nice approach for optional editing of an existing album.
Using $extra to pass the optional Album? into the AddAlbumRoute is a neat way to handle both creation and editing within the same screen.

app/lib/ui/flow/albums/add/add_album_state_notifier.dart (2)

20-41: Using a family provider to handle optional album editing is effective.
The logic for reading dependencies and constructing a notifier with an optional initial album is clean. This pattern ensures reusability.


43-68: Good handling of TextEditingController for album name.
The controller is properly initialized based on whether editing an existing album. Verified that it’s disposed of in dispose().

app/lib/ui/flow/media_selection/media_selection_state_notifier.dart (2)

19-32: Straightforward provider structure.
Accepting source and relevant dependencies decouples your logic from hardcoded assumptions, ensuring future-proof scalability if additional media sources are introduced.


59-114: Logical single-source loading approach.
Because each media source is handled distinctly, there’s little risk of confusion with concurrency. The code also gracefully handles restricted access or missing folder references.

app/lib/ui/flow/media_selection/media_selection_screen.dart (3)

24-31: Clear and concise constructor signature.
Requiring the AppMediaSource ensures the screen’s functionality is well-defined and reduces the risk of unintended usage.


33-49: Ref-listening pattern is well-implemented.
The _observeError function elegantly listens to changes in actionError and displays a snackbar, improving user visibility into runtime failures.


58-86: Pragmatic approach to returning selected media.
By popping state.selectedMedias when the user taps the check icon, the code preserves a clear, minimal flow for returning results upstream.

app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart (2)

80-83: Avoid multiple loads if usage is repeated.
reloadAlbum() calls loadAlbum() and then loadMedia() in quick succession. Verify that repeated or concurrent calls do not lead to unintended concurrency issues.


115-160: Investigate partial failure handling when adding medias.
If the album update fails for some medias, the current logic surfaces a single error. Confirm that partial updates either succeed or fail consistently to avoid inconsistent states.

app/lib/ui/navigation/app_route.g.dart (6)

11-14: Confirm route removal impact.
With $homeRoute, $accountRoute, and $transferRoute removed from top-level but placed in branches, double-check any external references.


17-26: Check updated on-board path.
The path changed to '/on-board'. Ensure other references to the original path are updated or removed.


111-115: Avoid potential collisions on '/albums'.
Ensure that your references to the albums path do not overlap with existing or future requirements for advanced searching, e.g., /albums/search.


128-147: Validate extra parameter usage for AddAlbumRoute.
Passing an Album? as $extra is helpful but ensure that any null scenarios and backward-compatibility are handled gracefully in AddAlbumScreen.


149-169: Confirm state handling for AlbumMediaListRoute.
An Album object is passed through $extra; confirm that it is used consistently in the target screen/notifier.


188-203: Ensure the new AccountRoute usage is stable.
The route path '/accounts' can conflict with older references. Confirm it’s fully integrated.

data/lib/services/google_drive_service.dart (3)

157-163: Return type is strong.
AppMedia from a Google Drive file is straightforward. Confirm that all required fields are handled if the file is unexpectedly missing certain metadata (mimeType, parents, etc.).


275-312: Check concurrency for simultaneous album listing.
getAlbums reads from the same albumFileName. Confirm that parallel calls from multiple devices or concurrency scenarios won’t overwrite or stale the file.


410-490: Blocking error for missing album.
updateAlbum throws an error if the album isn’t found. That’s correct logically. Ensure the UI notifies the user and recovers gracefully in case they try updating a non-existent album.

data/lib/models/album/album.dart (1)

11-22: Review schema changes for backward compatibility.
The Album model now includes created_at as required. Verify that older data or existing references can handle or provide this new field.

app/lib/components/app_sheet.dart (1)

9-9: Consider potential side effects of root navigation.

Using useRootNavigator: true may bypass nested navigators, potentially interfering with specialized navigation flows or localized context scopes. Ensure that global or parent-level context is indeed desired here.

data/lib/handlers/unique_id_generator.dart (2)

9-20: Assess integer overflow risks.

While 4 sets of 16-bit random values result in a 64-bit range, ensure your runtime environment consistently supports 64-bit integers and confirm no overflow concerns exist for extremely large usage scenarios.


22-37: Good compliance with UUIDv4 format.

Your logic for setting version and variant bits follows RFC 4122. This is generally robust and cryptographically sound.

style/lib/theme/app_theme_builder.dart (2)

32-32: Align the center title design across platforms.

Explicitly centering the title can improve visual consistency. Confirm that this aligns with the app’s overall design strategy on both iOS and Android.


47-47: Use of surface is consistent with Material guidelines.

Switching to surface ensures cohesive theming across components. This is generally recommended by modern Material design standards.

style/lib/buttons/radio_selection_button.dart (1)

20-61: Ensure consistent tap regions and accessible touch targets.
Wrapping the entire container in OnTapScale is good for feedback, but verify that it doesn't unintentionally capture taps outside the bounding box in certain scenarios. Confirm consistent spacing so that the widget has a sufficiently large touch target.

✅ Verification successful

Let me gather more information about the widget's usage context and spacing.


Touch targets and spacing are properly implemented
The RadioSelectionButton widget has appropriate spacing and touch targets:

  • Built-in vertical spacing of 8dp through Container margins
  • Horizontal padding of 8dp and sufficient touch area through the full-width container
  • Parent widget adds additional vertical spacing between buttons through Column's natural spacing
  • The widget is used within a Column with proper SizedBox spacing from other UI elements

The implementation follows accessibility best practices with:

  • Full-width clickable area through Container + OnTapScale
  • Clear visual feedback through border color changes
  • Proper touch target size through padding and margins
  • Consistent spacing between radio buttons in the Column layout
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify usage of RadioSelectionButton to ensure minimal spacing/padding
# between other UI elements
rg -A 5 "RadioSelectionButton"

Length of output: 2698


Script:

#!/bin/bash
# Check the parent widget structure in add_album_screen.dart to verify spacing
rg -B 5 -A 10 "RadioSelectionButton<AppMediaSource>" app/lib/ui/flow/albums/add/add_album_screen.dart

Length of output: 1304

app/lib/ui/app.dart (1)

42-42: Confirm that rootNavigatorKey is initialized and used consistently.
This is an excellent step to manage the top-level navigator. However, ensure rootNavigatorKey is defined in the correct scope (e.g., a global key or a parent widget) and that it refers to the correct navigation context to avoid unexpected navigation behavior.

✅ Verification successful

rootNavigatorKey is properly initialized and used consistently

The rootNavigatorKey is correctly defined as a global navigator key in app/lib/ui/navigation/app_route.dart and is consistently used across the navigation system:

  • Initialized as a global key: final rootNavigatorKey = GlobalKey<NavigatorState>();
  • Used as the main navigator key in app/lib/ui/app.dart
  • Referenced consistently as the parent navigator key in route configurations within app/lib/ui/navigation/app_route.dart
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Searching for the declaration or usage of "rootNavigatorKey"
rg "rootNavigatorKey"

Length of output: 406

app/lib/components/app_media_thumbnail.dart (2)

9-23: Fields usage looks clear.
It’s good how the constructor clearly highlights optional callbacks and a default selected flag. Keep an eye on cases where large thumbnails might benefit from lazy loading to optimize performance.


69-115: Clarify alpha usage in gradient colors.
Using Colors.black.withValues(alpha: 0.4) is fine, but ensure it translates as intended across theme variations. Also confirm that constant .4 or .40 meets the design guidelines for text contrast.

app/lib/ui/flow/albums/add/add_album_screen.dart (1)

83-136: Radio button references are clean and straightforward.
The usage of RadioSelectionButton for different storage sources is well structured. Just ensure that using multiple consecutive radio groups doesn't confuse the user. A heading or separator might be useful.

app/lib/components/app_page.dart (2)

15-15: Good move making resizeToAvoidBottomInset non-nullable.
This ensures a predictable, consistent behavior across the codebase and avoids unintentional runtime null issues.


93-93: Consistent center title usage.
Enforcing a centered title for both Material and Cupertino ensures uniform design across platforms. This is a good consistency improvement.

Also applies to: 155-156

data/lib/services/local_media_service.dart (3)

77-81: Check for performance impacts when retrieving AssetEntity by ID.
Fetching a single asset repeatedly can be less performant if used in a tight loop. Consider caching or batching operations where possible.


157-165: Confirm that attached media is properly cleaned up when an album is deleted.
If the album references local media that’s unique to it, you might need a separate deletion flow for those media to prevent orphaned references or leftover files.


167-187: Validate album data upon retrieval.
Although the mapping logic is straightforward, consider gracefully handling invalid or corrupted rows if they ever occur (e.g., missing columns or unparseable data).

app/lib/ui/flow/albums/media_list/album_media_list_screen.dart (3)

39-42: Avoid side effects in initState.
Reading a Riverpod provider in initState is allowed, but if you anticipate multiple changes or deeper logic, consider adding error handling or performing critical tasks in the didChangeDependencies method.


46-52: Praise for consistent usage of AppPage & FadeInSwitcher.
This consistent approach shows a well-structured UI pattern.


118-153: Adaptive grid logic is a nice touch, verify performance.
Adapting the grid by screen width is user-friendly. Ensure that large numbers of media won’t cause performance issues. Consider using pagination or lazy loading if the user can have thousands of media items.

style/lib/text/app_text_field.dart (2)

47-48: Confirm the default value of expands and enabled.
Having both expands default to false and enabled default to true is sensible. However, ensure all use cases are tested so that developers are aware of these defaults.


100-112: Ensure consistent context menu experience across all platforms.
The logic for deciding whether to show the system context menu vs. the flutter-rendered menu is good. Verify it works as intended on all supported platforms (iOS, Android, web, desktop, etc.).

data/lib/apis/dropbox/dropbox_content_endpoints.dart (2)

166-172: Check null usage of storagePath.
While storagePath is now optional, ensure there's a fallback if a local directory is needed to store downloaded files.


61-65: Check compatibility with Dropbox API.
Including property groups conditionally is helpful, but confirm that excluding them when appPropertyTemplateId is null doesn't cause unexpected behaviors in certain requests.

✅ Verification successful

Property groups handling is consistent with Dropbox API patterns

The code's handling of property groups is safe and follows consistent patterns:

  • In list operations, include_property_groups is conditionally included with proper .tag and filter_some structure when appPropertyTemplateId is provided
  • In upload/update operations, property_groups is only included when both appPropertyTemplateId and localRefId are present
  • The model parsing in AppMedia safely handles cases where property groups may or may not be present

The conditional inclusion/exclusion of property groups is a valid approach and won't cause issues when the values are null.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Searching for references to Dropbox docs that mention property groups
rg "property_groups" 

Length of output: 487


Script:

#!/bin/bash
# Let's check the context around property_groups usage
rg -A 5 -B 5 "property_groups"

Length of output: 3836


Script:

#!/bin/bash
# Let's check the Dropbox API documentation or comments in the codebase
rg -A 2 -B 2 "include_property_groups|property_groups" --type md --type dart --type txt

Length of output: 1822

app/assets/locales/app_en.arb (1)

120-124: Leverage consistent voice and style.
The newly added strings for empty albums match the casual, user-friendly style of existing texts. Keep it consistent with the rest of the file for user experience.

app/lib/ui/flow/albums/albums_screen.dart (2)

85-93: Verify placeholder text alignment.
The placeholder screen provides a friendly message. Make sure text alignment, especially for multiline messages, doesn't overflow or wrap awkwardly on smaller devices.

🧰 Tools
🪛 GitHub Actions: Analyze

[warning] File was automatically formatted, indicating it did not meet the required formatting standards


180-196: Display default folder icon vs. first media item.
The approach is consistent but ensure that if the album's first media is not an image, the UI handles the scenario gracefully (e.g., videos or other media types).

🧰 Tools
🪛 GitHub Actions: Analyze

[warning] File was automatically formatted, indicating it did not meet the required formatting standards

data/lib/models/album/album.freezed.dart (2)

52-95: Confirm immutability constraints
The autogenerated copyWith methods and List<String> usage (wrapped in EqualUnmodifiableListView) effectively enforce immutability. This is beneficial for Redux/Riverpod flows. Just ensure your team is aware that direct mutation of List<String> is impossible without reassigning a new list. If that is the desired behavior, no action needed.


189-226: Compare critical fields in equality checks
The generated == and hashCode confirm that name, id, medias, source, and created_at are used for equality. This matches your domain model well. If you add more fields in the future, remember to regenerate to keep comparisons consistent.

app/lib/ui/flow/albums/albums_view_notifier.freezed.dart (1)

172-243: Check default album list
The _AlbumsStateImpl assigns [] to albums by default. This is a safe default. Ensure that your system gracefully handles empty album arrays in the UI, e.g., showing an empty state message.

app/lib/ui/flow/albums/add/add_album_state_notifier.freezed.dart (1)

65-126: Validate text controllers
albumNameController references a TextEditingController, which is not trivially serializable in a typical Redux or Riverpod store. Align documentation so that anyone using or testing this code is aware that the controller is a UI concern not meant for deeper persistence.

data/lib/services/dropbox_services.dart (1)

461-493: Maintain a consistent user flow
For updateAlbum, you throw an error if the album isn’t found. This is consistent with a strict approach. Ensure the UI or consumer code handles that 404 gracefully (e.g., notifying the user that the album no longer exists).

Comment on lines +87 to +130
class GoogleDriveContentUpdateEndpoint extends Endpoint {
final AppMediaContent content;
final String id;
final CancelToken? cancellationToken;
final void Function(int chunk, int length)? onProgress;

const GoogleDriveContentUpdateEndpoint({
required this.content,
required this.id,
this.cancellationToken,
this.onProgress,
});

@override
String get baseUrl => BaseURL.googleDriveUploadV3;

@override
CancelToken? get cancelToken => cancellationToken;

@override
HttpMethod get method => HttpMethod.patch;

@override
Map<String, dynamic> get headers => {
'Content-Type': content.contentType,
'Content-Length': content.length.toString(),
};

@override
Object? get data => content.stream;

@override
String get path => '/files/$id';

@override
Map<String, dynamic>? get queryParameters => {
'uploadType': 'media',
'fields':
'id, name, description, mimeType, thumbnailLink, webContentLink, createdTime, modifiedTime, size, imageMediaMetadata, videoMediaMetadata, appProperties',
};

@override
void Function(int p1, int p2)? get onSendProgress => onProgress;
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Ensure null length is handled properly when streaming content
GoogleDriveContentUpdateEndpoint relies on content.length to set the 'Content-Length' header. If content.length is null, it might cause issues or inconsistencies, depending on how Dio or the server handles such requests. Consider either asserting that it’s non-null or gracefully handling the scenario where it may be unknown.

Comment on lines +37 to +46
void _observeError(BuildContext context) {
ref.listen(
_provider.select(
(value) => value.error,
), (previous, error) {
if (error != null) {
showErrorSnackBar(context: context, error: error);
}
});
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid repeated state listening calls in _observeError.
While this pattern works, consider combining _observeError and _observeSucceed in a single method or hooking into ref.listen at build() or initState() once. Keeping them separate can lead to confusion if the order of calls ever matters.

Comment on lines +114 to +120
'CREATE TABLE ${LocalDatabaseConstants.albumsTable} ('
'id TEXT PRIMARY KEY, '
'name TEXT NOT NULL, '
'source TEXT NOT NULL, '
'created_at TEXT NOT NULL, '
'medias TEXT NOT NULL '
')',
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Store media JSON instead of comma-separated strings.
Comma-separated lists are prone to parsing, ordering, and escaping issues. Storing them as JSON strings or in a separate table ensures better data integrity and easier migrations if the structure changes.

 'medias': '',
- 'medias TEXT NOT NULL '
+ 'medias TEXT NOT NULL' -- consider storing JSON, e.g., '["id1","id2"]' 

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +137 to +148
if (appPropertyTemplateId != null && localRefId != null)
'property_groups': [
{
"fields": [
{
"name": ProviderConstants.localRefIdKey,
"value": localRefId ?? '',
},
],
"template_id": appPropertyTemplateId,
}
],
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use caution when appending property groups.
If the file or folder has existing properties, confirm that overwriting or merging is the desired state. Using property_groups might inadvertently remove or override other properties.

Comment on lines +125 to +130
"@_ADD_ALBUM":{},
"add_album_screen_title": "Album",
"album_tame_field_title": "Album Name",
"store_in_title": "Store In",
"store_in_device_title": "Device",

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix possible typo: album_tame_field_title.
The key is “album_tame_field_title” but presumably should be “album_name_field_title.” Confirm the intended key.

-  "album_tame_field_title": "Album Name",
+  "album_name_field_title": "Album Name",
📝 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
"@_ADD_ALBUM":{},
"add_album_screen_title": "Album",
"album_tame_field_title": "Album Name",
"store_in_title": "Store In",
"store_in_device_title": "Device",
"@_ADD_ALBUM":{},
"add_album_screen_title": "Album",
"album_name_field_title": "Album Name",
"store_in_title": "Store In",
"store_in_device_title": "Device",

Comment on lines +113 to +151
onLongTap: () {
showAppSheet(
context: context,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
AppSheetAction(
title: context.l10n.common_edit,
onPressed: () async {
context.pop();
final res = await AddAlbumRoute(
$extra: album,
).push(context);
if (res == true) {
_notifier.loadAlbums();
}
},
icon: Icon(
Icons.edit_outlined,
size: 24,
color: context.colorScheme.textPrimary,
),
),
AppSheetAction(
title: context.l10n.common_delete,
onPressed: () {
context.pop();
_notifier.deleteAlbum(album);
},
icon: Icon(
CupertinoIcons.delete,
size: 24,
color: context.colorScheme.textPrimary,
),
),
],
),
);
},
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use confirmation alerts.
Upon deleting an album, consider prompting a confirmation dialog. Currently, there's a bottom sheet with an action, but confirm that there's a final "Are you sure?" step if data is critical.

🧰 Tools
🪛 GitHub Actions: Analyze

[warning] File was automatically formatted, indicating it did not meet the required formatting standards

Comment on lines 410 to 434
Future<void> createAlbum(Album album) async {
final albums = await getAlbums();
albums.add(album);
albums.sort((a, b) => b.created_at.compareTo(a.created_at));

final res = await _dropboxAuthenticatedDio.req(
DropboxUploadEndpoint(
mode: 'overwrite',
autoRename: false,
content: AppMediaContent(
stream: Stream.value(utf8.encode(jsonEncode(album))),
length: utf8.encode(jsonEncode(album)).length,
contentType: 'application/octet-stream',
),
filePath: "/${ProviderConstants.backupFolderName}/Albums.json",
),
);

if (res.statusCode == 200) return;

throw SomethingWentWrongError(
statusCode: res.statusCode,
message: res.statusMessage,
);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Prevent overwriting the entire albums list
When calling createAlbum, you sort and then upload only the single album in jsonEncode(album) instead of the entire list. This code references albums.add(album), but the upload stream references jsonEncode(album). That might overwrite Albums.json with only the single new album unless you intend to store just one at a time. Double-check your logic.

- content: AppMediaContent(
-   stream: Stream.value(utf8.encode(jsonEncode(album))),
-   length: utf8.encode(jsonEncode(album)).length,
-   contentType: 'application/octet-stream',
- ),
+ content: AppMediaContent(
+   stream: Stream.value(utf8.encode(jsonEncode(albums))),
+   length: utf8.encode(jsonEncode(albums)).length,
+   contentType: 'application/json',
+ ),
📝 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
Future<void> createAlbum(Album album) async {
final albums = await getAlbums();
albums.add(album);
albums.sort((a, b) => b.created_at.compareTo(a.created_at));
final res = await _dropboxAuthenticatedDio.req(
DropboxUploadEndpoint(
mode: 'overwrite',
autoRename: false,
content: AppMediaContent(
stream: Stream.value(utf8.encode(jsonEncode(album))),
length: utf8.encode(jsonEncode(album)).length,
contentType: 'application/octet-stream',
),
filePath: "/${ProviderConstants.backupFolderName}/Albums.json",
),
);
if (res.statusCode == 200) return;
throw SomethingWentWrongError(
statusCode: res.statusCode,
message: res.statusMessage,
);
}
Future<void> createAlbum(Album album) async {
final albums = await getAlbums();
albums.add(album);
albums.sort((a, b) => b.created_at.compareTo(a.created_at));
final res = await _dropboxAuthenticatedDio.req(
DropboxUploadEndpoint(
mode: 'overwrite',
autoRename: false,
content: AppMediaContent(
stream: Stream.value(utf8.encode(jsonEncode(albums))),
length: utf8.encode(jsonEncode(albums)).length,
contentType: 'application/json',
),
filePath: "/${ProviderConstants.backupFolderName}/Albums.json",
),
);
if (res.statusCode == 200) return;
throw SomethingWentWrongError(
statusCode: res.statusCode,
message: res.statusMessage,
);
}

Comment on lines 29 to 32
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/path_provider_android-2.2.12" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/photo_manager-3.6.2" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dev/shared_preferences_android-2.3.3" />
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove duplicate plugin entries with conflicting versions.

There are duplicate entries for the following plugins with different versions:

  • path_provider_android: 2.2.12 vs 2.2.14
  • photo_manager: 3.6.2 vs 3.6.3
  • path_provider_foundation: 2.4.0 vs 2.4.1
  • shared_preferences_android: 2.3.3 vs 2.3.4

This could lead to runtime issues. Instead of manually editing this auto-generated file:

  1. Update dependencies in pubspec.yaml
  2. Run flutter pub get to resolve dependencies
  3. Consider adding .idea/libraries/ to .gitignore

Copy link

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

♻️ Duplicate comments (1)
app/lib/ui/flow/albums/albums_screen.dart (1)

113-151: ⚠️ Potential issue

Add confirmation dialog for album deletion.

Direct deletion of albums without confirmation could lead to accidental data loss.

Add a confirmation dialog before deletion:

  AppSheetAction(
    title: context.l10n.common_delete,
    onPressed: () {
      context.pop();
-     _notifier.deleteAlbum(album);
+     showDialog(
+       context: context,
+       builder: (context) => AlertDialog(
+         title: Text(context.l10n.delete_album_confirmation_title),
+         content: Text(context.l10n.delete_album_confirmation_message),
+         actions: [
+           TextButton(
+             onPressed: () => context.pop(),
+             child: Text(context.l10n.common_cancel),
+           ),
+           TextButton(
+             onPressed: () {
+               context.pop();
+               _notifier.deleteAlbum(album);
+             },
+             child: Text(context.l10n.common_delete),
+           ),
+         ],
+       ),
+     );
    },
    icon: Icon(
      CupertinoIcons.delete,
      size: 24,
      color: context.colorScheme.textPrimary,
    ),
  ),
🧹 Nitpick comments (2)
app/lib/ui/flow/albums/albums_screen.dart (2)

40-49: Consider enhancing error handling.

While the error handling is functional, consider adding error type differentiation for more specific user feedback. For example, network errors might need different handling than permission errors.

Example enhancement:

  void _observeError(BuildContext context) {
    ref.listen(
        albumStateNotifierProvider.select(
          (value) => value.actionError,
        ), (previous, error) {
      if (error != null) {
-       showErrorSnackBar(context: context, error: error);
+       if (error is NetworkError) {
+         showErrorSnackBar(
+           context: context,
+           error: context.l10n.network_error_message,
+         );
+       } else if (error is PermissionError) {
+         showErrorSnackBar(
+           context: context,
+           error: context.l10n.permission_error_message,
+         );
+       } else {
+         showErrorSnackBar(context: context, error: error);
+       }
      }
    });
  }

173-176: Consider adding loading and error states for media thumbnails.

The thumbnail display doesn't handle loading or error states, which could lead to poor user experience when media loading fails.

Consider wrapping the media display with error handling:

  OnTapScale(
    onTap: onTap,
    onLongTap: onLongTap,
+   child: Stack(
+     children: [
+       child,
+       if (isLoading)
+         const Center(child: AppCircularProgressIndicator()),
+       if (hasError)
+         Center(
+           child: Icon(
+             Icons.error_outline,
+             color: context.colorScheme.error,
+           ),
+         ),
+     ],
+   ),
-   child: Column(...),
  ),
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3b2e47f and 73cccba.

📒 Files selected for processing (3)
  • .idea/libraries/Flutter_Plugins.xml (0 hunks)
  • app/lib/ui/flow/albums/albums_screen.dart (1 hunks)
  • data/.flutter-plugins-dependencies (1 hunks)
💤 Files with no reviewable changes (1)
  • .idea/libraries/Flutter_Plugins.xml
🚧 Files skipped from review as they are similar to previous changes (1)
  • data/.flutter-plugins-dependencies
🔇 Additional comments (1)
app/lib/ui/flow/albums/albums_screen.dart (1)

1-29: LGTM! Well-structured imports and class declaration.

The imports are well-organized, and the class is properly declared as a ConsumerStatefulWidget for Riverpod integration.

Comment on lines +34 to +38
@override
void initState() {
_notifier = ref.read(albumStateNotifierProvider.notifier);
super.initState();
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix initState method order.

The super.initState() should be called before any other initialization to ensure proper widget lifecycle handling.

Apply this fix:

  @override
  void initState() {
-   _notifier = ref.read(albumStateNotifierProvider.notifier);
    super.initState();
+   _notifier = ref.read(albumStateNotifierProvider.notifier);
  }
📝 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
@override
void initState() {
_notifier = ref.read(albumStateNotifierProvider.notifier);
super.initState();
}
@override
void initState() {
super.initState();
_notifier = ref.read(albumStateNotifierProvider.notifier);
}

app/lib/ui/flow/albums/albums_screen.dart Outdated Show resolved Hide resolved
Copy link

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

♻️ Duplicate comments (3)
app/lib/ui/flow/albums/albums_screen.dart (3)

36-40: ⚠️ Potential issue

Fix initState method order.

The super.initState() must be called before any other initialization to ensure proper widget lifecycle handling.

Apply this fix:

@override
void initState() {
-  _notifier = ref.read(albumStateNotifierProvider.notifier);
  super.initState();
+  _notifier = ref.read(albumStateNotifierProvider.notifier);
}

116-154: 🛠️ Refactor suggestion

Add confirmation dialog for album deletion.

While the bottom sheet provides actions, it's recommended to add a confirmation dialog before proceeding with album deletion to prevent accidental data loss.

Example implementation:

 onPressed: () {
   context.pop();
+  showDialog(
+    context: context,
+    builder: (context) => AlertDialog(
+      title: Text(context.l10n.delete_album_confirmation_title),
+      content: Text(context.l10n.delete_album_confirmation_message),
+      actions: [
+        TextButton(
+          onPressed: () => context.pop(),
+          child: Text(context.l10n.common_cancel),
+        ),
+        TextButton(
+          onPressed: () {
+            context.pop();
+            _notifier.deleteAlbum(album);
+          },
+          child: Text(context.l10n.common_delete),
+        ),
+      ],
+    ),
+  );
-  _notifier.deleteAlbum(album);
 },

199-207: ⚠️ Potential issue

Fix empty path in AppMediaImage.

Creating an AppMedia instance with an empty path could lead to issues in media handling or display.

Apply this fix:

 AppMediaImage(
   media: AppMedia(
     id: album.medias.first,
-    path: '',
+    path: album.mediaPath ?? '',  // Get path from album or handle null case
     type: AppMediaType.image,
     sources: [album.source],
   ),
-  size: Size(300, 300),
+  size: MediaQuery.of(context).size.width * 0.4,  // Responsive size
 ),
🧹 Nitpick comments (12)
app/lib/ui/flow/albums/media_list/album_media_list_screen.dart (1)

139-152: Improve grid view responsiveness and maintainability

The current grid implementation has hardcoded values and could be more maintainable.

Consider these improvements:

+ const double _kGridSpacing = 4.0;
+ const double _kMinThumbnailSize = 100.0;
+ const double _kMaxThumbnailSize = 180.0;

  return GridView.builder(
    gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
-     crossAxisCount: context.mediaQuerySize.width > 600
-         ? context.mediaQuerySize.width ~/ 180
-         : context.mediaQuerySize.width ~/ 100,
+     crossAxisCount: (context.mediaQuerySize.width / 
+       (context.mediaQuerySize.width > 600 ? _kMaxThumbnailSize : _kMinThumbnailSize)).floor(),
-     crossAxisSpacing: 4,
-     mainAxisSpacing: 4,
+     crossAxisSpacing: _kGridSpacing,
+     mainAxisSpacing: _kGridSpacing,
    ),
    itemCount: state.medias.length,
    itemBuilder: (context, index) => AppMediaThumbnail(
      heroTag: "album_media_list",
      media: state.medias[index],
    ),
  );
app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart (1)

215-226: Simplify error handling logic

The current error handling differentiates between empty and non-empty media lists in a way that might be confusing to maintain.

Consider simplifying the error handling:

- state = state.copyWith(
-   loading: false,
-   error: state.medias.isEmpty ? e : null,
-   actionError: state.medias.isNotEmpty ? e : null,
- );
+ state = state.copyWith(
+   loading: false,
+   error: e,
+   actionError: state.medias.isNotEmpty ? e : null,
+ );

This change makes it clearer that:

  • The main error state always reflects the latest error
  • The actionError is only set when we have existing media (for UI purposes)
app/lib/ui/flow/media_selection/media_selection_state_notifier.dart (4)

43-51: Consider deferring media loading until needed
Invoking loadMedias(reload: true) inside the constructor forces a media load immediately, which might conflict if the UI or other dependencies aren’t fully ready. A common best practice is to expose an explicit init or load method and let the UI trigger it when appropriate.

 MediaSelectionStateNotifier(
   ...
 ) : super(const MediaSelectionState()) {
-  loadMedias(reload: true);
 }

83-87: Handle missing backup folder more gracefully
Throwing a BackUpFolderNotFound() error is valid, but consider providing a user-friendly action or fallback (e.g., prompting the user to create the backup folder) rather than abruptly raising an exception.


183-208: Optimize handling of selected media IDs
Currently, when selecting or deselecting a media ID, the code copies and filters a list. Using a Set<String> might be more efficient for add/remove operations, then converting back to a list only if your UI requires it.

-    if (state.selectedMedias.contains(id)) {
-      state = state.copyWith(
-        selectedMedias: [
-          ...state.selectedMedias.where((element) => element != id),
-        ],
-      );
-    } else {
-      state = state.copyWith(
-        selectedMedias: [
-          ...state.selectedMedias,
-          id,
-        ],
-      );
-    }

+    final currentSet = state.selectedMedias.toSet();
+    if (!currentSet.remove(id)) {
+      currentSet.add(id);
+    }
+    state = state.copyWith(
+      selectedMedias: currentSet.toList(),
+    );

211-221: Differentiate between global and action-scoped errors
Storing errors in error vs. actionError is a good approach, but both are typed as Object?. Consider using specialized typed exceptions or sealed classes to distinguish between different failure modes (e.g., permission errors vs. loading errors) for more robust error handling.

app/lib/ui/flow/albums/add/add_album_screen.dart (1)

62-63: Move listener methods out of build().

Calling _observeError(context) and _observeSucceed(context) from inside the build method can lead to multiple state subscription triggers on every rebuild. Consider calling them once in initState() or a separate lifecycle method to prevent potential duplication of side effects.

app/lib/ui/flow/main/main_screen.dart (2)

23-85: Encapsulate tab definitions for maintainability.

Storing tab definitions (icons, labels) in a separate configuration or a static list can reduce duplication between Cupertino and Material tab bars and simplify adding/removing tabs in the future.

- final tabs = [
-   (
-     icon: CupertinoIcons.house_fill,
-     label: "Home",
-     activeIcon: CupertinoIcons.house_fill,
-   ),
-   ...
- ];

+ // Example: Move tab config to a static member or separate file:
+ static final List<NavigationTabConfig> _tabConfig = [
+   NavigationTabConfig(
+     icon: CupertinoIcons.house_fill,
+     label: "Home",
+     activeIcon: CupertinoIcons.house_fill,
+   ),
+   ...
+ ];

136-149: Use a consistent approach for tab-based navigation.

The _goBranch method uses a switch to navigate to different routes. Consider storing these routes in a list or mapping to avoid manual case branching. This approach will be easier to extend if new tabs are added.

app/lib/ui/flow/albums/albums_view_notifier.dart (1)

90-117: Ensure user feedback if album load partially succeeds.

Multiple calls run in parallel to fetch albums from local/remote sources. If one call fails (e.g., network error for Dropbox), the others still succeed. Consider providing partial success feedback (like partial success states or a combined error message) so users are aware of incomplete results.

app/lib/ui/flow/albums/add/add_album_state_notifier.dart (1)

84-155: Recommend robust offline handling for album creation.

createAlbum handles various sources, but if the user lacks connectivity (especially for GoogleDrive or Dropbox), the fallback is only a thrown exception. For a smoother UX, consider a retry mechanism or explicit offline checks to let the user know an album created offline can be synced later.

data/lib/services/dropbox_services.dart (1)

417-500: Refactor duplicate album operation code.

The album operations (create, delete, update) contain duplicate code for uploading the albums file. Consider extracting this into a reusable method.

Example implementation:

+ Future<void> _uploadAlbums(List<Album> albums) async {
+   final res = await _dropboxAuthenticatedDio.req(
+     DropboxUploadEndpoint(
+       mode: 'overwrite',
+       autoRename: false,
+       content: AppMediaContent(
+         stream: Stream.value(utf8.encode(jsonEncode(albums))),
+         length: utf8.encode(jsonEncode(albums)).length,
+         contentType: 'application/json',
+       ),
+       filePath: "/${ProviderConstants.backupFolderName}/Albums.json",
+     ),
+   );
+
+   if (res.statusCode != 200) {
+     throw SomethingWentWrongError(
+       statusCode: res.statusCode,
+       message: res.statusMessage,
+     );
+   }
+ }

  Future<void> createAlbum(Album album) async {
    final albums = await getAlbums();
    albums.add(album);
    albums.sort((a, b) => b.created_at.compareTo(a.created_at));
-   // Remove duplicate upload code
-   final res = await _dropboxAuthenticatedDio.req...
+   await _uploadAlbums(albums);
  }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 73cccba and db3077a.

📒 Files selected for processing (12)
  • .idea/libraries/Flutter_Plugins.xml (1 hunks)
  • app/lib/components/app_page.dart (6 hunks)
  • app/lib/ui/app.dart (1 hunks)
  • app/lib/ui/flow/albums/add/add_album_screen.dart (1 hunks)
  • app/lib/ui/flow/albums/add/add_album_state_notifier.dart (1 hunks)
  • app/lib/ui/flow/albums/albums_screen.dart (1 hunks)
  • app/lib/ui/flow/albums/albums_view_notifier.dart (1 hunks)
  • app/lib/ui/flow/albums/media_list/album_media_list_screen.dart (1 hunks)
  • app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart (1 hunks)
  • app/lib/ui/flow/main/main_screen.dart (1 hunks)
  • app/lib/ui/flow/media_selection/media_selection_state_notifier.dart (1 hunks)
  • data/lib/services/dropbox_services.dart (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/lib/ui/app.dart
  • app/lib/components/app_page.dart
  • .idea/libraries/Flutter_Plugins.xml
🔇 Additional comments (12)
app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart (2)

230-240: Well-structured state class

The state class is well-designed with appropriate use of Freezed and sensible default values.


156-158: Address TODO comment about removing deleted media

The TODO comment indicates a potential memory leak where deleted media items might still be displayed.

Let's verify if this is a critical issue:

Would you like me to help implement the media cleanup functionality and create a GitHub issue to track this?

app/lib/ui/flow/media_selection/media_selection_state_notifier.dart (1)

59-61: Safeguard against concurrent loading
The check if (state.loading || _maxLoaded) return; prevents duplicate loading but does not handle the scenario where the user might wish to load or refresh again after a potential state change. Consider adding logic to reset _maxLoaded or provide a more granular approach to concurrency if your UI supports multiple load requests over time.

app/lib/ui/flow/albums/add/add_album_screen.dart (2)

38-47: Consider consolidating repeated list listeners.

Invoking _observeError (and _observeSucceed) within build() may trigger multiple state listenings, potentially causing repeated side effects. A past review highlighted the same concern: hooking these listeners once, possibly in initState, can help avoid confusion if the widget rebuilds frequently.


90-143: Ensure cleanup of validations when dismissing screen.

Although the TextEditingController in the state is disposed in the notifier, confirm that cleanup is robust, especially if other states or listeners are added in the future. This avoids potential memory leaks or dangling references.

app/lib/ui/flow/albums/albums_view_notifier.dart (2)

28-33: Validate account changes in parallel.

When onGoogleDriveAccountChange or onDropboxAccountChange is triggered, the code updates the state and reloads albums. If multiple accounts change at roughly the same time, consider concurrency ordering or combining these changes into one refresh if appropriate.


119-148: Evaluate concurrency for album deletion.

When deleting albums from multiple sources, each deletion runs sequentially, but an in-flight deletion could conflict with a new load request if triggered. A concurrency control or a loading guard might help, ensuring that the user cannot repeatedly tap "delete" or simultaneously fetch updated list states.

app/lib/ui/flow/albums/add/add_album_state_notifier.dart (2)

157-161: Include edge-case checks for album name validation.

Currently, validateAlbumName only checks for an empty string. Consider supporting other constraints (e.g., maximum length, special characters) if needed. This will help maintain consistent naming standards across local and cloud album sources.


164-167: Good disposal of resources.

Disposing the albumNameController prevents memory leaks. The approach to resource management here is commendable.

app/lib/ui/flow/albums/albums_screen.dart (1)

42-51: LGTM! Good error handling implementation.

The error handling implementation properly observes state changes and provides user feedback through snackbar messages.

data/lib/services/dropbox_services.dart (2)

388-415: LGTM! Robust implementation of album retrieval.

The implementation properly handles:

  • File download and JSON parsing
  • Error cases including 409 status code
  • Empty album list scenarios

417-441: 🛠️ Refactor suggestion

Implement optimistic locking for album operations.

The current implementation could lead to race conditions if multiple users modify albums simultaneously. Consider implementing optimistic locking using a version number or timestamp.

Example implementation:

 Future<void> createAlbum(Album album) async {
+  final String eTag = await getAlbumsETag();
   final albums = await getAlbums();
   albums.add(album);
   albums.sort((a, b) => b.created_at.compareTo(a.created_at));

   final res = await _dropboxAuthenticatedDio.req(
     DropboxUploadEndpoint(
       mode: 'overwrite',
       autoRename: false,
+      ifMatch: eTag,  // Add ETag for optimistic locking
       content: AppMediaContent(
         stream: Stream.value(utf8.encode(jsonEncode(albums))),
         length: utf8.encode(jsonEncode(albums)).length,
         contentType: 'application/json',  // Use correct content type
       ),
       filePath: "/${ProviderConstants.backupFolderName}/Albums.json",
     ),
   );

-  if (res.statusCode == 200) return;
+  if (res.statusCode == 200) {
+    return;
+  } else if (res.statusCode == 412) {  // Precondition Failed
+    throw ConcurrentModificationError();
+  }

Likely invalid or redundant comment.

Comment on lines +37 to +42
@override
void initState() {
_provider = albumMediaListStateNotifierProvider(widget.album);
_notifier = ref.read(_provider.notifier);
super.initState();
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix initState method order

The super.initState() should be called before using ref to ensure proper widget initialization.

Apply this fix:

  @override
  void initState() {
+   super.initState();
    _provider = albumMediaListStateNotifierProvider(widget.album);
    _notifier = ref.read(_provider.notifier);
-   super.initState();
  }
📝 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
@override
void initState() {
_provider = albumMediaListStateNotifierProvider(widget.album);
_notifier = ref.read(_provider.notifier);
super.initState();
}
@override
void initState() {
super.initState();
_provider = albumMediaListStateNotifierProvider(widget.album);
_notifier = ref.read(_provider.notifier);
}

Comment on lines 169 to 211
if (state.album.source == AppMediaSource.local) {
final loadedMediaIds = state.medias.map((e) => e.id).toList();
final moreMediaIds = state.album.medias
.where((element) => !loadedMediaIds.contains(element))
.take(30)
.toList();

medias = await Future.wait(
moreMediaIds.map(
(id) => _localMediaService.getMedia(id: id),
),
).then(
(value) => value.nonNulls.toList(),
);
} else if (state.album.source == AppMediaSource.googleDrive) {
final loadedMediaIds =
state.medias.map((e) => e.driveMediaRefId).nonNulls.toList();
final moreMediaIds = state.album.medias
.where((element) => !loadedMediaIds.contains(element))
.take(30)
.toList();
medias = await Future.wait(
moreMediaIds.map(
(id) => _googleDriveService.getMedia(id: id),
),
).then(
(value) => value.nonNulls.toList(),
);
} else if (state.album.source == AppMediaSource.dropbox) {
final loadedMediaIds =
state.medias.map((e) => e.dropboxMediaRefId).nonNulls.toList();
final moreMediaIds = state.album.medias
.where((element) => !loadedMediaIds.contains(element))
.take(30)
.toList();
medias = await Future.wait(
moreMediaIds.map(
(id) => _dropboxService.getMedia(id: id),
),
).then(
(value) => value.nonNulls.toList(),
);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Refactor duplicate media loading logic

The media loading logic is duplicated for each source type with only minor variations.

Extract the common pattern into a reusable method:

+ private Future<List<AppMedia>> _loadMediaBatch(
+   List<AppMedia> currentMedias,
+   List<String> albumMedias,
+   Future<AppMedia?> Function(String) getMediaFn,
+   String Function(AppMedia) getRefId,
+ ) async {
+   final loadedMediaIds = currentMedias.map(getRefId).nonNulls.toList();
+   final moreMediaIds = albumMedias
+     .where((element) => !loadedMediaIds.contains(element))
+     .take(30)
+     .toList();
+   return Future.wait(
+     moreMediaIds.map(getMediaFn),
+   ).then((value) => value.nonNulls.toList());
+ }

  Future<void> loadMedia({bool reload = false}) async {
    // ... existing code ...

    if (state.album.source == AppMediaSource.local) {
-     final loadedMediaIds = state.medias.map((e) => e.id).toList();
-     final moreMediaIds = state.album.medias
-         .where((element) => !loadedMediaIds.contains(element))
-         .take(30)
-         .toList();
-     medias = await Future.wait(
-       moreMediaIds.map(
-         (id) => _localMediaService.getMedia(id: id),
-       ),
-     ).then(
-       (value) => value.nonNulls.toList(),
-     );
+     medias = await _loadMediaBatch(
+       state.medias,
+       state.album.medias,
+       (id) => _localMediaService.getMedia(id: id),
+       (media) => media.id,
+     );
    } else if (state.album.source == AppMediaSource.googleDrive) {
      // Similar refactoring for other sources
    }

Committable suggestion skipped: line range outside the PR's diff.

child: AppTextField(
controller: state.albumNameController,
onChanged: _notifier.validateAlbumName,
label: context.l10n.album_tame_field_title,
Copy link

Choose a reason for hiding this comment

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

💡 Codebase verification

Fix typo in localization key: album_tame_field_title should be album_name_field_title

The localization key album_tame_field_title appears to be a typo. While it's defined in the localization file and used consistently, the word "tame" doesn't make semantic sense in this context. The field is clearly meant for the album name, so the key should follow the conventional naming pattern and be album_name_field_title.

  • app/lib/ui/flow/albums/add/add_album_screen.dart:99: Update the key to album_name_field_title
  • app/assets/locales/app_en.arb: Update the key from album_tame_field_title to album_name_field_title
🔗 Analysis chain

Double-check localization key.

The key album_tame_field_title might be a typo or mistranslation. Verify that you intended "album_name_field_title" or a similar phrase to avoid confusion in your localization strings.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Searching for 'album_tame_field_title' references in the codebase
rg --context 3 'album_tame_field_title'

Length of output: 1056

Copy link

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

♻️ Duplicate comments (3)
data/lib/apis/google_drive/google_drive_endpoint.dart (1)

110-113: ⚠️ Potential issue

Ensure null length is handled properly when setting headers

The content.length is used directly in headers without null-safety checks, which could cause issues if the length is null.

Apply this diff to handle null length:

  Map<String, dynamic> get headers => {
        'Content-Type': content.contentType,
-        'Content-Length': content.length.toString(),
+        if (content.length != null) 'Content-Length': content.length.toString(),
      };
app/lib/ui/flow/albums/albums_screen.dart (2)

36-40: ⚠️ Potential issue

Fix initState method order and move _observeError to prevent multiple subscriptions

Calling super.initState(); should occur before any other initialization to ensure proper widget lifecycle handling. Additionally, calling ref.listen inside the build method can lead to multiple subscriptions and unintended behavior since build can be called multiple times. It's recommended to set up listeners in initState() instead.

Apply this diff to correct the method order and move _observeError(context); to initState:

 class _AlbumsScreenState extends ConsumerState<AlbumsScreen> {
   late AlbumStateNotifier _notifier;

   @override
   void initState() {
+    super.initState();
     _notifier = ref.read(albumStateNotifierProvider.notifier);
+    _observeError(context);
-    super.initState();
   }

-  @override
-  Widget build(BuildContext context) {
-    _observeError(context);
+  @override
+  Widget build(BuildContext context) {
     return AppPage(

Also applies to: 55-55


141-151: 🛠️ Refactor suggestion

Add confirmation dialog before deleting an album

Currently, selecting the delete option immediately deletes the album without any user confirmation. To prevent accidental deletions and enhance user experience, consider prompting a confirmation dialog before proceeding with the deletion.

You can implement a confirmation dialog as follows:

onPressed: () {
  context.pop();
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: Text(context.l10n.delete_album_title),
      content: Text(context.l10n.delete_album_confirmation),
      actions: [
        TextButton(
          onPressed: () => Navigator.of(context).pop(),
          child: Text(context.l10n.common_cancel),
        ),
        TextButton(
          onPressed: () {
            Navigator.of(context).pop(); // Close the dialog
            _notifier.deleteAlbum(album);
          },
          child: Text(context.l10n.common_delete),
        ),
      ],
    ),
  );
},

Make sure to add appropriate localization entries for delete_album_title and delete_album_confirmation.

🧹 Nitpick comments (6)
data/lib/domain/config.dart (1)

10-16: Consider consolidating databases for better maintainability.

While the constants are well-organized, having two separate databases (cloud-gallery.db and cloud-gallery-album.db) might introduce unnecessary complexity:

  • Increased overhead in managing multiple database connections
  • Potential synchronization challenges
  • More complex error handling and transaction management

Consider consolidating these into a single database file unless there's a specific requirement for separation.

app/lib/ui/flow/albums/albums_view_notifier.dart (3)

60-88: Consider reducing code duplication in account change handlers.

The onGoogleDriveAccountChange and onDropboxAccountChange methods share similar filtering logic. Consider extracting the common pattern into a shared method.

+  void _filterAlbumsBySource(AppMediaSource source) {
+    state = state.copyWith(
+      albums: state.albums
+          .where((element) => element.source != source)
+          .toList(),
+    );
+  }

   Future<void> onGoogleDriveAccountChange(
     GoogleSignInAccount? googleAccount,
   ) async {
     state = state.copyWith(googleAccount: googleAccount);
     if (googleAccount != null) {
       _backupFolderId = await _googleDriveService.getBackUpFolderId();
       loadAlbums();
     } else {
       _backupFolderId = null;
-      state = state.copyWith(
-        albums: state.albums
-            .where((element) => element.source != AppMediaSource.googleDrive)
-            .toList(),
-      );
+      _filterAlbumsBySource(AppMediaSource.googleDrive);
     }
   }

   void onDropboxAccountChange(DropboxAccount? dropboxAccount) {
     state = state.copyWith(dropboxAccount: dropboxAccount);
     if (dropboxAccount != null) {
       loadAlbums();
     } else {
-      state = state.copyWith(
-        albums: state.albums
-            .where((element) => element.source != AppMediaSource.dropbox)
-            .toList(),
-      );
+      _filterAlbumsBySource(AppMediaSource.dropbox);
     }
   }

106-158: Consider breaking down the complex loadAlbums method.

The method handles multiple responsibilities and could be more maintainable if split into smaller functions. Consider extracting the album loading and media fetching logic into separate methods.

+  Future<List<Album>> _loadAlbumsFromSources() async {
+    _backupFolderId ??= await _googleDriveService.getBackUpFolderId();
+    return Future.wait([
+      _localMediaService.getAlbums(),
+      (state.googleAccount != null && _backupFolderId != null)
+          ? _googleDriveService.getAlbums(folderId: _backupFolderId!)
+          : Future.value([]),
+      (state.dropboxAccount != null)
+          ? _dropboxService.getAlbums()
+          : Future.value([]),
+    ]);
+  }

+  Future<Map<String, AppMedia>> _fetchMediaThumbnails(List<List<Album>> albums) async {
+    final thumbnails = await Future.wait([
+      for (Album album in albums[0])
+        _getThumbnailMedia(
+          album: album,
+          fetchMedia: (id) => _localMediaService.getMedia(id: id),
+        ),
+      // ... similar for other sources
+    ]);
+    
+    return {
+      for (final item in thumbnails)
+        if (item != null) item.id: item.media,
+    };
+  }

Consider more specific error handling.

The error handling is currently generic. Consider catching and handling specific exceptions that might occur during album loading or media fetching.

   } catch (e, s) {
+    final errorMessage = switch(e) {
+      BackUpFolderNotFound() => "Google Drive backup folder not found",
+      DropboxAuthError() => "Dropbox authentication failed",
+      _ => "Error loading albums"
+    };
     state = state.copyWith(loading: false, error: e);
-    _logger.e(
-      "AlbumStateNotifier: Error loading albums",
-      error: e,
-      stackTrace: s,
-    );
+    _logger.e("AlbumStateNotifier: $errorMessage", error: e, stackTrace: s);
   }

160-189: Well-implemented deletion logic with proper error handling.

The method correctly handles deletions from different sources and maintains state consistency. Consider adding a loading state during deletion to prevent multiple simultaneous deletion attempts.

   Future<void> deleteAlbum(Album album) async {
+    if (state.loading) return;
     try {
-      state = state.copyWith(actionError: null);
+      state = state.copyWith(actionError: null, loading: true);
       if (album.source == AppMediaSource.local) {
         await _localMediaService.deleteAlbum(album.id);
       } else if (album.source == AppMediaSource.googleDrive) {
         // ... existing code ...
       }
       state = state.copyWith(
         albums: state.albums.where((element) => element.id != album.id).toList(),
+        loading: false,
       );
     } catch (e, s) {
-      state = state.copyWith(actionError: e);
+      state = state.copyWith(actionError: e, loading: false);
       _logger.e(
         "AlbumStateNotifier: Error deleting album",
         error: e,
         stackTrace: s,
       );
     }
   }
app/lib/ui/flow/albums/albums_screen.dart (2)

202-205: Use responsive sizing for AppMediaImage

The AppMediaImage is currently using a fixed size of Size(300, 300), which may not adapt well to different screen sizes and orientations. To improve responsiveness and ensure the image scales appropriately across various devices, consider using dynamic sizing based on the screen dimensions.

Update the size parameter to use a percentage of the screen width:

 : AppMediaImage(
     media: media!,
-    size: Size(300, 300),
+    size: Size(
+      MediaQuery.of(context).size.width * 0.45,
+      MediaQuery.of(context).size.width * 0.45,
+    ),
 ),

This change ensures the image occupies approximately 45% of the screen width, making it more responsive on different devices.


100-105: Enhance grid responsiveness with SliverGridDelegateWithMaxCrossAxisExtent

Using SliverGridDelegateWithFixedCrossAxisCount with a fixed crossAxisCount may not adapt optimally to varying screen sizes and orientations. To make the grid more responsive and ensure consistent item sizing across different devices, consider using SliverGridDelegateWithMaxCrossAxisExtent. This delegate allows you to specify the maximum pixel width for each grid item, adjusting the number of columns based on the available space.

Modify the grid delegate as follows:

 return GridView(
   padding: EdgeInsets.all(8),
-  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
-    crossAxisCount: 2,
+  gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
+    maxCrossAxisExtent: 200,
     childAspectRatio: 0.9,
     crossAxisSpacing: 8,
     mainAxisSpacing: 16,
   ),

With this change, grid items will have a maximum width of 200 pixels, and the grid will automatically adjust the number of columns to fit the screen width.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between db3077a and 67b14e6.

📒 Files selected for processing (5)
  • app/lib/ui/flow/albums/albums_screen.dart (1 hunks)
  • app/lib/ui/flow/albums/albums_view_notifier.dart (1 hunks)
  • app/lib/ui/flow/albums/albums_view_notifier.freezed.dart (1 hunks)
  • data/lib/apis/google_drive/google_drive_endpoint.dart (2 hunks)
  • data/lib/domain/config.dart (1 hunks)
🔇 Additional comments (8)
data/lib/apis/google_drive/google_drive_endpoint.dart (3)

115-116: Verify direct stream usage in PATCH request

Using the content stream directly as request data might need validation to ensure it's properly handled by the Google Drive API. Consider testing with various file types and sizes.


219-242: Implementation looks good!

The GoogleDriveGetEndpoint is well-structured and follows the established patterns. The comprehensive default fields parameter ensures all necessary metadata is retrieved.


137-137: Verify null save location handling

Making saveLocation optional suggests downloads might be handled in memory when null. Please ensure the parent DownloadEndpoint class and the download logic properly handle this case.

Run this script to check the implementation:

Also applies to: 145-145

✅ Verification successful

Null handling for saveLocation is properly implemented

The endpoint correctly implements the nullable storePath contract from the parent DownloadEndpoint class, allowing for flexible download handling in the higher layers.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check how null save location is handled in the download endpoint implementation
rg -A 10 "class DownloadEndpoint" 

Length of output: 462


Script:

#!/bin/bash
# Check GoogleDriveDownloadEndpoint implementation
rg -A 20 "class GoogleDriveDownloadEndpoint" data/lib/apis/google_drive/google_drive_endpoint.dart

Length of output: 560


Script:

#!/bin/bash
# Check storePath implementation and download handling
rg -A 10 "storePath|downloadTo" data/lib/apis/google_drive/google_drive_endpoint.dart

Length of output: 318

data/lib/domain/config.dart (2)

2-2: LGTM! The new constant follows consistent naming patterns.

The albumFileName constant is well-placed and follows the existing naming conventions.


19-19: Verify Google Drive integration readiness.

Enabling googleDriveSupport is a significant change. Please ensure:

  1. All necessary Google Drive integration code is in place and tested
  2. Required Google Drive API credentials are properly configured
  3. This feature flag was intentionally enabled as part of the album support feature

Let me help verify the Google Drive integration readiness:

✅ Verification successful

Google Drive integration is ready for enabling

The codebase has a complete and production-ready Google Drive implementation with:

  • Full media management capabilities
  • Album support
  • Proper error handling
  • Authentication via dependency injection

While adding tests would be beneficial, the implementation is robust enough to safely enable the feature flag.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check for essential Google Drive integration components

echo "Checking for Google Drive service implementation..."
rg -l "GoogleDriveService" 

echo "Checking for Google Drive API setup..."
rg -l "google.*credentials|google.*client.*id"

echo "Checking for Google Drive-related tests..."
fd -e dart test | rg -l "google.*drive|drive.*service"

Length of output: 1003


Script:

#!/bin/bash
# Check implementation and configuration details

echo "=== GoogleDriveService Implementation ==="
cat data/lib/services/google_drive_service.dart

echo "=== Configuration Files ==="
fd -e yaml -e json | xargs rg -l "google.*drive|drive.*api"

echo "=== Test Directory Structure ==="
fd . test/

Length of output: 16218

app/lib/ui/flow/albums/albums_view_notifier.dart (2)

18-35: Well-structured provider setup!

The provider implementation follows best practices with proper dependency injection and automatic cleanup through autoDispose.


90-104: Efficient thumbnail retrieval implementation!

The method is well-documented and efficiently implements the "first available media" logic using modern Dart features like records.

app/lib/ui/flow/albums/albums_view_notifier.freezed.dart (1)

1-302: Generated code looks correct.

This is an auto-generated file by the freezed package and appears to be properly generated with all necessary implementations.

Copy link

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

♻️ Duplicate comments (2)
app/lib/ui/flow/albums/albums_screen.dart (1)

111-149: 🛠️ Refactor suggestion

Add confirmation dialog for album deletion

Consider adding a confirmation dialog before deleting an album to prevent accidental deletions.

  onPressed: () {
    context.pop();
+   showDialog(
+     context: context,
+     builder: (context) => AlertDialog(
+       title: Text(context.l10n.delete_album_confirmation_title),
+       content: Text(context.l10n.delete_album_confirmation_message),
+       actions: [
+         TextButton(
+           onPressed: () => context.pop(),
+           child: Text(context.l10n.common_cancel),
+         ),
+         TextButton(
+           onPressed: () {
+             context.pop();
+             _notifier.deleteAlbum(album);
+           },
+           child: Text(context.l10n.common_delete),
+         ),
+       ],
+     ),
+   );
-   _notifier.deleteAlbum(album);
  },
app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart (1)

220-262: 🛠️ Refactor suggestion

Refactor duplicate media loading logic.

The media loading logic is duplicated across different sources with only minor variations.

Extract the common pattern into a reusable method as suggested in the previous review.

🧹 Nitpick comments (15)
app/lib/ui/flow/home/components/app_media_item.dart (1)

Line range hint 34-83: Consider decomposing into smaller, focused components.

The AppMediaItem widget handles multiple responsibilities (media display, selection, progress, source indicators, video duration). Consider extracting these into separate components for better maintainability and testability:

  • MediaSelectionOverlay
  • VideoDurationIndicator
  • MediaSourceIndicator
  • MediaProgressIndicator

This would follow the Single Responsibility Principle and make the code more modular.

app/lib/ui/flow/home/home_screen_view_model.dart (1)

Line range hint 272-457: Consider caching the media list for performance.

The method processes and returns a potentially large list of media items. For better performance, especially when state.cloudLoading && !force, consider caching the expanded list.

Here's a suggested implementation:

+  List<AppMedia>? _cachedMediaList;
+
   Future<List<AppMedia>> loadMedias({
     bool reload = false,
     bool force = false,
   }) async {
     if (state.cloudLoading && !force) {
-      return state.medias.values.expand((element) => element.values).toList();
+      return _cachedMediaList ?? 
+        (_cachedMediaList = state.medias.values
+          .expand((element) => element.values)
+          .toList());
     }
     // ... existing code ...
     state = state.copyWith(
       loading: false,
       medias: sortMedias(medias: [...allMergedMedias, ...cloudBasedMedias]),
       cloudLoading: false,
     );
-    return state.medias.values.expand((element) => element.values).toList();
+    return _cachedMediaList = state.medias.values
+      .expand((element) => element.values)
+      .toList();
   }

Don't forget to invalidate the cache when needed:

   void _mediaProcessObserve() {
+    _cachedMediaList = null;
     state = state.copyWith(
       // ... existing code ...
     );
   }
app/lib/ui/flow/albums/component/album_item.dart (2)

18-24: Make media parameter optional in constructor

The media parameter is marked as required but can be null. Consider making it optional in the constructor to better reflect its nullable nature.

  const AlbumItem({
    super.key,
    required this.album,
-   required this.media,
+   this.media,
    required this.onTap,
    required this.onLongTap,
  });

47-52: Add semantic label for accessibility

The folder icon should have a semantic label for better accessibility.

      child: Icon(
        CupertinoIcons.folder,
        size: 80,
        color: context.colorScheme.containerHighOnSurface,
+       semanticLabel: 'Folder icon',
      ),
app/lib/ui/flow/albums/albums_screen.dart (1)

94-99: Use responsive grid layout

The grid layout uses hardcoded values which might not work well on different screen sizes. Consider making it responsive.

  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
-   crossAxisCount: 2,
+   crossAxisCount: MediaQuery.of(context).size.width ~/ 180, // Adjust based on desired minimum item width
    childAspectRatio: 0.9,
    crossAxisSpacing: 8,
    mainAxisSpacing: 16,
  ),
app/lib/ui/navigation/app_route.dart (1)

108-118: Document route parameters

Add documentation for the $extra parameter to clarify its usage and requirements.

+ /// Route for adding/editing an album
+ /// [extra] Optional album object for editing mode. If null, creates a new album.
  class AddAlbumRoute extends GoRouteData {
    static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;

    final Album? $extra;

    const AddAlbumRoute({this.$extra});
app/lib/ui/flow/albums/albums_view_notifier.dart (2)

154-160: Improve error logging specificity

The error message could be more specific about which part of the loading process failed.

  _logger.e(
-   "AlbumStateNotifier: Error loading albums",
+   "AlbumStateNotifier: Failed to load albums - ${e is AppError ? e.message : 'Unknown error'}",
    error: e,
    stackTrace: s,
  );

163-192: Add retry mechanism for delete operation

Consider implementing a retry mechanism for failed delete operations, especially for network-related failures.

Would you like me to provide an implementation of a retry mechanism with exponential backoff for the delete operation?

app/assets/locales/app_en.arb (1)

139-140: Improve error message clarity.

The error message for cloud media access could be more user-friendly and actionable.

-  "no_cloud_media_access_message": "You don't have access to view media files, check you sign in with the cloud!",
+  "no_cloud_media_access_message": "You don't have access to view media files. Please sign in to your cloud account and try again.",
app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart (3)

208-208: Address TODO comment about removing deleted media.

The TODO comment indicates a missing implementation for handling deleted media, which could lead to stale data in the UI.

Would you like me to help implement the logic for removing deleted media or create a GitHub issue to track this task?


264-266: Optimize state updates and return value.

The code unnecessarily creates two identical lists and updates the state before returning.

-      state =
-          state.copyWith(medias: [...state.medias, ...medias], loading: false);
-      return [...state.medias, ...medias];
+      final updatedMedias = [...state.medias, ...medias];
+      state = state.copyWith(medias: updatedMedias, loading: false);
+      return updatedMedias;

46-78: Consider implementing retry mechanism for album loading.

The loadAlbum method could benefit from a retry mechanism for transient failures, especially for network-dependent operations with Google Drive and Dropbox.

Consider implementing an exponential backoff retry mechanism for network operations to improve reliability.

app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview.dart (1)

54-54: Consider using a dedicated method for hero tag generation.

The string interpolation "${widget.heroTag}${widget.media.toString()}" is used in multiple places. Consider extracting it to a dedicated method to ensure consistency and make future modifications easier.

+  String _generateHeroTag() => "${widget.heroTag}${widget.media.toString()}";
+
   @override
   Widget build(BuildContext context) {
     return Center(
       child: Hero(
-        tag: "${widget.heroTag}${widget.media.toString()}",
+        tag: _generateHeroTag(),
app/lib/ui/flow/media_preview/components/download_require_view.dart (1)

35-35: Consider using the same hero tag generation method.

For consistency with NetworkImagePreview, consider extracting the hero tag generation logic to a shared utility method.

app/lib/ui/flow/media_preview/media_preview_screen.dart (1)

253-253: Consider centralizing hero tag generation.

The hero tag string interpolation is repeated across multiple preview types. Consider extracting this to a shared utility method.

+  String _generateHeroTag(AppMedia media) => "${widget.heroTag}${media.toString()}";
+
   @override
   Widget build(BuildContext context) {
     // ... existing code ...
-    tag: "${widget.heroTag}${media.toString()}",
+    tag: _generateHeroTag(media),

Also applies to: 331-331, 342-342, 369-369

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 67b14e6 and dc8b12e.

📒 Files selected for processing (22)
  • .idea/libraries/Dart_Packages.xml (24 hunks)
  • .idea/libraries/Flutter_Plugins.xml (1 hunks)
  • app/assets/locales/app_en.arb (2 hunks)
  • app/lib/components/app_media_thumbnail.dart (1 hunks)
  • app/lib/ui/flow/albums/albums_screen.dart (1 hunks)
  • app/lib/ui/flow/albums/albums_view_notifier.dart (1 hunks)
  • app/lib/ui/flow/albums/component/album_item.dart (1 hunks)
  • app/lib/ui/flow/albums/media_list/album_media_list_screen.dart (1 hunks)
  • app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart (1 hunks)
  • app/lib/ui/flow/home/components/app_media_item.dart (3 hunks)
  • app/lib/ui/flow/home/home_screen.dart (2 hunks)
  • app/lib/ui/flow/home/home_screen_view_model.dart (2 hunks)
  • app/lib/ui/flow/media_preview/components/download_require_view.dart (2 hunks)
  • app/lib/ui/flow/media_preview/components/local_media_image_preview.dart (2 hunks)
  • app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview.dart (2 hunks)
  • app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview_view_model.dart (4 hunks)
  • app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview_view_model.freezed.dart (10 hunks)
  • app/lib/ui/flow/media_preview/media_preview_screen.dart (6 hunks)
  • app/lib/ui/flow/media_preview/media_preview_view_model.dart (1 hunks)
  • app/lib/ui/navigation/app_route.dart (5 hunks)
  • data/.flutter-plugins (1 hunks)
  • data/.flutter-plugins-dependencies (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • data/.flutter-plugins
🚧 Files skipped from review as they are similar to previous changes (5)
  • app/lib/ui/flow/albums/media_list/album_media_list_screen.dart
  • app/lib/ui/flow/home/home_screen.dart
  • app/lib/components/app_media_thumbnail.dart
  • data/.flutter-plugins-dependencies
  • .idea/libraries/Flutter_Plugins.xml
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build
🔇 Additional comments (19)
app/lib/ui/flow/home/components/app_media_item.dart (3)

16-16: LGTM! Well-placed field declaration.

The heroTag field is appropriately declared as a final String and well-positioned in the class structure.


26-26: LGTM! Proper constructor parameter addition.

The heroTag parameter is correctly marked as required and well-positioned in the constructor parameter list.


51-51: Verify hero tag uniqueness across the app.

While the heroTag is correctly passed to AppMediaImage, ensure that the tags remain unique across different screens to prevent hero animation conflicts.

Let's verify the uniqueness of hero tags in the codebase:

✅ Verification successful

Hero tag implementation verified - no conflicts found

The codebase follows a consistent pattern of unique hero tag generation:

  • Static tags for screen-level transitions
  • Dynamic tags with media identifiers for item-level transitions
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for other heroTag usages to ensure uniqueness
rg -A 2 "heroTag:" app/lib/

Length of output: 3045

app/lib/ui/flow/home/home_screen_view_model.dart (3)

276-278: ⚠️ Potential issue

Early return might skip important state updates.

The early return when state.cloudLoading && !force skips important state updates that occur later in the method, such as:

  • Permission checks
  • Internet connectivity verification
  • Error state updates
    This could lead to stale or incorrect state if callers rely on these side effects.

Consider this alternative implementation:

-if (state.cloudLoading && !force) {
-  return state.medias.values.expand((element) => element.values).toList();
-}
+if (state.cloudLoading && !force) {
+  state = state.copyWith(error: null);
+  return state.medias.values.expand((element) => element.values).toList();
+}

Likely invalid or redundant comment.


457-457: 🛠️ Refactor suggestion

Return value might be inconsistent with state.

The final return statement directly accesses state.medias without considering potential race conditions. If the state is modified between the last state update and this return, the returned list might be inconsistent with the actual state.

Consider this safer approach:

-return state.medias.values.expand((element) => element.values).toList();
+final currentMedias = state.medias.values.expand((element) => element.values).toList();
+return currentMedias;

Likely invalid or redundant comment.


272-275: Method signature change requires verification of all callers.

The return type change from Future<void> to Future<List<AppMedia>> is a breaking change that could affect existing callers.

Let's verify the impact:

✅ Verification successful

Return type change is backward compatible, no action needed.

The change from Future<void> to Future<List<AppMedia>> is safe as all 12 callers use "fire and forget" style invocation and don't depend on the return value.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for all calls to loadMedias to ensure they handle the returned List
rg "loadMedias\(" --type dart -B 2 -A 2

Length of output: 5237

app/lib/ui/flow/albums/albums_screen.dart (1)

30-34: Fix initState method order

The super.initState() should be called before any other initialization.

  @override
  void initState() {
-   _notifier = ref.read(albumStateNotifierProvider.notifier);
    super.initState();
+   _notifier = ref.read(albumStateNotifierProvider.notifier);
  }
app/assets/locales/app_en.arb (1)

128-128: Fix typo in localization key.

The key "album_tame_field_title" appears to be a typo and should be "album_name_field_title".

-  "album_tame_field_title": "Album Name",
+  "album_name_field_title": "Album Name",
.idea/libraries/Dart_Packages.xml (3)

1023-1039: Verify shared preferences compatibility

The shared preferences packages have been updated:

  • shared_preferences: 2.3.3 → 2.3.5
  • shared_preferences_android: 2.3.4 → 2.4.0
  • shared_preferences_foundation: 2.5.3 → 2.5.4

The android version bump from 2.3.4 to 2.4.0 indicates potential changes in functionality.

Run the following script to check for shared preferences usage that might be affected:

✅ Verification successful

Shared preferences update is safe to proceed

The update is safe because:

  • The codebase uses proper abstraction through provider pattern
  • Version constraints (^2.3.3) are compatible with the new version
  • No platform-specific implementation dependencies found
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for shared preferences usage patterns that might be affected by the update

# Test: Look for shared preferences initialization and usage
echo "Checking shared preferences usage..."
ast-grep --pattern 'SharedPreferences.$$$' || echo "No direct SharedPreferences usage found"

# Test: Look for platform-specific implementations
echo -e "\nChecking for platform-specific code..."
rg -l "shared_preferences_android|shared_preferences_foundation" || echo "No platform-specific imports found"

Length of output: 375


Script:

#!/bin/bash
# Check for shared preferences usage patterns
echo "Checking shared preferences imports and usage..."
rg "import.*shared_preferences" -A 3

echo -e "\nChecking pubspec.yaml for version constraints..."
cat app/pubspec.yaml | rg "shared_preferences:"

echo -e "\nChecking for method channel usage..."
rg "MethodChannel.*shared_preferences" -A 3

Length of output: 1352


Line range hint 100-155: Verify build package compatibility

The build-related packages have been updated to newer versions. While these are minor version updates and should maintain backward compatibility, it's good practice to verify the changes:

  • build: 2.4.1 → 2.4.2
  • build_config: 1.1.1 → 1.1.2
  • build_daemon: 4.0.2 → 4.0.3
  • build_resolvers: 2.4.2 → 2.4.3
  • build_runner: 2.4.13 → 2.4.14
  • build_runner_core: 7.3.2 → 8.0.0 (Minor version bump)

Run the following script to check for any breaking changes in the build configuration:

Also applies to: 487-500

✅ Verification successful

Build package updates are safe to proceed

The project actively uses code generation with multiple generated files in both app/ and data/ modules. The updates are mostly patch versions (backward compatible), and while build_runner_core has a minor version bump (7.3.2 → 8.0.0), it follows semantic versioning and should maintain compatibility. If any issues arise, the generated files can be regenerated using the build_watch command.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check build configuration and generated files

# Test: Look for build.yaml configuration
echo "Checking build configuration..."
cat build.yaml 2>/dev/null || echo "build.yaml not found"

# Test: Check for any custom build configurations
echo -e "\nChecking for custom build configurations..."
fd -e yaml -e yml | xargs rg "targets:|builders:" 2>/dev/null || echo "No custom build configurations found"

Length of output: 378


Script:

#!/bin/bash
# Check build dependencies and usage

# Check pubspec.yaml for build dependencies
echo "Checking pubspec.yaml for build dependencies..."
rg "build_runner|build:" -A 5 pubspec.yaml || echo "No build dependencies found in pubspec.yaml"

# Look for generated files
echo -e "\nChecking for generated files..."
fd ".g.dart$" || echo "No generated files found"

# Check for build_runner usage in code
echo -e "\nChecking for build_runner usage..."
rg "build_runner" || echo "No build_runner usage found"

Length of output: 1200


Line range hint 1-1710: Review package version conflicts

There are potential version conflicts in the following packages:

  1. flutter_lints has both 5.0.0 and 2.0.3 versions
  2. win32 has both 5.9.0 and 5.10.0 versions

Having multiple versions of the same package can lead to unexpected behavior.

Run the following script to check for potential compatibility issues:

✅ Verification successful

Multiple package versions are expected in this multi-module project

The different package versions found in .idea/libraries/Dart_Packages.xml are intentional and correct:

  • flutter_lints: Style module uses v2.x while data and app modules use v5.x
  • win32: Multiple versions are from transitive dependencies in platform-specific packages
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check pubspec.yaml for package version constraints and potential conflicts

# Test: Look for pubspec.yaml file
echo "Checking package versions in pubspec.yaml..."
cat pubspec.yaml 2>/dev/null || echo "pubspec.yaml not found"

# Test: Check for flutter_lints and win32 version constraints
echo -e "\nChecking specific package constraints..."
rg -A 1 "flutter_lints:|win32:" pubspec.yaml 2>/dev/null || echo "Package constraints not found"

Length of output: 385


Script:

#!/bin/bash
# Search for pubspec.yaml files in the repository
echo "Searching for pubspec.yaml files..."
fd pubspec.yaml

# If found, check the content focusing on the packages in question
echo -e "\nChecking package versions in found pubspec.yaml files..."
fd pubspec.yaml --exec cat {} \; 

Length of output: 3519

app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview_view_model.dart (3)

14-20: Good use of StateNotifierProvider.family for parameterized state management

Changing to StateNotifierProvider.family.autoDispose allows the provider to accept an AppMedia parameter, enhancing flexibility and enabling the state notifier to manage state based on specific media. This is a well-implemented improvement.


32-45: Efficient initialization based on media source

By passing AppMedia media to the constructor and initializing the state with it, the notifier can load images from the appropriate source depending on whether the media has a driveMediaRefId or dropboxMediaRefId. This conditional logic is clean and effectively handles media from different sources.


121-121: Including media in NetworkImagePreviewState enhances state completeness

Making AppMedia media a required field in NetworkImagePreviewState ensures that the state always has access to the associated media, improving consistency and reducing potential for errors.

app/lib/ui/flow/media_preview/components/local_media_image_preview.dart (1)

12-17: Addition of heroTag enhances hero animation control

Introducing the heroTag as a required parameter allows for more customizable hero animations, improving user experience by ensuring consistent and unique tags for Hero widgets.

app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview.dart (1)

14-20: LGTM! Constructor changes enhance hero animation control.

The addition of the required heroTag parameter improves the consistency and predictability of hero animations across the app.

app/lib/ui/flow/media_preview/components/download_require_view.dart (1)

Line range hint 14-26: LGTM! Constructor changes maintain hero animation consistency.

The addition of the required heroTag parameter aligns with the hero animation pattern used in NetworkImagePreview.

app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview_view_model.freezed.dart (1)

Line range hint 1-229: LGTM! Generated file follows Freezed conventions.

This is an auto-generated file by the Freezed package. The changes correctly reflect the addition of the required AppMedia field to the state class.

app/lib/ui/flow/media_preview/media_preview_screen.dart (1)

31-41: LGTM! New parameters enhance media preview functionality.

The addition of heroTag and onLoadMore parameters improves the user experience by enabling infinite scrolling and consistent hero animations.

Comment on lines +106 to +108
Future<void> loadAlbums() async {
if (state.loading) return;

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add loading state guard

The loading state check might not prevent race conditions in concurrent calls. Consider using a more robust solution.

+ bool _isLoading = false;
  Future<void> loadAlbums() async {
-   if (state.loading) return;
+   if (_isLoading || state.loading) return;
+   _isLoading = true;
    try {
      // ... existing code ...
    } finally {
+     _isLoading = false;
    }
  }

Committable suggestion skipped: line range outside the PR's diff.

Future<List<AppMedia>> loadMedia({bool reload = false}) async {
///TODO: remove deleted media
try {
if (state.loading) state.medias;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix incorrect state handling in loading check.

The loading check has incorrect logic and is missing a return statement.

-      if (state.loading) state.medias;
+      if (state.loading) return state.medias;
📝 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 (state.loading) state.medias;
if (state.loading) return state.medias;

Comment on lines +34 to +45
if (media.driveMediaRefId != null) {
loadImageFromGoogleDrive(
id: media.driveMediaRefId!,
extension: media.extension,
);
} else if (media.dropboxMediaRefId != null) {
loadImageFromDropbox(
id: media.dropboxMediaRefId!,
extension: media.extension,
);
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Handle scenario when media has no valid source references

If both media.driveMediaRefId and media.dropboxMediaRefId are null, neither loadImageFromGoogleDrive nor loadImageFromDropbox will be called, potentially leaving the image unloaded without any error handling or user feedback. Consider adding an else block to handle this scenario, perhaps setting an error state or providing a default behavior.

Apply this change to handle the scenario:

     } else if (media.dropboxMediaRefId != null) {
       loadImageFromDropbox(
         id: media.dropboxMediaRefId!,
         extension: media.extension,
       );
     }
+    else {
+      // Handle case when no media reference IDs are available
+      state = state.copyWith(
+        error: Exception('No valid media source available'),
+      );
+    }
📝 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 (media.driveMediaRefId != null) {
loadImageFromGoogleDrive(
id: media.driveMediaRefId!,
extension: media.extension,
);
} else if (media.dropboxMediaRefId != null) {
loadImageFromDropbox(
id: media.dropboxMediaRefId!,
extension: media.extension,
);
}
}
if (media.driveMediaRefId != null) {
loadImageFromGoogleDrive(
id: media.driveMediaRefId!,
extension: media.extension,
);
} else if (media.dropboxMediaRefId != null) {
loadImageFromDropbox(
id: media.dropboxMediaRefId!,
extension: media.extension,
);
}
else {
// Handle case when no media reference IDs are available
state = state.copyWith(
error: Exception('No valid media source available'),
);
}
}

Comment on lines +385 to +392
Future<void> changeVisibleMediaIndex(
int index,
Future<List<AppMedia>> Function() loadMoreMedia,
) async {
state = state.copyWith(currentIndex: index);

if (index == state.medias.length - 1) {
await _homeNotifier.loadMedias();
state = state.copyWith(medias: _getUpdatedMedias());
state = state.copyWith(medias: await loadMoreMedia());
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Ensure medias list is properly updated when loading more media

When loadMoreMedia() is called, the current implementation replaces the entire medias list in the state with the new list. This could result in losing the existing media items. If the intention is to append new media to the existing list, consider modifying the code to concatenate the lists.

Apply this diff to append new media:

     if (index == state.medias.length - 1) {
-      state = state.copyWith(medias: await loadMoreMedia());
+      final newMedias = await loadMoreMedia();
+      state = state.copyWith(medias: [...state.medias, ...newMedias]);
     }
📝 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
Future<void> changeVisibleMediaIndex(
int index,
Future<List<AppMedia>> Function() loadMoreMedia,
) async {
state = state.copyWith(currentIndex: index);
if (index == state.medias.length - 1) {
await _homeNotifier.loadMedias();
state = state.copyWith(medias: _getUpdatedMedias());
state = state.copyWith(medias: await loadMoreMedia());
Future<void> changeVisibleMediaIndex(
int index,
Future<List<AppMedia>> Function() loadMoreMedia,
) async {
state = state.copyWith(currentIndex: index);
if (index == state.medias.length - 1) {
final newMedias = await loadMoreMedia();
state = state.copyWith(medias: [...state.medias, ...newMedias]);
}

@@ -27,7 +29,7 @@ class LocalMediaImagePreview extends StatelessWidget {
: width;
return Center(
child: Hero(
tag: media,
tag: "$heroTag${media.toString()}",
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use a unique identifier for the Hero tag

Using media.toString() in the Hero widget's tag may not guarantee a unique or stable identifier, potentially causing issues with hero animations. It's recommended to use a unique property such as media.id:

Apply this diff to use a unique identifier:

     tag: "$heroTag${media.toString()}",
+    // Replace media.toString() with media.id for a unique and stable tag
-    tag: "$heroTag${media.toString()}",
+    tag: "$heroTag${media.id}",
📝 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
tag: "$heroTag${media.toString()}",
tag: "$heroTag${media.id}",

Comment on lines +197 to +200
onPageChanged: (value) => _notifier.changeVisibleMediaIndex(
value,
widget.onLoadMore,
),
Copy link

Choose a reason for hiding this comment

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

💡 Codebase verification

Error handling needed for async operations in changeVisibleMediaIndex

The implementation lacks error handling for the async loadMoreMedia() call, which could fail due to network issues or other errors. This could lead to uncaught exceptions and poor user experience.

  • app/lib/ui/flow/media_preview/media_preview_view_model.dart: Add try-catch block around the loadMoreMedia() call and state updates.
🔗 Analysis chain

Consider adding error handling for onLoadMore.

The onLoadMore callback might fail. Consider adding error handling and loading states.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check if error handling exists in the view model
ast-grep --pattern 'class MediaPreviewStateNotifier {
  $$$
  changeVisibleMediaIndex($_, $_) {
    $$$
    try {
      $$$
    } catch {
      $$$
    }
    $$$
  }
  $$$
}'

Length of output: 176


Script:

#!/bin/bash
# Find the implementation of changeVisibleMediaIndex
ast-grep --pattern 'changeVisibleMediaIndex($$$) {
  $$$
}'

# Look for error handling patterns in related files
rg "onLoadMore" -A 5 -B 5

Length of output: 6645


Script:

#!/bin/bash
# Find the state notifier file
fd "media_preview.*notifier\.dart$"

# Find the complete implementation
rg -A 20 "class MediaPreviewStateNotifier"

Length of output: 2023


Script:

#!/bin/bash
# Find the changeVisibleMediaIndex implementation
rg -A 15 "changeVisibleMediaIndex" app/lib/ui/flow/media_preview/media_preview_view_model.dart

Length of output: 548

Copy link

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

♻️ Duplicate comments (4)
app/lib/ui/flow/albums/albums_screen.dart (2)

31-34: ⚠️ Potential issue

Fix initState method order.

The super.initState() should be called before any other initialization to ensure proper widget lifecycle handling.

Apply this diff to fix the initialization order:

  @override
  void initState() {
-   _notifier = ref.read(albumStateNotifierProvider.notifier);
    super.initState();
+   _notifier = ref.read(albumStateNotifierProvider.notifier);
  }

112-150: 🛠️ Refactor suggestion

Add confirmation dialog before album deletion.

Currently, albums can be deleted without confirmation, which could lead to accidental deletions.

Add a confirmation dialog before deletion:

  onPressed: () {
    context.pop();
+   showDialog(
+     context: context,
+     builder: (context) => AlertDialog(
+       title: Text(context.l10n.delete_album_confirmation_title),
+       content: Text(context.l10n.delete_album_confirmation_message),
+       actions: [
+         TextButton(
+           onPressed: () => context.pop(),
+           child: Text(context.l10n.common_cancel),
+         ),
+         TextButton(
+           onPressed: () {
+             context.pop();
+             _notifier.deleteAlbum(album);
+           },
+           child: Text(context.l10n.common_delete),
+         ),
+       ],
+     ),
+   );
-   _notifier.deleteAlbum(album);
  },
app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart (2)

200-204: ⚠️ Potential issue

Fix incorrect loading state check.

The loading state check has incorrect syntax and is missing a return statement.

- //if (state.loading) state.medias;
+ if (state.loading) return state.medias;

213-254: 🛠️ Refactor suggestion

Refactor duplicate media loading logic.

The media loading logic is duplicated for each source type with only minor variations.

+ Future<List<AppMedia>> _loadMediaBatch(
+   List<AppMedia> currentMedias,
+   List<String> albumMedias,
+   Future<AppMedia?> Function(String) getMediaFn,
+   String Function(AppMedia) getRefId,
+ ) async {
+   final loadedMediaIds = currentMedias.map(getRefId).nonNulls.toList();
+   final moreMediaIds = albumMedias
+     .where((element) => !loadedMediaIds.contains(element))
+     .take(30)
+     .toList();
+   return Future.wait(
+     moreMediaIds.map(getMediaFn),
+   ).then((value) => value.nonNulls.toList());
+ }

  if (state.album.source == AppMediaSource.local) {
-   final loadedMediaIds = state.medias.map((e) => e.id).toList();
-   final moreMediaIds = state.album.medias
-       .where((element) => !loadedMediaIds.contains(element))
-       .take(30)
-       .toList();
-   medias = await Future.wait(
-     moreMediaIds.map(
-       (id) => _localMediaService.getMedia(id: id),
-     ),
-   ).then(
-     (value) => value.nonNulls.toList(),
-   );
+   medias = await _loadMediaBatch(
+     state.medias,
+     state.album.medias,
+     (id) => _localMediaService.getMedia(id: id),
+     (media) => media.id,
+   );
  } else if (state.album.source == AppMediaSource.googleDrive) {
    // Similar refactoring for other sources
  }
🧹 Nitpick comments (5)
app/lib/components/thumbnail_builder.dart (2)

11-11: LGTM! Consider adding documentation.

The type change from Object? to String? improves type safety and better aligns with Flutter's Hero widget requirements.

Consider adding documentation to explain the heroTag parameter usage and its impact on hero animations:

+ /// A unique string identifier for hero animations.
+ /// When null or empty, a default empty string is used as the hero tag.
  final String? heroTag;

Line range hint 31-32: Consider improving hero tag handling.

Using an empty string as a fallback for null hero tags could lead to unintended animations between unrelated widgets that share the same empty tag.

Consider one of these approaches:

  1. Generate a unique identifier when heroTag is null
  2. Conditionally wrap with Hero widget only when heroTag is non-null and non-empty
-      child: Hero(
-          tag: heroTag ?? '',
+      child: (heroTag?.isNotEmpty ?? false)
+          ? Hero(
+              tag: heroTag!,
+              child: Image(
+                // ... existing image configuration
+              ),
+            )
+          : Image(
+              // ... existing image configuration
+            ),
app/lib/ui/flow/media_selection/media_selection_screen.dart (2)

38-49: Improve error handling in _observeError.

The error handling implementation could be enhanced by clearing the error state after displaying the snackbar to prevent repeated displays of the same error.

 void _observeError(BuildContext context) {
   ref.listen(
     _provider.select(
       (value) => value.actionError,
     ),
     (previous, error) {
       if (error != null) {
         showErrorSnackBar(context: context, error: error);
+        _notifier.clearError();
       }
     },
   );
 }

148-157: Optimize grid layout calculation.

The grid layout calculation could be simplified and made more efficient by extracting the calculation logic and using a constant for the minimum item width.

+ static const double _minItemWidth = 100.0;
+ 
+ int _calculateCrossAxisCount(BuildContext context) {
+   final width = context.mediaQuerySize.width;
+   return width > 600 ? (width / 180).floor() : (width / _minItemWidth).floor();
+ }

  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
-   crossAxisCount: context.mediaQuerySize.width > 600
-       ? context.mediaQuerySize.width ~/ 180
-       : context.mediaQuerySize.width ~/ 100,
+   crossAxisCount: _calculateCrossAxisCount(context),
    crossAxisSpacing: 4,
    mainAxisSpacing: 4,
  ),
app/lib/ui/navigation/app_route.g.dart (1)

41-76: Consider adding route guards for authenticated routes.

The shell route contains sensitive sections (albums, accounts) that might need authentication. Consider adding route guards to protect these routes.

Example implementation:

RouteBase get $mainShellRoute => StatefulShellRouteData.$route(
      factory: $MainShellRouteExtension._fromState,
      guards: [AuthGuard()],  // Add authentication guard
      branches: [
        // ... existing branches
      ],
    );
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dc8b12e and 5f72865.

📒 Files selected for processing (7)
  • app/lib/components/thumbnail_builder.dart (1 hunks)
  • app/lib/ui/flow/albums/albums_screen.dart (1 hunks)
  • app/lib/ui/flow/albums/media_list/album_media_list_screen.dart (1 hunks)
  • app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart (1 hunks)
  • app/lib/ui/flow/media_selection/media_selection_screen.dart (1 hunks)
  • app/lib/ui/navigation/app_route.dart (5 hunks)
  • app/lib/ui/navigation/app_route.g.dart (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/lib/ui/flow/albums/media_list/album_media_list_screen.dart
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build
🔇 Additional comments (7)
app/lib/ui/flow/albums/albums_screen.dart (3)

36-45: LGTM!

The error handling logic is well-implemented, using Riverpod's ref.listen to observe errors and showing appropriate error snackbars.


47-68: LGTM!

The build method is well-structured with:

  • Proper error observation
  • Clear UI hierarchy using AppPage
  • Smooth transitions using FadeInSwitcher

70-90: LGTM!

The state handling is comprehensive and well-organized:

  • Loading state with progress indicator
  • Error state with retry option
  • Empty state with appropriate placeholder
app/lib/ui/navigation/app_route.dart (3)

21-29: LGTM!

The route paths are well-organized with:

  • Consistent naming conventions
  • Logical structure
  • Clear separation of concerns

43-76: LGTM!

The shell routes are well-structured with:

  • Logical branch organization (home, albums, transfer, accounts)
  • Proper use of TypedStatefulShellRoute
  • Clean implementation following go_router best practices

Line range hint 157-176: LGTM!

The route extensions are well-implemented with:

  • Consistent navigation methods (go, push, pushReplacement, replace)
  • Proper parameter handling
  • Correct location generation

Also applies to: 183-204, 264-284

app/lib/ui/flow/media_selection/media_selection_screen.dart (1)

51-56: 🛠️ Refactor suggestion

Consider moving provider initialization before super.initState().

The current initialization order might lead to potential issues as the provider is initialized after super.initState().

 @override
 void initState() {
+  super.initState();
   _provider = mediaSelectionStateNotifierProvider(widget.source);
   _notifier = ref.read(_provider.notifier);
-  super.initState();
 }

Likely invalid or redundant comment.

Comment on lines +181 to +199
return OnVisibleCallback(
onVisible: () {
runPostFrame(() {
_notifier.loadMedias();
});
},
child: FadeInSwitcher(
child: state.loading
? const Center(
child: Padding(
padding: EdgeInsets.all(16),
child: AppCircularProgressIndicator(
size: 20,
),
),
)
: const SizedBox(),
),
);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid using runPostFrame for loading more media.

Using runPostFrame for loading more media could lead to unnecessary frame delays. Consider using a simpler approach.

 return OnVisibleCallback(
   onVisible: () {
-    runPostFrame(() {
-      _notifier.loadMedias();
-    });
+    _notifier.loadMedias();
   },
   child: FadeInSwitcher(
     child: state.loading
         ? const Center(
             child: Padding(
               padding: EdgeInsets.all(16),
               child: AppCircularProgressIndicator(
                 size: 20,
               ),
             ),
           )
         : const SizedBox(),
   ),
 );
📝 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
return OnVisibleCallback(
onVisible: () {
runPostFrame(() {
_notifier.loadMedias();
});
},
child: FadeInSwitcher(
child: state.loading
? const Center(
child: Padding(
padding: EdgeInsets.all(16),
child: AppCircularProgressIndicator(
size: 20,
),
),
)
: const SizedBox(),
),
);
return OnVisibleCallback(
onVisible: () {
_notifier.loadMedias();
},
child: FadeInSwitcher(
child: state.loading
? const Center(
child: Padding(
padding: EdgeInsets.all(16),
child: AppCircularProgressIndicator(
size: 20,
),
),
)
: const SizedBox(),
),
);

Copy link

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

🔭 Outside diff range comments (1)
app/lib/ui/flow/media_metadata_details/media_metadata_details.dart (1)

Line range hint 14-17: Add missing heroTag parameter to constructor

According to the AI summary, the constructor should include an optional heroTag parameter. This parameter is missing in the implementation but is being used in the widget tree.

Apply this diff to align with the intended changes:

- const MediaMetadataDetailsScreen({super.key, required this.media});
+ const MediaMetadataDetailsScreen({
+   super.key,
+   required this.media,
+   String? heroTag,
+ });
🧹 Nitpick comments (16)
app/lib/ui/flow/home/home_screen_view_model.dart (2)

Line range hint 272-457: Consider breaking down this large method for better maintainability.

The loadMedias method is quite long (185+ lines) and handles multiple concerns (local media, Google Drive, Dropbox). Consider breaking it down into smaller, focused methods:

  • loadLocalMedias
  • loadGoogleDriveMedias
  • loadDropboxMedias
  • mergeMedias

This would improve readability, testability, and maintainability.

+  Future<List<AppMedia>> loadLocalMedias(bool hasLocalMediaAccess) async {
+    if (!hasLocalMediaAccess || _localMaxLoaded) return [];
+    
+    final localMedia = await _localMediaService.getLocalMedia(
+      start: _localMediaCount,
+      end: _localMediaCount + 30,
+    );
+    
+    _localMediaCount += localMedia.length;
+    if (localMedia.length < 30) {
+      _localMaxLoaded = true;
+    }
+    
+    return localMedia;
+  }
+
+  Future<List<AppMedia>> loadGoogleDriveMedias(bool hasInternet) async {
+    if (!_googleDriveMaxLoaded &&
+        state.googleAccount != null &&
+        _backUpFolderId != null &&
+        hasInternet) {
+      final res = await _googleDriveService.getPaginatedMedias(
+        folder: _backUpFolderId!,
+        nextPageToken: _googleDrivePageToken,
+        pageSize: 30,
+      );
+      _googleDriveMaxLoaded = res.nextPageToken == null;
+      _googleDrivePageToken = res.nextPageToken;
+
+      final gdMediaCollection = await splitLocalRefMedias(res.medias);
+      _googleDriveMediasWithLocalRef.addAll(gdMediaCollection.localRefMedias);
+      return gdMediaCollection.onlyCloudBasedMedias;
+    }
+    return [];
+  }

Line range hint 1-724: Consider splitting this large view model into smaller, focused components.

The view model has grown quite large and handles multiple complex operations (local media, Google Drive, Dropbox). Consider:

  1. Creating separate services/repositories for each media source
  2. Using a facade pattern to coordinate between different media sources
  3. Moving the media merging logic to a separate service

This would improve:

  • Separation of concerns
  • Code maintainability
  • Testing capabilities
  • Future extensibility
app/lib/components/thumbnail_builder.dart (1)

31-31: Consider adding unique prefix to hero tags

The hero tag is used directly in animations. To prevent conflicts across different screens, consider adding a unique prefix based on the screen or context.

Example:

-          tag: heroTag,
+          tag: "thumbnail_$heroTag",
app/lib/ui/flow/albums/component/album_item.dart (2)

35-59: Consider performance optimization for FadeInSwitcher

The FadeInSwitcher widget might impact performance when used in a list of albums. Consider:

  1. Using a simpler transition for better performance
  2. Implementing pagination if dealing with large lists

67-71: Optimize SVG asset loading

Loading SVG assets for each album item can be memory-intensive. Consider:

  1. Caching the SVG assets
  2. Using pre-rasterized images for better performance
  3. Using const widgets where possible

Also applies to: 75-79

app/lib/components/selection_menu.dart (1)

8-54: LGTM! Consider adding semantic labels.

The implementation is well-structured with good widget composition and consistent styling.

Consider adding semanticsLabel to improve accessibility:

 Text(
   title,
+  semanticsLabel: title,
   style: AppTextStyles.body.copyWith(
     color: context.colorScheme.textPrimary,
   ),
   textAlign: TextAlign.center,
   overflow: TextOverflow.visible,
 ),
app/lib/ui/flow/home/home_screen.dart (1)

202-203: Consider implementing lazy loading for better performance.

Loading all media items during preview might impact performance. Consider implementing lazy loading to fetch items only when needed.

app/lib/ui/flow/home/components/multi_selection_done_button.dart (1)

Line range hint 92-230: Consider extracting common dialog logic.

The confirmation dialog structure is repeated across actions. Consider extracting it into a reusable method.

Example:

void _showConfirmationDialog({
  required BuildContext context,
  required String title,
  required String message,
  required VoidCallback onConfirm,
  bool isDestructive = false,
}) {
  showAppAlertDialog(
    context: context,
    title: title,
    message: message,
    actions: [
      AppAlertAction(
        title: context.l10n.common_cancel,
        onPressed: () => context.pop(),
      ),
      AppAlertAction(
        isDestructiveAction: isDestructive,
        title: isDestructive ? context.l10n.common_delete : context.l10n.common_upload,
        onPressed: () {
          context.pop();
          onConfirm();
        },
      ),
    ],
  );
}
app/android/app/src/main/AndroidManifest.xml (1)

54-57: Consider deep linking strategy for album support

Since this PR adds album support, consider if deep linking might be needed for:

  1. Sharing albums via links
  2. Direct navigation to specific albums
  3. Future features like collaborative albums

If these features are planned, keeping deep linking enabled would be beneficial for the app's architecture.

Also applies to: 68-69

app/lib/ui/flow/media_selection/media_selection_screen.dart (1)

120-203: Improve performance by optimizing widget rebuilding.

The current implementation of the ListView.builder and nested GridView.builder within the itemBuilder could lead to performance issues due to excessive widget rebuilding, especially with large media lists. Consider refactoring to use ListView.separated or caching widgets where possible.

app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart (3)

3-3: Consider abstracting the logger dependency.

Instead of directly depending on the logger package, consider creating an abstract logging interface. This would make the code more testable and allow for different logging implementations.

-import 'package:logger/logger.dart';
+import 'package:data/log/logger_interface.dart';

Also applies to: 11-11


62-67: Simplify pagination logic.

The pagination logic using sublist and take operations is complex and could be simplified.

-final moreMediaIds = state.album.medias
-    .sublist(
-      (reload ? 0 : _loadedMediaCount),
-    )
-    .take(_loadedMediaCount + (reload ? _loadedMediaCount : 30))
-    .toList();
+final start = reload ? 0 : _loadedMediaCount;
+final count = reload ? _loadedMediaCount : 30;
+final moreMediaIds = state.album.medias
+    .skip(start)
+    .take(count)
+    .toList();

124-154: Add retry logic for network operations.

Network operations like loadAlbum could benefit from retry logic to handle temporary network issues.

+Future<T> _withRetry<T>(Future<T> Function() operation, {int maxAttempts = 3}) async {
+  int attempts = 0;
+  while (attempts < maxAttempts) {
+    try {
+      return await operation();
+    } catch (e) {
+      if (attempts == maxAttempts - 1) rethrow;
+      attempts++;
+      await Future.delayed(Duration(seconds: attempts));
+    }
+  }
+  throw StateError('Unreachable');
+}

 Future<void> loadAlbum() async {
   state = state.copyWith(actionError: null);
   List<Album> albums = [];
   try {
-    if (state.album.source == AppMediaSource.googleDrive) {
+    await _withRetry(() async {
+      if (state.album.source == AppMediaSource.googleDrive) {
         // ... existing code
-    }
+      }
+    });
data/lib/services/dropbox_services.dart (1)

233-255: Improve error handling in getMedia.

The error handling could be more specific about which errors are actually "not found" errors.

 } catch (e) {
   if (e is DioException &&
-      (e.response?.statusCode == 409 || e.response?.statusCode == 404)) {
+      ((e.response?.statusCode == 409 &&
+        e.response?.data?['error']?['.tag'] == 'not_found') ||
+       e.response?.statusCode == 404)) {
     return null;
   }
   rethrow;
 }
data/lib/services/google_drive_service.dart (2)

283-320: Cache album list response.

The getAlbums method could benefit from caching to reduce API calls.

+final _albumCache = <String, List<Album>>{};
+final _albumCacheExpiry = <String, DateTime>{};

 Future<List<Album>> getAlbums({required String folderId}) async {
+  final now = DateTime.now();
+  if (_albumCache.containsKey(folderId) &&
+      _albumCacheExpiry[folderId]!.isAfter(now)) {
+    return _albumCache[folderId]!;
+  }

   final res = await _client.req(
     // ... existing code
   );

   if (res.statusCode == 200) {
     // ... existing code
+    _albumCache[folderId] = albums;
+    _albumCacheExpiry[folderId] = now.add(Duration(minutes: 5));
     return albums;
   }

500-572: Add batch operations support.

The album removal could be optimized using Google Drive's batch operations.

+Future<void> removeAlbums({
+  required String folderId,
+  required List<String> ids,
+}) async {
+  if (ids.isEmpty) return;
+
+  final albums = await getAlbums(folderId: folderId);
+  albums.removeWhere((element) => ids.contains(element.id));
+
+  // Single update operation instead of multiple
+  await _updateAlbumFile(folderId, albums);
+}

 Future<void> removeAlbum({
   required String folderId,
   required String id,
 }) async {
-  // ... existing implementation
+  await removeAlbums(folderId: folderId, ids: [id]);
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5f72865 and e14aa75.

📒 Files selected for processing (17)
  • app/android/app/src/main/AndroidManifest.xml (1 hunks)
  • app/assets/locales/app_en.arb (2 hunks)
  • app/ios/Runner/Info.plist (1 hunks)
  • app/lib/components/selection_menu.dart (1 hunks)
  • app/lib/components/thumbnail_builder.dart (2 hunks)
  • app/lib/ui/flow/albums/component/album_item.dart (1 hunks)
  • app/lib/ui/flow/albums/media_list/album_media_list_screen.dart (1 hunks)
  • app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart (1 hunks)
  • app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.freezed.dart (1 hunks)
  • app/lib/ui/flow/home/components/multi_selection_done_button.dart (23 hunks)
  • app/lib/ui/flow/home/home_screen.dart (3 hunks)
  • app/lib/ui/flow/home/home_screen_view_model.dart (3 hunks)
  • app/lib/ui/flow/media_metadata_details/media_metadata_details.dart (1 hunks)
  • app/lib/ui/flow/media_selection/media_selection_screen.dart (1 hunks)
  • data/lib/domain/config.dart (1 hunks)
  • data/lib/services/dropbox_services.dart (6 hunks)
  • data/lib/services/google_drive_service.dart (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/lib/ui/flow/albums/media_list/album_media_list_screen.dart
  • data/lib/domain/config.dart
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build
🔇 Additional comments (16)
app/lib/ui/flow/home/home_screen_view_model.dart (2)

272-278: LGTM! Good optimization with early return.

The return type change and early return optimization improve the method's reusability and performance.


490-492: LGTM! Clear and concise implementation.

The clearSelection method follows good practices:

  • Clear naming that describes its purpose
  • Proper usage of immutable state updates
  • Single responsibility
app/lib/ui/flow/media_metadata_details/media_metadata_details.dart (1)

Line range hint 33-38: Verify hero animation consistency across screens

The hero animation changes are part of a larger feature. Let's verify the consistency of hero tags across related screens.

✅ Verification successful

Hero animation implementation is consistent

The hero tag "media_metadata_details${media.toString()}" follows the established pattern used across the app, ensuring proper animation transitions between screens. The implementation is verified to be correct.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for other AppMediaImage usages to verify hero tag implementation
ast-grep --pattern 'AppMediaImage(
  $$$
  heroTag: $_,
  $$$
)'

# Search for Hero widgets that might be related to media transitions
ast-grep --pattern 'Hero(
  $$$
  tag: $_,
  $$$
)'

Length of output: 120


Script:

#!/bin/bash
# Search for AppMediaImage usage
rg "AppMediaImage" -A 5

# Search for heroTag strings
rg "heroTag"

# Try simpler ast-grep pattern
ast-grep --pattern 'AppMediaImage'

# Search for Hero widget usage
rg "Hero\(" -A 2

Length of output: 14768

app/lib/components/thumbnail_builder.dart (1)

11-11: Breaking change: heroTag parameter type and nullability updated

The heroTag parameter has been changed from Object? to required String. While this change improves type safety, it requires updates to all existing AppMediaImage usages in the codebase.

Let's verify the impact:

Also applies to: 19-19

✅ Verification successful

✓ Verified: heroTag parameter change is safe

All existing AppMediaImage usages already provide string values for heroTag, either through string interpolation or parameter passing. The type change from Object? to String aligns with the actual usage patterns and improves type safety without breaking existing code.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for AppMediaImage usages that might need updates
rg "AppMediaImage\(" -A 5

Length of output: 2632

app/lib/ui/flow/albums/component/album_item.dart (1)

12-97: Well-structured album item implementation

The overall implementation is clean and well-organized:

  • Good separation of concerns with callback functions
  • Proper null handling for media
  • Consistent use of styling and theme extensions
  • Appropriate error boundaries
app/lib/ui/flow/home/home_screen.dart (2)

94-98: LGTM! Improved layout structure.

The Column layout provides better organization for the content and selection menu.


176-179: LGTM! Good use of clamp for responsive layout.

The column count calculation with clamp prevents layout issues on various screen sizes.

app/lib/ui/flow/home/components/multi_selection_done_button.dart (1)

Line range hint 17-77: LGTM! Well-structured menu implementation.

The refactor to HomeSelectionMenu improves code organization and reusability.

app/android/app/src/main/AndroidManifest.xml (1)

54-57: Review deep linking configuration inconsistency

The manifest has conflicting deep linking configurations:

  1. Intent filters are set up with android:autoVerify="true" for handling deep links
  2. Deep linking is explicitly disabled with flutter_deeplinking_enabled=false

This inconsistency could break:

  1. App link verification
  2. Authentication flow using the configured auth path prefix
  3. Potential album sharing features

Consider either:

  1. Removing the deep linking configuration if it's truly not needed
  2. Keeping deep linking enabled if it's required for auth or album sharing

Let's verify the intent filter usage:

app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.freezed.dart (1)

1-383: LGTM!

The auto-generated code correctly reflects the updates to the AlbumMediaListState class, including the new properties for state management. No issues found.

app/assets/locales/app_en.arb (1)

128-128: Fix possible typo: album_tame_field_title.

The key is "album_tame_field_title" but it should likely be "album_name_field_title". Please confirm and correct the key to ensure consistency and avoid confusion.

Apply this diff to correct the typo:

-  "album_tame_field_title": "Album Name",
+  "album_name_field_title": "Album Name",
app/lib/ui/flow/media_selection/media_selection_screen.dart (1)

184-186: Avoid using runPostFrame for loading more media.

Using runPostFrame to load more media introduces unnecessary complexity and potential delays. You can call _notifier.loadMedias() directly without wrapping it in runPostFrame.

Apply this diff to simplify the code:

-    runPostFrame(() {
-      _notifier.loadMedias();
-    });
+    _notifier.loadMedias();
app/lib/ui/flow/albums/media_list/album_media_list_state_notifier.dart (4)

323-336: LGTM! Clean and efficient implementation.

The media selection operations are well-implemented with clear state updates.


339-353: LGTM! Well-structured state definition.

The state class is properly defined using Freezed with appropriate default values and required fields.


71-86: 🛠️ Refactor suggestion

Refactor duplicate media loading logic.

The media loading logic is duplicated for each source type with only minor variations. Extract the common pattern into a reusable method.

+private Future<List<AppMedia>> _loadMediaBatch(
+  List<String> mediaIds,
+  Future<AppMedia?> Function(String) getMediaFn,
+  String Function(AppMedia) getRefId,
+) async {
+  final res = await Future.wait(
+    mediaIds.map(getMediaFn),
+  ).then((value) => value.nonNulls.toList());
+  return {for (final item in res) getRefId(item): item};
+}

-if (state.album.source == AppMediaSource.local) {
-  final res = await Future.wait(
-    moreMediaIds.map((id) => _localMediaService.getMedia(id: id)),
-  ).then((value) => value.nonNulls.toList());
-  medias = {for (final item in res) item.id: item};
-} else if (state.album.source == AppMediaSource.googleDrive) {
-  // ... similar code
-}
+medias = await _loadMediaBatch(
+  moreMediaIds,
+  (id) => _localMediaService.getMedia(id: id),
+  (media) => media.id,
+);

Likely invalid or redundant comment.


44-44: Consider deferring loadMedia call.

Calling loadMedia in the constructor could lead to race conditions if the UI isn't ready to handle state updates. Consider deferring this call to when the UI is mounted.

@@ -31,6 +31,7 @@ class MediaMetadataDetailsScreen extends StatelessWidget {
alignment: Alignment.center,
children: [
AppMediaImage(
heroTag: "media_metadata_details${media.toString()}",
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve hero tag generation for reliability

Using toString() for hero tags can be problematic as it might not guarantee uniqueness across different media items. Consider using a more reliable unique identifier.

Apply this diff to improve the hero tag implementation:

- heroTag: "media_metadata_details${media.toString()}",
+ heroTag: "media_metadata_details_${media.id}",

Also, consider extracting the prefix to a constant:

const String kMediaMetadataHeroTagPrefix = 'media_metadata_details_';

),
)
: AppMediaImage(
heroTag: "album${media.toString()}",
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid using toString() for hero tag generation

Using toString() on the media object for hero tags can be unstable and may not guarantee uniqueness. Consider using a more reliable identifier.

-                      heroTag: "album${media.toString()}",
+                      heroTag: "album_${media.id}",

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +56 to +108
class SelectionMenu extends StatelessWidget {
final List<Widget> items;
final bool useSystemPadding;
final bool show;

const SelectionMenu({
super.key,
required this.items,
this.useSystemPadding = true,
required this.show,
});

@override
Widget build(BuildContext context) {
return CrossFadeAnimation(
showChild: show,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: Align(
alignment: Alignment.bottomCenter,
child: Container(
padding: const EdgeInsets.only(bottom: 16, top: 24),
width: double.infinity,
decoration: BoxDecoration(
color: context.colorScheme.containerLowOnSurface,
border: Border(
top: BorderSide(
width: 1,
color: context.colorScheme.outline,
),
),
),
child: SafeArea(
top: false,
bottom: useSystemPadding,
left: false,
right: false,
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 4,
children: items,
),
),
),
),
),
),
);
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix invalid Row parameter.

The spacing parameter is not a valid property for Row widget.

Apply this fix:

 Row(
   crossAxisAlignment: CrossAxisAlignment.start,
-  spacing: 4,
   children: items,
 ),

If you need spacing between items, wrap each item with a Padding widget or use a custom Row implementation that supports spacing.

📝 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
class SelectionMenu extends StatelessWidget {
final List<Widget> items;
final bool useSystemPadding;
final bool show;
const SelectionMenu({
super.key,
required this.items,
this.useSystemPadding = true,
required this.show,
});
@override
Widget build(BuildContext context) {
return CrossFadeAnimation(
showChild: show,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: Align(
alignment: Alignment.bottomCenter,
child: Container(
padding: const EdgeInsets.only(bottom: 16, top: 24),
width: double.infinity,
decoration: BoxDecoration(
color: context.colorScheme.containerLowOnSurface,
border: Border(
top: BorderSide(
width: 1,
color: context.colorScheme.outline,
),
),
),
child: SafeArea(
top: false,
bottom: useSystemPadding,
left: false,
right: false,
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 4,
children: items,
),
),
),
),
),
),
);
}
}
class SelectionMenu extends StatelessWidget {
final List<Widget> items;
final bool useSystemPadding;
final bool show;
const SelectionMenu({
super.key,
required this.items,
this.useSystemPadding = true,
required this.show,
});
@override
Widget build(BuildContext context) {
return CrossFadeAnimation(
showChild: show,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: Align(
alignment: Alignment.bottomCenter,
child: Container(
padding: const EdgeInsets.only(bottom: 16, top: 24),
width: double.infinity,
decoration: BoxDecoration(
color: context.colorScheme.containerLowOnSurface,
border: Border(
top: BorderSide(
width: 1,
color: context.colorScheme.outline,
),
),
),
child: SafeArea(
top: false,
bottom: useSystemPadding,
left: false,
right: false,
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: items,
),
),
),
),
),
),
);
}
}

Comment on lines 409 to 430
Widget _shareAction(
BuildContext context,
Map<String, AppMedia> selectedMedias,
WidgetRef ref,
) {
return AppSheetAction(
return SelectionMenuAction(
icon: Icon(
Platform.isIOS ? CupertinoIcons.share : Icons.share_rounded,
color: context.colorScheme.textSecondary,
size: 24,
),
title: context.l10n.common_share,
onPressed: () {
onTap: () {
Share.shareXFiles(
selectedMedias.values
.where((element) => element.isLocalStored)
.map((e) => XFile(e.path))
.toList(),
);
context.pop();
ref.read(homeViewStateNotifier.notifier).clearSelection();
},
);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling for no local files.

The share action should handle cases where no local files are available for sharing.

Example:

 onTap: () {
+  final localFiles = selectedMedias.values
+      .where((element) => element.isLocalStored)
+      .map((e) => XFile(e.path))
+      .toList();
+  
+  if (localFiles.isEmpty) {
+    showErrorSnackBar(
+      context: context,
+      error: context.l10n.no_local_files_to_share,
+    );
+    return;
+  }
+
   Share.shareXFiles(
-    selectedMedias.values
-        .where((element) => element.isLocalStored)
-        .map((e) => XFile(e.path))
-        .toList(),
+    localFiles,
   );
   ref.read(homeViewStateNotifier.notifier).clearSelection();
 },

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +68 to +69
<key>FlutterDeepLinkingEnabled</key>
<false/>
Copy link

Choose a reason for hiding this comment

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

💡 Codebase verification

Deep linking must remain enabled for Dropbox authentication

Disabling deep linking will break the Dropbox OAuth flow. The DeepLinkHandler is actively used to:

  • Process OAuth redirect URLs with authentication codes
  • Set up Dropbox tokens and user accounts
  • Handle both initial and runtime authentication callbacks

Please remove the FlutterDeepLinkingEnabled key or set it to true.

🔗 Analysis chain

Verify the implications of disabling deep linking

Disabling deep linking while having URL schemes configured seems contradictory and might affect:

  1. Google Sign-In functionality which relies on URL schemes
  2. Authentication flow using the "cloudgallery://" scheme
  3. Future album sharing capabilities that might need deep links

Please verify if deep linking should really be disabled or if this was added unintentionally.

Let's verify the deep linking usage:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for deep link handling code in the codebase
echo "Searching for deep link related code..."
rg -i "deep.*link|url.*scheme|initial.*link|uni.*link|link.*stream" -t dart

# Search for auth-related navigation that might use deep links
echo "Searching for auth-related navigation..."
rg -i "navigate.*auth|auth.*route|sign.*in.*route" -t dart

Length of output: 1330


Script:

#!/bin/bash
# Check if deep linking code is recently modified or active
echo "Checking recent changes to deep linking files..."
git log --since="3 months ago" --pretty=format:"%h %ad %s" --date=short -- app/lib/domain/handlers/deep_links_handler.dart

# Check for actual usage of DeepLinkHandler
echo -e "\nChecking for DeepLinkHandler usage..."
rg -l "DeepLinkHandler" --type dart

# Check the deep links handler implementation
echo -e "\nChecking deep links handler implementation..."
cat app/lib/domain/handlers/deep_links_handler.dart

Length of output: 2251

Comment on lines +108 to +120
} catch (e, s) {
state = state.copyWith(
loading: false,
loadingMore: false,
error: state.medias.isEmpty ? e : null,
actionError: state.medias.isNotEmpty ? e : null,
);
_logger.e(
"AlbumMediaListStateNotifier: Error loading medias",
error: e,
stackTrace: s,
);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve error handling with specific error types.

The catch block catches all errors without distinguishing between different types. Consider handling specific error types differently.

-} catch (e, s) {
+} catch (e, s) {
+  final error = e is AppError ? e : SomethingWentWrongError(
+    message: e.toString(),
+  );
   state = state.copyWith(
     loading: false,
     loadingMore: false,
-    error: state.medias.isEmpty ? e : null,
-    actionError: state.medias.isNotEmpty ? e : null,
+    error: state.medias.isEmpty ? error : null,
+    actionError: state.medias.isNotEmpty ? error : null,
   );
   _logger.e(
     "AlbumMediaListStateNotifier: Error loading medias",
-    error: e,
+    error: error,
     stackTrace: s,
   );

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +186 to +256
Future<void> addMediaInAlbum({
required List<String> medias,
}) async {
state = state.copyWith(
actionError: null,
addingMedia: [...state.addingMedia, ...medias],
);
try {
//Remove duplicate media ids
final updatedMedias = {...state.album.medias, ...medias}.toList();

Map<String, AppMedia> moreMedia = {};

if (state.album.source == AppMediaSource.local) {
await _localMediaService.updateAlbum(
state.album.copyWith(medias: updatedMedias),
);

final res = await Future.wait(
medias.map((id) => _localMediaService.getMedia(id: id)),
).then((value) => value.nonNulls.toList());

moreMedia = {for (final item in res) item.id: item};
} else if (state.album.source == AppMediaSource.googleDrive) {
_backupFolderId ??= await _googleDriveService.getBackUpFolderId();
if (_backupFolderId == null) {
throw BackUpFolderNotFound();
}
await _googleDriveService.updateAlbum(
folderId: _backupFolderId!,
album: state.album.copyWith(medias: updatedMedias),
);
final res = await Future.wait(
medias.map((id) => _googleDriveService.getMedia(id: id)),
).then((value) => value.nonNulls.toList());

moreMedia = {for (final item in res) item.driveMediaRefId!: item};
} else if (state.album.source == AppMediaSource.dropbox) {
await _dropboxService.updateAlbum(
state.album.copyWith(medias: updatedMedias),
);
final res = await Future.wait(
medias.map((id) => _dropboxService.getMedia(id: id)),
).then((value) => value.nonNulls.toList());
moreMedia = {for (final item in res) item.dropboxMediaRefId!: item};
}

state = state.copyWith(
addingMedia: state.addingMedia
.where(
(element) => !medias.contains(element),
)
.toList(),
medias: {...state.medias, ...moreMedia},
);
} catch (e, s) {
state = state.copyWith(
actionError: e,
addingMedia: state.addingMedia
.where(
(element) => !medias.contains(element),
)
.toList(),
);
_logger.e(
"AlbumMediaListStateNotifier: Error while adding media",
error: e,
stackTrace: s,
);
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Protect against concurrent operations.

The addMediaInAlbum method doesn't protect against concurrent operations which could lead to race conditions.

+bool _isAddingMedia = false;

 Future<void> addMediaInAlbum({
   required List<String> medias,
 }) async {
+  if (_isAddingMedia) return;
+  _isAddingMedia = true;
   try {
     state = state.copyWith(
       actionError: null,
       addingMedia: [...state.addingMedia, ...medias],
     );
     // ... existing code
   } finally {
+    _isAddingMedia = 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
Future<void> addMediaInAlbum({
required List<String> medias,
}) async {
state = state.copyWith(
actionError: null,
addingMedia: [...state.addingMedia, ...medias],
);
try {
//Remove duplicate media ids
final updatedMedias = {...state.album.medias, ...medias}.toList();
Map<String, AppMedia> moreMedia = {};
if (state.album.source == AppMediaSource.local) {
await _localMediaService.updateAlbum(
state.album.copyWith(medias: updatedMedias),
);
final res = await Future.wait(
medias.map((id) => _localMediaService.getMedia(id: id)),
).then((value) => value.nonNulls.toList());
moreMedia = {for (final item in res) item.id: item};
} else if (state.album.source == AppMediaSource.googleDrive) {
_backupFolderId ??= await _googleDriveService.getBackUpFolderId();
if (_backupFolderId == null) {
throw BackUpFolderNotFound();
}
await _googleDriveService.updateAlbum(
folderId: _backupFolderId!,
album: state.album.copyWith(medias: updatedMedias),
);
final res = await Future.wait(
medias.map((id) => _googleDriveService.getMedia(id: id)),
).then((value) => value.nonNulls.toList());
moreMedia = {for (final item in res) item.driveMediaRefId!: item};
} else if (state.album.source == AppMediaSource.dropbox) {
await _dropboxService.updateAlbum(
state.album.copyWith(medias: updatedMedias),
);
final res = await Future.wait(
medias.map((id) => _dropboxService.getMedia(id: id)),
).then((value) => value.nonNulls.toList());
moreMedia = {for (final item in res) item.dropboxMediaRefId!: item};
}
state = state.copyWith(
addingMedia: state.addingMedia
.where(
(element) => !medias.contains(element),
)
.toList(),
medias: {...state.medias, ...moreMedia},
);
} catch (e, s) {
state = state.copyWith(
actionError: e,
addingMedia: state.addingMedia
.where(
(element) => !medias.contains(element),
)
.toList(),
);
_logger.e(
"AlbumMediaListStateNotifier: Error while adding media",
error: e,
stackTrace: s,
);
}
}
bool _isAddingMedia = false;
Future<void> addMediaInAlbum({
required List<String> medias,
}) async {
if (_isAddingMedia) return;
_isAddingMedia = true;
try {
state = state.copyWith(
actionError: null,
addingMedia: [...state.addingMedia, ...medias],
);
try {
//Remove duplicate media ids
final updatedMedias = {...state.album.medias, ...medias}.toList();
Map<String, AppMedia> moreMedia = {};
if (state.album.source == AppMediaSource.local) {
await _localMediaService.updateAlbum(
state.album.copyWith(medias: updatedMedias),
);
final res = await Future.wait(
medias.map((id) => _localMediaService.getMedia(id: id)),
).then((value) => value.nonNulls.toList());
moreMedia = {for (final item in res) item.id: item};
} else if (state.album.source == AppMediaSource.googleDrive) {
_backupFolderId ??= await _googleDriveService.getBackUpFolderId();
if (_backupFolderId == null) {
throw BackUpFolderNotFound();
}
await _googleDriveService.updateAlbum(
folderId: _backupFolderId!,
album: state.album.copyWith(medias: updatedMedias),
);
final res = await Future.wait(
medias.map((id) => _googleDriveService.getMedia(id: id)),
).then((value) => value.nonNulls.toList());
moreMedia = {for (final item in res) item.driveMediaRefId!: item};
} else if (state.album.source == AppMediaSource.dropbox) {
await _dropboxService.updateAlbum(
state.album.copyWith(medias: updatedMedias),
);
final res = await Future.wait(
medias.map((id) => _dropboxService.getMedia(id: id)),
).then((value) => value.nonNulls.toList());
moreMedia = {for (final item in res) item.dropboxMediaRefId!: item};
}
state = state.copyWith(
addingMedia: state.addingMedia
.where(
(element) => !medias.contains(element),
)
.toList(),
medias: {...state.medias, ...moreMedia},
);
} catch (e, s) {
state = state.copyWith(
actionError: e,
addingMedia: state.addingMedia
.where(
(element) => !medias.contains(element),
)
.toList(),
);
_logger.e(
"AlbumMediaListStateNotifier: Error while adding media",
error: e,
stackTrace: s,
);
}
} finally {
_isAddingMedia = false;
}
}

Comment on lines +396 to +423
Future<List<Album>> getAlbums() async {
try {
final res = await _dropboxAuthenticatedDio.req(
DropboxDownloadEndpoint(
filePath: "/${ProviderConstants.backupFolderName}/Albums.json",
),
);
if (res.statusCode != 200 || res.data is! ResponseBody) {
throw SomethingWentWrongError(
statusCode: res.statusCode,
message: res.statusMessage,
);
}
final List<int> bytes = [];
await for (final chunk in (res.data as ResponseBody).stream) {
bytes.addAll(chunk);
}
final json = jsonDecode(utf8.decode(bytes));
return json is! List
? <Album>[]
: json.map((e) => Album.fromJson(e)).toList();
} catch (e) {
if (e is DioException && e.response?.statusCode == 409) {
return <Album>[];
}
rethrow;
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add validation for album data.

The album operations don't validate the album data before saving, which could lead to corrupted data.

+void _validateAlbum(Album album) {
+  if (album.id.isEmpty) {
+    throw ValidationError('Album ID cannot be empty');
+  }
+  if (album.name.isEmpty) {
+    throw ValidationError('Album name cannot be empty');
+  }
+  if (album.created_at == null) {
+    throw ValidationError('Album creation date is required');
+  }
+}

 Future<void> createAlbum(Album album) async {
+  _validateAlbum(album);
   final albums = await getAlbums();
   // ... rest of the code

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +425 to +449
Future<void> createAlbum(Album album) async {
final albums = await getAlbums();
albums.add(album);
albums.sort((a, b) => b.created_at.compareTo(a.created_at));

final res = await _dropboxAuthenticatedDio.req(
DropboxUploadEndpoint(
mode: 'overwrite',
autoRename: false,
content: AppMediaContent(
stream: Stream.value(utf8.encode(jsonEncode(albums))),
length: utf8.encode(jsonEncode(albums)).length,
contentType: 'application/octet-stream',
),
filePath: "/${ProviderConstants.backupFolderName}/Albums.json",
),
);

if (res.statusCode == 200) return;

throw SomethingWentWrongError(
statusCode: res.statusCode,
message: res.statusMessage,
);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add concurrency control for album operations.

Multiple simultaneous album operations could lead to race conditions when updating the Albums.json file.

Consider implementing optimistic locking:

+private String? _albumsEtag;

 Future<void> createAlbum(Album album) async {
+  final currentEtag = _albumsEtag;
   final albums = await getAlbums();
   albums.add(album);
   albums.sort((a, b) => b.created_at.compareTo(a.created_at));

   final res = await _dropboxAuthenticatedDio.req(
     DropboxUploadEndpoint(
       mode: 'overwrite',
       autoRename: false,
+      ifMatch: currentEtag,
       content: AppMediaContent(
         stream: Stream.value(utf8.encode(jsonEncode(albums))),
         length: utf8.encode(jsonEncode(albums)).length,
         contentType: 'application/octet-stream',
       ),
       filePath: "/${ProviderConstants.backupFolderName}/Albums.json",
     ),
   );

+  if (res.statusCode == 412) {
+    throw ConcurrentModificationError();
+  }

   if (res.statusCode == 200) {
+    _albumsEtag = res.headers.value('etag');
     return;
   }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +30 to +64
Future<String?> getBackUpFolderId() async {
final res = await _client.req(
GoogleDriveListEndpoint(
q: "name='${ProviderConstants.backupFolderName}' and trashed=false and mimeType='application/vnd.google-apps.folder'",
pageSize: 1,
),
);

if (res.statusCode == 200) {
final body = drive.FileList.fromJson(res.data);
if (body.files?.isNotEmpty ?? false) {
return body.files?.first.id;
} else {
final createRes = await _client.req(
GoogleDriveCreateFolderEndpoint(
name: ProviderConstants.backupFolderName,
),
);

if (createRes.statusCode == 200) {
return drive.File.fromJson(createRes.data).id;
}

throw SomethingWentWrongError(
statusCode: createRes.statusCode,
message: createRes.statusMessage,
);
}
}

throw SomethingWentWrongError(
statusCode: res.statusCode,
message: res.statusMessage,
);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve backup folder handling.

The backup folder creation logic could be more robust by checking folder permissions.

 Future<String?> getBackUpFolderId() async {
+  // First check if we have permission to access/create folders
+  try {
+    final permissionRes = await _client.req(
+      GoogleDriveAboutEndpoint(fields: 'user,storageQuota'),
+    );
+    if (permissionRes.statusCode != 200) {
+      throw InsufficientPermissionsError();
+    }
+  } catch (e) {
+    _logger.e('Failed to check Drive permissions', error: e);
+    rethrow;
+  }

   final res = await _client.req(
     // ... rest of the code

Committable suggestion skipped: line range outside the PR's diff.

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

Successfully merging this pull request may close these issues.

2 participants