Compare commits

..

22 Commits

Author SHA1 Message Date
mertalev b84216180c update sync 2025-09-20 10:41:45 -04:00
Alex e1c6813ee0 Merge branch 'main' of github.com:immich-app/immich into feat/mobile-platform-clients 2025-09-20 08:18:22 -05:00
Jason Rasmussen de57fecb69 fix(web): copy to clipboard on safari (#22217) 2025-09-19 17:44:18 -04:00
renovate[bot] 1e0b4fac04 fix(deps): update typescript-projects (#21510)
* fix(deps): update typescript-projects

* chore: downgrade dependencies

* chore: downgrade svelte-gestures

* fix: svelte/no-navigation-without-resolve

* fix: dumb test

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Zack Pollard <zack@futo.org>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
Co-authored-by: Jason Rasmussen <jason@rasm.me>
2025-09-19 12:29:01 -04:00
Jason Rasmussen 34339ea69f fix(web): show danger/warning when taken dates overlap (#22213) 2025-09-19 12:20:09 -04:00
Jason Rasmussen 6da039780e fix: automatically remove leading/trailing whitespace from search que… (#22214)
fix: automatically remove leading/trailing whitespace from search queries
2025-09-19 12:19:26 -04:00
Jason Rasmussen 3f2e0780d5 feat: availability checks (#22185) 2025-09-19 12:18:42 -04:00
Mert 52363cf0fb chore(mobile): ignore ios build folder (#22212)
ignore ios build folder
2025-09-19 09:50:24 -05:00
Jason Rasmussen 86df09a0e4 fix(mobile): smaller search page size (#22210) 2025-09-19 10:11:11 -04:00
mertalev 5054719f43 disable disk cache by default 2025-09-19 09:23:32 -04:00
Alex 3423cf90bc Merge branch 'main' into feat/mobile-platform-clients 2025-09-18 23:58:49 -05:00
mertalev f406ba1e6c add back client parameter for testing 2025-09-18 20:31:29 -04:00
mertalev df186cc326 unrelated change 2025-09-18 20:18:07 -04:00
mertalev 11ebbe51a1 don't close client 2025-09-18 20:16:48 -04:00
mertalev 52fbf6fbc7 update other usages 2025-09-18 20:16:38 -04:00
mertalev cf7a3a91c2 move to bootstrap 2025-09-18 20:13:37 -04:00
mertalev dc73a860cc set defaults 2025-09-18 20:13:37 -04:00
mertalev 88b6da5e0a init before app launch 2025-09-18 20:13:37 -04:00
mertalev 740c50122e custom user agent 2025-09-18 20:13:37 -04:00
mertalev 9836392fbe fix hot reload 2025-09-18 20:13:37 -04:00
mertalev b1f3051608 uppercase http method 2025-09-18 20:13:37 -04:00
mertalev 87e1539912 platform clients 2025-09-18 20:13:37 -04:00
148 changed files with 2413 additions and 2266 deletions
+1
View File
@@ -18,6 +18,7 @@ mobile/libisar.dylib
mobile/openapi/test mobile/openapi/test
mobile/openapi/doc mobile/openapi/doc
mobile/openapi/.openapi-generator/FILES mobile/openapi/.openapi-generator/FILES
mobile/ios/build
open-api/typescript-sdk/build open-api/typescript-sdk/build
mobile/android/fastlane/report.xml mobile/android/fastlane/report.xml
@@ -169,8 +169,6 @@ Redis (Sentinel) URL example JSON before encoding:
| `MACHINE_LEARNING_ANN_TUNING_LEVEL` | ARM-NN GPU tuning level (1: rapid, 2: normal, 3: exhaustive) | `2` | machine learning | | `MACHINE_LEARNING_ANN_TUNING_LEVEL` | ARM-NN GPU tuning level (1: rapid, 2: normal, 3: exhaustive) | `2` | machine learning |
| `MACHINE_LEARNING_DEVICE_IDS`<sup>\*4</sup> | Device IDs to use in multi-GPU environments | `0` | machine learning | | `MACHINE_LEARNING_DEVICE_IDS`<sup>\*4</sup> | Device IDs to use in multi-GPU environments | `0` | machine learning |
| `MACHINE_LEARNING_MAX_BATCH_SIZE__FACIAL_RECOGNITION` | Set the maximum number of faces that will be processed at once by the facial recognition model | None (`1` if using OpenVINO) | machine learning | | `MACHINE_LEARNING_MAX_BATCH_SIZE__FACIAL_RECOGNITION` | Set the maximum number of faces that will be processed at once by the facial recognition model | None (`1` if using OpenVINO) | machine learning |
| `MACHINE_LEARNING_PING_TIMEOUT` | How long (ms) to wait for a PING response when checking if an ML server is available | `2000` | server |
| `MACHINE_LEARNING_AVAILABILITY_BACKOFF_TIME` | How long to ignore ML servers that are offline before trying again | `30000` | server |
| `MACHINE_LEARNING_RKNN` | Enable RKNN hardware acceleration if supported | `True` | machine learning | | `MACHINE_LEARNING_RKNN` | Enable RKNN hardware acceleration if supported | `True` | machine learning |
| `MACHINE_LEARNING_RKNN_THREADS` | How many threads of RKNN runtime should be spinned up while inferencing. | `1` | machine learning | | `MACHINE_LEARNING_RKNN_THREADS` | How many threads of RKNN runtime should be spinned up while inferencing. | `1` | machine learning |
+9
View File
@@ -123,6 +123,13 @@
"logging_enable_description": "Enable logging", "logging_enable_description": "Enable logging",
"logging_level_description": "When enabled, what log level to use.", "logging_level_description": "When enabled, what log level to use.",
"logging_settings": "Logging", "logging_settings": "Logging",
"machine_learning_availability_checks": "Availability checks",
"machine_learning_availability_checks_description": "Automatically detect and prefer available machine learning servers",
"machine_learning_availability_checks_enabled": "Enable availability checks",
"machine_learning_availability_checks_interval": "Check interval",
"machine_learning_availability_checks_interval_description": "Interval in milliseconds between availability checks",
"machine_learning_availability_checks_timeout": "Request timeout",
"machine_learning_availability_checks_timeout_description": "Timeout in milliseconds for availability checks",
"machine_learning_clip_model": "CLIP model", "machine_learning_clip_model": "CLIP model",
"machine_learning_clip_model_description": "The name of a CLIP model listed <link>here</link>. Note that you must re-run the 'Smart Search' job for all images upon changing a model.", "machine_learning_clip_model_description": "The name of a CLIP model listed <link>here</link>. Note that you must re-run the 'Smart Search' job for all images upon changing a model.",
"machine_learning_duplicate_detection": "Duplicate Detection", "machine_learning_duplicate_detection": "Duplicate Detection",
@@ -913,6 +920,7 @@
"cant_get_number_of_comments": "Can't get number of comments", "cant_get_number_of_comments": "Can't get number of comments",
"cant_search_people": "Can't search people", "cant_search_people": "Can't search people",
"cant_search_places": "Can't search places", "cant_search_places": "Can't search places",
"clipboard_unsupported_mime_type": "The system clipboard does not support copying this type of content: {mimeType}",
"error_adding_assets_to_album": "Error adding assets to album", "error_adding_assets_to_album": "Error adding assets to album",
"error_adding_users_to_album": "Error adding users to album", "error_adding_users_to_album": "Error adding users to album",
"error_deleting_shared_user": "Error deleting shared user", "error_deleting_shared_user": "Error deleting shared user",
@@ -1916,6 +1924,7 @@
"stacktrace": "Stacktrace", "stacktrace": "Stacktrace",
"start": "Start", "start": "Start",
"start_date": "Start date", "start_date": "Start date",
"start_date_before_end_date": "Start date must be before end date",
"state": "State", "state": "State",
"status": "Status", "status": "Status",
"stop_casting": "Stop casting", "stop_casting": "Stop casting",
+1 -1
View File
@@ -1,7 +1,7 @@
[tools] [tools]
node = "22.19.0" node = "22.19.0"
flutter = "3.35.4" flutter = "3.35.4"
pnpm = "10.14.0" pnpm = "10.15.1"
dart = "3.8.2" dart = "3.8.2"
[tools."github:CQLabs/homebrew-dcm"] [tools."github:CQLabs/homebrew-dcm"]
+164 -5
View File
@@ -17,7 +17,7 @@ linter:
# section below to disable rules from the `package:flutter_lints/flutter.yaml` # section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints # included above or to enable additional rules. A list of all available lints
# and their documentation is published at # and their documentation is published at
# https://dart.dev/tools/linter-rules # https://dart-lang.github.io/linter/lints/index.html.
# #
# Instead of disabling a lint rule for the entire project in the # Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code # section below, it can also be suppressed for a single line of code
@@ -28,7 +28,6 @@ linter:
rules: rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule # avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
unawaited_futures: true
use_build_context_synchronously: false use_build_context_synchronously: false
require_trailing_commas: true require_trailing_commas: true
unrelated_type_equality_checks: true unrelated_type_equality_checks: true
@@ -48,9 +47,6 @@ analyzer:
# plugins: # plugins:
# - custom_lint # - custom_lint
errors:
unawaited_futures: warning
custom_lint: custom_lint:
debug: true debug: true
rules: rules:
@@ -146,7 +142,170 @@ dart_code_metrics:
exclude-paths: exclude-paths:
- 'lib/utils/debug_print.dart' - 'lib/utils/debug_print.dart'
severity: perf severity: perf
# All rules from "recommended" preset
# Show potential errors
# - avoid-cascade-after-if-null
# - avoid-collection-methods-with-unrelated-types
# - avoid-duplicate-exports
# - avoid-dynamic
# - avoid-missing-enum-constant-in-map
# - avoid-passing-async-when-sync-expected
# - avoid-throw-in-catch-block
- avoid-unused-parameters - avoid-unused-parameters
# - avoid-unnecessary-type-assertions
# - avoid-unnecessary-type-casts
# - avoid-unrelated-type-assertions
# - avoid-unrelated-type-casts
# - no-empty-block
# - no-equal-then-else
# - prefer-correct-test-file-name
- prefer-const-border-radius - prefer-const-border-radius
# - prefer-match-file-name
# - prefer-return-await
# - avoid-self-assignment
# - avoid-self-compare
# - avoid-shadowing
# - prefer-iterable-of
# - no-equal-switch-case
# - no-equal-conditions
# - avoid-equal-expressions
# - avoid-missed-calls
# - avoid-unnecessary-negations
# - avoid-unused-generics
# - function-always-returns-null
# - avoid-throw-objects-without-tostring
# - avoid-unsafe-collection-methods
# - prefer-wildcard-pattern
# - no-equal-switch-expression-cases
# - avoid-future-tostring
# - avoid-unassigned-late-fields
# - avoid-nested-futures
# - avoid-generics-shadowing
# - prefer-parentheses-with-if-null
# - no-equal-nested-conditions
# - avoid-shadowed-extension-methods
# - avoid-unnecessary-conditionals
# - avoid-double-slash-imports
# - avoid-map-keys-contains
# - prefer-correct-json-casts
# - avoid-duplicate-mixins
# - avoid-nullable-interpolation
# - avoid-unused-instances
# - prefer-correct-for-loop-increment
# - prefer-public-exception-classes
# - avoid-uncaught-future-errors
# - always-remove-listener
# - avoid-unnecessary-setstate
# - check-for-equals-in-render-object-setters
# - consistent-update-render-object
# - use-setstate-synchronously
# - avoid-incomplete-copy-with
# - proper-super-calls
# - dispose-fields
# - avoid-empty-setstate
# - avoid-state-constructors
# - avoid-recursive-widget-calls
# - avoid-missing-image-alt
# - avoid-passing-self-as-argument
# - avoid-unnecessary-if
# - avoid-unconditional-break
# - avoid-referencing-discarded-variables
# - avoid-unnecessary-local-late
# - avoid-wildcard-cases-with-enums
# - match-getter-setter-field-names
# - avoid-accessing-collections-by-constant-index
# - prefer-unique-test-names
# - avoid-duplicate-cascades
# - prefer-specific-cases-first
# - avoid-duplicate-switch-case-conditions
# - prefer-explicit-function-type
# - avoid-misused-test-matchers
# - avoid-duplicate-test-assertions
# - prefer-switch-with-enums
# - prefer-any-or-every
# - avoid-duplicate-map-keys
# - avoid-nullable-tostring
# - avoid-undisposed-instances
# - avoid-duplicate-initializers
# - avoid-unassigned-stream-subscriptions
# - avoid-empty-test-groups
# - avoid-not-encodable-in-to-json
# - avoid-contradictory-expressions
# - avoid-excessive-expressions
# - prefer-private-extension-type-field
# - avoid-renaming-representation-getters
# - avoid-empty-spread
# - avoid-unnecessary-gesture-detector
# - avoid-missing-completer-stack-trace
# - avoid-casting-to-extension-type
# - prefer-overriding-parent-equality
# - avoid-missing-controller
# - avoid-unknown-pragma
# - avoid-conditions-with-boolean-literals
# - avoid-multi-assignment
# - avoid-collection-equality-checks
# - avoid-only-rethrow
# - avoid-incorrect-image-opacity
# - avoid-misused-set-literals
# - dispose-class-fields
# - avoid-suspicious-super-overrides
# - avoid-assignments-as-conditions
# - avoid-unused-assignment
# - avoid-unnecessary-overrides
# - avoid-implicitly-nullable-extension-types
# Enable with the next release
# - avoid-late-final-reassignment
# - avoid-duplicate-constant-values
# - function-always-returns-same-value
# - avoid-flexible-outside-flex
# - avoid-unnecessary-patterns
# - use-closest-build-context
# - avoid-commented-out-code
# - avoid-recursive-tostring
# - avoid-enum-values-by-index
# - avoid-constant-assert-conditions
# - avoid-inconsistent-digit-separators
# - pass-existing-future-to-future-builder
# - pass-existing-stream-to-stream-builder
# Code simplification
# - avoid-redundant-async
# - avoid-redundant-else
# - avoid-unnecessary-nullable-return-type
# - avoid-redundant-pragma-inline
# - avoid-nested-records
# - avoid-redundant-positional-field-name
# - avoid-explicit-pattern-field-name
# - prefer-simpler-patterns-null-check
# - avoid-unnecessary-return
# - avoid-duplicate-patterns
# - avoid-keywords-in-wildcard-pattern
# - avoid-unnecessary-futures
# - avoid-unnecessary-reassignment
# - avoid-unnecessary-call
# - avoid-unnecessary-stateful-widgets
# - prefer-dedicated-media-query-methods
# - avoid-unnecessary-overrides-in-state
# - move-variable-closer-to-its-usage
# - avoid-nullable-parameters-with-default-values
# - prefer-null-aware-spread
# - avoid-inferrable-type-arguments
# - avoid-unnecessary-super
# - avoid-unnecessary-collections
# - avoid-unnecessary-extends
# - avoid-unnecessary-enum-arguments
# - prefer-contains
# Enable with the next release
# - prefer-simpler-boolean-expressions
# - prefer-spacing
# - avoid-unnecessary-continue
# - avoid-unnecessary-compare-to
# Style
# - prefer-trailing-comma
# - unnecessary-trailing-comma
- prefer-declaring-const-constructor - prefer-declaring-const-constructor
# - prefer-single-widget-per-file
- prefer-switch-expression - prefer-switch-expression
# - prefer-prefixed-global-constants
# - prefer-correct-callback-field-name
+13
View File
@@ -6,6 +6,9 @@ PODS:
- FlutterMacOS - FlutterMacOS
- connectivity_plus (0.0.1): - connectivity_plus (0.0.1):
- Flutter - Flutter
- cupertino_http (0.0.1):
- Flutter
- FlutterMacOS
- device_info_plus (0.0.1): - device_info_plus (0.0.1):
- Flutter - Flutter
- DKImagePickerController/Core (4.3.9): - DKImagePickerController/Core (4.3.9):
@@ -77,6 +80,8 @@ PODS:
- Flutter - Flutter
- network_info_plus (0.0.1): - network_info_plus (0.0.1):
- Flutter - Flutter
- objective_c (0.0.1):
- Flutter
- package_info_plus (0.4.5): - package_info_plus (0.4.5):
- Flutter - Flutter
- path_provider_foundation (0.0.1): - path_provider_foundation (0.0.1):
@@ -136,6 +141,7 @@ DEPENDENCIES:
- background_downloader (from `.symlinks/plugins/background_downloader/ios`) - background_downloader (from `.symlinks/plugins/background_downloader/ios`)
- bonsoir_darwin (from `.symlinks/plugins/bonsoir_darwin/darwin`) - bonsoir_darwin (from `.symlinks/plugins/bonsoir_darwin/darwin`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- cupertino_http (from `.symlinks/plugins/cupertino_http/darwin`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
@@ -154,6 +160,7 @@ DEPENDENCIES:
- maplibre_gl (from `.symlinks/plugins/maplibre_gl/ios`) - maplibre_gl (from `.symlinks/plugins/maplibre_gl/ios`)
- native_video_player (from `.symlinks/plugins/native_video_player/ios`) - native_video_player (from `.symlinks/plugins/native_video_player/ios`)
- network_info_plus (from `.symlinks/plugins/network_info_plus/ios`) - network_info_plus (from `.symlinks/plugins/network_info_plus/ios`)
- objective_c (from `.symlinks/plugins/objective_c/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
@@ -184,6 +191,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/bonsoir_darwin/darwin" :path: ".symlinks/plugins/bonsoir_darwin/darwin"
connectivity_plus: connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios" :path: ".symlinks/plugins/connectivity_plus/ios"
cupertino_http:
:path: ".symlinks/plugins/cupertino_http/darwin"
device_info_plus: device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios" :path: ".symlinks/plugins/device_info_plus/ios"
file_picker: file_picker:
@@ -220,6 +229,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/native_video_player/ios" :path: ".symlinks/plugins/native_video_player/ios"
network_info_plus: network_info_plus:
:path: ".symlinks/plugins/network_info_plus/ios" :path: ".symlinks/plugins/network_info_plus/ios"
objective_c:
:path: ".symlinks/plugins/objective_c/ios"
package_info_plus: package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios" :path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation: path_provider_foundation:
@@ -249,6 +260,7 @@ SPEC CHECKSUMS:
background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad
bonsoir_darwin: 29c7ccf356646118844721f36e1de4b61f6cbd0e bonsoir_darwin: 29c7ccf356646118844721f36e1de4b61f6cbd0e
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
@@ -270,6 +282,7 @@ SPEC CHECKSUMS:
maplibre_gl: 3c924e44725147b03dda33430ad216005b40555f maplibre_gl: 3c924e44725147b03dda33430ad216005b40555f
native_video_player: b65c58951ede2f93d103a25366bdebca95081265 native_video_player: b65c58951ede2f93d103a25366bdebca95081265
network_info_plus: cf61925ab5205dce05a4f0895989afdb6aade5fc network_info_plus: cf61925ab5205dce05a4f0895989afdb6aade5fc
objective_c: 89e720c30d716b036faf9c9684022048eee1eee2
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
@@ -3,7 +3,6 @@ import 'dart:io';
import 'dart:ui'; import 'dart:ui';
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:cancellation_token_http/http.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/constants/constants.dart';
@@ -63,7 +62,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
final Drift _drift; final Drift _drift;
final DriftLogger _driftLogger; final DriftLogger _driftLogger;
final BackgroundWorkerBgHostApi _backgroundHostApi; final BackgroundWorkerBgHostApi _backgroundHostApi;
final CancellationToken _cancellationToken = CancellationToken(); final Completer _cancellationToken = Completer();
final Logger _logger = Logger('BackgroundWorkerBgService'); final Logger _logger = Logger('BackgroundWorkerBgService');
bool _isCleanedUp = false; bool _isCleanedUp = false;
@@ -117,10 +116,10 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
} }
// Notify the host that the background worker service has been initialized and is ready to use // Notify the host that the background worker service has been initialized and is ready to use
unawaited(_backgroundHostApi.onInitialized()); _backgroundHostApi.onInitialized();
} catch (error, stack) { } catch (error, stack) {
_logger.severe("Failed to initialize background worker", error, stack); _logger.severe("Failed to initialize background worker", error, stack);
unawaited(_backgroundHostApi.close()); _backgroundHostApi.close();
} }
} }
@@ -188,7 +187,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
_isCleanedUp = true; _isCleanedUp = true;
_ref.dispose(); _ref.dispose();
_cancellationToken.cancel(); _cancellationToken.complete();
_logger.info("Cleaning up background worker"); _logger.info("Cleaning up background worker");
final cleanupFutures = [ final cleanupFutures = [
workerManager.dispose().catchError((_) async { workerManager.dispose().catchError((_) async {
@@ -249,7 +249,7 @@ class LocalSyncService {
if (assetsToUpsert.isEmpty && assetsToDelete.isEmpty) { if (assetsToUpsert.isEmpty && assetsToDelete.isEmpty) {
_log.fine("No asset changes detected in album ${deviceAlbum.name}. Updating metadata."); _log.fine("No asset changes detected in album ${deviceAlbum.name}. Updating metadata.");
await _localAlbumRepository.upsert(updatedDeviceAlbum); _localAlbumRepository.upsert(updatedDeviceAlbum);
return true; return true;
} }
@@ -1,15 +1,16 @@
import 'dart:async'; import 'dart:async';
import 'dart:ffi'; import 'dart:ffi';
import 'dart:io';
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:cronet_http/cronet_http.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:ffi/ffi.dart'; import 'package:ffi/ffi.dart';
import 'package:http/http.dart' as http;
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/providers/image/cache/remote_image_cache_manager.dart'; import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:logging/logging.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
part 'local_image_request.dart'; part 'local_image_request.dart';
part 'thumbhash_image_request.dart'; part 'thumbhash_image_request.dart';
@@ -1,14 +1,18 @@
part of 'image_request.dart'; part of 'image_request.dart';
class RemoteImageRequest extends ImageRequest { class RemoteImageRequest extends ImageRequest {
static final log = Logger('RemoteImageRequest'); static final _client = const NetworkRepository().getHttpClient(
static final client = HttpClient()..maxConnectionsPerHost = 16; 'thumbnails',
final RemoteCacheManager? cacheManager; diskCapacity: kThumbnailDiskCacheSize,
memoryCapacity: 0,
maxConnections: 16,
cacheMode: CacheMode.disk,
);
final String uri; final String uri;
final Map<String, String> headers; final Map<String, String> headers;
HttpClientRequest? _request; final abortTrigger = Completer<void>();
RemoteImageRequest({required this.uri, required this.headers, this.cacheManager}); RemoteImageRequest({required this.uri, required this.headers});
@override @override
Future<ImageInfo?> load(ImageDecoderCallback decode, {double scale = 1.0}) async { Future<ImageInfo?> load(ImageDecoderCallback decode, {double scale = 1.0}) async {
@@ -16,15 +20,8 @@ class RemoteImageRequest extends ImageRequest {
return null; return null;
} }
// TODO: the cache manager makes everything sequential with its DB calls and its operations cannot be cancelled,
// so it ends up being a bottleneck. We only prefer fetching from it when it can skip the DB call.
final cachedFileImage = await _loadCachedFile(uri, decode, scale, inMemoryOnly: true);
if (cachedFileImage != null) {
return cachedFileImage;
}
try { try {
final buffer = await _downloadImage(uri); final buffer = await _downloadImage();
if (buffer == null) { if (buffer == null) {
return null; return null;
} }
@@ -35,57 +32,41 @@ class RemoteImageRequest extends ImageRequest {
return null; return null;
} }
final cachedFileImage = await _loadCachedFile(uri, decode, scale, inMemoryOnly: false);
if (cachedFileImage != null) {
return cachedFileImage;
}
rethrow; rethrow;
} finally {
_request = null;
} }
} }
Future<ImmutableBuffer?> _downloadImage(String url) async { Future<ImmutableBuffer?> _downloadImage() async {
if (_isCancelled) { if (_isCancelled) {
return null; return null;
} }
final request = _request = await client.getUrl(Uri.parse(url)); final req = http.AbortableRequest('GET', Uri.parse(uri), abortTrigger: abortTrigger.future);
if (_isCancelled) { req.headers.addAll(headers);
request.abort(); final res = await _client.send(req);
return _request = null;
}
for (final entry in headers.entries) {
request.headers.set(entry.key, entry.value);
}
final response = await request.close();
if (_isCancelled) { if (_isCancelled) {
_onCancelled();
return null; return null;
} }
final cacheManager = this.cacheManager; if (res.statusCode != 200) {
final streamController = StreamController<List<int>>(sync: true); throw Exception('Failed to download $uri: ${res.statusCode}');
final Stream<List<int>> stream; }
unawaited(cacheManager?.putStreamedFile(url, streamController.stream));
stream = response.map((chunk) { final stream = res.stream.map((chunk) {
if (_isCancelled) { if (_isCancelled) {
throw StateError('Cancelled request'); throw StateError('Cancelled request');
} }
if (cacheManager != null) {
streamController.add(chunk);
}
return chunk; return chunk;
}); });
try { try {
final Uint8List bytes = await _downloadBytes(stream, response.contentLength); final Uint8List bytes = await _downloadBytes(stream, res.contentLength ?? -1);
unawaited(streamController.close()); if (_isCancelled) {
return null;
}
return await ImmutableBuffer.fromUint8List(bytes); return await ImmutableBuffer.fromUint8List(bytes);
} catch (e) { } catch (e) {
streamController.addError(e);
unawaited(streamController.close());
if (_isCancelled) { if (_isCancelled) {
return null; return null;
} }
@@ -122,40 +103,6 @@ class RemoteImageRequest extends ImageRequest {
return bytes; return bytes;
} }
Future<ImageInfo?> _loadCachedFile(
String url,
ImageDecoderCallback decode,
double scale, {
required bool inMemoryOnly,
}) async {
final cacheManager = this.cacheManager;
if (_isCancelled || cacheManager == null) {
return null;
}
final file = await (inMemoryOnly ? cacheManager.getFileFromMemory(url) : cacheManager.getFileFromCache(url));
if (_isCancelled || file == null) {
return null;
}
try {
final buffer = await ImmutableBuffer.fromFilePath(file.file.path);
return await _decodeBuffer(buffer, decode, scale);
} catch (e) {
log.severe('Failed to decode cached image', e);
unawaited(_evictFile(url));
return null;
}
}
Future<void> _evictFile(String url) async {
try {
await cacheManager?.removeFile(url);
} catch (e) {
log.severe('Failed to remove cached image', e);
}
}
Future<ImageInfo?> _decodeBuffer(ImmutableBuffer buffer, ImageDecoderCallback decode, scale) async { Future<ImageInfo?> _decodeBuffer(ImmutableBuffer buffer, ImageDecoderCallback decode, scale) async {
if (_isCancelled) { if (_isCancelled) {
buffer.dispose(); buffer.dispose();
@@ -173,7 +120,6 @@ class RemoteImageRequest extends ImageRequest {
@override @override
void _onCancelled() { void _onCancelled() {
_request?.abort(); abortTrigger.complete();
_request = null;
} }
} }
@@ -361,13 +361,15 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
return _db.managers.localAlbumEntity.count(); return _db.managers.localAlbumEntity.count();
} }
Future<void> unlinkRemoteAlbum(String id) async { Future unlinkRemoteAlbum(String id) async {
final query = _db.localAlbumEntity.update()..where((row) => row.id.equals(id)); return _db.localAlbumEntity.update()
await query.write(const LocalAlbumEntityCompanion(linkedRemoteAlbumId: Value(null))); ..where((row) => row.id.equals(id))
..write(const LocalAlbumEntityCompanion(linkedRemoteAlbumId: Value(null)));
} }
Future<void> linkRemoteAlbum(String localAlbumId, String remoteAlbumId) async { Future linkRemoteAlbum(String localAlbumId, String remoteAlbumId) async {
final query = _db.localAlbumEntity.update()..where((row) => row.id.equals(localAlbumId)); return _db.localAlbumEntity.update()
await query.write(LocalAlbumEntityCompanion(linkedRemoteAlbumId: Value(remoteAlbumId))); ..where((row) => row.id.equals(localAlbumId))
..write(LocalAlbumEntityCompanion(linkedRemoteAlbumId: Value(remoteAlbumId)));
} }
} }
@@ -0,0 +1,67 @@
import 'dart:io';
import 'package:cronet_http/cronet_http.dart';
import 'package:cupertino_http/cupertino_http.dart';
import 'package:http/http.dart' as http;
import 'package:immich_mobile/utils/user_agent.dart';
import 'package:path_provider/path_provider.dart';
class NetworkRepository {
static late Directory _cachePath;
static late String _userAgent;
static final _clients = <String, http.Client>{};
static Future<void> init() {
return (
getTemporaryDirectory().then((cachePath) => _cachePath = cachePath),
getUserAgentString().then((userAgent) => _userAgent = userAgent),
).wait;
}
static void reset() {
Future.microtask(init);
for (final client in _clients.values) {
client.close();
}
_clients.clear();
}
const NetworkRepository();
/// Note: when disk caching is enabled, only one client may use a given directory at a time.
/// Different isolates or engines must use different directories.
http.Client getHttpClient(
String directoryName, {
CacheMode cacheMode = CacheMode.memory,
int diskCapacity = 0,
int maxConnections = 6,
int memoryCapacity = 10 << 20,
}) {
final cachedClient = _clients[directoryName];
if (cachedClient != null) {
return cachedClient;
}
final directory = Directory('${_cachePath.path}/$directoryName');
directory.createSync(recursive: true);
if (Platform.isAndroid) {
final engine = CronetEngine.build(
cacheMode: cacheMode,
cacheMaxSize: diskCapacity,
storagePath: directory.path,
userAgent: _userAgent,
);
return _clients[directoryName] = CronetClient.fromCronetEngine(engine, closeEngine: true);
}
final config = URLSessionConfiguration.defaultSessionConfiguration()
..httpMaximumConnectionsPerHost = maxConnections
..cache = URLCache.withCapacity(
diskCapacity: diskCapacity,
memoryCapacity: memoryCapacity,
directory: directory.uri,
)
..httpAdditionalHeaders = {'User-Agent': _userAgent};
return _clients[directoryName] = CupertinoClient.fromSessionConfiguration(config);
}
}
@@ -33,7 +33,7 @@ class SearchApiRepository extends ApiRepository {
personIds: filter.people.map((e) => e.id).toList(), personIds: filter.people.map((e) => e.id).toList(),
type: type, type: type,
page: page, page: page,
size: 1000, size: 100,
), ),
); );
} }
@@ -6,11 +6,13 @@ import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/sync_event.model.dart'; import 'package:immich_mobile/domain/models/sync_event.model.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/api.service.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
class SyncApiRepository { class SyncApiRepository {
static final _client = const NetworkRepository().getHttpClient('api');
final Logger _logger = Logger('SyncApiRepository'); final Logger _logger = Logger('SyncApiRepository');
final ApiService _api; final ApiService _api;
SyncApiRepository(this._api); SyncApiRepository(this._api);
@@ -26,7 +28,7 @@ class SyncApiRepository {
http.Client? httpClient, http.Client? httpClient,
}) async { }) async {
final stopwatch = Stopwatch()..start(); final stopwatch = Stopwatch()..start();
final client = httpClient ?? http.Client(); final client = httpClient ?? _client;
final endpoint = "${_api.apiClient.basePath}/sync/stream"; final endpoint = "${_api.apiClient.basePath}/sync/stream";
final headers = {'Content-Type': 'application/json', 'Accept': 'application/jsonlines+json'}; final headers = {'Content-Type': 'application/json', 'Accept': 'application/jsonlines+json'};
@@ -112,8 +114,6 @@ class SyncApiRepository {
} catch (error, stack) { } catch (error, stack) {
_logger.severe("Error processing stream", error, stack); _logger.severe("Error processing stream", error, stack);
return Future.error(error, stack); return Future.error(error, stack);
} finally {
client.close();
} }
stopwatch.stop(); stopwatch.stop();
_logger.info("Remote Sync completed in ${stopwatch.elapsed.inMilliseconds}ms"); _logger.info("Remote Sync completed in ${stopwatch.elapsed.inMilliseconds}ms");
+10 -1
View File
@@ -15,6 +15,7 @@ import 'package:immich_mobile/constants/locales.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/generated/codegen_loader.g.dart'; import 'package:immich_mobile/generated/codegen_loader.g.dart';
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/db.provider.dart';
@@ -153,7 +154,7 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
// Draw the app from edge to edge // Draw the app from edge to edge
unawaited(SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge)); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
// Sets the navigation bar color // Sets the navigation bar color
SystemUiOverlayStyle overlayStyle = const SystemUiOverlayStyle(systemNavigationBarColor: Colors.transparent); SystemUiOverlayStyle overlayStyle = const SystemUiOverlayStyle(systemNavigationBarColor: Colors.transparent);
@@ -222,6 +223,14 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
super.dispose(); super.dispose();
} }
@override
void reassemble() {
if (kDebugMode) {
NetworkRepository.reset();
}
super.reassemble();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final router = ref.watch(appRouterProvider); final router = ref.watch(appRouterProvider);
@@ -1,5 +1,3 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -53,7 +51,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
final isSuccess = await ref.read(albumProvider.notifier).leaveAlbum(album); final isSuccess = await ref.read(albumProvider.notifier).leaveAlbum(album);
if (isSuccess) { if (isSuccess) {
unawaited(context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()]))); context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()]));
} else { } else {
showErrorMessage(); showErrorMessage();
} }
@@ -1,5 +1,3 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -31,8 +29,8 @@ class AlbumSharedUserSelectionPage extends HookConsumerWidget {
if (newAlbum != null) { if (newAlbum != null) {
ref.watch(albumTitleProvider.notifier).clearAlbumTitle(); ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
unawaited(context.maybePop(true)); context.maybePop(true);
unawaited(context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()]))); context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()]));
} }
ScaffoldMessenger( ScaffoldMessenger(
@@ -111,8 +109,8 @@ class AlbumSharedUserSelectionPage extends HookConsumerWidget {
centerTitle: false, centerTitle: false,
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.close_rounded), icon: const Icon(Icons.close_rounded),
onPressed: () { onPressed: () async {
unawaited(context.maybePop()); context.maybePop();
}, },
), ),
actions: [ actions: [
@@ -155,7 +155,7 @@ class BackupControllerPage extends HookConsumerWidget {
// waited until returning from selection // waited until returning from selection
await ref.read(backupProvider.notifier).backupAlbumSelectionDone(); await ref.read(backupProvider.notifier).backupAlbumSelectionDone();
// waited until backup albums are stored in DB // waited until backup albums are stored in DB
await ref.read(albumProvider.notifier).refreshDeviceAlbums(); ref.read(albumProvider.notifier).refreshDeviceAlbums();
}, },
child: const Text("select", style: TextStyle(fontWeight: FontWeight.bold)).tr(), child: const Text("select", style: TextStyle(fontWeight: FontWeight.bold)).tr(),
), ),
@@ -211,7 +211,7 @@ class _BackupAlbumSelectionCard extends ConsumerWidget {
if (currentUser == null) { if (currentUser == null) {
return; return;
} }
unawaited(ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id)); ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id);
}, },
child: const Text("select", style: TextStyle(fontWeight: FontWeight.bold)).tr(), child: const Text("select", style: TextStyle(fontWeight: FontWeight.bold)).tr(),
), ),
@@ -1,5 +1,3 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -56,11 +54,9 @@ class DriftBackupOptionsPage extends ConsumerWidget {
); );
final backupNotifier = ref.read(driftBackupProvider.notifier); final backupNotifier = ref.read(driftBackupProvider.notifier);
unawaited( backupNotifier.cancel().then((_) {
backupNotifier.cancel().then((_) { backupNotifier.startBackup(currentUser.id);
backupNotifier.startBackup(currentUser.id); });
}),
);
} }
}, },
child: Scaffold( child: Scaffold(
@@ -1,12 +1,12 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
import 'package:immich_mobile/utils/bytes_units.dart'; import 'package:immich_mobile/utils/bytes_units.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
@@ -163,8 +163,8 @@ class DriftUploadDetailPage extends ConsumerWidget {
); );
} }
Future<void> _showFileDetailDialog(BuildContext context, DriftUploadStatus item) { Future<void> _showFileDetailDialog(BuildContext context, DriftUploadStatus item) async {
return showDialog( showDialog(
context: context, context: context,
builder: (context) => FileDetailDialog(uploadStatus: item), builder: (context) => FileDetailDialog(uploadStatus: item),
); );
+1 -1
View File
@@ -33,7 +33,7 @@ class ActivitiesPage extends HookConsumerWidget {
Future<void> onAddComment(String comment) async { Future<void> onAddComment(String comment) async {
await activityNotifier.addComment(comment); await activityNotifier.addComment(comment);
// Scroll to the end of the list to show the newly added activity // Scroll to the end of the list to show the newly added activity
await listViewScrollController.animateTo( listViewScrollController.animateTo(
listViewScrollController.position.maxScrollExtent + 200, listViewScrollController.position.maxScrollExtent + 200,
duration: const Duration(milliseconds: 600), duration: const Duration(milliseconds: 600),
curve: Curves.fastOutSlowIn, curve: Curves.fastOutSlowIn,
@@ -1,5 +1,3 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -172,11 +170,11 @@ class CreateAlbumPage extends HookConsumerWidget {
.createAlbum(ref.read(albumTitleProvider), selectedAssets.value); .createAlbum(ref.read(albumTitleProvider), selectedAssets.value);
if (newAlbum != null) { if (newAlbum != null) {
await ref.read(albumProvider.notifier).refreshRemoteAlbums(); ref.read(albumProvider.notifier).refreshRemoteAlbums();
selectedAssets.value = {}; selectedAssets.value = {};
ref.read(albumTitleProvider.notifier).clearAlbumTitle(); ref.read(albumTitleProvider.notifier).clearAlbumTitle();
ref.read(albumViewerProvider.notifier).disableEditAlbum(); ref.read(albumViewerProvider.notifier).disableEditAlbum();
unawaited(context.replaceRoute(AlbumViewerRoute(albumId: newAlbum.id))); context.replaceRoute(AlbumViewerRoute(albumId: newAlbum.id));
} }
} }
@@ -95,7 +95,7 @@ class GalleryViewerPage extends HookConsumerWidget {
} catch (e) { } catch (e) {
// swallow error silently // swallow error silently
log.severe('Error precaching next image: $e'); log.severe('Error precaching next image: $e');
await context.maybePop(); context.maybePop();
} }
} }
@@ -279,13 +279,11 @@ class NativeVideoViewerPage extends HookConsumerWidget {
nc.onPlaybackReady.addListener(onPlaybackReady); nc.onPlaybackReady.addListener(onPlaybackReady);
nc.onPlaybackEnded.addListener(onPlaybackEnded); nc.onPlaybackEnded.addListener(onPlaybackEnded);
unawaited( nc.loadVideoSource(source).catchError((error) {
nc.loadVideoSource(source).catchError((error) { log.severe('Error loading video source: $error');
log.severe('Error loading video source: $error'); });
}),
);
final loopVideo = ref.read(appSettingsServiceProvider).getSetting<bool>(AppSettingsEnum.loopVideo); final loopVideo = ref.read(appSettingsServiceProvider).getSetting<bool>(AppSettingsEnum.loopVideo);
unawaited(nc.setLoop(loopVideo)); nc.setLoop(loopVideo);
controller.value = nc; controller.value = nc;
Timer(const Duration(milliseconds: 200), checkIfBuffering); Timer(const Duration(milliseconds: 200), checkIfBuffering);
@@ -356,12 +354,12 @@ class NativeVideoViewerPage extends HookConsumerWidget {
useOnAppLifecycleStateChange((_, state) async { useOnAppLifecycleStateChange((_, state) async {
if (state == AppLifecycleState.resumed && shouldPlayOnForeground.value) { if (state == AppLifecycleState.resumed && shouldPlayOnForeground.value) {
await controller.value?.play(); controller.value?.play();
} else if (state == AppLifecycleState.paused) { } else if (state == AppLifecycleState.paused) {
final videoPlaying = await controller.value?.isPlaying(); final videoPlaying = await controller.value?.isPlaying();
if (videoPlaying ?? true) { if (videoPlaying ?? true) {
shouldPlayOnForeground.value = true; shouldPlayOnForeground.value = true;
await controller.value?.pause(); controller.value?.pause();
} else { } else {
shouldPlayOnForeground.value = false; shouldPlayOnForeground.value = false;
} }
+30 -34
View File
@@ -1,5 +1,3 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -55,41 +53,39 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
final backgroundManager = ref.read(backgroundSyncProvider); final backgroundManager = ref.read(backgroundSyncProvider);
final backupProvider = ref.read(driftBackupProvider.notifier); final backupProvider = ref.read(driftBackupProvider.notifier);
unawaited( ref.read(authProvider.notifier).saveAuthInfo(accessToken: accessToken).then(
ref.read(authProvider.notifier).saveAuthInfo(accessToken: accessToken).then( (_) async {
(_) async { try {
try { wsProvider.connect();
wsProvider.connect(); infoProvider.getServerInfo();
unawaited(infoProvider.getServerInfo());
if (Store.isBetaTimelineEnabled) { if (Store.isBetaTimelineEnabled) {
await Future.wait([backgroundManager.syncLocal(), backgroundManager.syncRemote()]); await Future.wait([backgroundManager.syncLocal(), backgroundManager.syncRemote()]);
await Future.wait([ await Future.wait([
backgroundManager.hashAssets().then((_) { backgroundManager.hashAssets().then((_) {
_resumeBackup(backupProvider); _resumeBackup(backupProvider);
}), }),
_resumeBackup(backupProvider), _resumeBackup(backupProvider),
]); ]);
if (Store.get(StoreKey.syncAlbums, false)) { if (Store.get(StoreKey.syncAlbums, false)) {
await backgroundManager.syncLinkedAlbum(); await backgroundManager.syncLinkedAlbum();
}
} }
} catch (e) {
log.severe('Failed establishing connection to the server: $e');
} }
}, } catch (e) {
onError: (exception) => { log.severe('Failed establishing connection to the server: $e');
log.severe('Failed to update auth info with access token: $accessToken'), }
ref.read(authProvider.notifier).logout(), },
context.replaceRoute(const LoginRoute()), onError: (exception) => {
}, log.severe('Failed to update auth info with access token: $accessToken'),
), ref.read(authProvider.notifier).logout(),
context.replaceRoute(const LoginRoute()),
},
); );
} else { } else {
log.severe('Missing crucial offline login info - Logging out completely'); log.severe('Missing crucial offline login info - Logging out completely');
unawaited(ref.read(authProvider.notifier).logout()); ref.read(authProvider.notifier).logout();
unawaited(context.replaceRoute(const LoginRoute())); context.replaceRoute(const LoginRoute());
return; return;
} }
@@ -99,11 +95,11 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
final needBetaMigration = Store.get(StoreKey.needBetaMigration, false); final needBetaMigration = Store.get(StoreKey.needBetaMigration, false);
if (needBetaMigration) { if (needBetaMigration) {
await Store.put(StoreKey.needBetaMigration, false); await Store.put(StoreKey.needBetaMigration, false);
unawaited(context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: true)])); context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: true)]);
return; return;
} }
unawaited(context.replaceRoute(Store.isBetaTimelineEnabled ? const TabShellRoute() : const TabControllerRoute())); context.replaceRoute(Store.isBetaTimelineEnabled ? const TabShellRoute() : const TabControllerRoute());
} }
if (Store.isBetaTimelineEnabled) { if (Store.isBetaTimelineEnabled) {
@@ -113,7 +109,7 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
final hasPermission = await ref.read(galleryPermissionNotifier.notifier).hasPermission; final hasPermission = await ref.read(galleryPermissionNotifier.notifier).hasPermission;
if (hasPermission) { if (hasPermission) {
// Resume backup (if enable) then navigate // Resume backup (if enable) then navigate
unawaited(ref.watch(backupProvider.notifier).resumeBackup()); ref.watch(backupProvider.notifier).resumeBackup();
} }
} }
@@ -123,7 +119,7 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
if (isEnableBackup) { if (isEnableBackup) {
final currentUser = Store.tryGet(StoreKey.currentUser); final currentUser = Store.tryGet(StoreKey.currentUser);
if (currentUser != null) { if (currentUser != null) {
unawaited(notifier.handleBackupResume(currentUser.id)); notifier.handleBackupResume(currentUser.id);
} }
} }
} }
+5 -8
View File
@@ -1,16 +1,13 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart';
import 'package:crop_image/crop_image.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:crop_image/crop_image.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/hooks/crop_controller_hook.dart'; import 'package:immich_mobile/utils/hooks/crop_controller_hook.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'edit.page.dart'; import 'edit.page.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:auto_route/auto_route.dart';
/// A widget for cropping an image. /// A widget for cropping an image.
/// This widget uses [HookWidget] to manage its lifecycle and state. It allows /// This widget uses [HookWidget] to manage its lifecycle and state. It allows
@@ -38,7 +35,7 @@ class CropImagePage extends HookWidget {
icon: Icon(Icons.done_rounded, color: context.primaryColor, size: 24), icon: Icon(Icons.done_rounded, color: context.primaryColor, size: 24),
onPressed: () async { onPressed: () async {
final croppedImage = await cropController.croppedImage(); final croppedImage = await cropController.croppedImage();
unawaited(context.pushRoute(EditImageRoute(asset: asset, image: croppedImage, isEdited: true))); context.pushRoute(EditImageRoute(asset: asset, image: croppedImage, isEdited: true));
}, },
), ),
], ],
+5 -6
View File
@@ -1,13 +1,12 @@
import 'dart:async'; import 'dart:async';
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:immich_mobile/constants/filters.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/constants/filters.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:auto_route/auto_route.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
/// A widget for filtering an image. /// A widget for filtering an image.
@@ -75,7 +74,7 @@ class FilterImagePage extends HookWidget {
icon: Icon(Icons.done_rounded, color: context.primaryColor, size: 24), icon: Icon(Icons.done_rounded, color: context.primaryColor, size: 24),
onPressed: () async { onPressed: () async {
final filteredImage = await applyFilterAndConvert(colorFilter.value); final filteredImage = await applyFilterAndConvert(colorFilter.value);
unawaited(context.pushRoute(EditImageRoute(asset: asset, image: filteredImage, isEdited: true))); context.pushRoute(EditImageRoute(asset: asset, image: filteredImage, isEdited: true));
}, },
), ),
], ],
@@ -1,16 +1,14 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart' show useState; import 'package:flutter_hooks/flutter_hooks.dart' show useState;
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/local_auth.provider.dart'; import 'package:immich_mobile/providers/local_auth.provider.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/widgets/forms/pin_registration_form.dart'; import 'package:immich_mobile/widgets/forms/pin_registration_form.dart';
import 'package:immich_mobile/widgets/forms/pin_verification_form.dart'; import 'package:immich_mobile/widgets/forms/pin_verification_form.dart';
import 'package:immich_mobile/entities/store.entity.dart';
@RoutePage() @RoutePage()
class PinAuthPage extends HookConsumerWidget { class PinAuthPage extends HookConsumerWidget {
@@ -37,9 +35,9 @@ class PinAuthPage extends HookConsumerWidget {
); );
if (isBetaTimeline) { if (isBetaTimeline) {
unawaited(context.replaceRoute(const DriftLockedFolderRoute())); context.replaceRoute(const DriftLockedFolderRoute());
} else { } else {
unawaited(context.replaceRoute(const LockedRoute())); context.replaceRoute(const LockedRoute());
} }
} }
} }
@@ -333,7 +333,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
changeExpiry: changeExpiry, changeExpiry: changeExpiry,
); );
ref.invalidate(sharedLinksStateProvider); ref.invalidate(sharedLinksStateProvider);
await context.maybePop(); context.maybePop();
} }
return Scaffold( return Scaffold(
+4 -6
View File
@@ -82,12 +82,10 @@ class PhotosPage extends HookConsumerWidget {
final fullRefresh = refreshCount.value > 0; final fullRefresh = refreshCount.value > 0;
if (fullRefresh) { if (fullRefresh) {
unawaited( Future.wait([
Future.wait([ ref.read(assetProvider.notifier).getAllAsset(clear: true),
ref.read(assetProvider.notifier).getAllAsset(clear: true), ref.read(albumProvider.notifier).refreshRemoteAlbums(),
ref.read(albumProvider.notifier).refreshRemoteAlbums(), ]);
]),
);
// refresh was forced: user requested another refresh within 2 seconds // refresh was forced: user requested another refresh within 2 seconds
refreshCount.value = 0; refreshCount.value = 0;
+4 -5
View File
@@ -1,4 +1,3 @@
import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
@@ -84,7 +83,7 @@ class MapPage extends HookConsumerWidget {
isLoading.value = true; isLoading.value = true;
markers.value = await ref.read(mapMarkersProvider.future); markers.value = await ref.read(mapMarkersProvider.future);
assetsDebouncer.run(updateAssetsInBounds); assetsDebouncer.run(updateAssetsInBounds);
await reloadLayers(); reloadLayers();
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }
@@ -129,7 +128,7 @@ class MapPage extends HookConsumerWidget {
); );
if (marker != null) { if (marker != null) {
await updateAssetMarkerPosition(marker); updateAssetMarkerPosition(marker);
} else { } else {
// If no asset was previously selected and no new asset is available, close the bottom sheet // If no asset was previously selected and no new asset is available, close the bottom sheet
if (selectedMarker.value == null) { if (selectedMarker.value == null) {
@@ -166,7 +165,7 @@ class MapPage extends HookConsumerWidget {
if (asset.isVideo) { if (asset.isVideo) {
ref.read(showControlsProvider.notifier).show = false; ref.read(showControlsProvider.notifier).show = false;
} }
unawaited(context.pushRoute(GalleryViewerRoute(initialIndex: 0, heroOffset: 0, renderList: renderList))); context.pushRoute(GalleryViewerRoute(initialIndex: 0, heroOffset: 0, renderList: renderList));
} }
/// BOTTOM SHEET CALLBACKS /// BOTTOM SHEET CALLBACKS
@@ -210,7 +209,7 @@ class MapPage extends HookConsumerWidget {
} }
if (mapController.value != null && location != null) { if (mapController.value != null && location != null) {
await mapController.value!.animateCamera( mapController.value!.animateCamera(
CameraUpdate.newLatLngZoom(LatLng(location.latitude, location.longitude), mapZoomToAssetLevel), CameraUpdate.newLatLngZoom(LatLng(location.latitude, location.longitude), mapZoomToAssetLevel),
duration: const Duration(milliseconds: 800), duration: const Duration(milliseconds: 800),
); );
@@ -8,9 +8,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart'; import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart';
import 'package:immich_mobile/utils/map_utils.dart';
import 'package:immich_mobile/widgets/map/map_theme_override.dart'; import 'package:immich_mobile/widgets/map/map_theme_override.dart';
import 'package:maplibre_gl/maplibre_gl.dart'; import 'package:maplibre_gl/maplibre_gl.dart';
import 'package:immich_mobile/utils/map_utils.dart';
@RoutePage() @RoutePage()
class MapLocationPickerPage extends HookConsumerWidget { class MapLocationPickerPage extends HookConsumerWidget {
@@ -30,7 +30,7 @@ class MapLocationPickerPage extends HookConsumerWidget {
Future<void> onMapClick(Point<num> point, LatLng centre) async { Future<void> onMapClick(Point<num> point, LatLng centre) async {
selectedLatLng.value = centre; selectedLatLng.value = centre;
await controller.value?.animateCamera(CameraUpdate.newLatLng(centre)); controller.value?.animateCamera(CameraUpdate.newLatLng(centre));
if (marker.value != null) { if (marker.value != null) {
await controller.value?.updateSymbol(marker.value!, SymbolOptions(geometry: centre)); await controller.value?.updateSymbol(marker.value!, SymbolOptions(geometry: centre));
} }
@@ -49,7 +49,7 @@ class MapLocationPickerPage extends HookConsumerWidget {
var currentLatLng = LatLng(currentLocation.latitude, currentLocation.longitude); var currentLatLng = LatLng(currentLocation.latitude, currentLocation.longitude);
selectedLatLng.value = currentLatLng; selectedLatLng.value = currentLatLng;
await controller.value?.animateCamera(CameraUpdate.newLatLng(currentLatLng)); controller.value?.animateCamera(CameraUpdate.newLatLng(currentLatLng));
} }
return MapThemeOverride( return MapThemeOverride(
+2 -2
View File
@@ -266,7 +266,7 @@ class SearchPage extends HookConsumerWidget {
filter.value = filter.value.copyWith(date: SearchDateFilter()); filter.value = filter.value.copyWith(date: SearchDateFilter());
dateRangeCurrentFilterWidget.value = null; dateRangeCurrentFilterWidget.value = null;
unawaited(search()); search();
return; return;
} }
@@ -295,7 +295,7 @@ class SearchPage extends HookConsumerWidget {
); );
} }
unawaited(search()); search();
} }
// MEDIA PICKER // MEDIA PICKER
@@ -1,5 +1,3 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -49,7 +47,7 @@ class DriftAlbumOptionsPage extends HookConsumerWidget {
void leaveAlbum() async { void leaveAlbum() async {
try { try {
await ref.read(remoteAlbumProvider.notifier).leaveAlbum(album.id, userId: userId); await ref.read(remoteAlbumProvider.notifier).leaveAlbum(album.id, userId: userId);
unawaited(context.navigateTo(const DriftAlbumsRoute())); context.navigateTo(const DriftAlbumsRoute());
} catch (_) { } catch (_) {
showErrorMessage(); showErrorMessage();
} }
@@ -1,5 +1,3 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -181,7 +179,7 @@ class _DriftCreateAlbumPageState extends ConsumerState<DriftCreateAlbumPage> {
if (album != null) { if (album != null) {
ref.read(currentRemoteAlbumProvider.notifier).setAlbum(album); ref.read(currentRemoteAlbumProvider.notifier).setAlbum(album);
unawaited(context.replaceRoute(RemoteAlbumRoute(album: album))); context.replaceRoute(RemoteAlbumRoute(album: album));
} }
} }
@@ -1,5 +1,3 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@@ -141,7 +139,7 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
toastType: ToastType.success, toastType: ToastType.success,
); );
unawaited(context.pushRoute(const DriftAlbumsRoute())); context.pushRoute(const DriftAlbumsRoute());
} catch (e) { } catch (e) {
ImmichToast.show( ImmichToast.show(
context: context, context: context,
@@ -163,12 +161,12 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
setState(() { setState(() {
_album = _album.copyWith(name: result.name, description: result.description ?? ''); _album = _album.copyWith(name: result.name, description: result.description ?? '');
}); });
unawaited(HapticFeedback.mediumImpact()); HapticFeedback.mediumImpact();
} }
} }
Future<void> showActivity(BuildContext context) async { Future<void> showActivity(BuildContext context) async {
unawaited(context.pushRoute(const DriftActivitiesRoute())); context.pushRoute(const DriftActivitiesRoute());
} }
void showOptionSheet(BuildContext context) { void showOptionSheet(BuildContext context) {
@@ -209,7 +207,7 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
}, },
onCreateSharedLink: () async { onCreateSharedLink: () async {
context.pop(); context.pop();
unawaited(context.pushRoute(SharedLinkEditRoute(albumId: _album.id))); context.pushRoute(SharedLinkEditRoute(albumId: _album.id));
}, },
onShowOptions: () { onShowOptions: () {
context.pop(); context.pop();
@@ -1,5 +1,3 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:crop_image/crop_image.dart'; import 'package:crop_image/crop_image.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
@@ -36,7 +34,7 @@ class DriftCropImagePage extends HookWidget {
icon: Icon(Icons.done_rounded, color: context.primaryColor, size: 24), icon: Icon(Icons.done_rounded, color: context.primaryColor, size: 24),
onPressed: () async { onPressed: () async {
final croppedImage = await cropController.croppedImage(); final croppedImage = await cropController.croppedImage();
unawaited(context.pushRoute(DriftEditImageRoute(asset: asset, image: croppedImage, isEdited: true))); context.pushRoute(DriftEditImageRoute(asset: asset, image: croppedImage, isEdited: true));
}, },
), ),
], ],
@@ -65,7 +65,7 @@ class DriftEditImagePage extends ConsumerWidget {
Logger("SaveEditedImage").warning("Failed to retrieve the saved image back from OS", e); Logger("SaveEditedImage").warning("Failed to retrieve the saved image back from OS", e);
} }
unawaited(ref.read(backgroundSyncProvider).syncLocal(full: true)); ref.read(backgroundSyncProvider).syncLocal(full: true);
context.navigator.popUntil((route) => route.isFirst); context.navigator.popUntil((route) => route.isFirst);
ImmichToast.show(durationInSecond: 3, context: context, msg: 'Image Saved!'); ImmichToast.show(durationInSecond: 3, context: context, msg: 'Image Saved!');
@@ -75,7 +75,7 @@ class DriftFilterImagePage extends HookWidget {
icon: Icon(Icons.done_rounded, color: context.primaryColor, size: 24), icon: Icon(Icons.done_rounded, color: context.primaryColor, size: 24),
onPressed: () async { onPressed: () async {
final filteredImage = await applyFilterAndConvert(colorFilter.value); final filteredImage = await applyFilterAndConvert(colorFilter.value);
unawaited(context.pushRoute(DriftEditImageRoute(asset: asset, image: filteredImage, isEdited: true))); context.pushRoute(DriftEditImageRoute(asset: asset, image: filteredImage, isEdited: true));
}, },
), ),
], ],
@@ -270,7 +270,7 @@ class DriftSearchPage extends HookConsumerWidget {
filter.value = filter.value.copyWith(date: SearchDateFilter()); filter.value = filter.value.copyWith(date: SearchDateFilter());
dateRangeCurrentFilterWidget.value = null; dateRangeCurrentFilterWidget.value = null;
unawaited(search()); search();
return; return;
} }
@@ -300,7 +300,7 @@ class DriftSearchPage extends HookConsumerWidget {
); );
} }
unawaited(search()); search();
} }
// MEDIA PICKER // MEDIA PICKER
@@ -1,5 +1,3 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/constants/enums.dart';
@@ -17,7 +15,7 @@ class AdvancedInfoActionButton extends ConsumerWidget {
return; return;
} }
unawaited(ref.read(actionProvider.notifier).troubleshoot(source, context)); ref.read(actionProvider.notifier).troubleshoot(source, context);
} }
@override @override
@@ -39,7 +39,7 @@ class ShareActionButton extends ConsumerWidget {
return; return;
} }
await showDialog( showDialog(
context: context, context: context,
builder: (BuildContext buildContext) { builder: (BuildContext buildContext) {
ref.read(actionProvider.notifier).shareAssets(source).then((ActionResult result) { ref.read(actionProvider.notifier).shareAssets(source).then((ActionResult result) {
@@ -121,7 +121,7 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
// we need to re-filter the albums after sorting // we need to re-filter the albums after sorting
// so shownAlbums gets updated // so shownAlbums gets updated
unawaited(filterAlbums()); filterAlbums();
} }
Future<void> filterAlbums() async { Future<void> filterAlbums() async {
@@ -711,7 +711,7 @@ class AddToAlbumHeader extends ConsumerWidget {
ref.read(currentRemoteAlbumProvider.notifier).setAlbum(newAlbum); ref.read(currentRemoteAlbumProvider.notifier).setAlbum(newAlbum);
ref.read(multiSelectProvider.notifier).reset(); ref.read(multiSelectProvider.notifier).reset();
unawaited(context.pushRoute(RemoteAlbumRoute(album: newAlbum))); context.pushRoute(RemoteAlbumRoute(album: newAlbum));
} }
return SliverPadding( return SliverPadding(
@@ -634,9 +634,9 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
// Listen for control visibility changes and change system UI mode accordingly // Listen for control visibility changes and change system UI mode accordingly
ref.listen(assetViewerProvider.select((value) => value.showingControls), (_, showingControls) async { ref.listen(assetViewerProvider.select((value) => value.showingControls), (_, showingControls) async {
if (showingControls) { if (showingControls) {
unawaited(SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge)); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
} else { } else {
unawaited(SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky)); SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
} }
}); });
@@ -298,13 +298,11 @@ class NativeVideoViewer extends HookConsumerWidget {
nc.onPlaybackReady.addListener(onPlaybackReady); nc.onPlaybackReady.addListener(onPlaybackReady);
nc.onPlaybackEnded.addListener(onPlaybackEnded); nc.onPlaybackEnded.addListener(onPlaybackEnded);
unawaited( nc.loadVideoSource(source).catchError((error) {
nc.loadVideoSource(source).catchError((error) { log.severe('Error loading video source: $error');
log.severe('Error loading video source: $error'); });
}),
);
final loopVideo = ref.read(appSettingsServiceProvider).getSetting<bool>(AppSettingsEnum.loopVideo); final loopVideo = ref.read(appSettingsServiceProvider).getSetting<bool>(AppSettingsEnum.loopVideo);
unawaited(nc.setLoop(!asset.isMotionPhoto && loopVideo)); nc.setLoop(!asset.isMotionPhoto && loopVideo);
controller.value = nc; controller.value = nc;
Timer(const Duration(milliseconds: 200), checkIfBuffering); Timer(const Duration(milliseconds: 200), checkIfBuffering);
@@ -375,12 +373,12 @@ class NativeVideoViewer extends HookConsumerWidget {
useOnAppLifecycleStateChange((_, state) async { useOnAppLifecycleStateChange((_, state) async {
if (state == AppLifecycleState.resumed && shouldPlayOnForeground.value) { if (state == AppLifecycleState.resumed && shouldPlayOnForeground.value) {
await controller.value?.play(); controller.value?.play();
} else if (state == AppLifecycleState.paused) { } else if (state == AppLifecycleState.paused) {
final videoPlaying = await controller.value?.isPlaying(); final videoPlaying = await controller.value?.isPlaying();
if (videoPlaying ?? true) { if (videoPlaying ?? true) {
shouldPlayOnForeground.value = true; shouldPlayOnForeground.value = true;
await controller.value?.pause(); controller.value?.pause();
} else { } else {
shouldPlayOnForeground.value = false; shouldPlayOnForeground.value = false;
} }
@@ -1,5 +1,3 @@
import 'dart:async';
import 'package:async/async.dart'; import 'package:async/async.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
@@ -53,14 +51,14 @@ mixin CancellableImageProviderMixin<T extends Object> on CancellableImageProvide
Stream<ImageInfo> loadRequest(ImageRequest request, ImageDecoderCallback decode) async* { Stream<ImageInfo> loadRequest(ImageRequest request, ImageDecoderCallback decode) async* {
if (isCancelled) { if (isCancelled) {
this.request = null; this.request = null;
unawaited(evict()); evict();
return; return;
} }
try { try {
final image = await request.load(decode); final image = await request.load(decode);
if (image == null || isCancelled) { if (image == null || isCancelled) {
unawaited(evict()); evict();
return; return;
} }
yield image; yield image;
@@ -85,7 +85,7 @@ class LocalFullImageProvider extends CancellableImageProvider<LocalFullImageProv
yield* initialImageStream(); yield* initialImageStream();
if (isCancelled) { if (isCancelled) {
unawaited(evict()); evict();
return; return;
} }
@@ -7,13 +7,11 @@ import 'package:immich_mobile/domain/services/setting.service.dart';
import 'package:immich_mobile/infrastructure/loaders/image_request.dart'; import 'package:immich_mobile/infrastructure/loaders/image_request.dart';
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
import 'package:immich_mobile/presentation/widgets/images/one_frame_multi_image_stream_completer.dart'; import 'package:immich_mobile/presentation/widgets/images/one_frame_multi_image_stream_completer.dart';
import 'package:immich_mobile/providers/image/cache/remote_image_cache_manager.dart';
import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart'; import 'package:immich_mobile/utils/image_url_builder.dart';
class RemoteThumbProvider extends CancellableImageProvider<RemoteThumbProvider> class RemoteThumbProvider extends CancellableImageProvider<RemoteThumbProvider>
with CancellableImageProviderMixin<RemoteThumbProvider> { with CancellableImageProviderMixin<RemoteThumbProvider> {
static final cacheManager = RemoteThumbnailCacheManager();
final String assetId; final String assetId;
RemoteThumbProvider({required this.assetId}); RemoteThumbProvider({required this.assetId});
@@ -39,7 +37,6 @@ class RemoteThumbProvider extends CancellableImageProvider<RemoteThumbProvider>
final request = this.request = RemoteImageRequest( final request = this.request = RemoteImageRequest(
uri: getThumbnailUrlForRemoteId(key.assetId), uri: getThumbnailUrlForRemoteId(key.assetId),
headers: ApiService.getRequestHeaders(), headers: ApiService.getRequestHeaders(),
cacheManager: cacheManager,
); );
return loadRequest(request, decode); return loadRequest(request, decode);
} }
@@ -60,7 +57,6 @@ class RemoteThumbProvider extends CancellableImageProvider<RemoteThumbProvider>
class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImageProvider> class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImageProvider>
with CancellableImageProviderMixin<RemoteFullImageProvider> { with CancellableImageProviderMixin<RemoteFullImageProvider> {
static final cacheManager = RemoteThumbnailCacheManager();
final String assetId; final String assetId;
RemoteFullImageProvider({required this.assetId}); RemoteFullImageProvider({required this.assetId});
@@ -87,20 +83,16 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
yield* initialImageStream(); yield* initialImageStream();
if (isCancelled) { if (isCancelled) {
unawaited(evict()); evict();
return; return;
} }
final headers = ApiService.getRequestHeaders(); final headers = ApiService.getRequestHeaders();
final request = this.request = RemoteImageRequest( final request = this.request = RemoteImageRequest(uri: getPreviewUrlForRemoteId(key.assetId), headers: headers);
uri: getPreviewUrlForRemoteId(key.assetId),
headers: headers,
cacheManager: cacheManager,
);
yield* loadRequest(request, decode); yield* loadRequest(request, decode);
if (isCancelled) { if (isCancelled) {
unawaited(evict()); evict();
return; return;
} }
@@ -9,8 +9,8 @@ import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/bottom_sheet/map_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/map_bottom_sheet.widget.dart';
import 'package:immich_mobile/presentation/widgets/map/map.state.dart';
import 'package:immich_mobile/presentation/widgets/map/map_utils.dart'; import 'package:immich_mobile/presentation/widgets/map/map_utils.dart';
import 'package:immich_mobile/presentation/widgets/map/map.state.dart';
import 'package:immich_mobile/utils/async_mutex.dart'; import 'package:immich_mobile/utils/async_mutex.dart';
import 'package:immich_mobile/utils/debounce.dart'; import 'package:immich_mobile/utils/debounce.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart';
@@ -114,14 +114,12 @@ class _DriftMapState extends ConsumerState<DriftMap> {
} }
final bounds = await controller.getVisibleRegion(); final bounds = await controller.getVisibleRegion();
unawaited( _reloadMutex.run(() async {
_reloadMutex.run(() async { if (mounted && ref.read(mapStateProvider.notifier).setBounds(bounds)) {
if (mounted && ref.read(mapStateProvider.notifier).setBounds(bounds)) { final markers = await ref.read(mapMarkerProvider(bounds).future);
final markers = await ref.read(mapMarkerProvider(bounds).future); await reloadMarkers(markers);
await reloadMarkers(markers); }
} });
}),
);
} }
Future<void> reloadMarkers(Map<String, dynamic> markers) async { Future<void> reloadMarkers(Map<String, dynamic> markers) async {
@@ -149,7 +147,7 @@ class _DriftMapState extends ConsumerState<DriftMap> {
final controller = mapController; final controller = mapController;
if (controller != null && location != null) { if (controller != null && location != null) {
await controller.animateCamera( controller.animateCamera(
CameraUpdate.newLatLngZoom(LatLng(location.latitude, location.longitude), MapUtils.mapZoomToAssetLevel), CameraUpdate.newLatLngZoom(LatLng(location.latitude, location.longitude), MapUtils.mapZoomToAssetLevel),
duration: const Duration(milliseconds: 800), duration: const Duration(milliseconds: 800),
); );
@@ -73,7 +73,7 @@ class MapUtils {
try { try {
bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled && !silent) { if (!serviceEnabled && !silent) {
unawaited(showDialog(context: context, builder: (context) => _LocationServiceDisabledDialog(context))); showDialog(context: context, builder: (context) => _LocationServiceDisabledDialog(context));
return (null, LocationPermission.deniedForever); return (null, LocationPermission.deniedForever);
} }
@@ -2,9 +2,11 @@ import 'dart:ui';
const double kTimelineHeaderExtent = 80.0; const double kTimelineHeaderExtent = 80.0;
const Size kTimelineFixedTileExtent = Size.square(256); const Size kTimelineFixedTileExtent = Size.square(256);
const Size kThumbnailResolution = Size.square(320); // TODO: make the resolution vary based on actual tile size
const double kTimelineSpacing = 2.0; const double kTimelineSpacing = 2.0;
const int kTimelineColumnCount = 3; const int kTimelineColumnCount = 3;
const Duration kTimelineScrubberFadeInDuration = Duration(milliseconds: 300); const Duration kTimelineScrubberFadeInDuration = Duration(milliseconds: 300);
const Duration kTimelineScrubberFadeOutDuration = Duration(milliseconds: 800); const Duration kTimelineScrubberFadeOutDuration = Duration(milliseconds: 800);
const Size kThumbnailResolution = Size.square(320); // TODO: make the resolution vary based on actual tile size
const kThumbnailDiskCacheSize = 1024 << 20; // 1GiB
@@ -1,4 +1,3 @@
import 'dart:async';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
@@ -16,8 +15,8 @@ import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart'
import 'package:immich_mobile/presentation/widgets/timeline/timeline_drag_region.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline_drag_region.dart';
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
@@ -157,13 +156,11 @@ class _AssetTileWidget extends ConsumerWidget {
await ref.read(timelineServiceProvider).loadAssets(assetIndex, 1); await ref.read(timelineServiceProvider).loadAssets(assetIndex, 1);
ref.read(isPlayingMotionVideoProvider.notifier).playing = false; ref.read(isPlayingMotionVideoProvider.notifier).playing = false;
AssetViewer.setAsset(ref, asset); AssetViewer.setAsset(ref, asset);
unawaited( ctx.pushRoute(
ctx.pushRoute( AssetViewerRoute(
AssetViewerRoute( initialIndex: assetIndex,
initialIndex: assetIndex, timelineService: ref.read(timelineServiceProvider),
timelineService: ref.read(timelineServiceProvider), heroOffset: heroOffset,
heroOffset: heroOffset,
),
), ),
); );
} }
@@ -233,7 +233,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
} }
try { try {
await LogService.I.flush(); LogService.I.flush();
} catch (_) {} } catch (_) {}
} }
@@ -242,7 +242,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
// Flush logs before closing database // Flush logs before closing database
try { try {
await LogService.I.flush(); LogService.I.flush();
} catch (_) {} } catch (_) {}
// Close Isar database safely // Close Isar database safely
+1 -1
View File
@@ -98,7 +98,7 @@ class AssetNotifier extends StateNotifier<bool> {
Future<void> onNewAssetUploaded(Asset newAsset) async { Future<void> onNewAssetUploaded(Asset newAsset) async {
// eTag on device is not valid after partially modifying the assets // eTag on device is not valid after partially modifying the assets
await Store.delete(StoreKey.assetETag); Store.delete(StoreKey.assetETag);
await _syncService.syncNewAssetToDb(newAsset); await _syncService.syncNewAssetToDb(newAsset);
} }
@@ -1,16 +1,14 @@
import 'dart:async';
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/download/download_state.model.dart'; import 'package:immich_mobile/models/download/download_state.model.dart';
import 'package:immich_mobile/models/download/livephotos_medatada.model.dart'; import 'package:immich_mobile/models/download/livephotos_medatada.model.dart';
import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/services/album.service.dart';
import 'package:immich_mobile/services/download.service.dart'; import 'package:immich_mobile/services/download.service.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/services/share.service.dart'; import 'package:immich_mobile/services/share.service.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:immich_mobile/widgets/common/share_dialog.dart'; import 'package:immich_mobile/widgets/common/share_dialog.dart';
@@ -161,26 +159,24 @@ class DownloadStateNotifier extends StateNotifier<DownloadState> {
} }
void shareAsset(Asset asset, BuildContext context) async { void shareAsset(Asset asset, BuildContext context) async {
unawaited( showDialog(
showDialog( context: context,
context: context, builder: (BuildContext buildContext) {
builder: (BuildContext buildContext) { _shareService.shareAsset(asset, context).then((bool status) {
_shareService.shareAsset(asset, context).then((bool status) { if (!status) {
if (!status) { ImmichToast.show(
ImmichToast.show( context: context,
context: context, msg: 'image_viewer_page_state_provider_share_error'.tr(),
msg: 'image_viewer_page_state_provider_share_error'.tr(), toastType: ToastType.error,
toastType: ToastType.error, gravity: ToastGravity.BOTTOM,
gravity: ToastGravity.BOTTOM, );
); }
} buildContext.pop();
buildContext.pop(); });
}); return const ShareDialog();
return const ShareDialog(); },
}, barrierDismissible: false,
barrierDismissible: false, useRootNavigator: false,
useRootNavigator: false,
),
); );
} }
} }
@@ -104,7 +104,7 @@ class ShareIntentUploadStateNotifier extends StateNotifier<List<ShareIntentAttac
Future<void> upload(File file) async { Future<void> upload(File file) async {
final task = await _buildUploadTask(hash(file.path).toString(), file); final task = await _buildUploadTask(hash(file.path).toString(), file);
await _uploadService.enqueueTasks([task]); _uploadService.enqueueTasks([task]);
} }
Future<UploadTask> _buildUploadTask(String id, File file, {Map<String, String>? fields}) async { Future<UploadTask> _buildUploadTask(String id, File file, {Map<String, String>? fields}) async {
@@ -380,7 +380,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
state = state.copyWith(backgroundBackup: isEnabled); state = state.copyWith(backgroundBackup: isEnabled);
if (isEnabled != Store.get(StoreKey.backgroundBackup, !isEnabled)) { if (isEnabled != Store.get(StoreKey.backgroundBackup, !isEnabled)) {
await Store.put(StoreKey.backgroundBackup, isEnabled); Store.put(StoreKey.backgroundBackup, isEnabled);
} }
if (state.backupProgress != BackUpProgressEnum.inBackground) { if (state.backupProgress != BackUpProgressEnum.inBackground) {
@@ -474,7 +474,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
); );
await notifyBackgroundServiceCanRun(); await notifyBackgroundServiceCanRun();
} else { } else {
await openAppSettings(); openAppSettings();
} }
} }
@@ -533,10 +533,10 @@ class BackupNotifier extends StateNotifier<BackUpState> {
progressInFileSpeedUpdateTime: DateTime.now(), progressInFileSpeedUpdateTime: DateTime.now(),
progressInFileSpeedUpdateSentBytes: 0, progressInFileSpeedUpdateSentBytes: 0,
); );
await _updatePersistentAlbumsSelection(); _updatePersistentAlbumsSelection();
} }
await updateDiskInfo(); updateDiskInfo();
} }
void _onUploadProgress(int sent, int total) { void _onUploadProgress(int sent, int total) {
@@ -2,10 +2,10 @@ import 'dart:async';
import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/services/backup_verification.service.dart'; import 'package:immich_mobile/services/backup_verification.service.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -44,7 +44,7 @@ class BackupVerification extends _$BackupVerification {
} }
return; return;
} }
unawaited(WakelockPlus.enable()); WakelockPlus.enable();
const limit = 100; const limit = 100;
final toDelete = await ref.read(backupVerificationServiceProvider).findWronglyBackedUpAssets(limit: limit); final toDelete = await ref.read(backupVerificationServiceProvider).findWronglyBackedUpAssets(limit: limit);
@@ -73,7 +73,7 @@ class BackupVerification extends _$BackupVerification {
} }
} }
} finally { } finally {
unawaited(WakelockPlus.disable()); WakelockPlus.disable();
state = false; state = false;
} }
} }
@@ -7,7 +7,7 @@ part of 'backup_verification.provider.dart';
// ************************************************************************** // **************************************************************************
String _$backupVerificationHash() => String _$backupVerificationHash() =>
r'b4b34909ed1af3f28877ea457d53a4a18b6417f8'; r'b204e43ab575d5fa5b2ee663297f32bcee9074f5';
/// See also [BackupVerification]. /// See also [BackupVerification].
@ProviderFor(BackupVerification) @ProviderFor(BackupVerification)
@@ -1,4 +1,3 @@
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:cancellation_token_http/http.dart'; import 'package:cancellation_token_http/http.dart';
@@ -27,11 +26,11 @@ import 'package:immich_mobile/services/backup.service.dart';
import 'package:immich_mobile/services/backup_album.service.dart'; import 'package:immich_mobile/services/backup_album.service.dart';
import 'package:immich_mobile/services/local_notification.service.dart'; import 'package:immich_mobile/services/local_notification.service.dart';
import 'package:immich_mobile/utils/backup_progress.dart'; import 'package:immich_mobile/utils/backup_progress.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:photo_manager/photo_manager.dart' show PMProgressHandler; import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
import 'package:immich_mobile/utils/debug_print.dart';
final manualUploadProvider = StateNotifierProvider<ManualUploadNotifier, ManualUploadState>((ref) { final manualUploadProvider = StateNotifierProvider<ManualUploadNotifier, ManualUploadState>((ref) {
return ManualUploadNotifier( return ManualUploadNotifier(
@@ -295,7 +294,7 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
); );
} }
} else { } else {
unawaited(openAppSettings()); openAppSettings();
dPrint(() => "[_startUpload] Do not have permission to the gallery"); dPrint(() => "[_startUpload] Do not have permission to the gallery");
} }
} catch (e) { } catch (e) {
@@ -1,148 +1,25 @@
import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart';
// ignore: implementation_imports
import 'package:flutter_cache_manager/src/cache_store.dart';
import 'package:logging/logging.dart';
import 'package:uuid/uuid.dart';
abstract class RemoteCacheManager extends CacheManager { class RemoteImageCacheManager extends CacheManager {
static final _log = Logger('RemoteCacheManager');
RemoteCacheManager.custom(super.config, CacheStore store)
// Unfortunately, CacheStore is not a public API
// ignore: invalid_use_of_visible_for_testing_member
: super.custom(cacheStore: store);
Future<void> putStreamedFile(
String url,
Stream<List<int>> source, {
String? key,
String? eTag,
Duration maxAge = const Duration(days: 30),
String fileExtension = 'file',
});
// Unlike `putFileStream`, this method handles request cancellation,
// does not make a (slow) DB call checking if the file is already cached,
// does not synchronously check if a file exists,
// and deletes the file on cancellation without making these checks again.
Future<void> putStreamedFileToStore(
CacheStore store,
String url,
Stream<List<int>> source, {
String? key,
String? eTag,
Duration maxAge = const Duration(days: 30),
String fileExtension = 'file',
}) async {
final path = '${const Uuid().v1()}.$fileExtension';
final file = await store.fileSystem.createFile(path);
final sink = file.openWrite();
try {
await source.listen(sink.add, cancelOnError: true).asFuture();
} catch (e) {
try {
await sink.close();
await file.delete();
} catch (e) {
_log.severe('Failed to delete incomplete cache file: $e');
}
return;
}
try {
await sink.flush();
await sink.close();
} catch (e) {
try {
await file.delete();
} catch (e) {
_log.severe('Failed to delete incomplete cache file: $e');
}
return;
}
final cacheObject = CacheObject(
url,
key: key,
relativePath: path,
validTill: DateTime.now().add(maxAge),
eTag: eTag,
);
try {
await store.putFile(cacheObject);
} catch (e) {
try {
await file.delete();
} catch (e) {
_log.severe('Failed to delete untracked cache file: $e');
}
}
}
}
class RemoteImageCacheManager extends RemoteCacheManager {
static const key = 'remoteImageCacheKey'; static const key = 'remoteImageCacheKey';
static final RemoteImageCacheManager _instance = RemoteImageCacheManager._(); static final RemoteImageCacheManager _instance = RemoteImageCacheManager._();
static final _config = Config(key, maxNrOfCacheObjects: 500, stalePeriod: const Duration(days: 30)); static final _config = Config(key, maxNrOfCacheObjects: 500, stalePeriod: const Duration(days: 30));
static final _store = CacheStore(_config);
factory RemoteImageCacheManager() { factory RemoteImageCacheManager() {
return _instance; return _instance;
} }
RemoteImageCacheManager._() : super.custom(_config, _store); RemoteImageCacheManager._() : super(_config);
@override
Future<void> putStreamedFile(
String url,
Stream<List<int>> source, {
String? key,
String? eTag,
Duration maxAge = const Duration(days: 30),
String fileExtension = 'file',
}) {
return putStreamedFileToStore(
_store,
url,
source,
key: key,
eTag: eTag,
maxAge: maxAge,
fileExtension: fileExtension,
);
}
} }
/// The cache manager for full size images [ImmichRemoteImageProvider] class RemoteThumbnailCacheManager extends CacheManager {
class RemoteThumbnailCacheManager extends RemoteCacheManager {
static const key = 'remoteThumbnailCacheKey'; static const key = 'remoteThumbnailCacheKey';
static final RemoteThumbnailCacheManager _instance = RemoteThumbnailCacheManager._(); static final RemoteThumbnailCacheManager _instance = RemoteThumbnailCacheManager._();
static final _config = Config(key, maxNrOfCacheObjects: 5000, stalePeriod: const Duration(days: 30)); static final _config = Config(key, maxNrOfCacheObjects: 5000, stalePeriod: const Duration(days: 30));
static final _store = CacheStore(_config);
factory RemoteThumbnailCacheManager() { factory RemoteThumbnailCacheManager() {
return _instance; return _instance;
} }
RemoteThumbnailCacheManager._() : super.custom(_config, _store); RemoteThumbnailCacheManager._() : super(_config);
@override
Future<void> putStreamedFile(
String url,
Stream<List<int>> source, {
String? key,
String? eTag,
Duration maxAge = const Duration(days: 30),
String fileExtension = 'file',
}) {
return putStreamedFileToStore(
_store,
url,
source,
key: key,
eTag: eTag,
maxAge: maxAge,
fileExtension: fileExtension,
);
}
} }
@@ -3,6 +3,7 @@ import 'dart:io';
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart'; import 'package:flutter/painting.dart';
import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart';
@@ -76,7 +77,7 @@ class ImmichLocalImageProvider extends ImageProvider<ImmichLocalImageProvider> {
} catch (error, stack) { } catch (error, stack) {
log.severe('Error loading local image ${asset.fileName}', error, stack); log.severe('Error loading local image ${asset.fileName}', error, stack);
} finally { } finally {
unawaited(chunkEvents.close()); chunkEvents.close();
} }
} }
@@ -1,5 +1,3 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -66,7 +64,7 @@ class ActionNotifier extends Notifier<void> {
void _downloadLivePhotoCallback(TaskStatusUpdate update) async { void _downloadLivePhotoCallback(TaskStatusUpdate update) async {
if (update.status == TaskStatus.complete) { if (update.status == TaskStatus.complete) {
final livePhotosId = LivePhotosMetadata.fromJson(update.task.metaData).id; final livePhotosId = LivePhotosMetadata.fromJson(update.task.metaData).id;
unawaited(_downloadService.saveLivePhotos(update.task, livePhotosId)); _downloadService.saveLivePhotos(update.task, livePhotosId);
} }
} }
@@ -124,7 +122,7 @@ class ActionNotifier extends Notifier<void> {
if (assets.length > 1) { if (assets.length > 1) {
return ActionResult(count: assets.length, success: false, error: 'Cannot troubleshoot multiple assets'); return ActionResult(count: assets.length, success: false, error: 'Cannot troubleshoot multiple assets');
} }
unawaited(context.pushRoute(AssetTroubleshootRoute(asset: assets.first))); context.pushRoute(AssetTroubleshootRoute(asset: assets.first));
return ActionResult(count: assets.length, success: true); return ActionResult(count: assets.length, success: true);
} }
@@ -1,5 +1,3 @@
import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/models/shared_link/shared_link.model.dart'; import 'package:immich_mobile/models/shared_link/shared_link.model.dart';
import 'package:immich_mobile/services/shared_link.service.dart'; import 'package:immich_mobile/services/shared_link.service.dart';
@@ -18,7 +16,7 @@ class SharedLinksNotifier extends StateNotifier<AsyncValue<List<SharedLink>>> {
Future<void> deleteLink(String id) async { Future<void> deleteLink(String id) async {
await _sharedLinkService.deleteSharedLink(id); await _sharedLinkService.deleteSharedLink(id);
state = const AsyncLoading(); state = const AsyncLoading();
unawaited(fetchLinks()); fetchLinks();
} }
} }
@@ -1,18 +1,17 @@
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart' as asset_entity; import 'package:immich_mobile/entities/asset.entity.dart' as asset_entity;
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/response_extensions.dart';
import 'package:immich_mobile/repositories/asset_api.repository.dart'; import 'package:immich_mobile/repositories/asset_api.repository.dart';
import 'package:immich_mobile/utils/hash.dart'; import 'package:immich_mobile/utils/hash.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:photo_manager/photo_manager.dart'; import 'package:photo_manager/photo_manager.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/extensions/response_extensions.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
final assetMediaRepositoryProvider = Provider((ref) => AssetMediaRepository(ref.watch(assetApiRepositoryProvider))); final assetMediaRepositoryProvider = Provider((ref) => AssetMediaRepository(ref.watch(assetApiRepositoryProvider)));
@@ -107,17 +106,15 @@ class AssetMediaRepository {
// we dont want to await the share result since the // we dont want to await the share result since the
// "preparing" dialog will not disappear unti // "preparing" dialog will not disappear unti
unawaited( Share.shareXFiles(downloadedXFiles).then((result) async {
Share.shareXFiles(downloadedXFiles).then((result) async { for (var file in downloadedXFiles) {
for (var file in downloadedXFiles) { try {
try { await File(file.path).delete();
await File(file.path).delete(); } catch (e) {
} catch (e) { _log.warning("Failed to delete temporary file: ${file.path}", e);
_log.warning("Failed to delete temporary file: ${file.path}", e);
}
} }
}), }
); });
return downloadedXFiles.length; return downloadedXFiles.length;
} }
+14 -7
View File
@@ -1,12 +1,14 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:cancellation_token_http/http.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:http/http.dart';
import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/utils/debug_print.dart';
@@ -20,6 +22,8 @@ class UploadTaskWithFile {
final uploadRepositoryProvider = Provider((ref) => UploadRepository()); final uploadRepositoryProvider = Provider((ref) => UploadRepository());
class UploadRepository { class UploadRepository {
static final _client = const NetworkRepository().getHttpClient('upload');
void Function(TaskStatusUpdate)? onUploadStatus; void Function(TaskStatusUpdate)? onUploadStatus;
void Function(TaskProgressUpdate)? onTaskProgress; void Function(TaskProgressUpdate)? onTaskProgress;
@@ -92,13 +96,12 @@ class UploadRepository {
); );
} }
Future<void> backupWithDartClient(Iterable<UploadTaskWithFile> tasks, CancellationToken cancelToken) async { Future<void> backupWithDartClient(Iterable<UploadTaskWithFile> tasks, Completer cancelToken) async {
final httpClient = Client();
final String savedEndpoint = Store.get(StoreKey.serverEndpoint); final String savedEndpoint = Store.get(StoreKey.serverEndpoint);
Logger logger = Logger('UploadRepository'); Logger logger = Logger('UploadRepository');
for (final candidate in tasks) { for (final candidate in tasks) {
if (cancelToken.isCancelled) { if (cancelToken.isCompleted) {
logger.warning("Backup was cancelled by the user"); logger.warning("Backup was cancelled by the user");
break; break;
} }
@@ -112,13 +115,17 @@ class UploadRepository {
filename: candidate.task.filename, filename: candidate.task.filename,
); );
final baseRequest = MultipartRequest('POST', Uri.parse('$savedEndpoint/assets')); final baseRequest = AbortableMultipartRequest(
'POST',
Uri.parse('$savedEndpoint/assets'),
abortTrigger: cancelToken.future,
)..headers['Accept'] = 'application/json';
baseRequest.headers.addAll(candidate.task.headers); baseRequest.headers.addAll(candidate.task.headers);
baseRequest.fields.addAll(candidate.task.fields); baseRequest.fields.addAll(candidate.task.fields);
baseRequest.files.add(assetRawUploadData); baseRequest.files.add(assetRawUploadData);
final response = await httpClient.send(baseRequest, cancellationToken: cancelToken); final response = await _client.send(baseRequest);
final responseBody = jsonDecode(await response.stream.bytesToString()); final responseBody = jsonDecode(await response.stream.bytesToString());
@@ -131,7 +138,7 @@ class UploadRepository {
continue; continue;
} }
} on CancelledException { } on RequestAbortedException {
logger.warning("Backup was cancelled by the user"); logger.warning("Backup was cancelled by the user");
break; break;
} catch (error, stackTrace) { } catch (error, stackTrace) {
@@ -1,5 +1,3 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -14,7 +12,7 @@ class AppNavigationObserver extends AutoRouterObserver {
@override @override
Future<void> didChangeTabRoute(TabPageRoute route, TabPageRoute previousRoute) async { Future<void> didChangeTabRoute(TabPageRoute route, TabPageRoute previousRoute) async {
unawaited(Future(() => ref.read(inLockedViewProvider.notifier).state = false)); Future(() => ref.read(inLockedViewProvider.notifier).state = false);
} }
@override @override
+3 -4
View File
@@ -1,4 +1,3 @@
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
@@ -27,18 +26,18 @@ class AuthGuard extends AutoRouteGuard {
if (res == null || res.authStatus != true) { if (res == null || res.authStatus != true) {
// If the access token is invalid, take user back to login // If the access token is invalid, take user back to login
_log.fine('User token is invalid. Redirecting to login'); _log.fine('User token is invalid. Redirecting to login');
unawaited(router.replaceAll([const LoginRoute()])); router.replaceAll([const LoginRoute()]);
} }
} on StoreKeyNotFoundException catch (_) { } on StoreKeyNotFoundException catch (_) {
// If there is no access token, take us to the login page // If there is no access token, take us to the login page
_log.warning('No access token in the store.'); _log.warning('No access token in the store.');
unawaited(router.replaceAll([const LoginRoute()])); router.replaceAll([const LoginRoute()]);
return; return;
} on ApiException catch (e) { } on ApiException catch (e) {
// On an unauthorized request, take us to the login page // On an unauthorized request, take us to the login page
if (e.code == HttpStatus.unauthorized) { if (e.code == HttpStatus.unauthorized) {
_log.warning("Unauthorized access token."); _log.warning("Unauthorized access token.");
unawaited(router.replaceAll([const LoginRoute()])); router.replaceAll([const LoginRoute()]);
return; return;
} }
} catch (e) { } catch (e) {
@@ -1,5 +1,3 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
@@ -15,7 +13,7 @@ class BackupPermissionGuard extends AutoRouteGuard {
if (p) { if (p) {
resolver.next(true); resolver.next(true);
} else { } else {
unawaited(router.push(const PermissionOnboardingRoute())); router.push(const PermissionOnboardingRoute());
} }
} }
} }
+6 -10
View File
@@ -1,5 +1,3 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
@@ -15,14 +13,12 @@ class GalleryGuard extends AutoRouteGuard {
// Replace instead of pushing duplicate // Replace instead of pushing duplicate
final args = resolver.route.args as GalleryViewerRouteArgs; final args = resolver.route.args as GalleryViewerRouteArgs;
unawaited( router.replace(
router.replace( GalleryViewerRoute(
GalleryViewerRoute( renderList: args.renderList,
renderList: args.renderList, initialIndex: args.initialIndex,
initialIndex: args.initialIndex, heroOffset: args.heroOffset,
heroOffset: args.heroOffset, showStack: args.showStack,
showStack: args.showStack,
),
), ),
); );
// Prevent further navigation since we replaced the route // Prevent further navigation since we replaced the route
+4 -5
View File
@@ -1,9 +1,8 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/local_auth.service.dart'; import 'package:immich_mobile/services/local_auth.service.dart';
import 'package:immich_mobile/services/secure_storage.service.dart'; import 'package:immich_mobile/services/secure_storage.service.dart';
@@ -31,7 +30,7 @@ class LockedGuard extends AutoRouteGuard {
/// Check if a pincode has been created but this user. Show the form to create if not exist /// Check if a pincode has been created but this user. Show the form to create if not exist
if (!authStatus.pinCode) { if (!authStatus.pinCode) {
unawaited(router.push(PinAuthRoute(createPinCode: true))); router.push(PinAuthRoute(createPinCode: true));
} }
if (authStatus.isElevated) { if (authStatus.isElevated) {
@@ -43,7 +42,7 @@ class LockedGuard extends AutoRouteGuard {
/// the user has enabled the biometric authentication /// the user has enabled the biometric authentication
final securePinCode = await _secureStorageService.read(kSecuredPinCode); final securePinCode = await _secureStorageService.read(kSecuredPinCode);
if (securePinCode == null) { if (securePinCode == null) {
unawaited(router.push(PinAuthRoute())); router.push(PinAuthRoute());
return; return;
} }
@@ -75,7 +74,7 @@ class LockedGuard extends AutoRouteGuard {
} on ApiException { } on ApiException {
// PIN code has changed, need to re-enter to access // PIN code has changed, need to re-enter to access
await _secureStorageService.delete(kSecuredPinCode); await _secureStorageService.delete(kSecuredPinCode);
unawaited(router.push(PinAuthRoute())); router.push(PinAuthRoute());
} catch (error) { } catch (error) {
_log.severe("Failed to access locked page", error); _log.severe("Failed to access locked page", error);
resolver.next(false); resolver.next(false);
+1 -3
View File
@@ -1,5 +1,3 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -52,7 +50,7 @@ class ActionService {
); );
Future<void> shareLink(List<String> remoteIds, BuildContext context) async { Future<void> shareLink(List<String> remoteIds, BuildContext context) async {
unawaited(context.pushRoute(SharedLinkEditRoute(assetsList: remoteIds))); context.pushRoute(SharedLinkEditRoute(assetsList: remoteIds));
} }
Future<void> favorite(List<String> remoteIds) async { Future<void> favorite(List<String> remoteIds) async {
+1 -1
View File
@@ -83,7 +83,7 @@ class AlbumService {
if (selectedIds.isEmpty) { if (selectedIds.isEmpty) {
final numLocal = await _albumRepository.count(local: true); final numLocal = await _albumRepository.count(local: true);
if (numLocal > 0) { if (numLocal > 0) {
await _syncService.removeAllLocalAlbumsAndAssets(); _syncService.removeAllLocalAlbumsAndAssets();
} }
return false; return false;
} }
+7 -7
View File
@@ -3,16 +3,17 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:http/http.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
import 'package:immich_mobile/utils/url_helper.dart'; import 'package:immich_mobile/utils/url_helper.dart';
import 'package:immich_mobile/utils/user_agent.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
import 'package:immich_mobile/utils/user_agent.dart';
import 'package:immich_mobile/utils/debug_print.dart';
class ApiService implements Authentication { class ApiService implements Authentication {
static final _client = const NetworkRepository().getHttpClient('api');
late ApiClient _apiClient; late ApiClient _apiClient;
late UsersApi usersApi; late UsersApi usersApi;
@@ -50,6 +51,7 @@ class ApiService implements Authentication {
setEndpoint(String endpoint) { setEndpoint(String endpoint) {
_apiClient = ApiClient(basePath: endpoint, authentication: this); _apiClient = ApiClient(basePath: endpoint, authentication: this);
_apiClient.client = _client;
_setUserAgentHeader(); _setUserAgentHeader();
if (_accessToken != null) { if (_accessToken != null) {
setAccessToken(_accessToken!); setAccessToken(_accessToken!);
@@ -86,7 +88,7 @@ class ApiService implements Authentication {
setEndpoint(endpoint); setEndpoint(endpoint);
// Save in local database for next startup // Save in local database for next startup
unawaited(Store.put(StoreKey.serverEndpoint, endpoint)); Store.put(StoreKey.serverEndpoint, endpoint);
return endpoint; return endpoint;
} }
@@ -134,13 +136,11 @@ class ApiService implements Authentication {
} }
Future<String> _getWellKnownEndpoint(String baseUrl) async { Future<String> _getWellKnownEndpoint(String baseUrl) async {
final Client client = Client();
try { try {
var headers = {"Accept": "application/json"}; var headers = {"Accept": "application/json"};
headers.addAll(getRequestHeaders()); headers.addAll(getRequestHeaders());
final res = await client final res = await _client
.get(Uri.parse("$baseUrl/.well-known/immich"), headers: headers) .get(Uri.parse("$baseUrl/.well-known/immich"), headers: headers)
.timeout(const Duration(seconds: 5)); .timeout(const Duration(seconds: 5));
+1 -1
View File
@@ -58,7 +58,7 @@ class AuthService {
Future<String> validateServerUrl(String url) async { Future<String> validateServerUrl(String url) async {
final validUrl = await _apiService.resolveAndSetEndpoint(url); final validUrl = await _apiService.resolveAndSetEndpoint(url);
await _apiService.setDeviceInfoHeader(); await _apiService.setDeviceInfoHeader();
await Store.put(StoreKey.serverUrl, validUrl); Store.put(StoreKey.serverUrl, validUrl);
return validUrl; return validUrl;
} }
+16 -22
View File
@@ -291,7 +291,7 @@ class BackgroundService {
case "backgroundProcessing": case "backgroundProcessing":
case "onAssetsChanged": case "onAssetsChanged":
try { try {
unawaited(_clearErrorNotifications()); _clearErrorNotifications();
// iOS should time out after some threshold so it doesn't wait // iOS should time out after some threshold so it doesn't wait
// indefinitely and can run later // indefinitely and can run later
@@ -342,7 +342,7 @@ class BackgroundService {
); );
HttpSSLOptions.apply(); HttpSSLOptions.apply();
await ref.read(apiServiceProvider).setAccessToken(Store.get(StoreKey.accessToken)); ref.read(apiServiceProvider).setAccessToken(Store.get(StoreKey.accessToken));
await ref.read(authServiceProvider).setOpenApiServiceEndpoint(); await ref.read(authServiceProvider).setOpenApiServiceEndpoint();
dPrint(() => "[BG UPLOAD] Using endpoint: ${ref.read(apiServiceProvider).apiClient.basePath}"); dPrint(() => "[BG UPLOAD] Using endpoint: ${ref.read(apiServiceProvider).apiClient.basePath}");
@@ -385,7 +385,7 @@ class BackgroundService {
await ref.read(backupAlbumRepositoryProvider).deleteAll(toDelete); await ref.read(backupAlbumRepositoryProvider).deleteAll(toDelete);
await ref.read(backupAlbumRepositoryProvider).updateAll(toUpsert); await ref.read(backupAlbumRepositoryProvider).updateAll(toUpsert);
} else if (Store.tryGet(StoreKey.backupFailedSince) == null) { } else if (Store.tryGet(StoreKey.backupFailedSince) == null) {
await Store.put(StoreKey.backupFailedSince, DateTime.now()); Store.put(StoreKey.backupFailedSince, DateTime.now());
return false; return false;
} }
// Android should check for new assets added while performing backup // Android should check for new assets added while performing backup
@@ -412,11 +412,9 @@ class BackgroundService {
try { try {
toUpload = await backupService.removeAlreadyUploadedAssets(toUpload); toUpload = await backupService.removeAlreadyUploadedAssets(toUpload);
} catch (e) { } catch (e) {
unawaited( _showErrorNotification(
_showErrorNotification( title: "backup_background_service_error_title".tr(),
title: "backup_background_service_error_title".tr(), content: "backup_background_service_connection_failed_message".tr(),
content: "backup_background_service_connection_failed_message".tr(),
),
); );
return false; return false;
} }
@@ -430,15 +428,13 @@ class BackgroundService {
} }
_assetsToUploadCount = toUpload.length; _assetsToUploadCount = toUpload.length;
_uploadedAssetsCount = 0; _uploadedAssetsCount = 0;
unawaited( _updateNotification(
_updateNotification( title: "backup_background_service_in_progress_notification".tr(),
title: "backup_background_service_in_progress_notification".tr(), content: notifyTotalProgress ? formatAssetBackupProgress(_uploadedAssetsCount, _assetsToUploadCount) : null,
content: notifyTotalProgress ? formatAssetBackupProgress(_uploadedAssetsCount, _assetsToUploadCount) : null, progress: 0,
progress: 0, max: notifyTotalProgress ? _assetsToUploadCount : 0,
max: notifyTotalProgress ? _assetsToUploadCount : 0, indeterminate: !notifyTotalProgress,
indeterminate: !notifyTotalProgress, onlyIfFG: !notifyTotalProgress,
onlyIfFG: !notifyTotalProgress,
),
); );
_cancellationToken = CancellationToken(); _cancellationToken = CancellationToken();
@@ -456,11 +452,9 @@ class BackgroundService {
); );
if (!ok && !_cancellationToken!.isCancelled) { if (!ok && !_cancellationToken!.isCancelled) {
unawaited( _showErrorNotification(
_showErrorNotification( title: "backup_background_service_error_title".tr(),
title: "backup_background_service_error_title".tr(), content: "backup_background_service_backup_failed_message".tr(),
content: "backup_background_service_backup_failed_message".tr(),
),
); );
} }
@@ -120,7 +120,7 @@ class BackupVerificationService {
await tuple.fileMediaRepository.enableBackgroundAccess(); await tuple.fileMediaRepository.enableBackgroundAccess();
final ApiService apiService = ApiService(); final ApiService apiService = ApiService();
apiService.setEndpoint(tuple.endpoint); apiService.setEndpoint(tuple.endpoint);
await apiService.setAccessToken(tuple.auth); apiService.setAccessToken(tuple.auth);
for (int i = 0; i < tuple.deleteCandidates.length; i++) { for (int i = 0; i < tuple.deleteCandidates.length; i++) {
if (await _compareAssets(tuple.deleteCandidates[i], tuple.originals[i], apiService)) { if (await _compareAssets(tuple.deleteCandidates[i], tuple.originals[i], apiService)) {
result.add(tuple.deleteCandidates[i]); result.add(tuple.deleteCandidates[i]);
+2 -2
View File
@@ -1,9 +1,9 @@
import 'package:immich_mobile/mixins/error_logger.mixin.dart'; import 'package:immich_mobile/mixins/error_logger.mixin.dart';
import 'package:immich_mobile/models/map/map_marker.model.dart'; import 'package:immich_mobile/models/map/map_marker.model.dart';
import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/utils/user_agent.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:maplibre_gl/maplibre_gl.dart'; import 'package:maplibre_gl/maplibre_gl.dart';
import 'package:immich_mobile/utils/user_agent.dart';
class MapService with ErrorLoggerMixin { class MapService with ErrorLoggerMixin {
final ApiService _apiService; final ApiService _apiService;
@@ -16,7 +16,7 @@ class MapService with ErrorLoggerMixin {
Future<void> _setMapUserAgentHeader() async { Future<void> _setMapUserAgentHeader() async {
final userAgent = await getUserAgentString(); final userAgent = await getUserAgentString();
await setHttpHeaders({'User-Agent': userAgent}); setHttpHeaders({'User-Agent': userAgent});
} }
Future<Iterable<MapMarker>> getMapMarkers({ Future<Iterable<MapMarker>> getMapMarkers({
+4 -8
View File
@@ -1,15 +1,13 @@
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/extensions/response_extensions.dart'; import 'package:immich_mobile/extensions/response_extensions.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/api.provider.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import 'api.service.dart'; import 'api.service.dart';
final shareServiceProvider = Provider((ref) => ShareService(ref.watch(apiServiceProvider))); final shareServiceProvider = Provider((ref) => ShareService(ref.watch(apiServiceProvider)));
@@ -60,11 +58,9 @@ class ShareService {
} }
final size = MediaQuery.of(context).size; final size = MediaQuery.of(context).size;
unawaited( Share.shareXFiles(
Share.shareXFiles( downloadedXFiles,
downloadedXFiles, sharePositionOrigin: Rect.fromPoints(Offset.zero, Offset(size.width / 3, size.height)),
sharePositionOrigin: Rect.fromPoints(Offset.zero, Offset(size.width / 3, size.height)),
),
); );
return true; return true;
} catch (error) { } catch (error) {
+1 -1
View File
@@ -705,7 +705,7 @@ class SyncService {
if (assets.isEmpty) return; if (assets.isEmpty) return;
if (Platform.isAndroid && _appSettingsService.getSetting<bool>(AppSettingsEnum.manageLocalMediaAndroid)) { if (Platform.isAndroid && _appSettingsService.getSetting<bool>(AppSettingsEnum.manageLocalMediaAndroid)) {
await _toggleTrashStatusForAssets(assets); _toggleTrashStatusForAssets(assets);
} }
try { try {
+3 -4
View File
@@ -3,7 +3,6 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:cancellation_token_http/http.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
@@ -158,7 +157,7 @@ class UploadService {
} }
} }
Future<void> startBackupWithHttpClient(String userId, bool hasWifi, CancellationToken token) async { Future<void> startBackupWithHttpClient(String userId, bool hasWifi, Completer token) async {
await _storageRepository.clearCache(); await _storageRepository.clearCache();
shouldAbortQueuingTasks = false; shouldAbortQueuingTasks = false;
@@ -170,7 +169,7 @@ class UploadService {
const batchSize = 100; const batchSize = 100;
for (int i = 0; i < candidates.length; i += batchSize) { for (int i = 0; i < candidates.length; i += batchSize) {
if (shouldAbortQueuingTasks || token.isCancelled) { if (shouldAbortQueuingTasks || token.isCompleted) {
break; break;
} }
@@ -251,7 +250,7 @@ class UploadService {
return; return;
} }
await enqueueTasks([uploadTask]); enqueueTasks([uploadTask]);
} catch (error, stackTrace) { } catch (error, stackTrace) {
dPrint(() => "Error handling live photo upload task: $error $stackTrace"); dPrint(() => "Error handling live photo upload task: $error $stackTrace");
} }
+3
View File
@@ -21,6 +21,7 @@ import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
@@ -106,5 +107,7 @@ abstract final class Bootstrap {
storeRepository: storeRepo, storeRepository: storeRepo,
shouldBuffer: shouldBufferLogs, shouldBuffer: shouldBufferLogs,
); );
await NetworkRepository.init();
} }
} }
+2 -4
View File
@@ -1,10 +1,8 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:immich_mobile/models/map/map_marker.model.dart'; import 'package:immich_mobile/models/map/map_marker.model.dart';
import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
import 'package:geolocator/geolocator.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:maplibre_gl/maplibre_gl.dart'; import 'package:maplibre_gl/maplibre_gl.dart';
@@ -70,7 +68,7 @@ class MapUtils {
try { try {
bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled && !silent) { if (!serviceEnabled && !silent) {
unawaited(showDialog(context: context, builder: (context) => _LocationServiceDisabledDialog())); showDialog(context: context, builder: (context) => _LocationServiceDisabledDialog());
return (null, LocationPermission.deniedForever); return (null, LocationPermission.deniedForever);
} }
+2 -2
View File
@@ -101,7 +101,7 @@ Future<void> handleEditDateTime(WidgetRef ref, BuildContext context, List<Asset>
return; return;
} }
await ref.read(assetServiceProvider).changeDateTime(selection.toList(), dateTime); ref.read(assetServiceProvider).changeDateTime(selection.toList(), dateTime);
} }
Future<void> handleEditLocation(WidgetRef ref, BuildContext context, List<Asset> selection) async { Future<void> handleEditLocation(WidgetRef ref, BuildContext context, List<Asset> selection) async {
@@ -120,7 +120,7 @@ Future<void> handleEditLocation(WidgetRef ref, BuildContext context, List<Asset>
return; return;
} }
await ref.read(assetServiceProvider).changeLocation(selection.toList(), location); ref.read(assetServiceProvider).changeLocation(selection.toList(), location);
} }
Future<void> handleSetAssetsVisibility( Future<void> handleSetAssetsVisibility(
@@ -1,5 +1,3 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -59,7 +57,7 @@ class AlbumViewerAppbar extends HookConsumerWidget implements PreferredSizeWidge
deleteAlbum() async { deleteAlbum() async {
final bool success = await ref.watch(albumProvider.notifier).deleteAlbum(album); final bool success = await ref.watch(albumProvider.notifier).deleteAlbum(album);
unawaited(context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()]))); context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()]));
if (!success) { if (!success) {
ImmichToast.show( ImmichToast.show(
@@ -107,7 +105,7 @@ class AlbumViewerAppbar extends HookConsumerWidget implements PreferredSizeWidge
bool isSuccess = await ref.watch(albumProvider.notifier).leaveAlbum(album); bool isSuccess = await ref.watch(albumProvider.notifier).leaveAlbum(album);
if (isSuccess) { if (isSuccess) {
unawaited(context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()]))); context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()]));
} else { } else {
context.pop(); context.pop();
ImmichToast.show( ImmichToast.show(
@@ -314,10 +314,10 @@ class MultiselectGrid extends HookConsumerWidget {
final result = await ref.read(albumServiceProvider).createAlbumWithGeneratedName(assets); final result = await ref.read(albumServiceProvider).createAlbumWithGeneratedName(assets);
if (result != null) { if (result != null) {
unawaited(ref.watch(albumProvider.notifier).refreshRemoteAlbums()); ref.watch(albumProvider.notifier).refreshRemoteAlbums();
selectionEnabledHook.value = false; selectionEnabledHook.value = false;
unawaited(context.pushRoute(AlbumViewerRoute(albumId: result.id))); context.pushRoute(AlbumViewerRoute(albumId: result.id));
} }
} finally { } finally {
processing.value = false; processing.value = false;
@@ -346,7 +346,7 @@ class MultiselectGrid extends HookConsumerWidget {
); );
if (remoteAssets.isNotEmpty) { if (remoteAssets.isNotEmpty) {
unawaited(handleEditDateTime(ref, context, remoteAssets.toList())); handleEditDateTime(ref, context, remoteAssets.toList());
} }
} finally { } finally {
selectionEnabledHook.value = false; selectionEnabledHook.value = false;
@@ -361,7 +361,7 @@ class MultiselectGrid extends HookConsumerWidget {
); );
if (remoteAssets.isNotEmpty) { if (remoteAssets.isNotEmpty) {
unawaited(handleEditLocation(ref, context, remoteAssets.toList())); handleEditLocation(ref, context, remoteAssets.toList());
} }
} finally { } finally {
selectionEnabledHook.value = false; selectionEnabledHook.value = false;
@@ -1,4 +1,3 @@
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
@@ -82,7 +81,7 @@ class BottomGalleryBar extends ConsumerWidget {
// to not throw the error when the next preCache index is called // to not throw the error when the next preCache index is called
if (totalAssets.value == 1 || assetIndex.value == totalAssets.value - 1) { if (totalAssets.value == 1 || assetIndex.value == totalAssets.value - 1) {
// Handle only one asset // Handle only one asset
await context.maybePop(); context.maybePop();
} }
totalAssets.value -= 1; totalAssets.value -= 1;
@@ -112,20 +111,18 @@ class BottomGalleryBar extends ConsumerWidget {
} }
// Asset is permanently removed // Asset is permanently removed
unawaited( showDialog(
showDialog( context: context,
context: context, builder: (BuildContext _) {
builder: (BuildContext _) { return DeleteDialog(
return DeleteDialog( onDelete: () async {
onDelete: () async { final isDeleted = await onDelete(true);
final isDeleted = await onDelete(true); if (isDeleted) {
if (isDeleted) { removeAssetFromStack();
removeAssetFromStack(); }
} },
}, );
); },
},
),
); );
} }
@@ -153,7 +150,7 @@ class BottomGalleryBar extends ConsumerWidget {
onTap: () async { onTap: () async {
await unStack(); await unStack();
ctx.pop(); ctx.pop();
await context.maybePop(); context.maybePop();
}, },
title: const Text("viewer_unstack", style: TextStyle(fontWeight: FontWeight.bold)).tr(), title: const Text("viewer_unstack", style: TextStyle(fontWeight: FontWeight.bold)).tr(),
), ),
@@ -181,11 +178,9 @@ class BottomGalleryBar extends ConsumerWidget {
void handleEdit() async { void handleEdit() async {
final image = Image(image: ImmichImage.imageProvider(asset: asset)); final image = Image(image: ImmichImage.imageProvider(asset: asset));
unawaited( context.navigator.push(
context.navigator.push( MaterialPageRoute(
MaterialPageRoute( builder: (context) => EditImagePage(asset: asset, image: image, isEdited: false),
builder: (context) => EditImagePage(asset: asset, image: image, isEdited: false),
),
), ),
); );
} }
@@ -1,5 +1,3 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -95,7 +93,7 @@ class CastDialog extends ConsumerWidget {
} }
if (!isCurrentDevice(deviceName)) { if (!isCurrentDevice(deviceName)) {
unawaited(ref.read(castProvider.notifier).connect(type, deviceObj)); ref.read(castProvider.notifier).connect(type, deviceObj);
} }
}, },
); );
@@ -1,12 +1,11 @@
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
import 'package:maplibre_gl/maplibre_gl.dart'; import 'package:maplibre_gl/maplibre_gl.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:immich_mobile/utils/debug_print.dart';
class ExifMap extends StatelessWidget { class ExifMap extends StatelessWidget {
final ExifInfo exifInfo; final ExifInfo exifInfo;
@@ -69,7 +68,7 @@ class ExifMap extends StatelessWidget {
} }
dPrint(() => 'Opening Map Uri: $uri'); dPrint(() => 'Opening Map Uri: $uri');
unawaited(launchUrl(uri)); launchUrl(uri);
}, },
onCreated: onMapCreated, onCreated: onMapCreated,
); );
@@ -1,21 +1,19 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/backup/backup_state.model.dart'; import 'package:immich_mobile/models/backup/backup_state.model.dart';
import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
import 'package:immich_mobile/providers/locale_provider.dart'; import 'package:immich_mobile/providers/locale_provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart';
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/bytes_units.dart'; import 'package:immich_mobile/utils/bytes_units.dart';
import 'package:immich_mobile/widgets/common/app_bar_dialog/app_bar_profile_info.dart'; import 'package:immich_mobile/widgets/common/app_bar_dialog/app_bar_profile_info.dart';
@@ -99,27 +97,25 @@ class ImmichAppBarDialog extends HookConsumerWidget {
return; return;
} }
unawaited( showDialog(
showDialog( context: context,
context: context, builder: (BuildContext ctx) {
builder: (BuildContext ctx) { return ConfirmDialog(
return ConfirmDialog( title: "app_bar_signout_dialog_title",
title: "app_bar_signout_dialog_title", content: "app_bar_signout_dialog_content",
content: "app_bar_signout_dialog_content", ok: "yes",
ok: "yes", onOk: () async {
onOk: () async { isLoggingOut.value = true;
isLoggingOut.value = true; await ref.read(authProvider.notifier).logout().whenComplete(() => isLoggingOut.value = false);
await ref.read(authProvider.notifier).logout().whenComplete(() => isLoggingOut.value = false);
ref.read(manualUploadProvider.notifier).cancelBackup(); ref.read(manualUploadProvider.notifier).cancelBackup();
ref.read(backupProvider.notifier).cancelBackup(); ref.read(backupProvider.notifier).cancelBackup();
unawaited(ref.read(assetProvider.notifier).clearAllAssets()); ref.read(assetProvider.notifier).clearAllAssets();
ref.read(websocketProvider.notifier).disconnect(); ref.read(websocketProvider.notifier).disconnect();
unawaited(context.replaceRoute(const LoginRoute())); context.replaceRoute(const LoginRoute());
}, },
); );
}, },
),
); );
}, },
trailing: isLoggingOut.value trailing: isLoggingOut.value
@@ -1,5 +1,3 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -8,8 +6,8 @@ import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/providers/upload_profile_image.provider.dart'; import 'package:immich_mobile/providers/upload_profile_image.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart'; import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart';
@@ -56,7 +54,7 @@ class AppBarProfileInfoBox extends HookConsumerWidget {
ref.read(currentUserProvider.notifier).refresh(); ref.read(currentUserProvider.notifier).refresh();
} }
unawaited(ref.read(backupProvider.notifier).updateDiskInfo()); ref.read(backupProvider.notifier).updateDiskInfo();
} }
} }
} }
@@ -1,4 +1,3 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:math'; import 'dart:math';
@@ -188,16 +187,16 @@ class LoginForm extends HookConsumerWidget {
final result = await ref.read(authProvider.notifier).login(emailController.text, passwordController.text); final result = await ref.read(authProvider.notifier).login(emailController.text, passwordController.text);
if (result.shouldChangePassword && !result.isAdmin) { if (result.shouldChangePassword && !result.isAdmin) {
unawaited(context.pushRoute(const ChangePasswordRoute())); context.pushRoute(const ChangePasswordRoute());
} else { } else {
final isBeta = Store.isBetaTimelineEnabled; final isBeta = Store.isBetaTimelineEnabled;
if (isBeta) { if (isBeta) {
await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission();
unawaited(handleSyncFlow()); handleSyncFlow();
unawaited(context.replaceRoute(const TabShellRoute())); context.replaceRoute(const TabShellRoute());
return; return;
} }
unawaited(context.replaceRoute(const TabControllerRoute())); context.replaceRoute(const TabControllerRoute());
} }
} catch (error) { } catch (error) {
ImmichToast.show( ImmichToast.show(
@@ -287,15 +286,15 @@ class LoginForm extends HookConsumerWidget {
final permission = ref.watch(galleryPermissionNotifier); final permission = ref.watch(galleryPermissionNotifier);
final isBeta = Store.isBetaTimelineEnabled; final isBeta = Store.isBetaTimelineEnabled;
if (!isBeta && (permission.isGranted || permission.isLimited)) { if (!isBeta && (permission.isGranted || permission.isLimited)) {
unawaited(ref.watch(backupProvider.notifier).resumeBackup()); ref.watch(backupProvider.notifier).resumeBackup();
} }
if (isBeta) { if (isBeta) {
await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission();
unawaited(handleSyncFlow()); handleSyncFlow();
unawaited(context.replaceRoute(const TabShellRoute())); context.replaceRoute(const TabShellRoute());
return; return;
} }
unawaited(context.replaceRoute(const TabControllerRoute())); context.replaceRoute(const TabControllerRoute());
} }
} catch (error, stack) { } catch (error, stack) {
log.severe('Error logging in with OAuth: $error', stack); log.severe('Error logging in with OAuth: $error', stack);
+3 -7
View File
@@ -1,11 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/map/map_event.model.dart'; import 'package:immich_mobile/models/map/map_event.model.dart';
import 'package:immich_mobile/utils/draggable_scroll_controller.dart';
import 'package:immich_mobile/widgets/map/map_asset_grid.dart'; import 'package:immich_mobile/widgets/map/map_asset_grid.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/utils/draggable_scroll_controller.dart';
class MapBottomSheet extends HookConsumerWidget { class MapBottomSheet extends HookConsumerWidget {
final Stream<MapEvent> mapEventStream; final Stream<MapEvent> mapEventStream;
@@ -34,11 +34,7 @@ class MapBottomSheet extends HookConsumerWidget {
void handleMapEvents(MapEvent event) async { void handleMapEvents(MapEvent event) async {
if (event is MapCloseBottomSheet) { if (event is MapCloseBottomSheet) {
await sheetController.animateTo( sheetController.animateTo(0.1, duration: const Duration(milliseconds: 200), curve: Curves.linearToEaseOut);
0.1,
duration: const Duration(milliseconds: 200),
curve: Curves.linearToEaseOut,
);
} }
} }
@@ -1,5 +1,3 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -47,7 +45,7 @@ class BetaTimelineListTile extends ConsumerWidget {
ElevatedButton( ElevatedButton(
onPressed: () async { onPressed: () async {
Navigator.of(context).pop(); Navigator.of(context).pop();
unawaited(context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: value)])); context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: value)]);
}, },
child: Text("ok".t(context: context)), child: Text("ok".t(context: context)),
), ),
@@ -1,5 +1,3 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
@@ -104,13 +102,13 @@ class LocalNetworkPreference extends HookConsumerWidget {
), ),
); );
} else { } else {
unawaited(saveWifiName(wifiName)); saveWifiName(wifiName);
} }
final serverEndpoint = ref.read(authProvider.notifier).getServerEndpoint(); final serverEndpoint = ref.read(authProvider.notifier).getServerEndpoint();
if (serverEndpoint != null) { if (serverEndpoint != null) {
unawaited(saveLocalEndpoint(serverEndpoint)); saveLocalEndpoint(serverEndpoint);
} }
} }
+1
View File
@@ -393,6 +393,7 @@ Class | Method | HTTP request | Description
- [LoginCredentialDto](doc//LoginCredentialDto.md) - [LoginCredentialDto](doc//LoginCredentialDto.md)
- [LoginResponseDto](doc//LoginResponseDto.md) - [LoginResponseDto](doc//LoginResponseDto.md)
- [LogoutResponseDto](doc//LogoutResponseDto.md) - [LogoutResponseDto](doc//LogoutResponseDto.md)
- [MachineLearningAvailabilityChecksDto](doc//MachineLearningAvailabilityChecksDto.md)
- [ManualJobName](doc//ManualJobName.md) - [ManualJobName](doc//ManualJobName.md)
- [MapMarkerResponseDto](doc//MapMarkerResponseDto.md) - [MapMarkerResponseDto](doc//MapMarkerResponseDto.md)
- [MapReverseGeocodeResponseDto](doc//MapReverseGeocodeResponseDto.md) - [MapReverseGeocodeResponseDto](doc//MapReverseGeocodeResponseDto.md)
+1
View File
@@ -164,6 +164,7 @@ part 'model/log_level.dart';
part 'model/login_credential_dto.dart'; part 'model/login_credential_dto.dart';
part 'model/login_response_dto.dart'; part 'model/login_response_dto.dart';
part 'model/logout_response_dto.dart'; part 'model/logout_response_dto.dart';
part 'model/machine_learning_availability_checks_dto.dart';
part 'model/manual_job_name.dart'; part 'model/manual_job_name.dart';
part 'model/map_marker_response_dto.dart'; part 'model/map_marker_response_dto.dart';
part 'model/map_reverse_geocode_response_dto.dart'; part 'model/map_reverse_geocode_response_dto.dart';
+2
View File
@@ -382,6 +382,8 @@ class ApiClient {
return LoginResponseDto.fromJson(value); return LoginResponseDto.fromJson(value);
case 'LogoutResponseDto': case 'LogoutResponseDto':
return LogoutResponseDto.fromJson(value); return LogoutResponseDto.fromJson(value);
case 'MachineLearningAvailabilityChecksDto':
return MachineLearningAvailabilityChecksDto.fromJson(value);
case 'ManualJobName': case 'ManualJobName':
return ManualJobNameTypeTransformer().decode(value); return ManualJobNameTypeTransformer().decode(value);
case 'MapMarkerResponseDto': case 'MapMarkerResponseDto':
@@ -0,0 +1,115 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class MachineLearningAvailabilityChecksDto {
/// Returns a new [MachineLearningAvailabilityChecksDto] instance.
MachineLearningAvailabilityChecksDto({
required this.enabled,
required this.interval,
required this.timeout,
});
bool enabled;
num interval;
num timeout;
@override
bool operator ==(Object other) => identical(this, other) || other is MachineLearningAvailabilityChecksDto &&
other.enabled == enabled &&
other.interval == interval &&
other.timeout == timeout;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(enabled.hashCode) +
(interval.hashCode) +
(timeout.hashCode);
@override
String toString() => 'MachineLearningAvailabilityChecksDto[enabled=$enabled, interval=$interval, timeout=$timeout]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'enabled'] = this.enabled;
json[r'interval'] = this.interval;
json[r'timeout'] = this.timeout;
return json;
}
/// Returns a new [MachineLearningAvailabilityChecksDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static MachineLearningAvailabilityChecksDto? fromJson(dynamic value) {
upgradeDto(value, "MachineLearningAvailabilityChecksDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return MachineLearningAvailabilityChecksDto(
enabled: mapValueOfType<bool>(json, r'enabled')!,
interval: num.parse('${json[r'interval']}'),
timeout: num.parse('${json[r'timeout']}'),
);
}
return null;
}
static List<MachineLearningAvailabilityChecksDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <MachineLearningAvailabilityChecksDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = MachineLearningAvailabilityChecksDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, MachineLearningAvailabilityChecksDto> mapFromJson(dynamic json) {
final map = <String, MachineLearningAvailabilityChecksDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = MachineLearningAvailabilityChecksDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of MachineLearningAvailabilityChecksDto-objects as value to a dart map
static Map<String, List<MachineLearningAvailabilityChecksDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<MachineLearningAvailabilityChecksDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = MachineLearningAvailabilityChecksDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'enabled',
'interval',
'timeout',
};
}

Some files were not shown because too many files have changed in this diff Show More