From ccb96dc2e4857c778a102c7da542125ed260605e Mon Sep 17 00:00:00 2001 From: Pratik Canopas <109139581+cp-pratik-k@users.noreply.github.com> Date: Mon, 9 Dec 2024 11:59:02 +0530 Subject: [PATCH] Dropbox Support (#57) * Implement backup folder * Implement backup folder * Implement backup folder * Implement upload and download for dropbox * Implement upload and download for dropbox * Implement delete media in dropbox * Minor fix * Minor fix * Fix upload error * Implement dropbox * Implement dropbox * Improve upload and download * Improve upload and download * Implement auto backup feature in google drive * Improve repo code * Implemented database update timer * Implement dropbox metadata * Implement dropbox metadata * Did some minor improvement * Did some minor improvement * Fix home screen error * Fix home screen error * Cache media into local storage and prevent memory usage * Cache media into local storage and prevent memory usage * Add dropbox preview * Improve l10n * Improve minor ui * Show dropbox preview * Minor improvement * Added debounce on database update * Create backup folder automated * Create backup folder automated * Implement create-dropbox-backup-folder * Remove secrets * Update workflow * Fix notification icon issue * Minor improvement * Remove unused import * Minor improvement --- .github/workflows/analyze.yml | 3 + .github/workflows/android_build.yml | 3 + .github/workflows/android_deploy.yml | 3 + .github/workflows/ios_deploy.yml | 4 +- .idea/libraries/Dart_Packages.xml | 200 ++-- .idea/libraries/Flutter_Plugins.xml | 55 +- .idea/modules.xml | 1 + app/android/app/src/main/AndroidManifest.xml | 3 + .../cloud_gallery_logo.png | Bin .../main/res/drawable/cloud_gallery_logo.png | Bin 0 -> 26297 bytes app/assets/images/icons/dropbox.svg | 4 - app/assets/images/icons/ic_dropbox.svg | 11 + .../{google-drive.svg => ic_google_drive.svg} | 0 app/assets/locales/app_en.arb | 120 ++- app/ios/Podfile.lock | 16 +- .../components/app_media_image_provider.dart | 120 --- app/lib/components/thumbnail_builder.dart | 16 +- app/lib/domain/assets/assets_paths.dart | 14 - .../extensions/media_list_extension.dart | 75 -- .../domain/handlers/deep_links_handler.dart | 1 + .../app_cached_network_image_provider.dart | 102 ++ .../app_media_image_provider.dart | 204 ++++ .../dropbox_image_provider.dart | 143 +++ .../local_asset_thunbnail_image_provider.dart | 104 ++ app/lib/gen/assets.gen.dart | 124 +++ app/lib/ui/app.dart | 23 + app/lib/ui/flow/accounts/accounts_screen.dart | 38 +- .../accounts/accounts_screen_view_model.dart | 55 +- .../accounts_screen_view_model.freezed.dart | 45 +- .../components/settings_action_list.dart | 64 +- .../flow/home/components/app_media_item.dart | 270 +++-- app/lib/ui/flow/home/components/hints.dart | 2 +- .../multi_selection_done_button.dart | 572 +++++++---- .../no_local_medias_access_screen.dart | 14 +- app/lib/ui/flow/home/home_screen.dart | 266 ++--- .../ui/flow/home/home_screen_view_model.dart | 730 +++++++++----- .../home/home_screen_view_model.freezed.dart | 269 +++-- .../home/home_view_model_helper_mixin.dart | 102 +- .../media_metadata_details.dart | 11 +- .../components/download_require_view.dart | 35 +- .../components/image_preview_screen.dart | 19 +- .../network_image_preview.dart | 10 +- .../network_image_preview_view_model.dart | 44 +- .../media_preview/components/top_bar.dart | 788 ++++++++++----- .../media_preview/media_preview_screen.dart | 83 +- .../media_preview_view_model.dart | 293 ++++-- .../media_preview_view_model.freezed.dart | 235 +++-- .../components/transfer_item.dart | 257 +++-- .../media_transfer/media_transfer_screen.dart | 48 +- .../media_transfer_view_model.dart | 37 +- .../media_transfer_view_model.freezed.dart | 99 +- app/lib/ui/flow/onboard/onboard_screen.dart | 4 +- app/pubspec.lock | 284 ++++-- app/pubspec.yaml | 4 + data/.flutter-plugins | 5 + data/.flutter-plugins-dependencies | 2 +- data/.gitignore | 3 + .../apis/dropbox/dropbox_auth_endpoints.dart | 80 ++ .../dropbox/dropbox_content_endpoints.dart | 289 ++++++ .../google_drive/google_drive_endpoint.dart | 39 +- data/lib/apis/network/client.dart | 107 +- .../dropbox_auth_interceptor.dart | 82 +- .../interceptors/logger_interceptor.dart | 76 ++ data/lib/apis/network/secrets.dart | 4 - data/lib/domain/config.dart | 12 +- .../domain/formatters}/byte_formatter.dart | 0 .../lib}/handlers/notification_handler.dart | 68 +- data/lib/log/logger.dart | 21 + data/lib/models/app_process/app_process.dart | 54 - .../app_process/app_process.freezed.dart | 431 -------- .../account}/dropbox_account.dart | 0 .../account}/dropbox_account.freezed.dart | 0 .../account}/dropbox_account.g.dart | 0 .../models/dropbox/entity/dropbox_entity.dart | 40 + .../entity/dropbox_entity.freezed.dart | 259 +++++ .../dropbox/entity/dropbox_entity.g.dart | 33 + .../models/dropbox/token/dropbox_token.dart | 22 + .../token/dropbox_token.freezed.dart} | 13 +- .../token/dropbox_token.g.dart} | 7 +- data/lib/models/media/media.dart | 43 +- data/lib/models/media/media.freezed.dart | 24 +- data/lib/models/media/media.g.dart | 3 + data/lib/models/media/media_extension.dart | 54 + .../models/media_process/media_process.dart | 142 +++ .../media_process/media_process.freezed.dart | 831 +++++++++++++++ .../models/media_process/media_process.g.dart | 100 ++ data/lib/models/token/token.dart | 40 - .../google_drive_process_repo.dart | 353 ------- .../media_process_repository.dart | 952 ++++++++++++++++++ data/lib/services/auth_service.dart | 33 +- data/lib/services/cloud_provider_service.dart | 49 + data/lib/services/dropbox_services.dart | 301 +++++- data/lib/services/google_drive_service.dart | 209 +++- data/lib/storage/app_preferences.dart | 9 +- data/pubspec.yaml | 7 + style/lib/animations/item_selector.dart | 59 -- style/lib/callback/on_visible_callback.dart | 28 + 97 files changed, 7793 insertions(+), 3118 deletions(-) rename app/android/app/src/main/res/{drawable-xxxhdpi => drawable-v21}/cloud_gallery_logo.png (100%) create mode 100644 app/android/app/src/main/res/drawable/cloud_gallery_logo.png delete mode 100644 app/assets/images/icons/dropbox.svg create mode 100644 app/assets/images/icons/ic_dropbox.svg rename app/assets/images/icons/{google-drive.svg => ic_google_drive.svg} (100%) delete mode 100644 app/lib/components/app_media_image_provider.dart delete mode 100644 app/lib/domain/assets/assets_paths.dart delete mode 100644 app/lib/domain/extensions/media_list_extension.dart create mode 100644 app/lib/domain/image_providers/app_cached_network_image_provider.dart create mode 100644 app/lib/domain/image_providers/app_media_image_provider.dart create mode 100644 app/lib/domain/image_providers/dropbox_image_provider.dart create mode 100644 app/lib/domain/image_providers/local_asset_thunbnail_image_provider.dart create mode 100644 app/lib/gen/assets.gen.dart create mode 100644 data/lib/apis/dropbox/dropbox_content_endpoints.dart create mode 100644 data/lib/apis/network/interceptors/logger_interceptor.dart delete mode 100644 data/lib/apis/network/secrets.dart rename {app/lib/domain/formatter => data/lib/domain/formatters}/byte_formatter.dart (100%) rename {app/lib/domain => data/lib}/handlers/notification_handler.dart (56%) create mode 100644 data/lib/log/logger.dart delete mode 100644 data/lib/models/app_process/app_process.dart delete mode 100644 data/lib/models/app_process/app_process.freezed.dart rename data/lib/models/{dropbox_account => dropbox/account}/dropbox_account.dart (100%) rename data/lib/models/{dropbox_account => dropbox/account}/dropbox_account.freezed.dart (100%) rename data/lib/models/{dropbox_account => dropbox/account}/dropbox_account.g.dart (100%) create mode 100644 data/lib/models/dropbox/entity/dropbox_entity.dart create mode 100644 data/lib/models/dropbox/entity/dropbox_entity.freezed.dart create mode 100644 data/lib/models/dropbox/entity/dropbox_entity.g.dart create mode 100644 data/lib/models/dropbox/token/dropbox_token.dart rename data/lib/models/{token/token.freezed.dart => dropbox/token/dropbox_token.freezed.dart} (96%) rename data/lib/models/{token/token.g.dart => dropbox/token/dropbox_token.g.dart} (82%) create mode 100644 data/lib/models/media_process/media_process.dart create mode 100644 data/lib/models/media_process/media_process.freezed.dart create mode 100644 data/lib/models/media_process/media_process.g.dart delete mode 100644 data/lib/models/token/token.dart delete mode 100644 data/lib/repositories/google_drive_process_repo.dart create mode 100644 data/lib/repositories/media_process_repository.dart create mode 100644 data/lib/services/cloud_provider_service.dart delete mode 100644 style/lib/animations/item_selector.dart create mode 100644 style/lib/callback/on_visible_callback.dart diff --git a/.github/workflows/analyze.yml b/.github/workflows/analyze.yml index 783cf28..877e8ef 100644 --- a/.github/workflows/analyze.yml +++ b/.github/workflows/analyze.yml @@ -22,6 +22,7 @@ jobs: GOOGLE_SERVICES_JSON_BASE64: ${{ secrets.GOOGLE_SERVICES_JSON_BASE64 }} GOOGLE_SERVICE_INFO_PLIST_BASE64: ${{ secrets.GOOGLE_SERVICE_INFO_PLIST_BASE64 }} FIREBASE_APP_ID_FILE_JSON_BASE64: ${{ secrets.FIREBASE_APP_ID_FILE_JSON_BASE64 }} + APP_SECRETS_BASE64: ${{ secrets.APP_SECRETS_BASE64 }} run: | cd app @@ -29,6 +30,8 @@ jobs: echo $GOOGLE_SERVICES_JSON_BASE64 | base64 -di > android/app/google-services.json echo $GOOGLE_SERVICE_INFO_PLIST_BASE64 | base64 --decode > ios/Runner/GoogleService-Info.plist echo $FIREBASE_APP_ID_FILE_JSON_BASE64 | base64 --decode > ios/firebase_app_id_file.json + cd ../data + echo $APP_SECRETS_BASE64 | base64 --decode > lib/apis/network/secrets.dart - name: Install dependencies run: | diff --git a/.github/workflows/android_build.yml b/.github/workflows/android_build.yml index aa6002d..f7bdad4 100644 --- a/.github/workflows/android_build.yml +++ b/.github/workflows/android_build.yml @@ -33,10 +33,13 @@ jobs: env: FIREBASE_OPTIONS_BASE64: ${{ secrets.FIREBASE_OPTIONS_BASE64 }} GOOGLE_SERVICES_JSON_BASE64: ${{ secrets.GOOGLE_SERVICES_JSON_BASE64 }} + APP_SECRETS_BASE64: ${{ secrets.APP_SECRETS_BASE64 }} run: | cd app echo $FIREBASE_OPTIONS_BASE64 | base64 -di > lib/firebase_options.dart echo $GOOGLE_SERVICES_JSON_BASE64 | base64 -di > android/app/google-services.json + cd ../data + echo $APP_SECRETS_BASE64 | base64 --decode > lib/apis/network/secrets.dart - name: Install dependencies run: | diff --git a/.github/workflows/android_deploy.yml b/.github/workflows/android_deploy.yml index ce979ae..1e6bcba 100644 --- a/.github/workflows/android_deploy.yml +++ b/.github/workflows/android_deploy.yml @@ -38,10 +38,13 @@ jobs: env: FIREBASE_OPTIONS_BASE64: ${{ secrets.FIREBASE_OPTIONS_BASE64 }} GOOGLE_SERVICES_JSON_BASE64: ${{ secrets.GOOGLE_SERVICES_JSON_BASE64 }} + APP_SECRETS_BASE64: ${{ secrets.APP_SECRETS_BASE64 }} run: | cd app echo $FIREBASE_OPTIONS_BASE64 | base64 -di > lib/firebase_options.dart echo $GOOGLE_SERVICES_JSON_BASE64 | base64 -di > android/app/google-services.json + cd ../data + echo $APP_SECRETS_BASE64 | base64 --decode > lib/apis/network/secrets.dart - name: Install dependencies run: | diff --git a/.github/workflows/ios_deploy.yml b/.github/workflows/ios_deploy.yml index d03ed73..4eabde2 100644 --- a/.github/workflows/ios_deploy.yml +++ b/.github/workflows/ios_deploy.yml @@ -34,12 +34,14 @@ jobs: FIREBASE_OPTIONS_BASE64: ${{ secrets.FIREBASE_OPTIONS_BASE64 }} GOOGLE_SERVICE_INFO_PLIST_BASE64: ${{ secrets.GOOGLE_SERVICE_INFO_PLIST_BASE64 }} FIREBASE_APP_ID_FILE_JSON_BASE64: ${{ secrets.FIREBASE_APP_ID_FILE_JSON_BASE64 }} + APP_SECRETS_BASE64: ${{ secrets.APP_SECRETS_BASE64 }} run: | cd app echo $FIREBASE_OPTIONS_BASE64 | base64 --decode > lib/firebase_options.dart echo $GOOGLE_SERVICE_INFO_PLIST_BASE64 | base64 --decode > ios/Runner/GoogleService-Info.plist echo $FIREBASE_APP_ID_FILE_JSON_BASE64 | base64 --decode > ios/firebase_app_id_file.json - + cd ../data + echo $APP_SECRETS_BASE64 | base64 --decode > lib/apis/network/secrets.dart - name: Install dependencies run: | cd app diff --git a/.idea/libraries/Dart_Packages.xml b/.idea/libraries/Dart_Packages.xml index 5017247..645f710 100644 --- a/.idea/libraries/Dart_Packages.xml +++ b/.idea/libraries/Dart_Packages.xml @@ -5,7 +5,6 @@ - @@ -69,7 +68,6 @@ - @@ -105,7 +103,6 @@ - @@ -127,7 +124,6 @@ - @@ -142,7 +138,6 @@ - @@ -192,7 +187,6 @@ - @@ -207,7 +201,6 @@ - @@ -219,10 +212,16 @@ + + + + + + - @@ -237,7 +236,6 @@ - @@ -245,7 +243,7 @@ - @@ -287,11 +285,17 @@ - + + + + + + @@ -330,7 +334,6 @@ - @@ -338,7 +341,6 @@ - @@ -367,7 +369,6 @@ - @@ -393,6 +394,20 @@ + + + + + + + + + + + + @@ -481,7 +496,6 @@ - @@ -510,7 +524,7 @@ - @@ -525,7 +539,6 @@ - @@ -533,7 +546,6 @@ - @@ -562,7 +574,6 @@ - @@ -570,7 +581,6 @@ - @@ -582,6 +592,13 @@ + + + + + + @@ -592,7 +609,7 @@ - @@ -617,6 +634,13 @@ + + + + + + @@ -634,7 +658,6 @@ - @@ -649,6 +672,7 @@ + @@ -682,10 +706,16 @@ + + + + + + - @@ -721,7 +751,6 @@ - @@ -764,7 +793,7 @@ - @@ -785,7 +814,6 @@ - @@ -807,7 +835,6 @@ - @@ -822,7 +849,7 @@ - @@ -836,14 +863,14 @@ - - @@ -878,7 +905,6 @@ - @@ -907,7 +933,6 @@ - @@ -922,6 +947,7 @@ + @@ -929,6 +955,7 @@ + @@ -1013,7 +1040,6 @@ - @@ -1126,7 +1152,7 @@ - @@ -1144,10 +1170,17 @@ + + + + + + - @@ -1161,7 +1194,6 @@ - @@ -1169,7 +1201,6 @@ - @@ -1177,7 +1208,6 @@ - @@ -1185,7 +1215,6 @@ - @@ -1193,7 +1222,6 @@ - @@ -1201,7 +1229,6 @@ - @@ -1244,7 +1271,7 @@ - @@ -1272,28 +1299,28 @@ - - - - @@ -1335,7 +1362,6 @@ - @@ -1350,7 +1376,7 @@ - @@ -1371,7 +1397,6 @@ - @@ -1379,7 +1404,6 @@ - @@ -1401,7 +1425,6 @@ - @@ -1410,20 +1433,16 @@ - - - - @@ -1431,41 +1450,37 @@ - - - + - - + - + - - - + + @@ -1476,83 +1491,77 @@ - - + - - - - + - + + - + - + - - + - - - + - - + + - - + + @@ -1564,7 +1573,6 @@ - @@ -1580,50 +1588,42 @@ - + - + + - - - - - - - + - - - - + + + + - - + - - diff --git a/.idea/libraries/Flutter_Plugins.xml b/.idea/libraries/Flutter_Plugins.xml index f3628d3..f5befea 100644 --- a/.idea/libraries/Flutter_Plugins.xml +++ b/.idea/libraries/Flutter_Plugins.xml @@ -1,62 +1,53 @@ - + + + + + + + - - - - - - - - - - - - + + + + + + + + + - + + - - - - - - + + - - - + + + - - - - - - - - - diff --git a/.idea/modules.xml b/.idea/modules.xml index adf7bca..3914824 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -3,6 +3,7 @@ + diff --git a/app/android/app/src/main/AndroidManifest.xml b/app/android/app/src/main/AndroidManifest.xml index 9c7b7ef..f10bf9e 100644 --- a/app/android/app/src/main/AndroidManifest.xml +++ b/app/android/app/src/main/AndroidManifest.xml @@ -29,6 +29,9 @@ + diff --git a/app/android/app/src/main/res/drawable-xxxhdpi/cloud_gallery_logo.png b/app/android/app/src/main/res/drawable-v21/cloud_gallery_logo.png similarity index 100% rename from app/android/app/src/main/res/drawable-xxxhdpi/cloud_gallery_logo.png rename to app/android/app/src/main/res/drawable-v21/cloud_gallery_logo.png diff --git a/app/android/app/src/main/res/drawable/cloud_gallery_logo.png b/app/android/app/src/main/res/drawable/cloud_gallery_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c73daf546179fb79f96c6a33447c32c0782221d4 GIT binary patch literal 26297 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 OK*>w1NYzT1fc^)n{d4#L literal 0 HcmV?d00001 diff --git a/app/assets/images/icons/dropbox.svg b/app/assets/images/icons/dropbox.svg deleted file mode 100644 index 5559d5f..0000000 --- a/app/assets/images/icons/dropbox.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/assets/images/icons/ic_dropbox.svg b/app/assets/images/icons/ic_dropbox.svg new file mode 100644 index 0000000..f225eab --- /dev/null +++ b/app/assets/images/icons/ic_dropbox.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/app/assets/images/icons/google-drive.svg b/app/assets/images/icons/ic_google_drive.svg similarity index 100% rename from app/assets/images/icons/google-drive.svg rename to app/assets/images/icons/ic_google_drive.svg diff --git a/app/assets/locales/app_en.arb b/app/assets/locales/app_en.arb index 9adaf0c..1967eba 100644 --- a/app/assets/locales/app_en.arb +++ b/app/assets/locales/app_en.arb @@ -1,87 +1,103 @@ { + "app_name": "Cloud Gallery", + "@_COMMON":{}, "common_get_started": "Get Started", - "common_done": "Done", - "common_sign_out": "Sign Out", - "common_auto_back_up": "Auto Back-Up", - "common_accounts": "Accounts", + "common_hey_there": "Hey There!", + "common_hey": "Hey", "common_local": "Local", "common_google_drive": "Google Drive", "common_dropbox": "Dropbox", - "common_term_and_condition": "Terms and Conditions", - "common_privacy_policy": "Privacy Policy", "common_today": "Today", "common_yesterday": "Yesterday", "common_tomorrow": "Tomorrow", - "common_info":"Info", - "common_share":"Share", - "common_upload":"Upload", - "common_download":"Download", + "common_upload": "Upload", + "common_download": "Download", "common_delete": "Delete", - "common_delete_from_google_drive": "Delete from Google Drive", - "common_delete_from_device": "Delete from Device", + "common_share": "Share", "common_cancel": "Cancel", + "common_done": "Done", "common_not_available": "N/A", + "common_open_settings": "Open Settings", + "@_ERROR":{}, "no_internet_connection_error": "No internet connection! Please check your network and try again.", "something_went_wrong_error": "Something went wrong! Please try again later.", "user_google_sign_in_account_not_found_error": "You haven't signed in with Google account yet. Please sign in with Google account and try again.", "back_up_folder_not_found_error": "Back up folder not found!", "auth_session_expired_error": "Your session has expired. Please log in again to continue using the app.", - "unable_to_load_media_error": "Unable to load media!", - - "unable_to_load_media_message": "Oops! It looks like we're having trouble loading the media right now. Please try again later.", - + "@_MEDIA_ACTIONS":{}, + "upload_to_google_drive_title": "Upload to Google Drive", + "download_from_google_drive_title": "Download from Google Drive", + "delete_from_google_drive_title": "Delete from Google Drive", + "delete_from_device_title": "Delete from Device", + "upload_to_dropbox_title": "Upload to Dropbox", + "download_from_dropbox_title": "Download from Dropbox", + "delete_from_dropbox_title": "Delete from Dropbox", + "upload_to_google_drive_confirmation_message": "Are you sure you want to upload to Google Drive?", + "upload_to_dropbox_confirmation_message": "Are you sure you want to upload to Dropbox?", + "download_from_dropbox_confirmation_message": "Are you sure you want to download from Dropbox? It will be saved to your gallery.", + "download_from_google_drive_confirmation_message": "Are you sure you want to download from Google Drive? It will be saved to your gallery.", + "delete_from_dropbox_confirmation_message": "Are you sure you want to delete? It will be permanently deleted from your Dropbox.", + "delete_from_google_drive_confirmation_message": "Are you sure you want to delete? It will be permanently deleted from your Google Drive.", + "delete_from_device_confirmation_message": "Are you sure you want to delete? It will be permanently deleted from your device.", + + "@_ON_BOARD":{}, "on_board_description": "Effortlessly move, share, and organize your photos and videos in a breeze. Access all your clouds in one friendly place. Your moments, your way, simplified for you! 🚀", - "back_up_on_google_drive_text": "Back up on Google Drive", - - "cant_find_media_title": "Can't find your photos or videos", - "ask_for_media_permission_message": "Please give us permission to access your local media, so you can load and enjoy all your favorite photos and videos effortlessly.", - "load_local_media_button_text": "Load local media", - - "theme_text": "Theme", - "notification_text": "Notification", - "light_theme_text": "Light", - "dark_theme_text": "Dark", - "system_theme_text": "System", - - "add_account_title": "Add account", - "version_text": "Version", - - "greetings_hey_there_text": "Hey There!", - "greetings_hey_text": "Hey", - + "@_HOME":{}, + "unable_to_load_media_error": "Unable to load media!", + "unable_to_load_media_message": "Oops! It looks like we're having trouble loading the media right now. Please try again later.", "hint_sign_in_message": "Sign in with Google Drive or Dropbox and enjoy quick access to all your awesome content in one spot", - "delete_media_from_device_confirmation_message": "Are you sure you want to delete this media? It will be permanently deleted from your device.", - "delete_media_from_google_drive_confirmation_message": "Are you sure you want to delete this media? It will be permanently deleted from your Google Drive.", - - "waiting_in_queue_text": "Waiting in Queue...", - "waiting_in_download_queue_message": "Your video download is in queue. We appreciate your patience!", - + "@_NO_MEDIA_ACCESS": {}, + "no_media_access_screen_title": "Media Access Required", + "no_media_access_screen_message": "To help you explore and enjoy all your favorite photos and videos, please allow us access to your local media.", + + "@_ACCOUNT": {}, + "accounts_title": "Accounts", + "auto_back_up_title": "Auto Back-Up", + "sign_out_title": "Sign Out", + "notification_title": "Notification", + "theme_title": "Theme", + "light_theme_title": "Light", + "dark_theme_title": "Dark", + "system_theme_title": "System", + "term_and_condition_title": "Terms and Conditions", + "privacy_policy_title": "Privacy Policy", + "add_account_title": "Add account", + "version_title": "Version", + "sign_in_with_google_drive_title": "Sign in with Google Drive", + "sign_in_with_dropbox_title": "Sign in with Dropbox", + + "@_UPLOAD_ITEM":{}, + "upload_status_waiting": "Upload is in the queue", + "upload_status_success": "Upload completed successfully", + "upload_status_failed": "Upload failed. Please try again.", + "upload_status_cancelled": "Upload was cancelled", + + "@_DOWNLOAD_ITEM":{}, + "download_status_waiting": "Download is in the queue", + "download_status_success": "Download completed successfully", + "download_status_failed": "Download failed. Please try again.", + "download_status_cancelled": "Download was cancelled", + + "@_TRANSFER":{}, "transfer_screen_title": "Transfer", - - "empty_upload_title":"No Files Being Uploads", + "empty_upload_title":"No Files Being Uploaded", "empty_upload_message": "No uploads are happening right now. If you have files to upload, go ahead and start uploading.", - - "empty_download_title":"No Files Being Downloads", + "empty_download_title":"No Files Being Downloaded", "empty_download_message": "No downloads are happening right now. If you have files to download, go ahead and start downloading.", - "download_in_progress_text": "Download in progress", - + "@_PREVIEW":{}, "download_require_text": "Download required", "download_require_message": "To watch the video, simply download it first. Tap the download button to begin downloading the video.", + "download_in_progress_text": "Download in progress", - "download_from_google_drive_text": "Download from Google Drive", - "download_from_google_drive_alert_message": " Are you sure you want to download this media? It will be saved to your gallery.", - - "sign_in_with_google_drive_text": "Sign in with Google Drive", - "sign_in_with_dropbox_text": "Sign in with Dropbox", - + "@_MEDIA_INFO":{}, "name_text": "Name", "size_text": "Size", "created_at_text": "Created at", diff --git a/app/ios/Podfile.lock b/app/ios/Podfile.lock index 957989c..9828fc4 100644 --- a/app/ios/Podfile.lock +++ b/app/ios/Podfile.lock @@ -25,9 +25,11 @@ PODS: - Flutter - Toast - google_sign_in_ios (0.0.1): + - AppAuth (>= 1.7.4) - Flutter - FlutterMacOS - - GoogleSignIn (~> 7.0) + - GoogleSignIn (~> 7.1) + - GTMSessionFetcher (>= 3.4.0) - GoogleSignIn (7.1.0): - AppAuth (< 2.0, >= 1.7.3) - GTMAppAuth (< 5.0, >= 4.1.1) @@ -43,7 +45,11 @@ PODS: - GTMAppAuth (4.1.1): - AppAuth/Core (~> 1.7) - GTMSessionFetcher/Core (< 4.0, >= 3.3) + - GTMSessionFetcher (3.5.0): + - GTMSessionFetcher/Full (= 3.5.0) - GTMSessionFetcher/Core (3.5.0) + - GTMSessionFetcher/Full (3.5.0): + - GTMSessionFetcher/Core - package_info_plus (0.4.5): - Flutter - path_provider_foundation (0.0.1): @@ -146,21 +152,21 @@ SPEC CHECKSUMS: Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_local_notifications: df98d66e515e1ca797af436137b4459b160ad8c9 fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c - google_sign_in_ios: 989eea5abe94af62050782714daf920be883d4a2 + google_sign_in_ios: 07375bfbf2620bc93a602c0e27160d6afc6ead38 GoogleSignIn: d4281ab6cf21542b1cfaff85c191f230b399d2db GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 - path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d Toast: 1f5ea13423a1e6674c4abdac5be53587ae481c4e - url_launcher_ios: 6116280ddcfe98ab8820085d8d76ae7449447586 - video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579 + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe + video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3 webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 PODFILE CHECKSUM: 4c438addb11b6da45ed7ae408823d68256222460 diff --git a/app/lib/components/app_media_image_provider.dart b/app/lib/components/app_media_image_provider.dart deleted file mode 100644 index ddbf998..0000000 --- a/app/lib/components/app_media_image_provider.dart +++ /dev/null @@ -1,120 +0,0 @@ -import 'dart:async'; -import 'dart:io'; -import 'dart:ui' as ui; -import 'package:data/models/isolate/isolate_parameters.dart'; -import 'package:data/models/media/media.dart'; -import 'package:data/models/media/media_extension.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -final _providerLocks = >{}; - -class AppMediaImageProvider extends ImageProvider { - final AppMedia media; - final Size thumbnailSize; - - const AppMediaImageProvider({ - required this.media, - this.thumbnailSize = const Size(500, 500), - }); - - @override - ImageStreamCompleter loadImage( - AppMediaImageProvider key, - ImageDecoderCallback decode, - ) { - return MultiFrameImageStreamCompleter( - codec: _loadAsync(key, decode), - scale: 1.0, - debugLabel: '${key.media.runtimeType}-' - '${key.media.id}-' - '${key.media.thumbnailLink ?? ''}' - '${key.media.sources.contains(AppMediaSource.local) ? 'local' : 'network'}' - '${key.thumbnailSize}', - informationCollector: () { - return [ - DiagnosticsProperty('Image Provider', this), - DiagnosticsProperty('Image Key', key), - ]; - }, - ); - } - - @override - Future obtainKey(ImageConfiguration configuration) { - return SynchronousFuture(this); - } - - Future _loadAsync( - AppMediaImageProvider key, - ImageDecoderCallback decode, - ) async { - if (_providerLocks.containsKey(key)) { - return _providerLocks[key]!.future; - } - final lock = Completer(); - _providerLocks[key] = lock; - Future(() async { - try { - if (media.sources.contains(AppMediaSource.local)) { - final Uint8List? bytes = - await media.loadThumbnail(size: thumbnailSize); - final buffer = await ui.ImmutableBuffer.fromUint8List(bytes!); - return decode(buffer); - } else if (media.thumbnailLink != null && - media.thumbnailLink?.isNotEmpty == true) { - final bytes = await compute( - _loadNetworkImageInBackground, - IsolateParameters(data: media.thumbnailLink!), - ); - final buffer = await ui.ImmutableBuffer.fromUint8List(bytes); - return decode(buffer); - } - throw Exception('No image source found.'); - } catch (e) { - Future.microtask( - () => PaintingBinding.instance.imageCache.evict(key), - ); - rethrow; - } - }).then((codec) { - lock.complete(codec); - }).catchError((e, s) { - lock.completeError(e, s); - }).whenComplete(() { - _providerLocks.remove(key); - }); - return lock.future; - } - - Future _loadNetworkImageInBackground( - IsolateParameters parameters, - ) async { - BackgroundIsolateBinaryMessenger.ensureInitialized( - parameters.rootIsolateToken!, - ); - final Uri resolved = Uri.base.resolve(parameters.data); - final HttpClientRequest request = await HttpClient().getUrl(resolved); - final HttpClientResponse response = await request.close(); - if (response.statusCode != HttpStatus.ok) { - throw NetworkImageLoadException( - statusCode: response.statusCode, - uri: resolved, - ); - } - final Uint8List bytes = await consolidateHttpClientResponseBytes(response); - return bytes; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other is AppMediaImageProvider && - other.media.path == media.path && - other.thumbnailSize == thumbnailSize); - } - - @override - int get hashCode => media.hashCode ^ thumbnailSize.hashCode; -} diff --git a/app/lib/components/thumbnail_builder.dart b/app/lib/components/thumbnail_builder.dart index 803f825..b3e6ade 100644 --- a/app/lib/components/thumbnail_builder.dart +++ b/app/lib/components/thumbnail_builder.dart @@ -1,10 +1,13 @@ import 'package:data/models/media/media.dart'; +import 'package:data/storage/app_preferences.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:style/extensions/context_extensions.dart'; import 'package:style/indicators/circular_progress_indicator.dart'; -import 'app_media_image_provider.dart'; -class AppMediaImage extends StatelessWidget { +import '../domain/image_providers/app_media_image_provider.dart'; + +class AppMediaImage extends ConsumerWidget { final Object? heroTag; final AppMedia media; final Size size; @@ -19,13 +22,18 @@ class AppMediaImage extends StatelessWidget { }); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, ref) { return ClipRRect( borderRadius: BorderRadius.circular(radius), child: Hero( tag: heroTag ?? '', child: Image( - image: AppMediaImageProvider(media: media, thumbnailSize: size * 2), + image: AppMediaImageProvider( + media: media, + dropboxAccessToken: + ref.read(AppPreferences.dropboxToken)?.access_token, + thumbnailSize: size * 2, + ), loadingBuilder: (context, child, loadingProgress) { if (loadingProgress != null) { return AppMediaPlaceHolder( diff --git a/app/lib/domain/assets/assets_paths.dart b/app/lib/domain/assets/assets_paths.dart deleted file mode 100644 index a639cbf..0000000 --- a/app/lib/domain/assets/assets_paths.dart +++ /dev/null @@ -1,14 +0,0 @@ -class Assets { - static PathImages images = PathImages(); -} - -class PathImages { - String get appIcon => 'assets/images/app_logo.png'; - - PathIcons get icons => PathIcons(); -} - -class PathIcons { - String get googleDrive => 'assets/images/icons/google-drive.svg'; - String get dropbox => 'assets/images/icons/dropbox.svg'; -} diff --git a/app/lib/domain/extensions/media_list_extension.dart b/app/lib/domain/extensions/media_list_extension.dart deleted file mode 100644 index 0da4808..0000000 --- a/app/lib/domain/extensions/media_list_extension.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'package:data/extensions/iterable_extension.dart'; -import 'package:data/models/app_process/app_process.dart'; -import 'package:data/models/media/media.dart'; -import 'package:data/models/media/media_extension.dart'; - -extension MediaListExtension on List { - void removeGoogleDriveRefFromMedias({List? removeFromIds}) { - for (int index = 0; index < length; index++) { - if (this[index].isGoogleDriveStored && - (removeFromIds?.contains(this[index].id) ?? true)) { - removeAt(index); - } else if (this[index].isCommonStored && - (removeFromIds?.contains(this[index].id) ?? true)) { - this[index] = this[index].copyWith( - sources: this[index].sources.toList() - ..remove(AppMediaSource.googleDrive), - thumbnailLink: null, - driveMediaRefId: null, - ); - } - } - } - - void removeLocalRefFromMedias({List? removeFromIds}) { - for (int index = 0; index < length; index++) { - if (this[index].isLocalStored && - (removeFromIds?.contains(this[index].id) ?? true)) { - removeAt(index); - } else if (this[index].isCommonStored && - (removeFromIds?.contains(this[index].id) ?? true)) { - this[index] = this[index].copyWith( - id: this[index].driveMediaRefId ?? this[index].id, - sources: this[index].sources.toList()..remove(AppMediaSource.local), - ); - } - } - } - - void addGoogleDriveRefInMedias({ - required List process, - List? processIds, - }) { - processIds ??= process.map((e) => e.id).toList(); - updateWhere( - where: (media) => processIds?.contains(media.id) ?? false, - update: (media) { - final res = process - .where((element) => element.id == media.id) - .first - .response as AppMedia?; - if (res == null) return media; - return media.mergeGoogleDriveMedia(res); - }, - ); - } - - void replaceMediaRefInMedias({ - required List process, - List? processIds, - }) { - processIds ??= process.map((e) => e.id).toList(); - updateWhere( - where: (media) => processIds?.contains(media.id) ?? false, - update: (media) { - final res = process - .where((element) => element.id == media.id) - .first - .response as AppMedia?; - - if (res == null) return media; - return res; - }, - ); - } -} diff --git a/app/lib/domain/handlers/deep_links_handler.dart b/app/lib/domain/handlers/deep_links_handler.dart index 8e93b88..6ba6de7 100644 --- a/app/lib/domain/handlers/deep_links_handler.dart +++ b/app/lib/domain/handlers/deep_links_handler.dart @@ -28,6 +28,7 @@ class DeepLinkHandler { final dropboxService = container.read(dropboxServiceProvider); await dropboxService.setCurrentUserAccount(); + await dropboxService.setFileIdAppPropertyTemplate(); } } diff --git a/app/lib/domain/image_providers/app_cached_network_image_provider.dart b/app/lib/domain/image_providers/app_cached_network_image_provider.dart new file mode 100644 index 0000000..2930829 --- /dev/null +++ b/app/lib/domain/image_providers/app_cached_network_image_provider.dart @@ -0,0 +1,102 @@ +import 'dart:io'; +import 'dart:ui' as ui; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:dio/dio.dart'; + +/// A custom ImageProvider for fetching network image with caching in local storage. +class AppCachedNetworkImageProvider + extends ImageProvider { + /// The URL of the image to be fetched. + final String url; + + const AppCachedNetworkImageProvider({required this.url}); + + @override + Future obtainKey( + ImageConfiguration configuration, + ) => + SynchronousFuture(this); + + @override + ImageStreamCompleter loadImage( + AppCachedNetworkImageProvider key, + ImageDecoderCallback decode, + ) => + OneFrameImageStreamCompleter(_loadAsync(key, decode)); + + Future _loadAsync( + AppCachedNetworkImageProvider key, + ImageDecoderCallback decode, + ) async { + // Get the cached file if it exists, otherwise fetch it from the network + final directory = await getApplicationDocumentsDirectory(); + final cacheFilePath = '${directory.path}/network_image_$url'; + final cacheFile = File(cacheFilePath); + + if (await cacheFile.exists()) { + // Decode existing file in the cache + final codec = await decode( + await ui.ImmutableBuffer.fromUint8List( + await cacheFile.readAsBytes(), + ), + ); + + // Return first frame of the image and release the image resources + final frame = await codec.getNextFrame(); + codec.dispose(); + return ImageInfo(image: frame.image); + } else { + // Create the cache file if it doesn't exist. + if (Platform.isIOS) { + await cacheFile.create(recursive: true); + } + + // Fetch the image from the network + final response = await Dio().get(url); + + if (response.statusCode == 200) { + // Write the fetched image to the cache + await cacheFile.writeAsBytes(response.data); + + // Decode the fetched image and return the first frame + final codec = + await decode(await ui.ImmutableBuffer.fromUint8List(response.data)); + final frame = await codec.getNextFrame(); + codec.dispose(); + return ImageInfo(image: frame.image); + } else { + throw NetworkImageLoadException( + statusCode: response.statusCode ?? 400, + uri: Uri.parse( + url, + ), + ); + } + } + } + + /// Clears the cached images from local storage. + static Future clearCache() async { + final directory = await getApplicationDocumentsDirectory(); + final cacheDirectory = Directory(directory.path); + + if (await cacheDirectory.exists()) { + final files = cacheDirectory.listSync().where( + (file) => file is File && file.path.contains('network_image_'), + ); + for (var file in files) { + await file.delete(); + } + } + } + + @override + bool operator ==(Object other) { + return other is AppCachedNetworkImageProvider && other.url == url; + } + + @override + int get hashCode => url.hashCode; +} diff --git a/app/lib/domain/image_providers/app_media_image_provider.dart b/app/lib/domain/image_providers/app_media_image_provider.dart new file mode 100644 index 0000000..4749751 --- /dev/null +++ b/app/lib/domain/image_providers/app_media_image_provider.dart @@ -0,0 +1,204 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:ui' as ui; +import 'package:data/models/isolate/isolate_parameters.dart'; +import 'package:data/models/media/media.dart'; +import 'package:data/models/media/media_extension.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:path_provider/path_provider.dart'; + +/// A custom ImageProvider for fetching [AppMedia] images with caching in local storage. +class AppMediaImageProvider extends ImageProvider { + /// The [AppMedia] object to fetch the image from. + final AppMedia media; + + /// The size of the thumbnail to be fetched. Defaults to 500x500. + final Size thumbnailSize; + + /// The scale of the image. Defaults to 1. + final double scale; + + /// A debug label for the image. + final String? debugLabel; + + /// The Dropbox access token for fetching Dropbox images. Required if the media is from Dropbox. + final String? dropboxAccessToken; + + const AppMediaImageProvider({ + required this.media, + this.scale = 1, + this.thumbnailSize = const Size(500, 500), + this.debugLabel, + this.dropboxAccessToken, + }); + + @override + ImageStreamCompleter loadImage( + AppMediaImageProvider key, + ImageDecoderCallback decode, + ) => + MultiFrameImageStreamCompleter( + codec: _loadAsync(key, decode), + scale: scale, + debugLabel: debugLabel ?? 'AppMediaImageProvider: ${media.id}', + ); + + @override + Future obtainKey(ImageConfiguration configuration) => + SynchronousFuture(this); + + Future _loadAsync( + AppMediaImageProvider key, + ImageDecoderCallback decode, + ) async { + // Get cached file directory. + final directory = await getApplicationDocumentsDirectory(); + final cacheFilePath = '${directory.path}/thumbnail_${media.id}'; + final cacheFile = File(cacheFilePath); + + if (await cacheFile.exists()) { + // Decode cached file if it exist and return it. + return await decode( + await ui.ImmutableBuffer.fromUint8List( + await cacheFile.readAsBytes(), + ), + ); + } else { + // Create the cache file if it doesn't exist. + if (Platform.isIOS) { + await cacheFile.create(recursive: true); + } + + if (media.sources.contains(AppMediaSource.local)) { + // If the media is local, generate thumbnail from the local asset and cache it. + final Uint8List? bytes = await media.loadThumbnail(size: thumbnailSize); + if (bytes != null) { + await cacheFile.writeAsBytes(bytes); + return decode(await ui.ImmutableBuffer.fromUint8List(bytes)); + } + throw Exception('Unable to load thumbnail from local: ${media.id}'); + } else if (media.thumbnailLink != null && + media.thumbnailLink?.isNotEmpty == true) { + // If the media is from network, fetch the image from the network and cache it. + final Uint8List bytes = await compute( + _loadNetworkImageInBackground, + IsolateParameters(data: media.thumbnailLink!), + ); + await cacheFile.writeAsBytes(bytes); + return decode(await ui.ImmutableBuffer.fromUint8List(bytes)); + } else if (media.dropboxMediaRefId != null) { + // If the media is from Dropbox, fetch the image from Dropbox API and cache it. + final Uint8List bytes = await compute( + _loadDropboxThumbnail, + IsolateParameters>( + data: [media.dropboxMediaRefId!, _getDropboxThumbnailSize()], + ), + ); + await cacheFile.writeAsBytes(bytes); + return decode(await ui.ImmutableBuffer.fromUint8List(bytes)); + } + throw Exception('No image source found for media: ${media.id}'); + } + } + + /// Loads the network image in the background. + Future _loadNetworkImageInBackground( + IsolateParameters parameters, + ) async { + BackgroundIsolateBinaryMessenger.ensureInitialized( + parameters.rootIsolateToken!, + ); + + final Uri resolved = Uri.base.resolve(parameters.data); + final HttpClientRequest request = await HttpClient().getUrl(resolved); + final HttpClientResponse response = await request.close(); + if (response.statusCode == HttpStatus.ok) { + final Uint8List bytes = + await consolidateHttpClientResponseBytes(response); + return bytes; + } + throw NetworkImageLoadException( + statusCode: response.statusCode, + uri: resolved, + ); + } + + /// Returns a thumbnail size string for Dropbox API based on [thumbnailSize]. + String _getDropboxThumbnailSize() { + if (thumbnailSize.width < 128) return 'w128h128'; + if (thumbnailSize.width < 256) return 'w256h256'; + if (thumbnailSize.width < 480) return 'w480h320'; + if (thumbnailSize.width < 640) return 'w640h480'; + if (thumbnailSize.width < 960) return 'w960h640'; + if (thumbnailSize.width < 1024) return 'w1024h768'; + if (thumbnailSize.width < 2048) return 'w2048h1536'; + return 'w256h256'; + } + + /// Loads the Dropbox thumbnail in the background. + Future _loadDropboxThumbnail( + IsolateParameters> parameters, + ) async { + BackgroundIsolateBinaryMessenger.ensureInitialized( + parameters.rootIsolateToken!, + ); + + final Uri resolved = Uri.base + .resolve("https://content.dropboxapi.com/2/files/get_thumbnail_v2"); + + final HttpClientRequest request = await HttpClient().postUrl(resolved); + + request.headers.add( + 'Authorization', + 'Bearer $dropboxAccessToken', + ); + request.headers.add( + 'Dropbox-API-Arg', + jsonEncode({ + "format": "png", + "mode": "bestfit", + "quality": "quality_80", + "resource": { + ".tag": "path", + "path": parameters.data.first, + }, + "size": parameters.data.last, + }), + ); + + final HttpClientResponse response = await request.close(); + + if (response.statusCode == HttpStatus.ok) { + final Uint8List bytes = + await consolidateHttpClientResponseBytes(response); + return bytes; + } + throw NetworkImageLoadException( + statusCode: response.statusCode, + uri: resolved, + ); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other is AppMediaImageProvider && + other.media.id == media.id && + other.media.thumbnailLink == media.thumbnailLink && + other.media.dropboxMediaRefId == media.dropboxMediaRefId && + other.media.sources == media.sources && + other.thumbnailSize == thumbnailSize); + } + + @override + int get hashCode => Object.hash( + media.id, + media.thumbnailLink, + media.dropboxMediaRefId, + media.sources, + thumbnailSize, + ); +} diff --git a/app/lib/domain/image_providers/dropbox_image_provider.dart b/app/lib/domain/image_providers/dropbox_image_provider.dart new file mode 100644 index 0000000..e280993 --- /dev/null +++ b/app/lib/domain/image_providers/dropbox_image_provider.dart @@ -0,0 +1,143 @@ +import 'dart:io'; +import 'dart:ui' as ui; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:dio/dio.dart'; + +/// A custom ImageProvider for fetching Dropbox thumbnails with caching it in local storage. +class DropboxThumbnailProvider extends ImageProvider { + /// The ID of the Dropbox file whose thumbnail is to be fetched. + final String id; + + /// The size of the thumbnail to be fetched. Defaults to 200. + final double size; + + /// The access token for Dropbox API authorization. + final String accessToken; + + const DropboxThumbnailProvider({ + required this.id, + required this.accessToken, + this.size = 200, + }); + + @override + Future obtainKey(ImageConfiguration configuration) { + return SynchronousFuture(this); + } + + @override + ImageStreamCompleter loadImage( + DropboxThumbnailProvider key, + ImageDecoderCallback decode, + ) => + OneFrameImageStreamCompleter(_loadAsync(key, decode)); + + Future _loadAsync( + DropboxThumbnailProvider key, + ImageDecoderCallback decode, + ) async { + // Get the cached file if it exists, otherwise fetch it from Dropbox API + final directory = await getApplicationDocumentsDirectory(); + final cacheFilePath = '${directory.path}/dropbox_thumbnail_$id'; + final cacheFile = File(cacheFilePath); + + if (await cacheFile.exists()) { + // Decode existing file in the cache + final codec = await decode( + await ui.ImmutableBuffer.fromUint8List( + await cacheFile.readAsBytes(), + ), + ); + + // Return first frame of the image and release the image resources + final frame = await codec.getNextFrame(); + codec.dispose(); + return ImageInfo(image: frame.image); + } else { + // Create the cache file if it doesn't exist. + if (Platform.isIOS) { + await cacheFile.create(recursive: true); + } + + // Fetch the thumbnail from Dropbox API + final response = await Dio().post( + 'https://content.dropboxapi.com/2/files/get_thumbnail_v2', + options: Options( + headers: { + 'Authorization': 'Bearer $accessToken', + 'Dropbox-API-Arg': { + "format": "png", + "mode": "bestfit", + "quality": "quality_80", + "resource": { + ".tag": "path", + "path": id, + }, + "size": _getDropboxThumbnailSize(size), + }, + }, + responseType: ResponseType.bytes, + ), + ); + + if (response.statusCode == 200) { + // Write the fetched image to the cache + await cacheFile.writeAsBytes(response.data); + + // Decode the fetched image and return the first frame + final codec = + await decode(await ui.ImmutableBuffer.fromUint8List(response.data)); + final frame = await codec.getNextFrame(); + codec.dispose(); + return ImageInfo(image: frame.image); + } else { + throw NetworkImageLoadException( + statusCode: response.statusCode ?? 400, + uri: Uri.parse( + 'https://content.dropboxapi.com/2/files/get_thumbnail_v2', + ), + ); + } + } + } + + /// Returns the Dropbox thumbnail size string based on the provided size. + String _getDropboxThumbnailSize(double size) { + if (size < 128) return 'w128h128'; + if (size < 256) return 'w256h256'; + if (size < 480) return 'w480h320'; + if (size < 640) return 'w640h480'; + if (size < 960) return 'w960h640'; + if (size < 1024) return 'w1024h768'; + if (size < 2048) return 'w2048h1536'; + return 'w256h256'; + } + + /// Clears the cached thumbnails from local storage. + static Future clearCache() async { + final directory = await getApplicationDocumentsDirectory(); + final cacheDirectory = Directory(directory.path); + + if (await cacheDirectory.exists()) { + final files = cacheDirectory.listSync().where( + (file) => file is File && file.path.contains('dropbox_thumbnail_'), + ); + for (var file in files) { + await file.delete(); + } + } + } + + @override + bool operator ==(Object other) { + return other is DropboxThumbnailProvider && + other.id == id && + other.accessToken == accessToken && + other.size == size; + } + + @override + int get hashCode => Object.hash(id, accessToken, size); +} diff --git a/app/lib/domain/image_providers/local_asset_thunbnail_image_provider.dart b/app/lib/domain/image_providers/local_asset_thunbnail_image_provider.dart new file mode 100644 index 0000000..74d5b2e --- /dev/null +++ b/app/lib/domain/image_providers/local_asset_thunbnail_image_provider.dart @@ -0,0 +1,104 @@ +import 'dart:io'; +import 'dart:ui' as ui; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:dio/dio.dart'; + +/// A custom ImageProvider for create thumbnail of local asset and caching in local storage. +class LocalAssetCachedThumbnailImageProvider + extends ImageProvider { + /// The URL of the image to be fetched. + final String url; + + const LocalAssetCachedThumbnailImageProvider({ + required this.url, + }); + + @override + Future obtainKey( + ImageConfiguration configuration, + ) => + SynchronousFuture(this); + + @override + ImageStreamCompleter loadImage( + LocalAssetCachedThumbnailImageProvider key, + ImageDecoderCallback decode, + ) => + OneFrameImageStreamCompleter(_loadAsync(key, decode)); + + Future _loadAsync( + LocalAssetCachedThumbnailImageProvider key, + ImageDecoderCallback decode, + ) async { + // Get the cached file if it exists, otherwise fetch it from the network + final directory = await getApplicationDocumentsDirectory(); + final cacheFilePath = '${directory.path}/network_image_$url'; + final cacheFile = File(cacheFilePath); + + if (await cacheFile.exists()) { + // Decode existing file in the cache + final codec = await decode( + await ui.ImmutableBuffer.fromUint8List( + await cacheFile.readAsBytes(), + ), + ); + + // Return first frame of the image and release the image resources + final frame = await codec.getNextFrame(); + codec.dispose(); + return ImageInfo(image: frame.image); + } else { + // Create the cache file if it doesn't exist. + if (Platform.isIOS) { + await cacheFile.create(recursive: true); + } + + // Fetch the image from the network + final response = await Dio().get(url); + + if (response.statusCode == 200) { + // Write the fetched image to the cache + await cacheFile.writeAsBytes(response.data); + + // Decode the fetched image and return the first frame + final codec = + await decode(await ui.ImmutableBuffer.fromUint8List(response.data)); + final frame = await codec.getNextFrame(); + codec.dispose(); + return ImageInfo(image: frame.image); + } else { + throw NetworkImageLoadException( + statusCode: response.statusCode ?? 400, + uri: Uri.parse( + url, + ), + ); + } + } + } + + /// Clears the cached images from local storage. + static Future clearCache() async { + final directory = await getApplicationDocumentsDirectory(); + final cacheDirectory = Directory(directory.path); + + if (await cacheDirectory.exists()) { + final files = cacheDirectory.listSync().where( + (file) => file is File && file.path.contains('network_image_'), + ); + for (var file in files) { + await file.delete(); + } + } + } + + @override + bool operator ==(Object other) { + return other is LocalAssetCachedThumbnailImageProvider && other.url == url; + } + + @override + int get hashCode => url.hashCode; +} diff --git a/app/lib/gen/assets.gen.dart b/app/lib/gen/assets.gen.dart new file mode 100644 index 0000000..7048b3e --- /dev/null +++ b/app/lib/gen/assets.gen.dart @@ -0,0 +1,124 @@ +/// GENERATED CODE - DO NOT MODIFY BY HAND +/// ***************************************************** +/// FlutterGen +/// ***************************************************** + +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: directives_ordering,unnecessary_import,implicit_dynamic_list_literal,deprecated_member_use + +import 'package:flutter/widgets.dart'; + +class $AssetsImagesGen { + const $AssetsImagesGen(); + + /// File path: assets/images/app_logo.png + AssetGenImage get appLogo => + const AssetGenImage('assets/images/app_logo.png'); + + /// Directory path: assets/images/icons + $AssetsImagesIconsGen get icons => const $AssetsImagesIconsGen(); + + /// List of all assets + List get values => [appLogo]; +} + +class $AssetsImagesIconsGen { + const $AssetsImagesIconsGen(); + + /// File path: assets/images/icons/ic_dropbox.svg + String get icDropbox => 'assets/images/icons/ic_dropbox.svg'; + + /// File path: assets/images/icons/ic_google_drive.svg + String get icGoogleDrive => 'assets/images/icons/ic_google_drive.svg'; + + /// List of all assets + List get values => [icDropbox, icGoogleDrive]; +} + +class Assets { + Assets._(); + + static const $AssetsImagesGen images = $AssetsImagesGen(); +} + +class AssetGenImage { + const AssetGenImage( + this._assetName, { + this.size, + this.flavors = const {}, + }); + + final String _assetName; + + final Size? size; + final Set flavors; + + Image image({ + Key? key, + AssetBundle? bundle, + ImageFrameBuilder? frameBuilder, + ImageErrorWidgetBuilder? errorBuilder, + String? semanticLabel, + bool excludeFromSemantics = false, + double? scale, + double? width, + double? height, + Color? color, + Animation? opacity, + BlendMode? colorBlendMode, + BoxFit? fit, + AlignmentGeometry alignment = Alignment.center, + ImageRepeat repeat = ImageRepeat.noRepeat, + Rect? centerSlice, + bool matchTextDirection = false, + bool gaplessPlayback = true, + bool isAntiAlias = false, + String? package, + FilterQuality filterQuality = FilterQuality.low, + int? cacheWidth, + int? cacheHeight, + }) { + return Image.asset( + _assetName, + key: key, + bundle: bundle, + frameBuilder: frameBuilder, + errorBuilder: errorBuilder, + semanticLabel: semanticLabel, + excludeFromSemantics: excludeFromSemantics, + scale: scale, + width: width, + height: height, + color: color, + opacity: opacity, + colorBlendMode: colorBlendMode, + fit: fit, + alignment: alignment, + repeat: repeat, + centerSlice: centerSlice, + matchTextDirection: matchTextDirection, + gaplessPlayback: gaplessPlayback, + isAntiAlias: isAntiAlias, + package: package, + filterQuality: filterQuality, + cacheWidth: cacheWidth, + cacheHeight: cacheHeight, + ); + } + + ImageProvider provider({ + AssetBundle? bundle, + String? package, + }) { + return AssetImage( + _assetName, + bundle: bundle, + package: package, + ); + } + + String get path => _assetName; + + String get keyName => _assetName; +} diff --git a/app/lib/ui/app.dart b/app/lib/ui/app.dart index e8bbd37..28dfecf 100644 --- a/app/lib/ui/app.dart +++ b/app/lib/ui/app.dart @@ -1,3 +1,4 @@ +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'navigation/app_route.dart'; import '../domain/extensions/context_extensions.dart'; import 'package:flutter/cupertino.dart'; @@ -11,6 +12,7 @@ import 'package:style/extensions/context_extensions.dart'; import 'package:style/theme/theme.dart'; import 'package:style/theme/app_theme_builder.dart'; import 'package:data/storage/app_preferences.dart'; +import 'package:data/handlers/notification_handler.dart'; class CloudGalleryApp extends ConsumerStatefulWidget { const CloudGalleryApp({super.key}); @@ -21,6 +23,7 @@ class CloudGalleryApp extends ConsumerStatefulWidget { class _CloudGalleryAppState extends ConsumerState { late GoRouter _router; + late NotificationHandler _notificationHandler; String _configureInitialRoute() { if (!ref.read(AppPreferences.isOnBoardComplete)) { @@ -32,6 +35,9 @@ class _CloudGalleryAppState extends ConsumerState { @override void initState() { + _notificationHandler = ref.read(notificationHandlerProvider); + _handleNotification(); + _router = GoRouter( initialLocation: _configureInitialRoute(), routes: $appRoutes, @@ -39,6 +45,20 @@ class _CloudGalleryAppState extends ConsumerState { super.initState(); } + Future _handleNotification() async { + final res = await _notificationHandler.init( + onDidReceiveBackgroundNotificationResponse: + onBackgroundNotificationReceived, + onDidReceiveNotificationResponse: (response) { + ///TODO: manage notification tap + }, + ); + if (res?.didNotificationLaunchApp == true) { + ///TODO: manage notification tap + } + _notificationHandler.requestPermission(); + } + @override void dispose() { PhotoManager.clearFileCache(); @@ -87,3 +107,6 @@ class _CloudGalleryAppState extends ConsumerState { ); } } + +@pragma('vm:entry-point') +void onBackgroundNotificationReceived(NotificationResponse response) {} diff --git a/app/lib/ui/flow/accounts/accounts_screen.dart b/app/lib/ui/flow/accounts/accounts_screen.dart index f1d3fe2..08e5330 100644 --- a/app/lib/ui/flow/accounts/accounts_screen.dart +++ b/app/lib/ui/flow/accounts/accounts_screen.dart @@ -1,7 +1,6 @@ import '../../../components/app_page.dart'; -import '../../../domain/assets/assets_paths.dart'; import '../../../domain/extensions/context_extensions.dart'; -import '../../../domain/extensions/widget_extensions.dart'; +import '../../../gen/assets.gen.dart'; import 'accounts_screen_view_model.dart'; import 'components/settings_action_list.dart'; import 'package:data/storage/app_preferences.dart'; @@ -33,7 +32,6 @@ class _AccountsScreenState extends ConsumerState { void initState() { super.initState(); notifier = ref.read(accountsStateNotifierProvider.notifier); - runPostFrame(() => notifier.init()); } void _errorObserver() { @@ -49,7 +47,7 @@ class _AccountsScreenState extends ConsumerState { Widget build(BuildContext context) { _errorObserver(); return AppPage( - title: context.l10n.common_accounts, + title: context.l10n.accounts_title, bodyBuilder: (context) { return ListView( padding: context.systemPadding + @@ -82,26 +80,20 @@ class _AccountsScreenState extends ConsumerState { actionList: ActionList( buttons: [ ActionListButton( - title: context.l10n.common_auto_back_up, + title: context.l10n.auto_back_up_title, trailing: Consumer( builder: (context, ref, child) { final googleDriveAutoBackUp = ref.watch(AppPreferences.googleDriveAutoBackUp); return AppSwitch( value: googleDriveAutoBackUp, - onChanged: (bool value) { - ref - .read( - AppPreferences.googleDriveAutoBackUp.notifier, - ) - .state = value; - }, + onChanged: notifier.toggleAutoBackupInGoogleDrive, ); }, ), ), ActionListButton( - title: context.l10n.common_sign_out, + title: context.l10n.sign_out_title, onPressed: notifier.signOutWithGoogle, ), ], @@ -113,11 +105,11 @@ class _AccountsScreenState extends ConsumerState { buttons: [ ActionListButton( leading: SvgPicture.asset( - Assets.images.icons.googleDrive, + Assets.images.icons.icGoogleDrive, height: 24, width: 24, ), - title: context.l10n.sign_in_with_google_drive_text, + title: context.l10n.sign_in_with_google_drive_title, onPressed: () { notifier.signInWithGoogle(); }, @@ -142,24 +134,20 @@ class _AccountsScreenState extends ConsumerState { actionList: ActionList( buttons: [ ActionListButton( - title: context.l10n.common_auto_back_up, + title: context.l10n.auto_back_up_title, trailing: Consumer( builder: (context, ref, child) { final dropboxAutoBackUp = ref.watch(AppPreferences.dropboxAutoBackUp); return AppSwitch( value: dropboxAutoBackUp, - onChanged: (bool value) { - ref - .read(AppPreferences.dropboxAutoBackUp.notifier) - .state = value; - }, + onChanged: notifier.toggleAutoBackupInDropbox, ); }, ), ), ActionListButton( - title: context.l10n.common_sign_out, + title: context.l10n.sign_out_title, onPressed: notifier.signOutWithDropbox, ), ], @@ -171,11 +159,11 @@ class _AccountsScreenState extends ConsumerState { buttons: [ ActionListButton( leading: SvgPicture.asset( - Assets.images.icons.dropbox, + Assets.images.icons.icDropbox, height: 24, width: 24, ), - title: context.l10n.sign_in_with_dropbox_text, + title: context.l10n.sign_in_with_dropbox_title, onPressed: () { notifier.signInWithDropbox(); }, @@ -194,7 +182,7 @@ class _AccountsScreenState extends ConsumerState { return Visibility( visible: version != null, child: Text( - "${context.l10n.version_text} $version", + "${context.l10n.version_title} $version", style: AppTextStyles.body2.copyWith( color: context.colorScheme.textSecondary, ), diff --git a/app/lib/ui/flow/accounts/accounts_screen_view_model.dart b/app/lib/ui/flow/accounts/accounts_screen_view_model.dart index fa22f31..cbf9b79 100644 --- a/app/lib/ui/flow/accounts/accounts_screen_view_model.dart +++ b/app/lib/ui/flow/accounts/accounts_screen_view_model.dart @@ -1,9 +1,14 @@ import 'dart:async'; +import 'package:data/models/media_process/media_process.dart'; +import 'package:data/repositories/media_process_repository.dart'; import 'package:data/services/auth_service.dart'; import 'package:data/services/device_service.dart'; +import 'package:data/storage/app_preferences.dart'; +import 'package:data/storage/provider/preferences_provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:google_sign_in/google_sign_in.dart'; +import 'package:permission_handler/permission_handler.dart'; part 'accounts_screen_view_model.freezed.dart'; @@ -12,16 +17,30 @@ final accountsStateNotifierProvider = (ref) => AccountsStateNotifier( ref.read(deviceServiceProvider), ref.read(authServiceProvider), + ref.read(AppPreferences.dropboxAutoBackUp.notifier), + ref.read(AppPreferences.googleDriveAutoBackUp.notifier), + ref.read(mediaProcessRepoProvider), ), ); class AccountsStateNotifier extends StateNotifier { final DeviceService _deviceService; final AuthService _authService; + final PreferenceNotifier _autoBackupInGoogleDriveController; + final PreferenceNotifier _autoBackupInDropboxController; + final MediaProcessRepo _mediaProcessRepo; StreamSubscription? _googleAccountSubscription; - AccountsStateNotifier(this._deviceService, this._authService) - : super(AccountsState(googleAccount: _authService.googleAccount)); + AccountsStateNotifier( + this._deviceService, + this._authService, + this._autoBackupInDropboxController, + this._autoBackupInGoogleDriveController, + this._mediaProcessRepo, + ) : super(AccountsState(googleAccount: _authService.googleAccount)) { + init(); + updateNotificationsPermissionStatus(); + } Future init() async { _getAppVersion(); @@ -37,6 +56,13 @@ class AccountsStateNotifier extends StateNotifier { super.dispose(); } + Future updateNotificationsPermissionStatus({ + PermissionStatus? status, + }) async { + status ??= await Permission.notification.status; + state = state.copyWith(notificationsPermissionStatus: status.isGranted); + } + void updateUser(GoogleSignInAccount? account) { state = state.copyWith(googleAccount: account); } @@ -53,6 +79,8 @@ class AccountsStateNotifier extends StateNotifier { Future signOutWithGoogle() async { try { state = state.copyWith(error: null); + _mediaProcessRepo + .removeAllWaitingUploadsOfProvider(MediaProvider.googleDrive); await _authService.signOutWithGoogle(); } catch (e) { state = state.copyWith(error: e); @@ -71,12 +99,34 @@ class AccountsStateNotifier extends StateNotifier { Future signOutWithDropbox() async { try { state = state.copyWith(error: null); + _mediaProcessRepo + .removeAllWaitingUploadsOfProvider(MediaProvider.dropbox); await _authService.signOutWithDropBox(); } catch (e) { state = state.copyWith(error: e); } } + Future toggleAutoBackupInGoogleDrive(bool value) async { + _autoBackupInGoogleDriveController.state = value; + + if (value) { + _mediaProcessRepo.autoBackupInGoogleDrive(); + } else { + _mediaProcessRepo.stopAutoBackup(MediaProvider.googleDrive); + } + } + + Future toggleAutoBackupInDropbox(bool value) async { + _autoBackupInDropboxController.state = value; + + if (value) { + _mediaProcessRepo.autoBackupInDropbox(); + } else { + _mediaProcessRepo.stopAutoBackup(MediaProvider.dropbox); + } + } + Future _getAppVersion() async { final version = await _deviceService.version; state = state.copyWith(version: version); @@ -86,6 +136,7 @@ class AccountsStateNotifier extends StateNotifier { @freezed class AccountsState with _$AccountsState { const factory AccountsState({ + @Default(true) bool notificationsPermissionStatus, String? version, Object? error, GoogleSignInAccount? googleAccount, diff --git a/app/lib/ui/flow/accounts/accounts_screen_view_model.freezed.dart b/app/lib/ui/flow/accounts/accounts_screen_view_model.freezed.dart index b60a67d..e30fbe1 100644 --- a/app/lib/ui/flow/accounts/accounts_screen_view_model.freezed.dart +++ b/app/lib/ui/flow/accounts/accounts_screen_view_model.freezed.dart @@ -16,6 +16,7 @@ final _privateConstructorUsedError = UnsupportedError( /// @nodoc mixin _$AccountsState { + bool get notificationsPermissionStatus => throw _privateConstructorUsedError; String? get version => throw _privateConstructorUsedError; Object? get error => throw _privateConstructorUsedError; GoogleSignInAccount? get googleAccount => throw _privateConstructorUsedError; @@ -34,7 +35,10 @@ abstract class $AccountsStateCopyWith<$Res> { _$AccountsStateCopyWithImpl<$Res, AccountsState>; @useResult $Res call( - {String? version, Object? error, GoogleSignInAccount? googleAccount}); + {bool notificationsPermissionStatus, + String? version, + Object? error, + GoogleSignInAccount? googleAccount}); } /// @nodoc @@ -52,11 +56,16 @@ class _$AccountsStateCopyWithImpl<$Res, $Val extends AccountsState> @pragma('vm:prefer-inline') @override $Res call({ + Object? notificationsPermissionStatus = null, Object? version = freezed, Object? error = freezed, Object? googleAccount = freezed, }) { return _then(_value.copyWith( + notificationsPermissionStatus: null == notificationsPermissionStatus + ? _value.notificationsPermissionStatus + : notificationsPermissionStatus // ignore: cast_nullable_to_non_nullable + as bool, version: freezed == version ? _value.version : version // ignore: cast_nullable_to_non_nullable @@ -79,7 +88,10 @@ abstract class _$$AccountsStateImplCopyWith<$Res> @override @useResult $Res call( - {String? version, Object? error, GoogleSignInAccount? googleAccount}); + {bool notificationsPermissionStatus, + String? version, + Object? error, + GoogleSignInAccount? googleAccount}); } /// @nodoc @@ -95,11 +107,16 @@ class __$$AccountsStateImplCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ + Object? notificationsPermissionStatus = null, Object? version = freezed, Object? error = freezed, Object? googleAccount = freezed, }) { return _then(_$AccountsStateImpl( + notificationsPermissionStatus: null == notificationsPermissionStatus + ? _value.notificationsPermissionStatus + : notificationsPermissionStatus // ignore: cast_nullable_to_non_nullable + as bool, version: freezed == version ? _value.version : version // ignore: cast_nullable_to_non_nullable @@ -116,8 +133,15 @@ class __$$AccountsStateImplCopyWithImpl<$Res> /// @nodoc class _$AccountsStateImpl implements _AccountsState { - const _$AccountsStateImpl({this.version, this.error, this.googleAccount}); + const _$AccountsStateImpl( + {this.notificationsPermissionStatus = true, + this.version, + this.error, + this.googleAccount}); + @override + @JsonKey() + final bool notificationsPermissionStatus; @override final String? version; @override @@ -127,7 +151,7 @@ class _$AccountsStateImpl implements _AccountsState { @override String toString() { - return 'AccountsState(version: $version, error: $error, googleAccount: $googleAccount)'; + return 'AccountsState(notificationsPermissionStatus: $notificationsPermissionStatus, version: $version, error: $error, googleAccount: $googleAccount)'; } @override @@ -135,6 +159,10 @@ class _$AccountsStateImpl implements _AccountsState { return identical(this, other) || (other.runtimeType == runtimeType && other is _$AccountsStateImpl && + (identical(other.notificationsPermissionStatus, + notificationsPermissionStatus) || + other.notificationsPermissionStatus == + notificationsPermissionStatus) && (identical(other.version, version) || other.version == version) && const DeepCollectionEquality().equals(other.error, error) && (identical(other.googleAccount, googleAccount) || @@ -142,8 +170,8 @@ class _$AccountsStateImpl implements _AccountsState { } @override - int get hashCode => Object.hash(runtimeType, version, - const DeepCollectionEquality().hash(error), googleAccount); + int get hashCode => Object.hash(runtimeType, notificationsPermissionStatus, + version, const DeepCollectionEquality().hash(error), googleAccount); /// Create a copy of AccountsState /// with the given fields replaced by the non-null parameter values. @@ -156,10 +184,13 @@ class _$AccountsStateImpl implements _AccountsState { abstract class _AccountsState implements AccountsState { const factory _AccountsState( - {final String? version, + {final bool notificationsPermissionStatus, + final String? version, final Object? error, final GoogleSignInAccount? googleAccount}) = _$AccountsStateImpl; + @override + bool get notificationsPermissionStatus; @override String? get version; @override diff --git a/app/lib/ui/flow/accounts/components/settings_action_list.dart b/app/lib/ui/flow/accounts/components/settings_action_list.dart index 60275e0..a7e6d2b 100644 --- a/app/lib/ui/flow/accounts/components/settings_action_list.dart +++ b/app/lib/ui/flow/accounts/components/settings_action_list.dart @@ -1,3 +1,4 @@ +import 'package:permission_handler/permission_handler.dart'; import '../../../../components/web_view_screen.dart'; import '../../../../domain/extensions/context_extensions.dart'; import 'package:data/storage/app_preferences.dart'; @@ -7,40 +8,79 @@ import 'package:style/buttons/buttons_list.dart'; import 'package:style/buttons/segmented_button.dart'; import 'package:style/buttons/switch.dart'; import 'package:style/extensions/context_extensions.dart'; +import '../accounts_screen_view_model.dart'; -class SettingsActionList extends ConsumerWidget { +class SettingsActionList extends ConsumerStatefulWidget { const SettingsActionList({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState createState() => _SettingsActionListState(); +} + +class _SettingsActionListState extends ConsumerState + with WidgetsBindingObserver { + late AccountsStateNotifier notifier; + + @override + void initState() { + WidgetsBinding.instance.addObserver(this); + notifier = ref.read(accountsStateNotifierProvider.notifier); + super.initState(); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + notifier.updateNotificationsPermissionStatus(); + } + } + + @override + Widget build(BuildContext context) { final isDarkMode = ref.watch(AppPreferences.isDarkMode); final notifications = ref.watch(AppPreferences.notifications); + final notificationsPermissionStatusAllowed = ref.watch( + accountsStateNotifierProvider.select( + (value) => value.notificationsPermissionStatus, + ), + ); return ActionList( buttons: [ ActionListButton( - title: context.l10n.notification_text, + title: context.l10n.notification_title, trailing: AppSwitch( - value: notifications, - onChanged: (value) { - ref.read(AppPreferences.notifications.notifier).state = value; + value: notificationsPermissionStatusAllowed ? notifications : false, + onChanged: (value) async { + if (notificationsPermissionStatusAllowed) { + ref.read(AppPreferences.notifications.notifier).state = value; + } else { + final status = await Permission.notification.request(); + notifier.updateNotificationsPermissionStatus(status: status); + } }, ), ), ActionListButton( - title: context.l10n.theme_text, + title: context.l10n.theme_title, trailing: AppSegmentedButton( segments: [ AppButtonSegment( value: true, - label: context.l10n.dark_theme_text, + label: context.l10n.dark_theme_title, ), AppButtonSegment( value: false, - label: context.l10n.light_theme_text, + label: context.l10n.light_theme_title, ), AppButtonSegment( value: null, - label: context.l10n.system_theme_text, + label: context.l10n.system_theme_title, ), ], selected: isDarkMode, @@ -50,7 +90,7 @@ class SettingsActionList extends ConsumerWidget { ), ), ActionListButton( - title: context.l10n.common_term_and_condition, + title: context.l10n.term_and_condition_title, onPressed: () { final colors = _getWebPageColors(context, ref); showWebView( @@ -60,7 +100,7 @@ class SettingsActionList extends ConsumerWidget { }, ), ActionListButton( - title: context.l10n.common_privacy_policy, + title: context.l10n.privacy_policy_title, onPressed: () { final colors = _getWebPageColors(context, ref); showWebView( diff --git a/app/lib/ui/flow/home/components/app_media_item.dart b/app/lib/ui/flow/home/components/app_media_item.dart index 4cc0852..822173b 100644 --- a/app/lib/ui/flow/home/components/app_media_item.dart +++ b/app/lib/ui/flow/home/components/app_media_item.dart @@ -1,21 +1,23 @@ +import 'package:data/models/media/media_extension.dart'; +import 'package:data/models/media_process/media_process.dart'; +import 'package:flutter/material.dart'; +import 'package:style/indicators/circular_progress_indicator.dart'; import '../../../../components/thumbnail_builder.dart'; import '../../../../domain/formatter/duration_formatter.dart'; -import 'package:data/models/app_process/app_process.dart'; import 'package:data/models/media/media.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_svg/svg.dart'; import 'package:style/extensions/context_extensions.dart'; -import 'package:style/indicators/circular_progress_indicator.dart'; import 'package:style/text/app_text_style.dart'; -import '../../../../domain/assets/assets_paths.dart'; -import 'package:style/animations/item_selector.dart'; +import '../../../../gen/assets.gen.dart'; class AppMediaItem extends StatelessWidget { final AppMedia media; final void Function()? onTap; final void Function()? onLongTap; final bool isSelected; - final AppProcess? process; + final UploadMediaProcess? uploadMediaProcess; + final DownloadMediaProcess? downloadMediaProcess; const AppMediaItem({ super.key, @@ -23,134 +25,214 @@ class AppMediaItem extends StatelessWidget { this.onTap, this.onLongTap, this.isSelected = false, - this.process, + this.uploadMediaProcess, + this.downloadMediaProcess, }); @override Widget build(BuildContext context) { return LayoutBuilder( - builder: (context, constraints) => ItemSelector( + builder: (context, constraints) => GestureDetector( onTap: onTap, - onLongTap: onLongTap, - isSelected: isSelected, - child: ClipRRect( - borderRadius: BorderRadius.circular(4), - child: Stack( - alignment: Alignment.bottomLeft, - children: [ - AppMediaImage( + onLongPress: onLongTap, + child: Stack( + alignment: Alignment.bottomLeft, + children: [ + AnimatedOpacity( + curve: Curves.easeInOut, + duration: const Duration(milliseconds: 100), + opacity: isSelected ? 0.6 : 1, + child: AppMediaImage( + radius: isSelected ? 4 : 0, size: constraints.biggest, media: media, heroTag: media, ), - if (media.type.isVideo) _videoDuration(context), + ), + if (media.type.isVideo) _videoDuration(context), + if (!media.isLocalStored || + uploadMediaProcess != null || + downloadMediaProcess != null) _sourceIndicators(context: context), - ], - ), + if (isSelected) + Align( + alignment: Alignment.topLeft, + child: Container( + margin: const EdgeInsets.all(4), + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: context.colorScheme.primary, + ), + child: const Icon( + CupertinoIcons.checkmark_alt, + color: Colors.white, + size: 14, + ), + ), + ), + ], ), ), ); } Widget _videoDuration(BuildContext context) => Align( - alignment: Alignment.bottomRight, - child: _BackgroundContainer( + alignment: Alignment.topCenter, + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + stops: [0.0, 0.9], + begin: Alignment.topRight, + end: Alignment.bottomRight, + colors: [ + Colors.black.withOpacity(0.4), + Colors.transparent, + ], + ), + ), + padding: const EdgeInsets.all(4).copyWith(bottom: 8), child: Row( - mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, children: [ - Icon( - CupertinoIcons.play_fill, - color: context.colorScheme.surfaceInverse, - size: 14, - ), - const SizedBox(width: 2), Text( (media.videoDuration ?? Duration.zero).format, style: AppTextStyles.caption.copyWith( - color: context.colorScheme.surfaceInverse, + color: Colors.white, + fontSize: 12, + shadows: [ + Shadow( + color: Colors.grey.shade400, + blurRadius: 5, + ), + ], ), ), + const SizedBox(width: 2), + Icon( + CupertinoIcons.play_fill, + color: Colors.white, + size: 14, + shadows: [ + Shadow( + color: Colors.grey.shade400, + blurRadius: 5, + ), + ], + ), ], ), ), ); Widget _sourceIndicators({required BuildContext context}) { - return Row( - children: [ - if (media.sources.contains(AppMediaSource.googleDrive)) - _BackgroundContainer( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (media.sources.contains(AppMediaSource.googleDrive)) - SvgPicture.asset( - Assets.images.icons.googleDrive, - height: 14, - width: 14, - ), - ], + return Container( + padding: const EdgeInsets.all(4).copyWith(top: 10), + decoration: BoxDecoration( + gradient: LinearGradient( + stops: [0.0, 0.9], + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [ + Colors.black.withOpacity(0.4), + Colors.transparent, + ], + ), + ), + child: Row( + children: [ + if (media.sources.contains(AppMediaSource.googleDrive)) + Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white, + ), + child: SvgPicture.asset( + Assets.images.icons.icGoogleDrive, + height: 10, + width: 10, + ), ), - ), - if (process?.status.isProcessing ?? false) - _BackgroundContainer( - margin: EdgeInsets.symmetric( - vertical: 4, - horizontal: - media.sources.contains(AppMediaSource.googleDrive) ? 0 : 4, + if (media.sources.contains(AppMediaSource.dropbox) && + media.sources.contains(AppMediaSource.googleDrive)) + const SizedBox(width: 4), + if (media.sources.contains(AppMediaSource.dropbox)) + Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white, + ), + child: SvgPicture.asset( + Assets.images.icons.icDropbox, + height: 10, + width: 10, + ), ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - AppCircularProgressIndicator( - size: 16, - value: process?.progress?.percentageInPoint, - color: context.colorScheme.surfaceInverse, + Spacer(), + if (uploadMediaProcess != null && + uploadMediaProcess!.status.isWaiting || + downloadMediaProcess != null && + downloadMediaProcess!.status.isWaiting) + Icon( + Icons.watch_later_outlined, + size: 14, + color: Colors.white, + shadows: [ + Shadow( + color: Colors.grey.shade400, + blurRadius: 5, ), - if (process?.progress != null) ...[ - const SizedBox(width: 4), - Text( - '${process?.progress?.percentage.toStringAsFixed(0)}%', - style: AppTextStyles.caption.copyWith( - color: context.colorScheme.surfaceInverse, - ), - ), - ], ], ), - ), - if (process?.status.isWaiting ?? false) - _BackgroundContainer( - child: Icon( - CupertinoIcons.time, - size: 16, - color: context.colorScheme.surfaceInverse, + if (uploadMediaProcess != null && + uploadMediaProcess!.status.isRunning) + _progressIndicator( + context: context, + progressPercentage: uploadMediaProcess!.progressPercentage, + progress: uploadMediaProcess!.progress, ), - ), - ], + if (downloadMediaProcess != null && + downloadMediaProcess!.status.isRunning) + _progressIndicator( + context: context, + progressPercentage: downloadMediaProcess!.progressPercentage, + progress: downloadMediaProcess!.progress, + ), + ], + ), ); } -} -class _BackgroundContainer extends StatelessWidget { - final Widget child; - final EdgeInsets margin; - - const _BackgroundContainer({ - required this.child, - this.margin = const EdgeInsets.all(4), - }); - - @override - Widget build(BuildContext context) { - return Container( - margin: margin, - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: context.colorScheme.surface.withOpacity(0.6), - borderRadius: const BorderRadius.all(Radius.circular(4)), - ), - child: child, + Widget _progressIndicator({ + required BuildContext context, + required double progressPercentage, + required double progress, + }) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + AppCircularProgressIndicator( + value: progress, + color: Colors.white, + size: 14, + backgroundColor: Colors.white38, + ), + const SizedBox(width: 4), + Text( + "${uploadMediaProcess?.progressPercentage.toInt()}%", + style: AppTextStyles.caption.copyWith( + color: Colors.white, + shadows: [ + Shadow( + color: Colors.grey.shade400, + blurRadius: 5, + ), + ], + ), + ), + ], ); } } diff --git a/app/lib/ui/flow/home/components/hints.dart b/app/lib/ui/flow/home/components/hints.dart index 02c2b90..af1554f 100644 --- a/app/lib/ui/flow/home/components/hints.dart +++ b/app/lib/ui/flow/home/components/hints.dart @@ -17,7 +17,7 @@ class HomeScreenHints extends ConsumerWidget { if (!signInHintShown && googleAccount == null && dropboxAccount == null) { return HintView( - title: context.l10n.greetings_hey_there_text, + title: context.l10n.common_hey_there, hint: context.l10n.hint_sign_in_message, onClose: () { ref.read(AppPreferences.signInHintShown.notifier).state = true; diff --git a/app/lib/ui/flow/home/components/multi_selection_done_button.dart b/app/lib/ui/flow/home/components/multi_selection_done_button.dart index 9b96d57..54da5c0 100644 --- a/app/lib/ui/flow/home/components/multi_selection_done_button.dart +++ b/app/lib/ui/flow/home/components/multi_selection_done_button.dart @@ -1,9 +1,10 @@ import 'dart:io'; +import 'package:data/models/media/media_extension.dart'; import '../../../../components/app_dialog.dart'; import '../../../../domain/extensions/context_extensions.dart'; +import '../../../../gen/assets.gen.dart'; import '../home_screen_view_model.dart'; import 'package:data/models/media/media.dart'; -import 'package:data/models/media/media_extension.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -13,28 +14,21 @@ import 'package:share_plus/share_plus.dart'; import 'package:style/extensions/context_extensions.dart'; import '../../../../components/action_sheet.dart'; import '../../../../components/app_sheet.dart'; -import '../../../../domain/assets/assets_paths.dart'; class MultiSelectionDoneButton extends ConsumerWidget { const MultiSelectionDoneButton({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final notifier = ref.read(homeViewStateNotifier.notifier); - final selectedMedias = ref - .watch(homeViewStateNotifier.select((state) => state.selectedMedias)); - - final bool showDeleteFromDriveButton = selectedMedias - .any((element) => element.sources.contains(AppMediaSource.googleDrive)); - final bool showDeleteFromDeviceButton = selectedMedias - .any((element) => element.sources.contains(AppMediaSource.local)); - final bool showUploadToDriveButton = selectedMedias.any( - (element) => !element.sources.contains(AppMediaSource.googleDrive), + final state = ref.watch( + homeViewStateNotifier.select( + (state) => ( + selectedMedias: state.selectedMedias, + googleAccount: state.googleAccount, + dropboxAccount: state.dropboxAccount + ), + ), ); - final bool showDownloadButton = - selectedMedias.any((element) => element.isGoogleDriveStored); - final bool showShareButton = - selectedMedias.any((element) => element.isLocalStored); return FloatingActionButton( elevation: 3, @@ -45,163 +39,47 @@ class MultiSelectionDoneButton extends ConsumerWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - if (showUploadToDriveButton) - AppSheetAction( - icon: Stack( - alignment: Alignment.bottomRight, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 0, right: 8), - child: Icon( - CupertinoIcons.cloud_upload, - color: context.colorScheme.textSecondary, - size: 22, - ), - ), - SvgPicture.asset( - Assets.images.icons.googleDrive, - width: 14, - height: 14, - ), - ], - ), - title: context.l10n.back_up_on_google_drive_text, - onPressed: () { - notifier.backUpMediaOnGoogleDrive(); - context.pop(); - }, - ), - if (showDeleteFromDeviceButton) - AppSheetAction( - icon: const Icon( - CupertinoIcons.delete, - size: 24, - ), - title: context.l10n.common_delete_from_device, - onPressed: () { - showAppAlertDialog( - context: context, - title: context.l10n.common_delete_from_device, - message: context - .l10n.delete_media_from_device_confirmation_message, - actions: [ - AppAlertAction( - title: context.l10n.common_cancel, - onPressed: () { - context.pop(); - }, - ), - AppAlertAction( - isDestructiveAction: true, - title: context.l10n.common_delete, - onPressed: () { - notifier.deleteMediasFromLocal(); - context.pop(); - }, - ), - ], - ); - }, - ), - if (showDeleteFromDriveButton) - AppSheetAction( - icon: Stack( - alignment: Alignment.bottomRight, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 2, right: 2), - child: Icon( - CupertinoIcons.trash, - color: context.colorScheme.textSecondary, - size: 22, - ), - ), - SvgPicture.asset( - Assets.images.icons.googleDrive, - width: 14, - height: 14, - ), - ], - ), - title: context.l10n.common_delete_from_google_drive, - onPressed: () { - context.pop(); - showAppAlertDialog( - context: context, - title: context.l10n.common_delete_from_google_drive, - message: context.l10n - .delete_media_from_google_drive_confirmation_message, - actions: [ - AppAlertAction( - title: context.l10n.common_cancel, - onPressed: () { - context.pop(); - }, - ), - AppAlertAction( - isDestructiveAction: true, - title: context.l10n.common_delete, - onPressed: () { - notifier.deleteMediasFromGoogleDrive(); - context.pop(); - }, - ), - ], - ); - }, - ), - if (showDownloadButton) - AppSheetAction( - icon: Icon( - CupertinoIcons.cloud_download, - size: 24, - color: context.colorScheme.textSecondary, - ), - title: context.l10n.download_from_google_drive_text, - onPressed: () async { - context.pop(); - showAppAlertDialog( - context: context, - title: context.l10n.download_from_google_drive_text, - message: - context.l10n.download_from_google_drive_alert_message, - actions: [ - AppAlertAction( - title: context.l10n.common_cancel, - onPressed: () { - context.pop(); - }, - ), - AppAlertAction( - isDestructiveAction: true, - title: context.l10n.common_download, - onPressed: () { - notifier.downloadMediaFromGoogleDrive(); - context.pop(); - }, - ), - ], - ); - }, - ), - if (showShareButton) - AppSheetAction( - icon: Icon( - Platform.isIOS ? CupertinoIcons.share : Icons.share_rounded, - color: context.colorScheme.textSecondary, - size: 24, - ), - title: context.l10n.common_share, - onPressed: () { - Share.shareXFiles( - selectedMedias - .where((element) => element.isLocalStored) - .map((e) => XFile(e.path)) - .toList(), - ); - context.pop(); - }, - ), + if (state.selectedMedias.values.any( + (element) => + !element.sources.contains(AppMediaSource.googleDrive) && + element.sources.contains(AppMediaSource.local) && + state.googleAccount != null, + )) + _uploadToGoogleDriveAction(context, ref), + if (state.selectedMedias.values + .any((element) => element.isGoogleDriveStored) && + state.googleAccount != null) + _downloadFromGoogleDriveAction(context, ref), + if (state.selectedMedias.values.any( + (element) => + element.sources.contains(AppMediaSource.googleDrive), + ) && + state.googleAccount != null) + _deleteMediaFromGoogleDriveAction(context, ref), + if (state.selectedMedias.values.any( + (element) => + !element.sources.contains(AppMediaSource.dropbox) && + element.sources.contains(AppMediaSource.local) && + state.dropboxAccount != null, + )) + _uploadToDropboxAction(context, ref), + if (state.selectedMedias.values + .any((element) => element.isDropboxStored) && + state.dropboxAccount != null) + _downloadFromDropboxAction(context, ref), + if (state.selectedMedias.values.any( + (element) => + element.sources.contains(AppMediaSource.dropbox), + ) && + state.dropboxAccount != null) + _deleteMediaFromDropboxAction(context, ref), + if (state.selectedMedias.values.any( + (element) => element.sources.contains(AppMediaSource.local), + )) + _deleteFromDevice(context, ref), + if (state.selectedMedias.values + .any((element) => element.isLocalStored)) + _shareAction(context, state.selectedMedias), ], ), ); @@ -212,4 +90,352 @@ class MultiSelectionDoneButton extends ConsumerWidget { ), ); } + + Widget _uploadToGoogleDriveAction(BuildContext context, WidgetRef ref) { + return AppSheetAction( + icon: Stack( + alignment: Alignment.bottomRight, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 0, right: 8), + child: Icon( + CupertinoIcons.cloud_upload, + color: context.colorScheme.textSecondary, + size: 22, + ), + ), + SvgPicture.asset( + Assets.images.icons.icGoogleDrive, + width: 14, + height: 14, + ), + ], + ), + title: context.l10n.upload_to_google_drive_title, + onPressed: () { + context.pop(); + showAppAlertDialog( + context: context, + title: context.l10n.upload_to_google_drive_title, + message: context.l10n.upload_to_google_drive_confirmation_message, + actions: [ + AppAlertAction( + title: context.l10n.common_cancel, + onPressed: () { + context.pop(); + }, + ), + AppAlertAction( + title: context.l10n.common_upload, + onPressed: () { + ref.read(homeViewStateNotifier.notifier).uploadToGoogleDrive(); + context.pop(); + }, + ), + ], + ); + }, + ); + } + + Widget _downloadFromGoogleDriveAction(BuildContext context, WidgetRef ref) { + return AppSheetAction( + icon: Stack( + alignment: Alignment.bottomRight, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 0, right: 8), + child: Icon( + CupertinoIcons.cloud_download, + color: context.colorScheme.textSecondary, + size: 22, + ), + ), + SvgPicture.asset( + Assets.images.icons.icGoogleDrive, + width: 14, + height: 14, + ), + ], + ), + title: context.l10n.download_from_google_drive_title, + onPressed: () async { + context.pop(); + showAppAlertDialog( + context: context, + title: context.l10n.download_from_google_drive_title, + message: context.l10n.download_from_google_drive_confirmation_message, + actions: [ + AppAlertAction( + title: context.l10n.common_cancel, + onPressed: () { + context.pop(); + }, + ), + AppAlertAction( + title: context.l10n.common_download, + onPressed: () { + ref + .read(homeViewStateNotifier.notifier) + .downloadFromGoogleDrive(); + context.pop(); + }, + ), + ], + ); + }, + ); + } + + Widget _deleteMediaFromGoogleDriveAction( + BuildContext context, + WidgetRef ref, + ) { + return AppSheetAction( + icon: Stack( + alignment: Alignment.bottomRight, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 2, right: 2), + child: Icon( + CupertinoIcons.trash, + color: context.colorScheme.textSecondary, + size: 22, + ), + ), + SvgPicture.asset( + Assets.images.icons.icGoogleDrive, + width: 14, + height: 14, + ), + ], + ), + title: context.l10n.delete_from_google_drive_title, + onPressed: () { + context.pop(); + showAppAlertDialog( + context: context, + title: context.l10n.delete_from_google_drive_title, + message: context.l10n.delete_from_google_drive_confirmation_message, + actions: [ + AppAlertAction( + title: context.l10n.common_cancel, + onPressed: () { + context.pop(); + }, + ), + AppAlertAction( + isDestructiveAction: true, + title: context.l10n.common_delete, + onPressed: () { + ref + .read(homeViewStateNotifier.notifier) + .deleteGoogleDriveMedias(); + context.pop(); + }, + ), + ], + ); + }, + ); + } + + Widget _uploadToDropboxAction(BuildContext context, WidgetRef ref) { + return AppSheetAction( + icon: Stack( + alignment: Alignment.bottomRight, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 0, right: 8), + child: Icon( + CupertinoIcons.cloud_upload, + color: context.colorScheme.textSecondary, + size: 22, + ), + ), + SvgPicture.asset( + Assets.images.icons.icDropbox, + width: 14, + height: 14, + ), + ], + ), + title: context.l10n.upload_to_dropbox_title, + onPressed: () { + context.pop(); + showAppAlertDialog( + context: context, + title: context.l10n.upload_to_dropbox_title, + message: context.l10n.upload_to_dropbox_confirmation_message, + actions: [ + AppAlertAction( + title: context.l10n.common_cancel, + onPressed: () { + context.pop(); + }, + ), + AppAlertAction( + title: context.l10n.common_upload, + onPressed: () { + ref.read(homeViewStateNotifier.notifier).uploadToDropbox(); + context.pop(); + }, + ), + ], + ); + }, + ); + } + + Widget _downloadFromDropboxAction(BuildContext context, WidgetRef ref) { + return AppSheetAction( + icon: Stack( + alignment: Alignment.bottomRight, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 0, right: 8), + child: Icon( + CupertinoIcons.cloud_download, + color: context.colorScheme.textSecondary, + size: 22, + ), + ), + SvgPicture.asset( + Assets.images.icons.icDropbox, + width: 14, + height: 14, + ), + ], + ), + title: context.l10n.download_from_dropbox_title, + onPressed: () async { + context.pop(); + showAppAlertDialog( + context: context, + title: context.l10n.download_from_dropbox_title, + message: context.l10n.download_from_dropbox_confirmation_message, + actions: [ + AppAlertAction( + title: context.l10n.common_cancel, + onPressed: () { + context.pop(); + }, + ), + AppAlertAction( + title: context.l10n.common_download, + onPressed: () { + ref.read(homeViewStateNotifier.notifier).downloadFromDropbox(); + context.pop(); + }, + ), + ], + ); + }, + ); + } + + Widget _deleteMediaFromDropboxAction(BuildContext context, WidgetRef ref) { + return AppSheetAction( + icon: Stack( + alignment: Alignment.bottomRight, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 2, right: 2), + child: Icon( + CupertinoIcons.trash, + color: context.colorScheme.textSecondary, + size: 22, + ), + ), + SvgPicture.asset( + Assets.images.icons.icDropbox, + width: 14, + height: 14, + ), + ], + ), + title: context.l10n.delete_from_dropbox_title, + onPressed: () { + context.pop(); + showAppAlertDialog( + context: context, + title: context.l10n.delete_from_dropbox_title, + message: context.l10n.delete_from_dropbox_confirmation_message, + actions: [ + AppAlertAction( + title: context.l10n.common_cancel, + onPressed: () { + context.pop(); + }, + ), + AppAlertAction( + isDestructiveAction: true, + title: context.l10n.common_delete, + onPressed: () { + ref.read(homeViewStateNotifier.notifier).deleteDropboxMedias(); + context.pop(); + }, + ), + ], + ); + }, + ); + } + + Widget _deleteFromDevice(BuildContext context, WidgetRef ref) { + return AppSheetAction( + icon: const Icon( + CupertinoIcons.delete, + size: 24, + ), + title: context.l10n.delete_from_device_title, + onPressed: () { + context.pop(); + showAppAlertDialog( + context: context, + title: context.l10n.delete_from_device_title, + message: context.l10n.delete_from_device_confirmation_message, + actions: [ + AppAlertAction( + title: context.l10n.common_cancel, + onPressed: () { + context.pop(); + }, + ), + AppAlertAction( + isDestructiveAction: true, + title: context.l10n.common_delete, + onPressed: () { + ref.read(homeViewStateNotifier.notifier).deleteLocalMedias(); + context.pop(); + }, + ), + ], + ); + }, + ); + } + + Widget _shareAction( + BuildContext context, + Map selectedMedias, + ) { + return AppSheetAction( + icon: Icon( + Platform.isIOS ? CupertinoIcons.share : Icons.share_rounded, + color: context.colorScheme.textSecondary, + size: 24, + ), + title: context.l10n.common_share, + onPressed: () { + Share.shareXFiles( + selectedMedias.values + .where((element) => element.isLocalStored) + .map((e) => XFile(e.path)) + .toList(), + ); + context.pop(); + }, + ); + } } diff --git a/app/lib/ui/flow/home/components/no_local_medias_access_screen.dart b/app/lib/ui/flow/home/components/no_local_medias_access_screen.dart index cd3bbb9..06a0512 100644 --- a/app/lib/ui/flow/home/components/no_local_medias_access_screen.dart +++ b/app/lib/ui/flow/home/components/no_local_medias_access_screen.dart @@ -13,19 +13,19 @@ class NoLocalMediasAccessScreen extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final notifier = ref.read(homeViewStateNotifier.notifier); return ErrorView( - title: context.l10n.cant_find_media_title, + title: context.l10n.no_media_access_screen_title, icon: Icon( - CupertinoIcons.photo, - color: context.colorScheme.containerHighOnSurface, - size: 100, + CupertinoIcons.photo_on_rectangle, + color: context.colorScheme.containerNormalOnSurface, + size: 120, ), - message: context.l10n.ask_for_media_permission_message, + message: context.l10n.no_media_access_screen_message, action: ErrorViewAction( onPressed: () async { await openAppSettings(); - await notifier.loadLocalMedia(); + notifier.loadMedias(reload: true); }, - title: context.l10n.load_local_media_button_text, + title: context.l10n.common_open_settings, ), ); } diff --git a/app/lib/ui/flow/home/home_screen.dart b/app/lib/ui/flow/home/home_screen.dart index 8039dcd..e325338 100644 --- a/app/lib/ui/flow/home/home_screen.dart +++ b/app/lib/ui/flow/home/home_screen.dart @@ -1,14 +1,13 @@ import 'dart:io'; +import 'package:flutter/services.dart'; import '../../../components/app_page.dart'; +import '../../../components/error_view.dart'; import '../../../domain/extensions/widget_extensions.dart'; import '../../../domain/formatter/date_formatter.dart'; import '../../../domain/extensions/context_extensions.dart'; -import '../../../domain/handlers/notification_handler.dart'; +import '../../../gen/assets.gen.dart'; import 'components/no_local_medias_access_screen.dart'; import 'home_screen_view_model.dart'; -import 'package:collection/collection.dart'; -import 'package:data/models/app_process/app_process.dart'; -import 'package:data/models/media/media.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -16,7 +15,6 @@ import 'package:style/extensions/context_extensions.dart'; import 'package:style/indicators/circular_progress_indicator.dart'; import 'package:style/text/app_text_style.dart'; import '../../../components/snack_bar.dart'; -import '../../../domain/assets/assets_paths.dart'; import '../../navigation/app_route.dart'; import 'components/app_media_item.dart'; import 'components/hints.dart'; @@ -33,14 +31,10 @@ class HomeScreen extends ConsumerStatefulWidget { class _HomeScreenState extends ConsumerState { late HomeViewStateNotifier _notifier; - late NotificationHandler _notificationHandler; final _scrollController = ScrollController(); @override void initState() { - _notificationHandler = ref.read(notificationHandlerProvider); - _notificationHandler.init(context); - _notificationHandler.requestPermission(); _notifier = ref.read(homeViewStateNotifier.notifier); super.initState(); } @@ -53,7 +47,7 @@ class _HomeScreenState extends ConsumerState { void _errorObserver() { ref.listen( - homeViewStateNotifier.select((value) => value.error), + homeViewStateNotifier.select((value) => value.actionError), (previous, next) { if (next != null) { showErrorSnackBar(context: context, error: next); @@ -62,131 +56,49 @@ class _HomeScreenState extends ConsumerState { ); } - void _notificationObserver() { - ref.listen(homeViewStateNotifier, (previous, next) { - if ((previous?.mediaProcesses.isEmpty ?? false) && - next.mediaProcesses.isNotEmpty) { - _notificationHandler.showNotification( - id: next.mediaProcesses.length, - name: "Sync to Google Drive", - description: "Syncing media files to Google Drive.", - ); - } - }); - } - @override Widget build(BuildContext context) { _errorObserver(); - _notificationObserver(); return AppPage( - titleWidget: _titleWidget(context: context), - actions: [ - FadeInSwitcher(child: _transferButton(context)), - _accountButton(context), + titleWidget: const HomeAppTitle(), + actions: const [ + HomeTransferButton(), + SizedBox(width: 8), + HomeAccountButton(), ], body: FadeInSwitcher(child: _body(context: context)), ); } - Widget _titleWidget({required BuildContext context}) { - return Row( - children: [ - if (Platform.isIOS) const SizedBox(width: 10), - Image.asset( - Assets.images.appIcon, - width: 28, - ), - const SizedBox(width: 10), - Text(context.l10n.app_name), - ], - ); - } - - Widget _transferButton(BuildContext context) { - return Consumer( - builder: (context, ref, child) { - final showTransferButton = ref.watch( - homeViewStateNotifier.select((value) => value.showTransfer), - ); - return Visibility( - visible: showTransferButton, - child: Padding( - padding: const EdgeInsets.only(right: 8.0), - child: ActionButton( - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - size: 36, - backgroundColor: context.colorScheme.containerNormal, - onPressed: () async { - await TransferRoute().push(context); - _notifier.loadMedias(); - }, - icon: Icon( - CupertinoIcons.arrow_up_arrow_down, - color: context.colorScheme.textSecondary, - size: 18, - ), - ), - ), - ); - }, - ); - } - - Widget _accountButton(BuildContext context) { - return ActionButton( - size: 36, - backgroundColor: context.colorScheme.containerNormal, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - onPressed: () async { - await AccountRoute().push(context); - _notifier.loadMedias(); - }, - icon: Icon( - CupertinoIcons.person, - color: context.colorScheme.textSecondary, - size: 18, - ), - ); - } - Widget _body({required BuildContext context}) { - final ({ - Map> medias, - List mediaProcesses, - List selectedMedias, - bool isLoading, - bool hasLocalMediaAccess, - String? lastLocalMediaId - }) state = ref.watch( + final state = ref.watch( homeViewStateNotifier.select( (value) => ( - medias: value.medias, - mediaProcesses: value.mediaProcesses, - selectedMedias: value.selectedMedias, + hasMedia: value.medias.isNotEmpty, + hasSelectedMedia: value.selectedMedias.isNotEmpty, isLoading: value.loading, hasLocalMediaAccess: value.hasLocalMediaAccess, - lastLocalMediaId: value.lastLocalMediaId, + error: value.error, ), ), ); - if (state.isLoading) { + if (state.isLoading && !state.hasMedia) { return const Center(child: AppCircularProgressIndicator()); - } else if (state.medias.isEmpty && !state.hasLocalMediaAccess) { + } else if (!state.hasMedia && !state.hasLocalMediaAccess) { return const NoLocalMediasAccessScreen(); + } else if (state.error != null) { + return ErrorView( + title: context.l10n.unable_to_load_media_error, + message: context.l10n.unable_to_load_media_message, + ); } + return Stack( alignment: Alignment.bottomRight, children: [ - _buildMediaList( - context: context, - medias: state.medias, - mediaProcesses: state.mediaProcesses, - selectedMedias: state.selectedMedias, - lastLocalMediaId: state.lastLocalMediaId, - ), - if (state.selectedMedias.isNotEmpty) + _buildMediaList(context: context), + if (state.hasSelectedMedia) Padding( padding: context.systemPadding + const EdgeInsets.all(16), child: const MultiSelectionDoneButton(), @@ -195,24 +107,44 @@ class _HomeScreenState extends ConsumerState { ); } - Widget _buildMediaList({ - required BuildContext context, - required Map> medias, - required List mediaProcesses, - required String? lastLocalMediaId, - required List selectedMedias, - }) { + Widget _buildMediaList({required BuildContext context}) { + final state = ref.watch( + homeViewStateNotifier.select( + (value) => ( + medias: value.medias, + uploadMediaProcesses: value.uploadMediaProcesses, + downloadMediaProcesses: value.downloadMediaProcesses, + loading: value.loading, + selectedMedias: value.selectedMedias, + lastLocalMediaId: value.lastLocalMediaId, + ), + ), + ); + return Scrollbar( controller: _scrollController, interactive: true, child: ListView.builder( controller: _scrollController, - itemCount: medias.length + 1, + itemCount: state.medias.length + 2, itemBuilder: (context, index) { if (index == 0) { return const HomeScreenHints(); + } else if (index == state.medias.length + 1) { + return FadeInSwitcher( + child: state.loading + ? const Center( + child: Padding( + padding: EdgeInsets.all(16), + child: AppCircularProgressIndicator( + size: 20, + ), + ), + ) + : const SizedBox(), + ); } else { - final gridEntry = medias.entries.elementAt(index - 1); + final gridEntry = state.medias.entries.elementAt(index - 1); return Column( children: [ Container( @@ -241,38 +173,42 @@ class _HomeScreenState extends ConsumerState { crossAxisSpacing: 4, mainAxisSpacing: 4, ), - itemCount: gridEntry.value.length, + itemCount: gridEntry.value.entries.length, itemBuilder: (context, index) { - final media = gridEntry.value[index]; - if (media.id == lastLocalMediaId) { + final media = + gridEntry.value.entries.elementAt(index).value; + + if (media.id == state.lastLocalMediaId) { runPostFrame(() { - _notifier.loadLocalMedia(append: true); + _notifier.loadMedias(); }); } + return AppMediaItem( key: ValueKey(media.id), onTap: () async { - if (selectedMedias.isNotEmpty) { + if (state.selectedMedias.isNotEmpty) { _notifier.toggleMediaSelection(media); + HapticFeedback.lightImpact(); } else { await MediaPreviewRoute( $extra: MediaPreviewRouteData( - medias: medias.values - .expand((element) => element) + medias: state.medias.values + .expand((element) => element.values) .toList(), startFrom: media.id, ), ).push(context); - _notifier.loadMedias(); } }, onLongTap: () { _notifier.toggleMediaSelection(media); + HapticFeedback.lightImpact(); }, - isSelected: selectedMedias.contains(media), - process: mediaProcesses.firstWhereOrNull( - (process) => process.id == media.id, - ), + isSelected: state.selectedMedias.containsKey(media.id), + uploadMediaProcess: state.uploadMediaProcesses[media.id], + downloadMediaProcess: + state.downloadMediaProcesses[media.id], media: media, ); }, @@ -285,3 +221,69 @@ class _HomeScreenState extends ConsumerState { ); } } + +class HomeAppTitle extends StatelessWidget { + const HomeAppTitle({super.key}); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + if (Platform.isIOS) const SizedBox(width: 10), + Image.asset( + Assets.images.appLogo.path, + width: 28, + ), + const SizedBox(width: 10), + Text( + context.l10n.app_name, + style: AppTextStyles.header3.copyWith( + color: context.colorScheme.textPrimary, + ), + ), + ], + ); + } +} + +class HomeAccountButton extends StatelessWidget { + const HomeAccountButton({super.key}); + + @override + Widget build(BuildContext context) { + return ActionButton( + size: 36, + backgroundColor: context.colorScheme.containerNormal, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + onPressed: () async { + await AccountRoute().push(context); + }, + icon: Icon( + CupertinoIcons.person, + color: context.colorScheme.textSecondary, + size: 18, + ), + ); + } +} + +class HomeTransferButton extends StatelessWidget { + const HomeTransferButton({super.key}); + + @override + Widget build(BuildContext context) { + return ActionButton( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + size: 36, + backgroundColor: context.colorScheme.containerNormal, + onPressed: () async { + await TransferRoute().push(context); + }, + icon: Icon( + CupertinoIcons.arrow_up_arrow_down, + color: context.colorScheme.textSecondary, + size: 18, + ), + ); + } +} diff --git a/app/lib/ui/flow/home/home_screen_view_model.dart b/app/lib/ui/flow/home/home_screen_view_model.dart index 654aa1b..e35e614 100644 --- a/app/lib/ui/flow/home/home_screen_view_model.dart +++ b/app/lib/ui/flow/home/home_screen_view_model.dart @@ -1,10 +1,10 @@ import 'dart:async'; -import '../../../domain/extensions/map_extensions.dart'; -import '../../../domain/extensions/media_list_extension.dart'; -import 'package:data/models/app_process/app_process.dart'; -import 'package:data/models/media/media.dart'; +import 'package:data/log/logger.dart'; +import 'package:data/models/dropbox/account/dropbox_account.dart'; import 'package:data/models/media/media_extension.dart'; -import 'package:data/repositories/google_drive_process_repo.dart'; +import 'package:data/models/media_process/media_process.dart'; +import 'package:data/services/dropbox_services.dart'; +import 'package:data/models/media/media.dart'; import 'package:data/services/auth_service.dart'; import 'package:data/services/google_drive_service.dart'; import 'package:data/services/local_media_service.dart'; @@ -12,358 +12,595 @@ import 'package:data/storage/app_preferences.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:google_sign_in/google_sign_in.dart'; -import 'package:style/extensions/list_extensions.dart'; +import 'package:logger/logger.dart'; import 'home_view_model_helper_mixin.dart'; +import 'package:data/repositories/media_process_repository.dart'; +import 'package:data/domain/config.dart'; part 'home_screen_view_model.freezed.dart'; final homeViewStateNotifier = StateNotifierProvider.autoDispose( (ref) { - final homeView = HomeViewStateNotifier( + final notifier = HomeViewStateNotifier( ref.read(localMediaServiceProvider), ref.read(googleDriveServiceProvider), + ref.read(dropboxServiceProvider), ref.read(authServiceProvider), - ref.read(googleDriveProcessRepoProvider), - ref.read(AppPreferences.googleDriveAutoBackUp), + ref.read(mediaProcessRepoProvider), + ref.read(loggerProvider), + ref.read(AppPreferences.dropboxCurrentUserAccount), ); - - ref.listen(AppPreferences.googleDriveAutoBackUp, (previous, next) { - homeView.updateAutoBackUpStatus(next); + ref.listen(AppPreferences.dropboxCurrentUserAccount, (previous, next) { + notifier.updateDropboxAccount(next); }); - return homeView; + return notifier; }); class HomeViewStateNotifier extends StateNotifier with HomeViewModelHelperMixin { - bool _autoBackUpStatus; final AuthService _authService; + final Logger _logger; final GoogleDriveService _googleDriveService; - final GoogleDriveProcessRepo _googleDriveProcessRepo; + final DropboxService _dropboxService; final LocalMediaService _localMediaService; + final MediaProcessRepo _mediaProcessRepo; StreamSubscription? _googleAccountSubscription; - List _uploadedMedia = []; + // Local + int _localMediaCount = 0; + bool _localMaxLoaded = false; + + // Google Drive String? _backUpFolderId; - bool _isGoogleDriveLoading = false; - bool _isLocalMediaLoading = false; - bool _isMaxLocalMediaLoaded = false; + String? _googleDrivePageToken; + bool _googleDriveMaxLoaded = false; + final List _googleDriveMediasWithLocalRef = []; + + // Dropbox + String? _dropboxPageToken; + bool _dropboxMaxLoaded = false; + final List _dropboxMediasWithLocalRef = []; HomeViewStateNotifier( this._localMediaService, this._googleDriveService, + this._dropboxService, this._authService, - this._googleDriveProcessRepo, - this._autoBackUpStatus, - ) : super(const HomeViewState()) { + this._mediaProcessRepo, + this._logger, + DropboxAccount? _dropboxAccount, + ) : super(HomeViewState(dropboxAccount: _dropboxAccount)) { + _mediaProcessRepo.addListener(_mediaProcessObserve); _listenUserGoogleAccount(); - _googleDriveProcessRepo.setBackUpFolderId(_backUpFolderId); - _googleDriveProcessRepo.addListener(_listenGoogleDriveProcess); loadMedias(); - _checkAutoBackUp(); - } - - void updateAutoBackUpStatus(bool status) { - _autoBackUpStatus = status; - _checkAutoBackUp(); - if (!status) { - _googleDriveProcessRepo.terminateAllAutoBackupProcess(); - } + _mediaProcessObserve(); } - void _checkAutoBackUp() { - if (_autoBackUpStatus) { - _googleDriveProcessRepo.autoBackInGoogleDrive(); - } - } + // ACCOUNT LISTENERS --------------------------------------------------------- + /// Listen to google account changes and update the state accordingly. void _listenUserGoogleAccount() { _googleAccountSubscription = _authService.onGoogleAccountChange.listen((event) async { - state = state.copyWith(googleAccount: event); - _googleDriveProcessRepo.clearAllQueue(); if (event != null) { - _backUpFolderId = await _googleDriveService.getBackupFolderId(); - _googleDriveProcessRepo.setBackUpFolderId(_backUpFolderId); - await loadGoogleDriveMedia(); + state = state.copyWith(googleAccount: event); + _backUpFolderId = await _googleDriveService.getBackUpFolderId(); + loadMedias(reload: true); } else { _backUpFolderId = null; - _uploadedMedia.clear(); state = state.copyWith( - medias: removeGoogleDriveRefFromMediaMap(medias: state.medias), + googleAccount: null, + medias: mediaMapUpdate( + update: (media) { + if (media.driveMediaRefId != null && + media.sources.contains(AppMediaSource.googleDrive) && + media.sources.length > 1) { + return media.removeGoogleDriveRef(); + } else if (!media.sources.contains(AppMediaSource.googleDrive) && + media.driveMediaRefId == null) { + return media; + } + return null; + }, + medias: state.medias, + ), ); } }); } - void _listenGoogleDriveProcess() { - final successUploads = _googleDriveProcessRepo.uploadQueue - .where((element) => element.status.isSuccess); - - final successDeletes = _googleDriveProcessRepo.deleteQueue - .where((element) => element.status.isSuccess) - .map((e) => e.id); - - final successDownloads = _googleDriveProcessRepo.downloadQueue - .where((element) => element.status.isSuccess); - - if (successUploads.isNotEmpty) { + /// Listen to dropbox account changes and update the state accordingly. + void updateDropboxAccount(DropboxAccount? dropboxAccount) { + if (dropboxAccount == null) { state = state.copyWith( - medias: addGoogleDriveRefInMediaMap( + dropboxAccount: null, + medias: mediaMapUpdate( + update: (media) { + if (media.dropboxMediaRefId != null && + media.sources.contains(AppMediaSource.dropbox) && + media.sources.length > 1) { + return media.removeDropboxRef(); + } else if (!media.sources.contains(AppMediaSource.dropbox) && + media.dropboxMediaRefId == null) { + return media; + } + return null; + }, medias: state.medias, - process: successUploads.toList(), - ), - ); - } - - if (successDeletes.isNotEmpty) { - state = state.copyWith( - medias: removeGoogleDriveRefFromMediaMap( - medias: state.medias, - removeFromIds: successDeletes.toList(), ), ); + } else { + state = state.copyWith(dropboxAccount: dropboxAccount); + loadMedias(reload: true); } + } - if (successDownloads.isNotEmpty) { - state = state.copyWith( - medias: replaceMediaRefInMediaMap( - medias: state.medias, - process: successDownloads.toList(), - ), - ); - } + // MEDIA PROCESS OBSERVER ---------------------------------------------------- + void _mediaProcessObserve() { state = state.copyWith( - mediaProcesses: [ - ..._googleDriveProcessRepo.uploadQueue, - ..._googleDriveProcessRepo.deleteQueue, - ..._googleDriveProcessRepo.downloadQueue, - ], - showTransfer: _googleDriveProcessRepo.uploadQueue.isNotEmpty || - _googleDriveProcessRepo.downloadQueue.isNotEmpty, + uploadMediaProcesses: Map.fromEntries( + _mediaProcessRepo.uploadQueue.map((e) => MapEntry(e.media_id, e)), + ), + downloadMediaProcesses: Map.fromEntries( + _mediaProcessRepo.downloadQueue.map((e) => MapEntry(e.media_id, e)), + ), ); - } - Future loadMedias() async { - state = state.copyWith(loading: state.medias.isEmpty, error: null); - final hasAccess = await _localMediaService.requestPermission(); - state = state.copyWith(hasLocalMediaAccess: hasAccess, loading: false); - if (hasAccess) { - await Future.wait([loadLocalMedia(), loadGoogleDriveMedia()]); - } else { - await loadGoogleDriveMedia(); + for (final process in _mediaProcessRepo.uploadQueue) { + if (process.status.isCompleted) { + state = state.copyWith( + medias: mediaMapUpdate( + update: (media) { + if (media.id == process.media_id && + process.provider == MediaProvider.googleDrive && + !media.sources.contains(AppMediaSource.googleDrive) && + process.response != null) { + return media.mergeGoogleDriveMedia(process.response!); + } else if (media.id == process.media_id && + process.provider == MediaProvider.dropbox && + !media.sources.contains(AppMediaSource.dropbox) && + process.response != null) { + return media.mergeDropboxMedia(process.response!); + } + return media; + }, + medias: state.medias, + ), + ); + } } - } - Future signInWithGoogle() async { - try { - await _authService.signInWithGoogle(); - state = state.copyWith(googleAccount: _authService.googleAccount); - } catch (e) { - state = state.copyWith(error: e); + for (final process in _mediaProcessRepo.downloadQueue) { + if (process.status.isCompleted) { + state = state.copyWith( + medias: mediaMapUpdate( + update: (media) { + if (media.driveMediaRefId != null && + media.driveMediaRefId == process.media_id && + process.provider == MediaProvider.googleDrive && + !media.sources.contains(AppMediaSource.local) && + process.response != null) { + return process.response!.mergeGoogleDriveMedia(media); + } else if (media.dropboxMediaRefId != null && + media.dropboxMediaRefId == process.media_id && + process.provider == MediaProvider.dropbox && + !media.sources.contains(AppMediaSource.local) && + process.response != null) { + return process.response!.mergeDropboxMedia(media); + } + return media; + }, + medias: state.medias, + ), + ); + } } } - Future loadLocalMedia({bool append = false}) async { - if (_isLocalMediaLoading || (_isMaxLocalMediaLoaded && append)) return; - if (_isMaxLocalMediaLoaded && !append) { - _isMaxLocalMediaLoaded = false; - } - _isLocalMediaLoading = true; - try { - state = state.copyWith(loading: state.medias.isEmpty, error: null); - - final loadedLocalMediaCount = state.medias - .valuesWhere((e) => e.sources.contains(AppMediaSource.local)) - .length; - - final localMedia = await _localMediaService.getLocalMedia( - start: append ? loadedLocalMediaCount : 0, - end: append - ? loadedLocalMediaCount + 30 - : loadedLocalMediaCount < 30 - ? 30 - : loadedLocalMediaCount, - ); + // MEDIA OPERATIONS --------------------------------------------------------- - if (localMedia.length < 30) { - _isMaxLocalMediaLoaded = true; + /// Loads medias from local, google drive and dropbox. + /// it append the medias to the existing medias if reload is false. + Future loadMedias({bool reload = false}) async { + if (state.loading || state.cloudLoading) return; + state = state.copyWith(loading: true, cloudLoading: true, error: null); + try { + // Reset all the variables if reload is true + if (reload) { + _localMediaCount = 0; + _localMaxLoaded = false; + _googleDrivePageToken = null; + _googleDriveMaxLoaded = false; + _googleDriveMediasWithLocalRef.clear(); + _dropboxPageToken = null; + _dropboxMaxLoaded = false; + _dropboxMediasWithLocalRef.clear(); } - final mergedMedia = mergeCommonMedia( - localMedias: localMedia, - googleDriveMedias: _uploadedMedia, - ); - List googleDriveMedia = []; - - if (!append) { - googleDriveMedia = state.medias.values - .expand( - (element) => element.where( - (element) => - element.sources.contains(AppMediaSource.googleDrive) && - element.sources.length == 1, - ), - ) - .toList(); + // Request local media permission if not granted + final hasAccess = await _localMediaService.requestPermission(); + state = state.copyWith(hasLocalMediaAccess: hasAccess); + + // Load local media if access is granted and not max loaded + final localMedia = !hasAccess || _localMaxLoaded + ? [] + : await _localMediaService.getLocalMedia( + start: _localMediaCount, + end: _localMediaCount + 30, + ); + + // Update the local media count and max loaded + _localMediaCount += localMedia.length; + if (localMedia.length < 30) { + _localMaxLoaded = true; } + // Update the state with the loaded medias and stop showing loading state = state.copyWith( + loading: false, medias: sortMedias( - medias: append - ? [ - ...state.medias.values.expand((element) => element), - ...mergedMedia, - ] - : [...mergedMedia, ...googleDriveMedia], + medias: reload + ? localMedia + : [ + ...state.medias.values.expand((element) => element.values), + ...localMedia, + ], ), - loading: false, - lastLocalMediaId: mergedMedia.length > 10 - ? mergedMedia.elementAt(mergedMedia.length - 10).id - : state.lastLocalMediaId, + lastLocalMediaId: localMedia.isNotEmpty ? localMedia.last.id : null, ); - if (append) { - _checkAutoBackUp(); + + // Here we store the only cloud based medias. + final List cloudBasedMedias = []; + + // Load medias from google drive and separate the local ref medias and only cloud based medias. + if (!_googleDriveMaxLoaded && state.googleAccount != null) { + _backUpFolderId ??= await _googleDriveService.getBackUpFolderId(); + + 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); + cloudBasedMedias.addAll(gdMediaCollection.onlyCloudBasedMedias); } - } catch (e) { - state = state.copyWith(loading: false, error: e); - } finally { - _isLocalMediaLoading = false; - } - } - Future loadGoogleDriveMedia() async { - if (state.googleAccount == null || _isGoogleDriveLoading) return; - _isGoogleDriveLoading = true; - try { - _backUpFolderId ??= await _googleDriveService.getBackupFolderId(); + // Load medias from dropbox and separate the local ref medias and only cloud based medias. + if (!_dropboxMaxLoaded && state.dropboxAccount != null) { + final res = await _dropboxService.getPaginatedMedias( + folder: ProviderConstants.backupFolderPath, + nextPageToken: _dropboxPageToken, + pageSize: 30, + ); + _dropboxMaxLoaded = res.nextPageToken == null; + _dropboxPageToken = res.nextPageToken; - state = state.copyWith(loading: state.medias.isEmpty, error: null); - final driveMedias = await _googleDriveService.getDriveMedias( - backUpFolderId: _backUpFolderId!, - ); + final dropboxMediaCollection = await splitLocalRefMedias(res.medias); + _dropboxMediasWithLocalRef.addAll( + dropboxMediaCollection.localRefMedias, + ); + cloudBasedMedias.addAll(dropboxMediaCollection.onlyCloudBasedMedias); + } - // Separate media by its local existence - final List googleDriveMedia = []; - final List uploadedMedia = []; - for (var media in driveMedias) { - if (media.path.trim().isNotEmpty && - await _localMediaService.isLocalFileExist( - type: media.type, - id: media.path, - )) { - uploadedMedia.add(media); - } else { - googleDriveMedia.add(media); + // Here we store all successfully merged medias. + final List allMergedMedias = []; + + for (final media + in state.medias.values.expand((element) => element.values)) { + // Refill the google drive local ref medias if it is empty and not max loaded + if (_googleDriveMediasWithLocalRef.isEmpty && + !_googleDriveMaxLoaded && + state.googleAccount != null) { + 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); + cloudBasedMedias.addAll(gdMediaCollection.onlyCloudBasedMedias); + } + + // Refill the dropbox local ref medias if it is empty and not max loaded + if (_dropboxMediasWithLocalRef.isEmpty && + !_dropboxMaxLoaded && + state.dropboxAccount != null) { + final res = await _dropboxService.getPaginatedMedias( + folder: ProviderConstants.backupFolderPath, + nextPageToken: _dropboxPageToken, + pageSize: 30, + ); + _dropboxMaxLoaded = res.nextPageToken == null; + _dropboxPageToken = res.nextPageToken; + + final dropboxMediaCollection = await splitLocalRefMedias(res.medias); + _dropboxMediasWithLocalRef.addAll( + dropboxMediaCollection.localRefMedias, + ); + cloudBasedMedias.addAll(dropboxMediaCollection.onlyCloudBasedMedias); + } + + // Merge the media with google drive or dropbox media if it exists + AppMedia mergedMedia = media; + + for (final gdMedia in _googleDriveMediasWithLocalRef.toList()) { + if (media.id == gdMedia.id) { + mergedMedia = media.mergeGoogleDriveMedia(gdMedia); + _googleDriveMediasWithLocalRef + .removeWhere((e) => e.id == gdMedia.id); + } + } + for (final dropboxMedia in _dropboxMediasWithLocalRef.toList()) { + if (media.id == dropboxMedia.id) { + mergedMedia = media.mergeDropboxMedia(dropboxMedia); + _dropboxMediasWithLocalRef + .removeWhere((e) => e.id == dropboxMedia.id); + } } - } - _uploadedMedia = uploadedMedia; - //override google drive media if exist. + allMergedMedias.add(mergedMedia); + } state = state.copyWith( - medias: sortMedias( - medias: [ - ...mergeCommonMedia( - localMedias: - state.medias.values.expand((element) => element).toList() - ..removeGoogleDriveRefFromMedias(), - googleDriveMedias: uploadedMedia, - ), - ...googleDriveMedia, - ], - ), loading: false, + medias: sortMedias(medias: [...allMergedMedias, ...cloudBasedMedias]), + cloudLoading: false, + ); + } catch (e, s) { + state = state.copyWith(error: e, loading: false, cloudLoading: false); + _logger.e( + "HomeViewStateNotifier: unable to load medias", + error: e, + stackTrace: s, ); - } catch (e) { - state = state.copyWith(loading: false, error: e); - } finally { - _isGoogleDriveLoading = false; } } + Future<({List onlyCloudBasedMedias, List localRefMedias})> + splitLocalRefMedias(List medias) async { + final list = await Future.wait( + [for (final media in medias) _findMediaIsExistOrNot(media)], + ); + + final Map mediaExistence = Map.fromEntries(list); + + return ( + onlyCloudBasedMedias: + medias.where((e) => !(mediaExistence[e.id] ?? false)).toList(), + localRefMedias: + medias.where((e) => mediaExistence[e.id] ?? false).toList(), + ); + } + + Future> _findMediaIsExistOrNot(AppMedia media) async { + return MapEntry(media.id, await media.assetEntity.exists); + } + void toggleMediaSelection(AppMedia media) { - final selectedMedias = state.selectedMedias.toList(); - selectedMedias.addOrRemove(element: media); + final selectedMedias = Map.from(state.selectedMedias); + if (selectedMedias.containsKey(media.id)) { + selectedMedias.remove(media.id); + } else { + selectedMedias[media.id] = media; + } state = state.copyWith(selectedMedias: selectedMedias); } - Future deleteMediasFromLocal() async { + Future uploadToGoogleDrive() async { + if (state.googleAccount == null) return; + final selectedMedias = state.selectedMedias.entries + .where( + (element) => element.value.sources.contains(AppMediaSource.local), + ) + .map((e) => e.value) + .toList(); + + state = state.copyWith( + selectedMedias: {}, + actionError: null, + ); + _mediaProcessRepo.uploadMedia( + medias: selectedMedias, + provider: MediaProvider.googleDrive, + folderId: _backUpFolderId!, + ); + } + + Future uploadToDropbox() async { + if (state.dropboxAccount == null) return; + final selectedMedias = state.selectedMedias.entries + .where( + (element) => element.value.sources.contains(AppMediaSource.local), + ) + .map((e) => e.value) + .toList(); + + state = state.copyWith( + selectedMedias: {}, + actionError: null, + ); + _mediaProcessRepo.uploadMedia( + medias: selectedMedias, + provider: MediaProvider.dropbox, + folderId: ProviderConstants.backupFolderPath, + ); + } + + Future downloadFromGoogleDrive() async { + if (state.googleAccount == null) return; + final selectedMedias = state.selectedMedias.entries + .where( + (element) => element.value.isGoogleDriveStored, + ) + .map((e) => e.value) + .toList(); + + state = state.copyWith(selectedMedias: {}, actionError: null); + + _mediaProcessRepo.downloadMedia( + folderId: _backUpFolderId!, + medias: selectedMedias, + provider: MediaProvider.googleDrive, + ); + } + + Future downloadFromDropbox() async { + if (state.dropboxAccount == null) return; + final selectedMedias = state.selectedMedias.entries + .where( + (element) => element.value.isDropboxStored, + ) + .map((e) => e.value) + .toList(); + + state = state.copyWith(selectedMedias: {}, actionError: null); + + _mediaProcessRepo.downloadMedia( + folderId: ProviderConstants.backupFolderPath, + medias: selectedMedias, + provider: MediaProvider.dropbox, + ); + } + + Future deleteLocalMedias() async { try { - final ids = state.selectedMedias - .where((element) => element.sources.contains(AppMediaSource.local)) - .map((e) => e.id) + final ids = state.selectedMedias.entries + .where( + (element) => element.value.sources.contains(AppMediaSource.local), + ) + .map((e) => e.key) .toList(); - _uploadedMedia.removeWhere((element) => ids.contains(element.id)); + state = state.copyWith(selectedMedias: {}, actionError: null); await _localMediaService.deleteMedias(ids); + state = state.copyWith( - selectedMedias: [], - medias: removeLocalRefFromMediaMap( + medias: mediaMapUpdate( + update: (media) { + if (ids.contains(media.id) && media.isCommonStored) { + return media.removeLocalRef(); + } else if (ids.contains(media.id) && media.isLocalStored) { + return null; + } + return media; + }, medias: state.medias, - removeFromIds: ids, ), ); - } catch (e) { - state = state.copyWith(error: e); + } catch (e, s) { + state = state.copyWith(actionError: e); + _logger.e( + "HomeViewStateNotifier: unable to delete local medias", + error: e, + stackTrace: s, + ); } } - Future deleteMediasFromGoogleDrive() async { + Future deleteGoogleDriveMedias() async { + if (state.googleAccount == null) return; try { - final medias = state.selectedMedias.where( - (element) => - element.sources.contains(AppMediaSource.googleDrive) && - element.driveMediaRefId != null, - ); + final ids = state.selectedMedias.entries + .where( + (element) => + element.value.sources.contains(AppMediaSource.googleDrive) && + element.value.driveMediaRefId != null, + ) + .map((e) => e.value.driveMediaRefId!) + .toList(); + + state = state.copyWith(selectedMedias: {}, actionError: null); - _googleDriveProcessRepo.deleteMediasFromGoogleDrive( - medias: medias.toList(), + await Future.wait( + ids.map((id) => _googleDriveService.deleteMedia(id: id)), ); - state = state.copyWith(selectedMedias: []); - } catch (e) { - state = state.copyWith(error: e); - } - } - Future downloadMediaFromGoogleDrive() async { - try { - final medias = - state.selectedMedias.where((element) => element.isGoogleDriveStored); - _googleDriveProcessRepo.downloadMediasFromGoogleDrive( - medias: medias.toList(), + state = state.copyWith( + medias: mediaMapUpdate( + update: (media) { + if (media.driveMediaRefId != null && + ids.contains(media.driveMediaRefId) && + media.isCommonStored) { + return media.removeGoogleDriveRef(); + } else if (media.driveMediaRefId != null && + ids.contains(media.driveMediaRefId) && + media.isGoogleDriveStored) { + return null; + } + return media; + }, + medias: state.medias, + ), + ); + } catch (e, s) { + state = state.copyWith(actionError: e); + _logger.e( + "HomeViewStateNotifier: unable to delete google drive medias", + error: e, + stackTrace: s, ); - state = state.copyWith(selectedMedias: []); - } catch (e) { - state = state.copyWith(error: e); } } - Future backUpMediaOnGoogleDrive() async { + Future deleteDropboxMedias() async { + if (state.dropboxAccount == null) return; try { - if (!_authService.signedInWithGoogle) { - await _authService.signInWithGoogle(); - await loadGoogleDriveMedia(); - } - final List medias = state.selectedMedias + final ids = state.selectedMedias.entries .where( - (element) => !element.sources.contains(AppMediaSource.googleDrive), + (element) => + element.value.sources.contains(AppMediaSource.dropbox) && + element.value.dropboxMediaRefId != null, ) + .map((e) => e.value.dropboxMediaRefId!) .toList(); - _googleDriveProcessRepo.uploadMedia(medias); - state = state.copyWith(selectedMedias: []); - } catch (error) { - state = state.copyWith(error: error); + state = state.copyWith(selectedMedias: {}, actionError: null); + + await Future.wait(ids.map((id) => _dropboxService.deleteMedia(id: id))); + + state = state.copyWith( + medias: mediaMapUpdate( + update: (media) { + if (media.dropboxMediaRefId != null && + ids.contains(media.dropboxMediaRefId) && + media.isCommonStored) { + return media.removeLocalRef(); + } else if (media.dropboxMediaRefId != null && + ids.contains(media.dropboxMediaRefId) && + media.isDropboxStored) { + return null; + } + return media; + }, + medias: state.medias, + ), + ); + } catch (e, s) { + state = state.copyWith(actionError: e); + _logger.e( + "HomeViewStateNotifier: unable to delete dropbox medias", + error: e, + stackTrace: s, + ); } } @override Future dispose() async { await _googleAccountSubscription?.cancel(); - _googleDriveProcessRepo.removeListener(_listenGoogleDriveProcess); + _mediaProcessRepo.removeListener(_mediaProcessObserve); super.dispose(); } } @@ -372,13 +609,16 @@ class HomeViewStateNotifier extends StateNotifier class HomeViewState with _$HomeViewState { const factory HomeViewState({ Object? error, + Object? actionError, @Default(false) bool hasLocalMediaAccess, @Default(false) bool loading, + @Default(false) bool cloudLoading, GoogleSignInAccount? googleAccount, - @Default(false) bool showTransfer, + DropboxAccount? dropboxAccount, + @Default({}) Map> medias, + @Default({}) Map selectedMedias, + @Default({}) Map uploadMediaProcesses, + @Default({}) Map downloadMediaProcesses, String? lastLocalMediaId, - @Default({}) Map> medias, - @Default([]) List selectedMedias, - @Default([]) List mediaProcesses, }) = _HomeViewState; } diff --git a/app/lib/ui/flow/home/home_screen_view_model.freezed.dart b/app/lib/ui/flow/home/home_screen_view_model.freezed.dart index 599593f..d0c08b6 100644 --- a/app/lib/ui/flow/home/home_screen_view_model.freezed.dart +++ b/app/lib/ui/flow/home/home_screen_view_model.freezed.dart @@ -17,15 +17,21 @@ final _privateConstructorUsedError = UnsupportedError( /// @nodoc mixin _$HomeViewState { Object? get error => throw _privateConstructorUsedError; + Object? get actionError => throw _privateConstructorUsedError; bool get hasLocalMediaAccess => throw _privateConstructorUsedError; bool get loading => throw _privateConstructorUsedError; + bool get cloudLoading => throw _privateConstructorUsedError; GoogleSignInAccount? get googleAccount => throw _privateConstructorUsedError; - bool get showTransfer => throw _privateConstructorUsedError; - String? get lastLocalMediaId => throw _privateConstructorUsedError; - Map> get medias => + DropboxAccount? get dropboxAccount => throw _privateConstructorUsedError; + Map> get medias => + throw _privateConstructorUsedError; + Map get selectedMedias => + throw _privateConstructorUsedError; + Map get uploadMediaProcesses => + throw _privateConstructorUsedError; + Map get downloadMediaProcesses => throw _privateConstructorUsedError; - List get selectedMedias => throw _privateConstructorUsedError; - List get mediaProcesses => throw _privateConstructorUsedError; + String? get lastLocalMediaId => throw _privateConstructorUsedError; /// Create a copy of HomeViewState /// with the given fields replaced by the non-null parameter values. @@ -42,14 +48,19 @@ abstract class $HomeViewStateCopyWith<$Res> { @useResult $Res call( {Object? error, + Object? actionError, bool hasLocalMediaAccess, bool loading, + bool cloudLoading, GoogleSignInAccount? googleAccount, - bool showTransfer, - String? lastLocalMediaId, - Map> medias, - List selectedMedias, - List mediaProcesses}); + DropboxAccount? dropboxAccount, + Map> medias, + Map selectedMedias, + Map uploadMediaProcesses, + Map downloadMediaProcesses, + String? lastLocalMediaId}); + + $DropboxAccountCopyWith<$Res>? get dropboxAccount; } /// @nodoc @@ -68,17 +79,21 @@ class _$HomeViewStateCopyWithImpl<$Res, $Val extends HomeViewState> @override $Res call({ Object? error = freezed, + Object? actionError = freezed, Object? hasLocalMediaAccess = null, Object? loading = null, + Object? cloudLoading = null, Object? googleAccount = freezed, - Object? showTransfer = null, - Object? lastLocalMediaId = freezed, + Object? dropboxAccount = freezed, Object? medias = null, Object? selectedMedias = null, - Object? mediaProcesses = null, + Object? uploadMediaProcesses = null, + Object? downloadMediaProcesses = null, + Object? lastLocalMediaId = freezed, }) { return _then(_value.copyWith( error: freezed == error ? _value.error : error, + actionError: freezed == actionError ? _value.actionError : actionError, hasLocalMediaAccess: null == hasLocalMediaAccess ? _value.hasLocalMediaAccess : hasLocalMediaAccess // ignore: cast_nullable_to_non_nullable @@ -87,32 +102,54 @@ class _$HomeViewStateCopyWithImpl<$Res, $Val extends HomeViewState> ? _value.loading : loading // ignore: cast_nullable_to_non_nullable as bool, + cloudLoading: null == cloudLoading + ? _value.cloudLoading + : cloudLoading // ignore: cast_nullable_to_non_nullable + as bool, googleAccount: freezed == googleAccount ? _value.googleAccount : googleAccount // ignore: cast_nullable_to_non_nullable as GoogleSignInAccount?, - showTransfer: null == showTransfer - ? _value.showTransfer - : showTransfer // ignore: cast_nullable_to_non_nullable - as bool, - lastLocalMediaId: freezed == lastLocalMediaId - ? _value.lastLocalMediaId - : lastLocalMediaId // ignore: cast_nullable_to_non_nullable - as String?, + dropboxAccount: freezed == dropboxAccount + ? _value.dropboxAccount + : dropboxAccount // ignore: cast_nullable_to_non_nullable + as DropboxAccount?, medias: null == medias ? _value.medias : medias // ignore: cast_nullable_to_non_nullable - as Map>, + as Map>, selectedMedias: null == selectedMedias ? _value.selectedMedias : selectedMedias // ignore: cast_nullable_to_non_nullable - as List, - mediaProcesses: null == mediaProcesses - ? _value.mediaProcesses - : mediaProcesses // ignore: cast_nullable_to_non_nullable - as List, + as Map, + uploadMediaProcesses: null == uploadMediaProcesses + ? _value.uploadMediaProcesses + : uploadMediaProcesses // ignore: cast_nullable_to_non_nullable + as Map, + downloadMediaProcesses: null == downloadMediaProcesses + ? _value.downloadMediaProcesses + : downloadMediaProcesses // ignore: cast_nullable_to_non_nullable + as Map, + lastLocalMediaId: freezed == lastLocalMediaId + ? _value.lastLocalMediaId + : lastLocalMediaId // ignore: cast_nullable_to_non_nullable + as String?, ) as $Val); } + + /// Create a copy of HomeViewState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $DropboxAccountCopyWith<$Res>? get dropboxAccount { + if (_value.dropboxAccount == null) { + return null; + } + + return $DropboxAccountCopyWith<$Res>(_value.dropboxAccount!, (value) { + return _then(_value.copyWith(dropboxAccount: value) as $Val); + }); + } } /// @nodoc @@ -125,14 +162,20 @@ abstract class _$$HomeViewStateImplCopyWith<$Res> @useResult $Res call( {Object? error, + Object? actionError, bool hasLocalMediaAccess, bool loading, + bool cloudLoading, GoogleSignInAccount? googleAccount, - bool showTransfer, - String? lastLocalMediaId, - Map> medias, - List selectedMedias, - List mediaProcesses}); + DropboxAccount? dropboxAccount, + Map> medias, + Map selectedMedias, + Map uploadMediaProcesses, + Map downloadMediaProcesses, + String? lastLocalMediaId}); + + @override + $DropboxAccountCopyWith<$Res>? get dropboxAccount; } /// @nodoc @@ -149,17 +192,21 @@ class __$$HomeViewStateImplCopyWithImpl<$Res> @override $Res call({ Object? error = freezed, + Object? actionError = freezed, Object? hasLocalMediaAccess = null, Object? loading = null, + Object? cloudLoading = null, Object? googleAccount = freezed, - Object? showTransfer = null, - Object? lastLocalMediaId = freezed, + Object? dropboxAccount = freezed, Object? medias = null, Object? selectedMedias = null, - Object? mediaProcesses = null, + Object? uploadMediaProcesses = null, + Object? downloadMediaProcesses = null, + Object? lastLocalMediaId = freezed, }) { return _then(_$HomeViewStateImpl( error: freezed == error ? _value.error : error, + actionError: freezed == actionError ? _value.actionError : actionError, hasLocalMediaAccess: null == hasLocalMediaAccess ? _value.hasLocalMediaAccess : hasLocalMediaAccess // ignore: cast_nullable_to_non_nullable @@ -168,30 +215,38 @@ class __$$HomeViewStateImplCopyWithImpl<$Res> ? _value.loading : loading // ignore: cast_nullable_to_non_nullable as bool, + cloudLoading: null == cloudLoading + ? _value.cloudLoading + : cloudLoading // ignore: cast_nullable_to_non_nullable + as bool, googleAccount: freezed == googleAccount ? _value.googleAccount : googleAccount // ignore: cast_nullable_to_non_nullable as GoogleSignInAccount?, - showTransfer: null == showTransfer - ? _value.showTransfer - : showTransfer // ignore: cast_nullable_to_non_nullable - as bool, - lastLocalMediaId: freezed == lastLocalMediaId - ? _value.lastLocalMediaId - : lastLocalMediaId // ignore: cast_nullable_to_non_nullable - as String?, + dropboxAccount: freezed == dropboxAccount + ? _value.dropboxAccount + : dropboxAccount // ignore: cast_nullable_to_non_nullable + as DropboxAccount?, medias: null == medias ? _value._medias : medias // ignore: cast_nullable_to_non_nullable - as Map>, + as Map>, selectedMedias: null == selectedMedias ? _value._selectedMedias : selectedMedias // ignore: cast_nullable_to_non_nullable - as List, - mediaProcesses: null == mediaProcesses - ? _value._mediaProcesses - : mediaProcesses // ignore: cast_nullable_to_non_nullable - as List, + as Map, + uploadMediaProcesses: null == uploadMediaProcesses + ? _value._uploadMediaProcesses + : uploadMediaProcesses // ignore: cast_nullable_to_non_nullable + as Map, + downloadMediaProcesses: null == downloadMediaProcesses + ? _value._downloadMediaProcesses + : downloadMediaProcesses // ignore: cast_nullable_to_non_nullable + as Map, + lastLocalMediaId: freezed == lastLocalMediaId + ? _value.lastLocalMediaId + : lastLocalMediaId // ignore: cast_nullable_to_non_nullable + as String?, )); } } @@ -201,63 +256,83 @@ class __$$HomeViewStateImplCopyWithImpl<$Res> class _$HomeViewStateImpl implements _HomeViewState { const _$HomeViewStateImpl( {this.error, + this.actionError, this.hasLocalMediaAccess = false, this.loading = false, + this.cloudLoading = false, this.googleAccount, - this.showTransfer = false, - this.lastLocalMediaId, - final Map> medias = const {}, - final List selectedMedias = const [], - final List mediaProcesses = const []}) + this.dropboxAccount, + final Map> medias = const {}, + final Map selectedMedias = const {}, + final Map uploadMediaProcesses = const {}, + final Map downloadMediaProcesses = const {}, + this.lastLocalMediaId}) : _medias = medias, _selectedMedias = selectedMedias, - _mediaProcesses = mediaProcesses; + _uploadMediaProcesses = uploadMediaProcesses, + _downloadMediaProcesses = downloadMediaProcesses; @override final Object? error; @override + final Object? actionError; + @override @JsonKey() final bool hasLocalMediaAccess; @override @JsonKey() final bool loading; @override - final GoogleSignInAccount? googleAccount; - @override @JsonKey() - final bool showTransfer; + final bool cloudLoading; @override - final String? lastLocalMediaId; - final Map> _medias; + final GoogleSignInAccount? googleAccount; + @override + final DropboxAccount? dropboxAccount; + final Map> _medias; @override @JsonKey() - Map> get medias { + Map> get medias { if (_medias is EqualUnmodifiableMapView) return _medias; // ignore: implicit_dynamic_type return EqualUnmodifiableMapView(_medias); } - final List _selectedMedias; + final Map _selectedMedias; + @override + @JsonKey() + Map get selectedMedias { + if (_selectedMedias is EqualUnmodifiableMapView) return _selectedMedias; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_selectedMedias); + } + + final Map _uploadMediaProcesses; @override @JsonKey() - List get selectedMedias { - if (_selectedMedias is EqualUnmodifiableListView) return _selectedMedias; + Map get uploadMediaProcesses { + if (_uploadMediaProcesses is EqualUnmodifiableMapView) + return _uploadMediaProcesses; // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_selectedMedias); + return EqualUnmodifiableMapView(_uploadMediaProcesses); } - final List _mediaProcesses; + final Map _downloadMediaProcesses; @override @JsonKey() - List get mediaProcesses { - if (_mediaProcesses is EqualUnmodifiableListView) return _mediaProcesses; + Map get downloadMediaProcesses { + if (_downloadMediaProcesses is EqualUnmodifiableMapView) + return _downloadMediaProcesses; // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_mediaProcesses); + return EqualUnmodifiableMapView(_downloadMediaProcesses); } + @override + final String? lastLocalMediaId; + @override String toString() { - return 'HomeViewState(error: $error, hasLocalMediaAccess: $hasLocalMediaAccess, loading: $loading, googleAccount: $googleAccount, showTransfer: $showTransfer, lastLocalMediaId: $lastLocalMediaId, medias: $medias, selectedMedias: $selectedMedias, mediaProcesses: $mediaProcesses)'; + return 'HomeViewState(error: $error, actionError: $actionError, hasLocalMediaAccess: $hasLocalMediaAccess, loading: $loading, cloudLoading: $cloudLoading, googleAccount: $googleAccount, dropboxAccount: $dropboxAccount, medias: $medias, selectedMedias: $selectedMedias, uploadMediaProcesses: $uploadMediaProcesses, downloadMediaProcesses: $downloadMediaProcesses, lastLocalMediaId: $lastLocalMediaId)'; } @override @@ -266,34 +341,43 @@ class _$HomeViewStateImpl implements _HomeViewState { (other.runtimeType == runtimeType && other is _$HomeViewStateImpl && const DeepCollectionEquality().equals(other.error, error) && + const DeepCollectionEquality() + .equals(other.actionError, actionError) && (identical(other.hasLocalMediaAccess, hasLocalMediaAccess) || other.hasLocalMediaAccess == hasLocalMediaAccess) && (identical(other.loading, loading) || other.loading == loading) && + (identical(other.cloudLoading, cloudLoading) || + other.cloudLoading == cloudLoading) && (identical(other.googleAccount, googleAccount) || other.googleAccount == googleAccount) && - (identical(other.showTransfer, showTransfer) || - other.showTransfer == showTransfer) && - (identical(other.lastLocalMediaId, lastLocalMediaId) || - other.lastLocalMediaId == lastLocalMediaId) && + (identical(other.dropboxAccount, dropboxAccount) || + other.dropboxAccount == dropboxAccount) && const DeepCollectionEquality().equals(other._medias, _medias) && const DeepCollectionEquality() .equals(other._selectedMedias, _selectedMedias) && const DeepCollectionEquality() - .equals(other._mediaProcesses, _mediaProcesses)); + .equals(other._uploadMediaProcesses, _uploadMediaProcesses) && + const DeepCollectionEquality().equals( + other._downloadMediaProcesses, _downloadMediaProcesses) && + (identical(other.lastLocalMediaId, lastLocalMediaId) || + other.lastLocalMediaId == lastLocalMediaId)); } @override int get hashCode => Object.hash( runtimeType, const DeepCollectionEquality().hash(error), + const DeepCollectionEquality().hash(actionError), hasLocalMediaAccess, loading, + cloudLoading, googleAccount, - showTransfer, - lastLocalMediaId, + dropboxAccount, const DeepCollectionEquality().hash(_medias), const DeepCollectionEquality().hash(_selectedMedias), - const DeepCollectionEquality().hash(_mediaProcesses)); + const DeepCollectionEquality().hash(_uploadMediaProcesses), + const DeepCollectionEquality().hash(_downloadMediaProcesses), + lastLocalMediaId); /// Create a copy of HomeViewState /// with the given fields replaced by the non-null parameter values. @@ -307,33 +391,42 @@ class _$HomeViewStateImpl implements _HomeViewState { abstract class _HomeViewState implements HomeViewState { const factory _HomeViewState( {final Object? error, + final Object? actionError, final bool hasLocalMediaAccess, final bool loading, + final bool cloudLoading, final GoogleSignInAccount? googleAccount, - final bool showTransfer, - final String? lastLocalMediaId, - final Map> medias, - final List selectedMedias, - final List mediaProcesses}) = _$HomeViewStateImpl; + final DropboxAccount? dropboxAccount, + final Map> medias, + final Map selectedMedias, + final Map uploadMediaProcesses, + final Map downloadMediaProcesses, + final String? lastLocalMediaId}) = _$HomeViewStateImpl; @override Object? get error; @override + Object? get actionError; + @override bool get hasLocalMediaAccess; @override bool get loading; @override + bool get cloudLoading; + @override GoogleSignInAccount? get googleAccount; @override - bool get showTransfer; + DropboxAccount? get dropboxAccount; @override - String? get lastLocalMediaId; + Map> get medias; + @override + Map get selectedMedias; @override - Map> get medias; + Map get uploadMediaProcesses; @override - List get selectedMedias; + Map get downloadMediaProcesses; @override - List get mediaProcesses; + String? get lastLocalMediaId; /// Create a copy of HomeViewState /// with the given fields replaced by the non-null parameter values. diff --git a/app/lib/ui/flow/home/home_view_model_helper_mixin.dart b/app/lib/ui/flow/home/home_view_model_helper_mixin.dart index 6631963..0eb4f2e 100644 --- a/app/lib/ui/flow/home/home_view_model_helper_mixin.dart +++ b/app/lib/ui/flow/home/home_view_model_helper_mixin.dart @@ -1,102 +1,44 @@ -import '../../../domain/extensions/media_list_extension.dart'; import '../../../domain/formatter/date_formatter.dart'; import 'package:collection/collection.dart'; -import 'package:data/models/app_process/app_process.dart'; import 'package:data/models/media/media.dart'; -import 'package:data/models/media/media_extension.dart'; mixin HomeViewModelHelperMixin { - List mergeCommonMedia({ - required List localMedias, - required List googleDriveMedias, + Map> sortMedias({ + required List medias, }) { - // If one of the lists is empty, return the other list. - if (googleDriveMedias.isEmpty) return localMedias; - if (localMedias.isEmpty) return []; - - // Convert the lists to mutable lists. - localMedias = localMedias.toList(); - googleDriveMedias = googleDriveMedias.toList(); - - final mergedMedias = []; - - // Add common media to mergedMedias and remove them from the lists. - for (AppMedia localMedia in localMedias.toList()) { - googleDriveMedias - .where((googleDriveMedia) => googleDriveMedia.path == localMedia.id) - .forEach((googleDriveMedia) { - localMedias.removeWhere((media) => media.id == localMedia.id); - - mergedMedias.add(localMedia.mergeGoogleDriveMedia(googleDriveMedia)); - }); - } - - return [...mergedMedias, ...localMedias]; - } - - Map> sortMedias({required List medias}) { medias.sort( (a, b) => (b.createdTime ?? DateTime.now()) .compareTo(a.createdTime ?? DateTime.now()), ); - return groupBy( - medias, - (AppMedia media) => - media.createdTime?.dateOnly ?? DateTime.now().dateOnly, - ); - } - Map> removeGoogleDriveRefFromMediaMap({ - required Map> medias, - List? removeFromIds, - }) { - return sortMedias( - medias: medias.values.expand((element) => element).toList() - ..removeGoogleDriveRefFromMedias(removeFromIds: removeFromIds), + final sortedGroupedMap = groupBy, DateTime>( + medias.map((media) => MapEntry(media.id, media)), + (e) => e.value.createdTime?.dateOnly ?? DateTime.now().dateOnly, ); - } - Map> removeLocalRefFromMediaMap({ - required Map> medias, - List? removeFromIds, - }) { - return sortMedias( - medias: medias.values.expand((element) => element).toList() - ..removeLocalRefFromMedias(removeFromIds: removeFromIds), - ); - } - - Map> addGoogleDriveRefInMediaMap({ - required Map> medias, - required List process, - }) { - final processIds = process.map((e) => e.id).toList(); - return medias.map((key, value) { + return sortedGroupedMap.map((key, value) { return MapEntry( key, - value - ..addGoogleDriveRefInMedias( - process: process, - processIds: processIds, - ), + Map.fromEntries(value.map((e) => MapEntry(e.key, e.value))), ); }); } - Map> replaceMediaRefInMediaMap({ - required Map> medias, - required List process, + Map> mediaMapUpdate({ + required Map> medias, + required AppMedia? Function(AppMedia media) update, }) { - final processIds = process.map((e) => e.id).toList(); - return medias.map((key, value) { - return MapEntry( - key, - value - ..replaceMediaRefInMedias( - process: process, - processIds: processIds, - ), - ); - }); + final List updatedMedias = []; + + for (final map in medias.values) { + for (final media in map.values) { + final updatedMedia = update(media); + if (updatedMedia != null) { + updatedMedias.add(updatedMedia); + } + } + } + + return sortMedias(medias: updatedMedias); } } diff --git a/app/lib/ui/flow/media_metadata_details/media_metadata_details.dart b/app/lib/ui/flow/media_metadata_details/media_metadata_details.dart index d48c0a2..1266753 100644 --- a/app/lib/ui/flow/media_metadata_details/media_metadata_details.dart +++ b/app/lib/ui/flow/media_metadata_details/media_metadata_details.dart @@ -1,15 +1,15 @@ import '../../../components/app_page.dart'; import '../../../components/thumbnail_builder.dart'; -import '../../../domain/assets/assets_paths.dart'; import '../../../domain/extensions/context_extensions.dart'; -import '../../../domain/formatter/byte_formatter.dart'; import '../../../domain/formatter/date_formatter.dart'; import '../../../domain/formatter/duration_formatter.dart'; +import 'package:data/domain/formatters/byte_formatter.dart'; import 'package:data/models/media/media.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:style/extensions/context_extensions.dart'; import 'package:style/text/app_text_style.dart'; +import '../../../gen/assets.gen.dart'; class MediaMetadataDetailsScreen extends StatelessWidget { final AppMedia media; @@ -117,7 +117,12 @@ class MediaMetadataDetailsScreen extends StatelessWidget { ), if (media.sources.contains(AppMediaSource.googleDrive)) SvgPicture.asset( - Assets.images.icons.googleDrive, + Assets.images.icons.icGoogleDrive, + width: 20, + ), + if (media.sources.contains(AppMediaSource.dropbox)) + SvgPicture.asset( + Assets.images.icons.icDropbox, width: 20, ), ], diff --git a/app/lib/ui/flow/media_preview/components/download_require_view.dart b/app/lib/ui/flow/media_preview/components/download_require_view.dart index aef7503..e8c4534 100644 --- a/app/lib/ui/flow/media_preview/components/download_require_view.dart +++ b/app/lib/ui/flow/media_preview/components/download_require_view.dart @@ -1,16 +1,18 @@ +import 'package:data/domain/formatters/byte_formatter.dart'; +import 'package:data/models/media_process/media_process.dart'; import '../../../../domain/extensions/context_extensions.dart'; -import '../../../../domain/formatter/byte_formatter.dart'; -import 'package:data/models/app_process/app_process.dart'; import 'package:data/models/media/media.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:style/extensions/context_extensions.dart'; import 'package:style/indicators/circular_progress_indicator.dart'; import '../../../../components/error_view.dart'; +import '../../../../domain/image_providers/app_media_image_provider.dart'; class DownloadRequireView extends StatelessWidget { final AppMedia media; - final AppProcess? downloadProcess; + final String? dropboxAccessToken; + final DownloadMediaProcess? downloadProcess; final void Function() onDownload; const DownloadRequireView({ @@ -18,6 +20,7 @@ class DownloadRequireView extends StatelessWidget { required this.media, this.downloadProcess, required this.onDownload, + this.dropboxAccessToken, }); @override @@ -28,10 +31,14 @@ class DownloadRequireView extends StatelessWidget { children: [ Hero( tag: media, - child: Image.network( + child: Image( + image: AppMediaImageProvider( + media: media, + dropboxAccessToken: dropboxAccessToken, + thumbnailSize: Size(2000, 1500), + ), height: double.infinity, width: double.infinity, - media.thumbnailLink ?? '', fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return const SizedBox(); @@ -44,7 +51,7 @@ class DownloadRequireView extends StatelessWidget { color: Colors.black38, ), if (downloadProcess?.progress != null && - downloadProcess!.status.isProcessing) ...[ + downloadProcess!.status.isRunning) ...[ ErrorView( foregroundColor: context.colorScheme.onPrimary, icon: Stack( @@ -55,7 +62,7 @@ class DownloadRequireView extends StatelessWidget { color: context.colorScheme.onPrimary, strokeWidth: 6, size: context.mediaQuerySize.width * 0.15, - value: downloadProcess?.progress?.percentageInPoint, + value: downloadProcess?.progress, ), Icon( CupertinoIcons.cloud_download, @@ -65,22 +72,10 @@ class DownloadRequireView extends StatelessWidget { ], ), title: - "${downloadProcess?.progress?.chunk.formatBytes ?? "0.0 B"} - ${downloadProcess?.progress?.total.formatBytes ?? "0.0 B"} ${downloadProcess?.progress?.percentage.toStringAsFixed(0) ?? "0.0"}%", + "${downloadProcess?.chunk.formatBytes ?? "0.0 B"} - ${downloadProcess?.total.formatBytes ?? "0.0 B"} ${downloadProcess?.progressPercentage.toStringAsFixed(0) ?? "0.0"}%", message: context.l10n.download_in_progress_text, ), ], - if (downloadProcess?.status.isWaiting ?? false) ...[ - ErrorView( - foregroundColor: context.colorScheme.onPrimary, - icon: Icon( - CupertinoIcons.time, - size: context.mediaQuerySize.width * 0.15, - color: context.colorScheme.onPrimary, - ), - title: context.l10n.waiting_in_queue_text, - message: context.l10n.waiting_in_download_queue_message, - ), - ], if (downloadProcess?.progress == null) ErrorView( foregroundColor: context.colorScheme.onPrimary, diff --git a/app/lib/ui/flow/media_preview/components/image_preview_screen.dart b/app/lib/ui/flow/media_preview/components/image_preview_screen.dart index ec9413f..4397abc 100644 --- a/app/lib/ui/flow/media_preview/components/image_preview_screen.dart +++ b/app/lib/ui/flow/media_preview/components/image_preview_screen.dart @@ -30,10 +30,17 @@ class _ImagePreviewScreenState extends ConsumerState { if (!widget.media.sources.contains(AppMediaSource.local)) { notifier = ref.read(networkImagePreviewStateNotifierProvider.notifier); runPostFrame(() async { - await notifier.loadImageFromGoogleDrive( - id: widget.media.id, - extension: widget.media.extension, - ); + if (widget.media.driveMediaRefId != null) { + await notifier.loadImageFromGoogleDrive( + id: widget.media.driveMediaRefId!, + extension: widget.media.extension, + ); + } else if (widget.media.dropboxMediaRefId != null) { + await notifier.loadImageFromDropbox( + id: widget.media.dropboxMediaRefId!, + extension: widget.media.extension, + ); + } }); } super.initState(); @@ -48,9 +55,7 @@ class _ImagePreviewScreenState extends ConsumerState { width: double.infinity, child: widget.media.sources.contains(AppMediaSource.local) ? _displayLocalImage(context: context) - : NetworkImagePreview( - media: widget.media, - ), + : NetworkImagePreview(media: widget.media), ), ), ); diff --git a/app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview.dart b/app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview.dart index 9951f66..870310a 100644 --- a/app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview.dart +++ b/app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview.dart @@ -35,12 +35,10 @@ class NetworkImagePreview extends ConsumerWidget { }, ), ); - } else if (state.error != null) { - return ErrorView( - title: context.l10n.unable_to_load_media_error, - message: context.l10n.unable_to_load_media_message, - ); } - return const SizedBox(); + return ErrorView( + title: context.l10n.unable_to_load_media_error, + message: context.l10n.unable_to_load_media_message, + ); } } diff --git a/app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview_view_model.dart b/app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview_view_model.dart index 500d33f..44e5a11 100644 --- a/app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview_view_model.dart +++ b/app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview_view_model.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'package:data/services/dropbox_services.dart'; import 'package:data/services/google_drive_service.dart'; import 'package:dio/dio.dart' show CancelToken; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -11,15 +12,21 @@ part 'network_image_preview_view_model.freezed.dart'; final networkImagePreviewStateNotifierProvider = StateNotifierProvider.autoDispose((ref) { - return NetworkImagePreviewStateNotifier(ref.read(googleDriveServiceProvider)); + return NetworkImagePreviewStateNotifier( + ref.read(googleDriveServiceProvider), + ref.read(dropboxServiceProvider), + ); }); class NetworkImagePreviewStateNotifier extends StateNotifier { final GoogleDriveService _googleDriveServices; + final DropboxService _dropboxService; - NetworkImagePreviewStateNotifier(this._googleDriveServices) - : super(const NetworkImagePreviewState()); + NetworkImagePreviewStateNotifier( + this._googleDriveServices, + this._dropboxService, + ) : super(const NetworkImagePreviewState()); File? tempFile; CancelToken? cancelToken; @@ -33,7 +40,36 @@ class NetworkImagePreviewStateNotifier cancelToken = CancelToken(); final dir = await getTemporaryDirectory(); tempFile = File('${dir.path}/$id.$extension'); - await _googleDriveServices.downloadFromGoogleDrive( + await _googleDriveServices.downloadMedia( + id: id, + saveLocation: tempFile!.path, + cancelToken: cancelToken, + onProgress: (progress, total) { + state = state.copyWith(progress: total <= 0 ? 0 : progress / total); + }, + ); + state = state.copyWith( + loading: false, + filePath: tempFile?.path, + ); + } catch (error) { + state = state.copyWith( + error: error, + loading: false, + ); + } + } + + Future loadImageFromDropbox({ + required String id, + required String extension, + }) async { + try { + state = state.copyWith(loading: true, error: null); + cancelToken = CancelToken(); + final dir = await getTemporaryDirectory(); + tempFile = File('${dir.path}/$id.$extension'); + await _dropboxService.downloadMedia( id: id, saveLocation: tempFile!.path, cancelToken: cancelToken, diff --git a/app/lib/ui/flow/media_preview/components/top_bar.dart b/app/lib/ui/flow/media_preview/components/top_bar.dart index 746b0da..9b238f9 100644 --- a/app/lib/ui/flow/media_preview/components/top_bar.dart +++ b/app/lib/ui/flow/media_preview/components/top_bar.dart @@ -1,12 +1,12 @@ import 'dart:io'; +import 'package:flutter_svg/svg.dart'; import '../../../../domain/extensions/context_extensions.dart'; +import '../../../../gen/assets.gen.dart'; import '../../../navigation/app_route.dart'; import 'package:data/models/media/media.dart'; -import 'package:data/models/media/media_extension.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/svg.dart'; import 'package:go_router/go_router.dart'; import 'package:style/animations/cross_fade_animation.dart'; import 'package:style/buttons/action_button.dart'; @@ -14,12 +14,11 @@ import 'package:style/extensions/context_extensions.dart'; import 'package:style/text/app_text_style.dart'; import '../../../../components/app_dialog.dart'; import '../../../../components/app_page.dart'; -import '../../../../domain/assets/assets_paths.dart'; import '../../../../domain/formatter/date_formatter.dart'; import '../media_preview_view_model.dart'; import 'package:share_plus/share_plus.dart'; -class PreviewTopBar extends StatelessWidget { +class PreviewTopBar extends ConsumerStatefulWidget { final AutoDisposeStateNotifierProvider provider; final void Function() onAction; @@ -30,270 +29,557 @@ class PreviewTopBar extends StatelessWidget { required this.onAction, }); + @override + ConsumerState createState() => _PreviewTopBarState(); +} + +class _PreviewTopBarState extends ConsumerState { + late MediaPreviewStateNotifier _notifier; + + @override + void initState() { + _notifier = ref.read(widget.provider.notifier); + super.initState(); + } + @override Widget build(BuildContext context) { - return Consumer( - builder: (context, ref, child) { - final notifier = ref.read(provider.notifier); - final media = ref.watch( - provider.select((state) => state.medias[state.currentIndex]), - ); - final showManu = - ref.watch(provider.select((state) => state.showActions)); + final state = ref.watch( + widget.provider.select( + (state) => ( + media: state.medias[state.currentIndex], + showAction: state.showActions, + googleAccount: state.googleAccount, + dropboxAccount: state.dropboxAccount, + ), + ), + ); - return CrossFadeAnimation( - showChild: showManu, - child: AdaptiveAppBar( - iosTransitionBetweenRoutes: false, - text: media.createdTime?.format(context, DateFormatType.relative) ?? + return CrossFadeAnimation( + showChild: state.showAction, + child: AdaptiveAppBar( + iosTransitionBetweenRoutes: false, + text: + state.media.createdTime?.format(context, DateFormatType.relative) ?? '', - actions: [ - ActionButton( - onPressed: () { - showMenu( - context: context, - position: RelativeRect.fromSize( - Rect.fromLTRB(context.mediaQuerySize.width, 50, 0, 0), - // Placeholder rect, will be overwritten - context.mediaQuerySize, // Size of the screen - ), - elevation: 1, - surfaceTintColor: context.colorScheme.surface, - color: context.colorScheme.surface, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - items: [ - PopupMenuItem( - onTap: () { - onAction(); - MediaMetadataDetailsRoute($extra: media) - .push(context); - }, - child: Row( - children: [ - Icon( - CupertinoIcons.info, - color: context.colorScheme.textSecondary, - size: 22, - ), - const SizedBox(width: 16), - Text( - context.l10n.common_info, - style: AppTextStyles.body2.copyWith( - color: context.colorScheme.textPrimary, - ), - ), - ], - ), - ), - if (media.isGoogleDriveStored) - PopupMenuItem( - onTap: () { - notifier.downloadMediaFromGoogleDrive(media: media); - }, - child: Row( - children: [ - Icon( - CupertinoIcons.cloud_download, - color: context.colorScheme.textSecondary, - size: 22, - ), - const SizedBox(width: 16), - Text( - context.l10n.common_download, - style: AppTextStyles.body2.copyWith( - color: context.colorScheme.textPrimary, - ), - ), - ], - ), - ), - if (media.isLocalStored) - PopupMenuItem( - onTap: () { - notifier.uploadMediaInGoogleDrive(media: media); - }, - child: Row( - children: [ - Icon( - CupertinoIcons.cloud_upload, - color: context.colorScheme.textSecondary, - size: 22, - ), - const SizedBox(width: 16), - Text( - context.l10n.common_upload, - style: AppTextStyles.body2.copyWith( - color: context.colorScheme.textPrimary, - ), - ), - ], - ), - ), - if (media.sources.contains(AppMediaSource.googleDrive)) - PopupMenuItem( - onTap: () async { - _showDeleteFromDriveDialog( - context: context, - onDelete: () { - notifier.deleteMediaFromGoogleDrive( - media.driveMediaRefId, - ); - context.pop(); - }, - ); - }, - child: Row( - children: [ - Stack( - alignment: Alignment.bottomRight, - children: [ - Padding( - padding: const EdgeInsets.only( - bottom: 2, - right: 2, - ), - child: Icon( - CupertinoIcons.trash, - color: context.colorScheme.textSecondary, - size: 22, - ), - ), - SvgPicture.asset( - Assets.images.icons.googleDrive, - width: 14, - height: 14, - ), - ], - ), - const SizedBox(width: 16), - Text( - context.l10n.common_delete_from_google_drive, - style: AppTextStyles.body2.copyWith( - color: context.colorScheme.textPrimary, - ), - ), - ], - ), - ), - if (media.sources.contains(AppMediaSource.local)) - PopupMenuItem( - onTap: () async { - _showDeleteFromDeviceDialog( - context: context, - onDelete: () { - notifier.deleteMediaFromLocal(media.id); - context.pop(); - }, - ); - }, - child: Row( - children: [ - Icon( - CupertinoIcons.trash, - color: context.colorScheme.textSecondary, - size: 22, - ), - const SizedBox(width: 16), - Text( - (media.isLocalStored) - ? context.l10n.common_delete - : context.l10n.common_delete_from_device, - style: AppTextStyles.body2.copyWith( - color: context.colorScheme.textPrimary, - ), - ), - ], - ), - ), - if (media.isLocalStored) - PopupMenuItem( - onTap: () async { - await Share.shareXFiles([XFile(media.path)]); - }, - child: Row( - children: [ - Icon( - Platform.isIOS - ? CupertinoIcons.share - : Icons.share_rounded, - color: context.colorScheme.textSecondary, - size: 22, - ), - const SizedBox(width: 16), - Text( - context.l10n.common_share, - style: AppTextStyles.body2.copyWith( - color: context.colorScheme.textPrimary, - ), - ), - ], - ), - ), - ], - ); - }, - icon: Icon( - Icons.more_vert_rounded, + actions: [ + ActionButton( + onPressed: () { + showMenu( + context: context, + position: RelativeRect.fromSize( + Rect.fromLTRB(context.mediaQuerySize.width, 50, 0, 0), + context.mediaQuerySize, // Size of the screen + ), + elevation: 1, + surfaceTintColor: context.colorScheme.surface, + color: context.colorScheme.surface, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + items: [ + infoAction(context, state.media), + if (!state.media.sources + .contains(AppMediaSource.googleDrive) && + state.media.sources.contains(AppMediaSource.local) && + state.googleAccount != null) + _uploadToGoogleDriveAction(context, state.media), + if (state.media.sources + .contains(AppMediaSource.googleDrive) && + !state.media.sources.contains(AppMediaSource.local) && + state.googleAccount != null) + _downloadFromGoogleDriveAction(context, state.media), + if (state.media.sources + .contains(AppMediaSource.googleDrive) && + state.googleAccount != null) + _deleteFromGoogleDriveAction(context, state.media), + if (!state.media.sources.contains(AppMediaSource.dropbox) && + state.media.sources.contains(AppMediaSource.local) && + state.dropboxAccount != null) + _uploadToDropboxAction(context, state.media), + if (state.media.sources.contains(AppMediaSource.dropbox) && + !state.media.sources.contains(AppMediaSource.local) && + state.dropboxAccount != null) + _downloadFromDropboxAction(context, state.media), + if (state.media.sources.contains(AppMediaSource.dropbox) && + state.dropboxAccount != null) + _deleteFromDropboxAction(context, state.media), + if (state.media.sources.contains(AppMediaSource.local)) + _deleteFromDeviceAction(context, state.media), + if (state.media.sources.contains(AppMediaSource.local)) + _shareAction(context, state.media), + ], + ); + }, + icon: Icon( + Icons.more_vert_rounded, + color: context.colorScheme.textSecondary, + size: 22, + ), + ), + if (!Platform.isIOS && !Platform.isMacOS) const SizedBox(width: 8), + ], + ), + ); + } + + PopupMenuItem infoAction(BuildContext context, AppMedia media) { + return PopupMenuItem( + onTap: () { + widget.onAction(); + MediaMetadataDetailsRoute($extra: media).push(context); + }, + child: Row( + children: [ + Icon( + CupertinoIcons.info, + color: context.colorScheme.textSecondary, + size: 22, + ), + const SizedBox(width: 16), + Text( + context.l10n.common_info, + style: AppTextStyles.body2.copyWith( + color: context.colorScheme.textPrimary, + ), + ), + ], + ), + ); + } + + PopupMenuItem _uploadToGoogleDriveAction( + BuildContext context, + AppMedia media, + ) { + return PopupMenuItem( + child: Row( + children: [ + Stack( + alignment: Alignment.bottomRight, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 0, right: 8), + child: Icon( + CupertinoIcons.cloud_upload, color: context.colorScheme.textSecondary, - size: 22, + size: 20, ), ), - if (!Platform.isIOS && !Platform.isMacOS) - const SizedBox(width: 8), + SvgPicture.asset( + Assets.images.icons.icGoogleDrive, + width: 14, + height: 14, + ), ], ), + const SizedBox(width: 16), + Text( + context.l10n.upload_to_google_drive_title, + style: AppTextStyles.body2.copyWith( + color: context.colorScheme.textPrimary, + ), + ), + ], + ), + onTap: () { + context.pop(); + showAppAlertDialog( + context: context, + title: context.l10n.upload_to_google_drive_title, + message: context.l10n.upload_to_google_drive_confirmation_message, + actions: [ + AppAlertAction( + title: context.l10n.common_cancel, + onPressed: () { + context.pop(); + }, + ), + AppAlertAction( + title: context.l10n.common_upload, + onPressed: () { + _notifier.uploadMediaInGoogleDrive(media: media); + context.pop(); + }, + ), + ], ); }, ); } - Future _showDeleteFromDriveDialog({ - required BuildContext context, - required void Function() onDelete, - }) async { - await showAppAlertDialog( - context: context, - title: context.l10n.common_delete_from_google_drive, - message: context.l10n.delete_media_from_google_drive_confirmation_message, - actions: [ - AppAlertAction( - title: context.l10n.common_cancel, - onPressed: () { - context.pop(); - }, - ), - AppAlertAction( - isDestructiveAction: true, - title: context.l10n.common_delete, - onPressed: onDelete, - ), - ], + PopupMenuItem _downloadFromGoogleDriveAction( + BuildContext context, + AppMedia media, + ) { + return PopupMenuItem( + child: Row( + children: [ + Stack( + alignment: Alignment.bottomRight, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 0, right: 8), + child: Icon( + CupertinoIcons.cloud_download, + color: context.colorScheme.textSecondary, + size: 20, + ), + ), + SvgPicture.asset( + Assets.images.icons.icGoogleDrive, + width: 14, + height: 14, + ), + ], + ), + const SizedBox(width: 16), + Text( + context.l10n.download_from_google_drive_title, + style: AppTextStyles.body2.copyWith( + color: context.colorScheme.textPrimary, + ), + ), + ], + ), + onTap: () { + context.pop(); + showAppAlertDialog( + context: context, + title: context.l10n.download_from_google_drive_title, + message: context.l10n.download_from_google_drive_confirmation_message, + actions: [ + AppAlertAction( + title: context.l10n.common_cancel, + onPressed: () { + context.pop(); + }, + ), + AppAlertAction( + title: context.l10n.common_download, + onPressed: () { + _notifier.downloadFromGoogleDrive(media: media); + context.pop(); + }, + ), + ], + ); + }, ); } - Future _showDeleteFromDeviceDialog({ - required BuildContext context, - required void Function() onDelete, - }) async { - await showAppAlertDialog( - context: context, - title: context.l10n.common_delete_from_device, - message: context.l10n.delete_media_from_device_confirmation_message, - actions: [ - AppAlertAction( - title: context.l10n.common_cancel, - onPressed: () { - context.pop(); - }, - ), - AppAlertAction( - isDestructiveAction: true, - title: context.l10n.common_delete, - onPressed: onDelete, - ), - ], + PopupMenuItem _deleteFromGoogleDriveAction( + BuildContext context, + AppMedia media, + ) { + return PopupMenuItem( + child: Row( + children: [ + Stack( + alignment: Alignment.bottomRight, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 2, right: 2), + child: Icon( + CupertinoIcons.trash, + color: context.colorScheme.textSecondary, + size: 20, + ), + ), + SvgPicture.asset( + Assets.images.icons.icGoogleDrive, + width: 14, + height: 14, + ), + ], + ), + const SizedBox(width: 16), + Text( + context.l10n.delete_from_google_drive_title, + style: AppTextStyles.body2.copyWith( + color: context.colorScheme.textPrimary, + ), + ), + ], + ), + onTap: () { + context.pop(); + showAppAlertDialog( + context: context, + title: context.l10n.delete_from_google_drive_title, + message: context.l10n.delete_from_google_drive_confirmation_message, + actions: [ + AppAlertAction( + title: context.l10n.common_cancel, + onPressed: () { + context.pop(); + }, + ), + AppAlertAction( + isDestructiveAction: true, + title: context.l10n.common_delete, + onPressed: () { + _notifier.deleteMediaFromGoogleDrive(media.driveMediaRefId!); + context.pop(); + }, + ), + ], + ); + }, + ); + } + + PopupMenuItem _uploadToDropboxAction( + BuildContext context, + AppMedia media, + ) { + return PopupMenuItem( + child: Row( + children: [ + Stack( + alignment: Alignment.bottomRight, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 0, right: 8), + child: Icon( + CupertinoIcons.cloud_upload, + color: context.colorScheme.textSecondary, + size: 20, + ), + ), + SvgPicture.asset( + Assets.images.icons.icDropbox, + width: 14, + height: 14, + ), + ], + ), + const SizedBox(width: 16), + Text( + context.l10n.upload_to_dropbox_title, + style: AppTextStyles.body2.copyWith( + color: context.colorScheme.textPrimary, + ), + ), + ], + ), + onTap: () { + context.pop(); + showAppAlertDialog( + context: context, + title: context.l10n.upload_to_dropbox_title, + message: context.l10n.upload_to_dropbox_confirmation_message, + actions: [ + AppAlertAction( + title: context.l10n.common_cancel, + onPressed: () { + context.pop(); + }, + ), + AppAlertAction( + title: context.l10n.common_upload, + onPressed: () { + _notifier.uploadMediaInDropbox(media: media); + context.pop(); + }, + ), + ], + ); + }, + ); + } + + PopupMenuItem _downloadFromDropboxAction( + BuildContext context, + AppMedia media, + ) { + return PopupMenuItem( + child: Row( + children: [ + Stack( + alignment: Alignment.bottomRight, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 0, right: 8), + child: Icon( + CupertinoIcons.cloud_download, + color: context.colorScheme.textSecondary, + size: 20, + ), + ), + SvgPicture.asset( + Assets.images.icons.icDropbox, + width: 14, + height: 14, + ), + ], + ), + const SizedBox(width: 16), + Text( + context.l10n.download_from_dropbox_title, + style: AppTextStyles.body2.copyWith( + color: context.colorScheme.textPrimary, + ), + ), + ], + ), + onTap: () { + context.pop(); + showAppAlertDialog( + context: context, + title: context.l10n.download_from_dropbox_title, + message: context.l10n.download_from_dropbox_confirmation_message, + actions: [ + AppAlertAction( + title: context.l10n.common_cancel, + onPressed: () { + context.pop(); + }, + ), + AppAlertAction( + title: context.l10n.common_download, + onPressed: () { + _notifier.downloadFromDropbox(media: media); + context.pop(); + }, + ), + ], + ); + }, + ); + } + + PopupMenuItem _deleteFromDropboxAction( + BuildContext context, + AppMedia media, + ) { + return PopupMenuItem( + child: Row( + children: [ + Stack( + alignment: Alignment.bottomRight, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 2, right: 2), + child: Icon( + CupertinoIcons.trash, + color: context.colorScheme.textSecondary, + size: 20, + ), + ), + SvgPicture.asset( + Assets.images.icons.icDropbox, + width: 14, + height: 14, + ), + ], + ), + const SizedBox(width: 16), + Text( + context.l10n.delete_from_dropbox_title, + style: AppTextStyles.body2.copyWith( + color: context.colorScheme.textPrimary, + ), + ), + ], + ), + onTap: () { + context.pop(); + showAppAlertDialog( + context: context, + title: context.l10n.delete_from_dropbox_title, + message: context.l10n.delete_from_dropbox_confirmation_message, + actions: [ + AppAlertAction( + title: context.l10n.common_cancel, + onPressed: () { + context.pop(); + }, + ), + AppAlertAction( + isDestructiveAction: true, + title: context.l10n.common_delete, + onPressed: () { + _notifier.deleteMediaFromDropbox(media.dropboxMediaRefId!); + context.pop(); + }, + ), + ], + ); + }, + ); + } + + PopupMenuItem _deleteFromDeviceAction( + BuildContext context, + AppMedia media, + ) { + return PopupMenuItem( + child: Row( + children: [ + Icon( + CupertinoIcons.delete, + color: context.colorScheme.textSecondary, + size: 22, + ), + const SizedBox(width: 16), + Text( + context.l10n.delete_from_device_title, + style: AppTextStyles.body2.copyWith( + color: context.colorScheme.textPrimary, + ), + ), + ], + ), + onTap: () { + context.pop(); + showAppAlertDialog( + context: context, + title: context.l10n.delete_from_device_title, + message: context.l10n.delete_from_device_confirmation_message, + actions: [ + AppAlertAction( + title: context.l10n.common_cancel, + onPressed: () { + context.pop(); + }, + ), + AppAlertAction( + isDestructiveAction: true, + title: context.l10n.common_delete, + onPressed: () { + _notifier.deleteMediaFromLocal(media.id); + context.pop(); + }, + ), + ], + ); + }, + ); + } + + PopupMenuItem _shareAction(BuildContext context, AppMedia media) { + return PopupMenuItem( + onTap: () async { + context.pop(); + await Share.shareXFiles([XFile(media.path)]); + }, + child: Row( + children: [ + Icon( + Platform.isIOS ? CupertinoIcons.share : Icons.share_rounded, + color: context.colorScheme.textSecondary, + size: 22, + ), + const SizedBox(width: 16), + Text( + context.l10n.common_share, + style: AppTextStyles.body2.copyWith( + color: context.colorScheme.textPrimary, + ), + ), + ], + ), ); } } diff --git a/app/lib/ui/flow/media_preview/media_preview_screen.dart b/app/lib/ui/flow/media_preview/media_preview_screen.dart index d02828d..24a0236 100644 --- a/app/lib/ui/flow/media_preview/media_preview_screen.dart +++ b/app/lib/ui/flow/media_preview/media_preview_screen.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'package:data/storage/app_preferences.dart'; import '../../../components/app_page.dart'; import '../../../components/error_view.dart'; import '../../../components/snack_bar.dart'; @@ -38,7 +39,7 @@ class _MediaPreviewState extends ConsumerState { late AutoDisposeStateNotifierProvider _provider; late PageController _pageController; - late MediaPreviewStateNotifier notifier; + late MediaPreviewStateNotifier _notifier; VideoPlayerController? _videoPlayerController; @@ -47,11 +48,10 @@ class _MediaPreviewState extends ConsumerState { final currentIndex = widget.medias.indexWhere((element) => element.id == widget.startFrom); - //initialize view notifier with initial state _provider = mediaPreviewStateNotifierProvider( - MediaPreviewState(currentIndex: currentIndex, medias: widget.medias), + (startIndex: currentIndex, medias: widget.medias), ); - notifier = ref.read(_provider.notifier); + _notifier = ref.read(_provider.notifier); _pageController = PageController(initialPage: currentIndex, keepPage: true); @@ -62,8 +62,7 @@ class _MediaPreviewState extends ConsumerState { path: widget.medias[currentIndex].path, ), ); - } else if (widget.medias[currentIndex].type.isVideo && - widget.medias[currentIndex].isGoogleDriveStored) {} + } super.initState(); } @@ -73,26 +72,26 @@ class _MediaPreviewState extends ConsumerState { _videoPlayerController = VideoPlayerController.file(File(path)); _videoPlayerController?.addListener(_observeVideoController); await _videoPlayerController?.initialize(); - notifier.updateVideoInitialized( + _notifier.updateVideoInitialized( _videoPlayerController?.value.isInitialized ?? false, ); await _videoPlayerController?.play(); } void _observeVideoController() { - notifier.updateVideoInitialized( + _notifier.updateVideoInitialized( _videoPlayerController?.value.isInitialized ?? false, ); - notifier.updateVideoBuffering( + _notifier.updateVideoBuffering( _videoPlayerController?.value.isBuffering ?? false, ); - notifier.updateVideoPosition( + _notifier.updateVideoPosition( _videoPlayerController?.value.position ?? Duration.zero, ); - notifier.updateVideoMaxDuration( + _notifier.updateVideoMaxDuration( _videoPlayerController?.value.duration ?? Duration.zero, ); - notifier + _notifier .updateVideoPlaying(_videoPlayerController?.value.isPlaying ?? false); } @@ -112,7 +111,7 @@ class _MediaPreviewState extends ConsumerState { (previous, next) { if (_videoPlayerController != null) { _videoPlayerController?.removeListener(_observeVideoController); - notifier.updateVideoInitialized(false); + _notifier.updateVideoInitialized(false); _videoPlayerController?.dispose(); _videoPlayerController = null; } @@ -147,7 +146,7 @@ class _MediaPreviewState extends ConsumerState { backgroundColor: context.colorScheme.surface, onProgress: (progress) { if (progress > 0 && state.showActions) { - notifier.toggleActionVisibility(); + _notifier.toggleActionVisibility(); } }, onDismiss: () { @@ -160,9 +159,9 @@ class _MediaPreviewState extends ConsumerState { children: [ GestureDetector( behavior: HitTestBehavior.opaque, - onTap: notifier.toggleActionVisibility, + onTap: _notifier.toggleActionVisibility, child: PageView.builder( - onPageChanged: notifier.changeVisibleMediaIndex, + onPageChanged: _notifier.changeVisibleMediaIndex, controller: _pageController, itemCount: state.medias.length, itemBuilder: (context, index) => @@ -200,24 +199,33 @@ class _MediaPreviewState extends ConsumerState { ), ); - if (!state.initialized || state.buffring) { + if (!state.initialized) { return AppCircularProgressIndicator( color: context.colorScheme.onPrimary, ); - } else { - return Hero( - tag: media, - child: AspectRatio( - aspectRatio: _videoPlayerController!.value.aspectRatio, - child: VideoPlayer(_videoPlayerController!), - ), - ); } + return Hero( + tag: media, + child: Stack( + alignment: Alignment.center, + children: [ + AspectRatio( + aspectRatio: _videoPlayerController!.value.aspectRatio, + child: VideoPlayer(_videoPlayerController!), + ), + if (state.buffring) + AppCircularProgressIndicator( + color: context.colorScheme.onPrimary, + ), + ], + ), + ); }, ), ); - } else if (media.type.isVideo && media.isGoogleDriveStored) { - return _googleDriveVideoView(context: context, media: media); + } else if (media.type.isVideo && + (media.isGoogleDriveStored || media.isDropboxStored)) { + return _cloudVideoView(context: context, media: media); } else if (media.type.isImage) { return ImagePreview(media: media); } else { @@ -228,7 +236,7 @@ class _MediaPreviewState extends ConsumerState { } } - Widget _googleDriveVideoView({ + Widget _cloudVideoView({ required BuildContext context, required AppMedia media, }) { @@ -236,16 +244,25 @@ class _MediaPreviewState extends ConsumerState { builder: (context, ref, child) { final process = ref.watch( _provider.select( - (value) => value.downloadProcess - .where((element) => element.id == media.id) - .firstOrNull, + (value) => + media.driveMediaRefId != null && media.isGoogleDriveStored + ? value.downloadMediaProcesses[media.driveMediaRefId] + : media.dropboxMediaRefId != null + ? value.downloadMediaProcesses[media.dropboxMediaRefId] + : null, ), ); return DownloadRequireView( + dropboxAccessToken: + ref.read(AppPreferences.dropboxToken)?.access_token, media: media, downloadProcess: process, onDownload: () { - notifier.downloadMediaFromGoogleDrive(media: media); + if (media.isGoogleDriveStored) { + _notifier.downloadFromGoogleDrive(media: media); + } else if (media.isDropboxStored) { + _notifier.downloadFromDropbox(media: media); + } }, ); }, @@ -317,7 +334,7 @@ class _MediaPreviewState extends ConsumerState { _videoPlayerController?.seekTo(duration); }, onChanged: (duration) { - notifier.updateVideoPosition(duration); + _notifier.updateVideoPosition(duration); }, ); }, diff --git a/app/lib/ui/flow/media_preview/media_preview_view_model.dart b/app/lib/ui/flow/media_preview/media_preview_view_model.dart index f83e2ae..939f7f5 100644 --- a/app/lib/ui/flow/media_preview/media_preview_view_model.dart +++ b/app/lib/ui/flow/media_preview/media_preview_view_model.dart @@ -1,116 +1,264 @@ -import '../../../domain/extensions/media_list_extension.dart'; -import 'package:data/models/app_process/app_process.dart'; +import 'dart:async'; +import 'package:data/domain/config.dart'; +import 'package:data/models/dropbox/account/dropbox_account.dart'; import 'package:data/models/media/media.dart'; -import 'package:data/repositories/google_drive_process_repo.dart'; +import 'package:data/models/media/media_extension.dart'; +import 'package:data/models/media_process/media_process.dart'; +import 'package:data/repositories/media_process_repository.dart'; +import 'package:data/services/auth_service.dart'; +import 'package:data/services/dropbox_services.dart'; import 'package:data/services/google_drive_service.dart'; import 'package:data/services/local_media_service.dart'; +import 'package:data/storage/app_preferences.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:google_sign_in/google_sign_in.dart'; part 'media_preview_view_model.freezed.dart'; -final mediaPreviewStateNotifierProvider = StateNotifierProvider.family - .autoDispose( - (ref, initial) => MediaPreviewStateNotifier( +final mediaPreviewStateNotifierProvider = + StateNotifierProvider.family.autoDispose< + MediaPreviewStateNotifier, + MediaPreviewState, + ({ + int startIndex, + List medias, + })>((ref, state) { + final notifier = MediaPreviewStateNotifier( ref.read(localMediaServiceProvider), ref.read(googleDriveServiceProvider), - ref.read(googleDriveProcessRepoProvider), - initial, - ), -); + ref.read(dropboxServiceProvider), + ref.read(mediaProcessRepoProvider), + ref.read(authServiceProvider), + state.medias, + state.startIndex, + ref.read(AppPreferences.dropboxCurrentUserAccount), + ); + ref.listen(AppPreferences.dropboxCurrentUserAccount, (previous, next) { + notifier._listenDropboxAccount(next); + }); + return notifier; +}); class MediaPreviewStateNotifier extends StateNotifier { final LocalMediaService _localMediaService; final GoogleDriveService _googleDriveService; - final GoogleDriveProcessRepo _googleDriveProcessRepo; + final DropboxService _dropboxService; + final MediaProcessRepo _mediaProcessRepo; + final AuthService _authService; + + StreamSubscription? _googleAccountSubscription; + String? _backUpFolderId; MediaPreviewStateNotifier( this._localMediaService, this._googleDriveService, - this._googleDriveProcessRepo, - MediaPreviewState initialState, - ) : super(initialState) { - _googleDriveProcessRepo.addListener(_listenGoogleDriveProcessUpdates); + this._dropboxService, + this._mediaProcessRepo, + this._authService, + List medias, + int startIndex, + DropboxAccount? dropboxAccount, + ) : super( + MediaPreviewState( + googleAccount: _authService.googleAccount, + currentIndex: startIndex, + medias: medias, + dropboxAccount: dropboxAccount, + ), + ) { + _mediaProcessRepo.addListener(_mediaProcessObserve); + _setBackUpFolderId(); + _listenGoogleAccount(); } - void _listenGoogleDriveProcessUpdates() { - final successUploads = _googleDriveProcessRepo.uploadQueue - .where((element) => element.status.isSuccess); + // Google Account Listener --------------------------------------------------- - final successDeletes = _googleDriveProcessRepo.deleteQueue - .where((element) => element.status.isSuccess) - .map((e) => e.id); + void _listenGoogleAccount() { + _googleAccountSubscription = _authService.onGoogleAccountChange.listen( + (account) { + state = state.copyWith(googleAccount: account); + if (account == null) { + _backUpFolderId = null; + } else { + _setBackUpFolderId(); + } + }, + ); + } - final successDownloads = _googleDriveProcessRepo.downloadQueue - .where((element) => element.status.isSuccess); + void _listenDropboxAccount(DropboxAccount? account) { + state = state.copyWith(dropboxAccount: account); + } - if (successUploads.isNotEmpty) { - state = state.copyWith( - medias: state.medias.toList() - ..addGoogleDriveRefInMedias( - process: successUploads.toList(), - ), - ); - } + Future _setBackUpFolderId() async { + if (state.googleAccount == null) return; + _backUpFolderId = await _googleDriveService.getBackUpFolderId(); + } - if (successDeletes.isNotEmpty) { - state = state.copyWith( - medias: state.medias.toList() - ..removeGoogleDriveRefFromMedias( - removeFromIds: successDeletes.toList(), - ), - ); - } - if (successDownloads.isNotEmpty) { - state = state.copyWith( - medias: state.medias.toList() - ..replaceMediaRefInMedias( - process: successDownloads.toList(), - ), - ); - } + // Media Process Observer ---------------------------------------------------- + void _mediaProcessObserve() { state = state.copyWith( - uploadProcess: _googleDriveProcessRepo.uploadQueue, - downloadProcess: _googleDriveProcessRepo.downloadQueue, - deleteProcess: _googleDriveProcessRepo.deleteQueue, + uploadMediaProcesses: Map.fromEntries( + _mediaProcessRepo.uploadQueue.map((e) => MapEntry(e.media_id, e)), + ), + downloadMediaProcesses: Map.fromEntries( + _mediaProcessRepo.downloadQueue.map((e) => MapEntry(e.media_id, e)), + ), ); - } - void changeVisibleMediaIndex(int index) { - state = state.copyWith(currentIndex: index); - } + for (final process in _mediaProcessRepo.uploadQueue) { + if (process.status.isCompleted) { + state = state.copyWith( + medias: state.medias.map( + (media) { + if (media.id == process.media_id && + process.provider == MediaProvider.googleDrive && + !media.sources.contains(AppMediaSource.googleDrive) && + process.response != null) { + return media.mergeGoogleDriveMedia(process.response!); + } else if (media.id == process.media_id && + process.provider == MediaProvider.dropbox && + !media.sources.contains(AppMediaSource.dropbox) && + process.response != null) { + return media.mergeDropboxMedia(process.response!); + } + return media; + }, + ).toList(), + ); + } + } - void toggleActionVisibility() { - state = state.copyWith(showActions: !state.showActions); + for (final process in _mediaProcessRepo.downloadQueue) { + if (process.status.isCompleted) { + state = state.copyWith( + medias: state.medias.map( + (media) { + if (media.driveMediaRefId != null && + media.driveMediaRefId == process.media_id && + process.provider == MediaProvider.googleDrive && + !media.sources.contains(AppMediaSource.local) && + process.response != null) { + return process.response!.mergeGoogleDriveMedia(media); + } else if (media.dropboxMediaRefId != null && + media.dropboxMediaRefId == process.media_id && + process.provider == MediaProvider.dropbox && + !media.sources.contains(AppMediaSource.local) && + process.response != null) { + return process.response!.mergeDropboxMedia(media); + } + return media; + }, + ).toList(), + ); + } + } } + // Media Actions ------------------------------------------------------------- + Future deleteMediaFromLocal(String id) async { try { + state = state.copyWith(actionError: null); await _localMediaService.deleteMedias([id]); - state = state.copyWith( - medias: state.medias.where((element) => element.id != id).toList(), - ); + final List medias = []; + for (final media in state.medias) { + if (media.id != id) { + medias.add(media); + } else if (media.id == id && media.isCommonStored) { + medias.add(media.removeLocalRef()); + } + } + state = state.copyWith(medias: medias); } catch (error) { - state = state.copyWith(error: error); + state = state.copyWith(actionError: error); } } - Future deleteMediaFromGoogleDrive(String? id) async { + Future deleteMediaFromGoogleDrive(String id) async { try { - await _googleDriveService.deleteMedia(id!); + if (state.googleAccount == null) return; + state = state.copyWith(actionError: null); + await _googleDriveService.deleteMedia(id: id); + final List medias = []; + for (final media in state.medias) { + if (media.driveMediaRefId != id) { + medias.add(media); + } else if (media.driveMediaRefId == id && media.isCommonStored) { + medias.add(media.removeGoogleDriveRef()); + } + } } catch (error) { - state = state.copyWith(error: error); + state = state.copyWith(actionError: error); } } - Future downloadMediaFromGoogleDrive({required AppMedia media}) async { - _googleDriveProcessRepo.downloadMediasFromGoogleDrive(medias: [media]); + Future deleteMediaFromDropbox(String id) async { + try { + if (_authService.dropboxAccount == null) return; + state = state.copyWith(actionError: null); + await _dropboxService.deleteMedia(id: id); + final List medias = []; + for (final media in state.medias) { + if (media.dropboxMediaRefId != id) { + medias.add(media); + } else if (media.dropboxMediaRefId == id && media.isCommonStored) { + medias.add(media.removeDropboxRef()); + } + } + } catch (error) { + state = state.copyWith(actionError: error); + } } Future uploadMediaInGoogleDrive({required AppMedia media}) async { - _googleDriveProcessRepo.uploadMedia([media]); + if (state.googleAccount == null) return; + _mediaProcessRepo.uploadMedia( + folderId: _backUpFolderId!, + medias: [media], + provider: MediaProvider.googleDrive, + ); + } + + Future uploadMediaInDropbox({required AppMedia media}) async { + if (_authService.dropboxAccount == null) return; + _mediaProcessRepo.uploadMedia( + folderId: ProviderConstants.backupFolderPath, + medias: [media], + provider: MediaProvider.dropbox, + ); + } + + Future downloadFromGoogleDrive({required AppMedia media}) async { + if (state.googleAccount == null) return; + _mediaProcessRepo.downloadMedia( + folderId: _backUpFolderId!, + medias: [media], + provider: MediaProvider.googleDrive, + ); + } + + Future downloadFromDropbox({required AppMedia media}) async { + if (_authService.dropboxAccount == null) return; + _mediaProcessRepo.downloadMedia( + folderId: ProviderConstants.backupFolderPath, + medias: [media], + provider: MediaProvider.dropbox, + ); + } + + // Preview Actions ----------------------------------------------------------- + + void changeVisibleMediaIndex(int index) { + state = state.copyWith(currentIndex: index); + } + + // Video Player Actions ------------------------------------------------------ + + void toggleActionVisibility() { + state = state.copyWith(showActions: !state.showActions); } void updateVideoPosition(Duration position) { @@ -140,7 +288,8 @@ class MediaPreviewStateNotifier extends StateNotifier { @override void dispose() { - _googleDriveProcessRepo.removeListener(_listenGoogleDriveProcessUpdates); + _googleAccountSubscription?.cancel(); + _mediaProcessRepo.removeListener(_mediaProcessObserve); super.dispose(); } } @@ -148,7 +297,10 @@ class MediaPreviewStateNotifier extends StateNotifier { @freezed class MediaPreviewState with _$MediaPreviewState { const factory MediaPreviewState({ + GoogleSignInAccount? googleAccount, + DropboxAccount? dropboxAccount, Object? error, + Object? actionError, @Default([]) List medias, @Default(0) int currentIndex, @Default(true) bool showActions, @@ -157,8 +309,7 @@ class MediaPreviewState with _$MediaPreviewState { @Default(Duration.zero) Duration videoPosition, @Default(Duration.zero) Duration videoMaxDuration, @Default(false) bool isVideoPlaying, - @Default([]) List uploadProcess, - @Default([]) List downloadProcess, - @Default([]) List deleteProcess, + @Default({}) Map uploadMediaProcesses, + @Default({}) Map downloadMediaProcesses, }) = _MediaPreviewState; } diff --git a/app/lib/ui/flow/media_preview/media_preview_view_model.freezed.dart b/app/lib/ui/flow/media_preview/media_preview_view_model.freezed.dart index 323704a..59f61df 100644 --- a/app/lib/ui/flow/media_preview/media_preview_view_model.freezed.dart +++ b/app/lib/ui/flow/media_preview/media_preview_view_model.freezed.dart @@ -16,7 +16,10 @@ final _privateConstructorUsedError = UnsupportedError( /// @nodoc mixin _$MediaPreviewState { + GoogleSignInAccount? get googleAccount => throw _privateConstructorUsedError; + DropboxAccount? get dropboxAccount => throw _privateConstructorUsedError; Object? get error => throw _privateConstructorUsedError; + Object? get actionError => throw _privateConstructorUsedError; List get medias => throw _privateConstructorUsedError; int get currentIndex => throw _privateConstructorUsedError; bool get showActions => throw _privateConstructorUsedError; @@ -25,9 +28,10 @@ mixin _$MediaPreviewState { Duration get videoPosition => throw _privateConstructorUsedError; Duration get videoMaxDuration => throw _privateConstructorUsedError; bool get isVideoPlaying => throw _privateConstructorUsedError; - List get uploadProcess => throw _privateConstructorUsedError; - List get downloadProcess => throw _privateConstructorUsedError; - List get deleteProcess => throw _privateConstructorUsedError; + Map get uploadMediaProcesses => + throw _privateConstructorUsedError; + Map get downloadMediaProcesses => + throw _privateConstructorUsedError; /// Create a copy of MediaPreviewState /// with the given fields replaced by the non-null parameter values. @@ -43,7 +47,10 @@ abstract class $MediaPreviewStateCopyWith<$Res> { _$MediaPreviewStateCopyWithImpl<$Res, MediaPreviewState>; @useResult $Res call( - {Object? error, + {GoogleSignInAccount? googleAccount, + DropboxAccount? dropboxAccount, + Object? error, + Object? actionError, List medias, int currentIndex, bool showActions, @@ -52,9 +59,10 @@ abstract class $MediaPreviewStateCopyWith<$Res> { Duration videoPosition, Duration videoMaxDuration, bool isVideoPlaying, - List uploadProcess, - List downloadProcess, - List deleteProcess}); + Map uploadMediaProcesses, + Map downloadMediaProcesses}); + + $DropboxAccountCopyWith<$Res>? get dropboxAccount; } /// @nodoc @@ -72,7 +80,10 @@ class _$MediaPreviewStateCopyWithImpl<$Res, $Val extends MediaPreviewState> @pragma('vm:prefer-inline') @override $Res call({ + Object? googleAccount = freezed, + Object? dropboxAccount = freezed, Object? error = freezed, + Object? actionError = freezed, Object? medias = null, Object? currentIndex = null, Object? showActions = null, @@ -81,12 +92,20 @@ class _$MediaPreviewStateCopyWithImpl<$Res, $Val extends MediaPreviewState> Object? videoPosition = null, Object? videoMaxDuration = null, Object? isVideoPlaying = null, - Object? uploadProcess = null, - Object? downloadProcess = null, - Object? deleteProcess = null, + Object? uploadMediaProcesses = null, + Object? downloadMediaProcesses = null, }) { return _then(_value.copyWith( + googleAccount: freezed == googleAccount + ? _value.googleAccount + : googleAccount // ignore: cast_nullable_to_non_nullable + as GoogleSignInAccount?, + dropboxAccount: freezed == dropboxAccount + ? _value.dropboxAccount + : dropboxAccount // ignore: cast_nullable_to_non_nullable + as DropboxAccount?, error: freezed == error ? _value.error : error, + actionError: freezed == actionError ? _value.actionError : actionError, medias: null == medias ? _value.medias : medias // ignore: cast_nullable_to_non_nullable @@ -119,20 +138,30 @@ class _$MediaPreviewStateCopyWithImpl<$Res, $Val extends MediaPreviewState> ? _value.isVideoPlaying : isVideoPlaying // ignore: cast_nullable_to_non_nullable as bool, - uploadProcess: null == uploadProcess - ? _value.uploadProcess - : uploadProcess // ignore: cast_nullable_to_non_nullable - as List, - downloadProcess: null == downloadProcess - ? _value.downloadProcess - : downloadProcess // ignore: cast_nullable_to_non_nullable - as List, - deleteProcess: null == deleteProcess - ? _value.deleteProcess - : deleteProcess // ignore: cast_nullable_to_non_nullable - as List, + uploadMediaProcesses: null == uploadMediaProcesses + ? _value.uploadMediaProcesses + : uploadMediaProcesses // ignore: cast_nullable_to_non_nullable + as Map, + downloadMediaProcesses: null == downloadMediaProcesses + ? _value.downloadMediaProcesses + : downloadMediaProcesses // ignore: cast_nullable_to_non_nullable + as Map, ) as $Val); } + + /// Create a copy of MediaPreviewState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $DropboxAccountCopyWith<$Res>? get dropboxAccount { + if (_value.dropboxAccount == null) { + return null; + } + + return $DropboxAccountCopyWith<$Res>(_value.dropboxAccount!, (value) { + return _then(_value.copyWith(dropboxAccount: value) as $Val); + }); + } } /// @nodoc @@ -144,7 +173,10 @@ abstract class _$$MediaPreviewStateImplCopyWith<$Res> @override @useResult $Res call( - {Object? error, + {GoogleSignInAccount? googleAccount, + DropboxAccount? dropboxAccount, + Object? error, + Object? actionError, List medias, int currentIndex, bool showActions, @@ -153,9 +185,11 @@ abstract class _$$MediaPreviewStateImplCopyWith<$Res> Duration videoPosition, Duration videoMaxDuration, bool isVideoPlaying, - List uploadProcess, - List downloadProcess, - List deleteProcess}); + Map uploadMediaProcesses, + Map downloadMediaProcesses}); + + @override + $DropboxAccountCopyWith<$Res>? get dropboxAccount; } /// @nodoc @@ -171,7 +205,10 @@ class __$$MediaPreviewStateImplCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ + Object? googleAccount = freezed, + Object? dropboxAccount = freezed, Object? error = freezed, + Object? actionError = freezed, Object? medias = null, Object? currentIndex = null, Object? showActions = null, @@ -180,12 +217,20 @@ class __$$MediaPreviewStateImplCopyWithImpl<$Res> Object? videoPosition = null, Object? videoMaxDuration = null, Object? isVideoPlaying = null, - Object? uploadProcess = null, - Object? downloadProcess = null, - Object? deleteProcess = null, + Object? uploadMediaProcesses = null, + Object? downloadMediaProcesses = null, }) { return _then(_$MediaPreviewStateImpl( + googleAccount: freezed == googleAccount + ? _value.googleAccount + : googleAccount // ignore: cast_nullable_to_non_nullable + as GoogleSignInAccount?, + dropboxAccount: freezed == dropboxAccount + ? _value.dropboxAccount + : dropboxAccount // ignore: cast_nullable_to_non_nullable + as DropboxAccount?, error: freezed == error ? _value.error : error, + actionError: freezed == actionError ? _value.actionError : actionError, medias: null == medias ? _value._medias : medias // ignore: cast_nullable_to_non_nullable @@ -218,18 +263,14 @@ class __$$MediaPreviewStateImplCopyWithImpl<$Res> ? _value.isVideoPlaying : isVideoPlaying // ignore: cast_nullable_to_non_nullable as bool, - uploadProcess: null == uploadProcess - ? _value._uploadProcess - : uploadProcess // ignore: cast_nullable_to_non_nullable - as List, - downloadProcess: null == downloadProcess - ? _value._downloadProcess - : downloadProcess // ignore: cast_nullable_to_non_nullable - as List, - deleteProcess: null == deleteProcess - ? _value._deleteProcess - : deleteProcess // ignore: cast_nullable_to_non_nullable - as List, + uploadMediaProcesses: null == uploadMediaProcesses + ? _value._uploadMediaProcesses + : uploadMediaProcesses // ignore: cast_nullable_to_non_nullable + as Map, + downloadMediaProcesses: null == downloadMediaProcesses + ? _value._downloadMediaProcesses + : downloadMediaProcesses // ignore: cast_nullable_to_non_nullable + as Map, )); } } @@ -238,7 +279,10 @@ class __$$MediaPreviewStateImplCopyWithImpl<$Res> class _$MediaPreviewStateImpl implements _MediaPreviewState { const _$MediaPreviewStateImpl( - {this.error, + {this.googleAccount, + this.dropboxAccount, + this.error, + this.actionError, final List medias = const [], this.currentIndex = 0, this.showActions = true, @@ -247,16 +291,21 @@ class _$MediaPreviewStateImpl implements _MediaPreviewState { this.videoPosition = Duration.zero, this.videoMaxDuration = Duration.zero, this.isVideoPlaying = false, - final List uploadProcess = const [], - final List downloadProcess = const [], - final List deleteProcess = const []}) + final Map uploadMediaProcesses = const {}, + final Map downloadMediaProcesses = + const {}}) : _medias = medias, - _uploadProcess = uploadProcess, - _downloadProcess = downloadProcess, - _deleteProcess = deleteProcess; + _uploadMediaProcesses = uploadMediaProcesses, + _downloadMediaProcesses = downloadMediaProcesses; + @override + final GoogleSignInAccount? googleAccount; + @override + final DropboxAccount? dropboxAccount; @override final Object? error; + @override + final Object? actionError; final List _medias; @override @JsonKey() @@ -287,36 +336,29 @@ class _$MediaPreviewStateImpl implements _MediaPreviewState { @override @JsonKey() final bool isVideoPlaying; - final List _uploadProcess; + final Map _uploadMediaProcesses; @override @JsonKey() - List get uploadProcess { - if (_uploadProcess is EqualUnmodifiableListView) return _uploadProcess; + Map get uploadMediaProcesses { + if (_uploadMediaProcesses is EqualUnmodifiableMapView) + return _uploadMediaProcesses; // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_uploadProcess); + return EqualUnmodifiableMapView(_uploadMediaProcesses); } - final List _downloadProcess; + final Map _downloadMediaProcesses; @override @JsonKey() - List get downloadProcess { - if (_downloadProcess is EqualUnmodifiableListView) return _downloadProcess; + Map get downloadMediaProcesses { + if (_downloadMediaProcesses is EqualUnmodifiableMapView) + return _downloadMediaProcesses; // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_downloadProcess); - } - - final List _deleteProcess; - @override - @JsonKey() - List get deleteProcess { - if (_deleteProcess is EqualUnmodifiableListView) return _deleteProcess; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_deleteProcess); + return EqualUnmodifiableMapView(_downloadMediaProcesses); } @override String toString() { - return 'MediaPreviewState(error: $error, medias: $medias, currentIndex: $currentIndex, showActions: $showActions, isVideoInitialized: $isVideoInitialized, isVideoBuffering: $isVideoBuffering, videoPosition: $videoPosition, videoMaxDuration: $videoMaxDuration, isVideoPlaying: $isVideoPlaying, uploadProcess: $uploadProcess, downloadProcess: $downloadProcess, deleteProcess: $deleteProcess)'; + return 'MediaPreviewState(googleAccount: $googleAccount, dropboxAccount: $dropboxAccount, error: $error, actionError: $actionError, medias: $medias, currentIndex: $currentIndex, showActions: $showActions, isVideoInitialized: $isVideoInitialized, isVideoBuffering: $isVideoBuffering, videoPosition: $videoPosition, videoMaxDuration: $videoMaxDuration, isVideoPlaying: $isVideoPlaying, uploadMediaProcesses: $uploadMediaProcesses, downloadMediaProcesses: $downloadMediaProcesses)'; } @override @@ -324,7 +366,13 @@ class _$MediaPreviewStateImpl implements _MediaPreviewState { return identical(this, other) || (other.runtimeType == runtimeType && other is _$MediaPreviewStateImpl && + (identical(other.googleAccount, googleAccount) || + other.googleAccount == googleAccount) && + (identical(other.dropboxAccount, dropboxAccount) || + other.dropboxAccount == dropboxAccount) && const DeepCollectionEquality().equals(other.error, error) && + const DeepCollectionEquality() + .equals(other.actionError, actionError) && const DeepCollectionEquality().equals(other._medias, _medias) && (identical(other.currentIndex, currentIndex) || other.currentIndex == currentIndex) && @@ -341,17 +389,18 @@ class _$MediaPreviewStateImpl implements _MediaPreviewState { (identical(other.isVideoPlaying, isVideoPlaying) || other.isVideoPlaying == isVideoPlaying) && const DeepCollectionEquality() - .equals(other._uploadProcess, _uploadProcess) && - const DeepCollectionEquality() - .equals(other._downloadProcess, _downloadProcess) && - const DeepCollectionEquality() - .equals(other._deleteProcess, _deleteProcess)); + .equals(other._uploadMediaProcesses, _uploadMediaProcesses) && + const DeepCollectionEquality().equals( + other._downloadMediaProcesses, _downloadMediaProcesses)); } @override int get hashCode => Object.hash( runtimeType, + googleAccount, + dropboxAccount, const DeepCollectionEquality().hash(error), + const DeepCollectionEquality().hash(actionError), const DeepCollectionEquality().hash(_medias), currentIndex, showActions, @@ -360,9 +409,8 @@ class _$MediaPreviewStateImpl implements _MediaPreviewState { videoPosition, videoMaxDuration, isVideoPlaying, - const DeepCollectionEquality().hash(_uploadProcess), - const DeepCollectionEquality().hash(_downloadProcess), - const DeepCollectionEquality().hash(_deleteProcess)); + const DeepCollectionEquality().hash(_uploadMediaProcesses), + const DeepCollectionEquality().hash(_downloadMediaProcesses)); /// Create a copy of MediaPreviewState /// with the given fields replaced by the non-null parameter values. @@ -376,22 +424,31 @@ class _$MediaPreviewStateImpl implements _MediaPreviewState { abstract class _MediaPreviewState implements MediaPreviewState { const factory _MediaPreviewState( - {final Object? error, - final List medias, - final int currentIndex, - final bool showActions, - final bool isVideoInitialized, - final bool isVideoBuffering, - final Duration videoPosition, - final Duration videoMaxDuration, - final bool isVideoPlaying, - final List uploadProcess, - final List downloadProcess, - final List deleteProcess}) = _$MediaPreviewStateImpl; + {final GoogleSignInAccount? googleAccount, + final DropboxAccount? dropboxAccount, + final Object? error, + final Object? actionError, + final List medias, + final int currentIndex, + final bool showActions, + final bool isVideoInitialized, + final bool isVideoBuffering, + final Duration videoPosition, + final Duration videoMaxDuration, + final bool isVideoPlaying, + final Map uploadMediaProcesses, + final Map downloadMediaProcesses}) = + _$MediaPreviewStateImpl; + @override + GoogleSignInAccount? get googleAccount; + @override + DropboxAccount? get dropboxAccount; @override Object? get error; @override + Object? get actionError; + @override List get medias; @override int get currentIndex; @@ -408,11 +465,9 @@ abstract class _MediaPreviewState implements MediaPreviewState { @override bool get isVideoPlaying; @override - List get uploadProcess; - @override - List get downloadProcess; + Map get uploadMediaProcesses; @override - List get deleteProcess; + Map get downloadMediaProcesses; /// Create a copy of MediaPreviewState /// with the given fields replaced by the non-null parameter values. diff --git a/app/lib/ui/flow/media_transfer/components/transfer_item.dart b/app/lib/ui/flow/media_transfer/components/transfer_item.dart index 67f7151..3e80b1e 100644 --- a/app/lib/ui/flow/media_transfer/components/transfer_item.dart +++ b/app/lib/ui/flow/media_transfer/components/transfer_item.dart @@ -1,89 +1,226 @@ +import 'package:data/domain/formatters/byte_formatter.dart'; +import 'package:data/models/media_process/media_process.dart'; import '../../../../domain/extensions/context_extensions.dart'; -import '../../../../domain/formatter/byte_formatter.dart'; -import 'package:data/models/app_process/app_process.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:style/buttons/action_button.dart'; import 'package:style/extensions/context_extensions.dart'; import 'package:style/text/app_text_style.dart'; -import '../../../../components/thumbnail_builder.dart'; -class ProcessItem extends StatelessWidget { - final AppProcess process; +class UploadProcessItem extends StatelessWidget { + final UploadMediaProcess process; final void Function() onCancelTap; + final void Function() onRemoveTap; - const ProcessItem({ + const UploadProcessItem({ super.key, required this.process, required this.onCancelTap, + required this.onRemoveTap, }); @override Widget build(BuildContext context) { - return Row( - children: [ - AppMediaImage(size: const Size(80, 80), media: process.media), - const SizedBox(width: 16), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - process.media.name != null && - process.media.name!.trim().isNotEmpty - ? process.media.name! - : process.media.path, - style: AppTextStyles.body.copyWith( - color: context.colorScheme.textPrimary, + return Padding( + padding: const EdgeInsets.only(left: 16, right: 8), + child: Row( + children: [ + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + process.path.split('/').lastOrNull ?? process.media_id, + style: AppTextStyles.body.copyWith( + color: context.colorScheme.textPrimary, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, ), - overflow: TextOverflow.ellipsis, - maxLines: 1, + const SizedBox(height: 2), + if (!process.status.isRunning) + Text( + _getUploadMessage(context), + style: AppTextStyles.body2.copyWith( + color: context.colorScheme.textSecondary, + ), + ), + if (process.status.isRunning) ...[ + LinearProgressIndicator( + value: process.progress, + backgroundColor: context.colorScheme.outline, + borderRadius: BorderRadius.circular(4), + valueColor: AlwaysStoppedAnimation( + context.colorScheme.primary, + ), + ), + const SizedBox(height: 2), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${process.chunk.formatBytes} - ${process.progressPercentage.toStringAsFixed(0)}%', + style: AppTextStyles.body2.copyWith( + color: context.colorScheme.textSecondary, + ), + ), + Text( + process.total.formatBytes, + style: AppTextStyles.body2.copyWith( + color: context.colorScheme.textSecondary, + ), + ), + ], + ), + ], + ], + ), + ), + if (process.status.isRunning || process.status.isWaiting) + ActionButton( + onPressed: onCancelTap, + icon: Icon( + CupertinoIcons.xmark, + color: context.colorScheme.textPrimary, + size: 20, + ), + ), + if (process.status.isTerminated || + process.status.isFailed || + process.status.isCompleted) + ActionButton( + onPressed: onRemoveTap, + icon: Icon( + CupertinoIcons.trash, + color: context.colorScheme.textSecondary, + size: 20, ), - const SizedBox(height: 8), - if (process.status.isWaiting) + ), + ], + ), + ); + } + + String _getUploadMessage(BuildContext context) { + if (process.status.isWaiting) { + return context.l10n.upload_status_waiting; + } else if (process.status.isFailed) { + return context.l10n.upload_status_failed; + } else if (process.status.isCompleted) { + return context.l10n.upload_status_success; + } else if (process.status.isTerminated) { + return context.l10n.upload_status_cancelled; + } + return ''; + } +} + +class DownloadProcessItem extends StatelessWidget { + final DownloadMediaProcess process; + final void Function() onCancelTap; + final void Function() onRemoveTap; + + const DownloadProcessItem({ + super.key, + required this.process, + required this.onCancelTap, + required this.onRemoveTap, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(left: 16, right: 8), + child: Row( + children: [ + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ Text( - context.l10n.waiting_in_queue_text, - style: AppTextStyles.body2.copyWith( - color: context.colorScheme.textSecondary, + process.name, + style: AppTextStyles.body.copyWith( + color: context.colorScheme.textPrimary, ), + overflow: TextOverflow.ellipsis, + maxLines: 1, ), - if (process.progress != null && process.status.isProcessing) ...[ - LinearProgressIndicator( - value: process.progress?.percentageInPoint, - backgroundColor: context.colorScheme.outline, - borderRadius: BorderRadius.circular(4), - valueColor: AlwaysStoppedAnimation( - context.colorScheme.primary, + const SizedBox(height: 2), + if (!process.status.isRunning) + Text( + _getMessage(context), + style: AppTextStyles.body2.copyWith( + color: context.colorScheme.textSecondary, + ), ), - ), - const SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '${process.progress?.chunk.formatBytes} ${process.progress?.percentage.toStringAsFixed(0)}%', - style: AppTextStyles.body2.copyWith( - color: context.colorScheme.textSecondary, - ), + if (process.status.isRunning) ...[ + LinearProgressIndicator( + value: process.progress, + backgroundColor: context.colorScheme.outline, + borderRadius: BorderRadius.circular(4), + valueColor: AlwaysStoppedAnimation( + context.colorScheme.primary, ), - Text( - '${process.progress?.total.formatBytes}', - style: AppTextStyles.body2.copyWith( - color: context.colorScheme.textSecondary, + ), + const SizedBox(height: 2), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${process.chunk.formatBytes} ${process.progressPercentage.toStringAsFixed(0)}%', + style: AppTextStyles.body2.copyWith( + color: context.colorScheme.textSecondary, + ), ), - ), - ], - ), + Text( + process.total.formatBytes, + style: AppTextStyles.body2.copyWith( + color: context.colorScheme.textSecondary, + ), + ), + ], + ), + ], ], - ], + ), ), - ), - ActionButton( - onPressed: onCancelTap, - icon: const Icon(CupertinoIcons.xmark), - ), - ], + if (process.status.isRunning || process.status.isWaiting) + ActionButton( + onPressed: onCancelTap, + icon: Icon( + CupertinoIcons.xmark, + color: context.colorScheme.textPrimary, + size: 20, + ), + ), + if (process.status.isTerminated || + process.status.isFailed || + process.status.isCompleted) + ActionButton( + onPressed: onRemoveTap, + icon: Icon( + CupertinoIcons.trash, + color: context.colorScheme.textPrimary, + size: 20, + ), + ), + ], + ), ); } + + String _getMessage(BuildContext context) { + if (process.status.isWaiting) { + return context.l10n.download_status_waiting; + } else if (process.status.isFailed) { + return context.l10n.download_status_failed; + } else if (process.status.isCompleted) { + return context.l10n.download_status_success; + } else if (process.status.isTerminated) { + return context.l10n.download_status_cancelled; + } + return ''; + } } diff --git a/app/lib/ui/flow/media_transfer/media_transfer_screen.dart b/app/lib/ui/flow/media_transfer/media_transfer_screen.dart index 413c153..6a95216 100644 --- a/app/lib/ui/flow/media_transfer/media_transfer_screen.dart +++ b/app/lib/ui/flow/media_transfer/media_transfer_screen.dart @@ -94,11 +94,12 @@ class _MediaTransferScreenState extends ConsumerState { Widget _uploadList() { return Consumer( builder: (context, ref, child) { - final upload = ref.watch( - mediaTransferStateNotifierProvider.select((value) => value.upload), + final uploadProcesses = ref.watch( + mediaTransferStateNotifierProvider + .select((value) => value.uploadProcesses), ); - if (upload.isEmpty) { + if (uploadProcesses.isEmpty) { return ErrorView( title: context.l10n.empty_upload_title, message: context.l10n.empty_upload_message, @@ -111,16 +112,19 @@ class _MediaTransferScreenState extends ConsumerState { } return ListView.separated( - padding: const EdgeInsets.all(16), - itemCount: upload.length, + padding: const EdgeInsets.symmetric(vertical: 16), + itemCount: uploadProcesses.length, separatorBuilder: (context, index) => const SizedBox( - height: 8, + height: 16, ), - itemBuilder: (context, index) => ProcessItem( - key: ValueKey(upload[index].id), - process: upload[index], + itemBuilder: (context, index) => UploadProcessItem( + key: ValueKey(uploadProcesses[index].id), + process: uploadProcesses[index], + onRemoveTap: () { + notifier.onRemoveUploadProcess(uploadProcesses[index].id); + }, onCancelTap: () { - notifier.onTerminateUploadProcess(upload[index].id); + notifier.onTerminateUploadProcess(uploadProcesses[index].id); }, ), ); @@ -131,11 +135,12 @@ class _MediaTransferScreenState extends ConsumerState { Widget _downloadList() { return Consumer( builder: (context, ref, child) { - final download = ref.watch( - mediaTransferStateNotifierProvider.select((value) => value.download), + final downloadProcesses = ref.watch( + mediaTransferStateNotifierProvider + .select((value) => value.downloadProcesses), ); - if (download.isEmpty) { + if (downloadProcesses.isEmpty) { return ErrorView( title: context.l10n.empty_download_title, message: context.l10n.empty_download_message, @@ -148,16 +153,19 @@ class _MediaTransferScreenState extends ConsumerState { } return ListView.separated( - padding: const EdgeInsets.all(16), - itemCount: download.length, + padding: const EdgeInsets.symmetric(vertical: 16), + itemCount: downloadProcesses.length, separatorBuilder: (context, index) => const SizedBox( - height: 8, + height: 16, ), - itemBuilder: (context, index) => ProcessItem( - key: ValueKey(download[index].id), - process: download[index], + itemBuilder: (context, index) => DownloadProcessItem( + key: ValueKey(downloadProcesses[index].id), + process: downloadProcesses[index], onCancelTap: () { - notifier.onTerminateDownloadProcess(download[index].id); + notifier.onTerminateDownloadProcess(downloadProcesses[index].id); + }, + onRemoveTap: () { + notifier.onRemoveDownloadProcess(downloadProcesses[index].id); }, ), ); diff --git a/app/lib/ui/flow/media_transfer/media_transfer_view_model.dart b/app/lib/ui/flow/media_transfer/media_transfer_view_model.dart index 082e8b5..64625e8 100644 --- a/app/lib/ui/flow/media_transfer/media_transfer_view_model.dart +++ b/app/lib/ui/flow/media_transfer/media_transfer_view_model.dart @@ -1,5 +1,5 @@ -import 'package:data/models/app_process/app_process.dart'; -import 'package:data/repositories/google_drive_process_repo.dart'; +import 'package:data/models/media_process/media_process.dart'; +import 'package:data/repositories/media_process_repository.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -7,21 +7,22 @@ part 'media_transfer_view_model.freezed.dart'; final mediaTransferStateNotifierProvider = StateNotifierProvider.autoDispose< MediaTransferStateNotifier, MediaTransferState>( - (ref) => MediaTransferStateNotifier(ref.read(googleDriveProcessRepoProvider)), + (ref) => MediaTransferStateNotifier(ref.read(mediaProcessRepoProvider)), ); class MediaTransferStateNotifier extends StateNotifier { - final GoogleDriveProcessRepo _googleDriveProcessRepo; + final MediaProcessRepo _mediaProcessRepo; - MediaTransferStateNotifier(this._googleDriveProcessRepo) + MediaTransferStateNotifier(this._mediaProcessRepo) : super(const MediaTransferState()) { - _googleDriveProcessRepo.addListener(_listenGoogleDriveProcess); + _listenMediaProcess(); + _mediaProcessRepo.addListener(_listenMediaProcess); } - void _listenGoogleDriveProcess() { + void _listenMediaProcess() { state = state.copyWith( - download: _googleDriveProcessRepo.downloadQueue.toList(), - upload: _googleDriveProcessRepo.uploadQueue.toList(), + downloadProcesses: _mediaProcessRepo.downloadQueue.toList(), + uploadProcesses: _mediaProcessRepo.uploadQueue.toList(), ); } @@ -30,16 +31,24 @@ class MediaTransferStateNotifier extends StateNotifier { } void onTerminateUploadProcess(String id) { - _googleDriveProcessRepo.terminateUploadProcess(id); + _mediaProcessRepo.terminateUploadProcess(id); + } + + void onRemoveUploadProcess(String id) { + _mediaProcessRepo.removeItemFromUploadQueue(id); + } + + void onRemoveDownloadProcess(String id) { + _mediaProcessRepo.removeItemFromDownloadQueue(id); } void onTerminateDownloadProcess(String id) { - _googleDriveProcessRepo.terminateDownloadProcess(id); + _mediaProcessRepo.terminateDownloadProcess(id); } @override void dispose() { - _googleDriveProcessRepo.removeListener(_listenGoogleDriveProcess); + _mediaProcessRepo.removeListener(_listenMediaProcess); super.dispose(); } } @@ -48,8 +57,8 @@ class MediaTransferStateNotifier extends StateNotifier { class MediaTransferState with _$MediaTransferState { const factory MediaTransferState({ Object? error, - @Default([]) List upload, - @Default([]) List download, + @Default([]) List uploadProcesses, + @Default([]) List downloadProcesses, @Default(0) int page, }) = _MediaTransferState; } diff --git a/app/lib/ui/flow/media_transfer/media_transfer_view_model.freezed.dart b/app/lib/ui/flow/media_transfer/media_transfer_view_model.freezed.dart index 5bc1b83..95e7919 100644 --- a/app/lib/ui/flow/media_transfer/media_transfer_view_model.freezed.dart +++ b/app/lib/ui/flow/media_transfer/media_transfer_view_model.freezed.dart @@ -17,8 +17,10 @@ final _privateConstructorUsedError = UnsupportedError( /// @nodoc mixin _$MediaTransferState { Object? get error => throw _privateConstructorUsedError; - List get upload => throw _privateConstructorUsedError; - List get download => throw _privateConstructorUsedError; + List get uploadProcesses => + throw _privateConstructorUsedError; + List get downloadProcesses => + throw _privateConstructorUsedError; int get page => throw _privateConstructorUsedError; /// Create a copy of MediaTransferState @@ -36,8 +38,8 @@ abstract class $MediaTransferStateCopyWith<$Res> { @useResult $Res call( {Object? error, - List upload, - List download, + List uploadProcesses, + List downloadProcesses, int page}); } @@ -57,20 +59,20 @@ class _$MediaTransferStateCopyWithImpl<$Res, $Val extends MediaTransferState> @override $Res call({ Object? error = freezed, - Object? upload = null, - Object? download = null, + Object? uploadProcesses = null, + Object? downloadProcesses = null, Object? page = null, }) { return _then(_value.copyWith( error: freezed == error ? _value.error : error, - upload: null == upload - ? _value.upload - : upload // ignore: cast_nullable_to_non_nullable - as List, - download: null == download - ? _value.download - : download // ignore: cast_nullable_to_non_nullable - as List, + uploadProcesses: null == uploadProcesses + ? _value.uploadProcesses + : uploadProcesses // ignore: cast_nullable_to_non_nullable + as List, + downloadProcesses: null == downloadProcesses + ? _value.downloadProcesses + : downloadProcesses // ignore: cast_nullable_to_non_nullable + as List, page: null == page ? _value.page : page // ignore: cast_nullable_to_non_nullable @@ -89,8 +91,8 @@ abstract class _$$MediaTransferStateImplCopyWith<$Res> @useResult $Res call( {Object? error, - List upload, - List download, + List uploadProcesses, + List downloadProcesses, int page}); } @@ -108,20 +110,20 @@ class __$$MediaTransferStateImplCopyWithImpl<$Res> @override $Res call({ Object? error = freezed, - Object? upload = null, - Object? download = null, + Object? uploadProcesses = null, + Object? downloadProcesses = null, Object? page = null, }) { return _then(_$MediaTransferStateImpl( error: freezed == error ? _value.error : error, - upload: null == upload - ? _value._upload - : upload // ignore: cast_nullable_to_non_nullable - as List, - download: null == download - ? _value._download - : download // ignore: cast_nullable_to_non_nullable - as List, + uploadProcesses: null == uploadProcesses + ? _value._uploadProcesses + : uploadProcesses // ignore: cast_nullable_to_non_nullable + as List, + downloadProcesses: null == downloadProcesses + ? _value._downloadProcesses + : downloadProcesses // ignore: cast_nullable_to_non_nullable + as List, page: null == page ? _value.page : page // ignore: cast_nullable_to_non_nullable @@ -135,30 +137,31 @@ class __$$MediaTransferStateImplCopyWithImpl<$Res> class _$MediaTransferStateImpl implements _MediaTransferState { const _$MediaTransferStateImpl( {this.error, - final List upload = const [], - final List download = const [], + final List uploadProcesses = const [], + final List downloadProcesses = const [], this.page = 0}) - : _upload = upload, - _download = download; + : _uploadProcesses = uploadProcesses, + _downloadProcesses = downloadProcesses; @override final Object? error; - final List _upload; + final List _uploadProcesses; @override @JsonKey() - List get upload { - if (_upload is EqualUnmodifiableListView) return _upload; + List get uploadProcesses { + if (_uploadProcesses is EqualUnmodifiableListView) return _uploadProcesses; // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_upload); + return EqualUnmodifiableListView(_uploadProcesses); } - final List _download; + final List _downloadProcesses; @override @JsonKey() - List get download { - if (_download is EqualUnmodifiableListView) return _download; + List get downloadProcesses { + if (_downloadProcesses is EqualUnmodifiableListView) + return _downloadProcesses; // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_download); + return EqualUnmodifiableListView(_downloadProcesses); } @override @@ -167,7 +170,7 @@ class _$MediaTransferStateImpl implements _MediaTransferState { @override String toString() { - return 'MediaTransferState(error: $error, upload: $upload, download: $download, page: $page)'; + return 'MediaTransferState(error: $error, uploadProcesses: $uploadProcesses, downloadProcesses: $downloadProcesses, page: $page)'; } @override @@ -176,8 +179,10 @@ class _$MediaTransferStateImpl implements _MediaTransferState { (other.runtimeType == runtimeType && other is _$MediaTransferStateImpl && const DeepCollectionEquality().equals(other.error, error) && - const DeepCollectionEquality().equals(other._upload, _upload) && - const DeepCollectionEquality().equals(other._download, _download) && + const DeepCollectionEquality() + .equals(other._uploadProcesses, _uploadProcesses) && + const DeepCollectionEquality() + .equals(other._downloadProcesses, _downloadProcesses) && (identical(other.page, page) || other.page == page)); } @@ -185,8 +190,8 @@ class _$MediaTransferStateImpl implements _MediaTransferState { int get hashCode => Object.hash( runtimeType, const DeepCollectionEquality().hash(error), - const DeepCollectionEquality().hash(_upload), - const DeepCollectionEquality().hash(_download), + const DeepCollectionEquality().hash(_uploadProcesses), + const DeepCollectionEquality().hash(_downloadProcesses), page); /// Create a copy of MediaTransferState @@ -202,16 +207,16 @@ class _$MediaTransferStateImpl implements _MediaTransferState { abstract class _MediaTransferState implements MediaTransferState { const factory _MediaTransferState( {final Object? error, - final List upload, - final List download, + final List uploadProcesses, + final List downloadProcesses, final int page}) = _$MediaTransferStateImpl; @override Object? get error; @override - List get upload; + List get uploadProcesses; @override - List get download; + List get downloadProcesses; @override int get page; diff --git a/app/lib/ui/flow/onboard/onboard_screen.dart b/app/lib/ui/flow/onboard/onboard_screen.dart index a32ae5d..78611e8 100644 --- a/app/lib/ui/flow/onboard/onboard_screen.dart +++ b/app/lib/ui/flow/onboard/onboard_screen.dart @@ -1,5 +1,6 @@ import '../../../components/app_page.dart'; import '../../../domain/extensions/context_extensions.dart'; +import '../../../gen/assets.gen.dart'; import '../../navigation/app_route.dart'; import 'package:data/storage/app_preferences.dart'; import 'package:flutter/material.dart'; @@ -7,7 +8,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:style/extensions/context_extensions.dart'; import 'package:style/text/app_text_style.dart'; import 'package:style/animations/on_tap_scale.dart'; -import '../../../domain/assets/assets_paths.dart'; class OnBoardScreen extends ConsumerWidget { const OnBoardScreen({super.key}); @@ -58,7 +58,7 @@ class OnBoardScreen extends ConsumerWidget { } Widget _appLogo(BuildContext context) => Image.asset( - Assets.images.appIcon, + Assets.images.appLogo.path, width: 250, ); diff --git a/app/pubspec.lock b/app/pubspec.lock index ad28987..3268a4b 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _discoveryapis_commons - sha256: f8bb1fdbd77f3d5c1d62b5b0eca75fbf1e41bf4f6c62628f880582e2182ae45d + sha256: "113c4100b90a5b70a983541782431b82168b3cae166ab130649c36eb3559d498" url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "1.0.7" _fe_analyzer_shared: dependency: transitive description: @@ -74,10 +74,10 @@ packages: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.6.0" async: dependency: transitive description: @@ -114,10 +114,10 @@ packages: dependency: transitive description: name: build_daemon - sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "4.0.2" build_resolvers: dependency: transitive description: @@ -138,10 +138,10 @@ packages: dependency: transitive description: name: build_runner_core - sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 url: "https://pub.dev" source: hosted - version: "7.3.0" + version: "7.3.2" built_collection: dependency: transitive description: @@ -154,10 +154,10 @@ packages: dependency: transitive description: name: built_value - sha256: fedde275e0a6b798c3296963c5cd224e3e1b55d0e478d5b7e65e6b540f363a0e + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb url: "https://pub.dev" source: hosted - version: "8.9.1" + version: "8.9.2" cached_network_image: dependency: "direct main" description: @@ -210,10 +210,10 @@ packages: dependency: transitive description: name: cli_util - sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c url: "https://pub.dev" source: hosted - version: "0.4.1" + version: "0.4.2" clock: dependency: transitive description: @@ -226,10 +226,10 @@ packages: dependency: transitive description: name: code_builder - sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" url: "https://pub.dev" source: hosted - version: "4.10.0" + version: "4.10.1" collection: dependency: "direct main" description: @@ -238,14 +238,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + color: + dependency: transitive + description: + name: color + sha256: ddcdf1b3badd7008233f5acffaf20ca9f5dc2cd0172b75f68f24526a5f5725cb + url: "https://pub.dev" + source: hosted + version: "3.0.0" convert: dependency: transitive description: name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" cross_file: dependency: transitive description: @@ -258,18 +266,18 @@ packages: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.6" csslib: dependency: transitive description: name: csslib - sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.2" cupertino_icons: dependency: "direct main" description: @@ -314,10 +322,18 @@ packages: dependency: transitive description: name: dart_style - sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" + url: "https://pub.dev" + source: hosted + version: "2.3.7" + dartx: + dependency: transitive + description: + name: dartx + sha256: "8b25435617027257d43e6508b5fe061012880ddfdaa75a71d607c3de2a13d244" url: "https://pub.dev" source: hosted - version: "2.3.6" + version: "1.2.0" data: dependency: "direct main" description: @@ -369,18 +385,18 @@ packages: dependency: transitive description: name: ffi - sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" file: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" firebase_core: dependency: "direct main" description: @@ -409,10 +425,10 @@ packages: dependency: transitive description: name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -434,6 +450,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.0" + flutter_gen_core: + dependency: transitive + description: + name: flutter_gen_core + sha256: "46ecf0e317413dd065547887c43f93f55e9653e83eb98dc13dd07d40dd225325" + url: "https://pub.dev" + source: hosted + version: "5.8.0" + flutter_gen_runner: + dependency: "direct dev" + description: + name: flutter_gen_runner + sha256: "77f0a02fc30d9fcf2549fe874eb3fde091435724904bcbb1af60aa40cbfab1f4" + url: "https://pub.dev" + source: hosted + version: "5.8.0" flutter_lints: dependency: "direct dev" description: @@ -525,10 +557,10 @@ packages: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" glob: dependency: transitive description: @@ -557,10 +589,10 @@ packages: dependency: transitive description: name: google_identity_services_web - sha256: bf215f340648e78697fdac486d75fca67f85944502e579bdcecd029babc6f4d8 + sha256: "55580f436822d64c8ff9a77e37d61f5fb1e6c7ec9d632a43ee324e2a05c3c6c9" url: "https://pub.dev" source: hosted - version: "0.3.2" + version: "0.3.3" google_sign_in: dependency: "direct main" description: @@ -573,18 +605,18 @@ packages: dependency: transitive description: name: google_sign_in_android - sha256: bfd42c81c30c6faba16e0f62968d5505a87504aaa672b3155ee931461abb0a49 + sha256: "0928059d2f0840f63c7b07a30cf73b593ae872cdd0dbd46d1b9ba878d2599c01" url: "https://pub.dev" source: hosted - version: "6.1.21" + version: "6.1.33" google_sign_in_ios: dependency: transitive description: name: google_sign_in_ios - sha256: a7d653803468d30b82ceb47ea00fe86d23c56e63eb2e5c2248bb68e9df203217 + sha256: "83f015169102df1ab2905cf8abd8934e28f87db9ace7a5fa676998842fed228a" url: "https://pub.dev" source: hosted - version: "5.7.4" + version: "5.7.8" google_sign_in_platform_interface: dependency: transitive description: @@ -613,18 +645,18 @@ packages: dependency: transitive description: name: googleapis_auth - sha256: cafc46446574fd42826aa4cd4d623c94482598fda0a5a5649bf2781bcbc09258 + sha256: befd71383a955535060acde8792e7efc11d2fccd03dd1d3ec434e85b68775938 url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.6.0" graphs: dependency: transitive description: name: graphs - sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" gtk: dependency: transitive description: @@ -633,6 +665,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + hashcodes: + dependency: transitive + description: + name: hashcodes + sha256: "80f9410a5b3c8e110c4b7604546034749259f5d6dcca63e0d3c17c9258f1a651" + url: "https://pub.dev" + source: hosted + version: "2.0.0" hotreloader: dependency: transitive description: @@ -645,10 +685,10 @@ packages: dependency: transitive description: name: html - sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" + sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec" url: "https://pub.dev" source: hosted - version: "0.15.4" + version: "0.15.5" http: dependency: transitive description: @@ -673,6 +713,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + image_size_getter: + dependency: transitive + description: + name: image_size_getter + sha256: "0511799498340b70993d2dfb34b55a2247b5b801d75a6cdd4543acfcafdb12b0" + url: "https://pub.dev" + source: hosted + version: "2.2.0" intl: dependency: "direct main" description: @@ -693,10 +741,10 @@ packages: dependency: transitive description: name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf url: "https://pub.dev" source: hosted - version: "0.6.7" + version: "0.7.1" json_annotation: dependency: transitive description: @@ -709,10 +757,10 @@ packages: dependency: "direct main" description: name: json_serializable - sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b + sha256: c2fcb3920cf2b6ae6845954186420fca40bc0a8abcc84903b7801f17d7050d7c url: "https://pub.dev" source: hosted - version: "6.8.0" + version: "6.9.0" leak_tracker: dependency: transitive description: @@ -745,14 +793,22 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + logger: + dependency: "direct main" + description: + name: logger + sha256: be4b23575aac7ebf01f225a241eb7f6b5641eeaf43c6a8613510fc2f8cf187d1 + url: "https://pub.dev" + source: hosted + version: "2.5.0" logging: dependency: transitive description: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" macros: dependency: transitive description: @@ -789,10 +845,10 @@ packages: dependency: transitive description: name: mime - sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "2.0.0" octo_image: dependency: transitive description: @@ -837,10 +893,10 @@ packages: dependency: transitive description: name: path_parsing - sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.0" path_provider: dependency: "direct main" description: @@ -861,10 +917,10 @@ packages: dependency: transitive description: name: path_provider_foundation - sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.0" path_provider_linux: dependency: transitive description: @@ -885,10 +941,10 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" permission_handler: dependency: "direct main" description: @@ -901,10 +957,10 @@ packages: dependency: transitive description: name: permission_handler_android - sha256: "1acac6bae58144b442f11e66621c062aead9c99841093c38f5bcdcc24c1c3474" + sha256: "71bbecfee799e65aff7c744761a57e817e73b738fedf62ab7afd5593da21f9f1" url: "https://pub.dev" source: hosted - version: "12.0.5" + version: "12.0.13" permission_handler_apple: dependency: transitive description: @@ -917,18 +973,18 @@ packages: dependency: transitive description: name: permission_handler_html - sha256: "54bf176b90f6eddd4ece307e2c06cf977fb3973719c35a93b85cc7093eb6070d" + sha256: "6b9cb54b7135073841a35513fba39e598b421702d5f4d92319992fd6eb5532a9" url: "https://pub.dev" source: hosted - version: "0.1.1" + version: "0.1.3+4" permission_handler_platform_interface: dependency: transitive description: name: permission_handler_platform_interface - sha256: "23dfba8447c076ab5be3dee9ceb66aad345c4a648f0cac292c77b1eb0e800b78" + sha256: e9c8eadee926c4532d0305dff94b85bf961f16759c3af791486613152af4b4f9 url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "4.2.3" permission_handler_windows: dependency: transitive description: @@ -965,10 +1021,10 @@ packages: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -997,10 +1053,10 @@ packages: dependency: transitive description: name: pubspec_parse - sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 url: "https://pub.dev" source: hosted - version: "1.2.3" + version: "1.3.0" riverpod: dependency: transitive description: @@ -1013,18 +1069,18 @@ packages: dependency: transitive description: name: riverpod_analyzer_utils - sha256: dc53a659cb543b203cdc35cd4e942ed08ea893eb6ef12029301323bdf18c5d95 + sha256: c6b8222b2b483cb87ae77ad147d6408f400c64f060df7a225b127f4afef4f8c8 url: "https://pub.dev" source: hosted - version: "0.5.7" + version: "0.5.8" riverpod_lint: dependency: "direct dev" description: name: riverpod_lint - sha256: "326efc199b87f21053b9a2afbf2aea26c41b3bf6f8ba346ce69126ee17d16ebd" + sha256: "83e4caa337a9840469b7b9bd8c2351ce85abad80f570d84146911b32086fbd99" url: "https://pub.dev" source: hosted - version: "2.6.2" + version: "2.6.3" rxdart: dependency: transitive description: @@ -1117,10 +1173,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.0.0" sky_engine: dependency: transitive description: flutter @@ -1249,10 +1305,10 @@ packages: dependency: transitive description: name: synchronized - sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" + sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" url: "https://pub.dev" source: hosted - version: "3.1.0+1" + version: "3.3.0+3" term_glyph: dependency: transitive description: @@ -1269,14 +1325,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.2" + time: + dependency: transitive + description: + name: time + sha256: "370572cf5d1e58adcb3e354c47515da3f7469dac3a95b447117e728e7be6f461" + url: "https://pub.dev" + source: hosted + version: "2.1.5" timezone: dependency: transitive description: name: timezone - sha256: "1cfd8ddc2d1cfd836bc93e67b9be88c3adaeca6f40a00ca999104c30693cdca0" + sha256: ffc9d5f4d1193534ef051f9254063fa53d588609418c84299956c3db9383587d url: "https://pub.dev" source: hosted - version: "0.9.2" + version: "0.10.0" timing: dependency: transitive description: @@ -1289,50 +1353,50 @@ packages: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" url_launcher: dependency: transitive description: name: url_launcher - sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e" + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" url: "https://pub.dev" source: hosted - version: "6.2.6" + version: "6.3.1" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "360a6ed2027f18b73c8d98e159dda67a61b7f2e0f6ec26e86c3ada33b0621775" + sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193" url: "https://pub.dev" source: hosted - version: "6.3.1" + version: "6.3.14" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "9149d493b075ed740901f3ee844a38a00b33116c7c5c10d7fb27df8987fb51d5" + sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e url: "https://pub.dev" source: hosted - version: "6.2.5" + version: "6.3.1" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.2.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 + sha256: "769549c999acdb42b8bcfa7c43d72bf79a382ca7441ab18a808e101149daf672" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.2.1" url_launcher_platform_interface: dependency: transitive description: @@ -1377,10 +1441,10 @@ packages: dependency: transitive description: name: vector_graphics_codec - sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da + sha256: "2430b973a4ca3c4dbc9999b62b8c719a160100dcbae5c819bae0cacce32c9cdb" url: "https://pub.dev" source: hosted - version: "1.1.11+1" + version: "1.1.12" vector_graphics_compiler: dependency: transitive description: @@ -1409,34 +1473,34 @@ packages: dependency: transitive description: name: video_player_android - sha256: "4dd9b8b86d70d65eecf3dcabfcdfbb9c9115d244d022654aba49a00336d540c2" + sha256: "391e092ba4abe2f93b3e625bd6b6a6ec7d7414279462c1c0ee42b5ab8d0a0898" url: "https://pub.dev" source: hosted - version: "2.4.12" + version: "2.7.16" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - sha256: "309e3962795e761be010869bae65c0b0e45b5230c5cee1bec72197ca7db040ed" + sha256: cd5ab8a8bc0eab65ab0cea40304097edc46da574c8c1ecdee96f28cd8ef3792f url: "https://pub.dev" source: hosted - version: "2.5.6" + version: "2.6.2" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - sha256: "236454725fafcacf98f0f39af0d7c7ab2ce84762e3b63f2cbb3ef9a7e0550bc6" + sha256: "229d7642ccd9f3dc4aba169609dd6b5f3f443bb4cc15b82f7785fcada5af9bbb" url: "https://pub.dev" source: hosted - version: "6.2.2" + version: "6.2.3" video_player_web: dependency: transitive description: name: video_player_web - sha256: "8e9cb7fe94e49490e67bbc15149691792b58a0ade31b32e3f3688d104a0e057b" + sha256: "881b375a934d8ebf868c7fb1423b2bfaa393a0a265fa3f733079a86536064a10" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.3" visibility_detector: dependency: "direct main" description: @@ -1469,14 +1533,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.dev" + source: hosted + version: "0.1.6" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "3.0.1" webview_flutter: dependency: "direct main" description: @@ -1489,10 +1561,10 @@ packages: dependency: transitive description: name: webview_flutter_android - sha256: "86c2d01c37c4578ee46560109cf2e18fb271f0d080a796f09188d0952352e057" + sha256: "285cedfd9441267f6cca8843458620b5fda1af75b04f5818d0441acda5d7df19" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.0" webview_flutter_platform_interface: dependency: transitive description: @@ -1513,18 +1585,18 @@ packages: dependency: transitive description: name: win32 - sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a" + sha256: "84ba388638ed7a8cb3445a320c8273136ab2631cd5f2c57888335504ddab1bc2" url: "https://pub.dev" source: hosted - version: "5.5.4" + version: "5.8.0" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" xml: dependency: transitive description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index d8a3583..5a8d034 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -65,6 +65,9 @@ dependencies: freezed_annotation: ^2.4.4 json_serializable: ^6.8.0 + # logging + logger: ^2.5.0 + dev_dependencies: flutter_test: sdk: flutter @@ -76,6 +79,7 @@ dev_dependencies: # code generation build_runner: ^2.4.13 go_router_builder: ^2.7.1 + flutter_gen_runner: ^5.8.0 flutter: uses-material-design: true diff --git a/data/.flutter-plugins b/data/.flutter-plugins index 2d28e59..9ddd561 100644 --- a/data/.flutter-plugins +++ b/data/.flutter-plugins @@ -1,4 +1,6 @@ # This is a generated file; do not edit or check into version control. +flutter_local_notifications=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/flutter_local_notifications-18.0.1/ +flutter_local_notifications_linux=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/flutter_local_notifications_linux-5.0.0/ google_sign_in=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in-6.2.2/ google_sign_in_android=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_android-6.1.33/ google_sign_in_ios=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_ios-5.7.8/ @@ -16,6 +18,9 @@ shared_preferences_foundation=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/sha shared_preferences_linux=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.1/ shared_preferences_web=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_web-2.4.2/ shared_preferences_windows=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.1/ +sqflite=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/sqflite-2.4.1/ +sqflite_android=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/sqflite_android-2.4.0/ +sqflite_darwin=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1/ url_launcher=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher-6.3.1/ url_launcher_android=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.14/ url_launcher_ios=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.1/ diff --git a/data/.flutter-plugins-dependencies b/data/.flutter-plugins-dependencies index d7885b4..cd50671 100644 --- a/data/.flutter-plugins-dependencies +++ b/data/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"google_sign_in_ios","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_ios-5.7.8/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.6.2/","native_build":true,"dependencies":[]},{"name":"shared_preferences_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.3/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"url_launcher_ios","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.1/","native_build":true,"dependencies":[]}],"android":[{"name":"google_sign_in_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_android-6.1.33/","native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","native_build":true,"dependencies":[]},{"name":"path_provider_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_android-2.2.12/","native_build":true,"dependencies":[]},{"name":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.6.2/","native_build":true,"dependencies":[]},{"name":"shared_preferences_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_android-2.3.3/","native_build":true,"dependencies":[]},{"name":"url_launcher_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.14/","native_build":true,"dependencies":[]}],"macos":[{"name":"google_sign_in_ios","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_ios-5.7.8/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.6.2/","native_build":true,"dependencies":[]},{"name":"shared_preferences_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.3/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"url_launcher_macos","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_macos-3.2.1/","native_build":true,"dependencies":[]}],"linux":[{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","native_build":false,"dependencies":[]},{"name":"path_provider_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[]},{"name":"shared_preferences_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.1/","native_build":false,"dependencies":["path_provider_linux"]},{"name":"url_launcher_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.1/","native_build":true,"dependencies":[]}],"windows":[{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","native_build":false,"dependencies":[]},{"name":"path_provider_windows","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[]},{"name":"shared_preferences_windows","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.1/","native_build":false,"dependencies":["path_provider_windows"]},{"name":"url_launcher_windows","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_windows-3.1.3/","native_build":true,"dependencies":[]}],"web":[{"name":"google_sign_in_web","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_web-0.12.4+3/","dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","dependencies":[]},{"name":"shared_preferences_web","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_web-2.4.2/","dependencies":[]},{"name":"url_launcher_web","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_web-2.3.3/","dependencies":[]}]},"dependencyGraph":[{"name":"google_sign_in","dependencies":["google_sign_in_android","google_sign_in_ios","google_sign_in_web"]},{"name":"google_sign_in_android","dependencies":[]},{"name":"google_sign_in_ios","dependencies":[]},{"name":"google_sign_in_web","dependencies":[]},{"name":"package_info_plus","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"photo_manager","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]},{"name":"url_launcher","dependencies":["url_launcher_android","url_launcher_ios","url_launcher_linux","url_launcher_macos","url_launcher_web","url_launcher_windows"]},{"name":"url_launcher_android","dependencies":[]},{"name":"url_launcher_ios","dependencies":[]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2024-11-19 10:29:04.046577","version":"3.24.4","swift_package_manager_enabled":false} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"flutter_local_notifications","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/flutter_local_notifications-18.0.1/","native_build":true,"dependencies":[]},{"name":"google_sign_in_ios","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_ios-5.7.8/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.6.2/","native_build":true,"dependencies":[]},{"name":"shared_preferences_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.3/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"sqflite_darwin","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"url_launcher_ios","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.1/","native_build":true,"dependencies":[]}],"android":[{"name":"flutter_local_notifications","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/flutter_local_notifications-18.0.1/","native_build":true,"dependencies":[]},{"name":"google_sign_in_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_android-6.1.33/","native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","native_build":true,"dependencies":[]},{"name":"path_provider_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_android-2.2.12/","native_build":true,"dependencies":[]},{"name":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.6.2/","native_build":true,"dependencies":[]},{"name":"shared_preferences_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_android-2.3.3/","native_build":true,"dependencies":[]},{"name":"sqflite_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/sqflite_android-2.4.0/","native_build":true,"dependencies":[]},{"name":"url_launcher_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.14/","native_build":true,"dependencies":[]}],"macos":[{"name":"flutter_local_notifications","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/flutter_local_notifications-18.0.1/","native_build":true,"dependencies":[]},{"name":"google_sign_in_ios","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_ios-5.7.8/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.6.2/","native_build":true,"dependencies":[]},{"name":"shared_preferences_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.3/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"sqflite_darwin","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"url_launcher_macos","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_macos-3.2.1/","native_build":true,"dependencies":[]}],"linux":[{"name":"flutter_local_notifications_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/flutter_local_notifications_linux-5.0.0/","native_build":false,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","native_build":false,"dependencies":[]},{"name":"path_provider_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[]},{"name":"shared_preferences_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.1/","native_build":false,"dependencies":["path_provider_linux"]},{"name":"url_launcher_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.1/","native_build":true,"dependencies":[]}],"windows":[{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","native_build":false,"dependencies":[]},{"name":"path_provider_windows","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[]},{"name":"shared_preferences_windows","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.1/","native_build":false,"dependencies":["path_provider_windows"]},{"name":"url_launcher_windows","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_windows-3.1.3/","native_build":true,"dependencies":[]}],"web":[{"name":"google_sign_in_web","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_web-0.12.4+3/","dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-8.1.1/","dependencies":[]},{"name":"shared_preferences_web","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_web-2.4.2/","dependencies":[]},{"name":"url_launcher_web","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/url_launcher_web-2.3.3/","dependencies":[]}]},"dependencyGraph":[{"name":"flutter_local_notifications","dependencies":["flutter_local_notifications_linux"]},{"name":"flutter_local_notifications_linux","dependencies":[]},{"name":"google_sign_in","dependencies":["google_sign_in_android","google_sign_in_ios","google_sign_in_web"]},{"name":"google_sign_in_android","dependencies":[]},{"name":"google_sign_in_ios","dependencies":[]},{"name":"google_sign_in_web","dependencies":[]},{"name":"package_info_plus","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"photo_manager","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]},{"name":"sqflite","dependencies":["sqflite_android","sqflite_darwin"]},{"name":"sqflite_android","dependencies":[]},{"name":"sqflite_darwin","dependencies":[]},{"name":"url_launcher","dependencies":["url_launcher_android","url_launcher_ios","url_launcher_linux","url_launcher_macos","url_launcher_web","url_launcher_windows"]},{"name":"url_launcher_android","dependencies":[]},{"name":"url_launcher_ios","dependencies":[]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2024-12-02 10:30:00.722748","version":"3.24.5","swift_package_manager_enabled":false} \ No newline at end of file diff --git a/data/.gitignore b/data/.gitignore index ac5aa98..15b2304 100644 --- a/data/.gitignore +++ b/data/.gitignore @@ -27,3 +27,6 @@ migrate_working_dir/ **/doc/api/ .dart_tool/ build/ + +# Credentials +lib/apis/network/secrets.dart \ No newline at end of file diff --git a/data/lib/apis/dropbox/dropbox_auth_endpoints.dart b/data/lib/apis/dropbox/dropbox_auth_endpoints.dart index ea813ed..cf3e74d 100644 --- a/data/lib/apis/dropbox/dropbox_auth_endpoints.dart +++ b/data/lib/apis/dropbox/dropbox_auth_endpoints.dart @@ -1,3 +1,4 @@ +import '../../domain/config.dart'; import '../network/urls.dart'; import '../network/endpoint.dart'; @@ -83,3 +84,82 @@ class DropboxGetUserAccountEndpoint extends Endpoint { @override String get path => '/users/get_current_account'; } + +class DropboxCreateAppPropertyTemplate extends Endpoint { + const DropboxCreateAppPropertyTemplate(); + + @override + String get baseUrl => BaseURL.dropboxV2; + + @override + HttpMethod get method => HttpMethod.post; + + @override + String get path => '/file_properties/templates/add_for_user'; + + @override + Map get data => { + "description": "This property is used to store file local information.", + "fields": [ + { + "description": "The local reference id of the file.", + "name": ProviderConstants.localRefIdKey, + "type": "string", + } + ], + "name": ProviderConstants.dropboxAppTemplateName, + }; +} + +class DropboxGetAppPropertyTemplate extends Endpoint { + const DropboxGetAppPropertyTemplate(); + + @override + String get baseUrl => BaseURL.dropboxV2; + + @override + HttpMethod get method => HttpMethod.post; + + @override + String get path => '/file_properties/templates/list_for_user'; +} + +class DropboxRemoveAppPropertyTemplate extends Endpoint { + final String id; + + const DropboxRemoveAppPropertyTemplate(this.id); + + @override + String get baseUrl => BaseURL.dropboxV2; + + @override + HttpMethod get method => HttpMethod.post; + + @override + String get path => '/file_properties/templates/remove_for_user'; + + @override + Object? get data => { + "template_id": id, + }; +} + +class DropboxGetAppPropertiesTemplateDetails extends Endpoint { + final String id; + + const DropboxGetAppPropertiesTemplateDetails(this.id); + + @override + String get baseUrl => BaseURL.dropboxV2; + + @override + HttpMethod get method => HttpMethod.post; + + @override + String get path => '/file_properties/templates/get_for_user'; + + @override + Object? get data => { + "template_id": id, + }; +} diff --git a/data/lib/apis/dropbox/dropbox_content_endpoints.dart b/data/lib/apis/dropbox/dropbox_content_endpoints.dart new file mode 100644 index 0000000..a35a0b6 --- /dev/null +++ b/data/lib/apis/dropbox/dropbox_content_endpoints.dart @@ -0,0 +1,289 @@ +import 'dart:convert'; +import 'package:dio/dio.dart'; +import '../../domain/config.dart'; +import '../../models/media_content/media_content.dart'; +import '../network/endpoint.dart'; +import '../network/urls.dart'; + +class DropboxCreateFolderEndpoint extends Endpoint { + final String name; + + const DropboxCreateFolderEndpoint({required this.name}); + + @override + String get baseUrl => BaseURL.dropboxV2; + + @override + HttpMethod get method => HttpMethod.post; + + @override + String get path => '/files/create_folder_v2'; + + @override + Object? get data => {"autorename": false, "path": "/$name"}; +} + +class DropboxListFolderEndpoint extends Endpoint { + final bool includeDeleted; + final String appPropertyTemplateId; + final bool includeHasExplicitSharedMembers; + final int limit; + final bool includeMountedFolders; + final bool includeNonDownloadableFiles; + final String folderPath; + final bool recursive; + + const DropboxListFolderEndpoint({ + this.includeDeleted = false, + this.includeHasExplicitSharedMembers = false, + required this.limit, + this.includeMountedFolders = false, + this.includeNonDownloadableFiles = false, + this.recursive = false, + required this.folderPath, + required this.appPropertyTemplateId, + }); + + @override + String get baseUrl => BaseURL.dropboxV2; + + @override + HttpMethod get method => HttpMethod.post; + + @override + String get path => '/files/list_folder'; + + @override + Object? get data => { + "include_deleted": includeDeleted, + "include_has_explicit_shared_members": includeHasExplicitSharedMembers, + "limit": limit, + 'include_property_groups': { + ".tag": "filter_some", + "filter_some": [appPropertyTemplateId], + }, + "include_mounted_folders": includeMountedFolders, + "include_non_downloadable_files": includeNonDownloadableFiles, + "path": folderPath, + "recursive": recursive, + }; +} + +class DropboxListFolderContinueEndpoint extends Endpoint { + final String cursor; + + const DropboxListFolderContinueEndpoint({ + required this.cursor, + }); + + @override + String get baseUrl => BaseURL.dropboxV2; + + @override + HttpMethod get method => HttpMethod.post; + + @override + String get path => '/files/list_folder/continue'; + + @override + Object? get data => { + "cursor": cursor, + }; +} + +class DropboxUploadEndpoint extends Endpoint { + final String appPropertyTemplateId; + final String filePath; + final String? localRefId; + final String mode; + final bool autoRename; + final bool mute; + final AppMediaContent content; + final bool strictConflict; + final void Function(int chunk, int length)? onProgress; + final CancelToken? cancellationToken; + + const DropboxUploadEndpoint({ + required this.appPropertyTemplateId, + required this.filePath, + this.mode = 'add', + this.autoRename = true, + this.mute = false, + this.localRefId, + this.strictConflict = false, + this.cancellationToken, + this.onProgress, + required this.content, + }); + + @override + String get baseUrl => BaseURL.dropboxContentV2; + + @override + HttpMethod get method => HttpMethod.post; + + @override + String get path => '/files/upload'; + + @override + Map get headers => { + 'Dropbox-API-Arg': jsonEncode({ + 'path': filePath, + 'mode': mode, + 'autorename': autoRename, + 'mute': mute, + 'strict_conflict': strictConflict, + 'property_groups': [ + { + "fields": [ + { + "name": ProviderConstants.localRefIdKey, + "value": localRefId ?? '', + }, + ], + "template_id": appPropertyTemplateId, + } + ], + }), + 'Content-Type': content.contentType, + 'Content-Length': content.length, + }; + + @override + Object? get data => content.stream; + + @override + CancelToken? get cancelToken => cancellationToken; + + @override + void Function(int p1, int p2)? get onSendProgress => onProgress; +} + +class DropboxDownloadEndpoint extends DownloadEndpoint { + final String filePath; + final String storagePath; + final void Function(int chunk, int length)? onProgress; + final CancelToken? cancellationToken; + + const DropboxDownloadEndpoint({ + required this.filePath, + required this.storagePath, + this.cancellationToken, + this.onProgress, + }); + + @override + String get baseUrl => BaseURL.dropboxContentV2; + + @override + HttpMethod get method => HttpMethod.post; + + @override + String get path => '/files/download'; + + @override + Map get headers => { + 'Dropbox-API-Arg': jsonEncode({ + 'path': filePath, + }), + }; + + @override + CancelToken? get cancelToken => cancellationToken; + + @override + void Function(int p1, int p2)? get onReceiveProgress => onProgress; + + @override + String? get storePath => storagePath; +} + +class DropboxDeleteEndpoint extends Endpoint { + final String id; + final CancelToken? cancellationToken; + + const DropboxDeleteEndpoint({ + required this.id, + this.cancellationToken, + }); + + @override + String get baseUrl => BaseURL.dropboxV2; + + @override + HttpMethod get method => HttpMethod.post; + + @override + String get path => '/files/delete_v2'; + + @override + Map get data => { + 'path': id, + }; + + @override + CancelToken? get cancelToken => cancellationToken; +} + +class DropboxUpdateAppPropertyEndpoint extends Endpoint { + final String id; + final String appPropertyTemplateId; + final String localRefId; + final CancelToken? cancellationToken; + + const DropboxUpdateAppPropertyEndpoint({ + required this.id, + required this.appPropertyTemplateId, + required this.localRefId, + this.cancellationToken, + }); + + @override + String get baseUrl => BaseURL.dropboxV2; + + @override + HttpMethod get method => HttpMethod.post; + + @override + String get path => '/file_properties/properties/overwrite'; + + @override + Map get data => { + "path": id, + "property_groups": [ + { + "fields": [ + { + "name": ProviderConstants.localRefIdKey, + "value": localRefId, + } + ], + "template_id": appPropertyTemplateId, + } + ], + }; + + @override + CancelToken? get cancelToken => cancellationToken; +} + +class DropboxGetFileMetadata extends Endpoint { + final String id; + + const DropboxGetFileMetadata({required this.id}); + + @override + String get baseUrl => BaseURL.dropboxV2; + + @override + HttpMethod get method => HttpMethod.post; + + @override + String get path => '/files/get_metadata'; + + @override + Map get data => { + "include_media_info": true, + 'path': id, + }; +} diff --git a/data/lib/apis/google_drive/google_drive_endpoint.dart b/data/lib/apis/google_drive/google_drive_endpoint.dart index 561e3e1..09759e5 100644 --- a/data/lib/apis/google_drive/google_drive_endpoint.dart +++ b/data/lib/apis/google_drive/google_drive_endpoint.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import '../../domain/config.dart'; import '../network/endpoint.dart'; import '../../models/media_content/media_content.dart'; import 'package:dio/dio.dart'; @@ -6,13 +7,13 @@ import 'package:googleapis/drive/v3.dart' as drive; import 'package:http_parser/http_parser.dart'; import '../network/urls.dart'; -class UploadGoogleDriveFile extends Endpoint { +class GoogleDriveUploadEndpoint extends Endpoint { final drive.File request; final AppMediaContent content; final CancelToken? cancellationToken; final void Function(int chunk, int length)? onProgress; - const UploadGoogleDriveFile({ + const GoogleDriveUploadEndpoint({ required this.request, required this.content, this.cancellationToken, @@ -30,7 +31,7 @@ class UploadGoogleDriveFile extends Endpoint { @override Map get headers => { - 'Content-Type': 'multipart/related', + 'Content-Type': content.contentType, 'Content-Length': content.length.toString(), }; @@ -60,7 +61,7 @@ class UploadGoogleDriveFile extends Endpoint { void Function(int p1, int p2)? get onSendProgress => onProgress; } -class DownloadGoogleDriveFileContent extends DownloadEndpoint { +class GoogleDriveDownloadEndpoint extends DownloadEndpoint { final String id; final void Function(int received, int total)? onProgress; @@ -69,7 +70,7 @@ class DownloadGoogleDriveFileContent extends DownloadEndpoint { final CancelToken? cancellationToken; - const DownloadGoogleDriveFileContent({ + const GoogleDriveDownloadEndpoint({ required this.id, this.cancellationToken, this.onProgress, @@ -96,3 +97,31 @@ class DownloadGoogleDriveFileContent extends DownloadEndpoint { @override String? get storePath => saveLocation; } + +class GoogleDriveUpdateAppPropertiesEndpoint extends Endpoint { + final String id; + final String localFileId; + final CancelToken? cancellationToken; + + const GoogleDriveUpdateAppPropertiesEndpoint({ + required this.id, + required this.localFileId, + this.cancellationToken, + }); + + @override + String get baseUrl => BaseURL.googleDriveV3; + + @override + String get path => '/files/$id'; + + @override + CancelToken? get cancelToken => cancellationToken; + + @override + Object? get data => { + "properties": { + ProviderConstants.localRefIdKey: localFileId, + }, + }; +} diff --git a/data/lib/apis/network/client.dart b/data/lib/apis/network/client.dart index b7f2419..3a28205 100644 --- a/data/lib/apis/network/client.dart +++ b/data/lib/apis/network/client.dart @@ -1,4 +1,4 @@ -import '../../errors/app_error.dart'; +import '../../log/logger.dart'; import '../../services/auth_service.dart'; import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -6,83 +6,84 @@ import '../../storage/app_preferences.dart'; import 'endpoint.dart'; import 'interceptors/dropbox_auth_interceptor.dart'; import 'interceptors/google_drive_auth_interceptor.dart'; +import 'interceptors/logger_interceptor.dart'; final googleAuthenticatedDioProvider = Provider((ref) { return Dio() ..options.connectTimeout = const Duration(seconds: 60) ..options.sendTimeout = const Duration(seconds: 60) ..options.receiveTimeout = const Duration(seconds: 60) - ..interceptors.add( - GoogleDriveAuthInterceptor( - googleSignIn: ref.read(googleSignInProvider), - ), - ); + ..interceptors.addAll([ + GoogleDriveAuthInterceptor(googleSignIn: ref.read(googleSignInProvider)), + LoggerInterceptor(logger: ref.read(loggerProvider)), + ]); }); final dropboxAuthenticatedDioProvider = Provider((ref) { + final dropboxInterceptor = DropboxAuthInterceptor( + authService: ref.read(authServiceProvider), + rawDio: ref.read(rawDioProvider), + dropboxToken: ref.read(AppPreferences.dropboxToken), + ); + ref.listen(AppPreferences.dropboxToken, (previous, next) { + dropboxInterceptor.updateToken(next); + }); return Dio() ..options.connectTimeout = const Duration(seconds: 60) ..options.sendTimeout = const Duration(seconds: 60) ..options.receiveTimeout = const Duration(seconds: 60) - ..interceptors.add( - DropboxAuthInterceptor( - authService: ref.read(authServiceProvider), - dropboxTokenController: ref.read(AppPreferences.dropboxToken.notifier), - ), - ); + ..interceptors.addAll([ + dropboxInterceptor, + LoggerInterceptor(logger: ref.read(loggerProvider)), + ]); }); final rawDioProvider = Provider((ref) { return Dio() ..options.connectTimeout = const Duration(seconds: 60) ..options.sendTimeout = const Duration(seconds: 60) - ..options.receiveTimeout = const Duration(seconds: 60); + ..options.receiveTimeout = const Duration(seconds: 60) + ..interceptors.addAll([ + LoggerInterceptor(logger: ref.read(loggerProvider)), + ]); }); extension DioExtensions on Dio { Future> req(Endpoint endpoint) async { - try { - return await request( - endpoint.baseUrl + endpoint.path, - queryParameters: endpoint.queryParameters, - options: Options( - method: endpoint.method.name, - headers: endpoint.headers, - responseType: endpoint.responseType, - contentType: endpoint.contentType, - validateStatus: (status) => - status != null && status >= 200 && status < 300, - ), - data: endpoint.data, - cancelToken: endpoint.cancelToken, - onReceiveProgress: endpoint.onReceiveProgress, - onSendProgress: endpoint.onSendProgress, - ); - } catch (e) { - throw AppError.fromError(e); - } + return await request( + endpoint.baseUrl + endpoint.path, + queryParameters: endpoint.queryParameters, + options: Options( + method: endpoint.method.name, + headers: endpoint.headers, + responseType: endpoint.responseType, + contentType: endpoint.contentType, + validateStatus: (status) => + status != null && status >= 200 && status < 300, + ), + data: endpoint.data, + cancelToken: endpoint.cancelToken, + onReceiveProgress: endpoint.onReceiveProgress, + onSendProgress: endpoint.onSendProgress, + ); } Future downloadReq(DownloadEndpoint endpoint) async { - try { - return await download( - endpoint.baseUrl + endpoint.path, - endpoint.storePath, - queryParameters: endpoint.queryParameters, - options: Options( - method: endpoint.method.name, - headers: endpoint.headers, - responseType: endpoint.responseType, - contentType: endpoint.contentType, - validateStatus: (status) => - status != null && status >= 200 && status < 300, - ), - data: endpoint.data, - cancelToken: endpoint.cancelToken, - onReceiveProgress: endpoint.onReceiveProgress, - ); - } catch (e) { - throw AppError.fromError(e); - } + return await download( + endpoint.baseUrl + endpoint.path, + endpoint.storePath, + queryParameters: endpoint.queryParameters, + options: Options( + method: endpoint.method.name, + headers: endpoint.headers, + responseType: endpoint.responseType, + contentType: endpoint.contentType, + validateStatus: (status) => + status != null && status >= 200 && status < 300, + ), + data: endpoint.data, + cancelToken: endpoint.cancelToken, + onReceiveProgress: endpoint.onReceiveProgress, + ); } } diff --git a/data/lib/apis/network/interceptors/dropbox_auth_interceptor.dart b/data/lib/apis/network/interceptors/dropbox_auth_interceptor.dart index 34d637c..a334568 100644 --- a/data/lib/apis/network/interceptors/dropbox_auth_interceptor.dart +++ b/data/lib/apis/network/interceptors/dropbox_auth_interceptor.dart @@ -1,18 +1,19 @@ import 'dart:async'; +import '../../../models/dropbox/token/dropbox_token.dart'; import '../../../services/auth_service.dart'; import 'package:dio/dio.dart'; -import '../../../models/token/token.dart'; -import '../../../storage/provider/preferences_provider.dart'; class DropboxAuthInterceptor extends Interceptor { final AuthService authService; - final PreferenceNotifier dropboxTokenController; + final Dio rawDio; + DropboxToken? dropboxToken; Completer? _refreshTokenCompleter; DropboxAuthInterceptor({ - required this.dropboxTokenController, + required this.dropboxToken, required this.authService, + required this.rawDio, }); @override @@ -20,11 +21,15 @@ class DropboxAuthInterceptor extends Interceptor { RequestOptions options, RequestInterceptorHandler handler, ) async { - final dropboxToken = dropboxTokenController.state; + /// Add authorization header to the request if the token is not expired + /// If the token is expired, refresh it and add the new token to the request if (dropboxToken != null) { - await _refreshTokenIfNeeded(dropboxToken); + if (dropboxToken!.expires_in.isBefore(DateTime.now())) { + await _refreshAccessToken(dropboxToken!); + } + options.headers.addAll({ - 'Authorization': 'Bearer ${dropboxToken.access_token}', + 'Authorization': 'Bearer ${dropboxToken!.access_token}', }); } handler.next(options); @@ -33,22 +38,61 @@ class DropboxAuthInterceptor extends Interceptor { @override void onError(DioException err, ErrorInterceptorHandler handler) async { if (err.response?.statusCode == 401 && - dropboxTokenController.state != null) { - await _refreshTokenIfNeeded(dropboxTokenController.state!); + err.response?.data?['error']?['.tag'] == 'expired_access_token') { + await _refreshAccessToken(dropboxToken!); + try { + handler.resolve(await _retry(err.requestOptions)); + } on DioException catch (e) { + handler.next(e); + } + return; } + + /// If the error is not due to expired access token, pass the error to the next handler handler.next(err); } - Future _refreshTokenIfNeeded(DropboxToken dropboxToken) async { - if (dropboxToken.expires_in.isBefore(DateTime.now())) { - if (_refreshTokenCompleter == null) { - _refreshTokenCompleter = Completer(); - await authService.refreshDropboxToken(); - _refreshTokenCompleter?.complete(); - _refreshTokenCompleter = null; - } else { - await _refreshTokenCompleter!.future; - } + void updateToken(DropboxToken? newToken) { + dropboxToken = newToken; + } + + Future _refreshAccessToken(DropboxToken dropboxToken) async { + /// If there is already a refresh token request in progress, wait for it to complete + if (_refreshTokenCompleter == null) { + _refreshTokenCompleter = Completer(); + + /// Refresh the token + await authService.refreshDropboxToken(); + _refreshTokenCompleter?.complete(); + _refreshTokenCompleter = null; + } else { + await _refreshTokenCompleter!.future; } } + + Future _retry( + RequestOptions requestOptions, + ) async { + /// Remove the Authorization header from the request + /// Add the new access token to the request + final newHeaders = Map.from(requestOptions.headers); + newHeaders.remove('Authorization'); + if (dropboxToken != null) { + newHeaders['Authorization'] = 'Bearer ${dropboxToken!.access_token}'; + } + + /// Retry the request + return rawDio.request( + requestOptions.baseUrl + requestOptions.path, + data: requestOptions.data, + queryParameters: requestOptions.queryParameters, + options: Options( + method: requestOptions.method, + headers: newHeaders, + ), + cancelToken: requestOptions.cancelToken, + onReceiveProgress: requestOptions.onReceiveProgress, + onSendProgress: requestOptions.onSendProgress, + ); + } } diff --git a/data/lib/apis/network/interceptors/logger_interceptor.dart b/data/lib/apis/network/interceptors/logger_interceptor.dart new file mode 100644 index 0000000..9c2227e --- /dev/null +++ b/data/lib/apis/network/interceptors/logger_interceptor.dart @@ -0,0 +1,76 @@ +import 'dart:convert'; + +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:logger/logger.dart'; + +class LoggerInterceptor extends Interceptor { + final Logger logger; + + LoggerInterceptor({required this.logger}); + + String prettyJson(Object? data) { + try { + if (data is Map || data is List) { + return JsonEncoder.withIndent(" ").convert(data); + } else { + return data.toString(); + } + } catch (e) { + return data.toString(); + } + } + + @override + void onRequest(RequestOptions options, RequestInterceptorHandler handler) { + String message = '⚡️ Request Started: [${options.method}] ${options.uri}'; + if (kDebugMode) { + if (options.headers.isNotEmpty) { + message += '\n⚡️ Headers: ${prettyJson(options.headers)}'; + } + if (options.data != null) { + message += '\n⚡️ Body: ${prettyJson(options.data)}'; + } + if (options.queryParameters.isNotEmpty) { + message += + '\n⚡️ Query Parameters: ${prettyJson(options.queryParameters)}'; + } + } + logger.d(message); + handler.next(options); + } + + @override + void onResponse(Response response, ResponseInterceptorHandler handler) { + String message = + '⚡️ Response: ${response.statusCode} -> [${response.requestOptions.method}] ${response.requestOptions.uri}'; + + if (kDebugMode) { + if (response.headers.map.isNotEmpty) { + message += '\n⚡️ Headers: ${prettyJson(response.headers.map)}'; + } + message += '\n⚡️ Response body: ${prettyJson(response.data)}'; + } + logger.d(message); + handler.next(response); + } + + @override + void onError(DioException err, ErrorInterceptorHandler handler) { + String message = + '⚡️ ERROR at [${err.requestOptions.method}] ${err.requestOptions.uri}: (${err.response?.statusCode}) ${err.message?.trim()}'; + if (err.response != null) { + if (err.response!.headers.map.isNotEmpty) { + message += '\n⚡️ Headers: ${prettyJson(err.response!.headers.map)}'; + } + message += '\n⚡️ Body: ${prettyJson(err.response?.data)}'; + } + logger.e( + message, + error: err.error, + stackTrace: err.stackTrace, + time: DateTime.now(), + ); + handler.next(err); + } +} diff --git a/data/lib/apis/network/secrets.dart b/data/lib/apis/network/secrets.dart deleted file mode 100644 index 470ebe4..0000000 --- a/data/lib/apis/network/secrets.dart +++ /dev/null @@ -1,4 +0,0 @@ -class AppSecretes { - static const dropBoxAppKey = '873x7j2iwh8mrea'; - static const dropBoxAppSecret = 'mq2azqdd6y1upzr'; -} diff --git a/data/lib/domain/config.dart b/data/lib/domain/config.dart index d2725f3..e489e4c 100644 --- a/data/lib/domain/config.dart +++ b/data/lib/domain/config.dart @@ -1,5 +1,11 @@ -import 'package:flutter/foundation.dart'; - class FeatureFlags { - static const dropboxEnabled = kDebugMode; + static const dropboxEnabled = true; +} + +class ProviderConstants { + static const String backupFolderName = 'Cloud Gallery Backup'; + static const String backupFolderPath = '/Cloud Gallery Backup'; + static const String localRefIdKey = 'local_ref_id'; + static const String dropboxAppTemplateName = + 'Cloud Gallery Local File Information'; } diff --git a/app/lib/domain/formatter/byte_formatter.dart b/data/lib/domain/formatters/byte_formatter.dart similarity index 100% rename from app/lib/domain/formatter/byte_formatter.dart rename to data/lib/domain/formatters/byte_formatter.dart diff --git a/app/lib/domain/handlers/notification_handler.dart b/data/lib/handlers/notification_handler.dart similarity index 56% rename from app/lib/domain/handlers/notification_handler.dart rename to data/lib/handlers/notification_handler.dart index 0ac1f30..ea29782 100644 --- a/app/lib/domain/handlers/notification_handler.dart +++ b/data/lib/handlers/notification_handler.dart @@ -1,8 +1,5 @@ -import '../../ui/navigation/app_route.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; const _androidChannel = AndroidNotificationChannel( 'notification-channel-cloud-gallery', // id @@ -20,13 +17,23 @@ final notificationHandlerProvider = Provider.autoDispose((ref) { class NotificationHandler { final _flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); - Future init(BuildContext context) async { + Future init({ + void Function(NotificationResponse)? + onDidReceiveBackgroundNotificationResponse, + void Function(NotificationResponse)? onDidReceiveNotificationResponse, + }) async { _flutterLocalNotificationsPlugin .resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin>() ?.createNotificationChannel(_androidChannel); - if (context.mounted) _initLocalNotifications(context); + await _initLocalNotifications( + onDidReceiveBackgroundNotificationResponse, + onDidReceiveNotificationResponse, + ); + + return await _flutterLocalNotificationsPlugin + .getNotificationAppLaunchDetails(); } void requestPermission() { @@ -36,7 +43,11 @@ class NotificationHandler { ?.requestNotificationsPermission(); } - Future _initLocalNotifications(BuildContext context) async { + Future _initLocalNotifications( + void Function(NotificationResponse)? + onDidReceiveBackgroundNotificationResponse, + void Function(NotificationResponse)? onDidReceiveNotificationResponse, + ) async { _flutterLocalNotificationsPlugin.initialize( const InitializationSettings( android: AndroidInitializationSettings('cloud_gallery_logo'), @@ -46,32 +57,34 @@ class NotificationHandler { requestSoundPermission: true, ), ), - onDidReceiveNotificationResponse: (response) { - if (context.mounted) { - context.go(AppRoutePath.home); - context.push(AppRoutePath.transfer); - } - }, + onDidReceiveBackgroundNotificationResponse: + onDidReceiveBackgroundNotificationResponse, + onDidReceiveNotificationResponse: onDidReceiveNotificationResponse, ); + } - final initial = await _flutterLocalNotificationsPlugin - .getNotificationAppLaunchDetails(); + Future cancelNotification(int id) async { + await _flutterLocalNotificationsPlugin.cancel(id); + } - if (initial?.didNotificationLaunchApp == true) { - if (context.mounted) { - context.go(AppRoutePath.home); - context.push(AppRoutePath.transfer); - } - } + Future cancelAllNotification() async { + await _flutterLocalNotificationsPlugin.cancelAll(); } Future showNotification({ required int id, required String name, required String description, + AndroidNotificationCategory? category, + bool fullScreenIntent = false, + StyleInformation? styleInformation, + bool setAsGroupSummary = false, + String? groupKey, bool vibration = true, + bool silent = false, int? progress, int maxProgress = 100, + bool onlyAlertOnce = false, }) async { await _flutterLocalNotificationsPlugin.show( id, @@ -89,7 +102,22 @@ class NotificationHandler { showProgress: progress != null, maxProgress: maxProgress, progress: progress ?? 0, + groupKey: groupKey, + category: category, + silent: silent, + fullScreenIntent: fullScreenIntent, + groupAlertBehavior: GroupAlertBehavior.all, + ongoing: progress != null, + styleInformation: styleInformation, channelDescription: _androidChannel.description, + setAsGroupSummary: setAsGroupSummary, + onlyAlertOnce: onlyAlertOnce, + ), + iOS: DarwinNotificationDetails( + presentSound: !silent, + threadIdentifier: groupKey, + presentBanner: !silent, + presentAlert: !silent, ), ), ); diff --git a/data/lib/log/logger.dart b/data/lib/log/logger.dart new file mode 100644 index 0000000..d65444d --- /dev/null +++ b/data/lib/log/logger.dart @@ -0,0 +1,21 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:logger/logger.dart'; + +final loggerProvider = Provider( + (ref) => Logger( + filter: ProductionFilter(), + printer: PrettyPrinter( + methodCount: 0, + errorMethodCount: 10, + printEmojis: false, + colors: false, + ), + ), +); + +class UnitTestLoggerFilter extends LogFilter { + @override + bool shouldLog(LogEvent event) { + return false; + } +} diff --git a/data/lib/models/app_process/app_process.dart b/data/lib/models/app_process/app_process.dart deleted file mode 100644 index 2683170..0000000 --- a/data/lib/models/app_process/app_process.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; - -import '../media/media.dart'; - -part 'app_process.freezed.dart'; - -enum AppProcessStatus { - waiting, - uploading, - deleting, - downloading, - success, - terminated, - failed; - - bool get isProcessing => - this == AppProcessStatus.uploading || - this == AppProcessStatus.deleting || - this == AppProcessStatus.downloading; - - bool get isWaiting => this == AppProcessStatus.waiting; - - bool get isSuccess => this == AppProcessStatus.success; - - bool get isFailed => this == AppProcessStatus.failed; - - bool get isTerminated => this == AppProcessStatus.terminated; -} - -@freezed -class AppProcess with _$AppProcess { - const factory AppProcess({ - required String id, - required AppMedia media, - required AppProcessStatus status, - @Default(false) bool isFromAutoBackup, - Object? response, - @Default(null) AppProcessProgress? progress, - }) = _AppProcess; -} - -@freezed -class AppProcessProgress with _$AppProcessProgress { - const factory AppProcessProgress({required int total, required int chunk}) = - _AppProcessProgress; -} - -extension AppProcessProgressExtension on AppProcessProgress { - /// Get the percentage of the progress 0.0 - 1.0 - double get percentageInPoint => total == 0 ? 0 : chunk / total; - - /// Get the percentage of the progress 0 - 100 - double get percentage => percentageInPoint * 100; -} diff --git a/data/lib/models/app_process/app_process.freezed.dart b/data/lib/models/app_process/app_process.freezed.dart deleted file mode 100644 index e11db6a..0000000 --- a/data/lib/models/app_process/app_process.freezed.dart +++ /dev/null @@ -1,431 +0,0 @@ -// coverage:ignore-file -// GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'app_process.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -T _$identity(T value) => value; - -final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); - -/// @nodoc -mixin _$AppProcess { - String get id => throw _privateConstructorUsedError; - AppMedia get media => throw _privateConstructorUsedError; - AppProcessStatus get status => throw _privateConstructorUsedError; - bool get isFromAutoBackup => throw _privateConstructorUsedError; - Object? get response => throw _privateConstructorUsedError; - AppProcessProgress? get progress => throw _privateConstructorUsedError; - - /// Create a copy of AppProcess - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $AppProcessCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $AppProcessCopyWith<$Res> { - factory $AppProcessCopyWith( - AppProcess value, $Res Function(AppProcess) then) = - _$AppProcessCopyWithImpl<$Res, AppProcess>; - @useResult - $Res call( - {String id, - AppMedia media, - AppProcessStatus status, - bool isFromAutoBackup, - Object? response, - AppProcessProgress? progress}); - - $AppMediaCopyWith<$Res> get media; - $AppProcessProgressCopyWith<$Res>? get progress; -} - -/// @nodoc -class _$AppProcessCopyWithImpl<$Res, $Val extends AppProcess> - implements $AppProcessCopyWith<$Res> { - _$AppProcessCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of AppProcess - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? id = null, - Object? media = null, - Object? status = null, - Object? isFromAutoBackup = null, - Object? response = freezed, - Object? progress = freezed, - }) { - return _then(_value.copyWith( - id: null == id - ? _value.id - : id // ignore: cast_nullable_to_non_nullable - as String, - media: null == media - ? _value.media - : media // ignore: cast_nullable_to_non_nullable - as AppMedia, - status: null == status - ? _value.status - : status // ignore: cast_nullable_to_non_nullable - as AppProcessStatus, - isFromAutoBackup: null == isFromAutoBackup - ? _value.isFromAutoBackup - : isFromAutoBackup // ignore: cast_nullable_to_non_nullable - as bool, - response: freezed == response ? _value.response : response, - progress: freezed == progress - ? _value.progress - : progress // ignore: cast_nullable_to_non_nullable - as AppProcessProgress?, - ) as $Val); - } - - /// Create a copy of AppProcess - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $AppMediaCopyWith<$Res> get media { - return $AppMediaCopyWith<$Res>(_value.media, (value) { - return _then(_value.copyWith(media: value) as $Val); - }); - } - - /// Create a copy of AppProcess - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $AppProcessProgressCopyWith<$Res>? get progress { - if (_value.progress == null) { - return null; - } - - return $AppProcessProgressCopyWith<$Res>(_value.progress!, (value) { - return _then(_value.copyWith(progress: value) as $Val); - }); - } -} - -/// @nodoc -abstract class _$$AppProcessImplCopyWith<$Res> - implements $AppProcessCopyWith<$Res> { - factory _$$AppProcessImplCopyWith( - _$AppProcessImpl value, $Res Function(_$AppProcessImpl) then) = - __$$AppProcessImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {String id, - AppMedia media, - AppProcessStatus status, - bool isFromAutoBackup, - Object? response, - AppProcessProgress? progress}); - - @override - $AppMediaCopyWith<$Res> get media; - @override - $AppProcessProgressCopyWith<$Res>? get progress; -} - -/// @nodoc -class __$$AppProcessImplCopyWithImpl<$Res> - extends _$AppProcessCopyWithImpl<$Res, _$AppProcessImpl> - implements _$$AppProcessImplCopyWith<$Res> { - __$$AppProcessImplCopyWithImpl( - _$AppProcessImpl _value, $Res Function(_$AppProcessImpl) _then) - : super(_value, _then); - - /// Create a copy of AppProcess - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? id = null, - Object? media = null, - Object? status = null, - Object? isFromAutoBackup = null, - Object? response = freezed, - Object? progress = freezed, - }) { - return _then(_$AppProcessImpl( - id: null == id - ? _value.id - : id // ignore: cast_nullable_to_non_nullable - as String, - media: null == media - ? _value.media - : media // ignore: cast_nullable_to_non_nullable - as AppMedia, - status: null == status - ? _value.status - : status // ignore: cast_nullable_to_non_nullable - as AppProcessStatus, - isFromAutoBackup: null == isFromAutoBackup - ? _value.isFromAutoBackup - : isFromAutoBackup // ignore: cast_nullable_to_non_nullable - as bool, - response: freezed == response ? _value.response : response, - progress: freezed == progress - ? _value.progress - : progress // ignore: cast_nullable_to_non_nullable - as AppProcessProgress?, - )); - } -} - -/// @nodoc - -class _$AppProcessImpl implements _AppProcess { - const _$AppProcessImpl( - {required this.id, - required this.media, - required this.status, - this.isFromAutoBackup = false, - this.response, - this.progress = null}); - - @override - final String id; - @override - final AppMedia media; - @override - final AppProcessStatus status; - @override - @JsonKey() - final bool isFromAutoBackup; - @override - final Object? response; - @override - @JsonKey() - final AppProcessProgress? progress; - - @override - String toString() { - return 'AppProcess(id: $id, media: $media, status: $status, isFromAutoBackup: $isFromAutoBackup, response: $response, progress: $progress)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$AppProcessImpl && - (identical(other.id, id) || other.id == id) && - (identical(other.media, media) || other.media == media) && - (identical(other.status, status) || other.status == status) && - (identical(other.isFromAutoBackup, isFromAutoBackup) || - other.isFromAutoBackup == isFromAutoBackup) && - const DeepCollectionEquality().equals(other.response, response) && - (identical(other.progress, progress) || - other.progress == progress)); - } - - @override - int get hashCode => Object.hash( - runtimeType, - id, - media, - status, - isFromAutoBackup, - const DeepCollectionEquality().hash(response), - progress); - - /// Create a copy of AppProcess - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$AppProcessImplCopyWith<_$AppProcessImpl> get copyWith => - __$$AppProcessImplCopyWithImpl<_$AppProcessImpl>(this, _$identity); -} - -abstract class _AppProcess implements AppProcess { - const factory _AppProcess( - {required final String id, - required final AppMedia media, - required final AppProcessStatus status, - final bool isFromAutoBackup, - final Object? response, - final AppProcessProgress? progress}) = _$AppProcessImpl; - - @override - String get id; - @override - AppMedia get media; - @override - AppProcessStatus get status; - @override - bool get isFromAutoBackup; - @override - Object? get response; - @override - AppProcessProgress? get progress; - - /// Create a copy of AppProcess - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$AppProcessImplCopyWith<_$AppProcessImpl> get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -mixin _$AppProcessProgress { - int get total => throw _privateConstructorUsedError; - int get chunk => throw _privateConstructorUsedError; - - /// Create a copy of AppProcessProgress - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $AppProcessProgressCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $AppProcessProgressCopyWith<$Res> { - factory $AppProcessProgressCopyWith( - AppProcessProgress value, $Res Function(AppProcessProgress) then) = - _$AppProcessProgressCopyWithImpl<$Res, AppProcessProgress>; - @useResult - $Res call({int total, int chunk}); -} - -/// @nodoc -class _$AppProcessProgressCopyWithImpl<$Res, $Val extends AppProcessProgress> - implements $AppProcessProgressCopyWith<$Res> { - _$AppProcessProgressCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of AppProcessProgress - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? total = null, - Object? chunk = null, - }) { - return _then(_value.copyWith( - total: null == total - ? _value.total - : total // ignore: cast_nullable_to_non_nullable - as int, - chunk: null == chunk - ? _value.chunk - : chunk // ignore: cast_nullable_to_non_nullable - as int, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$AppProcessProgressImplCopyWith<$Res> - implements $AppProcessProgressCopyWith<$Res> { - factory _$$AppProcessProgressImplCopyWith(_$AppProcessProgressImpl value, - $Res Function(_$AppProcessProgressImpl) then) = - __$$AppProcessProgressImplCopyWithImpl<$Res>; - @override - @useResult - $Res call({int total, int chunk}); -} - -/// @nodoc -class __$$AppProcessProgressImplCopyWithImpl<$Res> - extends _$AppProcessProgressCopyWithImpl<$Res, _$AppProcessProgressImpl> - implements _$$AppProcessProgressImplCopyWith<$Res> { - __$$AppProcessProgressImplCopyWithImpl(_$AppProcessProgressImpl _value, - $Res Function(_$AppProcessProgressImpl) _then) - : super(_value, _then); - - /// Create a copy of AppProcessProgress - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? total = null, - Object? chunk = null, - }) { - return _then(_$AppProcessProgressImpl( - total: null == total - ? _value.total - : total // ignore: cast_nullable_to_non_nullable - as int, - chunk: null == chunk - ? _value.chunk - : chunk // ignore: cast_nullable_to_non_nullable - as int, - )); - } -} - -/// @nodoc - -class _$AppProcessProgressImpl implements _AppProcessProgress { - const _$AppProcessProgressImpl({required this.total, required this.chunk}); - - @override - final int total; - @override - final int chunk; - - @override - String toString() { - return 'AppProcessProgress(total: $total, chunk: $chunk)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$AppProcessProgressImpl && - (identical(other.total, total) || other.total == total) && - (identical(other.chunk, chunk) || other.chunk == chunk)); - } - - @override - int get hashCode => Object.hash(runtimeType, total, chunk); - - /// Create a copy of AppProcessProgress - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$AppProcessProgressImplCopyWith<_$AppProcessProgressImpl> get copyWith => - __$$AppProcessProgressImplCopyWithImpl<_$AppProcessProgressImpl>( - this, _$identity); -} - -abstract class _AppProcessProgress implements AppProcessProgress { - const factory _AppProcessProgress( - {required final int total, - required final int chunk}) = _$AppProcessProgressImpl; - - @override - int get total; - @override - int get chunk; - - /// Create a copy of AppProcessProgress - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$AppProcessProgressImplCopyWith<_$AppProcessProgressImpl> get copyWith => - throw _privateConstructorUsedError; -} diff --git a/data/lib/models/dropbox_account/dropbox_account.dart b/data/lib/models/dropbox/account/dropbox_account.dart similarity index 100% rename from data/lib/models/dropbox_account/dropbox_account.dart rename to data/lib/models/dropbox/account/dropbox_account.dart diff --git a/data/lib/models/dropbox_account/dropbox_account.freezed.dart b/data/lib/models/dropbox/account/dropbox_account.freezed.dart similarity index 100% rename from data/lib/models/dropbox_account/dropbox_account.freezed.dart rename to data/lib/models/dropbox/account/dropbox_account.freezed.dart diff --git a/data/lib/models/dropbox_account/dropbox_account.g.dart b/data/lib/models/dropbox/account/dropbox_account.g.dart similarity index 100% rename from data/lib/models/dropbox_account/dropbox_account.g.dart rename to data/lib/models/dropbox/account/dropbox_account.g.dart diff --git a/data/lib/models/dropbox/entity/dropbox_entity.dart b/data/lib/models/dropbox/entity/dropbox_entity.dart new file mode 100644 index 0000000..b3306bf --- /dev/null +++ b/data/lib/models/dropbox/entity/dropbox_entity.dart @@ -0,0 +1,40 @@ +// ignore_for_file: non_constant_identifier_names + +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'dropbox_entity.freezed.dart'; + +part 'dropbox_entity.g.dart'; + +@freezed +class ApiDropboxEntity with _$ApiDropboxEntity { + const factory ApiDropboxEntity({ + required String id, + required String name, + required String path_lower, + required String path_display, + @JsonKey( + name: '.tag', + unknownEnumValue: ApiDropboxEntityType.unknown, + ) + required ApiDropboxEntityType type, + }) = _ApiDropboxEntity; + + factory ApiDropboxEntity.fromJson(Map json) => + _$ApiDropboxEntityFromJson(json); + + factory ApiDropboxEntity.fromFolderJson(Map json) { + json.addAll({".tag": "folder"}); + return _$ApiDropboxEntityFromJson(json); + } +} + +@JsonEnum(valueField: "value") +enum ApiDropboxEntityType { + folder('folder'), + unknown('unknown'); + + final String value; + + const ApiDropboxEntityType(this.value); +} diff --git a/data/lib/models/dropbox/entity/dropbox_entity.freezed.dart b/data/lib/models/dropbox/entity/dropbox_entity.freezed.dart new file mode 100644 index 0000000..6f6e433 --- /dev/null +++ b/data/lib/models/dropbox/entity/dropbox_entity.freezed.dart @@ -0,0 +1,259 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'dropbox_entity.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +ApiDropboxEntity _$ApiDropboxEntityFromJson(Map json) { + return _ApiDropboxEntity.fromJson(json); +} + +/// @nodoc +mixin _$ApiDropboxEntity { + String get id => throw _privateConstructorUsedError; + String get name => throw _privateConstructorUsedError; + String get path_lower => throw _privateConstructorUsedError; + String get path_display => throw _privateConstructorUsedError; + @JsonKey(name: '.tag', unknownEnumValue: ApiDropboxEntityType.unknown) + ApiDropboxEntityType get type => throw _privateConstructorUsedError; + + /// Serializes this ApiDropboxEntity to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of ApiDropboxEntity + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ApiDropboxEntityCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ApiDropboxEntityCopyWith<$Res> { + factory $ApiDropboxEntityCopyWith( + ApiDropboxEntity value, $Res Function(ApiDropboxEntity) then) = + _$ApiDropboxEntityCopyWithImpl<$Res, ApiDropboxEntity>; + @useResult + $Res call( + {String id, + String name, + String path_lower, + String path_display, + @JsonKey(name: '.tag', unknownEnumValue: ApiDropboxEntityType.unknown) + ApiDropboxEntityType type}); +} + +/// @nodoc +class _$ApiDropboxEntityCopyWithImpl<$Res, $Val extends ApiDropboxEntity> + implements $ApiDropboxEntityCopyWith<$Res> { + _$ApiDropboxEntityCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ApiDropboxEntity + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? name = null, + Object? path_lower = null, + Object? path_display = null, + Object? type = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + path_lower: null == path_lower + ? _value.path_lower + : path_lower // ignore: cast_nullable_to_non_nullable + as String, + path_display: null == path_display + ? _value.path_display + : path_display // ignore: cast_nullable_to_non_nullable + as String, + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as ApiDropboxEntityType, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$ApiDropboxEntityImplCopyWith<$Res> + implements $ApiDropboxEntityCopyWith<$Res> { + factory _$$ApiDropboxEntityImplCopyWith(_$ApiDropboxEntityImpl value, + $Res Function(_$ApiDropboxEntityImpl) then) = + __$$ApiDropboxEntityImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String id, + String name, + String path_lower, + String path_display, + @JsonKey(name: '.tag', unknownEnumValue: ApiDropboxEntityType.unknown) + ApiDropboxEntityType type}); +} + +/// @nodoc +class __$$ApiDropboxEntityImplCopyWithImpl<$Res> + extends _$ApiDropboxEntityCopyWithImpl<$Res, _$ApiDropboxEntityImpl> + implements _$$ApiDropboxEntityImplCopyWith<$Res> { + __$$ApiDropboxEntityImplCopyWithImpl(_$ApiDropboxEntityImpl _value, + $Res Function(_$ApiDropboxEntityImpl) _then) + : super(_value, _then); + + /// Create a copy of ApiDropboxEntity + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? name = null, + Object? path_lower = null, + Object? path_display = null, + Object? type = null, + }) { + return _then(_$ApiDropboxEntityImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + path_lower: null == path_lower + ? _value.path_lower + : path_lower // ignore: cast_nullable_to_non_nullable + as String, + path_display: null == path_display + ? _value.path_display + : path_display // ignore: cast_nullable_to_non_nullable + as String, + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as ApiDropboxEntityType, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$ApiDropboxEntityImpl implements _ApiDropboxEntity { + const _$ApiDropboxEntityImpl( + {required this.id, + required this.name, + required this.path_lower, + required this.path_display, + @JsonKey(name: '.tag', unknownEnumValue: ApiDropboxEntityType.unknown) + required this.type}); + + factory _$ApiDropboxEntityImpl.fromJson(Map json) => + _$$ApiDropboxEntityImplFromJson(json); + + @override + final String id; + @override + final String name; + @override + final String path_lower; + @override + final String path_display; + @override + @JsonKey(name: '.tag', unknownEnumValue: ApiDropboxEntityType.unknown) + final ApiDropboxEntityType type; + + @override + String toString() { + return 'ApiDropboxEntity(id: $id, name: $name, path_lower: $path_lower, path_display: $path_display, type: $type)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ApiDropboxEntityImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.name, name) || other.name == name) && + (identical(other.path_lower, path_lower) || + other.path_lower == path_lower) && + (identical(other.path_display, path_display) || + other.path_display == path_display) && + (identical(other.type, type) || other.type == type)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => + Object.hash(runtimeType, id, name, path_lower, path_display, type); + + /// Create a copy of ApiDropboxEntity + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ApiDropboxEntityImplCopyWith<_$ApiDropboxEntityImpl> get copyWith => + __$$ApiDropboxEntityImplCopyWithImpl<_$ApiDropboxEntityImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$ApiDropboxEntityImplToJson( + this, + ); + } +} + +abstract class _ApiDropboxEntity implements ApiDropboxEntity { + const factory _ApiDropboxEntity( + {required final String id, + required final String name, + required final String path_lower, + required final String path_display, + @JsonKey(name: '.tag', unknownEnumValue: ApiDropboxEntityType.unknown) + required final ApiDropboxEntityType type}) = _$ApiDropboxEntityImpl; + + factory _ApiDropboxEntity.fromJson(Map json) = + _$ApiDropboxEntityImpl.fromJson; + + @override + String get id; + @override + String get name; + @override + String get path_lower; + @override + String get path_display; + @override + @JsonKey(name: '.tag', unknownEnumValue: ApiDropboxEntityType.unknown) + ApiDropboxEntityType get type; + + /// Create a copy of ApiDropboxEntity + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ApiDropboxEntityImplCopyWith<_$ApiDropboxEntityImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/data/lib/models/dropbox/entity/dropbox_entity.g.dart b/data/lib/models/dropbox/entity/dropbox_entity.g.dart new file mode 100644 index 0000000..ca10bd1 --- /dev/null +++ b/data/lib/models/dropbox/entity/dropbox_entity.g.dart @@ -0,0 +1,33 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'dropbox_entity.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$ApiDropboxEntityImpl _$$ApiDropboxEntityImplFromJson( + Map json) => + _$ApiDropboxEntityImpl( + id: json['id'] as String, + name: json['name'] as String, + path_lower: json['path_lower'] as String, + path_display: json['path_display'] as String, + type: $enumDecode(_$ApiDropboxEntityTypeEnumMap, json['.tag'], + unknownValue: ApiDropboxEntityType.unknown), + ); + +Map _$$ApiDropboxEntityImplToJson( + _$ApiDropboxEntityImpl instance) => + { + 'id': instance.id, + 'name': instance.name, + 'path_lower': instance.path_lower, + 'path_display': instance.path_display, + '.tag': _$ApiDropboxEntityTypeEnumMap[instance.type]!, + }; + +const _$ApiDropboxEntityTypeEnumMap = { + ApiDropboxEntityType.folder: 'folder', + ApiDropboxEntityType.unknown: 'unknown', +}; diff --git a/data/lib/models/dropbox/token/dropbox_token.dart b/data/lib/models/dropbox/token/dropbox_token.dart new file mode 100644 index 0000000..8085d66 --- /dev/null +++ b/data/lib/models/dropbox/token/dropbox_token.dart @@ -0,0 +1,22 @@ +// ignore_for_file: non_constant_identifier_names + +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'dropbox_token.freezed.dart'; +part 'dropbox_token.g.dart'; + +@freezed +abstract class DropboxToken with _$DropboxToken { + const factory DropboxToken({ + required String access_token, + required String token_type, + required DateTime expires_in, + required String refresh_token, + required String account_id, + required String scope, + required String uid, + }) = _DropboxToken; + + factory DropboxToken.fromJson(Map json) => + _$DropboxTokenFromJson(json); +} diff --git a/data/lib/models/token/token.freezed.dart b/data/lib/models/dropbox/token/dropbox_token.freezed.dart similarity index 96% rename from data/lib/models/token/token.freezed.dart rename to data/lib/models/dropbox/token/dropbox_token.freezed.dart index 39879bc..9d56e23 100644 --- a/data/lib/models/token/token.freezed.dart +++ b/data/lib/models/dropbox/token/dropbox_token.freezed.dart @@ -3,7 +3,7 @@ // ignore_for_file: type=lint // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark -part of 'token.dart'; +part of 'dropbox_token.dart'; // ************************************************************************** // FreezedGenerator @@ -22,7 +22,6 @@ DropboxToken _$DropboxTokenFromJson(Map json) { mixin _$DropboxToken { String get access_token => throw _privateConstructorUsedError; String get token_type => throw _privateConstructorUsedError; - @ExpiresInJsonConverter() DateTime get expires_in => throw _privateConstructorUsedError; String get refresh_token => throw _privateConstructorUsedError; String get account_id => throw _privateConstructorUsedError; @@ -48,7 +47,7 @@ abstract class $DropboxTokenCopyWith<$Res> { $Res call( {String access_token, String token_type, - @ExpiresInJsonConverter() DateTime expires_in, + DateTime expires_in, String refresh_token, String account_id, String scope, @@ -122,7 +121,7 @@ abstract class _$$DropboxTokenImplCopyWith<$Res> $Res call( {String access_token, String token_type, - @ExpiresInJsonConverter() DateTime expires_in, + DateTime expires_in, String refresh_token, String account_id, String scope, @@ -189,7 +188,7 @@ class _$DropboxTokenImpl implements _DropboxToken { const _$DropboxTokenImpl( {required this.access_token, required this.token_type, - @ExpiresInJsonConverter() required this.expires_in, + required this.expires_in, required this.refresh_token, required this.account_id, required this.scope, @@ -203,7 +202,6 @@ class _$DropboxTokenImpl implements _DropboxToken { @override final String token_type; @override - @ExpiresInJsonConverter() final DateTime expires_in; @override final String refresh_token; @@ -263,7 +261,7 @@ abstract class _DropboxToken implements DropboxToken { const factory _DropboxToken( {required final String access_token, required final String token_type, - @ExpiresInJsonConverter() required final DateTime expires_in, + required final DateTime expires_in, required final String refresh_token, required final String account_id, required final String scope, @@ -277,7 +275,6 @@ abstract class _DropboxToken implements DropboxToken { @override String get token_type; @override - @ExpiresInJsonConverter() DateTime get expires_in; @override String get refresh_token; diff --git a/data/lib/models/token/token.g.dart b/data/lib/models/dropbox/token/dropbox_token.g.dart similarity index 82% rename from data/lib/models/token/token.g.dart rename to data/lib/models/dropbox/token/dropbox_token.g.dart index c8fa152..724d8a9 100644 --- a/data/lib/models/token/token.g.dart +++ b/data/lib/models/dropbox/token/dropbox_token.g.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'token.dart'; +part of 'dropbox_token.dart'; // ************************************************************************** // JsonSerializableGenerator @@ -10,8 +10,7 @@ _$DropboxTokenImpl _$$DropboxTokenImplFromJson(Map json) => _$DropboxTokenImpl( access_token: json['access_token'] as String, token_type: json['token_type'] as String, - expires_in: const ExpiresInJsonConverter() - .fromJson((json['expires_in'] as num).toInt()), + expires_in: DateTime.parse(json['expires_in'] as String), refresh_token: json['refresh_token'] as String, account_id: json['account_id'] as String, scope: json['scope'] as String, @@ -22,7 +21,7 @@ Map _$$DropboxTokenImplToJson(_$DropboxTokenImpl instance) => { 'access_token': instance.access_token, 'token_type': instance.token_type, - 'expires_in': const ExpiresInJsonConverter().toJson(instance.expires_in), + 'expires_in': instance.expires_in.toIso8601String(), 'refresh_token': instance.refresh_token, 'account_id': instance.account_id, 'scope': instance.scope, diff --git a/data/lib/models/media/media.dart b/data/lib/models/media/media.dart index f955238..c4afc31 100644 --- a/data/lib/models/media/media.dart +++ b/data/lib/models/media/media.dart @@ -3,6 +3,7 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:googleapis/drive/v3.dart' as drive show File; import 'package:photo_manager/photo_manager.dart' show AssetEntity; +import '../../domain/config.dart'; import '../../domain/json_converters/date_time_json_converter.dart'; import '../../domain/json_converters/duration_json_converter.dart'; @@ -83,7 +84,8 @@ enum AppMediaOrientation { @JsonEnum(valueField: 'value') enum AppMediaSource { local('local'), - googleDrive('google_drive'); + googleDrive('google_drive'), + dropbox('dropbox'); final String value; @@ -93,9 +95,11 @@ enum AppMediaSource { @freezed class AppMedia with _$AppMedia { const AppMedia._(); + const factory AppMedia({ required String id, String? driveMediaRefId, + String? dropboxMediaRefId, String? name, required String path, String? thumbnailLink, @@ -143,9 +147,10 @@ class AppMedia with _$AppMedia { int.parse(file.videoMediaMetadata?.durationMillis ?? '0'), ) : null; + return AppMedia( - id: file.id!, - path: file.description ?? '', + id: file.appProperties?[ProviderConstants.localRefIdKey] ?? file.id!, + path: file.name ?? '', thumbnailLink: file.thumbnailLink, name: file.name, driveMediaRefId: file.id, @@ -168,6 +173,7 @@ class AppMedia with _$AppMedia { final file = await asset.originFile; if (file == null) return null; + final type = AppMediaType.getType(mimeType: asset.mimeType, location: file.path); final length = await file.length(); @@ -191,4 +197,35 @@ class AppMedia with _$AppMedia { displayWidth: asset.size.width, ); } + + static AppMedia fromDropboxJson({ + required Map json, + Map? metadataJson, + }) { + return AppMedia( + id: json['property_groups'] != null && json['property_groups'].isNotEmpty + ? json['property_groups'][0]['fields'][0]['value'] + : json['id'], + path: json['path_display'], + name: json['name'], + videoDuration: + metadataJson?['media_info']?['metadata']?['duration'] != null + ? Duration( + milliseconds: + metadataJson!['media_info']!['metadata']!['duration'], + ) + : null, + displayHeight: metadataJson?['media_info']?['metadata']?['dimensions'] + ?['height'] + ?.toDouble(), + displayWidth: metadataJson?['media_info']?['metadata']?['dimensions'] + ?['width'] + ?.toDouble(), + size: json['size'].toString(), + dropboxMediaRefId: json['id'], + createdTime: DateTime.parse(json['client_modified']), + type: AppMediaType.getType(location: json['path_display']), + sources: [AppMediaSource.dropbox], + ); + } } diff --git a/data/lib/models/media/media.freezed.dart b/data/lib/models/media/media.freezed.dart index 1d86e08..1b2e138 100644 --- a/data/lib/models/media/media.freezed.dart +++ b/data/lib/models/media/media.freezed.dart @@ -22,6 +22,7 @@ AppMedia _$AppMediaFromJson(Map json) { mixin _$AppMedia { String get id => throw _privateConstructorUsedError; String? get driveMediaRefId => throw _privateConstructorUsedError; + String? get dropboxMediaRefId => throw _privateConstructorUsedError; String? get name => throw _privateConstructorUsedError; String get path => throw _privateConstructorUsedError; String? get thumbnailLink => throw _privateConstructorUsedError; @@ -59,6 +60,7 @@ abstract class $AppMediaCopyWith<$Res> { $Res call( {String id, String? driveMediaRefId, + String? dropboxMediaRefId, String? name, String path, String? thumbnailLink, @@ -93,6 +95,7 @@ class _$AppMediaCopyWithImpl<$Res, $Val extends AppMedia> $Res call({ Object? id = null, Object? driveMediaRefId = freezed, + Object? dropboxMediaRefId = freezed, Object? name = freezed, Object? path = null, Object? thumbnailLink = freezed, @@ -118,6 +121,10 @@ class _$AppMediaCopyWithImpl<$Res, $Val extends AppMedia> ? _value.driveMediaRefId : driveMediaRefId // ignore: cast_nullable_to_non_nullable as String?, + dropboxMediaRefId: freezed == dropboxMediaRefId + ? _value.dropboxMediaRefId + : dropboxMediaRefId // ignore: cast_nullable_to_non_nullable + as String?, name: freezed == name ? _value.name : name // ignore: cast_nullable_to_non_nullable @@ -193,6 +200,7 @@ abstract class _$$AppMediaImplCopyWith<$Res> $Res call( {String id, String? driveMediaRefId, + String? dropboxMediaRefId, String? name, String path, String? thumbnailLink, @@ -225,6 +233,7 @@ class __$$AppMediaImplCopyWithImpl<$Res> $Res call({ Object? id = null, Object? driveMediaRefId = freezed, + Object? dropboxMediaRefId = freezed, Object? name = freezed, Object? path = null, Object? thumbnailLink = freezed, @@ -250,6 +259,10 @@ class __$$AppMediaImplCopyWithImpl<$Res> ? _value.driveMediaRefId : driveMediaRefId // ignore: cast_nullable_to_non_nullable as String?, + dropboxMediaRefId: freezed == dropboxMediaRefId + ? _value.dropboxMediaRefId + : dropboxMediaRefId // ignore: cast_nullable_to_non_nullable + as String?, name: freezed == name ? _value.name : name // ignore: cast_nullable_to_non_nullable @@ -320,6 +333,7 @@ class _$AppMediaImpl extends _AppMedia { const _$AppMediaImpl( {required this.id, this.driveMediaRefId, + this.dropboxMediaRefId, this.name, required this.path, this.thumbnailLink, @@ -346,6 +360,8 @@ class _$AppMediaImpl extends _AppMedia { @override final String? driveMediaRefId; @override + final String? dropboxMediaRefId; + @override final String? name; @override final String path; @@ -387,7 +403,7 @@ class _$AppMediaImpl extends _AppMedia { @override String toString() { - return 'AppMedia(id: $id, driveMediaRefId: $driveMediaRefId, name: $name, path: $path, thumbnailLink: $thumbnailLink, displayHeight: $displayHeight, displayWidth: $displayWidth, type: $type, mimeType: $mimeType, createdTime: $createdTime, modifiedTime: $modifiedTime, orientation: $orientation, size: $size, videoDuration: $videoDuration, latitude: $latitude, longitude: $longitude, sources: $sources)'; + return 'AppMedia(id: $id, driveMediaRefId: $driveMediaRefId, dropboxMediaRefId: $dropboxMediaRefId, name: $name, path: $path, thumbnailLink: $thumbnailLink, displayHeight: $displayHeight, displayWidth: $displayWidth, type: $type, mimeType: $mimeType, createdTime: $createdTime, modifiedTime: $modifiedTime, orientation: $orientation, size: $size, videoDuration: $videoDuration, latitude: $latitude, longitude: $longitude, sources: $sources)'; } @override @@ -398,6 +414,8 @@ class _$AppMediaImpl extends _AppMedia { (identical(other.id, id) || other.id == id) && (identical(other.driveMediaRefId, driveMediaRefId) || other.driveMediaRefId == driveMediaRefId) && + (identical(other.dropboxMediaRefId, dropboxMediaRefId) || + other.dropboxMediaRefId == dropboxMediaRefId) && (identical(other.name, name) || other.name == name) && (identical(other.path, path) || other.path == path) && (identical(other.thumbnailLink, thumbnailLink) || @@ -431,6 +449,7 @@ class _$AppMediaImpl extends _AppMedia { runtimeType, id, driveMediaRefId, + dropboxMediaRefId, name, path, thumbnailLink, @@ -467,6 +486,7 @@ abstract class _AppMedia extends AppMedia { const factory _AppMedia( {required final String id, final String? driveMediaRefId, + final String? dropboxMediaRefId, final String? name, required final String path, final String? thumbnailLink, @@ -492,6 +512,8 @@ abstract class _AppMedia extends AppMedia { @override String? get driveMediaRefId; @override + String? get dropboxMediaRefId; + @override String? get name; @override String get path; diff --git a/data/lib/models/media/media.g.dart b/data/lib/models/media/media.g.dart index 5e4dab0..c83dc33 100644 --- a/data/lib/models/media/media.g.dart +++ b/data/lib/models/media/media.g.dart @@ -10,6 +10,7 @@ _$AppMediaImpl _$$AppMediaImplFromJson(Map json) => _$AppMediaImpl( id: json['id'] as String, driveMediaRefId: json['driveMediaRefId'] as String?, + dropboxMediaRefId: json['dropboxMediaRefId'] as String?, name: json['name'] as String?, path: json['path'] as String, thumbnailLink: json['thumbnailLink'] as String?, @@ -38,6 +39,7 @@ Map _$$AppMediaImplToJson(_$AppMediaImpl instance) => { 'id': instance.id, 'driveMediaRefId': instance.driveMediaRefId, + 'dropboxMediaRefId': instance.dropboxMediaRefId, 'name': instance.name, 'path': instance.path, 'thumbnailLink': instance.thumbnailLink, @@ -79,6 +81,7 @@ const _$AppMediaOrientationEnumMap = { const _$AppMediaSourceEnumMap = { AppMediaSource.local: 'local', AppMediaSource.googleDrive: 'google_drive', + AppMediaSource.dropbox: 'dropbox', }; Json? _$JsonConverterToJson( diff --git a/data/lib/models/media/media_extension.dart b/data/lib/models/media/media_extension.dart index d0049f5..38a755e 100644 --- a/data/lib/models/media/media_extension.dart +++ b/data/lib/models/media/media_extension.dart @@ -34,18 +34,72 @@ extension AppMediaExtension on AppMedia { AppMedia mergeGoogleDriveMedia(AppMedia media) { return copyWith( + mimeType: mimeType ?? media.mimeType, + longitude: longitude ?? media.longitude, + latitude: latitude ?? media.latitude, + orientation: orientation ?? media.orientation, + videoDuration: videoDuration ?? media.videoDuration, + displayWidth: displayWidth ?? media.displayWidth, + displayHeight: displayHeight ?? media.displayHeight, + size: size ?? media.size, + modifiedTime: modifiedTime ?? media.modifiedTime, + createdTime: createdTime ?? media.createdTime, + name: name ?? media.name, thumbnailLink: media.thumbnailLink, driveMediaRefId: media.driveMediaRefId, sources: sources.toList()..add(AppMediaSource.googleDrive), ); } + AppMedia removeGoogleDriveRef() { + return copyWith( + thumbnailLink: null, + driveMediaRefId: null, + sources: sources.toList()..remove(AppMediaSource.googleDrive), + ); + } + + AppMedia mergeDropboxMedia(AppMedia media) { + return copyWith( + mimeType: mimeType ?? media.mimeType, + longitude: longitude ?? media.longitude, + latitude: latitude ?? media.latitude, + orientation: orientation ?? media.orientation, + videoDuration: videoDuration ?? media.videoDuration, + displayWidth: displayWidth ?? media.displayWidth, + displayHeight: displayHeight ?? media.displayHeight, + size: size ?? media.size, + modifiedTime: modifiedTime ?? media.modifiedTime, + createdTime: createdTime ?? media.createdTime, + name: name ?? media.name, + dropboxMediaRefId: media.dropboxMediaRefId, + sources: sources.toList()..add(AppMediaSource.dropbox), + ); + } + + AppMedia removeDropboxRef() { + return copyWith( + dropboxMediaRefId: null, + sources: sources.toList()..remove(AppMediaSource.dropbox), + ); + } + + AppMedia removeLocalRef() { + return copyWith( + id: driveMediaRefId ?? dropboxMediaRefId ?? '', + sources: sources.toList()..remove(AppMediaSource.local), + ); + } + bool get isGoogleDriveStored => sources.contains(AppMediaSource.googleDrive) && sources.length == 1; bool get isLocalStored => sources.contains(AppMediaSource.local) && sources.length == 1; + bool get isDropboxStored => + sources.contains(AppMediaSource.dropbox) && sources.length == 1; + bool get isCommonStored => sources.length > 1; String get extension { diff --git a/data/lib/models/media_process/media_process.dart b/data/lib/models/media_process/media_process.dart new file mode 100644 index 0000000..3cbb3cb --- /dev/null +++ b/data/lib/models/media_process/media_process.dart @@ -0,0 +1,142 @@ +// ignore_for_file: non_constant_identifier_names + +import 'dart:convert'; + +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../media/media.dart'; + +part 'media_process.freezed.dart'; + +part 'media_process.g.dart'; + +@JsonEnum(valueField: 'value') +enum MediaQueueProcessStatus { + waiting('waiting'), + uploading('uploading'), + deleting('deleting'), + downloading('downloading'), + completed('completed'), + terminated('terminated'), + failed('failed'); + + final String value; + + const MediaQueueProcessStatus(this.value); + + bool get isRunning => + this == MediaQueueProcessStatus.uploading || + this == MediaQueueProcessStatus.deleting || + this == MediaQueueProcessStatus.downloading; + + bool get isWaiting => this == MediaQueueProcessStatus.waiting; + + bool get isCompleted => this == MediaQueueProcessStatus.completed; + + bool get isFailed => this == MediaQueueProcessStatus.failed; + + bool get isTerminated => this == MediaQueueProcessStatus.terminated; +} + +@JsonEnum(valueField: 'value') +enum MediaProvider { + googleDrive('google-drive'), + dropbox('dropbox'); + + final String value; + + const MediaProvider(this.value); +} + +class LocalDatabaseBoolConverter extends JsonConverter { + const LocalDatabaseBoolConverter(); + + @override + bool fromJson(int json) { + return json == 1; + } + + @override + int toJson(bool object) { + return object ? 1 : 0; + } +} + +class LocalDatabaseAppMediaConverter extends JsonConverter { + const LocalDatabaseAppMediaConverter(); + + @override + AppMedia? fromJson(String? json) { + try { + return json == null ? null : AppMedia.fromJson(jsonDecode(json)); + } catch (e) { + return null; + } + } + + @override + String? toJson(AppMedia? object) { + try { + return object == null ? null : jsonEncode(object.toJson()); + } catch (e) { + return null; + } + } +} + +@freezed +class DownloadMediaProcess with _$DownloadMediaProcess { + const DownloadMediaProcess._(); + + const factory DownloadMediaProcess({ + required String id, + required String name, + required String media_id, + required String folder_id, + required int notification_id, + required MediaProvider provider, + @Default(MediaQueueProcessStatus.waiting) MediaQueueProcessStatus status, + @LocalDatabaseAppMediaConverter() AppMedia? response, + @Default(1) int total, + required String extension, + @Default(0) int chunk, + }) = _DownloadMediaProcess; + + /// progress 0.0 - 1.0 + double get progress => total == 0 ? 0 : chunk / total; + + /// percentage of the progress 0 - 100 + double get progressPercentage => progress * 100; + + factory DownloadMediaProcess.fromJson(Map json) => + _$DownloadMediaProcessFromJson(json); +} + +@freezed +class UploadMediaProcess with _$UploadMediaProcess { + const UploadMediaProcess._(); + + const factory UploadMediaProcess({ + required String id, + required String media_id, + required int notification_id, + required String folder_id, + required MediaProvider provider, + required String path, + String? mime_type, + @Default(MediaQueueProcessStatus.waiting) MediaQueueProcessStatus status, + @LocalDatabaseBoolConverter() @Default(false) bool upload_using_auto_backup, + @LocalDatabaseAppMediaConverter() AppMedia? response, + @Default(1) int total, + @Default(0) int chunk, + }) = _UploadMediaProcess; + + /// progress 0.0 - 1.0 + double get progress => total == 0 ? 0 : chunk / total; + + /// percentage of the progress 0 - 100 + double get progressPercentage => progress * 100; + + factory UploadMediaProcess.fromJson(Map json) => + _$UploadMediaProcessFromJson(json); +} diff --git a/data/lib/models/media_process/media_process.freezed.dart b/data/lib/models/media_process/media_process.freezed.dart new file mode 100644 index 0000000..dc776fd --- /dev/null +++ b/data/lib/models/media_process/media_process.freezed.dart @@ -0,0 +1,831 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'media_process.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +DownloadMediaProcess _$DownloadMediaProcessFromJson(Map json) { + return _DownloadMediaProcess.fromJson(json); +} + +/// @nodoc +mixin _$DownloadMediaProcess { + String get id => throw _privateConstructorUsedError; + String get name => throw _privateConstructorUsedError; + String get media_id => throw _privateConstructorUsedError; + String get folder_id => throw _privateConstructorUsedError; + int get notification_id => throw _privateConstructorUsedError; + MediaProvider get provider => throw _privateConstructorUsedError; + MediaQueueProcessStatus get status => throw _privateConstructorUsedError; + @LocalDatabaseAppMediaConverter() + AppMedia? get response => throw _privateConstructorUsedError; + int get total => throw _privateConstructorUsedError; + String get extension => throw _privateConstructorUsedError; + int get chunk => throw _privateConstructorUsedError; + + /// Serializes this DownloadMediaProcess to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of DownloadMediaProcess + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $DownloadMediaProcessCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $DownloadMediaProcessCopyWith<$Res> { + factory $DownloadMediaProcessCopyWith(DownloadMediaProcess value, + $Res Function(DownloadMediaProcess) then) = + _$DownloadMediaProcessCopyWithImpl<$Res, DownloadMediaProcess>; + @useResult + $Res call( + {String id, + String name, + String media_id, + String folder_id, + int notification_id, + MediaProvider provider, + MediaQueueProcessStatus status, + @LocalDatabaseAppMediaConverter() AppMedia? response, + int total, + String extension, + int chunk}); + + $AppMediaCopyWith<$Res>? get response; +} + +/// @nodoc +class _$DownloadMediaProcessCopyWithImpl<$Res, + $Val extends DownloadMediaProcess> + implements $DownloadMediaProcessCopyWith<$Res> { + _$DownloadMediaProcessCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of DownloadMediaProcess + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? name = null, + Object? media_id = null, + Object? folder_id = null, + Object? notification_id = null, + Object? provider = null, + Object? status = null, + Object? response = freezed, + Object? total = null, + Object? extension = null, + Object? chunk = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + media_id: null == media_id + ? _value.media_id + : media_id // ignore: cast_nullable_to_non_nullable + as String, + folder_id: null == folder_id + ? _value.folder_id + : folder_id // ignore: cast_nullable_to_non_nullable + as String, + notification_id: null == notification_id + ? _value.notification_id + : notification_id // ignore: cast_nullable_to_non_nullable + as int, + provider: null == provider + ? _value.provider + : provider // ignore: cast_nullable_to_non_nullable + as MediaProvider, + status: null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as MediaQueueProcessStatus, + response: freezed == response + ? _value.response + : response // ignore: cast_nullable_to_non_nullable + as AppMedia?, + total: null == total + ? _value.total + : total // ignore: cast_nullable_to_non_nullable + as int, + extension: null == extension + ? _value.extension + : extension // ignore: cast_nullable_to_non_nullable + as String, + chunk: null == chunk + ? _value.chunk + : chunk // ignore: cast_nullable_to_non_nullable + as int, + ) as $Val); + } + + /// Create a copy of DownloadMediaProcess + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $AppMediaCopyWith<$Res>? get response { + if (_value.response == null) { + return null; + } + + return $AppMediaCopyWith<$Res>(_value.response!, (value) { + return _then(_value.copyWith(response: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$DownloadMediaProcessImplCopyWith<$Res> + implements $DownloadMediaProcessCopyWith<$Res> { + factory _$$DownloadMediaProcessImplCopyWith(_$DownloadMediaProcessImpl value, + $Res Function(_$DownloadMediaProcessImpl) then) = + __$$DownloadMediaProcessImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String id, + String name, + String media_id, + String folder_id, + int notification_id, + MediaProvider provider, + MediaQueueProcessStatus status, + @LocalDatabaseAppMediaConverter() AppMedia? response, + int total, + String extension, + int chunk}); + + @override + $AppMediaCopyWith<$Res>? get response; +} + +/// @nodoc +class __$$DownloadMediaProcessImplCopyWithImpl<$Res> + extends _$DownloadMediaProcessCopyWithImpl<$Res, _$DownloadMediaProcessImpl> + implements _$$DownloadMediaProcessImplCopyWith<$Res> { + __$$DownloadMediaProcessImplCopyWithImpl(_$DownloadMediaProcessImpl _value, + $Res Function(_$DownloadMediaProcessImpl) _then) + : super(_value, _then); + + /// Create a copy of DownloadMediaProcess + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? name = null, + Object? media_id = null, + Object? folder_id = null, + Object? notification_id = null, + Object? provider = null, + Object? status = null, + Object? response = freezed, + Object? total = null, + Object? extension = null, + Object? chunk = null, + }) { + return _then(_$DownloadMediaProcessImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + media_id: null == media_id + ? _value.media_id + : media_id // ignore: cast_nullable_to_non_nullable + as String, + folder_id: null == folder_id + ? _value.folder_id + : folder_id // ignore: cast_nullable_to_non_nullable + as String, + notification_id: null == notification_id + ? _value.notification_id + : notification_id // ignore: cast_nullable_to_non_nullable + as int, + provider: null == provider + ? _value.provider + : provider // ignore: cast_nullable_to_non_nullable + as MediaProvider, + status: null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as MediaQueueProcessStatus, + response: freezed == response + ? _value.response + : response // ignore: cast_nullable_to_non_nullable + as AppMedia?, + total: null == total + ? _value.total + : total // ignore: cast_nullable_to_non_nullable + as int, + extension: null == extension + ? _value.extension + : extension // ignore: cast_nullable_to_non_nullable + as String, + chunk: null == chunk + ? _value.chunk + : chunk // ignore: cast_nullable_to_non_nullable + as int, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$DownloadMediaProcessImpl extends _DownloadMediaProcess { + const _$DownloadMediaProcessImpl( + {required this.id, + required this.name, + required this.media_id, + required this.folder_id, + required this.notification_id, + required this.provider, + this.status = MediaQueueProcessStatus.waiting, + @LocalDatabaseAppMediaConverter() this.response, + this.total = 1, + required this.extension, + this.chunk = 0}) + : super._(); + + factory _$DownloadMediaProcessImpl.fromJson(Map json) => + _$$DownloadMediaProcessImplFromJson(json); + + @override + final String id; + @override + final String name; + @override + final String media_id; + @override + final String folder_id; + @override + final int notification_id; + @override + final MediaProvider provider; + @override + @JsonKey() + final MediaQueueProcessStatus status; + @override + @LocalDatabaseAppMediaConverter() + final AppMedia? response; + @override + @JsonKey() + final int total; + @override + final String extension; + @override + @JsonKey() + final int chunk; + + @override + String toString() { + return 'DownloadMediaProcess(id: $id, name: $name, media_id: $media_id, folder_id: $folder_id, notification_id: $notification_id, provider: $provider, status: $status, response: $response, total: $total, extension: $extension, chunk: $chunk)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$DownloadMediaProcessImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.name, name) || other.name == name) && + (identical(other.media_id, media_id) || + other.media_id == media_id) && + (identical(other.folder_id, folder_id) || + other.folder_id == folder_id) && + (identical(other.notification_id, notification_id) || + other.notification_id == notification_id) && + (identical(other.provider, provider) || + other.provider == provider) && + (identical(other.status, status) || other.status == status) && + (identical(other.response, response) || + other.response == response) && + (identical(other.total, total) || other.total == total) && + (identical(other.extension, extension) || + other.extension == extension) && + (identical(other.chunk, chunk) || other.chunk == chunk)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, id, name, media_id, folder_id, + notification_id, provider, status, response, total, extension, chunk); + + /// Create a copy of DownloadMediaProcess + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$DownloadMediaProcessImplCopyWith<_$DownloadMediaProcessImpl> + get copyWith => + __$$DownloadMediaProcessImplCopyWithImpl<_$DownloadMediaProcessImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$DownloadMediaProcessImplToJson( + this, + ); + } +} + +abstract class _DownloadMediaProcess extends DownloadMediaProcess { + const factory _DownloadMediaProcess( + {required final String id, + required final String name, + required final String media_id, + required final String folder_id, + required final int notification_id, + required final MediaProvider provider, + final MediaQueueProcessStatus status, + @LocalDatabaseAppMediaConverter() final AppMedia? response, + final int total, + required final String extension, + final int chunk}) = _$DownloadMediaProcessImpl; + const _DownloadMediaProcess._() : super._(); + + factory _DownloadMediaProcess.fromJson(Map json) = + _$DownloadMediaProcessImpl.fromJson; + + @override + String get id; + @override + String get name; + @override + String get media_id; + @override + String get folder_id; + @override + int get notification_id; + @override + MediaProvider get provider; + @override + MediaQueueProcessStatus get status; + @override + @LocalDatabaseAppMediaConverter() + AppMedia? get response; + @override + int get total; + @override + String get extension; + @override + int get chunk; + + /// Create a copy of DownloadMediaProcess + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$DownloadMediaProcessImplCopyWith<_$DownloadMediaProcessImpl> + get copyWith => throw _privateConstructorUsedError; +} + +UploadMediaProcess _$UploadMediaProcessFromJson(Map json) { + return _UploadMediaProcess.fromJson(json); +} + +/// @nodoc +mixin _$UploadMediaProcess { + String get id => throw _privateConstructorUsedError; + String get media_id => throw _privateConstructorUsedError; + int get notification_id => throw _privateConstructorUsedError; + String get folder_id => throw _privateConstructorUsedError; + MediaProvider get provider => throw _privateConstructorUsedError; + String get path => throw _privateConstructorUsedError; + String? get mime_type => throw _privateConstructorUsedError; + MediaQueueProcessStatus get status => throw _privateConstructorUsedError; + @LocalDatabaseBoolConverter() + bool get upload_using_auto_backup => throw _privateConstructorUsedError; + @LocalDatabaseAppMediaConverter() + AppMedia? get response => throw _privateConstructorUsedError; + int get total => throw _privateConstructorUsedError; + int get chunk => throw _privateConstructorUsedError; + + /// Serializes this UploadMediaProcess to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of UploadMediaProcess + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $UploadMediaProcessCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $UploadMediaProcessCopyWith<$Res> { + factory $UploadMediaProcessCopyWith( + UploadMediaProcess value, $Res Function(UploadMediaProcess) then) = + _$UploadMediaProcessCopyWithImpl<$Res, UploadMediaProcess>; + @useResult + $Res call( + {String id, + String media_id, + int notification_id, + String folder_id, + MediaProvider provider, + String path, + String? mime_type, + MediaQueueProcessStatus status, + @LocalDatabaseBoolConverter() bool upload_using_auto_backup, + @LocalDatabaseAppMediaConverter() AppMedia? response, + int total, + int chunk}); + + $AppMediaCopyWith<$Res>? get response; +} + +/// @nodoc +class _$UploadMediaProcessCopyWithImpl<$Res, $Val extends UploadMediaProcess> + implements $UploadMediaProcessCopyWith<$Res> { + _$UploadMediaProcessCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of UploadMediaProcess + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? media_id = null, + Object? notification_id = null, + Object? folder_id = null, + Object? provider = null, + Object? path = null, + Object? mime_type = freezed, + Object? status = null, + Object? upload_using_auto_backup = null, + Object? response = freezed, + Object? total = null, + Object? chunk = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + media_id: null == media_id + ? _value.media_id + : media_id // ignore: cast_nullable_to_non_nullable + as String, + notification_id: null == notification_id + ? _value.notification_id + : notification_id // ignore: cast_nullable_to_non_nullable + as int, + folder_id: null == folder_id + ? _value.folder_id + : folder_id // ignore: cast_nullable_to_non_nullable + as String, + provider: null == provider + ? _value.provider + : provider // ignore: cast_nullable_to_non_nullable + as MediaProvider, + path: null == path + ? _value.path + : path // ignore: cast_nullable_to_non_nullable + as String, + mime_type: freezed == mime_type + ? _value.mime_type + : mime_type // ignore: cast_nullable_to_non_nullable + as String?, + status: null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as MediaQueueProcessStatus, + upload_using_auto_backup: null == upload_using_auto_backup + ? _value.upload_using_auto_backup + : upload_using_auto_backup // ignore: cast_nullable_to_non_nullable + as bool, + response: freezed == response + ? _value.response + : response // ignore: cast_nullable_to_non_nullable + as AppMedia?, + total: null == total + ? _value.total + : total // ignore: cast_nullable_to_non_nullable + as int, + chunk: null == chunk + ? _value.chunk + : chunk // ignore: cast_nullable_to_non_nullable + as int, + ) as $Val); + } + + /// Create a copy of UploadMediaProcess + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $AppMediaCopyWith<$Res>? get response { + if (_value.response == null) { + return null; + } + + return $AppMediaCopyWith<$Res>(_value.response!, (value) { + return _then(_value.copyWith(response: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$UploadMediaProcessImplCopyWith<$Res> + implements $UploadMediaProcessCopyWith<$Res> { + factory _$$UploadMediaProcessImplCopyWith(_$UploadMediaProcessImpl value, + $Res Function(_$UploadMediaProcessImpl) then) = + __$$UploadMediaProcessImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String id, + String media_id, + int notification_id, + String folder_id, + MediaProvider provider, + String path, + String? mime_type, + MediaQueueProcessStatus status, + @LocalDatabaseBoolConverter() bool upload_using_auto_backup, + @LocalDatabaseAppMediaConverter() AppMedia? response, + int total, + int chunk}); + + @override + $AppMediaCopyWith<$Res>? get response; +} + +/// @nodoc +class __$$UploadMediaProcessImplCopyWithImpl<$Res> + extends _$UploadMediaProcessCopyWithImpl<$Res, _$UploadMediaProcessImpl> + implements _$$UploadMediaProcessImplCopyWith<$Res> { + __$$UploadMediaProcessImplCopyWithImpl(_$UploadMediaProcessImpl _value, + $Res Function(_$UploadMediaProcessImpl) _then) + : super(_value, _then); + + /// Create a copy of UploadMediaProcess + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? media_id = null, + Object? notification_id = null, + Object? folder_id = null, + Object? provider = null, + Object? path = null, + Object? mime_type = freezed, + Object? status = null, + Object? upload_using_auto_backup = null, + Object? response = freezed, + Object? total = null, + Object? chunk = null, + }) { + return _then(_$UploadMediaProcessImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + media_id: null == media_id + ? _value.media_id + : media_id // ignore: cast_nullable_to_non_nullable + as String, + notification_id: null == notification_id + ? _value.notification_id + : notification_id // ignore: cast_nullable_to_non_nullable + as int, + folder_id: null == folder_id + ? _value.folder_id + : folder_id // ignore: cast_nullable_to_non_nullable + as String, + provider: null == provider + ? _value.provider + : provider // ignore: cast_nullable_to_non_nullable + as MediaProvider, + path: null == path + ? _value.path + : path // ignore: cast_nullable_to_non_nullable + as String, + mime_type: freezed == mime_type + ? _value.mime_type + : mime_type // ignore: cast_nullable_to_non_nullable + as String?, + status: null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as MediaQueueProcessStatus, + upload_using_auto_backup: null == upload_using_auto_backup + ? _value.upload_using_auto_backup + : upload_using_auto_backup // ignore: cast_nullable_to_non_nullable + as bool, + response: freezed == response + ? _value.response + : response // ignore: cast_nullable_to_non_nullable + as AppMedia?, + total: null == total + ? _value.total + : total // ignore: cast_nullable_to_non_nullable + as int, + chunk: null == chunk + ? _value.chunk + : chunk // ignore: cast_nullable_to_non_nullable + as int, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$UploadMediaProcessImpl extends _UploadMediaProcess { + const _$UploadMediaProcessImpl( + {required this.id, + required this.media_id, + required this.notification_id, + required this.folder_id, + required this.provider, + required this.path, + this.mime_type, + this.status = MediaQueueProcessStatus.waiting, + @LocalDatabaseBoolConverter() this.upload_using_auto_backup = false, + @LocalDatabaseAppMediaConverter() this.response, + this.total = 1, + this.chunk = 0}) + : super._(); + + factory _$UploadMediaProcessImpl.fromJson(Map json) => + _$$UploadMediaProcessImplFromJson(json); + + @override + final String id; + @override + final String media_id; + @override + final int notification_id; + @override + final String folder_id; + @override + final MediaProvider provider; + @override + final String path; + @override + final String? mime_type; + @override + @JsonKey() + final MediaQueueProcessStatus status; + @override + @JsonKey() + @LocalDatabaseBoolConverter() + final bool upload_using_auto_backup; + @override + @LocalDatabaseAppMediaConverter() + final AppMedia? response; + @override + @JsonKey() + final int total; + @override + @JsonKey() + final int chunk; + + @override + String toString() { + return 'UploadMediaProcess(id: $id, media_id: $media_id, notification_id: $notification_id, folder_id: $folder_id, provider: $provider, path: $path, mime_type: $mime_type, status: $status, upload_using_auto_backup: $upload_using_auto_backup, response: $response, total: $total, chunk: $chunk)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$UploadMediaProcessImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.media_id, media_id) || + other.media_id == media_id) && + (identical(other.notification_id, notification_id) || + other.notification_id == notification_id) && + (identical(other.folder_id, folder_id) || + other.folder_id == folder_id) && + (identical(other.provider, provider) || + other.provider == provider) && + (identical(other.path, path) || other.path == path) && + (identical(other.mime_type, mime_type) || + other.mime_type == mime_type) && + (identical(other.status, status) || other.status == status) && + (identical( + other.upload_using_auto_backup, upload_using_auto_backup) || + other.upload_using_auto_backup == upload_using_auto_backup) && + (identical(other.response, response) || + other.response == response) && + (identical(other.total, total) || other.total == total) && + (identical(other.chunk, chunk) || other.chunk == chunk)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + media_id, + notification_id, + folder_id, + provider, + path, + mime_type, + status, + upload_using_auto_backup, + response, + total, + chunk); + + /// Create a copy of UploadMediaProcess + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$UploadMediaProcessImplCopyWith<_$UploadMediaProcessImpl> get copyWith => + __$$UploadMediaProcessImplCopyWithImpl<_$UploadMediaProcessImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$UploadMediaProcessImplToJson( + this, + ); + } +} + +abstract class _UploadMediaProcess extends UploadMediaProcess { + const factory _UploadMediaProcess( + {required final String id, + required final String media_id, + required final int notification_id, + required final String folder_id, + required final MediaProvider provider, + required final String path, + final String? mime_type, + final MediaQueueProcessStatus status, + @LocalDatabaseBoolConverter() final bool upload_using_auto_backup, + @LocalDatabaseAppMediaConverter() final AppMedia? response, + final int total, + final int chunk}) = _$UploadMediaProcessImpl; + const _UploadMediaProcess._() : super._(); + + factory _UploadMediaProcess.fromJson(Map json) = + _$UploadMediaProcessImpl.fromJson; + + @override + String get id; + @override + String get media_id; + @override + int get notification_id; + @override + String get folder_id; + @override + MediaProvider get provider; + @override + String get path; + @override + String? get mime_type; + @override + MediaQueueProcessStatus get status; + @override + @LocalDatabaseBoolConverter() + bool get upload_using_auto_backup; + @override + @LocalDatabaseAppMediaConverter() + AppMedia? get response; + @override + int get total; + @override + int get chunk; + + /// Create a copy of UploadMediaProcess + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$UploadMediaProcessImplCopyWith<_$UploadMediaProcessImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/data/lib/models/media_process/media_process.g.dart b/data/lib/models/media_process/media_process.g.dart new file mode 100644 index 0000000..768482e --- /dev/null +++ b/data/lib/models/media_process/media_process.g.dart @@ -0,0 +1,100 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'media_process.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$DownloadMediaProcessImpl _$$DownloadMediaProcessImplFromJson( + Map json) => + _$DownloadMediaProcessImpl( + id: json['id'] as String, + name: json['name'] as String, + media_id: json['media_id'] as String, + folder_id: json['folder_id'] as String, + notification_id: (json['notification_id'] as num).toInt(), + provider: $enumDecode(_$MediaProviderEnumMap, json['provider']), + status: $enumDecodeNullable( + _$MediaQueueProcessStatusEnumMap, json['status']) ?? + MediaQueueProcessStatus.waiting, + response: const LocalDatabaseAppMediaConverter() + .fromJson(json['response'] as String?), + total: (json['total'] as num?)?.toInt() ?? 1, + extension: json['extension'] as String, + chunk: (json['chunk'] as num?)?.toInt() ?? 0, + ); + +Map _$$DownloadMediaProcessImplToJson( + _$DownloadMediaProcessImpl instance) => + { + 'id': instance.id, + 'name': instance.name, + 'media_id': instance.media_id, + 'folder_id': instance.folder_id, + 'notification_id': instance.notification_id, + 'provider': _$MediaProviderEnumMap[instance.provider]!, + 'status': _$MediaQueueProcessStatusEnumMap[instance.status]!, + 'response': + const LocalDatabaseAppMediaConverter().toJson(instance.response), + 'total': instance.total, + 'extension': instance.extension, + 'chunk': instance.chunk, + }; + +const _$MediaProviderEnumMap = { + MediaProvider.googleDrive: 'google-drive', + MediaProvider.dropbox: 'dropbox', +}; + +const _$MediaQueueProcessStatusEnumMap = { + MediaQueueProcessStatus.waiting: 'waiting', + MediaQueueProcessStatus.uploading: 'uploading', + MediaQueueProcessStatus.deleting: 'deleting', + MediaQueueProcessStatus.downloading: 'downloading', + MediaQueueProcessStatus.completed: 'completed', + MediaQueueProcessStatus.terminated: 'terminated', + MediaQueueProcessStatus.failed: 'failed', +}; + +_$UploadMediaProcessImpl _$$UploadMediaProcessImplFromJson( + Map json) => + _$UploadMediaProcessImpl( + id: json['id'] as String, + media_id: json['media_id'] as String, + notification_id: (json['notification_id'] as num).toInt(), + folder_id: json['folder_id'] as String, + provider: $enumDecode(_$MediaProviderEnumMap, json['provider']), + path: json['path'] as String, + mime_type: json['mime_type'] as String?, + status: $enumDecodeNullable( + _$MediaQueueProcessStatusEnumMap, json['status']) ?? + MediaQueueProcessStatus.waiting, + upload_using_auto_backup: json['upload_using_auto_backup'] == null + ? false + : const LocalDatabaseBoolConverter() + .fromJson((json['upload_using_auto_backup'] as num).toInt()), + response: const LocalDatabaseAppMediaConverter() + .fromJson(json['response'] as String?), + total: (json['total'] as num?)?.toInt() ?? 1, + chunk: (json['chunk'] as num?)?.toInt() ?? 0, + ); + +Map _$$UploadMediaProcessImplToJson( + _$UploadMediaProcessImpl instance) => + { + 'id': instance.id, + 'media_id': instance.media_id, + 'notification_id': instance.notification_id, + 'folder_id': instance.folder_id, + 'provider': _$MediaProviderEnumMap[instance.provider]!, + 'path': instance.path, + 'mime_type': instance.mime_type, + 'status': _$MediaQueueProcessStatusEnumMap[instance.status]!, + 'upload_using_auto_backup': const LocalDatabaseBoolConverter() + .toJson(instance.upload_using_auto_backup), + 'response': + const LocalDatabaseAppMediaConverter().toJson(instance.response), + 'total': instance.total, + 'chunk': instance.chunk, + }; diff --git a/data/lib/models/token/token.dart b/data/lib/models/token/token.dart deleted file mode 100644 index 3a48909..0000000 --- a/data/lib/models/token/token.dart +++ /dev/null @@ -1,40 +0,0 @@ -// ignore_for_file: non_constant_identifier_names - -import '../../extensions/date_time_extension.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'token.freezed.dart'; -part 'token.g.dart'; - -class ExpiresInJsonConverter implements JsonConverter { - const ExpiresInJsonConverter(); - - @override - DateTime fromJson(int json) { - final date = DateTime.fromMillisecondsSinceEpoch( - DateTime.now().millisecondsSinceEpoch + (json * 1000), - ); - return date; - } - - @override - int toJson(DateTime dateTime) { - return dateTime.secondsSinceEpoch; - } -} - -@freezed -abstract class DropboxToken with _$DropboxToken { - const factory DropboxToken({ - required String access_token, - required String token_type, - @ExpiresInJsonConverter() required DateTime expires_in, - required String refresh_token, - required String account_id, - required String scope, - required String uid, - }) = _DropboxToken; - - factory DropboxToken.fromJson(Map json) => - _$DropboxTokenFromJson(json); -} diff --git a/data/lib/repositories/google_drive_process_repo.dart b/data/lib/repositories/google_drive_process_repo.dart deleted file mode 100644 index 8a4f3bc..0000000 --- a/data/lib/repositories/google_drive_process_repo.dart +++ /dev/null @@ -1,353 +0,0 @@ -import 'dart:async'; -import 'dart:io'; -import 'package:collection/collection.dart'; -import '../extensions/iterable_extension.dart'; -import '../models/app_process/app_process.dart'; -import '../models/media/media_extension.dart'; -import '../services/google_drive_service.dart'; -import '../services/local_media_service.dart'; -import 'package:dio/dio.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:path_provider/path_provider.dart'; -import '../errors/app_error.dart'; -import '../models/media/media.dart'; - -final googleDriveProcessRepoProvider = Provider((ref) { - return GoogleDriveProcessRepo( - ref.read(googleDriveServiceProvider), - ref.read(localMediaServiceProvider), - ); -}); - -class GoogleDriveProcessRepo extends ChangeNotifier { - final GoogleDriveService _googleDriveService; - final LocalMediaService _localMediaService; - - final List _uploadQueue = []; - final List _deleteQueue = []; - final List _downloadQueue = []; - - bool _uploadQueueRunning = false; - bool _deleteQueueRunning = false; - bool _downloadQueueRunning = false; - - List get uploadQueue => _uploadQueue; - - List get deleteQueue => _deleteQueue; - - List get downloadQueue => _downloadQueue; - - String? _backUpFolderID; - - GoogleDriveProcessRepo(this._googleDriveService, this._localMediaService); - - void setBackUpFolderId(String? backUpFolderId) async { - _backUpFolderID = backUpFolderId; - } - - Future autoBackInGoogleDrive() async { - final localMedias = await _localMediaService.getAllLocalMedia(); - - final dgMedias = await _googleDriveService.getDriveMedias( - backUpFolderId: _backUpFolderID!, - ); - - for (AppMedia localMedia in localMedias.toList()) { - if (_uploadQueue - .where((element) => element.id == localMedia.id) - .isNotEmpty || - dgMedias - .where((gdMedia) => gdMedia.path == localMedia.id) - .isNotEmpty) { - localMedias.removeWhere((media) => media.id == localMedia.id); - } - } - - _uploadQueue.addAll( - localMedias.map( - (media) => AppProcess( - isFromAutoBackup: true, - id: media.id, - media: media, - status: AppProcessStatus.waiting, - ), - ), - ); - notifyListeners(); - if (!_uploadQueueRunning) _startUploadQueueLoop(); - } - - Future uploadMedia(List medias) async { - _uploadQueue.addAll( - medias.map( - (media) => AppProcess( - isFromAutoBackup: false, - id: media.id, - media: media, - status: AppProcessStatus.waiting, - ), - ), - ); - notifyListeners(); - if (!_uploadQueueRunning) _startUploadQueueLoop(); - } - - Future _startUploadQueueLoop() async { - _uploadQueueRunning = true; - while (_uploadQueue.firstOrNull != null) { - await _uploadInGoogleDrive(_uploadQueue[0]); - _uploadQueue.removeAt(0); - notifyListeners(); - } - _uploadQueueRunning = false; - } - - Future _uploadInGoogleDrive(AppProcess process) async { - try { - _uploadQueue.updateWhere( - where: (element) => element.id == process.id, - update: (element) => - element.copyWith(status: AppProcessStatus.uploading), - ); - notifyListeners(); - - _backUpFolderID ??= await _googleDriveService.getBackupFolderId(); - - final cancelToken = CancelToken(); - - final res = await _googleDriveService.uploadInGoogleDrive( - folderID: _backUpFolderID!, - media: process.media, - onProgress: (chunk, total) { - if (_uploadQueue - .firstWhereOrNull((element) => element.id == process.id) - ?.status - .isTerminated ?? - true) { - cancelToken.cancel(); - } - _uploadQueue.updateWhere( - where: (element) => element.id == process.id, - update: (element) => element.copyWith( - progress: AppProcessProgress(total: total, chunk: chunk), - ), - ); - notifyListeners(); - }, - cancelToken: cancelToken, - ); - _uploadQueue.updateWhere( - where: (element) => element.id == process.id, - update: (element) => element.copyWith( - status: AppProcessStatus.success, - response: res, - ), - ); - } catch (error) { - if (error is RequestCancelledByUser) { - return; - } else if (error is BackUpFolderNotFound) { - _backUpFolderID = await _googleDriveService.getBackupFolderId(); - _uploadInGoogleDrive(process); - return; - } - _uploadQueue.updateWhere( - where: (element) => element.id == process.id, - update: (element) => element.copyWith(status: AppProcessStatus.failed), - ); - } finally { - notifyListeners(); - } - } - - void deleteMediasFromGoogleDrive({required List medias}) { - _deleteQueue.addAll( - medias.map( - (media) => AppProcess( - id: media.id, - media: media, - status: AppProcessStatus.waiting, - ), - ), - ); - notifyListeners(); - if (!_deleteQueueRunning) _startDeleteQueueLoop(); - } - - Future _startDeleteQueueLoop() async { - _deleteQueueRunning = true; - while (_deleteQueue.firstOrNull != null) { - await _deleteFromGoogleDrive(_deleteQueue[0]); - _deleteQueue.removeAt(0); - notifyListeners(); - } - _deleteQueueRunning = false; - } - - Future _deleteFromGoogleDrive(AppProcess process) async { - try { - _deleteQueue.updateWhere( - where: (element) => element.id == process.id, - update: (element) => - element.copyWith(status: AppProcessStatus.deleting), - ); - notifyListeners(); - await _googleDriveService.deleteMedia(process.media.driveMediaRefId!); - _deleteQueue.updateWhere( - where: (element) => element.id == process.id, - update: (element) => element.copyWith(status: AppProcessStatus.success), - ); - } catch (error) { - _deleteQueue.updateWhere( - where: (element) => element.id == process.id, - update: (element) => element.copyWith(status: AppProcessStatus.failed), - ); - } finally { - notifyListeners(); - } - } - - void downloadMediasFromGoogleDrive({required List medias}) { - _downloadQueue.addAll( - medias.map( - (media) => AppProcess( - id: media.id, - media: media, - status: AppProcessStatus.waiting, - ), - ), - ); - notifyListeners(); - if (!_downloadQueueRunning) _startDownloadQueueLoop(); - } - - Future _startDownloadQueueLoop() async { - _downloadQueueRunning = true; - while (_downloadQueue.firstOrNull != null) { - await _downloadFromGoogleDrive(_downloadQueue[0]); - _downloadQueue.removeAt(0); - notifyListeners(); - } - _downloadQueueRunning = false; - } - - Future _downloadFromGoogleDrive(AppProcess process) async { - String? tempFileLocation; - try { - _downloadQueue.updateWhere( - where: (element) => element.id == process.id, - update: (element) => - element.copyWith(status: AppProcessStatus.downloading), - ); - notifyListeners(); - - final tempDir = await getTemporaryDirectory(); - tempFileLocation = - "${tempDir.path}/${process.media.id}.${process.media.extension}"; - - final cancelToken = CancelToken(); - - await _googleDriveService.downloadFromGoogleDrive( - id: process.media.driveMediaRefId!, - saveLocation: tempFileLocation, - onProgress: (received, total) { - if (_downloadQueue - .firstWhereOrNull((element) => element.id == process.id) - ?.status - .isTerminated ?? - true) { - cancelToken.cancel(); - } - _downloadQueue.updateWhere( - where: (element) => element.id == process.id, - update: (element) => element.copyWith( - progress: AppProcessProgress(total: total, chunk: received), - ), - ); - notifyListeners(); - }, - cancelToken: cancelToken, - ); - - final localMedia = await _localMediaService.saveInGallery( - saveFromLocation: tempFileLocation, - type: process.media.type, - ); - - if (localMedia == null) { - throw const UnableToSaveFileInGallery(); - } - - final updatedMedia = await _googleDriveService.updateMediaDescription( - process.media.id, - localMedia.id, - ); - - _downloadQueue.updateWhere( - where: (element) => element.id == process.id, - update: (element) => element.copyWith( - status: AppProcessStatus.success, - response: localMedia.mergeGoogleDriveMedia(updatedMedia), - ), - ); - } catch (error) { - if (error is RequestCancelledByUser) { - return; - } - _downloadQueue.updateWhere( - where: (element) => element.id == process.id, - update: (element) => element.copyWith(status: AppProcessStatus.failed), - ); - } finally { - notifyListeners(); - if (tempFileLocation != null) { - await File(tempFileLocation).delete(); - } - } - } - - void clearAllQueue() { - _uploadQueue.clear(); - _deleteQueue.clear(); - _downloadQueue.clear(); - notifyListeners(); - } - - void terminateUploadProcess(String id) { - final previousStatus = - _uploadQueue.firstWhereOrNull((element) => element.id == id)?.status; - if (previousStatus?.isProcessing ?? false) { - _uploadQueue.updateWhere( - where: (element) => element.id == id, - update: (element) => - element.copyWith(status: AppProcessStatus.terminated), - ); - } else if (previousStatus?.isWaiting ?? false) { - _uploadQueue.removeWhere((element) => element.id == id); - } - - notifyListeners(); - } - - void terminateAllAutoBackupProcess() { - _uploadQueue.removeWhere( - (element) => element.isFromAutoBackup && element.status.isWaiting, - ); - notifyListeners(); - } - - void terminateDeleteProcess(String id) { - _deleteQueue.removeWhere((element) => element.id == id); - notifyListeners(); - } - - void terminateDownloadProcess(String id) { - _downloadQueue.updateWhere( - where: (element) => element.id == id, - update: (element) => - element.copyWith(status: AppProcessStatus.terminated), - ); - notifyListeners(); - } -} diff --git a/data/lib/repositories/media_process_repository.dart b/data/lib/repositories/media_process_repository.dart new file mode 100644 index 0000000..bc3c69c --- /dev/null +++ b/data/lib/repositories/media_process_repository.dart @@ -0,0 +1,952 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:math' as math; +import 'package:collection/collection.dart'; +import 'package:dio/dio.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:sqflite/sqflite.dart'; +import '../domain/config.dart'; +import '../domain/formatters/byte_formatter.dart'; +import '../errors/app_error.dart'; +import '../handlers/notification_handler.dart'; +import '../models/media/media.dart'; +import '../models/media/media_extension.dart'; +import '../models/media_process/media_process.dart'; +import '../services/dropbox_services.dart'; +import '../services/google_drive_service.dart'; +import '../services/local_media_service.dart'; +import '../storage/app_preferences.dart'; + +final mediaProcessRepoProvider = Provider((ref) { + final repo = MediaProcessRepo( + ref.read(googleDriveServiceProvider), + ref.read(dropboxServiceProvider), + ref.read(localMediaServiceProvider), + ref.read(notificationHandlerProvider), + ref.read(AppPreferences.notifications), + ); + ref.onDispose(repo.dispose); + ref.listen( + AppPreferences.notifications, + (previous, next) { + repo.updateShowNotification(next); + }, + ); + + return repo; +}); + +class LocalDatabaseConstants { + static const String databaseName = 'cloud-gallery.db'; + static const String uploadQueueTable = 'UploadQueue'; + static const String downloadQueueTable = 'DownloadQueue'; +} + +class ProcessNotificationConstants { + static const String uploadProcessGroupIdentifier = + 'cloud_gallery_upload_process'; + static const String downloadProcessGroupIdentifier = + 'cloud_gallery_download_process'; +} + +class MediaProcessRepo extends ChangeNotifier { + final GoogleDriveService _googleDriveService; + final DropboxService _dropboxService; + final LocalMediaService _localMediaService; + final NotificationHandler _notificationHandler; + bool _showNotification; + + late Database database; + + List _uploadQueue = []; + List _downloadQueue = []; + + List get uploadQueue => _uploadQueue; + + List get downloadQueue => _downloadQueue; + + MediaProcessRepo( + this._googleDriveService, + this._dropboxService, + this._localMediaService, + this._notificationHandler, + this._showNotification, + ) { + initializeLocalDatabase(); + } + + void updateShowNotification(bool showNotification) { + _showNotification = showNotification; + + if (!showNotification) { + _notificationHandler.cancelAllNotification(); + } + } + + // DATABASE COMMON OPERATIONS ------------------------------------------------ + + Future initializeLocalDatabase() async { + database = await openDatabase( + LocalDatabaseConstants.databaseName, + version: 1, + onCreate: (Database db, int version) async { + await db.execute( + 'CREATE TABLE ${LocalDatabaseConstants.uploadQueueTable} (' + 'id TEXT PRIMARY KEY, ' + 'media_id TEXT NOT NULL, ' + 'folder_id TEXT NOT NULL, ' + 'provider TEXT NOT NULL, ' + 'path TEXT NOT NULL, ' + 'status TEXT NOT NULL, ' + 'upload_using_auto_backup INTEGER NOT NULL, ' + 'notification_id INTEGER NOT NULL, ' + 'response TEXT, ' + 'mime_type TEXT, ' + 'total INTEGER NOT NULL, ' + 'chunk INTEGER NOT NULL' + ')', + ); + await db.execute( + 'CREATE TABLE ${LocalDatabaseConstants.downloadQueueTable} (' + 'id TEXT PRIMARY KEY, ' + 'media_id TEXT NOT NULL, ' + 'folder_id TEXT NOT NULL, ' + 'notification_id INTEGER NOT NULL, ' + 'name TEXT NOT NULL, ' + 'thumbnail TEXT, ' + 'provider TEXT NOT NULL, ' + 'status TEXT NOT NULL, ' + 'extension TEXT NOT NULL, ' + 'response TEXT, ' + 'total INTEGER NOT NULL, ' + 'chunk INTEGER NOT NULL' + ')', + ); + }, + onOpen: (Database db) async { + await updateQueue(db); + runAutoBackupQueue(); + }, + ); + } + + @override + Future dispose() async { + for (var element in _uploadQueue.where( + (element) => element.status.isRunning, + )) { + updateDownloadProcessStatus( + status: MediaQueueProcessStatus.failed, + id: element.id, + ); + } + + for (var element in _downloadQueue.where( + (element) => element.status.isRunning, + )) { + updateDownloadProcessStatus( + status: MediaQueueProcessStatus.failed, + id: element.id, + ); + } + await database.close(); + super.dispose(); + } + + Future updateQueue(Database db) async { + final res = await Future.wait([ + db.query(LocalDatabaseConstants.uploadQueueTable), + db.query(LocalDatabaseConstants.downloadQueueTable), + ]); + + _uploadQueue = res[0].map((e) => UploadMediaProcess.fromJson(e)).toList(); + _downloadQueue = + res[1].map((e) => DownloadMediaProcess.fromJson(e)).toList(); + + notifyListeners(); + } + + // AUTO BACKUP OPERATIONS ---------------------------------------------------- + + void autoBackupInGoogleDrive() async { + final backUpFolderId = await _googleDriveService.getBackUpFolderId(); + + if (backUpFolderId == null) { + throw BackUpFolderNotFound(); + } + + final res = await Future.wait([ + _localMediaService.getAllLocalMedia(), + _googleDriveService.getAllMedias(folder: backUpFolderId), + ]); + + final localMedias = res[0]; + final dgMedias = res[1]; + + for (AppMedia localMedia in localMedias.toList()) { + if (_uploadQueue + .where((element) => element.media_id == localMedia.id) + .isNotEmpty || + dgMedias + .where((gdMedia) => gdMedia.path == localMedia.id) + .isNotEmpty) { + localMedias.removeWhere((media) => media.id == localMedia.id); + } + } + + for (AppMedia media in localMedias) { + await database.insert( + LocalDatabaseConstants.uploadQueueTable, + UploadMediaProcess( + id: UniqueKey().toString(), + media_id: media.id, + folder_id: backUpFolderId, + provider: MediaProvider.googleDrive, + notification_id: _generateUniqueUploadNotificationId(), + path: media.path, + upload_using_auto_backup: true, + mime_type: media.mimeType, + ).toJson(), + ); + } + await updateQueue(database); + runAutoBackupQueue(); + } + + Future autoBackupInDropbox() async { + final res = await Future.wait([ + _localMediaService.getAllLocalMedia(), + _dropboxService.getAllMedias(folder: ProviderConstants.backupFolderPath), + ]); + + final localMedias = res[0]; + final dropboxMedias = res[1]; + + for (AppMedia localMedia in localMedias.toList()) { + if (_uploadQueue + .where((element) => element.media_id == localMedia.id) + .isNotEmpty || + dropboxMedias + .where((gdMedia) => gdMedia.path == localMedia.id) + .isNotEmpty) { + localMedias.removeWhere((media) => media.id == localMedia.id); + } + } + + for (AppMedia media in localMedias) { + await database.insert( + LocalDatabaseConstants.uploadQueueTable, + UploadMediaProcess( + id: UniqueKey().toString(), + media_id: media.id, + folder_id: ProviderConstants.backupFolderPath, + provider: MediaProvider.dropbox, + notification_id: _generateUniqueUploadNotificationId(), + path: media.path, + upload_using_auto_backup: true, + mime_type: media.mimeType, + ).toJson(), + ); + } + await updateQueue(database); + runAutoBackupQueue(); + } + + Future stopAutoBackup(MediaProvider provider) async { + final processes = _uploadQueue + .where( + (element) => + element.upload_using_auto_backup && + element.provider == provider && + element.status.isWaiting, + ) + .map((e) => e.id) + .toList(); + + if (processes.isNotEmpty) { + await database.delete( + LocalDatabaseConstants.uploadQueueTable, + where: 'id IN (${List.filled(processes.length, '?').join(',')})', + whereArgs: processes, + ); + } + + await updateQueue(database); + } + + bool _autoBackupQueueIsRunning = false; + + Future runAutoBackupQueue() async { + if (_autoBackupQueueIsRunning) return; + _autoBackupQueueIsRunning = true; + while (_uploadQueue.firstWhereOrNull( + (element) => + element.status.isWaiting && element.upload_using_auto_backup, + ) != + null) { + final process = _uploadQueue.firstWhere( + (element) => + element.status.isWaiting && element.upload_using_auto_backup, + ); + if (process.provider == MediaProvider.googleDrive) { + await _uploadInGoogleDrive(process); + } else if (process.provider == MediaProvider.dropbox) { + await _uploadInDropbox(process); + } else { + await updateUploadProcessStatus( + status: MediaQueueProcessStatus.failed, + id: process.id, + ); + } + } + _autoBackupQueueIsRunning = false; + } + + // UPLOAD QUEUE DATABASE OPERATIONS ------------------------------------------ + + int _generateUniqueUploadNotificationId() { + int baseId = math.Random().nextInt(9999999); + while (_uploadQueue.any((element) => element.notification_id == baseId)) { + baseId = math.Random().nextInt(9999999); + } + return baseId; + } + + void uploadMedia({ + required List medias, + required String folderId, + required MediaProvider provider, + }) async { + for (AppMedia media in medias) { + await database.insert( + LocalDatabaseConstants.uploadQueueTable, + UploadMediaProcess( + id: UniqueKey().toString(), + media_id: media.id, + folder_id: folderId, + provider: provider, + notification_id: _generateUniqueUploadNotificationId(), + path: media.path, + upload_using_auto_backup: false, + mime_type: media.mimeType, + ).toJson(), + ); + } + await updateQueue(database); + runUploadQueue(); + } + + Future updateUploadProcessStatus({ + required MediaQueueProcessStatus status, + required String id, + AppMedia? response, + }) async { + await database.rawUpdate( + "UPDATE ${LocalDatabaseConstants.uploadQueueTable} SET status = ?, response = ? WHERE id = ?", + [status.value, LocalDatabaseAppMediaConverter().toJson(response), id], + ); + await updateQueue(database); + } + + Future clearUploadProcessResponse({ + required String id, + }) async { + await database.rawUpdate( + "UPDATE ${LocalDatabaseConstants.uploadQueueTable} SET response = ? WHERE id = ?", + [null, id], + ); + await updateQueue(database); + } + + Future updateUploadProcessProgress({ + required String id, + required int chunk, + required int total, + }) async { + await database.rawUpdate( + "UPDATE ${LocalDatabaseConstants.uploadQueueTable} SET chunk = ?, total = ? WHERE id = ?", + [chunk, total, id], + ); + await updateQueue(database); + } + + Future terminateUploadProcess(String id) async { + await database.rawUpdate( + "UPDATE ${LocalDatabaseConstants.uploadQueueTable} SET status = ? WHERE id = ? AND (status = ? OR status = ?)", + [ + MediaQueueProcessStatus.terminated.value, + id, + MediaQueueProcessStatus.uploading.value, + MediaQueueProcessStatus.waiting.value, + ], + ); + await updateQueue(database); + } + + Future removeAllWaitingUploadsOfProvider(MediaProvider provider) async { + await database.rawDelete( + "DELETE FROM ${LocalDatabaseConstants.uploadQueueTable} WHERE provider = ? AND status = ?", + [MediaProvider.googleDrive.value, MediaQueueProcessStatus.waiting.value], + ); + await updateQueue(database); + } + + Future removeItemFromUploadQueue(String id) async { + await database.rawDelete( + "DELETE FROM ${LocalDatabaseConstants.uploadQueueTable} WHERE id = ?", + [id], + ); + await updateQueue(database); + } + + // UPLOAD QUEUE OPERATIONS --------------------------------------------------- + + Future runUploadQueue() async { + for (UploadMediaProcess process in _uploadQueue.where( + (element) => + element.status.isWaiting && !element.upload_using_auto_backup, + )) { + if (process.provider == MediaProvider.googleDrive) { + _uploadInGoogleDrive(process); + } else if (process.provider == MediaProvider.dropbox) { + _uploadInDropbox(process); + } else { + updateUploadProcessStatus( + status: MediaQueueProcessStatus.failed, + id: process.id, + ); + } + } + } + + Future _uploadInGoogleDrive(UploadMediaProcess uploadProcess) async { + UploadMediaProcess process = uploadProcess; + Timer? updateDatabaseDebounce; + + Future showNotification( + String message, { + int? chunk, + int total = 100, + }) async { + if (!_showNotification) return; + _notificationHandler.showNotification( + silent: true, + id: process.notification_id, + name: process.path.split('/').last, + description: message, + groupKey: ProcessNotificationConstants.uploadProcessGroupIdentifier, + progress: chunk, + maxProgress: total, + category: chunk != null ? AndroidNotificationCategory.progress : null, + ); + } + + try { + showNotification('Uploading to Google Drive'); + + await updateUploadProcessStatus( + status: MediaQueueProcessStatus.uploading, + id: process.id, + ); + + final cancelToken = CancelToken(); + + final res = await _googleDriveService.uploadMedia( + folderId: process.folder_id, + path: process.path, + mimeType: process.mime_type, + localRefId: process.media_id, + onProgress: (chunk, total) async { + process = + _uploadQueue.firstWhere((element) => element.id == process.id); + if (process.status.isTerminated) { + cancelToken.cancel(); + } + + if (updateDatabaseDebounce == null || + !updateDatabaseDebounce!.isActive) { + updateDatabaseDebounce = Timer(Duration(milliseconds: 300), () {}); + + if (!process.status.isTerminated) { + showNotification( + '${chunk.formatBytes} / ${total.formatBytes} - ${total <= 0 ? 0 : (chunk / total * 100).round()}%', + chunk: chunk, + total: total, + ); + } + + await updateUploadProcessProgress( + id: process.id, + chunk: chunk, + total: total, + ); + } + }, + cancelToken: cancelToken, + ); + + showNotification('Uploaded to Google Drive successfully'); + + await updateUploadProcessStatus( + status: MediaQueueProcessStatus.completed, + response: res, + id: process.id, + ); + + await clearUploadProcessResponse(id: process.id); + } catch (e) { + if (e is RequestCancelledByUser) { + showNotification('Upload to Google Drive cancelled'); + return; + } + + showNotification('Failed to upload to Google Drive'); + + await updateDownloadProcessStatus( + status: MediaQueueProcessStatus.failed, + id: process.id, + ); + } + } + + Future _uploadInDropbox(UploadMediaProcess uploadProcess) async { + UploadMediaProcess process = uploadProcess; + Timer? updateDatabaseDebounce; + + Future showNotification( + String message, { + int? chunk, + int total = 100, + }) async { + if (!_showNotification) return; + _notificationHandler.showNotification( + silent: true, + id: process.notification_id, + name: process.path.split('/').last, + description: message, + groupKey: ProcessNotificationConstants.uploadProcessGroupIdentifier, + progress: chunk, + maxProgress: total, + category: chunk != null ? AndroidNotificationCategory.progress : null, + ); + } + + try { + showNotification('Uploading to Dropbox'); + + await updateUploadProcessStatus( + status: MediaQueueProcessStatus.uploading, + id: process.id, + ); + + final cancelToken = CancelToken(); + + final res = await _dropboxService.uploadMedia( + folderId: process.folder_id, + path: process.path, + mimeType: process.mime_type, + localRefId: process.media_id, + onProgress: (chunk, total) async { + process = + _uploadQueue.firstWhere((element) => element.id == process.id); + if (process.status.isTerminated) { + cancelToken.cancel(); + } + if (updateDatabaseDebounce == null || + !updateDatabaseDebounce!.isActive) { + updateDatabaseDebounce = Timer(Duration(milliseconds: 300), () {}); + + if (!process.status.isTerminated) { + showNotification( + '${chunk.formatBytes} / ${total.formatBytes} - ${total <= 0 ? 0 : (chunk / total * 100).round()}%', + chunk: chunk, + total: total, + ); + } + + await updateUploadProcessProgress( + id: process.id, + chunk: chunk, + total: total, + ); + } + }, + cancelToken: cancelToken, + ); + + showNotification('Uploaded to Dropbox successfully'); + + await updateUploadProcessStatus( + status: MediaQueueProcessStatus.completed, + response: res, + id: process.id, + ); + + await clearUploadProcessResponse(id: process.id); + } catch (e) { + if (e is RequestCancelledByUser) { + showNotification('Upload to Dropbox cancelled'); + return; + } + + showNotification('Failed to upload to Dropbox'); + + await updateUploadProcessStatus( + status: MediaQueueProcessStatus.failed, + id: process.id, + ); + return; + } + } + + // DOWNLOAD QUEUE DATABASE OPERATIONS ---------------------------------------- + + int _generateUniqueDownloadNotificationId() { + int baseId = math.Random().nextInt(9999999); + while (_downloadQueue.any((element) => element.notification_id == baseId)) { + baseId = math.Random().nextInt(9999999); + } + return baseId; + } + + void downloadMedia({ + required List medias, + required String folderId, + required MediaProvider provider, + }) async { + for (AppMedia media in medias) { + final id = provider == MediaProvider.googleDrive + ? media.driveMediaRefId + : media.dropboxMediaRefId; + await database.insert( + LocalDatabaseConstants.downloadQueueTable, + DownloadMediaProcess( + name: media.path.split('/').last.trim().isEmpty + ? media.id + : media.path.split('/').last, + id: UniqueKey().toString(), + media_id: id ?? media.id, + folder_id: folderId, + notification_id: _generateUniqueDownloadNotificationId(), + provider: provider, + extension: media.extension, + ).toJson(), + ); + } + await updateQueue(database); + runDownloadQueue(); + } + + Future updateDownloadProcessStatus({ + required MediaQueueProcessStatus status, + required String id, + AppMedia? response, + }) async { + await database.rawUpdate( + "UPDATE ${LocalDatabaseConstants.downloadQueueTable} SET status = ?, response = ? WHERE id = ?", + [status.value, LocalDatabaseAppMediaConverter().toJson(response), id], + ); + await updateQueue(database); + } + + Future clearDownloadProcessResponse({ + required String id, + }) async { + await database.rawUpdate( + "UPDATE ${LocalDatabaseConstants.downloadQueueTable} SET response = ? WHERE id = ?", + [null, id], + ); + await updateQueue(database); + } + + Future updateDownloadProcessProgress({ + required String id, + required int received, + required int total, + }) async { + await database.rawUpdate( + "UPDATE ${LocalDatabaseConstants.downloadQueueTable} SET chunk = ?, total = ? WHERE id = ?", + [received, total, id], + ); + await updateQueue(database); + } + + Future terminateDownloadProcess(String id) async { + await database.rawUpdate( + "UPDATE ${LocalDatabaseConstants.downloadQueueTable} SET status = ? WHERE id = ? AND (status = ? OR status = ?)", + [ + MediaQueueProcessStatus.terminated.value, + id, + MediaQueueProcessStatus.downloading.value, + MediaQueueProcessStatus.waiting.value, + ], + ); + await updateQueue(database); + } + + Future removeItemFromDownloadQueue(String id) async { + await database.rawDelete( + "DELETE FROM ${LocalDatabaseConstants.downloadQueueTable} WHERE id = ?", + [id], + ); + await updateQueue(database); + } + + // DOWNLOAD QUEUE OPERATIONS ------------------------------------------------- + + Future runDownloadQueue() async { + for (final process + in _downloadQueue.where((element) => element.status.isWaiting)) { + if (process.provider == MediaProvider.googleDrive) { + _downloadFromGoogleDrive(process); + } else if (process.provider == MediaProvider.dropbox) { + _downloadFromDropbox(process); + } else { + updateDownloadProcessStatus( + status: MediaQueueProcessStatus.failed, + id: process.id, + ); + } + } + } + + Future _downloadFromGoogleDrive( + DownloadMediaProcess downloadProcess, + ) async { + DownloadMediaProcess process = downloadProcess; + String? tempFileLocation; + Timer? updateDatabaseDebounce; + + Future showNotification( + String message, { + int? chunk, + int total = 100, + }) async { + if (!_showNotification) return; + _notificationHandler.showNotification( + silent: true, + id: process.notification_id, + name: process.media_id, + description: message, + groupKey: ProcessNotificationConstants.downloadProcessGroupIdentifier, + progress: chunk, + maxProgress: total, + category: chunk != null ? AndroidNotificationCategory.progress : null, + ); + } + + try { + showNotification('Downloading from Google Drive'); + + await updateDownloadProcessStatus( + status: MediaQueueProcessStatus.downloading, + id: process.id, + ); + + final tempDir = await getTemporaryDirectory(); + tempFileLocation = + "${tempDir.path}/${process.media_id}.${process.extension}"; + + final cancelToken = CancelToken(); + + await _googleDriveService.downloadMedia( + id: process.media_id, + saveLocation: tempFileLocation, + onProgress: (received, total) async { + process = + _downloadQueue.firstWhere((element) => element.id == process.id); + if (process.status.isTerminated) { + cancelToken.cancel(); + } + + if (updateDatabaseDebounce == null || + !updateDatabaseDebounce!.isActive) { + updateDatabaseDebounce = Timer(Duration(milliseconds: 300), () {}); + + if (!process.status.isTerminated) { + showNotification( + '${received.formatBytes} / ${total.formatBytes} - ${total <= 0 ? 0 : (received / total * 100).round()}%', + chunk: received, + total: total, + ); + } + + await updateDownloadProcessProgress( + id: process.id, + received: received, + total: total, + ); + } + }, + cancelToken: cancelToken, + ); + + final localMedia = await _localMediaService.saveInGallery( + saveFromLocation: tempFileLocation, + type: AppMediaType.fromLocation(location: tempFileLocation), + ); + + if (localMedia == null) { + showNotification('Failed to save media in gallery'); + + await updateDownloadProcessStatus( + status: MediaQueueProcessStatus.failed, + id: process.id, + ); + return; + } + + await _googleDriveService.updateAppProperties( + id: process.media_id, + localRefId: localMedia.id, + ); + + showNotification('Downloaded from Google Drive successfully'); + + await updateDownloadProcessStatus( + status: MediaQueueProcessStatus.completed, + response: localMedia, + id: process.id, + ); + + await clearDownloadProcessResponse(id: process.id); + } catch (error) { + if (error is RequestCancelledByUser) { + showNotification('Download from Google Drive cancelled'); + return; + } + + showNotification('Failed to download from Google Drive'); + + await updateDownloadProcessStatus( + status: MediaQueueProcessStatus.failed, + id: process.id, + ); + } finally { + if (tempFileLocation != null && await File(tempFileLocation).exists()) { + await File(tempFileLocation).delete(); + } + } + } + + Future _downloadFromDropbox( + DownloadMediaProcess downloadProcess, + ) async { + DownloadMediaProcess process = downloadProcess; + String? tempFileLocation; + Timer? updateDatabaseDebounce; + + Future showNotification( + String message, { + int? chunk, + int total = 100, + }) async { + if (!_showNotification) return; + _notificationHandler.showNotification( + silent: true, + id: process.notification_id, + name: process.media_id, + description: message, + groupKey: ProcessNotificationConstants.downloadProcessGroupIdentifier, + progress: chunk, + maxProgress: total, + category: chunk != null ? AndroidNotificationCategory.progress : null, + ); + } + + try { + showNotification('Downloading from Dropbox'); + + await updateDownloadProcessStatus( + status: MediaQueueProcessStatus.downloading, + id: process.id, + ); + + final tempDir = await getTemporaryDirectory(); + tempFileLocation = + "${tempDir.path}/${process.media_id}.${process.extension}"; + + final cancelToken = CancelToken(); + + await _dropboxService.downloadMedia( + id: process.media_id, + saveLocation: tempFileLocation, + onProgress: (received, total) async { + process = + _downloadQueue.firstWhere((element) => element.id == process.id); + if (process.status.isTerminated) { + cancelToken.cancel(); + } + + if (updateDatabaseDebounce == null || + !updateDatabaseDebounce!.isActive) { + updateDatabaseDebounce = Timer(Duration(milliseconds: 300), () {}); + + if (!process.status.isTerminated) { + showNotification( + '${received.formatBytes} / ${total.formatBytes} - ${total <= 0 ? 0 : (received / total * 100).round()}%', + chunk: received, + total: total, + ); + } + + await updateDownloadProcessProgress( + id: process.id, + received: received, + total: total, + ); + } + }, + cancelToken: cancelToken, + ); + + final localMedia = await _localMediaService.saveInGallery( + saveFromLocation: tempFileLocation, + type: AppMediaType.fromLocation(location: tempFileLocation), + ); + + if (localMedia == null) { + showNotification('Failed to save media in gallery'); + + await updateDownloadProcessStatus( + status: MediaQueueProcessStatus.failed, + id: process.id, + ); + } + + await _dropboxService.updateAppProperties( + id: process.media_id, + localRefId: localMedia!.id, + ); + + showNotification('Downloaded from Dropbox successfully'); + + await updateDownloadProcessStatus( + status: MediaQueueProcessStatus.completed, + response: localMedia, + id: process.id, + ); + + await clearDownloadProcessResponse(id: process.id); + } catch (error) { + if (error is RequestCancelledByUser) { + showNotification('Download from Dropbox cancelled'); + return; + } + + showNotification('Failed to download from Dropbox'); + + await updateDownloadProcessStatus( + status: MediaQueueProcessStatus.failed, + id: process.id, + ); + } finally { + if (tempFileLocation != null && await File(tempFileLocation).exists()) { + await File(tempFileLocation).delete(); + } + } + } +} diff --git a/data/lib/services/auth_service.dart b/data/lib/services/auth_service.dart index 85e5f05..ce93618 100644 --- a/data/lib/services/auth_service.dart +++ b/data/lib/services/auth_service.dart @@ -2,7 +2,8 @@ import 'dart:async'; import '../apis/network/client.dart'; import '../apis/network/oauth2.dart'; import '../errors/app_error.dart'; -import '../models/dropbox_account/dropbox_account.dart'; +import '../models/dropbox/account/dropbox_account.dart'; +import '../models/dropbox/token/dropbox_token.dart'; import '../storage/app_preferences.dart'; import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -12,7 +13,6 @@ import 'package:url_launcher/url_launcher.dart'; import '../apis/dropbox/dropbox_auth_endpoints.dart'; import '../apis/network/secrets.dart'; import '../apis/network/urls.dart'; -import '../models/token/token.dart'; import '../storage/provider/preferences_provider.dart'; final googleUserAccountProvider = StateProvider((ref) { @@ -43,6 +43,7 @@ final authServiceProvider = Provider( ref.read(AppPreferences.dropboxCurrentUserAccount.notifier), ref.read(AppPreferences.googleDriveAutoBackUp.notifier), ref.read(AppPreferences.dropboxAutoBackUp.notifier), + ref.read(AppPreferences.dropboxFileIdAppPropertyTemplateId.notifier), ), ); @@ -55,6 +56,8 @@ class AuthService { final PreferenceNotifier _dropboxCodeVerifierPrefProvider; final PreferenceNotifier _googleDriveAutoBackUpController; final PreferenceNotifier _dropboxAutoBackUpController; + final PreferenceNotifier + _dropboxFileIdAppPropertyTemplateIdController; AuthService( this._googleSignIn, @@ -65,6 +68,7 @@ class AuthService { this._dropboxAccountController, this._googleDriveAutoBackUpController, this._dropboxAutoBackUpController, + this._dropboxFileIdAppPropertyTemplateIdController, ) { signInSilently(); } @@ -134,7 +138,16 @@ class AuthService { ), ); if (res.data != null) { - _dropboxTokenController.state = DropboxToken.fromJson(res.data); + _dropboxTokenController.state = DropboxToken( + access_token: res.data['access_token'], + token_type: res.data['token_type'], + refresh_token: res.data['refresh_token'], + expires_in: + DateTime.now().add(Duration(seconds: res.data['expires_in'])), + account_id: res.data['account_id'], + scope: res.data['scope'], + uid: res.data['uid'], + ); _dropboxCodeVerifierPrefProvider.state = null; } } catch (e) { @@ -146,6 +159,7 @@ class AuthService { _dropboxTokenController.state = null; _dropboxAccountController.state = null; _dropboxAutoBackUpController.state = false; + _dropboxFileIdAppPropertyTemplateIdController.state = null; } Future refreshDropboxToken() async { @@ -158,11 +172,14 @@ class AuthService { clientSecret: AppSecretes.dropBoxAppSecret, ), ); - final newToken = DropboxToken.fromJson(res.data); _dropboxTokenController.state = _dropboxTokenController.state!.copyWith( - access_token: newToken.access_token, - expires_in: newToken.expires_in, - token_type: newToken.token_type, + access_token: res.data['access_token'], + expires_in: DateTime.now().add( + Duration( + seconds: res.data['expires_in'], + ), + ), + token_type: res.data['token_type'], ); } else { throw const AuthSessionExpiredError(); @@ -178,4 +195,6 @@ class AuthService { Stream get onGoogleAccountChange => _googleSignIn.onCurrentUserChanged; + + DropboxAccount? get dropboxAccount => _dropboxAccountController.state; } diff --git a/data/lib/services/cloud_provider_service.dart b/data/lib/services/cloud_provider_service.dart new file mode 100644 index 0000000..a1f82fa --- /dev/null +++ b/data/lib/services/cloud_provider_service.dart @@ -0,0 +1,49 @@ +import 'package:dio/dio.dart'; +import '../models/media/media.dart'; + +abstract class CloudProviderService { + const CloudProviderService(); + + Future createFolder(String folderName); + + Future uploadMedia({ + required String folderId, + required String path, + String? mimeType, + String? localRefId, + CancelToken? cancelToken, + void Function(int sent, int total)? onProgress, + }); + + Future downloadMedia({ + required String id, + required String saveLocation, + CancelToken? cancelToken, + void Function(int sent, int total)? onProgress, + }); + + Future deleteMedia({ + required String id, + CancelToken? cancelToken, + }); + + Future getPaginatedMedias({ + required String folder, + String? nextPageToken, + int pageSize = 30, + }); + + Future> getAllMedias({ + required String folder, + }); +} + +class GetPaginatedMediasResponse { + final List medias; + final String? nextPageToken; + + const GetPaginatedMediasResponse({ + required this.medias, + this.nextPageToken, + }); +} diff --git a/data/lib/services/dropbox_services.dart b/data/lib/services/dropbox_services.dart index 2861ce0..cec601a 100644 --- a/data/lib/services/dropbox_services.dart +++ b/data/lib/services/dropbox_services.dart @@ -1,26 +1,37 @@ +import 'dart:io'; +import 'package:collection/collection.dart'; +import '../apis/dropbox/dropbox_content_endpoints.dart'; import '../apis/network/client.dart'; +import '../domain/config.dart'; import '../errors/app_error.dart'; -import '../models/dropbox_account/dropbox_account.dart'; +import '../models/dropbox/account/dropbox_account.dart'; +import '../models/media/media.dart'; +import '../models/media_content/media_content.dart'; import '../storage/app_preferences.dart'; import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../apis/dropbox/dropbox_auth_endpoints.dart'; import '../storage/provider/preferences_provider.dart'; +import 'cloud_provider_service.dart'; final dropboxServiceProvider = Provider((ref) { return DropboxService( ref.read(dropboxAuthenticatedDioProvider), ref.read(AppPreferences.dropboxCurrentUserAccount.notifier), + ref.read(AppPreferences.dropboxFileIdAppPropertyTemplateId.notifier), ); }); -class DropboxService { +class DropboxService extends CloudProviderService { final Dio _dropboxAuthenticatedDio; final PreferenceNotifier _dropboxAccountController; + final PreferenceNotifier + _dropboxFileIdAppPropertyTemplateIdController; const DropboxService( this._dropboxAuthenticatedDio, this._dropboxAccountController, + this._dropboxFileIdAppPropertyTemplateIdController, ); Future setCurrentUserAccount() async { @@ -32,4 +43,290 @@ class DropboxService { AppError.fromError(e); } } + + Future setFileIdAppPropertyTemplate() async { + try { + // Get all the app property templates + final res = await _dropboxAuthenticatedDio + .req(const DropboxGetAppPropertyTemplate()); + + if (res.statusCode == 200) { + final templateIds = res.data['template_ids'] as List; + + // Find the template id for the app + String? appTemplateId; + for (final templateId in templateIds) { + final res = await _dropboxAuthenticatedDio.req( + DropboxGetAppPropertiesTemplateDetails(templateId), + ); + + if (res.statusCode == 200) { + if (res.data is Map && + res.data['name'] == ProviderConstants.dropboxAppTemplateName) { + appTemplateId = templateId; + break; + } + } + } + + // If the template id is found, set it else create a new one + if (appTemplateId != null) { + _dropboxFileIdAppPropertyTemplateIdController.state = appTemplateId; + } else { + final res = await _dropboxAuthenticatedDio.req( + DropboxCreateAppPropertyTemplate(), + ); + if (res.statusCode == 200) { + _dropboxFileIdAppPropertyTemplateIdController.state = + res.data['template_id']; + } + } + } + } catch (e) { + AppError.fromError(e); + } + } + + @override + Future> getAllMedias({ + required String folder, + }) async { + try { + if (_dropboxFileIdAppPropertyTemplateIdController.state == null) { + await setFileIdAppPropertyTemplate(); + } + bool hasMore = true; + String? nextPageToken; + final List medias = []; + + while (hasMore) { + final response = await _dropboxAuthenticatedDio.req( + nextPageToken == null + ? DropboxListFolderEndpoint( + folderPath: folder, + limit: 2000, + appPropertyTemplateId: + _dropboxFileIdAppPropertyTemplateIdController.state!, + ) + : DropboxListFolderContinueEndpoint(cursor: nextPageToken), + ); + if (response.statusCode == 200) { + hasMore = response.data['has_more'] == true; + nextPageToken = response.data['cursor']; + medias.addAll( + (response.data['entries'] as List) + .where((element) => element['.tag'] == 'file') + .map((e) => AppMedia.fromDropboxJson(json: e)) + .toList(), + ); + } else { + throw AppError.fromError(response.statusMessage ?? ''); + } + } + + return medias; + } catch (e) { + throw AppError.fromError(e); + } + } + + @override + Future getPaginatedMedias({ + required String folder, + String? nextPageToken, + int pageSize = 30, + }) async { + if (_dropboxFileIdAppPropertyTemplateIdController.state == null) { + await setFileIdAppPropertyTemplate(); + } + try { + final response = await _dropboxAuthenticatedDio.req( + nextPageToken == null + ? DropboxListFolderEndpoint( + folderPath: folder, + limit: pageSize, + appPropertyTemplateId: + _dropboxFileIdAppPropertyTemplateIdController.state!, + ) + : DropboxListFolderContinueEndpoint(cursor: nextPageToken), + ); + if (response.statusCode == 200) { + final files = (response.data['entries'] as List).where( + (element) => element['.tag'] == 'file', + ); + + final metadataResponses = await Future.wait( + files.map( + (e) => _dropboxAuthenticatedDio.req( + DropboxGetFileMetadata(id: e['id']), + ), + ), + ); + + return GetPaginatedMediasResponse( + medias: files + .map( + (e) => AppMedia.fromDropboxJson( + json: e, + metadataJson: metadataResponses + .firstWhereOrNull( + (m) => m.statusCode == 200 && m.data['id'] == e['id'], + ) + ?.data, + ), + ) + .toList(), + nextPageToken: response.data['has_more'] == true + ? response.data['cursor'] + : null, + ); + } + throw SomethingWentWrongError( + statusCode: response.statusCode, + message: response.statusMessage ?? '', + ); + } catch (e) { + if (e is DioException && + e.response?.statusCode == 409 && + e.response?.data?['error']?['path']?['.tag'] == 'not_found') { + await createFolder(ProviderConstants.backupFolderName); + return getPaginatedMedias( + folder: folder, + nextPageToken: nextPageToken, + pageSize: pageSize, + ); + } + throw AppError.fromError(e); + } + } + + @override + Future createFolder(String folderName) async { + final response = await _dropboxAuthenticatedDio.req( + DropboxCreateFolderEndpoint(name: folderName), + ); + + if (response.statusCode == 200) { + return response.data['metadata']['id']; + } + + throw AppError.fromError(response.statusMessage ?? ''); + } + + @override + Future uploadMedia({ + required String folderId, + required String path, + String? mimeType, + String? localRefId, + CancelToken? cancelToken, + void Function(int sent, int total)? onProgress, + }) async { + if (_dropboxFileIdAppPropertyTemplateIdController.state == null) { + await setFileIdAppPropertyTemplate(); + } + final localFile = File(path); + try { + final res = await _dropboxAuthenticatedDio.req( + DropboxUploadEndpoint( + appPropertyTemplateId: + _dropboxFileIdAppPropertyTemplateIdController.state!, + localRefId: localRefId, + content: AppMediaContent( + stream: localFile.openRead(), + length: localFile.lengthSync(), + contentType: 'application/octet-stream', + ), + filePath: + "/${ProviderConstants.backupFolderName}/${localFile.path.split('/').last}", + onProgress: onProgress, + cancellationToken: cancelToken, + ), + ); + final metadata = await _dropboxAuthenticatedDio.req( + DropboxGetFileMetadata(id: res.data['id']), + ); + + if (res.statusCode == 200) { + return AppMedia.fromDropboxJson( + json: res.data, + metadataJson: metadata.data, + ); + } + throw AppError.fromError(res.statusMessage ?? ''); + } catch (error) { + throw AppError.fromError(error); + } + } + + @override + Future downloadMedia({ + required String id, + required String saveLocation, + CancelToken? cancelToken, + void Function(int sent, int total)? onProgress, + }) async { + try { + await _dropboxAuthenticatedDio.downloadReq( + DropboxDownloadEndpoint( + filePath: id, + storagePath: saveLocation, + cancellationToken: cancelToken, + onProgress: onProgress, + ), + ); + } catch (e) { + throw AppError.fromError(e); + } + } + + Future updateAppProperties({ + required String id, + required String localRefId, + CancelToken? cancelToken, + }) async { + try { + await _dropboxAuthenticatedDio.req( + DropboxUpdateAppPropertyEndpoint( + id: id, + cancellationToken: cancelToken, + appPropertyTemplateId: + _dropboxFileIdAppPropertyTemplateIdController.state!, + localRefId: localRefId, + ), + ); + } catch (e) { + throw AppError.fromError(e); + } + } + + @override + Future deleteMedia({ + required String id, + CancelToken? cancelToken, + }) async { + try { + final res = await _dropboxAuthenticatedDio.req( + DropboxDeleteEndpoint( + id: id, + cancellationToken: cancelToken, + ), + ); + if (res.statusCode == 200) return; + + throw AppError.fromError(res.statusMessage ?? ''); + } catch (e) { + throw AppError.fromError(e); + } + } +} + +class DropboxMediaListResponse { + final List medias; + final String? cursor; + + const DropboxMediaListResponse({ + required this.medias, + required this.cursor, + }); } diff --git a/data/lib/services/google_drive_service.dart b/data/lib/services/google_drive_service.dart index c17b62c..caa6760 100644 --- a/data/lib/services/google_drive_service.dart +++ b/data/lib/services/google_drive_service.dart @@ -1,7 +1,9 @@ import 'dart:async'; + import 'dart:io'; import '../apis/google_drive/google_drive_endpoint.dart'; import '../apis/network/client.dart'; +import '../domain/config.dart'; import '../models/media/media.dart'; import '../models/media_content/media_content.dart'; import 'package:dio/dio.dart'; @@ -11,6 +13,41 @@ import 'package:google_sign_in/google_sign_in.dart'; import 'package:googleapis/drive/v3.dart' as drive; import '../errors/app_error.dart'; import 'auth_service.dart'; +import 'cloud_provider_service.dart'; + +final backUpFolderIdProvider = + StateNotifierProvider((ref) { + return BackUpFolderIdStateNotifier( + ref.read(authServiceProvider), + ref.read(googleDriveServiceProvider), + ); +}); + +class BackUpFolderIdStateNotifier extends StateNotifier { + final AuthService _authService; + final GoogleDriveService _googleDriveService; + StreamSubscription? _googleAccountSubscription; + + BackUpFolderIdStateNotifier(this._authService, this._googleDriveService) + : super(null) { + _googleAccountSubscription = + _authService.onGoogleAccountChange.listen((event) { + setBackUpFolderId(event); + }); + setBackUpFolderId(_authService.googleAccount); + } + + void setBackUpFolderId(GoogleSignInAccount? account) async { + state = + account == null ? null : await _googleDriveService.getBackUpFolderId(); + } + + @override + void dispose() { + _googleAccountSubscription?.cancel(); + super.dispose(); + } +} final googleDriveServiceProvider = Provider( (ref) => GoogleDriveService( @@ -19,8 +56,7 @@ final googleDriveServiceProvider = Provider( ), ); -class GoogleDriveService { - final String _backUpFolderName = "Cloud Gallery Backup"; +class GoogleDriveService extends CloudProviderService { final Dio _client; final GoogleSignIn _googleSignIn; @@ -36,88 +72,148 @@ class GoogleDriveService { return api; } - Future getBackupFolderId() async { + @override + Future> getAllMedias({ + required String folder, + }) async { try { final driveApi = await _getGoogleDriveAPI(); - final response = await driveApi.files.list( - q: "name='$_backUpFolderName' and trashed=false and mimeType='application/vnd.google-apps.folder'", - ); + bool hasMore = true; + String? pageToken; + final List medias = []; - if (response.files?.isNotEmpty ?? false) { - return response.files?.first.id; - } else { - final folder = drive.File( - name: _backUpFolderName, - mimeType: 'application/vnd.google-apps.folder', + while (hasMore) { + final response = await driveApi.files.list( + q: "'$folder' in parents and trashed=false", + $fields: + "files(id, name, description, mimeType, thumbnailLink, webContentLink, createdTime, modifiedTime, size, imageMediaMetadata, videoMediaMetadata, appProperties)", + pageSize: 1000, + pageToken: pageToken, + orderBy: "createdTime desc", + ); + hasMore = response.nextPageToken != null; + pageToken = response.nextPageToken; + medias.addAll( + (response.files ?? []) + .map( + (e) => AppMedia.fromGoogleDriveFile(e), + ) + .toList(), ); - final googleDriveFolder = await driveApi.files.create(folder); - return googleDriveFolder.id; } + + return medias; } catch (e) { throw AppError.fromError(e); } } - Future> getDriveMedias({ - required String backUpFolderId, + @override + Future getPaginatedMedias({ + required String folder, + String? nextPageToken, + int pageSize = 30, }) async { try { final driveApi = await _getGoogleDriveAPI(); final response = await driveApi.files.list( - q: "'$backUpFolderId' in parents and trashed=false", + q: "'$folder' in parents and trashed=false", + orderBy: "createdTime desc", $fields: - "files(id, name, description, mimeType, thumbnailLink, webContentLink, createdTime, modifiedTime, size, imageMediaMetadata, videoMediaMetadata)", + "files(id, name, description, mimeType, thumbnailLink, webContentLink, createdTime, modifiedTime, size, imageMediaMetadata, videoMediaMetadata, appProperties)", + pageSize: pageSize, + pageToken: nextPageToken, ); - return (response.files ?? []) - .map( - (e) => AppMedia.fromGoogleDriveFile(e), - ) - .toList(); + return GetPaginatedMediasResponse( + nextPageToken: response.nextPageToken, + medias: (response.files ?? []) + .map( + (e) => AppMedia.fromGoogleDriveFile(e), + ) + .toList(), + ); } catch (e) { throw AppError.fromError(e); } } - Future updateMediaDescription(String id, String description) async { + @override + Future deleteMedia({ + required String id, + CancelToken? cancelToken, + }) async { try { final driveApi = await _getGoogleDriveAPI(); - final file = drive.File(description: description); - final updatedFile = await driveApi.files.update(file, id); - return AppMedia.fromGoogleDriveFile(updatedFile); + await driveApi.files.delete(id); } catch (e) { throw AppError.fromError(e); } } - Future deleteMedia(String id) async { + Future getBackUpFolderId() async { try { final driveApi = await _getGoogleDriveAPI(); - await driveApi.files.delete(id); + + final response = await driveApi.files.list( + q: "name='${ProviderConstants.backupFolderName}' and trashed=false and mimeType='application/vnd.google-apps.folder'", + ); + + if (response.files?.isNotEmpty ?? false) { + return response.files?.first.id; + } else { + final folder = drive.File( + name: ProviderConstants.backupFolderName, + mimeType: 'application/vnd.google-apps.folder', + ); + final googleDriveFolder = await driveApi.files.create(folder); + return googleDriveFolder.id; + } + } catch (e) { + throw AppError.fromError(e); + } + } + + @override + Future createFolder(String folderName) async { + try { + final driveApi = await _getGoogleDriveAPI(); + + final folder = drive.File( + name: folderName, + mimeType: 'application/vnd.google-apps.folder', + ); + final googleDriveFolder = await driveApi.files.create(folder); + return googleDriveFolder.id; } catch (e) { throw AppError.fromError(e); } } - Future uploadInGoogleDrive({ - required String folderID, - required AppMedia media, + @override + Future uploadMedia({ + required String folderId, + required String path, + String? localRefId, + String? mimeType, CancelToken? cancelToken, - void Function(int chunk, int total)? onProgress, + void Function(int sent, int total)? onProgress, }) async { - final localFile = File(media.path); + final localFile = File(path); try { final file = drive.File( - name: media.name ?? localFile.path.split('/').last, - mimeType: media.mimeType, - description: media.id, - parents: [folderID], + name: localFile.path.split('/').last, + mimeType: mimeType, + appProperties: { + ProviderConstants.localRefIdKey: localRefId, + }, + parents: [folderId], ); final res = await _client.req( - UploadGoogleDriveFile( + GoogleDriveUploadEndpoint( request: file, content: AppMediaContent( stream: localFile.openRead(), @@ -138,15 +234,16 @@ class GoogleDriveService { } } - Future downloadFromGoogleDrive({ + @override + Future downloadMedia({ required String id, required String saveLocation, - void Function(int chunk, int total)? onProgress, CancelToken? cancelToken, + void Function(int sent, int total)? onProgress, }) async { try { await _client.downloadReq( - DownloadGoogleDriveFileContent( + GoogleDriveDownloadEndpoint( id: id, cancellationToken: cancelToken, saveLocation: saveLocation, @@ -157,4 +254,32 @@ class GoogleDriveService { throw AppError.fromError(e); } } + + Future updateAppProperties({ + required String id, + required String localRefId, + CancelToken? cancelToken, + }) async { + try { + await _client.req( + GoogleDriveUpdateAppPropertiesEndpoint( + id: id, + cancellationToken: cancelToken, + localFileId: localRefId, + ), + ); + } catch (e) { + throw AppError.fromError(e); + } + } +} + +class GoogleDriveListResponse { + final List medias; + final String? pageToken; + + const GoogleDriveListResponse({ + required this.medias, + required this.pageToken, + }); } diff --git a/data/lib/storage/app_preferences.dart b/data/lib/storage/app_preferences.dart index c468e7b..bf64e40 100644 --- a/data/lib/storage/app_preferences.dart +++ b/data/lib/storage/app_preferences.dart @@ -1,5 +1,5 @@ -import '../models/dropbox_account/dropbox_account.dart'; -import '../models/token/token.dart'; +import '../models/dropbox/account/dropbox_account.dart'; +import '../models/dropbox/token/dropbox_token.dart'; import 'provider/preferences_provider.dart'; class AppPreferences { @@ -46,6 +46,11 @@ class AppPreferences { fromJson: (json) => DropboxAccount.fromJson(json), ); + static final dropboxFileIdAppPropertyTemplateId = createPrefProvider( + prefKey: "dropbox_file_id_app_property_template_id", + defaultValue: null, + ); + static final dropboxPKCECodeVerifier = createPrefProvider( prefKey: "dropbox_code_verifier", defaultValue: null, diff --git a/data/pubspec.yaml b/data/pubspec.yaml index 817ae6d..805aea1 100644 --- a/data/pubspec.yaml +++ b/data/pubspec.yaml @@ -24,11 +24,15 @@ dependencies: google_sign_in: ^6.2.2 extension_google_sign_in_as_googleapis_auth: ^2.0.12 + # notifications + flutter_local_notifications: ^18.0.1 + # state management flutter_riverpod: ^2.6.1 # storage shared_preferences: ^2.3.3 + sqflite: ^2.4.1 # code generation freezed: ^2.5.7 @@ -37,6 +41,9 @@ dependencies: collection: ^1.18.0 + # logging + logger: ^2.5.0 + dev_dependencies: flutter_test: sdk: flutter diff --git a/style/lib/animations/item_selector.dart b/style/lib/animations/item_selector.dart deleted file mode 100644 index 5750e01..0000000 --- a/style/lib/animations/item_selector.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import '../extensions/context_extensions.dart'; - -class ItemSelector extends StatelessWidget { - final void Function()? onTap; - final void Function()? onLongTap; - final bool isSelected; - final Widget child; - - const ItemSelector({ - super.key, - this.onTap, - this.onLongTap, - required this.isSelected, - required this.child, - }); - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap, - onLongPress: onLongTap, - child: Stack( - children: [ - AnimatedScale( - curve: Curves.easeInOut, - scale: isSelected ? 0.9 : 1, - duration: const Duration(milliseconds: 100), - child: AnimatedOpacity( - curve: Curves.easeInOut, - duration: const Duration(milliseconds: 100), - opacity: isSelected ? 0.7 : 1, - child: child, - ), - ), - if (isSelected) - Align( - alignment: Alignment.topRight, - child: Container( - padding: const EdgeInsets.all(2), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: context.colorScheme.surface, - border: Border.all( - color: const Color(0xff808080), - ), - ), - child: const Icon( - CupertinoIcons.checkmark_alt, - color: Color(0xff808080), - size: 16, - ), - ), - ), - ], - ), - ); - } -} diff --git a/style/lib/callback/on_visible_callback.dart b/style/lib/callback/on_visible_callback.dart new file mode 100644 index 0000000..be9cae6 --- /dev/null +++ b/style/lib/callback/on_visible_callback.dart @@ -0,0 +1,28 @@ +import 'package:flutter/cupertino.dart'; + +class OnVisibleCallback extends StatefulWidget { + final void Function() onVisible; + final Widget child; + + const OnVisibleCallback({ + super.key, + required this.onVisible, + required this.child, + }); + + @override + State createState() => _OnCreateState(); +} + +class _OnCreateState extends State { + @override + void initState() { + widget.onVisible(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return widget.child; + } +}