Compare commits

..

8 Commits

Author SHA1 Message Date
Mert 4ab7f4297c Merge branch 'main' into fix/mobile-unawaited-futures 2025-09-19 09:49:23 -04:00
shenlong-tanwen e8a9a2209d conflict resolution 2025-09-17 17:06:45 +05:30
shenlong-tanwen 379d9ab1e4 merge main 2025-09-17 16:08:34 +05:30
shenlong-tanwen f3e427f268 review changes 2025-09-09 15:22:17 +05:30
shenlong-tanwen 28380b1b46 auto gen file 2025-09-08 21:07:41 +05:30
shenlong-tanwen abb5df8bab fix warning 2025-09-08 10:03:43 +05:30
shenlong-tanwen 8face037da remove unused dcm lints
They will be added back later on a case by case basis
2025-09-08 09:35:48 +05:30
shenlong-tanwen 4a449104aa chore: add unawaited_futures lint as warning 2025-09-08 09:35:25 +05:30
101 changed files with 683 additions and 969 deletions
-1
View File
@@ -18,7 +18,6 @@ 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
+5 -164
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-lang.github.io/linter/lints/index.html. # https://dart.dev/tools/linter-rules
# #
# 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,6 +28,7 @@ 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
@@ -47,6 +48,9 @@ analyzer:
# plugins: # plugins:
# - custom_lint # - custom_lint
errors:
unawaited_futures: warning
custom_lint: custom_lint:
debug: true debug: true
rules: rules:
@@ -142,170 +146,7 @@ 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
@@ -3,19 +3,14 @@ package app.alextran.immich.images
import android.content.ContentResolver import android.content.ContentResolver
import android.content.ContentUris import android.content.ContentUris
import android.content.Context import android.content.Context
import android.content.res.Resources
import android.graphics.* import android.graphics.*
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle
import android.os.CancellationSignal import android.os.CancellationSignal
import android.os.OperationCanceledException import android.os.OperationCanceledException
import android.provider.DocumentsContract
import android.provider.MediaStore.Images import android.provider.MediaStore.Images
import android.provider.MediaStore.Video import android.provider.MediaStore.Video
import android.system.Int64Ref
import android.util.Size import android.util.Size
import androidx.annotation.RequiresApi
import java.nio.ByteBuffer import java.nio.ByteBuffer
import kotlin.math.* import kotlin.math.*
import java.util.concurrent.Executors import java.util.concurrent.Executors
@@ -177,7 +172,7 @@ class ThumbnailsImpl(context: Context) : ThumbnailApi {
} }
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
loadThumbnail(uri, Size(targetWidth, targetHeight), signal) resolver.loadThumbnail(uri, Size(targetWidth, targetHeight), signal)
} else { } else {
signal.setOnCancelListener { Images.Thumbnails.cancelThumbnailRequest(resolver, id) } signal.setOnCancelListener { Images.Thumbnails.cancelThumbnailRequest(resolver, id) }
Images.Thumbnails.getThumbnail(resolver, id, Images.Thumbnails.MINI_KIND, OPTIONS) Images.Thumbnails.getThumbnail(resolver, id, Images.Thumbnails.MINI_KIND, OPTIONS)
@@ -190,7 +185,7 @@ class ThumbnailsImpl(context: Context) : ThumbnailApi {
signal.throwIfCanceled() signal.throwIfCanceled()
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val uri = ContentUris.withAppendedId(Video.Media.EXTERNAL_CONTENT_URI, id) val uri = ContentUris.withAppendedId(Video.Media.EXTERNAL_CONTENT_URI, id)
loadThumbnail(uri, Size(targetWidth, targetHeight), signal) resolver.loadThumbnail(uri, Size(targetWidth, targetHeight), signal)
} else { } else {
signal.setOnCancelListener { Video.Thumbnails.cancelThumbnailRequest(resolver, id) } signal.setOnCancelListener { Video.Thumbnails.cancelThumbnailRequest(resolver, id) }
Video.Thumbnails.getThumbnail(resolver, id, Video.Thumbnails.MINI_KIND, OPTIONS) Video.Thumbnails.getThumbnail(resolver, id, Video.Thumbnails.MINI_KIND, OPTIONS)
@@ -220,72 +215,4 @@ class ThumbnailsImpl(context: Context) : ThumbnailApi {
ref.get() ref.get()
} }
} }
/*
* Copyright (C) 2006 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@RequiresApi(Build.VERSION_CODES.Q)
fun loadThumbnail(uri: Uri, size: Size, signal: CancellationSignal?): Bitmap {
// Convert to Point, since that's what the API is defined as
val opts = Bundle()
if (size.width < 512 && size.height < 512) {
opts.putParcelable(ContentResolver.EXTRA_SIZE, Point(size.width, size.height))
}
val orientation = Int64Ref(0)
var bitmap =
ImageDecoder.decodeBitmap(
ImageDecoder.createSource {
val afd =
resolver.openTypedAssetFile(uri, "image/*", opts, signal)
?: throw Resources.NotFoundException("Asset $uri not found")
val extras = afd.extras
orientation.value =
(extras?.getInt(DocumentsContract.EXTRA_ORIENTATION, 0) ?: 0).toLong()
afd
}
) { decoder: ImageDecoder, info: ImageDecoder.ImageInfo, _: ImageDecoder.Source ->
decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE)
// One last-ditch check to see if we've been canceled.
signal?.throwIfCanceled()
// We requested a rough thumbnail size, but the remote size may have
// returned something giant, so defensively scale down as needed.
// This is modified from the original to target the smaller edge instead of the larger edge.
val widthSample = info.size.width.toDouble() / size.width
val heightSample = info.size.height.toDouble() / size.height
val sample = min(widthSample, heightSample)
if (sample > 1) {
val width = (info.size.width / sample).toInt()
val height = (info.size.height / sample).toInt()
decoder.setTargetSize(width, height)
}
}
// Transform the bitmap if requested. We use a side-channel to
// communicate the orientation, since EXIF thumbnails don't contain
// the rotation flags of the original image.
if (orientation.value != 0L) {
val width = bitmap.getWidth()
val height = bitmap.getHeight()
val m = Matrix()
m.setRotate(orientation.value.toFloat(), (width / 2).toFloat(), (height / 2).toFloat())
bitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height, m, false)
}
return bitmap
}
} }
+14 -38
View File
@@ -29,11 +29,9 @@
FAC6F89B2D287C890078CB2F /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = FAC6F8902D287C890078CB2F /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; FAC6F89B2D287C890078CB2F /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = FAC6F8902D287C890078CB2F /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
FAC6F8B72D287F120078CB2F /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC6F8B52D287F120078CB2F /* ShareViewController.swift */; }; FAC6F8B72D287F120078CB2F /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC6F8B52D287F120078CB2F /* ShareViewController.swift */; };
FAC6F8B92D287F120078CB2F /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FAC6F8B32D287F120078CB2F /* MainInterface.storyboard */; }; FAC6F8B92D287F120078CB2F /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FAC6F8B32D287F120078CB2F /* MainInterface.storyboard */; };
FEC340D12E7326630050078A /* AssetResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC340C92E7326630050078A /* AssetResolver.swift */; }; FEAFA8732E4D42F4001E47FE /* Thumbhash.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEAFA8722E4D42F4001E47FE /* Thumbhash.swift */; };
FEC340D22E7326630050078A /* Thumbhash.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC340CB2E7326630050078A /* Thumbhash.swift */; }; FED3B1962E253E9B0030FD97 /* ThumbnailsImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED3B1942E253E9B0030FD97 /* ThumbnailsImpl.swift */; };
FEC340D32E7326630050078A /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC340CF2E7326630050078A /* Request.swift */; }; FED3B1972E253E9B0030FD97 /* Thumbnails.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED3B1932E253E9B0030FD97 /* Thumbnails.g.swift */; };
FEC340D42E7326630050078A /* ThumbnailResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC340CC2E7326630050078A /* ThumbnailResolver.swift */; };
FEC340D52E7326630050078A /* Thumbnails.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC340CD2E7326630050078A /* Thumbnails.g.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@@ -117,11 +115,9 @@
FAC6F8B42D287F120078CB2F /* ShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ShareExtension.entitlements; sourceTree = "<group>"; }; FAC6F8B42D287F120078CB2F /* ShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ShareExtension.entitlements; sourceTree = "<group>"; };
FAC6F8B52D287F120078CB2F /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; }; FAC6F8B52D287F120078CB2F /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerProfile.entitlements; sourceTree = "<group>"; }; FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerProfile.entitlements; sourceTree = "<group>"; };
FEC340C92E7326630050078A /* AssetResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetResolver.swift; sourceTree = "<group>"; }; FEAFA8722E4D42F4001E47FE /* Thumbhash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thumbhash.swift; sourceTree = "<group>"; };
FEC340CB2E7326630050078A /* Thumbhash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thumbhash.swift; sourceTree = "<group>"; }; FED3B1932E253E9B0030FD97 /* Thumbnails.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thumbnails.g.swift; sourceTree = "<group>"; };
FEC340CC2E7326630050078A /* ThumbnailResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThumbnailResolver.swift; sourceTree = "<group>"; }; FED3B1942E253E9B0030FD97 /* ThumbnailsImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThumbnailsImpl.swift; sourceTree = "<group>"; };
FEC340CD2E7326630050078A /* Thumbnails.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thumbnails.g.swift; sourceTree = "<group>"; };
FEC340CF2E7326630050078A /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
@@ -251,7 +247,6 @@
97C146F01CF9000F007C117D /* Runner */ = { 97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
FEC340D02E7326630050078A /* Resolvers */,
B25D37792E72CA15008B6CA7 /* Connectivity */, B25D37792E72CA15008B6CA7 /* Connectivity */,
B21E34A62E5AF9760031FDB9 /* Background */, B21E34A62E5AF9760031FDB9 /* Background */,
B2CF7F8C2DDE4EBB00744BF6 /* Sync */, B2CF7F8C2DDE4EBB00744BF6 /* Sync */,
@@ -266,6 +261,7 @@
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
FED3B1952E253E9B0030FD97 /* Images */,
); );
path = Runner; path = Runner;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -300,34 +296,16 @@
path = ShareExtension; path = ShareExtension;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
FEC340CA2E7326630050078A /* Assets */ = { FED3B1952E253E9B0030FD97 /* Images */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
FEC340C92E7326630050078A /* AssetResolver.swift */, FEAFA8722E4D42F4001E47FE /* Thumbhash.swift */,
); FED3B1932E253E9B0030FD97 /* Thumbnails.g.swift */,
path = Assets; FED3B1942E253E9B0030FD97 /* ThumbnailsImpl.swift */,
sourceTree = "<group>";
};
FEC340CE2E7326630050078A /* Images */ = {
isa = PBXGroup;
children = (
FEC340CB2E7326630050078A /* Thumbhash.swift */,
FEC340CC2E7326630050078A /* ThumbnailResolver.swift */,
FEC340CD2E7326630050078A /* Thumbnails.g.swift */,
); );
path = Images; path = Images;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
FEC340D02E7326630050078A /* Resolvers */ = {
isa = PBXGroup;
children = (
FEC340CA2E7326630050078A /* Assets */,
FEC340CE2E7326630050078A /* Images */,
FEC340CF2E7326630050078A /* Request.swift */,
);
path = Resolvers;
sourceTree = "<group>";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
@@ -595,16 +573,14 @@
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */, B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */,
B25D377A2E72CA15008B6CA7 /* Connectivity.g.swift in Sources */, B25D377A2E72CA15008B6CA7 /* Connectivity.g.swift in Sources */,
FEAFA8732E4D42F4001E47FE /* Thumbhash.swift in Sources */,
B25D377C2E72CA26008B6CA7 /* ConnectivityApiImpl.swift in Sources */, B25D377C2E72CA26008B6CA7 /* ConnectivityApiImpl.swift in Sources */,
FED3B1962E253E9B0030FD97 /* ThumbnailsImpl.swift in Sources */,
B21E34AA2E5AFD2B0031FDB9 /* BackgroundWorkerApiImpl.swift in Sources */, B21E34AA2E5AFD2B0031FDB9 /* BackgroundWorkerApiImpl.swift in Sources */,
FED3B1972E253E9B0030FD97 /* Thumbnails.g.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
B2BE315F2E5E5229006EEF88 /* BackgroundWorker.g.swift in Sources */, B2BE315F2E5E5229006EEF88 /* BackgroundWorker.g.swift in Sources */,
65F32F33299D349D00CE9261 /* BackgroundSyncWorker.swift in Sources */, 65F32F33299D349D00CE9261 /* BackgroundSyncWorker.swift in Sources */,
FEC340D12E7326630050078A /* AssetResolver.swift in Sources */,
FEC340D22E7326630050078A /* Thumbhash.swift in Sources */,
FEC340D32E7326630050078A /* Request.swift in Sources */,
FEC340D42E7326630050078A /* ThumbnailResolver.swift in Sources */,
FEC340D52E7326630050078A /* Thumbnails.g.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
+2 -2
View File
@@ -15,7 +15,7 @@ import UIKit
) -> Bool { ) -> Bool {
// Required for flutter_local_notification // Required for flutter_local_notification
if #available(iOS 10.0, *) { if #available(iOS 10.0, *) {
UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
} }
GeneratedPluginRegistrant.register(with: self) GeneratedPluginRegistrant.register(with: self)
@@ -53,7 +53,7 @@ import UIKit
public static func registerPlugins(binaryMessenger: FlutterBinaryMessenger) { public static func registerPlugins(binaryMessenger: FlutterBinaryMessenger) {
NativeSyncApiSetup.setUp(binaryMessenger: binaryMessenger, api: NativeSyncApiImpl()) NativeSyncApiSetup.setUp(binaryMessenger: binaryMessenger, api: NativeSyncApiImpl())
ThumbnailApiSetup.setUp(binaryMessenger: binaryMessenger, api: ThumbnailResolver()) ThumbnailApiSetup.setUp(binaryMessenger: binaryMessenger, api: ThumbnailApiImpl())
BackgroundWorkerFgHostApiSetup.setUp(binaryMessenger: binaryMessenger, api: BackgroundWorkerApiImpl()) BackgroundWorkerFgHostApiSetup.setUp(binaryMessenger: binaryMessenger, api: BackgroundWorkerApiImpl())
} }
} }
@@ -0,0 +1,211 @@
import CryptoKit
import Flutter
import MobileCoreServices
import Photos
class Request {
weak var workItem: DispatchWorkItem?
var isCancelled = false
let callback: (Result<[String: Int64], any Error>) -> Void
init(callback: @escaping (Result<[String: Int64], any Error>) -> Void) {
self.callback = callback
}
}
class ThumbnailApiImpl: ThumbnailApi {
private static let imageManager = PHImageManager.default()
private static let fetchOptions = {
let fetchOptions = PHFetchOptions()
fetchOptions.fetchLimit = 1
fetchOptions.wantsIncrementalChangeDetails = false
return fetchOptions
}()
private static let requestOptions = {
let requestOptions = PHImageRequestOptions()
requestOptions.isNetworkAccessAllowed = true
requestOptions.deliveryMode = .highQualityFormat
requestOptions.resizeMode = .fast
requestOptions.isSynchronous = true
requestOptions.version = .current
return requestOptions
}()
private static let assetQueue = DispatchQueue(label: "thumbnail.assets", qos: .userInitiated)
private static let requestQueue = DispatchQueue(label: "thumbnail.requests", qos: .userInitiated)
private static let cancelQueue = DispatchQueue(label: "thumbnail.cancellation", qos: .default)
private static let processingQueue = DispatchQueue(label: "thumbnail.processing", qos: .userInteractive, attributes: .concurrent)
private static let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
private static let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue).rawValue
private static var requests = [Int64: Request]()
private static let cancelledResult = Result<[String: Int64], any Error>.success([:])
private static let concurrencySemaphore = DispatchSemaphore(value: ProcessInfo.processInfo.activeProcessorCount * 2)
private static let assetCache = {
let assetCache = NSCache<NSString, PHAsset>()
assetCache.countLimit = 10000
return assetCache
}()
private static let activitySemaphore = DispatchSemaphore(value: 1)
private static let willResignActiveObserver = NotificationCenter.default.addObserver(
forName: UIApplication.willResignActiveNotification,
object: nil,
queue: .main
) { _ in
processingQueue.suspend()
activitySemaphore.wait()
}
private static let didBecomeActiveObserver = NotificationCenter.default.addObserver(
forName: UIApplication.didBecomeActiveNotification,
object: nil,
queue: .main
) { _ in
processingQueue.resume()
activitySemaphore.signal()
}
func getThumbhash(thumbhash: String, completion: @escaping (Result<[String : Int64], any Error>) -> Void) {
Self.processingQueue.async {
guard let data = Data(base64Encoded: thumbhash)
else { return completion(.failure(PigeonError(code: "", message: "Invalid base64 string: \(thumbhash)", details: nil)))}
let (width, height, pointer) = thumbHashToRGBA(hash: data)
self.waitForActiveState()
completion(.success(["pointer": Int64(Int(bitPattern: pointer.baseAddress)), "width": Int64(width), "height": Int64(height)]))
}
}
func requestImage(assetId: String, requestId: Int64, width: Int64, height: Int64, isVideo: Bool, completion: @escaping (Result<[String: Int64], any Error>) -> Void) {
let request = Request(callback: completion)
let item = DispatchWorkItem {
if request.isCancelled {
return completion(Self.cancelledResult)
}
Self.concurrencySemaphore.wait()
defer {
Self.concurrencySemaphore.signal()
}
if request.isCancelled {
return completion(Self.cancelledResult)
}
guard let asset = Self.requestAsset(assetId: assetId)
else {
Self.removeRequest(requestId: requestId)
completion(.failure(PigeonError(code: "", message: "Could not get asset data for \(assetId)", details: nil)))
return
}
if request.isCancelled {
return completion(Self.cancelledResult)
}
var image: UIImage?
Self.imageManager.requestImage(
for: asset,
targetSize: width > 0 && height > 0 ? CGSize(width: Double(width), height: Double(height)) : PHImageManagerMaximumSize,
contentMode: .aspectFill,
options: Self.requestOptions,
resultHandler: { (_image, info) -> Void in
image = _image
}
)
if request.isCancelled {
return completion(Self.cancelledResult)
}
guard let image = image,
let cgImage = image.cgImage else {
Self.removeRequest(requestId: requestId)
return completion(.failure(PigeonError(code: "", message: "Could not get pixel data for \(assetId)", details: nil)))
}
let pointer = UnsafeMutableRawPointer.allocate(
byteCount: Int(cgImage.width) * Int(cgImage.height) * 4,
alignment: MemoryLayout<UInt8>.alignment
)
if request.isCancelled {
pointer.deallocate()
return completion(Self.cancelledResult)
}
guard let context = CGContext(
data: pointer,
width: cgImage.width,
height: cgImage.height,
bitsPerComponent: 8,
bytesPerRow: cgImage.width * 4,
space: Self.rgbColorSpace,
bitmapInfo: Self.bitmapInfo
) else {
pointer.deallocate()
Self.removeRequest(requestId: requestId)
return completion(.failure(PigeonError(code: "", message: "Could not create context for \(assetId)", details: nil)))
}
if request.isCancelled {
pointer.deallocate()
return completion(Self.cancelledResult)
}
context.interpolationQuality = .none
context.draw(cgImage, in: CGRect(x: 0, y: 0, width: cgImage.width, height: cgImage.height))
if request.isCancelled {
pointer.deallocate()
return completion(Self.cancelledResult)
}
self.waitForActiveState()
completion(.success(["pointer": Int64(Int(bitPattern: pointer)), "width": Int64(cgImage.width), "height": Int64(cgImage.height)]))
Self.removeRequest(requestId: requestId)
}
request.workItem = item
Self.addRequest(requestId: requestId, request: request)
Self.processingQueue.async(execute: item)
}
func cancelImageRequest(requestId: Int64) {
Self.cancelRequest(requestId: requestId)
}
private static func addRequest(requestId: Int64, request: Request) -> Void {
requestQueue.sync { requests[requestId] = request }
}
private static func removeRequest(requestId: Int64) -> Void {
requestQueue.sync { requests[requestId] = nil }
}
private static func cancelRequest(requestId: Int64) -> Void {
requestQueue.async {
guard let request = requests.removeValue(forKey: requestId) else { return }
request.isCancelled = true
guard let item = request.workItem else { return }
if item.isCancelled {
cancelQueue.async { request.callback(Self.cancelledResult) }
}
}
}
private static func requestAsset(assetId: String) -> PHAsset? {
var asset: PHAsset?
assetQueue.sync { asset = assetCache.object(forKey: assetId as NSString) }
if asset != nil { return asset }
guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: Self.fetchOptions).firstObject
else { return nil }
assetQueue.async { assetCache.setObject(asset, forKey: assetId as NSString) }
return asset
}
func waitForActiveState() {
Self.activitySemaphore.wait()
Self.activitySemaphore.signal()
}
}
@@ -1,115 +0,0 @@
import Photos
class AssetRequest: Request {
let assetId: String
var completion: (PHAsset?) -> Void
init(cancellationToken: CancellationToken, assetId: String, completion: @escaping (PHAsset?) -> Void) {
self.assetId = assetId
self.completion = completion
super.init(cancellationToken: cancellationToken)
}
}
class AssetResolver {
private let requestQueue: DispatchQueue
private let processingQueue: DispatchQueue
private var batchTimer: DispatchWorkItem?
private let batchLock = NSLock()
private let batchTimeout: TimeInterval
private let fetchOptions: PHFetchOptions
private var assetRequests = [AssetRequest]()
private let assetCache: NSCache<NSString, PHAsset>
init(
fetchOptions: PHFetchOptions,
batchTimeout: TimeInterval = 0.00025, // 250μs
cacheSize: Int = 10000,
qos: DispatchQoS = .unspecified
) {
self.fetchOptions = fetchOptions
self.batchTimeout = batchTimeout
self.assetCache = NSCache<NSString, PHAsset>()
self.assetCache.countLimit = cacheSize
self.requestQueue = DispatchQueue(label: "assets.requests", qos: qos)
self.processingQueue = DispatchQueue(label: "assets.processing", qos: qos)
}
func requestAsset(request: AssetRequest) {
requestQueue.async {
if (request.isCancelled) {
request.completion(nil)
return
}
if let cachedAsset = self.assetCache.object(forKey: request.assetId as NSString) {
request.completion(cachedAsset)
return
}
self.batchLock.lock()
if (request.isCancelled) {
self.batchLock.unlock()
request.completion(nil)
return
}
self.assetRequests.append(request)
self.batchTimer?.cancel()
let timer = DispatchWorkItem(block: self.processBatch)
self.batchTimer = timer
self.batchLock.unlock()
self.processingQueue.asyncAfter(deadline: .now() + self.batchTimeout, execute: timer)
}
}
private func processBatch() {
batchLock.lock()
if assetRequests.isEmpty {
batchTimer = nil
batchLock.unlock()
return
}
var completionMap = [String: [(PHAsset?) -> Void]]()
var activeAssetIds = [String]()
completionMap.reserveCapacity(assetRequests.count)
activeAssetIds.reserveCapacity(assetRequests.count)
for request in assetRequests {
if (request.isCancelled) {
request.completion(nil)
continue
}
if var completions = completionMap[request.assetId] {
completions.append(request.completion)
} else {
activeAssetIds.append(request.assetId)
completionMap[request.assetId] = [request.completion]
}
}
assetRequests.removeAll(keepingCapacity: true)
batchTimer = nil
batchLock.unlock()
guard !activeAssetIds.isEmpty else { return }
let assets = PHAsset.fetchAssets(withLocalIdentifiers: activeAssetIds, options: self.fetchOptions)
assets.enumerateObjects { asset, _, _ in
let assetId = asset.localIdentifier
for completion in completionMap.removeValue(forKey: assetId)! {
completion(asset)
}
self.requestQueue.async { self.assetCache.setObject(asset, forKey: assetId as NSString) }
}
for completions in completionMap.values {
for completion in completions {
completion(nil)
}
}
}
}
@@ -1,198 +0,0 @@
import CryptoKit
import Flutter
import MobileCoreServices
import Photos
class ThumbnailRequest: Request {
weak var workItem: DispatchWorkItem?
let completion: (Result<[String: Int64], any Error>) -> Void
init(cancellationToken: CancellationToken, completion: @escaping (Result<[String: Int64], any Error>) -> Void) {
self.completion = completion
super.init(cancellationToken: cancellationToken)
}
}
class ThumbnailResolver: ThumbnailApi {
private static let imageManager = PHImageManager.default()
private static let assetResolver = AssetResolver(fetchOptions: {
let fetchOptions = PHFetchOptions()
fetchOptions.wantsIncrementalChangeDetails = false
return fetchOptions
}(), qos: .userInitiated)
private static let requestOptions = {
let requestOptions = PHImageRequestOptions()
requestOptions.isNetworkAccessAllowed = true
requestOptions.deliveryMode = .highQualityFormat
requestOptions.resizeMode = .fast
requestOptions.isSynchronous = true
requestOptions.version = .current
return requestOptions
}()
private static let requestQueue = DispatchQueue(label: "thumbnail.requests", qos: .userInitiated)
private static let cancelQueue = DispatchQueue(label: "thumbnail.cancellation", qos: .default)
private static let processingQueue = DispatchQueue(label: "thumbnail.processing", qos: .userInteractive, attributes: .concurrent)
private static let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
private static let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue).rawValue
private static var requests = [Int64: ThumbnailRequest]()
private static let cancelledResult = Result<[String: Int64], any Error>.success([:])
private static let thumbnailConcurrencySemaphore = DispatchSemaphore(value: ProcessInfo.processInfo.activeProcessorCount / 2 + 1)
private static let activitySemaphore = DispatchSemaphore(value: 1)
private static let willResignActiveObserver = NotificationCenter.default.addObserver(
forName: UIApplication.willResignActiveNotification,
object: nil,
queue: .main
) { _ in
processingQueue.suspend()
activitySemaphore.wait()
}
private static let didBecomeActiveObserver = NotificationCenter.default.addObserver(
forName: UIApplication.didBecomeActiveNotification,
object: nil,
queue: .main
) { _ in
processingQueue.resume()
activitySemaphore.signal()
}
func getThumbhash(thumbhash: String, completion: @escaping (Result<[String : Int64], any Error>) -> Void) {
Self.processingQueue.async {
guard let data = Data(base64Encoded: thumbhash)
else { return completion(.failure(PigeonError(code: "", message: "Invalid base64 string: \(thumbhash)", details: nil)))}
let (width, height, pointer) = thumbHashToRGBA(hash: data)
self.waitForActiveState()
completion(.success(["pointer": Int64(Int(bitPattern: pointer.baseAddress)), "width": Int64(width), "height": Int64(height)]))
}
}
func requestImage(assetId: String, requestId: Int64, width: Int64, height: Int64, isVideo: Bool, completion: @escaping (Result<[String: Int64], any Error>) -> Void) {
let cancellationToken = CancellationToken()
let thumbnailRequest = ThumbnailRequest(cancellationToken: cancellationToken, completion: completion)
Self.assetResolver.requestAsset(request: AssetRequest(cancellationToken: cancellationToken, assetId: assetId) { asset in
if cancellationToken.isCancelled {
return completion(Self.cancelledResult)
}
let item = DispatchWorkItem {
if cancellationToken.isCancelled {
return completion(Self.cancelledResult)
}
guard let asset = asset else {
if cancellationToken.isCancelled {
return completion(Self.cancelledResult)
}
Self.removeRequest(requestId: requestId)
completion(.failure(PigeonError(code: "", message: "Could not get asset data for \(assetId)", details: nil)))
return
}
Self.thumbnailConcurrencySemaphore.wait()
defer { Self.thumbnailConcurrencySemaphore.signal() }
if cancellationToken.isCancelled {
return completion(Self.cancelledResult)
}
var image: UIImage?
Self.imageManager.requestImage(
for: asset,
targetSize: width > 0 && height > 0 ? CGSize(width: Double(width), height: Double(height)) : PHImageManagerMaximumSize,
contentMode: .aspectFill,
options: Self.requestOptions,
resultHandler: { (_image, info) -> Void in
image = _image
}
)
if cancellationToken.isCancelled {
return completion(Self.cancelledResult)
}
guard let image = image,
let cgImage = image.cgImage else {
Self.removeRequest(requestId: requestId)
return completion(.failure(PigeonError(code: "", message: "Could not get pixel data for \(assetId)", details: nil)))
}
let pointer = UnsafeMutableRawPointer.allocate(
byteCount: Int(cgImage.width) * Int(cgImage.height) * 4,
alignment: MemoryLayout<UInt8>.alignment
)
if cancellationToken.isCancelled {
pointer.deallocate()
return completion(Self.cancelledResult)
}
guard let context = CGContext(
data: pointer,
width: cgImage.width,
height: cgImage.height,
bitsPerComponent: 8,
bytesPerRow: cgImage.width * 4,
space: Self.rgbColorSpace,
bitmapInfo: Self.bitmapInfo
) else {
pointer.deallocate()
Self.removeRequest(requestId: requestId)
return completion(.failure(PigeonError(code: "", message: "Could not create context for \(assetId)", details: nil)))
}
if cancellationToken.isCancelled {
pointer.deallocate()
return completion(Self.cancelledResult)
}
context.interpolationQuality = .none
context.draw(cgImage, in: CGRect(x: 0, y: 0, width: cgImage.width, height: cgImage.height))
if cancellationToken.isCancelled {
pointer.deallocate()
return completion(Self.cancelledResult)
}
self.waitForActiveState()
completion(.success(["pointer": Int64(Int(bitPattern: pointer)), "width": Int64(cgImage.width), "height": Int64(cgImage.height)]))
Self.removeRequest(requestId: requestId)
}
thumbnailRequest.workItem = item
Self.processingQueue.async(execute: item)
})
Self.addRequest(requestId: requestId, request: thumbnailRequest)
}
func cancelImageRequest(requestId: Int64) {
Self.cancelRequest(requestId: requestId)
}
private static func addRequest(requestId: Int64, request: ThumbnailRequest) -> Void {
requestQueue.sync { requests[requestId] = request }
}
private static func removeRequest(requestId: Int64) -> Void {
requestQueue.sync { requests[requestId] = nil }
}
private static func cancelRequest(requestId: Int64) -> Void {
requestQueue.async {
guard let request = requests.removeValue(forKey: requestId) else { return }
request.isCancelled = true
guard let item = request.workItem else { return }
item.cancel()
if item.isCancelled {
cancelQueue.async { request.completion(Self.cancelledResult) }
}
}
}
func waitForActiveState() {
Self.activitySemaphore.wait()
Self.activitySemaphore.signal()
}
}
-20
View File
@@ -1,20 +0,0 @@
class CancellationToken {
var isCancelled = false
}
class Request {
let cancellationToken: CancellationToken
init(cancellationToken: CancellationToken) {
self.cancellationToken = cancellationToken
}
var isCancelled: Bool {
get {
return cancellationToken.isCancelled
}
set(newValue) {
cancellationToken.isCancelled = newValue
}
}
}
@@ -117,10 +117,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
_backgroundHostApi.onInitialized(); unawaited(_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);
_backgroundHostApi.close(); unawaited(_backgroundHostApi.close());
} }
} }
@@ -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.");
_localAlbumRepository.upsert(updatedDeviceAlbum); await _localAlbumRepository.upsert(updatedDeviceAlbum);
return true; return true;
} }
@@ -68,7 +68,7 @@ class RemoteImageRequest extends ImageRequest {
final cacheManager = this.cacheManager; final cacheManager = this.cacheManager;
final streamController = StreamController<List<int>>(sync: true); final streamController = StreamController<List<int>>(sync: true);
final Stream<List<int>> stream; final Stream<List<int>> stream;
cacheManager?.putStreamedFile(url, streamController.stream); unawaited(cacheManager?.putStreamedFile(url, streamController.stream));
stream = response.map((chunk) { stream = response.map((chunk) {
if (_isCancelled) { if (_isCancelled) {
throw StateError('Cancelled request'); throw StateError('Cancelled request');
@@ -81,11 +81,11 @@ class RemoteImageRequest extends ImageRequest {
try { try {
final Uint8List bytes = await _downloadBytes(stream, response.contentLength); final Uint8List bytes = await _downloadBytes(stream, response.contentLength);
streamController.close(); unawaited(streamController.close());
return await ImmutableBuffer.fromUint8List(bytes); return await ImmutableBuffer.fromUint8List(bytes);
} catch (e) { } catch (e) {
streamController.addError(e); streamController.addError(e);
streamController.close(); unawaited(streamController.close());
if (_isCancelled) { if (_isCancelled) {
return null; return null;
} }
@@ -143,7 +143,7 @@ class RemoteImageRequest extends ImageRequest {
return await _decodeBuffer(buffer, decode, scale); return await _decodeBuffer(buffer, decode, scale);
} catch (e) { } catch (e) {
log.severe('Failed to decode cached image', e); log.severe('Failed to decode cached image', e);
_evictFile(url); unawaited(_evictFile(url));
return null; return null;
} }
} }
@@ -361,15 +361,13 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
return _db.managers.localAlbumEntity.count(); return _db.managers.localAlbumEntity.count();
} }
Future unlinkRemoteAlbum(String id) async { Future<void> unlinkRemoteAlbum(String id) async {
return _db.localAlbumEntity.update() final query = _db.localAlbumEntity.update()..where((row) => row.id.equals(id));
..where((row) => row.id.equals(id)) await query.write(const LocalAlbumEntityCompanion(linkedRemoteAlbumId: Value(null)));
..write(const LocalAlbumEntityCompanion(linkedRemoteAlbumId: Value(null)));
} }
Future linkRemoteAlbum(String localAlbumId, String remoteAlbumId) async { Future<void> linkRemoteAlbum(String localAlbumId, String remoteAlbumId) async {
return _db.localAlbumEntity.update() final query = _db.localAlbumEntity.update()..where((row) => row.id.equals(localAlbumId));
..where((row) => row.id.equals(localAlbumId)) await query.write(LocalAlbumEntityCompanion(linkedRemoteAlbumId: Value(remoteAlbumId)));
..write(LocalAlbumEntityCompanion(linkedRemoteAlbumId: Value(remoteAlbumId)));
} }
} }
@@ -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: 100, size: 1000,
), ),
); );
} }
+1 -1
View File
@@ -153,7 +153,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
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); unawaited(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);
@@ -1,3 +1,5 @@
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';
@@ -51,7 +53,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) {
context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()])); unawaited(context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()])));
} else { } else {
showErrorMessage(); showErrorMessage();
} }
@@ -1,3 +1,5 @@
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';
@@ -29,8 +31,8 @@ class AlbumSharedUserSelectionPage extends HookConsumerWidget {
if (newAlbum != null) { if (newAlbum != null) {
ref.watch(albumTitleProvider.notifier).clearAlbumTitle(); ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
context.maybePop(true); unawaited(context.maybePop(true));
context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()])); unawaited(context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()])));
} }
ScaffoldMessenger( ScaffoldMessenger(
@@ -109,8 +111,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: () async { onPressed: () {
context.maybePop(); unawaited(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
ref.read(albumProvider.notifier).refreshDeviceAlbums(); await 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;
} }
ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id); unawaited(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,3 +1,5 @@
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';
@@ -54,9 +56,11 @@ class DriftBackupOptionsPage extends ConsumerWidget {
); );
final backupNotifier = ref.read(driftBackupProvider.notifier); final backupNotifier = ref.read(driftBackupProvider.notifier);
backupNotifier.cancel().then((_) { unawaited(
backupNotifier.startBackup(currentUser.id); backupNotifier.cancel().then((_) {
}); 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/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/presentation/widgets/images/thumbnail.widget.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/infrastructure/asset.provider.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) async { Future<void> _showFileDetailDialog(BuildContext context, DriftUploadStatus item) {
showDialog( return 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
listViewScrollController.animateTo( await 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,3 +1,5 @@
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';
@@ -170,11 +172,11 @@ class CreateAlbumPage extends HookConsumerWidget {
.createAlbum(ref.read(albumTitleProvider), selectedAssets.value); .createAlbum(ref.read(albumTitleProvider), selectedAssets.value);
if (newAlbum != null) { if (newAlbum != null) {
ref.read(albumProvider.notifier).refreshRemoteAlbums(); await 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();
context.replaceRoute(AlbumViewerRoute(albumId: newAlbum.id)); unawaited(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');
context.maybePop(); await context.maybePop();
} }
} }
@@ -279,11 +279,13 @@ class NativeVideoViewerPage extends HookConsumerWidget {
nc.onPlaybackReady.addListener(onPlaybackReady); nc.onPlaybackReady.addListener(onPlaybackReady);
nc.onPlaybackEnded.addListener(onPlaybackEnded); nc.onPlaybackEnded.addListener(onPlaybackEnded);
nc.loadVideoSource(source).catchError((error) { unawaited(
log.severe('Error loading video source: $error'); nc.loadVideoSource(source).catchError((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);
nc.setLoop(loopVideo); unawaited(nc.setLoop(loopVideo));
controller.value = nc; controller.value = nc;
Timer(const Duration(milliseconds: 200), checkIfBuffering); Timer(const Duration(milliseconds: 200), checkIfBuffering);
@@ -354,12 +356,12 @@ class NativeVideoViewerPage extends HookConsumerWidget {
useOnAppLifecycleStateChange((_, state) async { useOnAppLifecycleStateChange((_, state) async {
if (state == AppLifecycleState.resumed && shouldPlayOnForeground.value) { if (state == AppLifecycleState.resumed && shouldPlayOnForeground.value) {
controller.value?.play(); await 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;
controller.value?.pause(); await controller.value?.pause();
} else { } else {
shouldPlayOnForeground.value = false; shouldPlayOnForeground.value = false;
} }
+34 -30
View File
@@ -1,3 +1,5 @@
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';
@@ -53,39 +55,41 @@ 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);
ref.read(authProvider.notifier).saveAuthInfo(accessToken: accessToken).then( unawaited(
(_) async { ref.read(authProvider.notifier).saveAuthInfo(accessToken: accessToken).then(
try { (_) async {
wsProvider.connect(); try {
infoProvider.getServerInfo(); wsProvider.connect();
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) { },
log.severe('Failed establishing connection to the server: $e'); onError: (exception) => {
} log.severe('Failed to update auth info with access token: $accessToken'),
}, ref.read(authProvider.notifier).logout(),
onError: (exception) => { context.replaceRoute(const LoginRoute()),
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');
ref.read(authProvider.notifier).logout(); unawaited(ref.read(authProvider.notifier).logout());
context.replaceRoute(const LoginRoute()); unawaited(context.replaceRoute(const LoginRoute()));
return; return;
} }
@@ -95,11 +99,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);
context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: true)]); unawaited(context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: true)]));
return; return;
} }
context.replaceRoute(Store.isBetaTimelineEnabled ? const TabShellRoute() : const TabControllerRoute()); unawaited(context.replaceRoute(Store.isBetaTimelineEnabled ? const TabShellRoute() : const TabControllerRoute()));
} }
if (Store.isBetaTimelineEnabled) { if (Store.isBetaTimelineEnabled) {
@@ -109,7 +113,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
ref.watch(backupProvider.notifier).resumeBackup(); unawaited(ref.watch(backupProvider.notifier).resumeBackup());
} }
} }
@@ -119,7 +123,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) {
notifier.handleBackupResume(currentUser.id); unawaited(notifier.handleBackupResume(currentUser.id));
} }
} }
} }
+8 -5
View File
@@ -1,13 +1,16 @@
import 'package:flutter/material.dart'; import 'dart:async';
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:flutter/material.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
@@ -35,7 +38,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();
context.pushRoute(EditImageRoute(asset: asset, image: croppedImage, isEdited: true)); unawaited(context.pushRoute(EditImageRoute(asset: asset, image: croppedImage, isEdited: true)));
}, },
), ),
], ],
+6 -5
View File
@@ -1,12 +1,13 @@
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/extensions/build_context_extensions.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/constants/filters.dart'; import 'package:immich_mobile/constants/filters.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:auto_route/auto_route.dart'; import 'package:immich_mobile/extensions/build_context_extensions.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.
@@ -74,7 +75,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);
context.pushRoute(EditImageRoute(asset: asset, image: filteredImage, isEdited: true)); unawaited(context.pushRoute(EditImageRoute(asset: asset, image: filteredImage, isEdited: true)));
}, },
), ),
], ],
@@ -1,14 +1,16 @@
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 {
@@ -35,9 +37,9 @@ class PinAuthPage extends HookConsumerWidget {
); );
if (isBetaTimeline) { if (isBetaTimeline) {
context.replaceRoute(const DriftLockedFolderRoute()); unawaited(context.replaceRoute(const DriftLockedFolderRoute()));
} else { } else {
context.replaceRoute(const LockedRoute()); unawaited(context.replaceRoute(const LockedRoute()));
} }
} }
} }
@@ -333,7 +333,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
changeExpiry: changeExpiry, changeExpiry: changeExpiry,
); );
ref.invalidate(sharedLinksStateProvider); ref.invalidate(sharedLinksStateProvider);
context.maybePop(); await context.maybePop();
} }
return Scaffold( return Scaffold(
+6 -4
View File
@@ -82,10 +82,12 @@ class PhotosPage extends HookConsumerWidget {
final fullRefresh = refreshCount.value > 0; final fullRefresh = refreshCount.value > 0;
if (fullRefresh) { if (fullRefresh) {
Future.wait([ unawaited(
ref.read(assetProvider.notifier).getAllAsset(clear: true), Future.wait([
ref.read(albumProvider.notifier).refreshRemoteAlbums(), ref.read(assetProvider.notifier).getAllAsset(clear: true),
]); 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;
+5 -4
View File
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
@@ -83,7 +84,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);
reloadLayers(); await reloadLayers();
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }
@@ -128,7 +129,7 @@ class MapPage extends HookConsumerWidget {
); );
if (marker != null) { if (marker != null) {
updateAssetMarkerPosition(marker); await 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) {
@@ -165,7 +166,7 @@ class MapPage extends HookConsumerWidget {
if (asset.isVideo) { if (asset.isVideo) {
ref.read(showControlsProvider.notifier).show = false; ref.read(showControlsProvider.notifier).show = false;
} }
context.pushRoute(GalleryViewerRoute(initialIndex: 0, heroOffset: 0, renderList: renderList)); unawaited(context.pushRoute(GalleryViewerRoute(initialIndex: 0, heroOffset: 0, renderList: renderList)));
} }
/// BOTTOM SHEET CALLBACKS /// BOTTOM SHEET CALLBACKS
@@ -209,7 +210,7 @@ class MapPage extends HookConsumerWidget {
} }
if (mapController.value != null && location != null) { if (mapController.value != null && location != null) {
mapController.value!.animateCamera( await 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;
controller.value?.animateCamera(CameraUpdate.newLatLng(centre)); await 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;
controller.value?.animateCamera(CameraUpdate.newLatLng(currentLatLng)); await 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;
search(); unawaited(search());
return; return;
} }
@@ -295,7 +295,7 @@ class SearchPage extends HookConsumerWidget {
); );
} }
search(); unawaited(search());
} }
// MEDIA PICKER // MEDIA PICKER
@@ -1,3 +1,5 @@
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';
@@ -47,7 +49,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);
context.navigateTo(const DriftAlbumsRoute()); unawaited(context.navigateTo(const DriftAlbumsRoute()));
} catch (_) { } catch (_) {
showErrorMessage(); showErrorMessage();
} }
@@ -1,3 +1,5 @@
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';
@@ -179,7 +181,7 @@ class _DriftCreateAlbumPageState extends ConsumerState<DriftCreateAlbumPage> {
if (album != null) { if (album != null) {
ref.read(currentRemoteAlbumProvider.notifier).setAlbum(album); ref.read(currentRemoteAlbumProvider.notifier).setAlbum(album);
context.replaceRoute(RemoteAlbumRoute(album: album)); unawaited(context.replaceRoute(RemoteAlbumRoute(album: album)));
} }
} }
@@ -1,3 +1,5 @@
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';
@@ -139,7 +141,7 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
toastType: ToastType.success, toastType: ToastType.success,
); );
context.pushRoute(const DriftAlbumsRoute()); unawaited(context.pushRoute(const DriftAlbumsRoute()));
} catch (e) { } catch (e) {
ImmichToast.show( ImmichToast.show(
context: context, context: context,
@@ -161,12 +163,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 ?? '');
}); });
HapticFeedback.mediumImpact(); unawaited(HapticFeedback.mediumImpact());
} }
} }
Future<void> showActivity(BuildContext context) async { Future<void> showActivity(BuildContext context) async {
context.pushRoute(const DriftActivitiesRoute()); unawaited(context.pushRoute(const DriftActivitiesRoute()));
} }
void showOptionSheet(BuildContext context) { void showOptionSheet(BuildContext context) {
@@ -207,7 +209,7 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
}, },
onCreateSharedLink: () async { onCreateSharedLink: () async {
context.pop(); context.pop();
context.pushRoute(SharedLinkEditRoute(albumId: _album.id)); unawaited(context.pushRoute(SharedLinkEditRoute(albumId: _album.id)));
}, },
onShowOptions: () { onShowOptions: () {
context.pop(); context.pop();
@@ -1,3 +1,5 @@
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';
@@ -34,7 +36,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();
context.pushRoute(DriftEditImageRoute(asset: asset, image: croppedImage, isEdited: true)); unawaited(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);
} }
ref.read(backgroundSyncProvider).syncLocal(full: true); unawaited(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);
context.pushRoute(DriftEditImageRoute(asset: asset, image: filteredImage, isEdited: true)); unawaited(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;
search(); unawaited(search());
return; return;
} }
@@ -300,7 +300,7 @@ class DriftSearchPage extends HookConsumerWidget {
); );
} }
search(); unawaited(search());
} }
// MEDIA PICKER // MEDIA PICKER
@@ -1,3 +1,5 @@
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';
@@ -15,7 +17,7 @@ class AdvancedInfoActionButton extends ConsumerWidget {
return; return;
} }
ref.read(actionProvider.notifier).troubleshoot(source, context); unawaited(ref.read(actionProvider.notifier).troubleshoot(source, context));
} }
@override @override
@@ -39,7 +39,7 @@ class ShareActionButton extends ConsumerWidget {
return; return;
} }
showDialog( await 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
filterAlbums(); unawaited(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();
context.pushRoute(RemoteAlbumRoute(album: newAlbum)); unawaited(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) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); unawaited(SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge));
} else { } else {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); unawaited(SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky));
} }
}); });
@@ -298,11 +298,13 @@ class NativeVideoViewer extends HookConsumerWidget {
nc.onPlaybackReady.addListener(onPlaybackReady); nc.onPlaybackReady.addListener(onPlaybackReady);
nc.onPlaybackEnded.addListener(onPlaybackEnded); nc.onPlaybackEnded.addListener(onPlaybackEnded);
nc.loadVideoSource(source).catchError((error) { unawaited(
log.severe('Error loading video source: $error'); nc.loadVideoSource(source).catchError((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);
nc.setLoop(!asset.isMotionPhoto && loopVideo); unawaited(nc.setLoop(!asset.isMotionPhoto && loopVideo));
controller.value = nc; controller.value = nc;
Timer(const Duration(milliseconds: 200), checkIfBuffering); Timer(const Duration(milliseconds: 200), checkIfBuffering);
@@ -373,12 +375,12 @@ class NativeVideoViewer extends HookConsumerWidget {
useOnAppLifecycleStateChange((_, state) async { useOnAppLifecycleStateChange((_, state) async {
if (state == AppLifecycleState.resumed && shouldPlayOnForeground.value) { if (state == AppLifecycleState.resumed && shouldPlayOnForeground.value) {
controller.value?.play(); await 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;
controller.value?.pause(); await controller.value?.pause();
} else { } else {
shouldPlayOnForeground.value = false; shouldPlayOnForeground.value = false;
} }
@@ -1,3 +1,5 @@
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';
@@ -51,14 +53,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;
evict(); unawaited(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) {
evict(); unawaited(evict());
return; return;
} }
yield image; yield image;
@@ -16,9 +16,8 @@ class LocalThumbProvider extends CancellableImageProvider<LocalThumbProvider>
final String id; final String id;
final Size size; final Size size;
final AssetType assetType; final AssetType assetType;
final bool exact;
LocalThumbProvider({required this.id, required this.assetType, this.size = kThumbnailResolution, this.exact = true}); LocalThumbProvider({required this.id, required this.assetType, this.size = kThumbnailResolution});
@override @override
Future<LocalThumbProvider> obtainKey(ImageConfiguration configuration) { Future<LocalThumbProvider> obtainKey(ImageConfiguration configuration) {
@@ -38,12 +37,7 @@ class LocalThumbProvider extends CancellableImageProvider<LocalThumbProvider>
} }
Stream<ImageInfo> _codec(LocalThumbProvider key, ImageDecoderCallback decode) { Stream<ImageInfo> _codec(LocalThumbProvider key, ImageDecoderCallback decode) {
final devicePixelRatio = PlatformDispatcher.instance.views.first.devicePixelRatio; final request = this.request = LocalImageRequest(localId: key.id, size: key.size, assetType: key.assetType);
final request = this.request = LocalImageRequest(
localId: key.id,
size: key.size * devicePixelRatio,
assetType: key.assetType,
);
return loadRequest(request, decode); return loadRequest(request, decode);
} }
@@ -51,7 +45,7 @@ class LocalThumbProvider extends CancellableImageProvider<LocalThumbProvider>
bool operator ==(Object other) { bool operator ==(Object other) {
if (identical(this, other)) return true; if (identical(this, other)) return true;
if (other is LocalThumbProvider) { if (other is LocalThumbProvider) {
return id == other.id && (!exact || size == other.size); return id == other.id;
} }
return false; return false;
} }
@@ -66,12 +60,7 @@ class LocalFullImageProvider extends CancellableImageProvider<LocalFullImageProv
final Size size; final Size size;
final AssetType assetType; final AssetType assetType;
LocalFullImageProvider({ LocalFullImageProvider({required this.id, required this.assetType, required this.size});
required this.id,
required this.assetType,
required this.size,
LocalThumbProvider? initialProvider,
});
@override @override
Future<LocalFullImageProvider> obtainKey(ImageConfiguration configuration) { Future<LocalFullImageProvider> obtainKey(ImageConfiguration configuration) {
@@ -82,7 +71,7 @@ class LocalFullImageProvider extends CancellableImageProvider<LocalFullImageProv
ImageStreamCompleter loadImage(LocalFullImageProvider key, ImageDecoderCallback decode) { ImageStreamCompleter loadImage(LocalFullImageProvider key, ImageDecoderCallback decode) {
return OneFramePlaceholderImageStreamCompleter( return OneFramePlaceholderImageStreamCompleter(
_codec(key, decode), _codec(key, decode),
initialImage: getInitialImage(LocalThumbProvider(id: id, assetType: assetType, exact: false)), initialImage: getInitialImage(LocalThumbProvider(id: key.id, assetType: key.assetType)),
informationCollector: () => <DiagnosticsNode>[ informationCollector: () => <DiagnosticsNode>[
DiagnosticsProperty<ImageProvider>('Image provider', this), DiagnosticsProperty<ImageProvider>('Image provider', this),
DiagnosticsProperty<String>('Id', key.id), DiagnosticsProperty<String>('Id', key.id),
@@ -96,7 +85,7 @@ class LocalFullImageProvider extends CancellableImageProvider<LocalFullImageProv
yield* initialImageStream(); yield* initialImageStream();
if (isCancelled) { if (isCancelled) {
evict(); unawaited(evict());
return; return;
} }
@@ -87,7 +87,7 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
yield* initialImageStream(); yield* initialImageStream();
if (isCancelled) { if (isCancelled) {
evict(); unawaited(evict());
return; return;
} }
@@ -100,7 +100,7 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
yield* loadRequest(request, decode); yield* loadRequest(request, decode);
if (isCancelled) { if (isCancelled) {
evict(); unawaited(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_utils.dart';
import 'package:immich_mobile/presentation/widgets/map/map.state.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/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,12 +114,14 @@ class _DriftMapState extends ConsumerState<DriftMap> {
} }
final bounds = await controller.getVisibleRegion(); final bounds = await controller.getVisibleRegion();
_reloadMutex.run(() async { unawaited(
if (mounted && ref.read(mapStateProvider.notifier).setBounds(bounds)) { _reloadMutex.run(() async {
final markers = await ref.read(mapMarkerProvider(bounds).future); if (mounted && ref.read(mapStateProvider.notifier).setBounds(bounds)) {
await reloadMarkers(markers); final markers = await ref.read(mapMarkerProvider(bounds).future);
} await reloadMarkers(markers);
}); }
}),
);
} }
Future<void> reloadMarkers(Map<String, dynamic> markers) async { Future<void> reloadMarkers(Map<String, dynamic> markers) async {
@@ -147,7 +149,7 @@ class _DriftMapState extends ConsumerState<DriftMap> {
final controller = mapController; final controller = mapController;
if (controller != null && location != null) { if (controller != null && location != null) {
controller.animateCamera( await 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) {
showDialog(context: context, builder: (context) => _LocationServiceDisabledDialog(context)); unawaited(showDialog(context: context, builder: (context) => _LocationServiceDisabledDialog(context)));
return (null, LocationPermission.deniedForever); return (null, LocationPermission.deniedForever);
} }
@@ -2,7 +2,7 @@ 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(128); 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;
@@ -1,3 +1,4 @@
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';
@@ -15,8 +16,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/timeline.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/infrastructure/timeline.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';
@@ -121,7 +122,6 @@ class _FixedSegmentRow extends ConsumerWidget {
} }
Widget _buildAssetRow(BuildContext context, List<BaseAsset> assets, TimelineService timelineService) { Widget _buildAssetRow(BuildContext context, List<BaseAsset> assets, TimelineService timelineService) {
final size = Size.square(tileHeight);
return FixedTimelineRow( return FixedTimelineRow(
dimension: tileHeight, dimension: tileHeight,
spacing: spacing, spacing: spacing,
@@ -135,7 +135,6 @@ class _FixedSegmentRow extends ConsumerWidget {
key: ValueKey(Object.hash(assets[i].heroTag, assetIndex + i, timelineService.hashCode)), key: ValueKey(Object.hash(assets[i].heroTag, assetIndex + i, timelineService.hashCode)),
asset: assets[i], asset: assets[i],
assetIndex: assetIndex + i, assetIndex: assetIndex + i,
size: size,
), ),
), ),
], ],
@@ -146,9 +145,8 @@ class _FixedSegmentRow extends ConsumerWidget {
class _AssetTileWidget extends ConsumerWidget { class _AssetTileWidget extends ConsumerWidget {
final BaseAsset asset; final BaseAsset asset;
final int assetIndex; final int assetIndex;
final Size size;
const _AssetTileWidget({super.key, required this.asset, required this.assetIndex, required this.size}); const _AssetTileWidget({super.key, required this.asset, required this.assetIndex});
Future _handleOnTap(BuildContext ctx, WidgetRef ref, int assetIndex, BaseAsset asset, int? heroOffset) async { Future _handleOnTap(BuildContext ctx, WidgetRef ref, int assetIndex, BaseAsset asset, int? heroOffset) async {
final multiSelectState = ref.read(multiSelectProvider); final multiSelectState = ref.read(multiSelectProvider);
@@ -159,11 +157,13 @@ 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);
ctx.pushRoute( unawaited(
AssetViewerRoute( ctx.pushRoute(
initialIndex: assetIndex, AssetViewerRoute(
timelineService: ref.read(timelineServiceProvider), initialIndex: assetIndex,
heroOffset: heroOffset, timelineService: ref.read(timelineServiceProvider),
heroOffset: heroOffset,
),
), ),
); );
} }
@@ -206,7 +206,6 @@ class _AssetTileWidget extends ConsumerWidget {
lockSelection: lockSelection, lockSelection: lockSelection,
showStorageIndicator: showStorageIndicator, showStorageIndicator: showStorageIndicator,
heroOffset: heroOffset, heroOffset: heroOffset,
size: size,
), ),
), ),
); );
@@ -233,7 +233,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
} }
try { try {
LogService.I.flush(); await 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 {
LogService.I.flush(); await 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
Store.delete(StoreKey.assetETag); await Store.delete(StoreKey.assetETag);
await _syncService.syncNewAssetToDb(newAsset); await _syncService.syncNewAssetToDb(newAsset);
} }
@@ -1,14 +1,16 @@
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';
@@ -159,24 +161,26 @@ class DownloadStateNotifier extends StateNotifier<DownloadState> {
} }
void shareAsset(Asset asset, BuildContext context) async { void shareAsset(Asset asset, BuildContext context) async {
showDialog( unawaited(
context: context, showDialog(
builder: (BuildContext buildContext) { context: context,
_shareService.shareAsset(asset, context).then((bool status) { builder: (BuildContext buildContext) {
if (!status) { _shareService.shareAsset(asset, context).then((bool status) {
ImmichToast.show( if (!status) {
context: context, ImmichToast.show(
msg: 'image_viewer_page_state_provider_share_error'.tr(), context: context,
toastType: ToastType.error, msg: 'image_viewer_page_state_provider_share_error'.tr(),
gravity: ToastGravity.BOTTOM, toastType: ToastType.error,
); gravity: ToastGravity.BOTTOM,
} );
buildContext.pop(); }
}); buildContext.pop();
return const ShareDialog(); });
}, return const ShareDialog();
barrierDismissible: false, },
useRootNavigator: false, barrierDismissible: 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);
_uploadService.enqueueTasks([task]); await _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)) {
Store.put(StoreKey.backgroundBackup, isEnabled); await 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 {
openAppSettings(); await openAppSettings();
} }
} }
@@ -533,10 +533,10 @@ class BackupNotifier extends StateNotifier<BackUpState> {
progressInFileSpeedUpdateTime: DateTime.now(), progressInFileSpeedUpdateTime: DateTime.now(),
progressInFileSpeedUpdateSentBytes: 0, progressInFileSpeedUpdateSentBytes: 0,
); );
_updatePersistentAlbumsSelection(); await _updatePersistentAlbumsSelection();
} }
updateDiskInfo(); await 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/providers/backup/backup.provider.dart';
import 'package:immich_mobile/services/backup_verification.service.dart';
import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/services/backup_verification.service.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;
} }
WakelockPlus.enable(); unawaited(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 {
WakelockPlus.disable(); unawaited(WakelockPlus.disable());
state = false; state = false;
} }
} }
@@ -7,7 +7,7 @@ part of 'backup_verification.provider.dart';
// ************************************************************************** // **************************************************************************
String _$backupVerificationHash() => String _$backupVerificationHash() =>
r'b204e43ab575d5fa5b2ee663297f32bcee9074f5'; r'b4b34909ed1af3f28877ea457d53a4a18b6417f8';
/// See also [BackupVerification]. /// See also [BackupVerification].
@ProviderFor(BackupVerification) @ProviderFor(BackupVerification)
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:cancellation_token_http/http.dart'; import 'package:cancellation_token_http/http.dart';
@@ -26,11 +27,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(
@@ -294,7 +295,7 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
); );
} }
} else { } else {
openAppSettings(); unawaited(openAppSettings());
dPrint(() => "[_startUpload] Do not have permission to the gallery"); dPrint(() => "[_startUpload] Do not have permission to the gallery");
} }
} catch (e) { } catch (e) {
@@ -3,7 +3,6 @@ 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';
@@ -77,7 +76,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 {
chunkEvents.close(); unawaited(chunkEvents.close());
} }
} }
@@ -1,3 +1,5 @@
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';
@@ -64,7 +66,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;
_downloadService.saveLivePhotos(update.task, livePhotosId); unawaited(_downloadService.saveLivePhotos(update.task, livePhotosId));
} }
} }
@@ -122,7 +124,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');
} }
context.pushRoute(AssetTroubleshootRoute(asset: assets.first)); unawaited(context.pushRoute(AssetTroubleshootRoute(asset: assets.first)));
return ActionResult(count: assets.length, success: true); return ActionResult(count: assets.length, success: true);
} }
@@ -1,3 +1,5 @@
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';
@@ -16,7 +18,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();
fetchLinks(); unawaited(fetchLinks());
} }
} }
@@ -1,17 +1,18 @@
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)));
@@ -106,15 +107,17 @@ 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
Share.shareXFiles(downloadedXFiles).then((result) async { unawaited(
for (var file in downloadedXFiles) { Share.shareXFiles(downloadedXFiles).then((result) async {
try { for (var file in downloadedXFiles) {
await File(file.path).delete(); try {
} catch (e) { await File(file.path).delete();
_log.warning("Failed to delete temporary file: ${file.path}", e); } catch (e) {
_log.warning("Failed to delete temporary file: ${file.path}", e);
}
} }
} }),
}); );
return downloadedXFiles.length; return downloadedXFiles.length;
} }
@@ -1,3 +1,5 @@
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';
@@ -12,7 +14,7 @@ class AppNavigationObserver extends AutoRouterObserver {
@override @override
Future<void> didChangeTabRoute(TabPageRoute route, TabPageRoute previousRoute) async { Future<void> didChangeTabRoute(TabPageRoute route, TabPageRoute previousRoute) async {
Future(() => ref.read(inLockedViewProvider.notifier).state = false); unawaited(Future(() => ref.read(inLockedViewProvider.notifier).state = false));
} }
@override @override
+4 -3
View File
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
@@ -26,18 +27,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');
router.replaceAll([const LoginRoute()]); unawaited(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.');
router.replaceAll([const LoginRoute()]); unawaited(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.");
router.replaceAll([const LoginRoute()]); unawaited(router.replaceAll([const LoginRoute()]));
return; return;
} }
} catch (e) { } catch (e) {
@@ -1,3 +1,5 @@
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';
@@ -13,7 +15,7 @@ class BackupPermissionGuard extends AutoRouteGuard {
if (p) { if (p) {
resolver.next(true); resolver.next(true);
} else { } else {
router.push(const PermissionOnboardingRoute()); unawaited(router.push(const PermissionOnboardingRoute()));
} }
} }
} }
+10 -6
View File
@@ -1,3 +1,5 @@
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';
@@ -13,12 +15,14 @@ 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;
router.replace( unawaited(
GalleryViewerRoute( router.replace(
renderList: args.renderList, GalleryViewerRoute(
initialIndex: args.initialIndex, renderList: args.renderList,
heroOffset: args.heroOffset, initialIndex: args.initialIndex,
showStack: args.showStack, heroOffset: args.heroOffset,
showStack: args.showStack,
),
), ),
); );
// Prevent further navigation since we replaced the route // Prevent further navigation since we replaced the route
+5 -4
View File
@@ -1,8 +1,9 @@
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';
@@ -30,7 +31,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) {
router.push(PinAuthRoute(createPinCode: true)); unawaited(router.push(PinAuthRoute(createPinCode: true)));
} }
if (authStatus.isElevated) { if (authStatus.isElevated) {
@@ -42,7 +43,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) {
router.push(PinAuthRoute()); unawaited(router.push(PinAuthRoute()));
return; return;
} }
@@ -74,7 +75,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);
router.push(PinAuthRoute()); unawaited(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);
+3 -1
View File
@@ -1,3 +1,5 @@
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';
@@ -50,7 +52,7 @@ class ActionService {
); );
Future<void> shareLink(List<String> remoteIds, BuildContext context) async { Future<void> shareLink(List<String> remoteIds, BuildContext context) async {
context.pushRoute(SharedLinkEditRoute(assetsList: remoteIds)); unawaited(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) {
_syncService.removeAllLocalAlbumsAndAssets(); await _syncService.removeAllLocalAlbumsAndAssets();
} }
return false; return false;
} }
+3 -3
View File
@@ -6,11 +6,11 @@ import 'package:device_info_plus/device_info_plus.dart';
import 'package:http/http.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/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 {
late ApiClient _apiClient; late ApiClient _apiClient;
@@ -86,7 +86,7 @@ class ApiService implements Authentication {
setEndpoint(endpoint); setEndpoint(endpoint);
// Save in local database for next startup // Save in local database for next startup
Store.put(StoreKey.serverEndpoint, endpoint); unawaited(Store.put(StoreKey.serverEndpoint, endpoint));
return endpoint; return endpoint;
} }
+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();
Store.put(StoreKey.serverUrl, validUrl); await Store.put(StoreKey.serverUrl, validUrl);
return validUrl; return validUrl;
} }
+22 -16
View File
@@ -291,7 +291,7 @@ class BackgroundService {
case "backgroundProcessing": case "backgroundProcessing":
case "onAssetsChanged": case "onAssetsChanged":
try { try {
_clearErrorNotifications(); unawaited(_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();
ref.read(apiServiceProvider).setAccessToken(Store.get(StoreKey.accessToken)); await 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) {
Store.put(StoreKey.backupFailedSince, DateTime.now()); await 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,9 +412,11 @@ class BackgroundService {
try { try {
toUpload = await backupService.removeAlreadyUploadedAssets(toUpload); toUpload = await backupService.removeAlreadyUploadedAssets(toUpload);
} catch (e) { } catch (e) {
_showErrorNotification( unawaited(
title: "backup_background_service_error_title".tr(), _showErrorNotification(
content: "backup_background_service_connection_failed_message".tr(), title: "backup_background_service_error_title".tr(),
content: "backup_background_service_connection_failed_message".tr(),
),
); );
return false; return false;
} }
@@ -428,13 +430,15 @@ class BackgroundService {
} }
_assetsToUploadCount = toUpload.length; _assetsToUploadCount = toUpload.length;
_uploadedAssetsCount = 0; _uploadedAssetsCount = 0;
_updateNotification( unawaited(
title: "backup_background_service_in_progress_notification".tr(), _updateNotification(
content: notifyTotalProgress ? formatAssetBackupProgress(_uploadedAssetsCount, _assetsToUploadCount) : null, title: "backup_background_service_in_progress_notification".tr(),
progress: 0, content: notifyTotalProgress ? formatAssetBackupProgress(_uploadedAssetsCount, _assetsToUploadCount) : null,
max: notifyTotalProgress ? _assetsToUploadCount : 0, progress: 0,
indeterminate: !notifyTotalProgress, max: notifyTotalProgress ? _assetsToUploadCount : 0,
onlyIfFG: !notifyTotalProgress, indeterminate: !notifyTotalProgress,
onlyIfFG: !notifyTotalProgress,
),
); );
_cancellationToken = CancellationToken(); _cancellationToken = CancellationToken();
@@ -452,9 +456,11 @@ class BackgroundService {
); );
if (!ok && !_cancellationToken!.isCancelled) { if (!ok && !_cancellationToken!.isCancelled) {
_showErrorNotification( unawaited(
title: "backup_background_service_error_title".tr(), _showErrorNotification(
content: "backup_background_service_backup_failed_message".tr(), title: "backup_background_service_error_title".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);
apiService.setAccessToken(tuple.auth); await 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();
setHttpHeaders({'User-Agent': userAgent}); await setHttpHeaders({'User-Agent': userAgent});
} }
Future<Iterable<MapMarker>> getMapMarkers({ Future<Iterable<MapMarker>> getMapMarkers({
+8 -4
View File
@@ -1,13 +1,15 @@
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/extensions/response_extensions.dart';
import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/extensions/response_extensions.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)));
@@ -58,9 +60,11 @@ class ShareService {
} }
final size = MediaQuery.of(context).size; final size = MediaQuery.of(context).size;
Share.shareXFiles( unawaited(
downloadedXFiles, Share.shareXFiles(
sharePositionOrigin: Rect.fromPoints(Offset.zero, Offset(size.width / 3, size.height)), downloadedXFiles,
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)) {
_toggleTrashStatusForAssets(assets); await _toggleTrashStatusForAssets(assets);
} }
try { try {
+1 -1
View File
@@ -251,7 +251,7 @@ class UploadService {
return; return;
} }
enqueueTasks([uploadTask]); await 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");
} }
+4 -2
View File
@@ -1,8 +1,10 @@
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';
@@ -68,7 +70,7 @@ class MapUtils {
try { try {
bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled && !silent) { if (!serviceEnabled && !silent) {
showDialog(context: context, builder: (context) => _LocationServiceDisabledDialog()); unawaited(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;
} }
ref.read(assetServiceProvider).changeDateTime(selection.toList(), dateTime); await 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;
} }
ref.read(assetServiceProvider).changeLocation(selection.toList(), location); await ref.read(assetServiceProvider).changeLocation(selection.toList(), location);
} }
Future<void> handleSetAssetsVisibility( Future<void> handleSetAssetsVisibility(
@@ -1,3 +1,5 @@
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';
@@ -57,7 +59,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);
context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()])); unawaited(context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()])));
if (!success) { if (!success) {
ImmichToast.show( ImmichToast.show(
@@ -105,7 +107,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) {
context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()])); unawaited(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) {
ref.watch(albumProvider.notifier).refreshRemoteAlbums(); unawaited(ref.watch(albumProvider.notifier).refreshRemoteAlbums());
selectionEnabledHook.value = false; selectionEnabledHook.value = false;
context.pushRoute(AlbumViewerRoute(albumId: result.id)); unawaited(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) {
handleEditDateTime(ref, context, remoteAssets.toList()); unawaited(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) {
handleEditLocation(ref, context, remoteAssets.toList()); unawaited(handleEditLocation(ref, context, remoteAssets.toList()));
} }
} finally { } finally {
selectionEnabledHook.value = false; selectionEnabledHook.value = false;
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
@@ -81,7 +82,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
context.maybePop(); await context.maybePop();
} }
totalAssets.value -= 1; totalAssets.value -= 1;
@@ -111,18 +112,20 @@ class BottomGalleryBar extends ConsumerWidget {
} }
// Asset is permanently removed // Asset is permanently removed
showDialog( unawaited(
context: context, showDialog(
builder: (BuildContext _) { context: context,
return DeleteDialog( builder: (BuildContext _) {
onDelete: () async { return DeleteDialog(
final isDeleted = await onDelete(true); onDelete: () async {
if (isDeleted) { final isDeleted = await onDelete(true);
removeAssetFromStack(); if (isDeleted) {
} removeAssetFromStack();
}, }
); },
}, );
},
),
); );
} }
@@ -150,7 +153,7 @@ class BottomGalleryBar extends ConsumerWidget {
onTap: () async { onTap: () async {
await unStack(); await unStack();
ctx.pop(); ctx.pop();
context.maybePop(); await context.maybePop();
}, },
title: const Text("viewer_unstack", style: TextStyle(fontWeight: FontWeight.bold)).tr(), title: const Text("viewer_unstack", style: TextStyle(fontWeight: FontWeight.bold)).tr(),
), ),
@@ -178,9 +181,11 @@ 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));
context.navigator.push( unawaited(
MaterialPageRoute( context.navigator.push(
builder: (context) => EditImagePage(asset: asset, image: image, isEdited: false), MaterialPageRoute(
builder: (context) => EditImagePage(asset: asset, image: image, isEdited: false),
),
), ),
); );
} }
@@ -1,3 +1,5 @@
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';
@@ -93,7 +95,7 @@ class CastDialog extends ConsumerWidget {
} }
if (!isCurrentDevice(deviceName)) { if (!isCurrentDevice(deviceName)) {
ref.read(castProvider.notifier).connect(type, deviceObj); unawaited(ref.read(castProvider.notifier).connect(type, deviceObj));
} }
}, },
); );
@@ -1,11 +1,12 @@
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;
@@ -68,7 +69,7 @@ class ExifMap extends StatelessWidget {
} }
dPrint(() => 'Opening Map Uri: $uri'); dPrint(() => 'Opening Map Uri: $uri');
launchUrl(uri); unawaited(launchUrl(uri));
}, },
onCreated: onMapCreated, onCreated: onMapCreated,
); );
@@ -1,19 +1,21 @@
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:immich_mobile/entities/store.entity.dart';
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/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';
@@ -97,25 +99,27 @@ class ImmichAppBarDialog extends HookConsumerWidget {
return; return;
} }
showDialog( unawaited(
context: context, showDialog(
builder: (BuildContext ctx) { context: context,
return ConfirmDialog( builder: (BuildContext ctx) {
title: "app_bar_signout_dialog_title", return ConfirmDialog(
content: "app_bar_signout_dialog_content", title: "app_bar_signout_dialog_title",
ok: "yes", content: "app_bar_signout_dialog_content",
onOk: () async { ok: "yes",
isLoggingOut.value = true; onOk: () async {
await ref.read(authProvider.notifier).logout().whenComplete(() => isLoggingOut.value = false); isLoggingOut.value = true;
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();
ref.read(assetProvider.notifier).clearAllAssets(); unawaited(ref.read(assetProvider.notifier).clearAllAssets());
ref.read(websocketProvider.notifier).disconnect(); ref.read(websocketProvider.notifier).disconnect();
context.replaceRoute(const LoginRoute()); unawaited(context.replaceRoute(const LoginRoute()));
}, },
); );
}, },
),
); );
}, },
trailing: isLoggingOut.value trailing: isLoggingOut.value
@@ -1,3 +1,5 @@
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';
@@ -6,8 +8,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/infrastructure/readonly_mode.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/infrastructure/readonly_mode.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';
@@ -54,7 +56,7 @@ class AppBarProfileInfoBox extends HookConsumerWidget {
ref.read(currentUserProvider.notifier).refresh(); ref.read(currentUserProvider.notifier).refresh();
} }
ref.read(backupProvider.notifier).updateDiskInfo(); unawaited(ref.read(backupProvider.notifier).updateDiskInfo());
} }
} }
} }
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:math'; import 'dart:math';
@@ -187,16 +188,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) {
context.pushRoute(const ChangePasswordRoute()); unawaited(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();
handleSyncFlow(); unawaited(handleSyncFlow());
context.replaceRoute(const TabShellRoute()); unawaited(context.replaceRoute(const TabShellRoute()));
return; return;
} }
context.replaceRoute(const TabControllerRoute()); unawaited(context.replaceRoute(const TabControllerRoute()));
} }
} catch (error) { } catch (error) {
ImmichToast.show( ImmichToast.show(
@@ -286,15 +287,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)) {
ref.watch(backupProvider.notifier).resumeBackup(); unawaited(ref.watch(backupProvider.notifier).resumeBackup());
} }
if (isBeta) { if (isBeta) {
await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission(); await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission();
handleSyncFlow(); unawaited(handleSyncFlow());
context.replaceRoute(const TabShellRoute()); unawaited(context.replaceRoute(const TabShellRoute()));
return; return;
} }
context.replaceRoute(const TabControllerRoute()); unawaited(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);
+7 -3
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/widgets/map/map_asset_grid.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/utils/draggable_scroll_controller.dart'; import 'package:immich_mobile/utils/draggable_scroll_controller.dart';
import 'package:immich_mobile/widgets/map/map_asset_grid.dart';
class MapBottomSheet extends HookConsumerWidget { class MapBottomSheet extends HookConsumerWidget {
final Stream<MapEvent> mapEventStream; final Stream<MapEvent> mapEventStream;
@@ -34,7 +34,11 @@ class MapBottomSheet extends HookConsumerWidget {
void handleMapEvents(MapEvent event) async { void handleMapEvents(MapEvent event) async {
if (event is MapCloseBottomSheet) { if (event is MapCloseBottomSheet) {
sheetController.animateTo(0.1, duration: const Duration(milliseconds: 200), curve: Curves.linearToEaseOut); await sheetController.animateTo(
0.1,
duration: const Duration(milliseconds: 200),
curve: Curves.linearToEaseOut,
);
} }
} }
@@ -1,3 +1,5 @@
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';
@@ -45,7 +47,7 @@ class BetaTimelineListTile extends ConsumerWidget {
ElevatedButton( ElevatedButton(
onPressed: () async { onPressed: () async {
Navigator.of(context).pop(); Navigator.of(context).pop();
context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: value)]); unawaited(context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: value)]));
}, },
child: Text("ok".t(context: context)), child: Text("ok".t(context: context)),
), ),
@@ -1,3 +1,5 @@
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';
@@ -102,13 +104,13 @@ class LocalNetworkPreference extends HookConsumerWidget {
), ),
); );
} else { } else {
saveWifiName(wifiName); unawaited(saveWifiName(wifiName));
} }
final serverEndpoint = ref.read(authProvider.notifier).getServerEndpoint(); final serverEndpoint = ref.read(authProvider.notifier).getServerEndpoint();
if (serverEndpoint != null) { if (serverEndpoint != null) {
saveLocalEndpoint(serverEndpoint); unawaited(saveLocalEndpoint(serverEndpoint));
} }
} }
@@ -53,7 +53,7 @@ void main() {
}); });
tearDown(() async { tearDown(() async {
sut.dispose(); unawaited(sut.dispose());
await controller.close(); await controller.close();
}); });
@@ -129,7 +129,7 @@ void main() {
final stream = sut.watch(StoreKey.accessToken); final stream = sut.watch(StoreKey.accessToken);
final events = <String?>[_kAccessToken, _kAccessToken.toUpperCase(), null, _kAccessToken.toLowerCase()]; final events = <String?>[_kAccessToken, _kAccessToken.toUpperCase(), null, _kAccessToken.toLowerCase()];
expectLater(stream, emitsInOrder(events)); unawaited(expectLater(stream, emitsInOrder(events)));
for (final event in events) { for (final event in events) {
valueController.add(event); valueController.add(event);
@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.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/user.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart';
@@ -99,7 +101,7 @@ void main() {
final count = await db.storeValues.count(); final count = await db.storeValues.count();
expect(count, isNot(isZero)); expect(count, isNot(isZero));
await sut.deleteAll(); await sut.deleteAll();
expectLater(await db.storeValues.count(), isZero); unawaited(expectLater(await db.storeValues.count(), isZero));
}); });
}); });
@@ -124,29 +126,31 @@ void main() {
test('watch()', () async { test('watch()', () async {
final stream = sut.watch(StoreKey.version); final stream = sut.watch(StoreKey.version);
expectLater(stream, emitsInOrder([_kTestVersion, _kTestVersion + 10])); unawaited(expectLater(stream, emitsInOrder([_kTestVersion, _kTestVersion + 10])));
await pumpEventQueue(); await pumpEventQueue();
await sut.upsert(StoreKey.version, _kTestVersion + 10); await sut.upsert(StoreKey.version, _kTestVersion + 10);
}); });
test('watchAll()', () async { test('watchAll()', () async {
final stream = sut.watchAll(); final stream = sut.watchAll();
expectLater( unawaited(
stream, expectLater(
emitsInOrder([ stream,
[ emitsInOrder([
const StoreDto<Object>(StoreKey.version, _kTestVersion), [
StoreDto<Object>(StoreKey.backupFailedSince, _kTestBackupFailed), const StoreDto<Object>(StoreKey.version, _kTestVersion),
const StoreDto<Object>(StoreKey.accessToken, _kTestAccessToken), StoreDto<Object>(StoreKey.backupFailedSince, _kTestBackupFailed),
const StoreDto<Object>(StoreKey.colorfulInterface, _kTestColorfulInterface), const StoreDto<Object>(StoreKey.accessToken, _kTestAccessToken),
], const StoreDto<Object>(StoreKey.colorfulInterface, _kTestColorfulInterface),
[ ],
const StoreDto<Object>(StoreKey.version, _kTestVersion + 10), [
StoreDto<Object>(StoreKey.backupFailedSince, _kTestBackupFailed), const StoreDto<Object>(StoreKey.version, _kTestVersion + 10),
const StoreDto<Object>(StoreKey.accessToken, _kTestAccessToken), StoreDto<Object>(StoreKey.backupFailedSince, _kTestBackupFailed),
const StoreDto<Object>(StoreKey.colorfulInterface, _kTestColorfulInterface), const StoreDto<Object>(StoreKey.accessToken, _kTestAccessToken),
], const StoreDto<Object>(StoreKey.colorfulInterface, _kTestColorfulInterface),
]), ],
]),
),
); );
await sut.upsert(StoreKey.version, _kTestVersion + 10); await sut.upsert(StoreKey.version, _kTestVersion + 10);
}); });
@@ -64,9 +64,9 @@ void main() {
TestUtils.init(); TestUtils.init();
db = await TestUtils.initIsar(); db = await TestUtils.initIsar();
await StoreService.init(storeRepository: IsarStoreRepository(db)); await StoreService.init(storeRepository: IsarStoreRepository(db));
Store.put(StoreKey.currentUser, UserStub.admin); await Store.put(StoreKey.currentUser, UserStub.admin);
Store.put(StoreKey.serverEndpoint, ''); await Store.put(StoreKey.serverEndpoint, '');
Store.put(StoreKey.accessToken, ''); await Store.put(StoreKey.accessToken, '');
}); });
setUp(() async { setUp(() async {
@@ -35,8 +35,8 @@ void main() {
TestUtils.init(); TestUtils.init();
db = await TestUtils.initIsar(); db = await TestUtils.initIsar();
await StoreService.init(storeRepository: IsarStoreRepository(db)); await StoreService.init(storeRepository: IsarStoreRepository(db));
Store.put(StoreKey.currentUser, UserStub.admin); await Store.put(StoreKey.currentUser, UserStub.admin);
Store.put(StoreKey.serverEndpoint, ''); await Store.put(StoreKey.serverEndpoint, '');
}); });
setUp(() { setUp(() {
@@ -31,9 +31,9 @@ void main() {
db = await TestUtils.initIsar(); db = await TestUtils.initIsar();
// For UserCircleAvatar // For UserCircleAvatar
await StoreService.init(storeRepository: IsarStoreRepository(db)); await StoreService.init(storeRepository: IsarStoreRepository(db));
Store.put(StoreKey.currentUser, UserStub.admin); await Store.put(StoreKey.currentUser, UserStub.admin);
Store.put(StoreKey.serverEndpoint, ''); await Store.put(StoreKey.serverEndpoint, '');
Store.put(StoreKey.accessToken, ''); await Store.put(StoreKey.accessToken, '');
}); });
setUp(() { setUp(() {

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