Compare commits
22 Commits
refactor/r
...
feat/mobil
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7404e7495 | ||
|
|
7cd5b7f64d | ||
|
|
fb798a3492 | ||
|
|
73d650de23 | ||
|
|
a07f3c2ba4 | ||
|
|
9ccd98d871 | ||
|
|
188dcbf7d0 | ||
|
|
51d106d192 | ||
|
|
3e427e42cb | ||
|
|
f6a99602e9 | ||
|
|
52363cf0fb | ||
|
|
86df09a0e4 | ||
|
|
e1e24f3d60 | ||
|
|
33d76fb386 | ||
|
|
642065f506 | ||
|
|
de897f6069 | ||
|
|
68f3ed89c5 | ||
|
|
78516a97b3 | ||
|
|
b8a17c3c26 | ||
|
|
e42886b767 | ||
|
|
d36c26bf97 | ||
|
|
dcbc266b83 |
@@ -5,8 +5,7 @@
|
|||||||
"immich-server",
|
"immich-server",
|
||||||
"redis",
|
"redis",
|
||||||
"database",
|
"database",
|
||||||
"immich-machine-learning",
|
"immich-machine-learning"
|
||||||
"init"
|
|
||||||
],
|
],
|
||||||
"dockerComposeFile": [
|
"dockerComposeFile": [
|
||||||
"../docker/docker-compose.dev.yml",
|
"../docker/docker-compose.dev.yml",
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -18,6 +18,7 @@ mobile/libisar.dylib
|
|||||||
mobile/openapi/test
|
mobile/openapi/test
|
||||||
mobile/openapi/doc
|
mobile/openapi/doc
|
||||||
mobile/openapi/.openapi-generator/FILES
|
mobile/openapi/.openapi-generator/FILES
|
||||||
|
mobile/ios/build
|
||||||
|
|
||||||
open-api/typescript-sdk/build
|
open-api/typescript-sdk/build
|
||||||
mobile/android/fastlane/report.xml
|
mobile/android/fastlane/report.xml
|
||||||
|
|||||||
34
mise.lock
34
mise.lock
@@ -1,34 +0,0 @@
|
|||||||
[tools.dart]
|
|
||||||
version = "3.8.2"
|
|
||||||
backend = "asdf:dart"
|
|
||||||
|
|
||||||
[tools.flutter]
|
|
||||||
version = "3.35.3-stable"
|
|
||||||
backend = "asdf:flutter"
|
|
||||||
|
|
||||||
[tools."github:CQLabs/homebrew-dcm"]
|
|
||||||
version = "1.31.4"
|
|
||||||
backend = "github:CQLabs/homebrew-dcm"
|
|
||||||
|
|
||||||
[tools."github:CQLabs/homebrew-dcm".platforms.linux-x64]
|
|
||||||
checksum = "blake3:e9df5b765df327e1248fccf2c6165a89d632a065667f99c01765bf3047b94955"
|
|
||||||
size = 8821083
|
|
||||||
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.31.4/dcm-linux-x64-release.zip"
|
|
||||||
|
|
||||||
[tools.node]
|
|
||||||
version = "22.18.0"
|
|
||||||
backend = "core:node"
|
|
||||||
|
|
||||||
[tools.node.platforms.linux-x64]
|
|
||||||
checksum = "sha256:a2e703725d8683be86bb5da967bf8272f4518bdaf10f21389e2b2c9eaeae8c8a"
|
|
||||||
size = 54824343
|
|
||||||
url = "https://nodejs.org/dist/v22.18.0/node-v22.18.0-linux-x64.tar.gz"
|
|
||||||
|
|
||||||
[tools.pnpm]
|
|
||||||
version = "10.14.0"
|
|
||||||
backend = "aqua:pnpm/pnpm"
|
|
||||||
|
|
||||||
[tools.pnpm.platforms.linux-x64]
|
|
||||||
checksum = "blake3:13dfa46b7173d3cad3bad60a756a492ecf0bce48b23eb9f793e7ccec5a09b46d"
|
|
||||||
size = 66231525
|
|
||||||
url = "https://github.com/pnpm/pnpm/releases/download/v10.14.0/pnpm-linux-x64"
|
|
||||||
@@ -11,7 +11,6 @@ postinstall = "chmod +x $MISE_TOOL_INSTALL_PATH/dcm"
|
|||||||
|
|
||||||
[settings]
|
[settings]
|
||||||
experimental = true
|
experimental = true
|
||||||
lockfile = true
|
|
||||||
pin = true
|
pin = true
|
||||||
|
|
||||||
# .github
|
# .github
|
||||||
|
|||||||
@@ -3,14 +3,19 @@ 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
|
||||||
@@ -172,7 +177,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) {
|
||||||
resolver.loadThumbnail(uri, Size(targetWidth, targetHeight), signal)
|
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)
|
||||||
@@ -185,7 +190,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)
|
||||||
resolver.loadThumbnail(uri, Size(targetWidth, targetHeight), signal)
|
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)
|
||||||
@@ -215,4 +220,72 @@ 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
mobile/drift_schemas/main/drift_schema_v11.json
generated
Normal file
1
mobile/drift_schemas/main/drift_schema_v11.json
generated
Normal file
File diff suppressed because one or more lines are too long
@@ -3,7 +3,7 @@
|
|||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 77;
|
objectVersion = 54;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
@@ -29,9 +29,11 @@
|
|||||||
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 */; };
|
||||||
FEAFA8732E4D42F4001E47FE /* Thumbhash.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEAFA8722E4D42F4001E47FE /* Thumbhash.swift */; };
|
FEC340D12E7326630050078A /* AssetResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC340C92E7326630050078A /* AssetResolver.swift */; };
|
||||||
FED3B1962E253E9B0030FD97 /* ThumbnailsImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED3B1942E253E9B0030FD97 /* ThumbnailsImpl.swift */; };
|
FEC340D22E7326630050078A /* Thumbhash.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC340CB2E7326630050078A /* Thumbhash.swift */; };
|
||||||
FED3B1972E253E9B0030FD97 /* Thumbnails.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED3B1932E253E9B0030FD97 /* Thumbnails.g.swift */; };
|
FEC340D32E7326630050078A /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC340CF2E7326630050078A /* Request.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 */
|
||||||
@@ -115,9 +117,11 @@
|
|||||||
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>"; };
|
||||||
FEAFA8722E4D42F4001E47FE /* Thumbhash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thumbhash.swift; sourceTree = "<group>"; };
|
FEC340C92E7326630050078A /* AssetResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetResolver.swift; sourceTree = "<group>"; };
|
||||||
FED3B1932E253E9B0030FD97 /* Thumbnails.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thumbnails.g.swift; sourceTree = "<group>"; };
|
FEC340CB2E7326630050078A /* Thumbhash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thumbhash.swift; sourceTree = "<group>"; };
|
||||||
FED3B1942E253E9B0030FD97 /* ThumbnailsImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThumbnailsImpl.swift; sourceTree = "<group>"; };
|
FEC340CC2E7326630050078A /* ThumbnailResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThumbnailResolver.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 */
|
||||||
@@ -133,6 +137,8 @@
|
|||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = {
|
B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
|
exceptions = (
|
||||||
|
);
|
||||||
path = Sync;
|
path = Sync;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -245,6 +251,7 @@
|
|||||||
97C146F01CF9000F007C117D /* Runner */ = {
|
97C146F01CF9000F007C117D /* Runner */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
FEC340D02E7326630050078A /* Resolvers */,
|
||||||
B25D37792E72CA15008B6CA7 /* Connectivity */,
|
B25D37792E72CA15008B6CA7 /* Connectivity */,
|
||||||
B21E34A62E5AF9760031FDB9 /* Background */,
|
B21E34A62E5AF9760031FDB9 /* Background */,
|
||||||
B2CF7F8C2DDE4EBB00744BF6 /* Sync */,
|
B2CF7F8C2DDE4EBB00744BF6 /* Sync */,
|
||||||
@@ -259,7 +266,6 @@
|
|||||||
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>";
|
||||||
@@ -294,16 +300,34 @@
|
|||||||
path = ShareExtension;
|
path = ShareExtension;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
FED3B1952E253E9B0030FD97 /* Images */ = {
|
FEC340CA2E7326630050078A /* Assets */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
FEAFA8722E4D42F4001E47FE /* Thumbhash.swift */,
|
FEC340C92E7326630050078A /* AssetResolver.swift */,
|
||||||
FED3B1932E253E9B0030FD97 /* Thumbnails.g.swift */,
|
);
|
||||||
FED3B1942E253E9B0030FD97 /* ThumbnailsImpl.swift */,
|
path = Assets;
|
||||||
|
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 */
|
||||||
@@ -519,14 +543,10 @@
|
|||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
inputPaths = (
|
|
||||||
);
|
|
||||||
name = "[CP] Copy Pods Resources";
|
name = "[CP] Copy Pods Resources";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
outputPaths = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||||
@@ -555,14 +575,10 @@
|
|||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
inputPaths = (
|
|
||||||
);
|
|
||||||
name = "[CP] Embed Pods Frameworks";
|
name = "[CP] Embed Pods Frameworks";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
outputPaths = (
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||||
@@ -579,14 +595,16 @@
|
|||||||
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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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: ThumbnailApiImpl())
|
ThumbnailApiSetup.setUp(binaryMessenger: binaryMessenger, api: ThumbnailResolver())
|
||||||
BackgroundWorkerFgHostApiSetup.setUp(binaryMessenger: binaryMessenger, api: BackgroundWorkerApiImpl())
|
BackgroundWorkerFgHostApiSetup.setUp(binaryMessenger: binaryMessenger, api: BackgroundWorkerApiImpl())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,211 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
115
mobile/ios/Runner/Resolvers/Assets/AssetResolver.swift
Normal file
115
mobile/ios/Runner/Resolvers/Assets/AssetResolver.swift
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
198
mobile/ios/Runner/Resolvers/Images/ThumbnailResolver.swift
Normal file
198
mobile/ios/Runner/Resolvers/Images/ThumbnailResolver.swift
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
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
mobile/ios/Runner/Resolvers/Request.swift
Normal file
20
mobile/ios/Runner/Resolvers/Request.swift
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,9 @@ class LocalAlbumAssetEntity extends Table with DriftDefaultsMixin {
|
|||||||
|
|
||||||
TextColumn get albumId => text().references(LocalAlbumEntity, #id, onDelete: KeyAction.cascade)();
|
TextColumn get albumId => text().references(LocalAlbumEntity, #id, onDelete: KeyAction.cascade)();
|
||||||
|
|
||||||
|
// Used for mark & sweep
|
||||||
|
BoolColumn get marker_ => boolean().nullable()();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Set<Column> get primaryKey => {assetId, albumId};
|
Set<Column> get primaryKey => {assetId, albumId};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,11 +15,13 @@ typedef $$LocalAlbumAssetEntityTableCreateCompanionBuilder =
|
|||||||
i1.LocalAlbumAssetEntityCompanion Function({
|
i1.LocalAlbumAssetEntityCompanion Function({
|
||||||
required String assetId,
|
required String assetId,
|
||||||
required String albumId,
|
required String albumId,
|
||||||
|
i0.Value<bool?> marker_,
|
||||||
});
|
});
|
||||||
typedef $$LocalAlbumAssetEntityTableUpdateCompanionBuilder =
|
typedef $$LocalAlbumAssetEntityTableUpdateCompanionBuilder =
|
||||||
i1.LocalAlbumAssetEntityCompanion Function({
|
i1.LocalAlbumAssetEntityCompanion Function({
|
||||||
i0.Value<String> assetId,
|
i0.Value<String> assetId,
|
||||||
i0.Value<String> albumId,
|
i0.Value<String> albumId,
|
||||||
|
i0.Value<bool?> marker_,
|
||||||
});
|
});
|
||||||
|
|
||||||
final class $$LocalAlbumAssetEntityTableReferences
|
final class $$LocalAlbumAssetEntityTableReferences
|
||||||
@@ -113,6 +115,11 @@ class $$LocalAlbumAssetEntityTableFilterComposer
|
|||||||
super.$addJoinBuilderToRootComposer,
|
super.$addJoinBuilderToRootComposer,
|
||||||
super.$removeJoinBuilderFromRootComposer,
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
});
|
});
|
||||||
|
i0.ColumnFilters<bool> get marker_ => $composableBuilder(
|
||||||
|
column: $table.marker_,
|
||||||
|
builder: (column) => i0.ColumnFilters(column),
|
||||||
|
);
|
||||||
|
|
||||||
i3.$$LocalAssetEntityTableFilterComposer get assetId {
|
i3.$$LocalAssetEntityTableFilterComposer get assetId {
|
||||||
final i3.$$LocalAssetEntityTableFilterComposer composer = $composerBuilder(
|
final i3.$$LocalAssetEntityTableFilterComposer composer = $composerBuilder(
|
||||||
composer: this,
|
composer: this,
|
||||||
@@ -177,6 +184,11 @@ class $$LocalAlbumAssetEntityTableOrderingComposer
|
|||||||
super.$addJoinBuilderToRootComposer,
|
super.$addJoinBuilderToRootComposer,
|
||||||
super.$removeJoinBuilderFromRootComposer,
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
});
|
});
|
||||||
|
i0.ColumnOrderings<bool> get marker_ => $composableBuilder(
|
||||||
|
column: $table.marker_,
|
||||||
|
builder: (column) => i0.ColumnOrderings(column),
|
||||||
|
);
|
||||||
|
|
||||||
i3.$$LocalAssetEntityTableOrderingComposer get assetId {
|
i3.$$LocalAssetEntityTableOrderingComposer get assetId {
|
||||||
final i3.$$LocalAssetEntityTableOrderingComposer composer =
|
final i3.$$LocalAssetEntityTableOrderingComposer composer =
|
||||||
$composerBuilder(
|
$composerBuilder(
|
||||||
@@ -243,6 +255,9 @@ class $$LocalAlbumAssetEntityTableAnnotationComposer
|
|||||||
super.$addJoinBuilderToRootComposer,
|
super.$addJoinBuilderToRootComposer,
|
||||||
super.$removeJoinBuilderFromRootComposer,
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
});
|
});
|
||||||
|
i0.GeneratedColumn<bool> get marker_ =>
|
||||||
|
$composableBuilder(column: $table.marker_, builder: (column) => column);
|
||||||
|
|
||||||
i3.$$LocalAssetEntityTableAnnotationComposer get assetId {
|
i3.$$LocalAssetEntityTableAnnotationComposer get assetId {
|
||||||
final i3.$$LocalAssetEntityTableAnnotationComposer composer =
|
final i3.$$LocalAssetEntityTableAnnotationComposer composer =
|
||||||
$composerBuilder(
|
$composerBuilder(
|
||||||
@@ -344,16 +359,22 @@ class $$LocalAlbumAssetEntityTableTableManager
|
|||||||
({
|
({
|
||||||
i0.Value<String> assetId = const i0.Value.absent(),
|
i0.Value<String> assetId = const i0.Value.absent(),
|
||||||
i0.Value<String> albumId = const i0.Value.absent(),
|
i0.Value<String> albumId = const i0.Value.absent(),
|
||||||
|
i0.Value<bool?> marker_ = const i0.Value.absent(),
|
||||||
}) => i1.LocalAlbumAssetEntityCompanion(
|
}) => i1.LocalAlbumAssetEntityCompanion(
|
||||||
assetId: assetId,
|
assetId: assetId,
|
||||||
albumId: albumId,
|
albumId: albumId,
|
||||||
|
marker_: marker_,
|
||||||
),
|
),
|
||||||
createCompanionCallback:
|
createCompanionCallback:
|
||||||
({required String assetId, required String albumId}) =>
|
({
|
||||||
i1.LocalAlbumAssetEntityCompanion.insert(
|
required String assetId,
|
||||||
assetId: assetId,
|
required String albumId,
|
||||||
albumId: albumId,
|
i0.Value<bool?> marker_ = const i0.Value.absent(),
|
||||||
),
|
}) => i1.LocalAlbumAssetEntityCompanion.insert(
|
||||||
|
assetId: assetId,
|
||||||
|
albumId: albumId,
|
||||||
|
marker_: marker_,
|
||||||
|
),
|
||||||
withReferenceMapper: (p0) => p0
|
withReferenceMapper: (p0) => p0
|
||||||
.map(
|
.map(
|
||||||
(e) => (
|
(e) => (
|
||||||
@@ -477,8 +498,22 @@ class $LocalAlbumAssetEntityTable extends i2.LocalAlbumAssetEntity
|
|||||||
'REFERENCES local_album_entity (id) ON DELETE CASCADE',
|
'REFERENCES local_album_entity (id) ON DELETE CASCADE',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
static const i0.VerificationMeta _marker_Meta = const i0.VerificationMeta(
|
||||||
|
'marker_',
|
||||||
|
);
|
||||||
@override
|
@override
|
||||||
List<i0.GeneratedColumn> get $columns => [assetId, albumId];
|
late final i0.GeneratedColumn<bool> marker_ = i0.GeneratedColumn<bool>(
|
||||||
|
'marker',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i0.DriftSqlType.bool,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("marker" IN (0, 1))',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
@override
|
||||||
|
List<i0.GeneratedColumn> get $columns => [assetId, albumId, marker_];
|
||||||
@override
|
@override
|
||||||
String get aliasedName => _alias ?? actualTableName;
|
String get aliasedName => _alias ?? actualTableName;
|
||||||
@override
|
@override
|
||||||
@@ -507,6 +542,12 @@ class $LocalAlbumAssetEntityTable extends i2.LocalAlbumAssetEntity
|
|||||||
} else if (isInserting) {
|
} else if (isInserting) {
|
||||||
context.missing(_albumIdMeta);
|
context.missing(_albumIdMeta);
|
||||||
}
|
}
|
||||||
|
if (data.containsKey('marker')) {
|
||||||
|
context.handle(
|
||||||
|
_marker_Meta,
|
||||||
|
marker_.isAcceptableOrUnknown(data['marker']!, _marker_Meta),
|
||||||
|
);
|
||||||
|
}
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -527,6 +568,10 @@ class $LocalAlbumAssetEntityTable extends i2.LocalAlbumAssetEntity
|
|||||||
i0.DriftSqlType.string,
|
i0.DriftSqlType.string,
|
||||||
data['${effectivePrefix}album_id'],
|
data['${effectivePrefix}album_id'],
|
||||||
)!,
|
)!,
|
||||||
|
marker_: attachedDatabase.typeMapping.read(
|
||||||
|
i0.DriftSqlType.bool,
|
||||||
|
data['${effectivePrefix}marker'],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -545,15 +590,20 @@ class LocalAlbumAssetEntityData extends i0.DataClass
|
|||||||
implements i0.Insertable<i1.LocalAlbumAssetEntityData> {
|
implements i0.Insertable<i1.LocalAlbumAssetEntityData> {
|
||||||
final String assetId;
|
final String assetId;
|
||||||
final String albumId;
|
final String albumId;
|
||||||
|
final bool? marker_;
|
||||||
const LocalAlbumAssetEntityData({
|
const LocalAlbumAssetEntityData({
|
||||||
required this.assetId,
|
required this.assetId,
|
||||||
required this.albumId,
|
required this.albumId,
|
||||||
|
this.marker_,
|
||||||
});
|
});
|
||||||
@override
|
@override
|
||||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||||
final map = <String, i0.Expression>{};
|
final map = <String, i0.Expression>{};
|
||||||
map['asset_id'] = i0.Variable<String>(assetId);
|
map['asset_id'] = i0.Variable<String>(assetId);
|
||||||
map['album_id'] = i0.Variable<String>(albumId);
|
map['album_id'] = i0.Variable<String>(albumId);
|
||||||
|
if (!nullToAbsent || marker_ != null) {
|
||||||
|
map['marker'] = i0.Variable<bool>(marker_);
|
||||||
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -565,6 +615,7 @@ class LocalAlbumAssetEntityData extends i0.DataClass
|
|||||||
return LocalAlbumAssetEntityData(
|
return LocalAlbumAssetEntityData(
|
||||||
assetId: serializer.fromJson<String>(json['assetId']),
|
assetId: serializer.fromJson<String>(json['assetId']),
|
||||||
albumId: serializer.fromJson<String>(json['albumId']),
|
albumId: serializer.fromJson<String>(json['albumId']),
|
||||||
|
marker_: serializer.fromJson<bool?>(json['marker_']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@override
|
@override
|
||||||
@@ -573,20 +624,26 @@ class LocalAlbumAssetEntityData extends i0.DataClass
|
|||||||
return <String, dynamic>{
|
return <String, dynamic>{
|
||||||
'assetId': serializer.toJson<String>(assetId),
|
'assetId': serializer.toJson<String>(assetId),
|
||||||
'albumId': serializer.toJson<String>(albumId),
|
'albumId': serializer.toJson<String>(albumId),
|
||||||
|
'marker_': serializer.toJson<bool?>(marker_),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
i1.LocalAlbumAssetEntityData copyWith({String? assetId, String? albumId}) =>
|
i1.LocalAlbumAssetEntityData copyWith({
|
||||||
i1.LocalAlbumAssetEntityData(
|
String? assetId,
|
||||||
assetId: assetId ?? this.assetId,
|
String? albumId,
|
||||||
albumId: albumId ?? this.albumId,
|
i0.Value<bool?> marker_ = const i0.Value.absent(),
|
||||||
);
|
}) => i1.LocalAlbumAssetEntityData(
|
||||||
|
assetId: assetId ?? this.assetId,
|
||||||
|
albumId: albumId ?? this.albumId,
|
||||||
|
marker_: marker_.present ? marker_.value : this.marker_,
|
||||||
|
);
|
||||||
LocalAlbumAssetEntityData copyWithCompanion(
|
LocalAlbumAssetEntityData copyWithCompanion(
|
||||||
i1.LocalAlbumAssetEntityCompanion data,
|
i1.LocalAlbumAssetEntityCompanion data,
|
||||||
) {
|
) {
|
||||||
return LocalAlbumAssetEntityData(
|
return LocalAlbumAssetEntityData(
|
||||||
assetId: data.assetId.present ? data.assetId.value : this.assetId,
|
assetId: data.assetId.present ? data.assetId.value : this.assetId,
|
||||||
albumId: data.albumId.present ? data.albumId.value : this.albumId,
|
albumId: data.albumId.present ? data.albumId.value : this.albumId,
|
||||||
|
marker_: data.marker_.present ? data.marker_.value : this.marker_,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -594,51 +651,60 @@ class LocalAlbumAssetEntityData extends i0.DataClass
|
|||||||
String toString() {
|
String toString() {
|
||||||
return (StringBuffer('LocalAlbumAssetEntityData(')
|
return (StringBuffer('LocalAlbumAssetEntityData(')
|
||||||
..write('assetId: $assetId, ')
|
..write('assetId: $assetId, ')
|
||||||
..write('albumId: $albumId')
|
..write('albumId: $albumId, ')
|
||||||
|
..write('marker_: $marker_')
|
||||||
..write(')'))
|
..write(')'))
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(assetId, albumId);
|
int get hashCode => Object.hash(assetId, albumId, marker_);
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
(other is i1.LocalAlbumAssetEntityData &&
|
(other is i1.LocalAlbumAssetEntityData &&
|
||||||
other.assetId == this.assetId &&
|
other.assetId == this.assetId &&
|
||||||
other.albumId == this.albumId);
|
other.albumId == this.albumId &&
|
||||||
|
other.marker_ == this.marker_);
|
||||||
}
|
}
|
||||||
|
|
||||||
class LocalAlbumAssetEntityCompanion
|
class LocalAlbumAssetEntityCompanion
|
||||||
extends i0.UpdateCompanion<i1.LocalAlbumAssetEntityData> {
|
extends i0.UpdateCompanion<i1.LocalAlbumAssetEntityData> {
|
||||||
final i0.Value<String> assetId;
|
final i0.Value<String> assetId;
|
||||||
final i0.Value<String> albumId;
|
final i0.Value<String> albumId;
|
||||||
|
final i0.Value<bool?> marker_;
|
||||||
const LocalAlbumAssetEntityCompanion({
|
const LocalAlbumAssetEntityCompanion({
|
||||||
this.assetId = const i0.Value.absent(),
|
this.assetId = const i0.Value.absent(),
|
||||||
this.albumId = const i0.Value.absent(),
|
this.albumId = const i0.Value.absent(),
|
||||||
|
this.marker_ = const i0.Value.absent(),
|
||||||
});
|
});
|
||||||
LocalAlbumAssetEntityCompanion.insert({
|
LocalAlbumAssetEntityCompanion.insert({
|
||||||
required String assetId,
|
required String assetId,
|
||||||
required String albumId,
|
required String albumId,
|
||||||
|
this.marker_ = const i0.Value.absent(),
|
||||||
}) : assetId = i0.Value(assetId),
|
}) : assetId = i0.Value(assetId),
|
||||||
albumId = i0.Value(albumId);
|
albumId = i0.Value(albumId);
|
||||||
static i0.Insertable<i1.LocalAlbumAssetEntityData> custom({
|
static i0.Insertable<i1.LocalAlbumAssetEntityData> custom({
|
||||||
i0.Expression<String>? assetId,
|
i0.Expression<String>? assetId,
|
||||||
i0.Expression<String>? albumId,
|
i0.Expression<String>? albumId,
|
||||||
|
i0.Expression<bool>? marker_,
|
||||||
}) {
|
}) {
|
||||||
return i0.RawValuesInsertable({
|
return i0.RawValuesInsertable({
|
||||||
if (assetId != null) 'asset_id': assetId,
|
if (assetId != null) 'asset_id': assetId,
|
||||||
if (albumId != null) 'album_id': albumId,
|
if (albumId != null) 'album_id': albumId,
|
||||||
|
if (marker_ != null) 'marker': marker_,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
i1.LocalAlbumAssetEntityCompanion copyWith({
|
i1.LocalAlbumAssetEntityCompanion copyWith({
|
||||||
i0.Value<String>? assetId,
|
i0.Value<String>? assetId,
|
||||||
i0.Value<String>? albumId,
|
i0.Value<String>? albumId,
|
||||||
|
i0.Value<bool?>? marker_,
|
||||||
}) {
|
}) {
|
||||||
return i1.LocalAlbumAssetEntityCompanion(
|
return i1.LocalAlbumAssetEntityCompanion(
|
||||||
assetId: assetId ?? this.assetId,
|
assetId: assetId ?? this.assetId,
|
||||||
albumId: albumId ?? this.albumId,
|
albumId: albumId ?? this.albumId,
|
||||||
|
marker_: marker_ ?? this.marker_,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -651,6 +717,9 @@ class LocalAlbumAssetEntityCompanion
|
|||||||
if (albumId.present) {
|
if (albumId.present) {
|
||||||
map['album_id'] = i0.Variable<String>(albumId.value);
|
map['album_id'] = i0.Variable<String>(albumId.value);
|
||||||
}
|
}
|
||||||
|
if (marker_.present) {
|
||||||
|
map['marker'] = i0.Variable<bool>(marker_.value);
|
||||||
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -658,7 +727,8 @@ class LocalAlbumAssetEntityCompanion
|
|||||||
String toString() {
|
String toString() {
|
||||||
return (StringBuffer('LocalAlbumAssetEntityCompanion(')
|
return (StringBuffer('LocalAlbumAssetEntityCompanion(')
|
||||||
..write('assetId: $assetId, ')
|
..write('assetId: $assetId, ')
|
||||||
..write('albumId: $albumId')
|
..write('albumId: $albumId, ')
|
||||||
|
..write('marker_: $marker_')
|
||||||
..write(')'))
|
..write(')'))
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ class Drift extends $Drift implements IDatabaseRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 10;
|
int get schemaVersion => 11;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration => MigrationStrategy(
|
MigrationStrategy get migration => MigrationStrategy(
|
||||||
@@ -156,6 +156,9 @@ class Drift extends $Drift implements IDatabaseRepository {
|
|||||||
await m.addColumn(v10.userEntity, v10.userEntity.avatarColor);
|
await m.addColumn(v10.userEntity, v10.userEntity.avatarColor);
|
||||||
await m.alterTable(TableMigration(v10.userEntity));
|
await m.alterTable(TableMigration(v10.userEntity));
|
||||||
},
|
},
|
||||||
|
from10To11: (m, v11) async {
|
||||||
|
await m.addColumn(v11.localAlbumAssetEntity, v11.localAlbumAssetEntity.marker_);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -4270,6 +4270,395 @@ i1.GeneratedColumn<String> _column_94(String aliasedName) =>
|
|||||||
true,
|
true,
|
||||||
type: i1.DriftSqlType.string,
|
type: i1.DriftSqlType.string,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final class Schema11 extends i0.VersionedSchema {
|
||||||
|
Schema11({required super.database}) : super(version: 11);
|
||||||
|
@override
|
||||||
|
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||||
|
userEntity,
|
||||||
|
remoteAssetEntity,
|
||||||
|
stackEntity,
|
||||||
|
localAssetEntity,
|
||||||
|
remoteAlbumEntity,
|
||||||
|
localAlbumEntity,
|
||||||
|
localAlbumAssetEntity,
|
||||||
|
idxLocalAssetChecksum,
|
||||||
|
idxRemoteAssetOwnerChecksum,
|
||||||
|
uQRemoteAssetsOwnerChecksum,
|
||||||
|
uQRemoteAssetsOwnerLibraryChecksum,
|
||||||
|
idxRemoteAssetChecksum,
|
||||||
|
authUserEntity,
|
||||||
|
userMetadataEntity,
|
||||||
|
partnerEntity,
|
||||||
|
remoteExifEntity,
|
||||||
|
remoteAlbumAssetEntity,
|
||||||
|
remoteAlbumUserEntity,
|
||||||
|
memoryEntity,
|
||||||
|
memoryAssetEntity,
|
||||||
|
personEntity,
|
||||||
|
assetFaceEntity,
|
||||||
|
storeEntity,
|
||||||
|
idxLatLng,
|
||||||
|
];
|
||||||
|
late final Shape20 userEntity = Shape20(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'user_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_1,
|
||||||
|
_column_3,
|
||||||
|
_column_84,
|
||||||
|
_column_85,
|
||||||
|
_column_91,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape17 remoteAssetEntity = Shape17(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_1,
|
||||||
|
_column_8,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_10,
|
||||||
|
_column_11,
|
||||||
|
_column_12,
|
||||||
|
_column_0,
|
||||||
|
_column_13,
|
||||||
|
_column_14,
|
||||||
|
_column_15,
|
||||||
|
_column_16,
|
||||||
|
_column_17,
|
||||||
|
_column_18,
|
||||||
|
_column_19,
|
||||||
|
_column_20,
|
||||||
|
_column_21,
|
||||||
|
_column_86,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape3 stackEntity = Shape3(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'stack_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [_column_0, _column_9, _column_5, _column_15, _column_75],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape2 localAssetEntity = Shape2(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'local_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_1,
|
||||||
|
_column_8,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_10,
|
||||||
|
_column_11,
|
||||||
|
_column_12,
|
||||||
|
_column_0,
|
||||||
|
_column_22,
|
||||||
|
_column_14,
|
||||||
|
_column_23,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape9 remoteAlbumEntity = Shape9(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_album_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_1,
|
||||||
|
_column_56,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_15,
|
||||||
|
_column_57,
|
||||||
|
_column_58,
|
||||||
|
_column_59,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape19 localAlbumEntity = Shape19(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'local_album_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_1,
|
||||||
|
_column_5,
|
||||||
|
_column_31,
|
||||||
|
_column_32,
|
||||||
|
_column_90,
|
||||||
|
_column_33,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape22 localAlbumAssetEntity = Shape22(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'local_album_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
|
||||||
|
columns: [_column_34, _column_35, _column_33],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
final i1.Index idxLocalAssetChecksum = i1.Index(
|
||||||
|
'idx_local_asset_checksum',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteAssetOwnerChecksum = i1.Index(
|
||||||
|
'idx_remote_asset_owner_checksum',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)',
|
||||||
|
);
|
||||||
|
final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index(
|
||||||
|
'UQ_remote_assets_owner_checksum',
|
||||||
|
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
|
||||||
|
);
|
||||||
|
final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index(
|
||||||
|
'UQ_remote_assets_owner_library_checksum',
|
||||||
|
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteAssetChecksum = i1.Index(
|
||||||
|
'idx_remote_asset_checksum',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)',
|
||||||
|
);
|
||||||
|
late final Shape21 authUserEntity = Shape21(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'auth_user_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_1,
|
||||||
|
_column_3,
|
||||||
|
_column_2,
|
||||||
|
_column_84,
|
||||||
|
_column_85,
|
||||||
|
_column_92,
|
||||||
|
_column_93,
|
||||||
|
_column_7,
|
||||||
|
_column_94,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape4 userMetadataEntity = Shape4(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'user_metadata_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(user_id, "key")'],
|
||||||
|
columns: [_column_25, _column_26, _column_27],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape5 partnerEntity = Shape5(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'partner_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'],
|
||||||
|
columns: [_column_28, _column_29, _column_30],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape8 remoteExifEntity = Shape8(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_exif_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(asset_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_36,
|
||||||
|
_column_37,
|
||||||
|
_column_38,
|
||||||
|
_column_39,
|
||||||
|
_column_40,
|
||||||
|
_column_41,
|
||||||
|
_column_11,
|
||||||
|
_column_10,
|
||||||
|
_column_42,
|
||||||
|
_column_43,
|
||||||
|
_column_44,
|
||||||
|
_column_45,
|
||||||
|
_column_46,
|
||||||
|
_column_47,
|
||||||
|
_column_48,
|
||||||
|
_column_49,
|
||||||
|
_column_50,
|
||||||
|
_column_51,
|
||||||
|
_column_52,
|
||||||
|
_column_53,
|
||||||
|
_column_54,
|
||||||
|
_column_55,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape7 remoteAlbumAssetEntity = Shape7(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_album_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
|
||||||
|
columns: [_column_36, _column_60],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape10 remoteAlbumUserEntity = Shape10(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_album_user_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(album_id, user_id)'],
|
||||||
|
columns: [_column_60, _column_25, _column_61],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape11 memoryEntity = Shape11(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'memory_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_18,
|
||||||
|
_column_15,
|
||||||
|
_column_8,
|
||||||
|
_column_62,
|
||||||
|
_column_63,
|
||||||
|
_column_64,
|
||||||
|
_column_65,
|
||||||
|
_column_66,
|
||||||
|
_column_67,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape12 memoryAssetEntity = Shape12(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'memory_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'],
|
||||||
|
columns: [_column_36, _column_68],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape14 personEntity = Shape14(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'person_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_15,
|
||||||
|
_column_1,
|
||||||
|
_column_69,
|
||||||
|
_column_71,
|
||||||
|
_column_72,
|
||||||
|
_column_73,
|
||||||
|
_column_74,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape15 assetFaceEntity = Shape15(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'asset_face_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_36,
|
||||||
|
_column_76,
|
||||||
|
_column_77,
|
||||||
|
_column_78,
|
||||||
|
_column_79,
|
||||||
|
_column_80,
|
||||||
|
_column_81,
|
||||||
|
_column_82,
|
||||||
|
_column_83,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape18 storeEntity = Shape18(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'store_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [_column_87, _column_88, _column_89],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
final i1.Index idxLatLng = i1.Index(
|
||||||
|
'idx_lat_lng',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Shape22 extends i0.VersionedTable {
|
||||||
|
Shape22({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get assetId =>
|
||||||
|
columnsByName['asset_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get albumId =>
|
||||||
|
columnsByName['album_id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<bool> get marker_ =>
|
||||||
|
columnsByName['marker']! as i1.GeneratedColumn<bool>;
|
||||||
|
}
|
||||||
|
|
||||||
i0.MigrationStepWithVersion migrationSteps({
|
i0.MigrationStepWithVersion migrationSteps({
|
||||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||||
@@ -4280,6 +4669,7 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||||||
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
|
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
|
||||||
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
|
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
|
||||||
required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10,
|
required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema11 schema) from10To11,
|
||||||
}) {
|
}) {
|
||||||
return (currentVersion, database) async {
|
return (currentVersion, database) async {
|
||||||
switch (currentVersion) {
|
switch (currentVersion) {
|
||||||
@@ -4328,6 +4718,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||||||
final migrator = i1.Migrator(database, schema);
|
final migrator = i1.Migrator(database, schema);
|
||||||
await from9To10(migrator, schema);
|
await from9To10(migrator, schema);
|
||||||
return 10;
|
return 10;
|
||||||
|
case 10:
|
||||||
|
final schema = Schema11(database: database);
|
||||||
|
final migrator = i1.Migrator(database, schema);
|
||||||
|
await from10To11(migrator, schema);
|
||||||
|
return 11;
|
||||||
default:
|
default:
|
||||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||||
}
|
}
|
||||||
@@ -4344,6 +4739,7 @@ i1.OnUpgrade stepByStep({
|
|||||||
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
|
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
|
||||||
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
|
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
|
||||||
required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10,
|
required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema11 schema) from10To11,
|
||||||
}) => i0.VersionedSchema.stepByStepHelper(
|
}) => i0.VersionedSchema.stepByStepHelper(
|
||||||
step: migrationSteps(
|
step: migrationSteps(
|
||||||
from1To2: from1To2,
|
from1To2: from1To2,
|
||||||
@@ -4355,5 +4751,6 @@ i1.OnUpgrade stepByStep({
|
|||||||
from7To8: from7To8,
|
from7To8: from7To8,
|
||||||
from8To9: from8To9,
|
from8To9: from8To9,
|
||||||
from9To10: from9To10,
|
from9To10: from9To10,
|
||||||
|
from10To11: from10To11,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -72,17 +72,33 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
|||||||
return Future.value();
|
return Future.value();
|
||||||
}
|
}
|
||||||
|
|
||||||
final deleteSmt = _db.localAssetEntity.delete();
|
return _db.transaction(() async {
|
||||||
deleteSmt.where((localAsset) {
|
await _db.managers.localAlbumAssetEntity
|
||||||
final subQuery = _db.localAlbumAssetEntity.selectOnly()
|
.filter((row) => row.albumId.id.equals(albumId))
|
||||||
..addColumns([_db.localAlbumAssetEntity.assetId])
|
.update((album) => album(marker_: const Value(true)));
|
||||||
..join([innerJoin(_db.localAlbumEntity, _db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id))]);
|
|
||||||
subQuery.where(
|
await _db.batch((batch) {
|
||||||
_db.localAlbumEntity.id.equals(albumId) & _db.localAlbumAssetEntity.assetId.isNotIn(assetIdsToKeep),
|
for (final assetId in assetIdsToKeep) {
|
||||||
);
|
batch.update(
|
||||||
return localAsset.id.isInQuery(subQuery);
|
_db.localAlbumAssetEntity,
|
||||||
|
const LocalAlbumAssetEntityCompanion(marker_: Value(null)),
|
||||||
|
where: (row) => row.assetId.equals(assetId) & row.albumId.equals(albumId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
final query = _db.localAssetEntity.delete()
|
||||||
|
..where(
|
||||||
|
(row) => row.id.isInQuery(
|
||||||
|
_db.localAlbumAssetEntity.selectOnly()
|
||||||
|
..addColumns([_db.localAlbumAssetEntity.assetId])
|
||||||
|
..where(
|
||||||
|
_db.localAlbumAssetEntity.albumId.equals(albumId) & _db.localAlbumAssetEntity.marker_.isNotNull(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await query.go();
|
||||||
});
|
});
|
||||||
await deleteSmt.go();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> upsert(
|
Future<void> upsert(
|
||||||
@@ -198,10 +214,9 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
|||||||
// List<String>
|
// List<String>
|
||||||
await _db.batch((batch) async {
|
await _db.batch((batch) async {
|
||||||
assetAlbums.cast<String, List<Object?>>().forEach((assetId, albumIds) {
|
assetAlbums.cast<String, List<Object?>>().forEach((assetId, albumIds) {
|
||||||
batch.deleteWhere(
|
for (final albumId in albumIds.cast<String?>().nonNulls) {
|
||||||
_db.localAlbumAssetEntity,
|
batch.deleteWhere(_db.localAlbumAssetEntity, (f) => f.albumId.equals(albumId) & f.assetId.equals(assetId));
|
||||||
(f) => f.albumId.isNotIn(albumIds.cast<String?>().nonNulls) & f.assetId.equals(assetId),
|
}
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
await _db.batch((batch) async {
|
await _db.batch((batch) async {
|
||||||
@@ -288,12 +303,14 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
|||||||
|
|
||||||
return transaction(() async {
|
return transaction(() async {
|
||||||
if (assetsToUnLink.isNotEmpty) {
|
if (assetsToUnLink.isNotEmpty) {
|
||||||
await _db.batch(
|
await _db.batch((batch) {
|
||||||
(batch) => batch.deleteWhere(
|
for (final assetId in assetsToUnLink) {
|
||||||
_db.localAlbumAssetEntity,
|
batch.deleteWhere(
|
||||||
(f) => f.assetId.isIn(assetsToUnLink) & f.albumId.equals(albumId),
|
_db.localAlbumAssetEntity,
|
||||||
),
|
(row) => row.assetId.equals(assetId) & row.albumId.equals(albumId),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await _deleteAssets(assetsToDelete);
|
await _deleteAssets(assetsToDelete);
|
||||||
@@ -320,7 +337,9 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return _db.batch((batch) {
|
return _db.batch((batch) {
|
||||||
batch.deleteWhere(_db.localAssetEntity, (f) => f.id.isIn(ids));
|
for (final id in ids) {
|
||||||
|
batch.deleteWhere(_db.localAssetEntity, (row) => row.id.equals(id));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
@@ -58,8 +57,8 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return _db.batch((batch) {
|
return _db.batch((batch) {
|
||||||
for (final slice in ids.slices(32000)) {
|
for (final id in ids) {
|
||||||
batch.deleteWhere(_db.localAssetEntity, (e) => e.id.isIn(slice));
|
batch.deleteWhere(_db.localAssetEntity, (e) => e.id.equals(id));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,8 +166,15 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> removeAssets(String albumId, List<String> assetIds) {
|
Future<void> removeAssets(String albumId, List<String> assetIds) {
|
||||||
return _db.remoteAlbumAssetEntity.deleteWhere((tbl) => tbl.albumId.equals(albumId) & tbl.assetId.isIn(assetIds));
|
return _db.batch((batch) {
|
||||||
|
for (final assetId in assetIds) {
|
||||||
|
batch.deleteWhere(
|
||||||
|
_db.remoteAlbumAssetEntity,
|
||||||
|
(row) => row.albumId.equals(albumId) & row.assetId.equals(assetId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
FutureOr<(DateTime, DateTime)> getDateRange(String albumId) {
|
FutureOr<(DateTime, DateTime)> getDateRange(String albumId) {
|
||||||
|
|||||||
@@ -160,7 +160,11 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> delete(List<String> ids) {
|
Future<void> delete(List<String> ids) {
|
||||||
return _db.remoteAssetEntity.deleteWhere((row) => row.id.isIn(ids));
|
return _db.batch((batch) {
|
||||||
|
for (final id in ids) {
|
||||||
|
batch.deleteWhere(_db.remoteAssetEntity, (row) => row.id.equals(id));
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateLocation(List<String> ids, LatLng location) {
|
Future<void> updateLocation(List<String> ids, LatLng location) {
|
||||||
@@ -199,7 +203,11 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
|
|||||||
.map((row) => row.id)
|
.map((row) => row.id)
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
await _db.stackEntity.deleteWhere((row) => row.id.isIn(stackIds));
|
await _db.batch((batch) {
|
||||||
|
for (final stackId in stackIds) {
|
||||||
|
batch.deleteWhere(_db.stackEntity, (row) => row.id.equals(stackId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
await _db.batch((batch) {
|
await _db.batch((batch) {
|
||||||
final companion = StackEntityCompanion(ownerId: Value(userId), primaryAssetId: Value(stack.primaryAssetId));
|
final companion = StackEntityCompanion(ownerId: Value(userId), primaryAssetId: Value(stack.primaryAssetId));
|
||||||
@@ -219,15 +227,21 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
|
|||||||
|
|
||||||
Future<void> unStack(List<String> stackIds) {
|
Future<void> unStack(List<String> stackIds) {
|
||||||
return _db.transaction(() async {
|
return _db.transaction(() async {
|
||||||
await _db.stackEntity.deleteWhere((row) => row.id.isIn(stackIds));
|
await _db.batch((batch) {
|
||||||
|
for (final stackId in stackIds) {
|
||||||
|
batch.deleteWhere(_db.stackEntity, (row) => row.id.equals(stackId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// TODO: delete this after adding foreign key on stackId
|
// TODO: delete this after adding foreign key on stackId
|
||||||
await _db.batch((batch) {
|
await _db.batch((batch) {
|
||||||
batch.update(
|
for (final stackId in stackIds) {
|
||||||
_db.remoteAssetEntity,
|
batch.update(
|
||||||
const RemoteAssetEntityCompanion(stackId: Value(null)),
|
_db.remoteAssetEntity,
|
||||||
where: (e) => e.stackId.isIn(stackIds),
|
const RemoteAssetEntityCompanion(stackId: Value(null)),
|
||||||
);
|
where: (e) => e.stackId.equals(stackId),
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class SearchApiRepository extends ApiRepository {
|
|||||||
personIds: filter.people.map((e) => e.id).toList(),
|
personIds: filter.people.map((e) => e.id).toList(),
|
||||||
type: type,
|
type: type,
|
||||||
page: page,
|
page: page,
|
||||||
size: 1000,
|
size: 100,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,7 +93,11 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
|
|
||||||
Future<void> deleteUsersV1(Iterable<SyncUserDeleteV1> data) async {
|
Future<void> deleteUsersV1(Iterable<SyncUserDeleteV1> data) async {
|
||||||
try {
|
try {
|
||||||
await _db.userEntity.deleteWhere((row) => row.id.isIn(data.map((e) => e.userId)));
|
await _db.batch((batch) {
|
||||||
|
for (final user in data) {
|
||||||
|
batch.deleteWhere(_db.userEntity, (row) => row.id.equals(user.userId));
|
||||||
|
}
|
||||||
|
});
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
_logger.severe('Error: SyncUserDeleteV1', error, stack);
|
_logger.severe('Error: SyncUserDeleteV1', error, stack);
|
||||||
rethrow;
|
rethrow;
|
||||||
@@ -158,7 +162,11 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
|
|
||||||
Future<void> deleteAssetsV1(Iterable<SyncAssetDeleteV1> data, {String debugLabel = 'user'}) async {
|
Future<void> deleteAssetsV1(Iterable<SyncAssetDeleteV1> data, {String debugLabel = 'user'}) async {
|
||||||
try {
|
try {
|
||||||
await _db.remoteAssetEntity.deleteWhere((row) => row.id.isIn(data.map((e) => e.assetId)));
|
await _db.batch((batch) {
|
||||||
|
for (final asset in data) {
|
||||||
|
batch.deleteWhere(_db.remoteAssetEntity, (row) => row.id.equals(asset.assetId));
|
||||||
|
}
|
||||||
|
});
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
_logger.severe('Error: deleteAssetsV1 - $debugLabel', error, stack);
|
_logger.severe('Error: deleteAssetsV1 - $debugLabel', error, stack);
|
||||||
rethrow;
|
rethrow;
|
||||||
@@ -243,7 +251,11 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
|
|
||||||
Future<void> deleteAlbumsV1(Iterable<SyncAlbumDeleteV1> data) async {
|
Future<void> deleteAlbumsV1(Iterable<SyncAlbumDeleteV1> data) async {
|
||||||
try {
|
try {
|
||||||
await _db.remoteAlbumEntity.deleteWhere((row) => row.id.isIn(data.map((e) => e.albumId)));
|
await _db.batch((batch) {
|
||||||
|
for (final album in data) {
|
||||||
|
batch.deleteWhere(_db.remoteAlbumEntity, (row) => row.id.equals(album.albumId));
|
||||||
|
}
|
||||||
|
});
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
_logger.severe('Error: deleteAlbumsV1', error, stack);
|
_logger.severe('Error: deleteAlbumsV1', error, stack);
|
||||||
rethrow;
|
rethrow;
|
||||||
@@ -379,7 +391,11 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
|
|
||||||
Future<void> deleteMemoriesV1(Iterable<SyncMemoryDeleteV1> data) async {
|
Future<void> deleteMemoriesV1(Iterable<SyncMemoryDeleteV1> data) async {
|
||||||
try {
|
try {
|
||||||
await _db.memoryEntity.deleteWhere((row) => row.id.isIn(data.map((e) => e.memoryId)));
|
await _db.batch((batch) {
|
||||||
|
for (final memory in data) {
|
||||||
|
batch.deleteWhere(_db.memoryEntity, (row) => row.id.equals(memory.memoryId));
|
||||||
|
}
|
||||||
|
});
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
_logger.severe('Error: deleteMemoriesV1', error, stack);
|
_logger.severe('Error: deleteMemoriesV1', error, stack);
|
||||||
rethrow;
|
rethrow;
|
||||||
@@ -443,7 +459,11 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
|
|
||||||
Future<void> deleteStacksV1(Iterable<SyncStackDeleteV1> data, {String debugLabel = 'user'}) async {
|
Future<void> deleteStacksV1(Iterable<SyncStackDeleteV1> data, {String debugLabel = 'user'}) async {
|
||||||
try {
|
try {
|
||||||
await _db.stackEntity.deleteWhere((row) => row.id.isIn(data.map((e) => e.stackId)));
|
await _db.batch((batch) {
|
||||||
|
for (final stack in data) {
|
||||||
|
batch.deleteWhere(_db.stackEntity, (row) => row.id.equals(stack.stackId));
|
||||||
|
}
|
||||||
|
});
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
_logger.severe('Error: deleteStacksV1 - $debugLabel', error, stack);
|
_logger.severe('Error: deleteStacksV1 - $debugLabel', error, stack);
|
||||||
rethrow;
|
rethrow;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import 'package:immich_mobile/presentation/widgets/backup/backup_toggle_button.w
|
|||||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/sync_status.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/widgets/backup/backup_info_card.dart';
|
import 'package:immich_mobile/widgets/backup/backup_info_card.dart';
|
||||||
@@ -33,7 +34,14 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id);
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
|
await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id);
|
||||||
|
await ref.read(backgroundSyncProvider).syncRemote();
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -44,7 +52,6 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
|
|||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
final backupNotifier = ref.read(driftBackupProvider.notifier);
|
final backupNotifier = ref.read(driftBackupProvider.notifier);
|
||||||
final backgroundManager = ref.read(backgroundSyncProvider);
|
|
||||||
|
|
||||||
Future<void> startBackup() async {
|
Future<void> startBackup() async {
|
||||||
final currentUser = Store.tryGet(StoreKey.currentUser);
|
final currentUser = Store.tryGet(StoreKey.currentUser);
|
||||||
@@ -52,7 +59,6 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await backgroundManager.syncRemote();
|
|
||||||
await backupNotifier.getBackupStatus(currentUser.id);
|
await backupNotifier.getBackupStatus(currentUser.id);
|
||||||
await backupNotifier.startBackup(currentUser.id);
|
await backupNotifier.startBackup(currentUser.id);
|
||||||
}
|
}
|
||||||
@@ -235,11 +241,13 @@ class _BackupCard extends ConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final backupCount = ref.watch(driftBackupProvider.select((p) => p.backupCount));
|
final backupCount = ref.watch(driftBackupProvider.select((p) => p.backupCount));
|
||||||
|
final syncStatus = ref.watch(syncStatusProvider);
|
||||||
|
|
||||||
return BackupInfoCard(
|
return BackupInfoCard(
|
||||||
title: "backup_controller_page_backup".tr(),
|
title: "backup_controller_page_backup".tr(),
|
||||||
subtitle: "backup_controller_page_backup_sub".tr(),
|
subtitle: "backup_controller_page_backup_sub".tr(),
|
||||||
info: backupCount.toString(),
|
info: backupCount.toString(),
|
||||||
|
isLoading: syncStatus.isRemoteSyncing,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -250,10 +258,13 @@ class _RemainderCard extends ConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final remainderCount = ref.watch(driftBackupProvider.select((p) => p.remainderCount));
|
final remainderCount = ref.watch(driftBackupProvider.select((p) => p.remainderCount));
|
||||||
|
final syncStatus = ref.watch(syncStatusProvider);
|
||||||
|
|
||||||
return BackupInfoCard(
|
return BackupInfoCard(
|
||||||
title: "backup_controller_page_remainder".tr(),
|
title: "backup_controller_page_remainder".tr(),
|
||||||
subtitle: "backup_controller_page_remainder_sub".tr(),
|
subtitle: "backup_controller_page_remainder_sub".tr(),
|
||||||
info: remainderCount.toString(),
|
info: remainderCount.toString(),
|
||||||
|
isLoading: syncStatus.isRemoteSyncing,
|
||||||
onTap: () => context.pushRoute(const DriftBackupAssetDetailRoute()),
|
onTap: () => context.pushRoute(const DriftBackupAssetDetailRoute()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
57
mobile/lib/presentation/pages/download_info.page.dart
Normal file
57
mobile/lib/presentation/pages/download_info.page.dart
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/pages/common/download_panel.dart';
|
||||||
|
import 'package:immich_mobile/providers/asset_viewer/download.provider.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class DownloadInfoPage extends ConsumerWidget {
|
||||||
|
const DownloadInfoPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final tasks = ref.watch(downloadStateProvider.select((state) => state.taskProgress)).entries.toList();
|
||||||
|
|
||||||
|
onCancelDownload(String id) {
|
||||||
|
ref.watch(downloadStateProvider.notifier).cancelDownload(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text("download".t(context: context)),
|
||||||
|
actions: [],
|
||||||
|
),
|
||||||
|
body: ListView.builder(
|
||||||
|
physics: const ClampingScrollPhysics(),
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: tasks.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final task = tasks[index];
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
|
||||||
|
child: DownloadTaskTile(
|
||||||
|
progress: task.value.progress,
|
||||||
|
fileName: task.value.fileName,
|
||||||
|
status: task.value.status,
|
||||||
|
onCancelDownload: () => onCancelDownload(task.key),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
persistentFooterButtons: [
|
||||||
|
OutlinedButton(
|
||||||
|
onPressed: () {
|
||||||
|
tasks.map((e) => e.key).forEach(onCancelDownload);
|
||||||
|
},
|
||||||
|
style: OutlinedButton.styleFrom(side: BorderSide(color: context.colorScheme.primary)),
|
||||||
|
child: Text(
|
||||||
|
'clear_all'.t(context: context),
|
||||||
|
style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.primary),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -633,7 +633,7 @@ class _SearchResultGrid extends ConsumerWidget {
|
|||||||
groupBy: GroupAssetsBy.none,
|
groupBy: GroupAssetsBy.none,
|
||||||
appBar: null,
|
appBar: null,
|
||||||
bottomSheet: const GeneralBottomSheet(minChildSize: 0.20),
|
bottomSheet: const GeneralBottomSheet(minChildSize: 0.20),
|
||||||
withScrubber: false,
|
snapToMonth: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,54 +1,45 @@
|
|||||||
import 'package:fluttertoast/fluttertoast.dart';
|
|
||||||
import 'package:immich_mobile/constants/enums.dart';
|
import 'package:immich_mobile/constants/enums.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/utils/background_sync.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
|
||||||
|
|
||||||
class DownloadActionButton extends ConsumerWidget {
|
class DownloadActionButton extends ConsumerWidget {
|
||||||
final ActionSource source;
|
final ActionSource source;
|
||||||
|
final bool menuItem;
|
||||||
|
const DownloadActionButton({super.key, required this.source, this.menuItem = false});
|
||||||
|
|
||||||
const DownloadActionButton({super.key, required this.source});
|
void _onTap(BuildContext context, WidgetRef ref, BackgroundSyncManager backgroundSyncManager) async {
|
||||||
|
|
||||||
void _onTap(BuildContext context, WidgetRef ref) async {
|
|
||||||
if (!context.mounted) {
|
if (!context.mounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final result = await ref.read(actionProvider.notifier).downloadAll(source);
|
try {
|
||||||
ref.read(multiSelectProvider.notifier).reset();
|
await ref.read(actionProvider.notifier).downloadAll(source);
|
||||||
|
|
||||||
if (!context.mounted) {
|
Future.delayed(const Duration(seconds: 1), () async {
|
||||||
return;
|
await backgroundSyncManager.syncLocal();
|
||||||
}
|
await backgroundSyncManager.hashAssets();
|
||||||
|
});
|
||||||
if (!result.success) {
|
} finally {
|
||||||
ImmichToast.show(
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
context: context,
|
|
||||||
msg: 'scaffold_body_error_occurred'.t(context: context),
|
|
||||||
gravity: ToastGravity.BOTTOM,
|
|
||||||
toastType: ToastType.error,
|
|
||||||
);
|
|
||||||
} else if (result.count > 0) {
|
|
||||||
ImmichToast.show(
|
|
||||||
context: context,
|
|
||||||
msg: 'download_action_prompt'.t(context: context, args: {'count': result.count.toString()}),
|
|
||||||
gravity: ToastGravity.BOTTOM,
|
|
||||||
toastType: ToastType.success,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final backgroundManager = ref.watch(backgroundSyncProvider);
|
||||||
|
|
||||||
return BaseActionButton(
|
return BaseActionButton(
|
||||||
iconData: Icons.download,
|
iconData: Icons.download,
|
||||||
maxWidth: 95,
|
maxWidth: 95,
|
||||||
label: "download".t(context: context),
|
label: "download".t(context: context),
|
||||||
onPressed: () => _onTap(context, ref),
|
menuItem: menuItem,
|
||||||
|
onPressed: () => _onTap(context, ref, backgroundManager),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/providers/asset_viewer/download.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
|
||||||
|
class DownloadStatusFloatingButton extends ConsumerWidget {
|
||||||
|
const DownloadStatusFloatingButton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final shouldShow = ref.watch(downloadStateProvider.select((state) => state.showProgress));
|
||||||
|
final itemCount = ref.watch(downloadStateProvider.select((state) => state.taskProgress.length));
|
||||||
|
final isDownloading = ref
|
||||||
|
.watch(downloadStateProvider.select((state) => state.taskProgress))
|
||||||
|
.values
|
||||||
|
.where((element) => element.progress != 1)
|
||||||
|
.isNotEmpty;
|
||||||
|
|
||||||
|
return shouldShow
|
||||||
|
? Badge.count(
|
||||||
|
count: itemCount,
|
||||||
|
textColor: context.colorScheme.onPrimary,
|
||||||
|
backgroundColor: context.colorScheme.primary,
|
||||||
|
child: FloatingActionButton(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||||
|
side: BorderSide(color: context.colorScheme.outlineVariant, width: 1),
|
||||||
|
),
|
||||||
|
backgroundColor: context.isDarkTheme
|
||||||
|
? context.colorScheme.surfaceContainer
|
||||||
|
: context.colorScheme.surfaceBright,
|
||||||
|
elevation: 2,
|
||||||
|
onPressed: () {
|
||||||
|
context.pushRoute(const DownloadInfoRoute());
|
||||||
|
},
|
||||||
|
child: Stack(
|
||||||
|
alignment: AlignmentDirectional.center,
|
||||||
|
children: [
|
||||||
|
isDownloading
|
||||||
|
? Icon(Icons.downloading_rounded, color: context.colorScheme.primary, size: 28)
|
||||||
|
: Icon(
|
||||||
|
Icons.download_done,
|
||||||
|
color: context.isDarkTheme ? Colors.green[200] : Colors.green[400],
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
if (isDownloading)
|
||||||
|
const SizedBox(
|
||||||
|
height: 31,
|
||||||
|
width: 31,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
value: null, // Indeterminate progress
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import 'package:immich_mobile/domain/utils/event_stream.dart';
|
|||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/scroll_extensions.dart';
|
import 'package:immich_mobile/extensions/scroll_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/download_status_floating_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.provider.dart';
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.provider.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
||||||
@@ -649,20 +650,25 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
appBar: const ViewerTopAppBar(),
|
appBar: const ViewerTopAppBar(),
|
||||||
extendBody: true,
|
extendBody: true,
|
||||||
extendBodyBehindAppBar: true,
|
extendBodyBehindAppBar: true,
|
||||||
body: PhotoViewGallery.builder(
|
floatingActionButton: const DownloadStatusFloatingButton(),
|
||||||
gaplessPlayback: true,
|
body: Stack(
|
||||||
loadingBuilder: _placeholderBuilder,
|
children: [
|
||||||
pageController: pageController,
|
PhotoViewGallery.builder(
|
||||||
scrollPhysics: CurrentPlatform.isIOS
|
gaplessPlayback: true,
|
||||||
? const FastScrollPhysics() // Use bouncing physics for iOS
|
loadingBuilder: _placeholderBuilder,
|
||||||
: const FastClampingScrollPhysics(), // Use heavy physics for Android
|
pageController: pageController,
|
||||||
itemCount: totalAssets,
|
scrollPhysics: CurrentPlatform.isIOS
|
||||||
onPageChanged: _onPageChanged,
|
? const FastScrollPhysics() // Use bouncing physics for iOS
|
||||||
onPageBuild: _onPageBuild,
|
: const FastClampingScrollPhysics(), // Use heavy physics for Android
|
||||||
scaleStateChangedCallback: _onScaleStateChanged,
|
itemCount: totalAssets,
|
||||||
builder: _assetBuilder,
|
onPageChanged: _onPageChanged,
|
||||||
backgroundDecoration: BoxDecoration(color: backgroundColor),
|
onPageBuild: _onPageBuild,
|
||||||
enablePanAlways: true,
|
scaleStateChangedCallback: _onScaleStateChanged,
|
||||||
|
builder: _assetBuilder,
|
||||||
|
backgroundDecoration: BoxDecoration(color: backgroundColor),
|
||||||
|
enablePanAlways: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
bottomNavigationBar: showingBottomSheet
|
bottomNavigationBar: showingBottomSheet
|
||||||
? const SizedBox.shrink()
|
? const SizedBox.shrink()
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:immich_mobile/domain/utils/event_stream.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/action_buttons/cast_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/cast_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart';
|
||||||
@@ -56,6 +57,7 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||||||
final isCasting = ref.watch(castProvider.select((c) => c.isCasting));
|
final isCasting = ref.watch(castProvider.select((c) => c.isCasting));
|
||||||
|
|
||||||
final actions = <Widget>[
|
final actions = <Widget>[
|
||||||
|
if (asset.hasRemote) const DownloadActionButton(source: ActionSource.viewer, menuItem: true),
|
||||||
if (isCasting || (asset.hasRemote)) const CastActionButton(menuItem: true),
|
if (isCasting || (asset.hasRemote)) const CastActionButton(menuItem: true),
|
||||||
if (album != null && album.isActivityEnabled && album.isShared)
|
if (album != null && album.isActivityEnabled && album.isShared)
|
||||||
IconButton(
|
IconButton(
|
||||||
|
|||||||
@@ -16,8 +16,9 @@ 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});
|
LocalThumbProvider({required this.id, required this.assetType, this.size = kThumbnailResolution, this.exact = true});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<LocalThumbProvider> obtainKey(ImageConfiguration configuration) {
|
Future<LocalThumbProvider> obtainKey(ImageConfiguration configuration) {
|
||||||
@@ -37,7 +38,12 @@ class LocalThumbProvider extends CancellableImageProvider<LocalThumbProvider>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Stream<ImageInfo> _codec(LocalThumbProvider key, ImageDecoderCallback decode) {
|
Stream<ImageInfo> _codec(LocalThumbProvider key, ImageDecoderCallback decode) {
|
||||||
final request = this.request = LocalImageRequest(localId: key.id, size: key.size, assetType: key.assetType);
|
final devicePixelRatio = PlatformDispatcher.instance.views.first.devicePixelRatio;
|
||||||
|
final request = this.request = LocalImageRequest(
|
||||||
|
localId: key.id,
|
||||||
|
size: key.size * devicePixelRatio,
|
||||||
|
assetType: key.assetType,
|
||||||
|
);
|
||||||
return loadRequest(request, decode);
|
return loadRequest(request, decode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +51,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;
|
return id == other.id && (!exact || size == other.size);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -60,7 +66,12 @@ class LocalFullImageProvider extends CancellableImageProvider<LocalFullImageProv
|
|||||||
final Size size;
|
final Size size;
|
||||||
final AssetType assetType;
|
final AssetType assetType;
|
||||||
|
|
||||||
LocalFullImageProvider({required this.id, required this.assetType, required this.size});
|
LocalFullImageProvider({
|
||||||
|
required this.id,
|
||||||
|
required this.assetType,
|
||||||
|
required this.size,
|
||||||
|
LocalThumbProvider? initialProvider,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<LocalFullImageProvider> obtainKey(ImageConfiguration configuration) {
|
Future<LocalFullImageProvider> obtainKey(ImageConfiguration configuration) {
|
||||||
@@ -71,7 +82,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: key.id, assetType: key.assetType)),
|
initialImage: getInitialImage(LocalThumbProvider(id: id, assetType: assetType, exact: false)),
|
||||||
informationCollector: () => <DiagnosticsNode>[
|
informationCollector: () => <DiagnosticsNode>[
|
||||||
DiagnosticsProperty<ImageProvider>('Image provider', this),
|
DiagnosticsProperty<ImageProvider>('Image provider', this),
|
||||||
DiagnosticsProperty<String>('Id', key.id),
|
DiagnosticsProperty<String>('Id', key.id),
|
||||||
|
|||||||
@@ -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(320); // TODO: make the resolution vary based on actual tile size
|
const Size kThumbnailResolution = Size.square(128);
|
||||||
const double kTimelineSpacing = 2.0;
|
const double kTimelineSpacing = 2.0;
|
||||||
const int kTimelineColumnCount = 3;
|
const int kTimelineColumnCount = 3;
|
||||||
|
|
||||||
|
|||||||
@@ -101,7 +101,6 @@ class _FixedSegmentRow extends ConsumerWidget {
|
|||||||
if (isScrubbing) {
|
if (isScrubbing) {
|
||||||
return _buildPlaceholder(context);
|
return _buildPlaceholder(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timelineService.hasRange(assetIndex, assetCount)) {
|
if (timelineService.hasRange(assetIndex, assetCount)) {
|
||||||
return _buildAssetRow(context, timelineService.getAssets(assetIndex, assetCount), timelineService);
|
return _buildAssetRow(context, timelineService.getAssets(assetIndex, assetCount), timelineService);
|
||||||
}
|
}
|
||||||
@@ -122,6 +121,7 @@ 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,6 +135,7 @@ 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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -145,8 +146,9 @@ 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});
|
const _AssetTileWidget({super.key, required this.asset, required this.assetIndex, required this.size});
|
||||||
|
|
||||||
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);
|
||||||
@@ -204,6 +206,7 @@ class _AssetTileWidget extends ConsumerWidget {
|
|||||||
lockSelection: lockSelection,
|
lockSelection: lockSelection,
|
||||||
showStorageIndicator: showStorageIndicator,
|
showStorageIndicator: showStorageIndicator,
|
||||||
heroOffset: heroOffset,
|
heroOffset: heroOffset,
|
||||||
|
size: size,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
|
|||||||
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
|
||||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||||
|
import 'package:immich_mobile/utils/debounce.dart';
|
||||||
import 'package:intl/intl.dart' hide TextDirection;
|
import 'package:intl/intl.dart' hide TextDirection;
|
||||||
|
|
||||||
/// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged
|
/// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged
|
||||||
@@ -30,6 +31,11 @@ class Scrubber extends ConsumerStatefulWidget {
|
|||||||
|
|
||||||
final double? monthSegmentSnappingOffset;
|
final double? monthSegmentSnappingOffset;
|
||||||
|
|
||||||
|
final bool snapToMonth;
|
||||||
|
|
||||||
|
/// Whether an app bar is present, affects coordinate calculations
|
||||||
|
final bool hasAppBar;
|
||||||
|
|
||||||
Scrubber({
|
Scrubber({
|
||||||
super.key,
|
super.key,
|
||||||
Key? scrollThumbKey,
|
Key? scrollThumbKey,
|
||||||
@@ -38,6 +44,8 @@ class Scrubber extends ConsumerStatefulWidget {
|
|||||||
this.topPadding = 0,
|
this.topPadding = 0,
|
||||||
this.bottomPadding = 0,
|
this.bottomPadding = 0,
|
||||||
this.monthSegmentSnappingOffset,
|
this.monthSegmentSnappingOffset,
|
||||||
|
this.snapToMonth = true,
|
||||||
|
this.hasAppBar = true,
|
||||||
required this.child,
|
required this.child,
|
||||||
}) : assert(child.scrollDirection == Axis.vertical);
|
}) : assert(child.scrollDirection == Axis.vertical);
|
||||||
|
|
||||||
@@ -81,6 +89,8 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
|
|||||||
bool _isDragging = false;
|
bool _isDragging = false;
|
||||||
List<_Segment> _segments = [];
|
List<_Segment> _segments = [];
|
||||||
int _monthCount = 0;
|
int _monthCount = 0;
|
||||||
|
DateTime? _currentScrubberDate;
|
||||||
|
Debouncer? _scrubberDebouncer;
|
||||||
|
|
||||||
late AnimationController _thumbAnimationController;
|
late AnimationController _thumbAnimationController;
|
||||||
Timer? _fadeOutTimer;
|
Timer? _fadeOutTimer;
|
||||||
@@ -133,6 +143,7 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
|
|||||||
_thumbAnimationController.dispose();
|
_thumbAnimationController.dispose();
|
||||||
_labelAnimationController.dispose();
|
_labelAnimationController.dispose();
|
||||||
_fadeOutTimer?.cancel();
|
_fadeOutTimer?.cancel();
|
||||||
|
_scrubberDebouncer?.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,11 +187,25 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onDragStart(DragStartDetails _) {
|
void _onScrubberDateChanged(DateTime date) {
|
||||||
if (_monthCount >= kMinMonthsToEnableScrubberSnap) {
|
if (_currentScrubberDate != date) {
|
||||||
|
// Date changed, immediately set scrubbing to true
|
||||||
|
_currentScrubberDate = date;
|
||||||
ref.read(timelineStateProvider.notifier).setScrubbing(true);
|
ref.read(timelineStateProvider.notifier).setScrubbing(true);
|
||||||
}
|
|
||||||
|
|
||||||
|
// Initialize debouncer if needed
|
||||||
|
_scrubberDebouncer ??= Debouncer(interval: const Duration(milliseconds: 50));
|
||||||
|
|
||||||
|
// Debounce setting scrubbing to false
|
||||||
|
_scrubberDebouncer!.run(() {
|
||||||
|
if (_currentScrubberDate == date) {
|
||||||
|
ref.read(timelineStateProvider.notifier).setScrubbing(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onDragStart(DragStartDetails _) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isDragging = true;
|
_isDragging = true;
|
||||||
_labelAnimationController.forward();
|
_labelAnimationController.forward();
|
||||||
@@ -206,10 +231,15 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
|
|||||||
if (_lastLabel != label) {
|
if (_lastLabel != label) {
|
||||||
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
||||||
_lastLabel = label;
|
_lastLabel = label;
|
||||||
|
|
||||||
|
// Notify timeline state of the new scrubber date position
|
||||||
|
if (_monthCount >= kMinMonthsToEnableScrubberSnap) {
|
||||||
|
_onScrubberDateChanged(nearestMonthSegment.date);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_monthCount < kMinMonthsToEnableScrubberSnap) {
|
if (_monthCount < kMinMonthsToEnableScrubberSnap || !widget.snapToMonth) {
|
||||||
// If there are less than kMinMonthsToEnableScrubberSnap months, we don't need to snap to segments
|
// If there are less than kMinMonthsToEnableScrubberSnap months, we don't need to snap to segments
|
||||||
setState(() {
|
setState(() {
|
||||||
_thumbTopOffset = dragPosition;
|
_thumbTopOffset = dragPosition;
|
||||||
@@ -236,14 +266,28 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
|
|||||||
/// - If user drags to global Y position that's 100 pixels from the top
|
/// - If user drags to global Y position that's 100 pixels from the top
|
||||||
/// - The relative position would be 100 - 50 = 50 (50 pixels into the scrubber area)
|
/// - The relative position would be 100 - 50 = 50 (50 pixels into the scrubber area)
|
||||||
double _calculateDragPosition(DragUpdateDetails details) {
|
double _calculateDragPosition(DragUpdateDetails details) {
|
||||||
|
if (widget.hasAppBar) {
|
||||||
|
final dragAreaTop = widget.topPadding;
|
||||||
|
final dragAreaBottom = widget.timelineHeight - widget.bottomPadding;
|
||||||
|
final dragAreaHeight = dragAreaBottom - dragAreaTop;
|
||||||
|
|
||||||
|
final relativePosition = details.globalPosition.dy - dragAreaTop;
|
||||||
|
|
||||||
|
// Make sure the position stays within the scrubber's bounds
|
||||||
|
return relativePosition.clamp(0.0, dragAreaHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the local position relative to the gesture detector
|
||||||
|
final RenderBox? renderBox = context.findRenderObject() as RenderBox?;
|
||||||
|
if (renderBox != null) {
|
||||||
|
final localPosition = renderBox.globalToLocal(details.globalPosition);
|
||||||
|
return localPosition.dy.clamp(0.0, _scrubberHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to current logic if render box is not available
|
||||||
final dragAreaTop = widget.topPadding;
|
final dragAreaTop = widget.topPadding;
|
||||||
final dragAreaBottom = widget.timelineHeight - widget.bottomPadding;
|
|
||||||
final dragAreaHeight = dragAreaBottom - dragAreaTop;
|
|
||||||
|
|
||||||
final relativePosition = details.globalPosition.dy - dragAreaTop;
|
final relativePosition = details.globalPosition.dy - dragAreaTop;
|
||||||
|
return relativePosition.clamp(0.0, _scrubberHeight);
|
||||||
// Make sure the position stays within the scrubber's bounds
|
|
||||||
return relativePosition.clamp(0.0, dragAreaHeight);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find the segment closest to the given position
|
/// Find the segment closest to the given position
|
||||||
@@ -294,12 +338,18 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onDragEnd(DragEndDetails _) {
|
void _onDragEnd(DragEndDetails _) {
|
||||||
ref.read(timelineStateProvider.notifier).setScrubbing(false);
|
|
||||||
_labelAnimationController.reverse();
|
_labelAnimationController.reverse();
|
||||||
setState(() {
|
setState(() {
|
||||||
_isDragging = false;
|
_isDragging = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ref.read(timelineStateProvider.notifier).setScrubbing(false);
|
||||||
|
|
||||||
|
// Reset scrubber tracking when drag ends
|
||||||
|
_currentScrubberDate = null;
|
||||||
|
_scrubberDebouncer?.dispose();
|
||||||
|
_scrubberDebouncer = null;
|
||||||
|
|
||||||
_resetThumbTimer();
|
_resetThumbTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,8 +72,6 @@ class TimelineState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class TimelineStateNotifier extends Notifier<TimelineState> {
|
class TimelineStateNotifier extends Notifier<TimelineState> {
|
||||||
TimelineStateNotifier();
|
|
||||||
|
|
||||||
void setScrubbing(bool isScrubbing) {
|
void setScrubbing(bool isScrubbing) {
|
||||||
state = state.copyWith(isScrubbing: isScrubbing);
|
state = state.copyWith(isScrubbing: isScrubbing);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import 'package:immich_mobile/domain/models/timeline.model.dart';
|
|||||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
import 'package:immich_mobile/domain/utils/event_stream.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/presentation/widgets/action_buttons/download_status_floating_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/scrubber.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/scrubber.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
|
||||||
@@ -38,6 +39,7 @@ class Timeline extends StatelessWidget {
|
|||||||
this.bottomSheet = const GeneralBottomSheet(minChildSize: 0.18),
|
this.bottomSheet = const GeneralBottomSheet(minChildSize: 0.18),
|
||||||
this.groupBy,
|
this.groupBy,
|
||||||
this.withScrubber = true,
|
this.withScrubber = true,
|
||||||
|
this.snapToMonth = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Widget? topSliverWidget;
|
final Widget? topSliverWidget;
|
||||||
@@ -48,11 +50,13 @@ class Timeline extends StatelessWidget {
|
|||||||
final bool withStack;
|
final bool withStack;
|
||||||
final GroupAssetsBy? groupBy;
|
final GroupAssetsBy? groupBy;
|
||||||
final bool withScrubber;
|
final bool withScrubber;
|
||||||
|
final bool snapToMonth;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
resizeToAvoidBottomInset: false,
|
resizeToAvoidBottomInset: false,
|
||||||
|
floatingActionButton: const DownloadStatusFloatingButton(),
|
||||||
body: LayoutBuilder(
|
body: LayoutBuilder(
|
||||||
builder: (_, constraints) => ProviderScope(
|
builder: (_, constraints) => ProviderScope(
|
||||||
overrides: [
|
overrides: [
|
||||||
@@ -73,6 +77,7 @@ class Timeline extends StatelessWidget {
|
|||||||
appBar: appBar,
|
appBar: appBar,
|
||||||
bottomSheet: bottomSheet,
|
bottomSheet: bottomSheet,
|
||||||
withScrubber: withScrubber,
|
withScrubber: withScrubber,
|
||||||
|
snapToMonth: snapToMonth,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -87,6 +92,7 @@ class _SliverTimeline extends ConsumerStatefulWidget {
|
|||||||
this.appBar,
|
this.appBar,
|
||||||
this.bottomSheet,
|
this.bottomSheet,
|
||||||
this.withScrubber = true,
|
this.withScrubber = true,
|
||||||
|
this.snapToMonth = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Widget? topSliverWidget;
|
final Widget? topSliverWidget;
|
||||||
@@ -94,6 +100,7 @@ class _SliverTimeline extends ConsumerStatefulWidget {
|
|||||||
final Widget? appBar;
|
final Widget? appBar;
|
||||||
final Widget? bottomSheet;
|
final Widget? bottomSheet;
|
||||||
final bool withScrubber;
|
final bool withScrubber;
|
||||||
|
final bool snapToMonth;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState createState() => _SliverTimelineState();
|
ConsumerState createState() => _SliverTimelineState();
|
||||||
@@ -309,11 +316,13 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
final Widget timeline;
|
final Widget timeline;
|
||||||
if (widget.withScrubber) {
|
if (widget.withScrubber) {
|
||||||
timeline = Scrubber(
|
timeline = Scrubber(
|
||||||
|
snapToMonth: widget.snapToMonth,
|
||||||
layoutSegments: segments,
|
layoutSegments: segments,
|
||||||
timelineHeight: maxHeight,
|
timelineHeight: maxHeight,
|
||||||
topPadding: topPadding,
|
topPadding: topPadding,
|
||||||
bottomPadding: bottomPadding,
|
bottomPadding: bottomPadding,
|
||||||
monthSegmentSnappingOffset: widget.topSliverWidgetHeight ?? 0 + appBarExpandedHeight,
|
monthSegmentSnappingOffset: widget.topSliverWidgetHeight ?? 0 + appBarExpandedHeight,
|
||||||
|
hasAppBar: widget.appBar != null,
|
||||||
child: grid,
|
child: grid,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -235,7 +235,9 @@ class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
|
|||||||
switch (update.status) {
|
switch (update.status) {
|
||||||
case TaskStatus.complete:
|
case TaskStatus.complete:
|
||||||
if (update.task.group == kBackupGroup) {
|
if (update.task.group == kBackupGroup) {
|
||||||
state = state.copyWith(backupCount: state.backupCount + 1, remainderCount: state.remainderCount - 1);
|
if (update.responseStatusCode == 201) {
|
||||||
|
state = state.copyWith(backupCount: state.backupCount + 1, remainderCount: state.remainderCount - 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the completed task from the upload items
|
// Remove the completed task from the upload items
|
||||||
|
|||||||
@@ -356,7 +356,6 @@ class ActionNotifier extends Notifier<void> {
|
|||||||
|
|
||||||
Future<ActionResult> downloadAll(ActionSource source) async {
|
Future<ActionResult> downloadAll(ActionSource source) async {
|
||||||
final assets = _getAssets(source).whereType<RemoteAsset>().toList(growable: false);
|
final assets = _getAssets(source).whereType<RemoteAsset>().toList(growable: false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final didEnqueue = await _service.downloadAll(assets);
|
final didEnqueue = await _service.downloadAll(assets);
|
||||||
final enqueueCount = didEnqueue.where((e) => e).length;
|
final enqueueCount = didEnqueue.where((e) => e).length;
|
||||||
|
|||||||
@@ -90,7 +90,11 @@ class DownloadRepository {
|
|||||||
final isVideo = asset.isVideo;
|
final isVideo = asset.isVideo;
|
||||||
final url = getOriginalUrlForRemoteId(id);
|
final url = getOriginalUrlForRemoteId(id);
|
||||||
|
|
||||||
if (Platform.isAndroid || livePhotoVideoId == null || isVideo) {
|
// on iOS it cannot link the image, check if the filename has .MP extension
|
||||||
|
// to avoid downloading the video part
|
||||||
|
final isAndroidMotionPhoto = asset.name.contains(".MP");
|
||||||
|
|
||||||
|
if (Platform.isAndroid || livePhotoVideoId == null || isVideo || isAndroidMotionPhoto) {
|
||||||
tasks[taskIndex++] = DownloadTask(
|
tasks[taskIndex++] = DownloadTask(
|
||||||
taskId: id,
|
taskId: id,
|
||||||
url: url,
|
url: url,
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ import 'package:immich_mobile/pages/share_intent/share_intent.page.dart';
|
|||||||
import 'package:immich_mobile/presentation/pages/dev/feat_in_development.page.dart';
|
import 'package:immich_mobile/presentation/pages/dev/feat_in_development.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/dev/main_timeline.page.dart';
|
import 'package:immich_mobile/presentation/pages/dev/main_timeline.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/dev/media_stat.page.dart';
|
import 'package:immich_mobile/presentation/pages/dev/media_stat.page.dart';
|
||||||
|
import 'package:immich_mobile/presentation/pages/download_info.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/drift_activities.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_activities.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/drift_album.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_album.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/drift_album_options.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_album_options.page.dart';
|
||||||
@@ -345,6 +346,7 @@ class AppRouter extends RootStackRouter {
|
|||||||
AutoRoute(page: DriftActivitiesRoute.page, guards: [_authGuard, _duplicateGuard]),
|
AutoRoute(page: DriftActivitiesRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||||
AutoRoute(page: DriftBackupAssetDetailRoute.page, guards: [_authGuard, _duplicateGuard]),
|
AutoRoute(page: DriftBackupAssetDetailRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||||
AutoRoute(page: AssetTroubleshootRoute.page, guards: [_authGuard, _duplicateGuard]),
|
AutoRoute(page: AssetTroubleshootRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||||
|
AutoRoute(page: DownloadInfoRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||||
// required to handle all deeplinks in deep_link.service.dart
|
// required to handle all deeplinks in deep_link.service.dart
|
||||||
// auto_route_library#1722
|
// auto_route_library#1722
|
||||||
RedirectRoute(path: '*', redirectTo: '/'),
|
RedirectRoute(path: '*', redirectTo: '/'),
|
||||||
|
|||||||
@@ -688,6 +688,22 @@ class CropImageRouteArgs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [DownloadInfoPage]
|
||||||
|
class DownloadInfoRoute extends PageRouteInfo<void> {
|
||||||
|
const DownloadInfoRoute({List<PageRouteInfo>? children})
|
||||||
|
: super(DownloadInfoRoute.name, initialChildren: children);
|
||||||
|
|
||||||
|
static const String name = 'DownloadInfoRoute';
|
||||||
|
|
||||||
|
static PageInfo page = PageInfo(
|
||||||
|
name,
|
||||||
|
builder: (data) {
|
||||||
|
return const DownloadInfoPage();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [DriftActivitiesPage]
|
/// [DriftActivitiesPage]
|
||||||
class DriftActivitiesRoute extends PageRouteInfo<void> {
|
class DriftActivitiesRoute extends PageRouteInfo<void> {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:immich_mobile/repositories/download.repository.dart';
|
|
||||||
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:immich_mobile/constants/enums.dart';
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
||||||
@@ -11,6 +10,7 @@ import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||||
import 'package:immich_mobile/repositories/asset_api.repository.dart';
|
import 'package:immich_mobile/repositories/asset_api.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
||||||
|
import 'package:immich_mobile/repositories/download.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/widgets/common/date_time_picker.dart';
|
import 'package:immich_mobile/widgets/common/date_time_picker.dart';
|
||||||
@@ -199,14 +199,11 @@ class ActionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<int> removeFromAlbum(List<String> remoteIds, String albumId) async {
|
Future<int> removeFromAlbum(List<String> remoteIds, String albumId) async {
|
||||||
int removedCount = 0;
|
|
||||||
final result = await _albumApiRepository.removeAssets(albumId, remoteIds);
|
final result = await _albumApiRepository.removeAssets(albumId, remoteIds);
|
||||||
|
|
||||||
if (result.removed.isNotEmpty) {
|
if (result.removed.isNotEmpty) {
|
||||||
removedCount = await _remoteAlbumRepository.removeAssets(albumId, result.removed);
|
await _remoteAlbumRepository.removeAssets(albumId, result.removed);
|
||||||
}
|
}
|
||||||
|
return result.removed.length;
|
||||||
return removedCount;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> updateDescription(String assetId, String description) async {
|
Future<bool> updateDescription(String assetId, String description) async {
|
||||||
|
|||||||
@@ -8,8 +8,17 @@ class BackupInfoCard extends StatelessWidget {
|
|||||||
final String title;
|
final String title;
|
||||||
final String subtitle;
|
final String subtitle;
|
||||||
final String info;
|
final String info;
|
||||||
|
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
const BackupInfoCard({super.key, required this.title, required this.subtitle, required this.info, this.onTap});
|
final bool isLoading;
|
||||||
|
const BackupInfoCard({
|
||||||
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
required this.subtitle,
|
||||||
|
required this.info,
|
||||||
|
this.onTap,
|
||||||
|
this.isLoading = false,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -38,8 +47,36 @@ class BackupInfoCard extends StatelessWidget {
|
|||||||
trailing: Column(
|
trailing: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(info, style: context.textTheme.titleLarge),
|
Stack(
|
||||||
Text("backup_info_card_assets", style: context.textTheme.labelLarge).tr(),
|
children: [
|
||||||
|
Text(
|
||||||
|
info,
|
||||||
|
style: context.textTheme.titleLarge?.copyWith(
|
||||||
|
color: context.colorScheme.onSurface.withAlpha(isLoading ? 50 : 255),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isLoading)
|
||||||
|
Positioned.fill(
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: context.colorScheme.onSurface.withAlpha(150),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"backup_info_card_assets",
|
||||||
|
style: context.textTheme.labelLarge?.copyWith(
|
||||||
|
color: context.colorScheme.onSurface.withAlpha(isLoading ? 50 : 255),
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
5
mobile/test/drift/main/generated/schema.dart
generated
5
mobile/test/drift/main/generated/schema.dart
generated
@@ -13,6 +13,7 @@ import 'schema_v7.dart' as v7;
|
|||||||
import 'schema_v8.dart' as v8;
|
import 'schema_v8.dart' as v8;
|
||||||
import 'schema_v9.dart' as v9;
|
import 'schema_v9.dart' as v9;
|
||||||
import 'schema_v10.dart' as v10;
|
import 'schema_v10.dart' as v10;
|
||||||
|
import 'schema_v11.dart' as v11;
|
||||||
|
|
||||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||||
@override
|
@override
|
||||||
@@ -38,10 +39,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
|||||||
return v9.DatabaseAtV9(db);
|
return v9.DatabaseAtV9(db);
|
||||||
case 10:
|
case 10:
|
||||||
return v10.DatabaseAtV10(db);
|
return v10.DatabaseAtV10(db);
|
||||||
|
case 11:
|
||||||
|
return v11.DatabaseAtV11(db);
|
||||||
default:
|
default:
|
||||||
throw MissingSchemaException(version, versions);
|
throw MissingSchemaException(version, versions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
|
||||||
}
|
}
|
||||||
|
|||||||
7198
mobile/test/drift/main/generated/schema_v11.dart
generated
Normal file
7198
mobile/test/drift/main/generated/schema_v11.dart
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -57,28 +57,28 @@ export class MediaRepository {
|
|||||||
const buffer = await exiftool.extractBinaryTagToBuffer('JpgFromRaw2', input);
|
const buffer = await exiftool.extractBinaryTagToBuffer('JpgFromRaw2', input);
|
||||||
return { buffer, format: RawExtractedFormat.Jpeg };
|
return { buffer, format: RawExtractedFormat.Jpeg };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.logger.debug('Could not extract JpgFromRaw2 buffer from image, trying JPEG from RAW next', error.message);
|
this.logger.debug(`Could not extract JpgFromRaw2 buffer from image, trying JPEG from RAW next: ${error}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const buffer = await exiftool.extractBinaryTagToBuffer('JpgFromRaw', input);
|
const buffer = await exiftool.extractBinaryTagToBuffer('JpgFromRaw', input);
|
||||||
return { buffer, format: RawExtractedFormat.Jpeg };
|
return { buffer, format: RawExtractedFormat.Jpeg };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.logger.debug('Could not extract JPEG buffer from image, trying PreviewJXL next', error.message);
|
this.logger.debug(`Could not extract JPEG buffer from image, trying PreviewJXL next: ${error}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const buffer = await exiftool.extractBinaryTagToBuffer('PreviewJXL', input);
|
const buffer = await exiftool.extractBinaryTagToBuffer('PreviewJXL', input);
|
||||||
return { buffer, format: RawExtractedFormat.Jxl };
|
return { buffer, format: RawExtractedFormat.Jxl };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.logger.debug('Could not extract PreviewJXL buffer from image, trying PreviewImage next', error.message);
|
this.logger.debug(`Could not extract PreviewJXL buffer from image, trying PreviewImage next: ${error}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const buffer = await exiftool.extractBinaryTagToBuffer('PreviewImage', input);
|
const buffer = await exiftool.extractBinaryTagToBuffer('PreviewImage', input);
|
||||||
return { buffer, format: RawExtractedFormat.Jpeg };
|
return { buffer, format: RawExtractedFormat.Jpeg };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.logger.debug('Could not extract preview buffer from image', error.message);
|
this.logger.debug(`Could not extract preview buffer from image: ${error}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ export class MetadataRepository {
|
|||||||
|
|
||||||
readTags(path: string): Promise<ImmichTags> {
|
readTags(path: string): Promise<ImmichTags> {
|
||||||
return this.exiftool.read(path).catch((error) => {
|
return this.exiftool.read(path).catch((error) => {
|
||||||
this.logger.warn(`Error reading exif data (${path}): ${error}`, error?.stack);
|
this.logger.warn(`Error reading exif data (${path}): ${error}\n${error?.stack}`);
|
||||||
return {};
|
return {};
|
||||||
}) as Promise<ImmichTags>;
|
}) as Promise<ImmichTags>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -344,7 +344,7 @@ export class AuthService extends BaseService {
|
|||||||
await this.jobRepository.queue({ name: JobName.FileDelete, data: { files: [oldPath] } });
|
await this.jobRepository.queue({ name: JobName.FileDelete, data: { files: [oldPath] } });
|
||||||
}
|
}
|
||||||
} catch (error: Error | any) {
|
} catch (error: Error | any) {
|
||||||
this.logger.warn(`Unable to sync oauth profile picture: ${error}`, error?.stack);
|
this.logger.warn(`Unable to sync oauth profile picture: ${error}\n${error?.stack}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -132,12 +132,12 @@ export class BackupService extends BaseService {
|
|||||||
gzip.stdout.pipe(fileStream);
|
gzip.stdout.pipe(fileStream);
|
||||||
|
|
||||||
pgdump.on('error', (err) => {
|
pgdump.on('error', (err) => {
|
||||||
this.logger.error('Backup failed with error', err);
|
this.logger.error(`Backup failed with error: ${err}`);
|
||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
gzip.on('error', (err) => {
|
gzip.on('error', (err) => {
|
||||||
this.logger.error('Gzip failed with error', err);
|
this.logger.error(`Gzip failed with error: ${err}`);
|
||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -175,10 +175,10 @@ export class BackupService extends BaseService {
|
|||||||
});
|
});
|
||||||
await this.storageRepository.rename(backupFilePath, backupFilePath.replace('.tmp', ''));
|
await this.storageRepository.rename(backupFilePath, backupFilePath.replace('.tmp', ''));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('Database Backup Failure', error);
|
this.logger.error(`Database Backup Failure: ${error}`);
|
||||||
await this.storageRepository
|
await this.storageRepository
|
||||||
.unlink(backupFilePath)
|
.unlink(backupFilePath)
|
||||||
.catch((error) => this.logger.error('Failed to delete failed backup file', error));
|
.catch((error) => this.logger.error(`Failed to delete failed backup file: ${error}`));
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ export class LibraryService extends BaseService {
|
|||||||
job.paths.map((path) =>
|
job.paths.map((path) =>
|
||||||
this.processEntity(path, library.ownerId, job.libraryId)
|
this.processEntity(path, library.ownerId, job.libraryId)
|
||||||
.then((asset) => assetImports.push(asset))
|
.then((asset) => assetImports.push(asset))
|
||||||
.catch((error: any) => this.logger.error(`Error processing ${path} for library ${job.libraryId}`, error)),
|
.catch((error: any) => this.logger.error(`Error processing ${path} for library ${job.libraryId}: ${error}`)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export class MemoryService extends BaseService {
|
|||||||
try {
|
try {
|
||||||
await Promise.all(users.map((owner, i) => this.createOnThisDayMemories(owner.id, usersIds[i], target)));
|
await Promise.all(users.map((owner, i) => this.createOnThisDayMemories(owner.id, usersIds[i], target)));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`Failed to create memories for ${target.toISO()}`, error);
|
this.logger.error(`Failed to create memories for ${target.toISO()}: ${error}`);
|
||||||
}
|
}
|
||||||
// update system metadata even when there is an error to minimize the chance of duplicates
|
// update system metadata even when there is an error to minimize the chance of duplicates
|
||||||
await this.systemMetadataRepository.set(SystemMetadataKey.MemoriesState, {
|
await this.systemMetadataRepository.set(SystemMetadataKey.MemoriesState, {
|
||||||
|
|||||||
@@ -338,7 +338,7 @@ export class StorageTemplateService extends BaseService {
|
|||||||
|
|
||||||
return destination;
|
return destination;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.logger.error(`Unable to get template path for ${filename}`, error);
|
this.logger.error(`Unable to get template path for ${filename}: ${error}`);
|
||||||
return asset.originalPath;
|
return asset.originalPath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export class VersionService extends BaseService {
|
|||||||
this.eventRepository.clientBroadcast('on_new_release', asNotification(metadata));
|
this.eventRepository.clientBroadcast('on_new_release', asNotification(metadata));
|
||||||
}
|
}
|
||||||
} catch (error: Error | any) {
|
} catch (error: Error | any) {
|
||||||
this.logger.warn(`Unable to run version check: ${error}`, error?.stack);
|
this.logger.warn(`Unable to run version check: ${error}\n${error?.stack}`);
|
||||||
return JobStatus.Failed;
|
return JobStatus.Failed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export const sendFile = async (
|
|||||||
|
|
||||||
// log non-http errors
|
// log non-http errors
|
||||||
if (error instanceof HttpException === false) {
|
if (error instanceof HttpException === false) {
|
||||||
logger.error(`Unable to send file: ${error.name}`, error.stack);
|
logger.error(`Unable to send file: ${error}`, error.stack);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.header('Cache-Control', 'none');
|
res.header('Cache-Control', 'none');
|
||||||
|
|||||||
@@ -240,7 +240,7 @@
|
|||||||
use:zoomImageAction
|
use:zoomImageAction
|
||||||
use:swipe={() => ({})}
|
use:swipe={() => ({})}
|
||||||
onswipe={onSwipe}
|
onswipe={onSwipe}
|
||||||
class="h-full w-full"
|
class="h-full w-full flex"
|
||||||
transition:fade={{ duration: haveFadeTransition ? assetViewerFadeDuration : 0 }}
|
transition:fade={{ duration: haveFadeTransition ? assetViewerFadeDuration : 0 }}
|
||||||
>
|
>
|
||||||
{#if $slideshowState !== SlideshowState.None && $slideshowLook === SlideshowLook.BlurredBackground}
|
{#if $slideshowState !== SlideshowState.None && $slideshowLook === SlideshowLook.BlurredBackground}
|
||||||
@@ -255,7 +255,7 @@
|
|||||||
bind:this={$photoViewerImgElement}
|
bind:this={$photoViewerImgElement}
|
||||||
src={assetFileUrl}
|
src={assetFileUrl}
|
||||||
alt={$getAltText(toTimelineAsset(asset))}
|
alt={$getAltText(toTimelineAsset(asset))}
|
||||||
class="h-full w-full {$slideshowState === SlideshowState.None
|
class="max-h-full max-w-full h-auto w-auto mx-auto my-auto {$slideshowState === SlideshowState.None
|
||||||
? 'object-contain'
|
? 'object-contain'
|
||||||
: slideshowLookCssMapping[$slideshowLook]}"
|
: slideshowLookCssMapping[$slideshowLook]}"
|
||||||
draggable="false"
|
draggable="false"
|
||||||
|
|||||||
Reference in New Issue
Block a user