mirror of
https://github.com/immich-app/immich.git
synced 2026-07-08 05:17:54 -07:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d5b23aa4e | |||
| 3aebb82905 | |||
| 7a9b9e01be | |||
| 0cbe07c4d4 | |||
| 45ea248e8b | |||
| 8cbf0db3b3 | |||
| a225c962de | |||
| daabab8c1c | |||
| a4f28dafb2 | |||
| 704b287bba | |||
| c7b4a798cb | |||
| f8e78792b8 | |||
| 0ebb723b39 | |||
| 99a8354145 | |||
| 7ee2f3644f | |||
| d2a46ade78 |
@@ -78,10 +78,10 @@ jobs:
|
|||||||
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
|
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0
|
||||||
if: ${{ !github.event.pull_request.head.repo.fork }}
|
if: ${{ !github.event.pull_request.head.repo.fork }}
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
@@ -96,7 +96,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate docker image tags
|
- name: Generate docker image tags
|
||||||
id: metadata
|
id: metadata
|
||||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
|
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||||
with:
|
with:
|
||||||
flavor: |
|
flavor: |
|
||||||
latest=false
|
latest=false
|
||||||
@@ -107,7 +107,7 @@ jobs:
|
|||||||
type=raw,value=latest,enable=${{ github.event_name == 'release' && !github.event.release.prerelease }}
|
type=raw,value=latest,enable=${{ github.event_name == 'release' && !github.event.release.prerelease }}
|
||||||
|
|
||||||
- name: Build and push image
|
- name: Build and push image
|
||||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
|
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||||
with:
|
with:
|
||||||
file: packages/cli/Dockerfile
|
file: packages/cli/Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
@@ -70,7 +70,7 @@ jobs:
|
|||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
uses: github/codeql-action/autobuild@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||||
@@ -83,6 +83,6 @@ jobs:
|
|||||||
# ./location_of_script_within_repo/buildscript.sh
|
# ./location_of_script_within_repo/buildscript.sh
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
|
||||||
with:
|
with:
|
||||||
category: '/language:${{matrix.language}}'
|
category: '/language:${{matrix.language}}'
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ jobs:
|
|||||||
suffix: ['', '-cuda', '-rocm', '-openvino', '-armnn', '-rknn']
|
suffix: ['', '-cuda', '-rocm', '-openvino', '-armnn', '-rknn']
|
||||||
steps:
|
steps:
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@@ -90,7 +90,7 @@ jobs:
|
|||||||
suffix: ['']
|
suffix: ['']
|
||||||
steps:
|
steps:
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ This environment includes the services below. Additional details are available i
|
|||||||
- Redis
|
- Redis
|
||||||
- PostgreSQL development database with exposed port `5432` so you can use any database client to access it
|
- PostgreSQL development database with exposed port `5432` so you can use any database client to access it
|
||||||
|
|
||||||
All the services are packaged to run as with single Docker Compose command.
|
All the services are packaged to run with a single Docker Compose command.
|
||||||
|
|
||||||
:::tip mise
|
:::tip mise
|
||||||
[mise](https://mise.jdx.dev) is used throughout the project to manage tool versions and run tasks. [Install mise](https://mise.jdx.dev/installing-mise.html), then from the repo root run `mise trust` and `mise install` to get all required tools. Tasks for each service can be run from the repo root using `mise //namespace:task` (e.g. `mise //server:lint`). To list all available tasks, run `mise tasks ls --all`.
|
[mise](https://mise.jdx.dev) is used throughout the project to manage tool versions and run tasks. [Install mise](https://mise.jdx.dev/installing-mise.html), then from the repo root run `mise trust` and `mise install` to get all required tools. Tasks for each service can be run from the repo root using `mise //namespace:task` (e.g. `mise //server:lint`). To list all available tasks, run `mise tasks ls --all`.
|
||||||
@@ -41,7 +41,7 @@ All the services are packaged to run as with single Docker Compose command.
|
|||||||
1. Clone the project repo.
|
1. Clone the project repo.
|
||||||
2. Run `cp docker/example.env docker/.env`.
|
2. Run `cp docker/example.env docker/.env`.
|
||||||
3. Edit `docker/.env` to provide values for the required variable `UPLOAD_LOCATION`.
|
3. Edit `docker/.env` to provide values for the required variable `UPLOAD_LOCATION`.
|
||||||
4. Install dependencies - `pnpm i`
|
4. Install dependencies - `mise x -- pnpm i`
|
||||||
5. From the root directory, run:
|
5. From the root directory, run:
|
||||||
|
|
||||||
```bash title="Start development server"
|
```bash title="Start development server"
|
||||||
@@ -52,7 +52,7 @@ mise dev
|
|||||||
|
|
||||||
All the services will be started with hot-reloading enabled for a quick feedback loop.
|
All the services will be started with hot-reloading enabled for a quick feedback loop.
|
||||||
|
|
||||||
You can access the web from `http://your-machine-ip:3000` or `http://localhost:3000` and access the server from the mobile app at `http://your-machine-ip:3000/api`
|
You can access the web from `http://your-machine-ip:3000` or `http://localhost:3000` and access the server from the mobile app at `http://your-machine-ip:3000`
|
||||||
|
|
||||||
**Notes:**
|
**Notes:**
|
||||||
|
|
||||||
|
|||||||
+3626
File diff suppressed because it is too large
Load Diff
@@ -70,7 +70,8 @@
|
|||||||
debugDocumentVersioning = "YES"
|
debugDocumentVersioning = "YES"
|
||||||
debugServiceExtension = "internal"
|
debugServiceExtension = "internal"
|
||||||
enableGPUValidationMode = "1"
|
enableGPUValidationMode = "1"
|
||||||
allowLocationSimulation = "YES">
|
allowLocationSimulation = "YES"
|
||||||
|
queueDebuggingEnabled = "NO">
|
||||||
<BuildableProductRunnable
|
<BuildableProductRunnable
|
||||||
runnableDebuggingMode = "0">
|
runnableDebuggingMode = "0">
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
|
|||||||
@@ -1,19 +1,65 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
enum AspectRatioPreset {
|
class CropAspectRatio {
|
||||||
free(ratio: null, label: 'Free', icon: Icons.crop_free_rounded),
|
final int? numerator;
|
||||||
square(ratio: 1.0, label: '1:1', icon: Icons.crop_square_rounded),
|
final int? denominator;
|
||||||
ratio16x9(ratio: 16 / 9, label: '16:9', icon: Icons.crop_16_9_rounded),
|
|
||||||
ratio3x2(ratio: 3 / 2, label: '3:2', icon: Icons.crop_3_2_rounded),
|
|
||||||
ratio7x5(ratio: 7 / 5, label: '7:5', icon: Icons.crop_7_5_rounded),
|
|
||||||
ratio9x16(ratio: 9 / 16, label: '9:16', icon: Icons.crop_16_9_rounded, iconRotated: true),
|
|
||||||
ratio2x3(ratio: 2 / 3, label: '2:3', icon: Icons.crop_3_2_rounded, iconRotated: true),
|
|
||||||
ratio5x7(ratio: 5 / 7, label: '5:7', icon: Icons.crop_7_5_rounded, iconRotated: true);
|
|
||||||
|
|
||||||
final double? ratio;
|
final String? customLabel;
|
||||||
final String label;
|
final IconData? icon;
|
||||||
final IconData icon;
|
|
||||||
final bool iconRotated;
|
|
||||||
|
|
||||||
const AspectRatioPreset({required this.ratio, required this.label, required this.icon, this.iconRotated = false});
|
const CropAspectRatio({this.numerator, this.denominator, this.customLabel, this.icon});
|
||||||
|
|
||||||
|
static const free = CropAspectRatio(customLabel: "Free", icon: Icons.crop_free);
|
||||||
|
static const original = CropAspectRatio(customLabel: "Original", icon: Icons.crop_original);
|
||||||
|
|
||||||
|
String get label {
|
||||||
|
return customLabel ?? (numerator != null && denominator != null ? '$numerator:$denominator' : 'Free');
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get hasFlippedVariant => numerator != denominator;
|
||||||
|
double? get ratio => (numerator != null && denominator != null) ? numerator! / denominator! : null;
|
||||||
|
|
||||||
|
CropAspectRatio get flipped {
|
||||||
|
return CropAspectRatio(numerator: denominator, denominator: numerator, customLabel: customLabel, icon: icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return other is CropAspectRatio &&
|
||||||
|
other.numerator == numerator &&
|
||||||
|
other.denominator == denominator &&
|
||||||
|
other.customLabel == customLabel &&
|
||||||
|
other.icon == icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
return numerator.hashCode ^ denominator.hashCode ^ customLabel.hashCode ^ icon.hashCode;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const aspectRatioFree = CropAspectRatio(customLabel: "Free", icon: Icons.crop_free);
|
||||||
|
const aspectRatioOriginal = CropAspectRatio(customLabel: "Original", icon: Icons.crop_original);
|
||||||
|
|
||||||
|
final aspectRatioPresets = [
|
||||||
|
CropAspectRatio.free,
|
||||||
|
CropAspectRatio.original,
|
||||||
|
|
||||||
|
const CropAspectRatio(numerator: 1, denominator: 1),
|
||||||
|
|
||||||
|
// lanscape
|
||||||
|
const CropAspectRatio(numerator: 16, denominator: 9),
|
||||||
|
const CropAspectRatio(numerator: 3, denominator: 2),
|
||||||
|
const CropAspectRatio(numerator: 7, denominator: 5),
|
||||||
|
const CropAspectRatio(numerator: 4, denominator: 3),
|
||||||
|
|
||||||
|
// portrait
|
||||||
|
const CropAspectRatio(numerator: 16, denominator: 9).flipped,
|
||||||
|
const CropAspectRatio(numerator: 3, denominator: 2).flipped,
|
||||||
|
const CropAspectRatio(numerator: 7, denominator: 5).flipped,
|
||||||
|
const CropAspectRatio(numerator: 4, denominator: 3).flipped,
|
||||||
|
];
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ sealed class BaseAsset {
|
|||||||
final int? height;
|
final int? height;
|
||||||
final int? durationMs;
|
final int? durationMs;
|
||||||
final bool isFavorite;
|
final bool isFavorite;
|
||||||
final String? livePhotoVideoId;
|
|
||||||
final bool isEdited;
|
final bool isEdited;
|
||||||
|
|
||||||
const BaseAsset({
|
const BaseAsset({
|
||||||
@@ -40,32 +39,15 @@ sealed class BaseAsset {
|
|||||||
this.height,
|
this.height,
|
||||||
this.durationMs,
|
this.durationMs,
|
||||||
this.isFavorite = false,
|
this.isFavorite = false,
|
||||||
this.livePhotoVideoId,
|
|
||||||
required this.isEdited,
|
required this.isEdited,
|
||||||
});
|
});
|
||||||
|
|
||||||
bool get isImage => type == AssetType.image;
|
bool get isImage => type == AssetType.image;
|
||||||
bool get isVideo => type == AssetType.video;
|
bool get isVideo => type == AssetType.video;
|
||||||
|
|
||||||
bool get isMotionPhoto => livePhotoVideoId != null;
|
bool get isMotionPhoto => playbackStyle == AssetPlaybackStyle.livePhoto;
|
||||||
bool get isAnimatedImage => playbackStyle == AssetPlaybackStyle.imageAnimated;
|
bool get isAnimatedImage => playbackStyle == AssetPlaybackStyle.imageAnimated;
|
||||||
|
|
||||||
AssetPlaybackStyle get playbackStyle {
|
|
||||||
if (isVideo) {
|
|
||||||
return AssetPlaybackStyle.video;
|
|
||||||
}
|
|
||||||
if (isMotionPhoto) {
|
|
||||||
return AssetPlaybackStyle.livePhoto;
|
|
||||||
}
|
|
||||||
if (isImage && durationMs != null && durationMs! > 0) {
|
|
||||||
return AssetPlaybackStyle.imageAnimated;
|
|
||||||
}
|
|
||||||
if (isImage) {
|
|
||||||
return AssetPlaybackStyle.image;
|
|
||||||
}
|
|
||||||
return AssetPlaybackStyle.unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
Duration get duration {
|
Duration get duration {
|
||||||
final durationMs = this.durationMs;
|
final durationMs = this.durationMs;
|
||||||
if (durationMs != null) {
|
if (durationMs != null) {
|
||||||
@@ -86,6 +68,7 @@ sealed class BaseAsset {
|
|||||||
String? get localId;
|
String? get localId;
|
||||||
String? get remoteId;
|
String? get remoteId;
|
||||||
String get heroTag;
|
String get heroTag;
|
||||||
|
AssetPlaybackStyle get playbackStyle;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ class LocalAsset extends BaseAsset {
|
|||||||
super.height,
|
super.height,
|
||||||
super.durationMs,
|
super.durationMs,
|
||||||
super.isFavorite = false,
|
super.isFavorite = false,
|
||||||
super.livePhotoVideoId,
|
|
||||||
this.orientation = 0,
|
this.orientation = 0,
|
||||||
required this.playbackStyle,
|
required this.playbackStyle,
|
||||||
this.adjustmentTime,
|
this.adjustmentTime,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ class RemoteAsset extends BaseAsset {
|
|||||||
final AssetVisibility visibility;
|
final AssetVisibility visibility;
|
||||||
final String ownerId;
|
final String ownerId;
|
||||||
final String? stackId;
|
final String? stackId;
|
||||||
|
final String? livePhotoVideoId;
|
||||||
final DateTime? uploadedAt;
|
final DateTime? uploadedAt;
|
||||||
final DateTime? deletedAt;
|
final DateTime? deletedAt;
|
||||||
|
|
||||||
@@ -29,7 +30,7 @@ class RemoteAsset extends BaseAsset {
|
|||||||
super.isFavorite = false,
|
super.isFavorite = false,
|
||||||
this.thumbHash,
|
this.thumbHash,
|
||||||
this.visibility = AssetVisibility.timeline,
|
this.visibility = AssetVisibility.timeline,
|
||||||
super.livePhotoVideoId,
|
this.livePhotoVideoId,
|
||||||
this.stackId,
|
this.stackId,
|
||||||
required super.isEdited,
|
required super.isEdited,
|
||||||
this.deletedAt,
|
this.deletedAt,
|
||||||
@@ -38,6 +39,23 @@ class RemoteAsset extends BaseAsset {
|
|||||||
@override
|
@override
|
||||||
String? get localId => localAssetId;
|
String? get localId => localAssetId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
AssetPlaybackStyle get playbackStyle {
|
||||||
|
if (isVideo) {
|
||||||
|
return AssetPlaybackStyle.video;
|
||||||
|
}
|
||||||
|
if (livePhotoVideoId != null) {
|
||||||
|
return AssetPlaybackStyle.livePhoto;
|
||||||
|
}
|
||||||
|
if (isImage && durationMs != null && durationMs! > 0) {
|
||||||
|
return AssetPlaybackStyle.imageAnimated;
|
||||||
|
}
|
||||||
|
if (isImage) {
|
||||||
|
return AssetPlaybackStyle.image;
|
||||||
|
}
|
||||||
|
return AssetPlaybackStyle.unknown;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? get remoteId => id;
|
String? get remoteId => id;
|
||||||
|
|
||||||
@@ -90,6 +108,7 @@ class RemoteAsset extends BaseAsset {
|
|||||||
thumbHash == other.thumbHash &&
|
thumbHash == other.thumbHash &&
|
||||||
visibility == other.visibility &&
|
visibility == other.visibility &&
|
||||||
stackId == other.stackId &&
|
stackId == other.stackId &&
|
||||||
|
livePhotoVideoId == other.livePhotoVideoId &&
|
||||||
uploadedAt == other.uploadedAt &&
|
uploadedAt == other.uploadedAt &&
|
||||||
deletedAt == other.deletedAt;
|
deletedAt == other.deletedAt;
|
||||||
}
|
}
|
||||||
@@ -103,6 +122,7 @@ class RemoteAsset extends BaseAsset {
|
|||||||
thumbHash.hashCode ^
|
thumbHash.hashCode ^
|
||||||
visibility.hashCode ^
|
visibility.hashCode ^
|
||||||
stackId.hashCode ^
|
stackId.hashCode ^
|
||||||
|
livePhotoVideoId.hashCode ^
|
||||||
uploadedAt.hashCode ^
|
uploadedAt.hashCode ^
|
||||||
deletedAt.hashCode;
|
deletedAt.hashCode;
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ class AssetService {
|
|||||||
return _localRepository.getByChecksum(checksum);
|
return _localRepository.getByChecksum(checksum);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<LocalAsset?> getLocalAsset(String id) {
|
||||||
|
return _localRepository.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
Future<RemoteAsset?> getRemoteAssetByChecksum(String checksum) {
|
Future<RemoteAsset?> getRemoteAssetByChecksum(String checksum) {
|
||||||
return _remoteRepository.getByChecksum(checksum);
|
return _remoteRepository.getByChecksum(checksum);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ WHERE (library_id IS NOT NULL);
|
|||||||
CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created
|
CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created
|
||||||
ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)
|
ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)
|
||||||
''')
|
''')
|
||||||
|
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_remote_asset_uploaded ON remote_asset_entity (uploaded_at)')
|
||||||
class RemoteAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
|
class RemoteAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
|
||||||
const RemoteAssetEntity();
|
const RemoteAssetEntity();
|
||||||
|
|
||||||
|
|||||||
@@ -1779,3 +1779,7 @@ i0.Index get idxRemoteAssetOwnerVisibilityDeletedCreated => i0.Index(
|
|||||||
'idx_remote_asset_owner_visibility_deleted_created',
|
'idx_remote_asset_owner_visibility_deleted_created',
|
||||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)',
|
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)',
|
||||||
);
|
);
|
||||||
|
i0.Index get idxRemoteAssetUploaded => i0.Index(
|
||||||
|
'idx_remote_asset_uploaded',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_asset_uploaded ON remote_asset_entity (uploaded_at)',
|
||||||
|
);
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ class Drift extends $Drift {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 30;
|
int get schemaVersion => 31;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration => MigrationStrategy(
|
MigrationStrategy get migration => MigrationStrategy(
|
||||||
@@ -128,199 +128,209 @@ class Drift extends $Drift {
|
|||||||
// Run migration steps without foreign keys and re-enable them later
|
// Run migration steps without foreign keys and re-enable them later
|
||||||
await customStatement('PRAGMA foreign_keys = OFF');
|
await customStatement('PRAGMA foreign_keys = OFF');
|
||||||
|
|
||||||
await m.runMigrationSteps(
|
try {
|
||||||
from: from,
|
await transaction(
|
||||||
to: to,
|
() => m.runMigrationSteps(
|
||||||
steps: migrationSteps(
|
from: from,
|
||||||
from1To2: (m, v2) async {
|
to: to,
|
||||||
for (final entity in v2.entities) {
|
steps: migrationSteps(
|
||||||
await m.drop(entity);
|
from1To2: (m, v2) async {
|
||||||
await m.create(entity);
|
for (final entity in v2.entities) {
|
||||||
}
|
await m.drop(entity);
|
||||||
},
|
await m.create(entity);
|
||||||
from2To3: (m, v3) async {
|
}
|
||||||
// Removed foreign key constraint on stack.primaryAssetId
|
},
|
||||||
await m.alterTable(TableMigration(v3.stackEntity));
|
from2To3: (m, v3) async {
|
||||||
},
|
// Removed foreign key constraint on stack.primaryAssetId
|
||||||
from3To4: (m, v4) async {
|
await m.alterTable(TableMigration(v3.stackEntity));
|
||||||
// Thumbnail path column got removed from person_entity
|
},
|
||||||
await m.alterTable(TableMigration(v4.personEntity));
|
from3To4: (m, v4) async {
|
||||||
// asset_face_entity is added
|
// Thumbnail path column got removed from person_entity
|
||||||
await m.create(v4.assetFaceEntity);
|
await m.alterTable(TableMigration(v4.personEntity));
|
||||||
},
|
// asset_face_entity is added
|
||||||
from4To5: (m, v5) async {
|
await m.create(v4.assetFaceEntity);
|
||||||
await m.alterTable(
|
},
|
||||||
TableMigration(
|
from4To5: (m, v5) async {
|
||||||
v5.userEntity,
|
await m.alterTable(
|
||||||
newColumns: [v5.userEntity.hasProfileImage, v5.userEntity.profileChangedAt],
|
TableMigration(
|
||||||
columnTransformer: {v5.userEntity.profileChangedAt: currentDateAndTime},
|
v5.userEntity,
|
||||||
),
|
newColumns: [v5.userEntity.hasProfileImage, v5.userEntity.profileChangedAt],
|
||||||
);
|
columnTransformer: {v5.userEntity.profileChangedAt: currentDateAndTime},
|
||||||
},
|
),
|
||||||
from5To6: (m, v6) async {
|
);
|
||||||
// Drops the (checksum, ownerId) and adds it back as (ownerId, checksum)
|
},
|
||||||
await customStatement('DROP INDEX IF EXISTS UQ_remote_asset_owner_checksum');
|
from5To6: (m, v6) async {
|
||||||
await m.drop(v6.idxRemoteAssetOwnerChecksum);
|
// Drops the (checksum, ownerId) and adds it back as (ownerId, checksum)
|
||||||
await m.create(v6.idxRemoteAssetOwnerChecksum);
|
await customStatement('DROP INDEX IF EXISTS UQ_remote_asset_owner_checksum');
|
||||||
// Adds libraryId to remote_asset_entity
|
await m.drop(v6.idxRemoteAssetOwnerChecksum);
|
||||||
await m.addColumn(v6.remoteAssetEntity, v6.remoteAssetEntity.libraryId);
|
await m.create(v6.idxRemoteAssetOwnerChecksum);
|
||||||
await m.drop(v6.uQRemoteAssetsOwnerChecksum);
|
// Adds libraryId to remote_asset_entity
|
||||||
await m.create(v6.uQRemoteAssetsOwnerChecksum);
|
await m.addColumn(v6.remoteAssetEntity, v6.remoteAssetEntity.libraryId);
|
||||||
await m.drop(v6.uQRemoteAssetsOwnerLibraryChecksum);
|
await m.drop(v6.uQRemoteAssetsOwnerChecksum);
|
||||||
await m.create(v6.uQRemoteAssetsOwnerLibraryChecksum);
|
await m.create(v6.uQRemoteAssetsOwnerChecksum);
|
||||||
},
|
await m.drop(v6.uQRemoteAssetsOwnerLibraryChecksum);
|
||||||
from6To7: (m, v7) async {
|
await m.create(v6.uQRemoteAssetsOwnerLibraryChecksum);
|
||||||
await m.createIndex(v7.idxLatLng);
|
},
|
||||||
},
|
from6To7: (m, v7) async {
|
||||||
from7To8: (m, v8) async {
|
await m.createIndex(v7.idxLatLng);
|
||||||
await m.create(v8.storeEntity);
|
},
|
||||||
},
|
from7To8: (m, v8) async {
|
||||||
from8To9: (m, v9) async {
|
await m.create(v8.storeEntity);
|
||||||
await m.addColumn(v9.localAlbumEntity, v9.localAlbumEntity.linkedRemoteAlbumId);
|
},
|
||||||
},
|
from8To9: (m, v9) async {
|
||||||
from9To10: (m, v10) async {
|
await m.addColumn(v9.localAlbumEntity, v9.localAlbumEntity.linkedRemoteAlbumId);
|
||||||
await m.createTable(v10.authUserEntity);
|
},
|
||||||
await m.addColumn(v10.userEntity, v10.userEntity.avatarColor);
|
from9To10: (m, v10) async {
|
||||||
await m.alterTable(TableMigration(v10.userEntity));
|
await m.createTable(v10.authUserEntity);
|
||||||
},
|
await m.addColumn(v10.userEntity, v10.userEntity.avatarColor);
|
||||||
from10To11: (m, v11) async {
|
await m.alterTable(TableMigration(v10.userEntity));
|
||||||
await m.addColumn(v11.localAlbumAssetEntity, v11.localAlbumAssetEntity.marker_);
|
},
|
||||||
},
|
from10To11: (m, v11) async {
|
||||||
from11To12: (m, v12) async {
|
await m.addColumn(v11.localAlbumAssetEntity, v11.localAlbumAssetEntity.marker_);
|
||||||
final localToUTCMapping = {
|
},
|
||||||
v12.localAssetEntity: [v12.localAssetEntity.createdAt, v12.localAssetEntity.updatedAt],
|
from11To12: (m, v12) async {
|
||||||
v12.localAlbumEntity: [v12.localAlbumEntity.updatedAt],
|
final localToUTCMapping = {
|
||||||
};
|
v12.localAssetEntity: [v12.localAssetEntity.createdAt, v12.localAssetEntity.updatedAt],
|
||||||
|
v12.localAlbumEntity: [v12.localAlbumEntity.updatedAt],
|
||||||
|
};
|
||||||
|
|
||||||
for (final entry in localToUTCMapping.entries) {
|
for (final entry in localToUTCMapping.entries) {
|
||||||
final table = entry.key;
|
final table = entry.key;
|
||||||
await m.alterTable(
|
await m.alterTable(
|
||||||
TableMigration(
|
TableMigration(
|
||||||
table,
|
table,
|
||||||
columnTransformer: {
|
columnTransformer: {
|
||||||
for (final column in entry.value)
|
for (final column in entry.value)
|
||||||
column: column.modify(const DateTimeModifier.utc()).strftime('%Y-%m-%dT%H:%M:%fZ'),
|
column: column.modify(const DateTimeModifier.utc()).strftime('%Y-%m-%dT%H:%M:%fZ'),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
from12To13: (m, v13) async {
|
from12To13: (m, v13) async {
|
||||||
await m.create(v13.trashedLocalAssetEntity);
|
await m.create(v13.trashedLocalAssetEntity);
|
||||||
await m.createIndex(v13.idxTrashedLocalAssetChecksum);
|
await m.createIndex(v13.idxTrashedLocalAssetChecksum);
|
||||||
await m.createIndex(v13.idxTrashedLocalAssetAlbum);
|
await m.createIndex(v13.idxTrashedLocalAssetAlbum);
|
||||||
},
|
},
|
||||||
from13To14: (m, v14) async {
|
from13To14: (m, v14) async {
|
||||||
await m.addColumn(v14.localAssetEntity, v14.localAssetEntity.adjustmentTime);
|
await m.addColumn(v14.localAssetEntity, v14.localAssetEntity.adjustmentTime);
|
||||||
await m.addColumn(v14.localAssetEntity, v14.localAssetEntity.latitude);
|
await m.addColumn(v14.localAssetEntity, v14.localAssetEntity.latitude);
|
||||||
await m.addColumn(v14.localAssetEntity, v14.localAssetEntity.longitude);
|
await m.addColumn(v14.localAssetEntity, v14.localAssetEntity.longitude);
|
||||||
},
|
},
|
||||||
from14To15: (m, v15) async {
|
from14To15: (m, v15) async {
|
||||||
await m.alterTable(
|
await m.alterTable(
|
||||||
TableMigration(
|
TableMigration(
|
||||||
v15.trashedLocalAssetEntity,
|
v15.trashedLocalAssetEntity,
|
||||||
columnTransformer: {v15.trashedLocalAssetEntity.source: Constant(TrashOrigin.localSync.index)},
|
columnTransformer: {v15.trashedLocalAssetEntity.source: Constant(TrashOrigin.localSync.index)},
|
||||||
newColumns: [v15.trashedLocalAssetEntity.source],
|
newColumns: [v15.trashedLocalAssetEntity.source],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
from15To16: (m, v16) async {
|
from15To16: (m, v16) async {
|
||||||
// Add i_cloud_id to local and remote asset tables
|
// Add i_cloud_id to local and remote asset tables
|
||||||
await m.addColumn(v16.localAssetEntity, v16.localAssetEntity.iCloudId);
|
await m.addColumn(v16.localAssetEntity, v16.localAssetEntity.iCloudId);
|
||||||
await m.createIndex(v16.idxLocalAssetCloudId);
|
await m.createIndex(v16.idxLocalAssetCloudId);
|
||||||
await m.createTable(v16.remoteAssetCloudIdEntity);
|
await m.createTable(v16.remoteAssetCloudIdEntity);
|
||||||
},
|
},
|
||||||
from16To17: (m, v17) async {
|
from16To17: (m, v17) async {
|
||||||
await m.addColumn(v17.remoteAssetEntity, v17.remoteAssetEntity.isEdited);
|
await m.addColumn(v17.remoteAssetEntity, v17.remoteAssetEntity.isEdited);
|
||||||
},
|
},
|
||||||
from17To18: (m, v18) async {
|
from17To18: (m, v18) async {
|
||||||
await m.createIndex(v18.idxRemoteAssetCloudId);
|
await m.createIndex(v18.idxRemoteAssetCloudId);
|
||||||
},
|
},
|
||||||
from18To19: (m, v19) async {
|
from18To19: (m, v19) async {
|
||||||
await m.createIndex(v19.idxAssetFacePersonId);
|
await m.createIndex(v19.idxAssetFacePersonId);
|
||||||
await m.createIndex(v19.idxAssetFaceAssetId);
|
await m.createIndex(v19.idxAssetFaceAssetId);
|
||||||
await m.createIndex(v19.idxLocalAlbumAssetAlbumAsset);
|
await m.createIndex(v19.idxLocalAlbumAssetAlbumAsset);
|
||||||
await m.createIndex(v19.idxPartnerSharedWithId);
|
await m.createIndex(v19.idxPartnerSharedWithId);
|
||||||
await m.createIndex(v19.idxPersonOwnerId);
|
await m.createIndex(v19.idxPersonOwnerId);
|
||||||
await m.createIndex(v19.idxRemoteAlbumOwnerId);
|
await m.createIndex(v19.idxRemoteAlbumOwnerId);
|
||||||
await m.createIndex(v19.idxRemoteAlbumAssetAlbumAsset);
|
await m.createIndex(v19.idxRemoteAlbumAssetAlbumAsset);
|
||||||
await m.createIndex(v19.idxRemoteAssetStackId);
|
await m.createIndex(v19.idxRemoteAssetStackId);
|
||||||
await m.createIndex(v19.idxRemoteAssetLocalDateTimeDay);
|
await m.createIndex(v19.idxRemoteAssetLocalDateTimeDay);
|
||||||
await m.createIndex(v19.idxRemoteAssetLocalDateTimeMonth);
|
await m.createIndex(v19.idxRemoteAssetLocalDateTimeMonth);
|
||||||
await m.createIndex(v19.idxStackPrimaryAssetId);
|
await m.createIndex(v19.idxStackPrimaryAssetId);
|
||||||
},
|
},
|
||||||
from19To20: (m, v20) async {
|
from19To20: (m, v20) async {
|
||||||
await m.addColumn(v20.assetFaceEntity, v20.assetFaceEntity.isVisible);
|
await m.addColumn(v20.assetFaceEntity, v20.assetFaceEntity.isVisible);
|
||||||
await m.addColumn(v20.assetFaceEntity, v20.assetFaceEntity.deletedAt);
|
await m.addColumn(v20.assetFaceEntity, v20.assetFaceEntity.deletedAt);
|
||||||
},
|
},
|
||||||
from20To21: (m, v21) async {
|
from20To21: (m, v21) async {
|
||||||
await m.addColumn(v21.localAssetEntity, v21.localAssetEntity.playbackStyle);
|
await m.addColumn(v21.localAssetEntity, v21.localAssetEntity.playbackStyle);
|
||||||
await m.addColumn(v21.trashedLocalAssetEntity, v21.trashedLocalAssetEntity.playbackStyle);
|
await m.addColumn(v21.trashedLocalAssetEntity, v21.trashedLocalAssetEntity.playbackStyle);
|
||||||
},
|
},
|
||||||
from21To22: (m, v22) async {
|
from21To22: (m, v22) async {
|
||||||
await m.createTable(v22.assetEditEntity);
|
await m.createTable(v22.assetEditEntity);
|
||||||
await m.createIndex(v22.idxAssetEditAssetId);
|
await m.createIndex(v22.idxAssetEditAssetId);
|
||||||
},
|
},
|
||||||
from22To23: (m, v23) async {
|
from22To23: (m, v23) async {
|
||||||
await m.renameColumn(v23.localAssetEntity, 'duration_in_seconds', v23.localAssetEntity.durationMs);
|
await m.renameColumn(v23.localAssetEntity, 'duration_in_seconds', v23.localAssetEntity.durationMs);
|
||||||
await m.renameColumn(v23.remoteAssetEntity, 'duration_in_seconds', v23.remoteAssetEntity.durationMs);
|
await m.renameColumn(v23.remoteAssetEntity, 'duration_in_seconds', v23.remoteAssetEntity.durationMs);
|
||||||
await m.renameColumn(
|
await m.renameColumn(
|
||||||
v23.trashedLocalAssetEntity,
|
v23.trashedLocalAssetEntity,
|
||||||
'duration_in_seconds',
|
'duration_in_seconds',
|
||||||
v23.trashedLocalAssetEntity.durationMs,
|
v23.trashedLocalAssetEntity.durationMs,
|
||||||
);
|
);
|
||||||
|
|
||||||
await localAssetEntity.update().write(
|
await localAssetEntity.update().write(
|
||||||
LocalAssetEntityCompanion.custom(durationMs: v23.localAssetEntity.durationMs * const Constant(1000)),
|
LocalAssetEntityCompanion.custom(durationMs: v23.localAssetEntity.durationMs * const Constant(1000)),
|
||||||
);
|
);
|
||||||
await remoteAssetEntity.update().write(
|
await remoteAssetEntity.update().write(
|
||||||
RemoteAssetEntityCompanion.custom(durationMs: v23.remoteAssetEntity.durationMs * const Constant(1000)),
|
RemoteAssetEntityCompanion.custom(
|
||||||
);
|
durationMs: v23.remoteAssetEntity.durationMs * const Constant(1000),
|
||||||
await trashedLocalAssetEntity.update().write(
|
),
|
||||||
TrashedLocalAssetEntityCompanion.custom(
|
);
|
||||||
durationMs: v23.trashedLocalAssetEntity.durationMs * const Constant(1000),
|
await trashedLocalAssetEntity.update().write(
|
||||||
),
|
TrashedLocalAssetEntityCompanion.custom(
|
||||||
);
|
durationMs: v23.trashedLocalAssetEntity.durationMs * const Constant(1000),
|
||||||
},
|
),
|
||||||
from23To24: (m, v24) async {
|
);
|
||||||
await customStatement('DROP INDEX IF EXISTS idx_remote_album_owner_id');
|
},
|
||||||
await m.alterTable(TableMigration(v24.remoteAlbumEntity));
|
from23To24: (m, v24) async {
|
||||||
},
|
await customStatement('DROP INDEX IF EXISTS idx_remote_album_owner_id');
|
||||||
from24To25: (m, v25) async {
|
await m.alterTable(TableMigration(v24.remoteAlbumEntity));
|
||||||
await m.createTable(v25.metadata);
|
},
|
||||||
await customStatement('DROP INDEX IF EXISTS idx_remote_asset_owner_checksum');
|
from24To25: (m, v25) async {
|
||||||
await customStatement('DROP INDEX IF EXISTS idx_remote_asset_local_date_time_day');
|
await m.createTable(v25.metadata);
|
||||||
await customStatement('DROP INDEX IF EXISTS idx_remote_asset_local_date_time_month');
|
await customStatement('DROP INDEX IF EXISTS idx_remote_asset_owner_checksum');
|
||||||
await m.createIndex(v25.idxRemoteAssetOwnerVisibilityDeletedCreated);
|
await customStatement('DROP INDEX IF EXISTS idx_remote_asset_local_date_time_day');
|
||||||
await m.createIndex(v25.idxRemoteExifCity);
|
await customStatement('DROP INDEX IF EXISTS idx_remote_asset_local_date_time_month');
|
||||||
await m.createIndex(v25.idxAssetFaceVisiblePerson);
|
await m.createIndex(v25.idxRemoteAssetOwnerVisibilityDeletedCreated);
|
||||||
},
|
await m.createIndex(v25.idxRemoteExifCity);
|
||||||
from25To26: (m, v26) async {
|
await m.createIndex(v25.idxAssetFaceVisiblePerson);
|
||||||
await m.addColumn(v26.remoteAssetEntity, v26.remoteAssetEntity.uploadedAt);
|
},
|
||||||
},
|
from25To26: (m, v26) async {
|
||||||
from26To27: (m, v27) async {
|
await m.addColumn(v26.remoteAssetEntity, v26.remoteAssetEntity.uploadedAt);
|
||||||
await customStatement('ALTER TABLE metadata RENAME TO settings');
|
},
|
||||||
},
|
from26To27: (m, v27) async {
|
||||||
from27To28: (m, v28) async {
|
await customStatement('ALTER TABLE metadata RENAME TO settings');
|
||||||
await m.createIndex(v28.idxLocalAssetCreatedAt);
|
},
|
||||||
},
|
from27To28: (m, v28) async {
|
||||||
from28To29: (m, v29) async {
|
await m.createIndex(v28.idxLocalAssetCreatedAt);
|
||||||
await m.createTable(v29.assetOcrEntity);
|
},
|
||||||
await m.createIndex(v29.idxAssetOcrAssetId);
|
from28To29: (m, v29) async {
|
||||||
},
|
await m.createTable(v29.assetOcrEntity);
|
||||||
from29To30: (m, v30) async {
|
await m.createIndex(v29.idxAssetOcrAssetId);
|
||||||
await m.alterTable(TableMigration(v30.settings));
|
},
|
||||||
},
|
from29To30: (m, v30) async {
|
||||||
),
|
await m.alterTable(TableMigration(v30.settings));
|
||||||
);
|
},
|
||||||
|
from30To31: (m, v31) async {
|
||||||
|
await m.createIndex(v31.idxRemoteAssetUploaded);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
// Fail if the migration broke foreign keys
|
// Fail if the migration broke foreign keys
|
||||||
final wrongFKs = await customSelect('PRAGMA foreign_key_check').get();
|
final wrongFKs = await customSelect('PRAGMA foreign_key_check').get();
|
||||||
assert(wrongFKs.isEmpty, '${wrongFKs.map((e) => e.data)}');
|
assert(wrongFKs.isEmpty, '${wrongFKs.map((e) => e.data)}');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await customStatement('PRAGMA foreign_keys = ON;');
|
||||||
}
|
}
|
||||||
|
|
||||||
await customStatement('PRAGMA foreign_keys = ON;');
|
|
||||||
await optimize();
|
await optimize();
|
||||||
},
|
},
|
||||||
beforeOpen: (details) async {
|
beforeOpen: (details) async {
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
|||||||
i2.idxRemoteAssetChecksum,
|
i2.idxRemoteAssetChecksum,
|
||||||
i2.idxRemoteAssetStackId,
|
i2.idxRemoteAssetStackId,
|
||||||
i2.idxRemoteAssetOwnerVisibilityDeletedCreated,
|
i2.idxRemoteAssetOwnerVisibilityDeletedCreated,
|
||||||
|
i2.idxRemoteAssetUploaded,
|
||||||
authUserEntity,
|
authUserEntity,
|
||||||
userMetadataEntity,
|
userMetadataEntity,
|
||||||
partnerEntity,
|
partnerEntity,
|
||||||
|
|||||||
@@ -15920,6 +15920,592 @@ i1.GeneratedColumn<String> _column_224(String aliasedName) =>
|
|||||||
type: i1.DriftSqlType.string,
|
type: i1.DriftSqlType.string,
|
||||||
$customConstraints: 'NULL',
|
$customConstraints: 'NULL',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final class Schema31 extends i0.VersionedSchema {
|
||||||
|
Schema31({required super.database}) : super(version: 31);
|
||||||
|
@override
|
||||||
|
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||||
|
userEntity,
|
||||||
|
remoteAssetEntity,
|
||||||
|
stackEntity,
|
||||||
|
localAssetEntity,
|
||||||
|
remoteAlbumEntity,
|
||||||
|
localAlbumEntity,
|
||||||
|
localAlbumAssetEntity,
|
||||||
|
idxLocalAlbumAssetAlbumAsset,
|
||||||
|
idxLocalAssetChecksum,
|
||||||
|
idxLocalAssetCloudId,
|
||||||
|
idxLocalAssetCreatedAt,
|
||||||
|
idxStackPrimaryAssetId,
|
||||||
|
uQRemoteAssetsOwnerChecksum,
|
||||||
|
uQRemoteAssetsOwnerLibraryChecksum,
|
||||||
|
idxRemoteAssetChecksum,
|
||||||
|
idxRemoteAssetStackId,
|
||||||
|
idxRemoteAssetOwnerVisibilityDeletedCreated,
|
||||||
|
idxRemoteAssetUploaded,
|
||||||
|
authUserEntity,
|
||||||
|
userMetadataEntity,
|
||||||
|
partnerEntity,
|
||||||
|
remoteExifEntity,
|
||||||
|
remoteAlbumAssetEntity,
|
||||||
|
remoteAlbumUserEntity,
|
||||||
|
remoteAssetCloudIdEntity,
|
||||||
|
memoryEntity,
|
||||||
|
memoryAssetEntity,
|
||||||
|
personEntity,
|
||||||
|
assetFaceEntity,
|
||||||
|
storeEntity,
|
||||||
|
trashedLocalAssetEntity,
|
||||||
|
assetEditEntity,
|
||||||
|
settings,
|
||||||
|
assetOcrEntity,
|
||||||
|
idxPartnerSharedWithId,
|
||||||
|
idxLatLng,
|
||||||
|
idxRemoteExifCity,
|
||||||
|
idxRemoteAlbumAssetAlbumAsset,
|
||||||
|
idxRemoteAssetCloudId,
|
||||||
|
idxPersonOwnerId,
|
||||||
|
idxAssetFacePersonId,
|
||||||
|
idxAssetFaceAssetId,
|
||||||
|
idxAssetFaceVisiblePerson,
|
||||||
|
idxTrashedLocalAssetChecksum,
|
||||||
|
idxTrashedLocalAssetAlbum,
|
||||||
|
idxAssetEditAssetId,
|
||||||
|
idxAssetOcrAssetId,
|
||||||
|
];
|
||||||
|
late final Shape33 userEntity = Shape33(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'user_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_107,
|
||||||
|
_column_108,
|
||||||
|
_column_109,
|
||||||
|
_column_110,
|
||||||
|
_column_111,
|
||||||
|
_column_112,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape50 remoteAssetEntity = Shape50(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_108,
|
||||||
|
_column_113,
|
||||||
|
_column_114,
|
||||||
|
_column_115,
|
||||||
|
_column_116,
|
||||||
|
_column_117,
|
||||||
|
_column_118,
|
||||||
|
_column_107,
|
||||||
|
_column_119,
|
||||||
|
_column_120,
|
||||||
|
_column_121,
|
||||||
|
_column_122,
|
||||||
|
_column_123,
|
||||||
|
_column_124,
|
||||||
|
_column_212,
|
||||||
|
_column_125,
|
||||||
|
_column_126,
|
||||||
|
_column_127,
|
||||||
|
_column_128,
|
||||||
|
_column_129,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape35 stackEntity = Shape35(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'stack_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_107,
|
||||||
|
_column_114,
|
||||||
|
_column_115,
|
||||||
|
_column_121,
|
||||||
|
_column_130,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape36 localAssetEntity = Shape36(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'local_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_108,
|
||||||
|
_column_113,
|
||||||
|
_column_114,
|
||||||
|
_column_115,
|
||||||
|
_column_116,
|
||||||
|
_column_117,
|
||||||
|
_column_118,
|
||||||
|
_column_107,
|
||||||
|
_column_131,
|
||||||
|
_column_120,
|
||||||
|
_column_132,
|
||||||
|
_column_133,
|
||||||
|
_column_134,
|
||||||
|
_column_135,
|
||||||
|
_column_136,
|
||||||
|
_column_137,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape48 remoteAlbumEntity = Shape48(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_album_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_107,
|
||||||
|
_column_108,
|
||||||
|
_column_138,
|
||||||
|
_column_114,
|
||||||
|
_column_115,
|
||||||
|
_column_139,
|
||||||
|
_column_140,
|
||||||
|
_column_141,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape38 localAlbumEntity = Shape38(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'local_album_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_107,
|
||||||
|
_column_108,
|
||||||
|
_column_115,
|
||||||
|
_column_142,
|
||||||
|
_column_143,
|
||||||
|
_column_144,
|
||||||
|
_column_145,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape39 localAlbumAssetEntity = Shape39(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'local_album_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
|
||||||
|
columns: [_column_146, _column_147, _column_145],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
final i1.Index idxLocalAlbumAssetAlbumAsset = i1.Index(
|
||||||
|
'idx_local_album_asset_album_asset',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)',
|
||||||
|
);
|
||||||
|
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 idxLocalAssetCloudId = i1.Index(
|
||||||
|
'idx_local_asset_cloud_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxLocalAssetCreatedAt = i1.Index(
|
||||||
|
'idx_local_asset_created_at',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_local_asset_created_at ON local_asset_entity (created_at)',
|
||||||
|
);
|
||||||
|
final i1.Index idxStackPrimaryAssetId = i1.Index(
|
||||||
|
'idx_stack_primary_asset_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)',
|
||||||
|
);
|
||||||
|
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)',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteAssetStackId = i1.Index(
|
||||||
|
'idx_remote_asset_stack_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteAssetOwnerVisibilityDeletedCreated = i1.Index(
|
||||||
|
'idx_remote_asset_owner_visibility_deleted_created',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteAssetUploaded = i1.Index(
|
||||||
|
'idx_remote_asset_uploaded',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_asset_uploaded ON remote_asset_entity (uploaded_at)',
|
||||||
|
);
|
||||||
|
late final Shape40 authUserEntity = Shape40(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'auth_user_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_107,
|
||||||
|
_column_108,
|
||||||
|
_column_109,
|
||||||
|
_column_148,
|
||||||
|
_column_110,
|
||||||
|
_column_111,
|
||||||
|
_column_149,
|
||||||
|
_column_150,
|
||||||
|
_column_151,
|
||||||
|
_column_152,
|
||||||
|
],
|
||||||
|
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_153, _column_154, _column_155],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape41 partnerEntity = Shape41(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'partner_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'],
|
||||||
|
columns: [_column_156, _column_157, _column_158],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape42 remoteExifEntity = Shape42(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_exif_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(asset_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_159,
|
||||||
|
_column_160,
|
||||||
|
_column_161,
|
||||||
|
_column_162,
|
||||||
|
_column_163,
|
||||||
|
_column_164,
|
||||||
|
_column_117,
|
||||||
|
_column_116,
|
||||||
|
_column_165,
|
||||||
|
_column_166,
|
||||||
|
_column_167,
|
||||||
|
_column_168,
|
||||||
|
_column_135,
|
||||||
|
_column_136,
|
||||||
|
_column_169,
|
||||||
|
_column_170,
|
||||||
|
_column_171,
|
||||||
|
_column_172,
|
||||||
|
_column_173,
|
||||||
|
_column_174,
|
||||||
|
_column_175,
|
||||||
|
_column_176,
|
||||||
|
],
|
||||||
|
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_159, _column_177],
|
||||||
|
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_177, _column_153, _column_178],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape43 remoteAssetCloudIdEntity = Shape43(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_asset_cloud_id_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(asset_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_159,
|
||||||
|
_column_179,
|
||||||
|
_column_180,
|
||||||
|
_column_134,
|
||||||
|
_column_135,
|
||||||
|
_column_136,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape44 memoryEntity = Shape44(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'memory_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_107,
|
||||||
|
_column_114,
|
||||||
|
_column_115,
|
||||||
|
_column_124,
|
||||||
|
_column_121,
|
||||||
|
_column_113,
|
||||||
|
_column_181,
|
||||||
|
_column_182,
|
||||||
|
_column_183,
|
||||||
|
_column_184,
|
||||||
|
_column_185,
|
||||||
|
_column_186,
|
||||||
|
],
|
||||||
|
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_159, _column_187],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape45 personEntity = Shape45(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'person_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_107,
|
||||||
|
_column_114,
|
||||||
|
_column_115,
|
||||||
|
_column_121,
|
||||||
|
_column_108,
|
||||||
|
_column_188,
|
||||||
|
_column_189,
|
||||||
|
_column_190,
|
||||||
|
_column_191,
|
||||||
|
_column_192,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape46 assetFaceEntity = Shape46(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'asset_face_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_107,
|
||||||
|
_column_159,
|
||||||
|
_column_193,
|
||||||
|
_column_194,
|
||||||
|
_column_195,
|
||||||
|
_column_196,
|
||||||
|
_column_197,
|
||||||
|
_column_198,
|
||||||
|
_column_199,
|
||||||
|
_column_200,
|
||||||
|
_column_201,
|
||||||
|
_column_124,
|
||||||
|
],
|
||||||
|
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_202, _column_203, _column_204],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape47 trashedLocalAssetEntity = Shape47(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'trashed_local_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id, album_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_108,
|
||||||
|
_column_113,
|
||||||
|
_column_114,
|
||||||
|
_column_115,
|
||||||
|
_column_116,
|
||||||
|
_column_117,
|
||||||
|
_column_118,
|
||||||
|
_column_107,
|
||||||
|
_column_205,
|
||||||
|
_column_131,
|
||||||
|
_column_120,
|
||||||
|
_column_132,
|
||||||
|
_column_206,
|
||||||
|
_column_137,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape32 assetEditEntity = Shape32(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'asset_edit_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_107,
|
||||||
|
_column_159,
|
||||||
|
_column_207,
|
||||||
|
_column_208,
|
||||||
|
_column_209,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape49 settings = Shape49(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'settings',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY("key")'],
|
||||||
|
columns: [_column_210, _column_224, _column_115],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape51 assetOcrEntity = Shape51(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'asset_ocr_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_107,
|
||||||
|
_column_159,
|
||||||
|
_column_213,
|
||||||
|
_column_214,
|
||||||
|
_column_215,
|
||||||
|
_column_216,
|
||||||
|
_column_217,
|
||||||
|
_column_218,
|
||||||
|
_column_219,
|
||||||
|
_column_220,
|
||||||
|
_column_221,
|
||||||
|
_column_222,
|
||||||
|
_column_223,
|
||||||
|
_column_201,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
final i1.Index idxPartnerSharedWithId = i1.Index(
|
||||||
|
'idx_partner_shared_with_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxLatLng = i1.Index(
|
||||||
|
'idx_lat_lng',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteExifCity = i1.Index(
|
||||||
|
'idx_remote_exif_city',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_exif_city ON remote_exif_entity (city) WHERE city IS NOT NULL',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteAlbumAssetAlbumAsset = i1.Index(
|
||||||
|
'idx_remote_album_asset_album_asset',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteAssetCloudId = i1.Index(
|
||||||
|
'idx_remote_asset_cloud_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxPersonOwnerId = i1.Index(
|
||||||
|
'idx_person_owner_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxAssetFacePersonId = i1.Index(
|
||||||
|
'idx_asset_face_person_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxAssetFaceAssetId = i1.Index(
|
||||||
|
'idx_asset_face_asset_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxAssetFaceVisiblePerson = i1.Index(
|
||||||
|
'idx_asset_face_visible_person',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person ON asset_face_entity (person_id, asset_id) WHERE is_visible = 1 AND deleted_at IS NULL',
|
||||||
|
);
|
||||||
|
final i1.Index idxTrashedLocalAssetChecksum = i1.Index(
|
||||||
|
'idx_trashed_local_asset_checksum',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)',
|
||||||
|
);
|
||||||
|
final i1.Index idxTrashedLocalAssetAlbum = i1.Index(
|
||||||
|
'idx_trashed_local_asset_album',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxAssetEditAssetId = i1.Index(
|
||||||
|
'idx_asset_edit_asset_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)',
|
||||||
|
);
|
||||||
|
final i1.Index idxAssetOcrAssetId = i1.Index(
|
||||||
|
'idx_asset_ocr_asset_id',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_asset_ocr_asset_id ON asset_ocr_entity (asset_id)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
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,
|
||||||
@@ -15950,6 +16536,7 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||||||
required Future<void> Function(i1.Migrator m, Schema28 schema) from27To28,
|
required Future<void> Function(i1.Migrator m, Schema28 schema) from27To28,
|
||||||
required Future<void> Function(i1.Migrator m, Schema29 schema) from28To29,
|
required Future<void> Function(i1.Migrator m, Schema29 schema) from28To29,
|
||||||
required Future<void> Function(i1.Migrator m, Schema30 schema) from29To30,
|
required Future<void> Function(i1.Migrator m, Schema30 schema) from29To30,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema31 schema) from30To31,
|
||||||
}) {
|
}) {
|
||||||
return (currentVersion, database) async {
|
return (currentVersion, database) async {
|
||||||
switch (currentVersion) {
|
switch (currentVersion) {
|
||||||
@@ -16098,6 +16685,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||||||
final migrator = i1.Migrator(database, schema);
|
final migrator = i1.Migrator(database, schema);
|
||||||
await from29To30(migrator, schema);
|
await from29To30(migrator, schema);
|
||||||
return 30;
|
return 30;
|
||||||
|
case 30:
|
||||||
|
final schema = Schema31(database: database);
|
||||||
|
final migrator = i1.Migrator(database, schema);
|
||||||
|
await from30To31(migrator, schema);
|
||||||
|
return 31;
|
||||||
default:
|
default:
|
||||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||||
}
|
}
|
||||||
@@ -16134,6 +16726,7 @@ i1.OnUpgrade stepByStep({
|
|||||||
required Future<void> Function(i1.Migrator m, Schema28 schema) from27To28,
|
required Future<void> Function(i1.Migrator m, Schema28 schema) from27To28,
|
||||||
required Future<void> Function(i1.Migrator m, Schema29 schema) from28To29,
|
required Future<void> Function(i1.Migrator m, Schema29 schema) from28To29,
|
||||||
required Future<void> Function(i1.Migrator m, Schema30 schema) from29To30,
|
required Future<void> Function(i1.Migrator m, Schema30 schema) from29To30,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema31 schema) from30To31,
|
||||||
}) => i0.VersionedSchema.stepByStepHelper(
|
}) => i0.VersionedSchema.stepByStepHelper(
|
||||||
step: migrationSteps(
|
step: migrationSteps(
|
||||||
from1To2: from1To2,
|
from1To2: from1To2,
|
||||||
@@ -16165,5 +16758,6 @@ i1.OnUpgrade stepByStep({
|
|||||||
from27To28: from27To28,
|
from27To28: from27To28,
|
||||||
from28To29: from28To29,
|
from28To29: from28To29,
|
||||||
from29To30: from29To30,
|
from29To30: from29To30,
|
||||||
|
from30To31: from30To31,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class StorageRepository {
|
|||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(agg23): Unify these methods
|
||||||
Future<File?> getMotionFileForAsset(LocalAsset asset) async {
|
Future<File?> getMotionFileForAsset(LocalAsset asset) async {
|
||||||
File? file;
|
File? file;
|
||||||
final log = Logger('StorageRepository');
|
final log = Logger('StorageRepository');
|
||||||
|
|||||||
@@ -614,7 +614,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
return (
|
return (
|
||||||
bucketSource: () => _watchRemoteBucket(filter: filter, groupBy: groupBy, sortBy: sortBy),
|
bucketSource: () => _watchRemoteBucket(filter: filter, groupBy: groupBy, sortBy: sortBy),
|
||||||
assetSource: (offset, count) =>
|
assetSource: (offset, count) =>
|
||||||
_getRemoteAssets(filter: filter, offset: offset, count: count, joinLocal: joinLocal),
|
_getRemoteAssets(filter: filter, offset: offset, count: count, joinLocal: joinLocal, sortBy: sortBy),
|
||||||
origin: origin,
|
origin: origin,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -651,6 +651,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
required int offset,
|
required int offset,
|
||||||
required int count,
|
required int count,
|
||||||
bool joinLocal = false,
|
bool joinLocal = false,
|
||||||
|
SortAssetsBy sortBy = SortAssetsBy.taken,
|
||||||
}) {
|
}) {
|
||||||
if (joinLocal) {
|
if (joinLocal) {
|
||||||
final query =
|
final query =
|
||||||
@@ -663,7 +664,11 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
])
|
])
|
||||||
..addColumns([_db.localAssetEntity.id])
|
..addColumns([_db.localAssetEntity.id])
|
||||||
..where(filter(_db.remoteAssetEntity))
|
..where(filter(_db.remoteAssetEntity))
|
||||||
..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)])
|
..orderBy([
|
||||||
|
OrderingTerm.desc(
|
||||||
|
sortBy == SortAssetsBy.uploaded ? _db.remoteAssetEntity.uploadedAt : _db.remoteAssetEntity.createdAt,
|
||||||
|
),
|
||||||
|
])
|
||||||
..limit(count, offset: offset);
|
..limit(count, offset: offset);
|
||||||
|
|
||||||
return query
|
return query
|
||||||
@@ -672,7 +677,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
} else {
|
} else {
|
||||||
final query = _db.remoteAssetEntity.select()
|
final query = _db.remoteAssetEntity.select()
|
||||||
..where(filter)
|
..where(filter)
|
||||||
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
|
..orderBy([(row) => OrderingTerm.desc(sortBy == SortAssetsBy.uploaded ? row.uploadedAt : row.createdAt)])
|
||||||
..limit(count, offset: offset);
|
..limit(count, offset: offset);
|
||||||
|
|
||||||
return query.map((row) => row.toDto()).get();
|
return query.map((row) => row.toDto()).get();
|
||||||
|
|||||||
@@ -114,7 +114,6 @@ class _AssetPropertiesSectionState extends ConsumerState<_AssetPropertiesSection
|
|||||||
_PropertyItem(label: 'Height', value: asset.height?.toString()),
|
_PropertyItem(label: 'Height', value: asset.height?.toString()),
|
||||||
_PropertyItem(label: 'Duration', value: asset.durationMs != null ? '${asset.durationMs} ms' : null),
|
_PropertyItem(label: 'Duration', value: asset.durationMs != null ? '${asset.durationMs} ms' : null),
|
||||||
_PropertyItem(label: 'Is Favorite', value: asset.isFavorite.toString()),
|
_PropertyItem(label: 'Is Favorite', value: asset.isFavorite.toString()),
|
||||||
_PropertyItem(label: 'Live Photo Video ID', value: asset.livePhotoVideoId),
|
|
||||||
_PropertyItem(label: 'Is Edited', value: asset.isEdited.toString()),
|
_PropertyItem(label: 'Is Edited', value: asset.isEdited.toString()),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -151,6 +150,7 @@ class _AssetPropertiesSectionState extends ConsumerState<_AssetPropertiesSection
|
|||||||
_PropertyItem(label: 'Thumb Hash', value: asset.thumbHash),
|
_PropertyItem(label: 'Thumb Hash', value: asset.thumbHash),
|
||||||
_PropertyItem(label: 'Visibility', value: asset.visibility.toString()),
|
_PropertyItem(label: 'Visibility', value: asset.visibility.toString()),
|
||||||
_PropertyItem(label: 'Stack ID', value: asset.stackId),
|
_PropertyItem(label: 'Stack ID', value: asset.stackId),
|
||||||
|
_PropertyItem(label: 'Live Photo Video ID', value: asset.livePhotoVideoId),
|
||||||
];
|
];
|
||||||
|
|
||||||
properties.insertAll(4, additionalProps);
|
properties.insertAll(4, additionalProps);
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ class _DriftEditImagePageState extends ConsumerState<DriftEditImagePage> with Ti
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _AspectRatioButton extends StatelessWidget {
|
class _AspectRatioButton extends StatelessWidget {
|
||||||
final AspectRatioPreset ratio;
|
final CropAspectRatio ratio;
|
||||||
final bool isSelected;
|
final bool isSelected;
|
||||||
final VoidCallback onPressed;
|
final VoidCallback onPressed;
|
||||||
|
|
||||||
@@ -162,15 +162,16 @@ class _AspectRatioButton extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final color = isSelected ? context.primaryColor : context.themeData.iconTheme.color;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
iconSize: 36,
|
iconSize: 36,
|
||||||
icon: Transform.rotate(
|
icon: ratio.ratio != null
|
||||||
angle: ratio.iconRotated ? pi / 2 : 0,
|
? _AspectRatioRect(ratio: ratio.ratio!, color: color)
|
||||||
child: Icon(ratio.icon, color: isSelected ? context.primaryColor : context.themeData.iconTheme.color),
|
: Icon(ratio.icon, color: color),
|
||||||
),
|
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
),
|
),
|
||||||
Text(ratio.label, style: context.textTheme.displayMedium),
|
Text(ratio.label, style: context.textTheme.displayMedium),
|
||||||
@@ -179,6 +180,32 @@ class _AspectRatioButton extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _AspectRatioRect extends StatelessWidget {
|
||||||
|
final double ratio;
|
||||||
|
final Color? color;
|
||||||
|
|
||||||
|
const _AspectRatioRect({required this.ratio, required this.color});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
child: Center(
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: ratio,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: color ?? Colors.transparent, width: 3),
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _AspectRatioSelector extends ConsumerWidget {
|
class _AspectRatioSelector extends ConsumerWidget {
|
||||||
const _AspectRatioSelector();
|
const _AspectRatioSelector();
|
||||||
|
|
||||||
@@ -187,22 +214,16 @@ class _AspectRatioSelector extends ConsumerWidget {
|
|||||||
final editorState = ref.watch(editorStateProvider);
|
final editorState = ref.watch(editorStateProvider);
|
||||||
final editorNotifier = ref.read(editorStateProvider.notifier);
|
final editorNotifier = ref.read(editorStateProvider.notifier);
|
||||||
|
|
||||||
// the whole crop view is rotated, so we need to swap the aspect ratio when the rotation is 90 or 270 degrees
|
|
||||||
double? selectedAspectRatio = editorState.aspectRatio;
|
|
||||||
if (editorState.rotationAngle % 180 != 0 && selectedAspectRatio != null) {
|
|
||||||
selectedAspectRatio = 1 / selectedAspectRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: AspectRatioPreset.values.map((entry) {
|
children: aspectRatioPresets.map((entry) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
child: _AspectRatioButton(
|
child: _AspectRatioButton(
|
||||||
ratio: entry,
|
ratio: entry,
|
||||||
isSelected: selectedAspectRatio == entry.ratio,
|
isSelected: editorState.aspectRatio == entry,
|
||||||
onPressed: () => editorNotifier.setAspectRatio(entry.ratio),
|
onPressed: () => editorNotifier.setAspectRatio(entry),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
@@ -357,8 +378,22 @@ class _EditorPreviewState extends ConsumerState<_EditorPreview> with TickerProvi
|
|||||||
final editorState = ref.watch(editorStateProvider);
|
final editorState = ref.watch(editorStateProvider);
|
||||||
final editorNotifier = ref.read(editorStateProvider.notifier);
|
final editorNotifier = ref.read(editorStateProvider.notifier);
|
||||||
|
|
||||||
ref.listen(editorStateProvider, (_, current) {
|
ref.listen(editorStateProvider, (previous, current) {
|
||||||
cropController.aspectRatio = current.aspectRatio;
|
// Only re-apply the aspect ratio when it changes, otherwise the crop rect will shrink on every rotation
|
||||||
|
if (previous?.aspectRatio != current.aspectRatio) {
|
||||||
|
double? ratio;
|
||||||
|
|
||||||
|
ratio = switch (current.aspectRatio) {
|
||||||
|
CropAspectRatio.original => current.originalWidth / current.originalHeight,
|
||||||
|
_ => current.aspectRatio.ratio,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (current.rotationAngle % 180 != 0) {
|
||||||
|
ratio = ratio != null ? 1 / ratio : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
cropController.aspectRatio = ratio;
|
||||||
|
}
|
||||||
|
|
||||||
if (cropController.crop != current.crop) {
|
if (cropController.crop != current.crop) {
|
||||||
cropController.crop = current.crop;
|
cropController.crop = current.crop;
|
||||||
@@ -386,7 +421,9 @@ class _EditorPreviewState extends ConsumerState<_EditorPreview> with TickerProvi
|
|||||||
1.0,
|
1.0,
|
||||||
1.0,
|
1.0,
|
||||||
),
|
),
|
||||||
child: Container(
|
child: AnimatedContainer(
|
||||||
|
duration: editorState.animationDuration,
|
||||||
|
curve: Curves.easeInOut,
|
||||||
padding: const EdgeInsets.all(10),
|
padding: const EdgeInsets.all(10),
|
||||||
width: (editorState.rotationAngle % 180 == 0) ? baseWidth : baseHeight,
|
width: (editorState.rotationAngle % 180 == 0) ? baseWidth : baseHeight,
|
||||||
height: (editorState.rotationAngle % 180 == 0) ? baseHeight : baseWidth,
|
height: (editorState.rotationAngle % 180 == 0) ? baseHeight : baseWidth,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/aspect_ratios.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset_edit.model.dart';
|
import 'package:immich_mobile/domain/models/asset_edit.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||||
import 'package:immich_mobile/utils/editor.utils.dart';
|
import 'package:immich_mobile/utils/editor.utils.dart';
|
||||||
@@ -60,13 +61,8 @@ class EditorProvider extends Notifier<EditorState> {
|
|||||||
state = state.copyWith(crop: crop, hasUnsavedEdits: true);
|
state = state.copyWith(crop: crop, hasUnsavedEdits: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setAspectRatio(double? aspectRatio) {
|
void setAspectRatio(CropAspectRatio preset) {
|
||||||
if (aspectRatio != null && state.rotationAngle % 180 != 0) {
|
state = state.copyWith(aspectRatio: preset, hasUnsavedEdits: true);
|
||||||
// When rotated 90 or 270 degrees, swap width and height for aspect ratio calculations
|
|
||||||
aspectRatio = 1 / aspectRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
state = state.copyWith(aspectRatio: aspectRatio);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void resetEdits() {
|
void resetEdits() {
|
||||||
@@ -76,19 +72,19 @@ class EditorProvider extends Notifier<EditorState> {
|
|||||||
flipHorizontal: false,
|
flipHorizontal: false,
|
||||||
flipVertical: false,
|
flipVertical: false,
|
||||||
crop: const Rect.fromLTRB(0, 0, 1, 1),
|
crop: const Rect.fromLTRB(0, 0, 1, 1),
|
||||||
aspectRatio: null,
|
aspectRatio: CropAspectRatio.free,
|
||||||
hasUnsavedEdits: true,
|
hasUnsavedEdits: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void rotateCCW() {
|
void rotateCCW() {
|
||||||
_animateRotation(state.rotationAngle - 90);
|
_animateRotation(state.rotationAngle - 90);
|
||||||
state = state.copyWith(hasUnsavedEdits: true);
|
state = state.copyWith(aspectRatio: state.aspectRatio.flipped, hasUnsavedEdits: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void rotateCW() {
|
void rotateCW() {
|
||||||
_animateRotation(state.rotationAngle + 90);
|
_animateRotation(state.rotationAngle + 90);
|
||||||
state = state.copyWith(hasUnsavedEdits: true);
|
state = state.copyWith(aspectRatio: state.aspectRatio.flipped, hasUnsavedEdits: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void flipHorizontally() {
|
void flipHorizontally() {
|
||||||
@@ -117,7 +113,7 @@ class EditorState {
|
|||||||
final bool flipHorizontal;
|
final bool flipHorizontal;
|
||||||
final bool flipVertical;
|
final bool flipVertical;
|
||||||
final Rect crop;
|
final Rect crop;
|
||||||
final double? aspectRatio;
|
final CropAspectRatio aspectRatio;
|
||||||
|
|
||||||
final int originalWidth;
|
final int originalWidth;
|
||||||
final int originalHeight;
|
final int originalHeight;
|
||||||
@@ -132,7 +128,7 @@ class EditorState {
|
|||||||
bool? flipHorizontal,
|
bool? flipHorizontal,
|
||||||
bool? flipVertical,
|
bool? flipVertical,
|
||||||
Rect? crop,
|
Rect? crop,
|
||||||
this.aspectRatio,
|
CropAspectRatio? aspectRatio,
|
||||||
int? originalWidth,
|
int? originalWidth,
|
||||||
int? originalHeight,
|
int? originalHeight,
|
||||||
Duration? animationDuration,
|
Duration? animationDuration,
|
||||||
@@ -145,6 +141,7 @@ class EditorState {
|
|||||||
originalWidth = originalWidth ?? 0,
|
originalWidth = originalWidth ?? 0,
|
||||||
originalHeight = originalHeight ?? 0,
|
originalHeight = originalHeight ?? 0,
|
||||||
crop = crop ?? const Rect.fromLTRB(0, 0, 1, 1),
|
crop = crop ?? const Rect.fromLTRB(0, 0, 1, 1),
|
||||||
|
aspectRatio = aspectRatio ?? CropAspectRatio.free,
|
||||||
hasUnsavedEdits = hasUnsavedEdits ?? false;
|
hasUnsavedEdits = hasUnsavedEdits ?? false;
|
||||||
|
|
||||||
EditorState copyWith({
|
EditorState copyWith({
|
||||||
@@ -152,7 +149,7 @@ class EditorState {
|
|||||||
int? rotationAngle,
|
int? rotationAngle,
|
||||||
bool? flipHorizontal,
|
bool? flipHorizontal,
|
||||||
bool? flipVertical,
|
bool? flipVertical,
|
||||||
double? aspectRatio = double.infinity,
|
CropAspectRatio? aspectRatio,
|
||||||
int? originalWidth,
|
int? originalWidth,
|
||||||
int? originalHeight,
|
int? originalHeight,
|
||||||
Duration? animationDuration,
|
Duration? animationDuration,
|
||||||
@@ -164,7 +161,7 @@ class EditorState {
|
|||||||
rotationAngle: rotationAngle ?? this.rotationAngle,
|
rotationAngle: rotationAngle ?? this.rotationAngle,
|
||||||
flipHorizontal: flipHorizontal ?? this.flipHorizontal,
|
flipHorizontal: flipHorizontal ?? this.flipHorizontal,
|
||||||
flipVertical: flipVertical ?? this.flipVertical,
|
flipVertical: flipVertical ?? this.flipVertical,
|
||||||
aspectRatio: aspectRatio == double.infinity ? this.aspectRatio : aspectRatio,
|
aspectRatio: aspectRatio ?? this.aspectRatio,
|
||||||
animationDuration: animationDuration ?? this.animationDuration,
|
animationDuration: animationDuration ?? this.animationDuration,
|
||||||
originalWidth: originalWidth ?? this.originalWidth,
|
originalWidth: originalWidth ?? this.originalWidth,
|
||||||
originalHeight: originalHeight ?? this.originalHeight,
|
originalHeight: originalHeight ?? this.originalHeight,
|
||||||
|
|||||||
@@ -122,9 +122,14 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (videoAsset.hasLocal && videoAsset.livePhotoVideoId == null) {
|
// Attempt to retrieve LocalAsset, falling back to remote if it cannot be found
|
||||||
final id = videoAsset is LocalAsset ? videoAsset.id : (videoAsset as RemoteAsset).localId!;
|
final localAsset = await _localPlaybackAsset(videoAsset);
|
||||||
final file = await StorageRepository().getFileForAsset(id);
|
|
||||||
|
if (localAsset != null) {
|
||||||
|
final file = localAsset.isMotionPhoto
|
||||||
|
? await StorageRepository().getMotionFileForAsset(localAsset)
|
||||||
|
: await StorageRepository().getFileForAsset(localAsset.id);
|
||||||
|
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -141,14 +146,13 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final remoteId = (videoAsset as RemoteAsset).id;
|
final remoteAsset = videoAsset as RemoteAsset;
|
||||||
|
|
||||||
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
|
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
|
||||||
final isOriginalVideo = ref.read(appConfigProvider).viewer.loadOriginalVideo;
|
final isOriginalVideo = ref.read(appConfigProvider).viewer.loadOriginalVideo;
|
||||||
final String postfixUrl = isOriginalVideo ? 'original' : 'video/playback';
|
final String postfixUrl = isOriginalVideo ? 'original' : 'video/playback';
|
||||||
final String videoUrl = videoAsset.livePhotoVideoId != null
|
final String assetId = remoteAsset.livePhotoVideoId ?? remoteAsset.id;
|
||||||
? '$serverEndpoint/assets/${videoAsset.livePhotoVideoId}/$postfixUrl'
|
final String videoUrl = '$serverEndpoint/assets/$assetId/$postfixUrl';
|
||||||
: '$serverEndpoint/assets/$remoteId/$postfixUrl';
|
|
||||||
|
|
||||||
return VideoSource.init(path: videoUrl, type: VideoSourceType.network, headers: ApiService.getRequestHeaders());
|
return VideoSource.init(path: videoUrl, type: VideoSourceType.network, headers: ApiService.getRequestHeaders());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -157,6 +161,43 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<LocalAsset?> _localPlaybackAsset(BaseAsset baseAsset) async {
|
||||||
|
if (!baseAsset.hasLocal) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalAsset? localAsset;
|
||||||
|
|
||||||
|
if (baseAsset is LocalAsset) {
|
||||||
|
localAsset = baseAsset;
|
||||||
|
} else {
|
||||||
|
final localId = (baseAsset as RemoteAsset).localId;
|
||||||
|
localAsset = localId != null ? await ref.read(assetServiceProvider).getLocalAsset(localId) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localAsset == null) {
|
||||||
|
_log.severe(
|
||||||
|
'Invariant violation: asset ${baseAsset.name} (${baseAsset.localId}) is marked `hasLocal` but local asset could not be retrieved',
|
||||||
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clients (local) may not correctly recognize a given asset as a motion photo. This allows for a scenario where both remote and local
|
||||||
|
// have the same asset (hash), but only the remote properly recognizes it as a motion asset
|
||||||
|
// If this scenario occurs, fall back to using the remote asset
|
||||||
|
if (baseAsset.isMotionPhoto && !localAsset.isMotionPhoto) {
|
||||||
|
// Platform mismatch for motion photo, use remote instead
|
||||||
|
_log.warning(
|
||||||
|
'Mismatched local and remote motion states on ${baseAsset.name} (${baseAsset.localId}), local = ${localAsset.isMotionPhoto}, remote = ${baseAsset.isMotionPhoto}',
|
||||||
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return localAsset;
|
||||||
|
}
|
||||||
|
|
||||||
void _onPlaybackReady() async {
|
void _onPlaybackReady() async {
|
||||||
if (!mounted || !widget.isCurrent) {
|
if (!mounted || !widget.isCurrent) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -297,8 +297,7 @@ class _AssetTypeIcons extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final remoteAsset = asset is RemoteAsset ? asset as RemoteAsset : null;
|
final isLivePhoto = asset.isMotionPhoto;
|
||||||
final isLivePhoto = remoteAsset?.livePhotoVideoId != null;
|
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ class _SliverTimeline extends ConsumerStatefulWidget {
|
|||||||
ConsumerState createState() => _SliverTimelineState();
|
ConsumerState createState() => _SliverTimelineState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
class _SliverTimelineState extends ConsumerState<_SliverTimeline> with WidgetsBindingObserver {
|
||||||
late final ScrollController _scrollController;
|
late final ScrollController _scrollController;
|
||||||
StreamSubscription? _eventSubscription;
|
StreamSubscription? _eventSubscription;
|
||||||
|
|
||||||
@@ -153,6 +153,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
_scrollController = ScrollController(onAttach: _restoreAssetPosition);
|
_scrollController = ScrollController(onAttach: _restoreAssetPosition);
|
||||||
_eventSubscription = EventStream.shared.listen(_onEvent);
|
_eventSubscription = EventStream.shared.listen(_onEvent);
|
||||||
|
|
||||||
@@ -178,17 +179,14 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Capture iOS status bar tap
|
||||||
|
@override
|
||||||
|
void handleStatusBarTap() => _scrollToTop();
|
||||||
|
|
||||||
void _onEvent(Event event) {
|
void _onEvent(Event event) {
|
||||||
switch (event) {
|
switch (event) {
|
||||||
case ScrollToTopEvent():
|
case ScrollToTopEvent():
|
||||||
{
|
_scrollToTop();
|
||||||
final timelineState = ref.read(timelineStateProvider.notifier);
|
|
||||||
timelineState.setScrubbing(true);
|
|
||||||
_scrollController
|
|
||||||
.animateTo(0, duration: const Duration(milliseconds: 250), curve: Curves.easeInOut)
|
|
||||||
.whenComplete(() => timelineState.setScrubbing(false));
|
|
||||||
}
|
|
||||||
|
|
||||||
case ScrollToDateEvent scrollToDateEvent:
|
case ScrollToDateEvent scrollToDateEvent:
|
||||||
_scrollToDate(scrollToDateEvent.date);
|
_scrollToDate(scrollToDateEvent.date);
|
||||||
case TimelineReloadEvent():
|
case TimelineReloadEvent():
|
||||||
@@ -246,11 +244,24 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
_scrollController.dispose();
|
_scrollController.dispose();
|
||||||
_eventSubscription?.cancel();
|
_eventSubscription?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _scrollToTop() {
|
||||||
|
if (!_scrollController.hasClients) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final timelineState = ref.read(timelineStateProvider.notifier);
|
||||||
|
timelineState.setScrubbing(true);
|
||||||
|
_scrollController
|
||||||
|
.animateTo(0, duration: const Duration(milliseconds: 250), curve: Curves.easeInOut)
|
||||||
|
.whenComplete(() => timelineState.setScrubbing(false));
|
||||||
|
}
|
||||||
|
|
||||||
void _scrollToDate(DateTime date) {
|
void _scrollToDate(DateTime date) {
|
||||||
final timelineState = ref.read(timelineStateProvider.notifier);
|
final timelineState = ref.read(timelineStateProvider.notifier);
|
||||||
final asyncSegments = ref.read(timelineSegmentProvider);
|
final asyncSegments = ref.read(timelineSegmentProvider);
|
||||||
@@ -381,6 +392,9 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
child: PrimaryScrollController(
|
child: PrimaryScrollController(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
|
// This removes the built in Scaffold `handleStatusBarTap` implementation, preventing duplicate
|
||||||
|
// events when we provide our own
|
||||||
|
primary: false,
|
||||||
resizeToAvoidBottomInset: false,
|
resizeToAvoidBottomInset: false,
|
||||||
floatingActionButton: const DownloadStatusFloatingButton(),
|
floatingActionButton: const DownloadStatusFloatingButton(),
|
||||||
body: asyncSegments.widgetWhen(
|
body: asyncSegments.widgetWhen(
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import 'package:immich_mobile/repositories/upload.repository.dart';
|
|||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:immich_mobile/utils/debug_print.dart';
|
import 'package:immich_mobile/utils/debug_print.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:openapi/api.dart' as api;
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
final backgroundUploadServiceProvider = Provider((ref) {
|
final backgroundUploadServiceProvider = Provider((ref) {
|
||||||
@@ -335,7 +336,8 @@ class BackgroundUploadService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final fields = {'livePhotoVideoId': livePhotoVideoId};
|
// Visibility hidden on upload to prevent the server from running regular jobs on the live photo asset
|
||||||
|
final fields = {'livePhotoVideoId': livePhotoVideoId, 'visibility': api.AssetVisibility.hidden.value};
|
||||||
|
|
||||||
final requiresWiFi = _shouldRequireWiFi(asset);
|
final requiresWiFi = _shouldRequireWiFi(asset);
|
||||||
final originalFileName = await _assetMediaRepository.getOriginalFilename(asset.id) ?? asset.name;
|
final originalFileName = await _assetMediaRepository.getOriginalFilename(asset.id) ?? asset.name;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/asset_metadata.model.dart';
|
import 'package:immich_mobile/domain/models/asset/asset_metadata.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' hide AssetVisibility;
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/network_capability_extensions.dart';
|
import 'package:immich_mobile/extensions/network_capability_extensions.dart';
|
||||||
@@ -19,6 +19,7 @@ import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
|
|||||||
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/upload.repository.dart';
|
import 'package:immich_mobile/repositories/upload.repository.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
|
import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
|
||||||
|
|
||||||
@@ -338,7 +339,8 @@ class ForegroundUploadService {
|
|||||||
final livePhotoResult = await _uploadRepository.uploadFile(
|
final livePhotoResult = await _uploadRepository.uploadFile(
|
||||||
file: livePhotoFile,
|
file: livePhotoFile,
|
||||||
originalFileName: livePhotoTitle,
|
originalFileName: livePhotoTitle,
|
||||||
fields: fields,
|
// Visibility hidden on upload to prevent the server from running regular jobs on the live photo asset
|
||||||
|
fields: {...fields, 'visibility': AssetVisibility.hidden.value},
|
||||||
cancelToken: cancelToken,
|
cancelToken: cancelToken,
|
||||||
onProgress: onProgress != null
|
onProgress: onProgress != null
|
||||||
? (bytes, totalBytes) => onProgress(asset.localId!, livePhotoTitle, bytes, totalBytes)
|
? (bytes, totalBytes) => onProgress(asset.localId!, livePhotoTitle, bytes, totalBytes)
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ class SystemConfigIntegrityChecksumJob {
|
|||||||
/// Percentage limit of the integrity checksum job
|
/// Percentage limit of the integrity checksum job
|
||||||
///
|
///
|
||||||
/// Minimum value: 0
|
/// Minimum value: 0
|
||||||
/// Maximum value: 9007199254740991
|
/// Maximum value: 1
|
||||||
int percentageLimit;
|
double percentageLimit;
|
||||||
|
|
||||||
/// How long the integrity checksum job may run for
|
/// How long the integrity checksum job may run for
|
||||||
///
|
///
|
||||||
@@ -75,7 +75,7 @@ class SystemConfigIntegrityChecksumJob {
|
|||||||
return SystemConfigIntegrityChecksumJob(
|
return SystemConfigIntegrityChecksumJob(
|
||||||
cronExpression: mapValueOfType<String>(json, r'cronExpression')!,
|
cronExpression: mapValueOfType<String>(json, r'cronExpression')!,
|
||||||
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
||||||
percentageLimit: mapValueOfType<int>(json, r'percentageLimit')!,
|
percentageLimit: mapValueOfType<double>(json, r'percentageLimit')!,
|
||||||
timeLimit: mapValueOfType<int>(json, r'timeLimit')!,
|
timeLimit: mapValueOfType<int>(json, r'timeLimit')!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-7
@@ -309,8 +309,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "pkgs/cupertino_http"
|
path: "pkgs/cupertino_http"
|
||||||
ref: a0a933358517c6d01cff37fc2a2752ee2d744a3c
|
ref: "58b03c756b81d16b3975a8ae4b91d25360123bb5"
|
||||||
resolved-ref: a0a933358517c6d01cff37fc2a2752ee2d744a3c
|
resolved-ref: "58b03c756b81d16b3975a8ae4b91d25360123bb5"
|
||||||
url: "https://github.com/mertalev/http"
|
url: "https://github.com/mertalev/http"
|
||||||
source: git
|
source: git
|
||||||
version: "3.0.0-wip"
|
version: "3.0.0-wip"
|
||||||
@@ -1158,12 +1158,13 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.0"
|
version: "0.5.0"
|
||||||
objective_c:
|
objective_c:
|
||||||
dependency: transitive
|
dependency: "direct overridden"
|
||||||
description:
|
description:
|
||||||
name: objective_c
|
path: "pkgs/objective_c"
|
||||||
sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed"
|
ref: "2915556701f4734a784222f290a82adb1b101682"
|
||||||
url: "https://pub.dev"
|
resolved-ref: "2915556701f4734a784222f290a82adb1b101682"
|
||||||
source: hosted
|
url: "https://github.com/mertalev/native"
|
||||||
|
source: git
|
||||||
version: "9.4.1"
|
version: "9.4.1"
|
||||||
octo_image:
|
octo_image:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
|
|||||||
+6
-1
@@ -84,7 +84,7 @@ dependencies:
|
|||||||
cupertino_http:
|
cupertino_http:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/mertalev/http
|
url: https://github.com/mertalev/http
|
||||||
ref: 'a0a933358517c6d01cff37fc2a2752ee2d744a3c' # https://github.com/dart-lang/http/pull/1876
|
ref: '58b03c756b81d16b3975a8ae4b91d25360123bb5'
|
||||||
path: pkgs/cupertino_http/
|
path: pkgs/cupertino_http/
|
||||||
ok_http:
|
ok_http:
|
||||||
git:
|
git:
|
||||||
@@ -114,6 +114,11 @@ dev_dependencies:
|
|||||||
# Pin bonsoir to 5.x until cast releases a version compatible with bonsoir 6.x.
|
# Pin bonsoir to 5.x until cast releases a version compatible with bonsoir 6.x.
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
bonsoir: ^5.1.11
|
bonsoir: ^5.1.11
|
||||||
|
objective_c:
|
||||||
|
git:
|
||||||
|
url: https://github.com/mertalev/native
|
||||||
|
ref: '2915556701f4734a784222f290a82adb1b101682' # https://github.com/dart-lang/native/pull/3458
|
||||||
|
path: pkgs/objective_c/
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|||||||
+4
@@ -34,6 +34,7 @@ import 'schema_v27.dart' as v27;
|
|||||||
import 'schema_v28.dart' as v28;
|
import 'schema_v28.dart' as v28;
|
||||||
import 'schema_v29.dart' as v29;
|
import 'schema_v29.dart' as v29;
|
||||||
import 'schema_v30.dart' as v30;
|
import 'schema_v30.dart' as v30;
|
||||||
|
import 'schema_v31.dart' as v31;
|
||||||
|
|
||||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||||
@override
|
@override
|
||||||
@@ -99,6 +100,8 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
|||||||
return v29.DatabaseAtV29(db);
|
return v29.DatabaseAtV29(db);
|
||||||
case 30:
|
case 30:
|
||||||
return v30.DatabaseAtV30(db);
|
return v30.DatabaseAtV30(db);
|
||||||
|
case 31:
|
||||||
|
return v31.DatabaseAtV31(db);
|
||||||
default:
|
default:
|
||||||
throw MissingSchemaException(version, versions);
|
throw MissingSchemaException(version, versions);
|
||||||
}
|
}
|
||||||
@@ -135,5 +138,6 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
|||||||
28,
|
28,
|
||||||
29,
|
29,
|
||||||
30,
|
30,
|
||||||
|
31,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
+10032
File diff suppressed because it is too large
Load Diff
@@ -70,4 +70,36 @@ void main() {
|
|||||||
expect((assets.first as RemoteAsset).id, asset.id);
|
expect((assets.first as RemoteAsset).id, asset.id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('live photos', () {
|
||||||
|
test('remote-only live photo contains livePhotoVideoId and is marked as a motion photo', () async {
|
||||||
|
final user = await ctx.newUser();
|
||||||
|
final asset = await ctx.newRemoteAsset(ownerId: user.id, livePhotoVideoId: 'motion-photo-1');
|
||||||
|
|
||||||
|
final assets = await sut.main([user.id], .day).assetSource(0, 10);
|
||||||
|
|
||||||
|
expect(assets, hasLength(1));
|
||||||
|
final remote = assets.single as RemoteAsset;
|
||||||
|
expect(remote.id, asset.id);
|
||||||
|
expect(remote.livePhotoVideoId, 'motion-photo-1');
|
||||||
|
expect(remote.isMotionPhoto, isTrue);
|
||||||
|
expect(remote.localId, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('merged live photo resolves localId and is marked as a motion photo', () async {
|
||||||
|
final user = await ctx.newUser();
|
||||||
|
const checksum = 'shared-live-photo-checksum';
|
||||||
|
final asset = await ctx.newRemoteAsset(ownerId: user.id, checksum: checksum, livePhotoVideoId: 'motion-photo-2');
|
||||||
|
final local = await ctx.newLocalAsset(checksum: checksum);
|
||||||
|
|
||||||
|
final assets = await sut.main([user.id], .day).assetSource(0, 10);
|
||||||
|
|
||||||
|
expect(assets, hasLength(1));
|
||||||
|
final remote = assets.single as RemoteAsset;
|
||||||
|
expect(remote.id, asset.id);
|
||||||
|
expect(remote.livePhotoVideoId, 'motion-photo-2');
|
||||||
|
expect(remote.isMotionPhoto, isTrue);
|
||||||
|
expect(remote.localId, local.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ void main() {
|
|||||||
expect(task, isNotNull);
|
expect(task, isNotNull);
|
||||||
expect(task!.fields['filename'], equals('OriginalLivePhoto.HEIC'));
|
expect(task!.fields['filename'], equals('OriginalLivePhoto.HEIC'));
|
||||||
expect(task.fields['livePhotoVideoId'], equals('video-id-123'));
|
expect(task.fields['livePhotoVideoId'], equals('video-id-123'));
|
||||||
|
expect(task.fields['visibility'], equals('hidden'));
|
||||||
verify(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).called(1);
|
verify(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -333,6 +334,7 @@ void main() {
|
|||||||
expect(task, isNotNull);
|
expect(task, isNotNull);
|
||||||
expect(task!.fields.containsKey('metadata'), isTrue);
|
expect(task!.fields.containsKey('metadata'), isTrue);
|
||||||
expect(task.fields['livePhotoVideoId'], equals('video-123'));
|
expect(task.fields['livePhotoVideoId'], equals('video-123'));
|
||||||
|
expect(task.fields['visibility'], equals('hidden'));
|
||||||
|
|
||||||
final metadata = jsonDecode(task.fields['metadata']!) as List;
|
final metadata = jsonDecode(task.fields['metadata']!) as List;
|
||||||
expect(metadata, hasLength(1));
|
expect(metadata, hasLength(1));
|
||||||
|
|||||||
@@ -25917,9 +25917,10 @@
|
|||||||
},
|
},
|
||||||
"percentageLimit": {
|
"percentageLimit": {
|
||||||
"description": "Percentage limit of the integrity checksum job",
|
"description": "Percentage limit of the integrity checksum job",
|
||||||
"maximum": 9007199254740991,
|
"format": "double",
|
||||||
|
"maximum": 1,
|
||||||
"minimum": 0,
|
"minimum": 0,
|
||||||
"type": "integer"
|
"type": "number"
|
||||||
},
|
},
|
||||||
"timeLimit": {
|
"timeLimit": {
|
||||||
"description": "How long the integrity checksum job may run for",
|
"description": "How long the integrity checksum job may run for",
|
||||||
|
|||||||
@@ -151,14 +151,14 @@ const methods = wrapper<Manifest>({
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
webhook: ({ config, data, functions, type, trigger }) => {
|
webhook: ({ config, data, functions, type, trigger }) => {
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {};
|
||||||
'Content-Type': 'application/json',
|
|
||||||
};
|
|
||||||
|
|
||||||
if (config.headerName && config.headerValue) {
|
if (config.headerName && config.headerValue) {
|
||||||
headers[config.headerName] = config.headerValue;
|
headers[config.headerName] = config.headerValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
headers['Content-Type'] = 'application/json';
|
||||||
|
|
||||||
functions.httpRequest(config.url, {
|
functions.httpRequest(config.url, {
|
||||||
method: config.method ?? 'POST',
|
method: config.method ?? 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|||||||
@@ -74,7 +74,12 @@ const SystemConfigIntegrityJobSchema = z
|
|||||||
|
|
||||||
const SystemConfigIntegrityChecksumJobSchema = SystemConfigIntegrityJobSchema.extend({
|
const SystemConfigIntegrityChecksumJobSchema = SystemConfigIntegrityJobSchema.extend({
|
||||||
timeLimit: z.int().nonnegative().describe('How long the integrity checksum job may run for'),
|
timeLimit: z.int().nonnegative().describe('How long the integrity checksum job may run for'),
|
||||||
percentageLimit: z.int().nonnegative().describe('Percentage limit of the integrity checksum job'),
|
percentageLimit: z
|
||||||
|
.float32()
|
||||||
|
.nonnegative()
|
||||||
|
.max(1)
|
||||||
|
.describe('Percentage limit of the integrity checksum job')
|
||||||
|
.meta({ format: 'double' }),
|
||||||
})
|
})
|
||||||
.describe('Integrity checksum job config')
|
.describe('Integrity checksum job config')
|
||||||
.meta({ id: 'SystemConfigIntegrityChecksumJob' });
|
.meta({ id: 'SystemConfigIntegrityChecksumJob' });
|
||||||
|
|||||||
@@ -130,10 +130,9 @@ from
|
|||||||
where
|
where
|
||||||
"asset"."deletedAt" is null
|
"asset"."deletedAt" is null
|
||||||
and "asset"."isExternal" = false
|
and "asset"."isExternal" = false
|
||||||
and "integrity_report"."createdAt" >= $2
|
and "asset"."createdAt" >= $2
|
||||||
and "integrity_report"."createdAt" <= $3
|
|
||||||
order by
|
order by
|
||||||
"integrity_report"."createdAt" asc
|
"asset"."createdAt" asc
|
||||||
|
|
||||||
-- IntegrityRepository.streamIntegrityReports
|
-- IntegrityRepository.streamIntegrityReports
|
||||||
select
|
select
|
||||||
|
|||||||
@@ -160,8 +160,8 @@ export class IntegrityRepository {
|
|||||||
>;
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.DATE, DummyValue.DATE], stream: true })
|
@GenerateSql({ params: [DummyValue.DATE], stream: true })
|
||||||
streamAssetChecksums(startMarker?: Date, endMarker?: Date) {
|
streamAssetChecksums(startMarker?: Date) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('asset')
|
.selectFrom('asset')
|
||||||
.where('asset.deletedAt', 'is', null)
|
.where('asset.deletedAt', 'is', null)
|
||||||
@@ -178,9 +178,8 @@ export class IntegrityRepository {
|
|||||||
'integrity_report.id as reportId',
|
'integrity_report.id as reportId',
|
||||||
])
|
])
|
||||||
.where('asset.isExternal', '=', sql.lit(false))
|
.where('asset.isExternal', '=', sql.lit(false))
|
||||||
.$if(startMarker !== undefined, (qb) => qb.where('integrity_report.createdAt', '>=', startMarker!))
|
.$if(startMarker !== undefined, (qb) => qb.where('asset.createdAt', '>=', startMarker!))
|
||||||
.$if(endMarker !== undefined, (qb) => qb.where('integrity_report.createdAt', '<=', endMarker!))
|
.orderBy('asset.createdAt', 'asc')
|
||||||
.orderBy('integrity_report.createdAt', 'asc')
|
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,9 @@ import { ExifDateTime, exiftool, WriteTags } from 'exiftool-vendored';
|
|||||||
import ffmpeg, { FfprobeData, FfprobeStream } from 'fluent-ffmpeg';
|
import ffmpeg, { FfprobeData, FfprobeStream } from 'fluent-ffmpeg';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { Duration } from 'luxon';
|
import { Duration } from 'luxon';
|
||||||
import { execFile as execFileCb } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import { Writable } from 'node:stream';
|
import { Writable } from 'node:stream';
|
||||||
import { promisify } from 'node:util';
|
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
import { ORIENTATION_TO_SHARP_ROTATION } from 'src/constants';
|
import { ORIENTATION_TO_SHARP_ROTATION } from 'src/constants';
|
||||||
import { Exif } from 'src/database';
|
import { Exif } from 'src/database';
|
||||||
@@ -44,8 +43,6 @@ const probe = (input: string, options: string[]): Promise<FfprobeData> =>
|
|||||||
ffmpeg.ffprobe(input, options, (error, data) => (error ? reject(error) : resolve(data))),
|
ffmpeg.ffprobe(input, options, (error, data) => (error ? reject(error) : resolve(data))),
|
||||||
);
|
);
|
||||||
|
|
||||||
const execFile = promisify(execFileCb);
|
|
||||||
|
|
||||||
sharp.concurrency(0);
|
sharp.concurrency(0);
|
||||||
sharp.cache({ files: 0 });
|
sharp.cache({ files: 0 });
|
||||||
|
|
||||||
@@ -291,33 +288,37 @@ export class MediaRepository {
|
|||||||
* Needed for accurate segments, especially when remuxing, seeking and/or VFR is involved.
|
* Needed for accurate segments, especially when remuxing, seeking and/or VFR is involved.
|
||||||
* Scanning packets for keyframes in JS is much faster than -skip_frame nokey since it avoids decoding the video.
|
* Scanning packets for keyframes in JS is much faster than -skip_frame nokey since it avoids decoding the video.
|
||||||
*/
|
*/
|
||||||
async probePackets(input: string, streamIndex: number): Promise<VideoPacketInfo | null> {
|
probePackets(input: string, streamIndex: number): Promise<VideoPacketInfo | null> {
|
||||||
const { stdout } = await execFile('ffprobe', [
|
const ffprobe = spawn(
|
||||||
'-v',
|
'ffprobe',
|
||||||
'error',
|
[
|
||||||
'-select_streams',
|
'-v',
|
||||||
String(streamIndex),
|
'error',
|
||||||
'-show_entries',
|
'-select_streams',
|
||||||
'packet=pts,duration,flags',
|
String(streamIndex),
|
||||||
'-of',
|
'-show_entries',
|
||||||
'csv=p=0',
|
'packet=pts,duration,flags',
|
||||||
input,
|
'-of',
|
||||||
]);
|
'csv=p=0',
|
||||||
|
input,
|
||||||
|
],
|
||||||
|
{ stdio: ['ignore', 'pipe', 'pipe'] },
|
||||||
|
);
|
||||||
|
|
||||||
let totalDuration = 0;
|
let totalDuration = 0;
|
||||||
const keyframePts: number[] = [];
|
const keyframePts: number[] = [];
|
||||||
const keyframeAccDuration: number[] = [];
|
const keyframeAccDuration: number[] = [];
|
||||||
const keyframeOwnDuration: number[] = [];
|
const keyframeOwnDuration: number[] = [];
|
||||||
const postDiscard: { pts: number; duration: number }[] = [];
|
const postDiscard: { pts: number; duration: number }[] = [];
|
||||||
for (const line of stdout.split('\n')) {
|
const parseLine = (line: string) => {
|
||||||
if (!line) {
|
if (!line) {
|
||||||
continue;
|
return;
|
||||||
}
|
}
|
||||||
const [ptsStr, durationStr, flags] = line.split(',');
|
const [ptsStr, durationStr, flags] = line.split(',');
|
||||||
const pts = Number.parseInt(ptsStr);
|
const pts = Number.parseInt(ptsStr);
|
||||||
const duration = Number.parseInt(durationStr);
|
const duration = Number.parseInt(durationStr);
|
||||||
if (Number.isNaN(pts) || Number.isNaN(duration)) {
|
if (Number.isNaN(pts) || Number.isNaN(duration) || !flags) {
|
||||||
continue;
|
return;
|
||||||
}
|
}
|
||||||
// Discarded packets don't contribute to packet count, but still contribute to video duration
|
// Discarded packets don't contribute to packet count, but still contribute to video duration
|
||||||
totalDuration += duration;
|
totalDuration += duration;
|
||||||
@@ -332,20 +333,43 @@ export class MediaRepository {
|
|||||||
// Non-keyframes are accounted for in totalDuration.
|
// Non-keyframes are accounted for in totalDuration.
|
||||||
keyframeOwnDuration.push(duration);
|
keyframeOwnDuration.push(duration);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (postDiscard.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
totalDuration,
|
|
||||||
packetCount: postDiscard.length,
|
|
||||||
outputFrames: this.cfrOutputFrames(postDiscard, postDiscard.length / totalDuration),
|
|
||||||
keyframePts,
|
|
||||||
keyframeAccDuration,
|
|
||||||
keyframeOwnDuration,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let stderr = '';
|
||||||
|
let remainder = '';
|
||||||
|
ffprobe.stderr.setEncoding('utf8');
|
||||||
|
ffprobe.stderr.on('data', (chunk: string) => (stderr += chunk));
|
||||||
|
ffprobe.stdout.setEncoding('utf8');
|
||||||
|
ffprobe.stdout.on('data', (chunk: string) => {
|
||||||
|
const lines = chunk.split('\n');
|
||||||
|
lines[0] = remainder + lines[0];
|
||||||
|
remainder = lines.pop() as string;
|
||||||
|
for (const line of lines) {
|
||||||
|
parseLine(line);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise<VideoPacketInfo | null>((resolve, reject) => {
|
||||||
|
ffprobe.on('error', reject);
|
||||||
|
ffprobe.on('close', (code) => {
|
||||||
|
if (code !== 0) {
|
||||||
|
return reject(new Error(`ffprobe exited with code ${code}: ${stderr.trim()}`));
|
||||||
|
}
|
||||||
|
parseLine(remainder);
|
||||||
|
if (postDiscard.length === 0) {
|
||||||
|
return resolve(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
totalDuration,
|
||||||
|
packetCount: postDiscard.length,
|
||||||
|
outputFrames: this.cfrOutputFrames(postDiscard, postDiscard.length / totalDuration),
|
||||||
|
keyframePts,
|
||||||
|
keyframeAccDuration,
|
||||||
|
keyframeOwnDuration,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
transcode(input: string, output: string | Writable, options: TranscodeCommand): Promise<void> {
|
transcode(input: string, output: string | Writable, options: TranscodeCommand): Promise<void> {
|
||||||
|
|||||||
@@ -426,7 +426,7 @@ export class AuthService extends BaseService {
|
|||||||
throw new BadRequestException('This OAuth account has already been linked to another user.');
|
throw new BadRequestException('This OAuth account has already been linked to another user.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (auth.session) {
|
if (auth.session && sid) {
|
||||||
await this.sessionRepository.update(auth.session.id, { oauthSid: sid });
|
await this.sessionRepository.update(auth.session.id, { oauthSid: sid });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -407,7 +407,7 @@ export class DatabaseBackupService {
|
|||||||
progressCb?.('restore', progress);
|
progressCb?.('restore', progress);
|
||||||
});
|
});
|
||||||
|
|
||||||
await pipeline(sqlStream, progressSource, psql, progressSink);
|
await pipeline(sqlStream, createSqlOwnerTransformStream(databaseUsername), progressSource, psql, progressSink);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
progressCb?.('migrations', 0.9);
|
progressCb?.('migrations', 0.9);
|
||||||
@@ -572,3 +572,69 @@ function createSqlProgressStreams(cb: (progress: number) => void) {
|
|||||||
|
|
||||||
return [source, sink];
|
return [source, sink];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createSqlOwnerTransformStream(databaseUsername: string) {
|
||||||
|
const OWNER_MARKER_START = new TextEncoder().encode('OWNER TO ');
|
||||||
|
const DATA_MARKER_START = new TextEncoder().encode('FROM stdin');
|
||||||
|
const LINE_END = new TextEncoder().encode(';');
|
||||||
|
|
||||||
|
const owner = new TextEncoder().encode(databaseUsername);
|
||||||
|
|
||||||
|
let ownerSequenceIndex = 0;
|
||||||
|
|
||||||
|
let replacingOwnerIndex = 0;
|
||||||
|
let replacingOwner = false;
|
||||||
|
|
||||||
|
let readingDataIndex = 0;
|
||||||
|
|
||||||
|
let dataPart = false;
|
||||||
|
|
||||||
|
return new PassThrough({
|
||||||
|
transform(chunk, _encoding, callback) {
|
||||||
|
let result = chunk;
|
||||||
|
if (!dataPart) {
|
||||||
|
for (let index = 0; index < result.length; index++) {
|
||||||
|
if (replacingOwner) {
|
||||||
|
if (result[index] === LINE_END[0]) {
|
||||||
|
result = Buffer.concat([result.slice(0, index), owner.slice(replacingOwnerIndex), result.slice(index)]);
|
||||||
|
replacingOwnerIndex = owner.length;
|
||||||
|
} else {
|
||||||
|
result[index] = owner[replacingOwnerIndex];
|
||||||
|
replacingOwnerIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (replacingOwnerIndex === owner.length) {
|
||||||
|
replacingOwner = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result[index] === OWNER_MARKER_START[ownerSequenceIndex]) {
|
||||||
|
ownerSequenceIndex++;
|
||||||
|
} else {
|
||||||
|
ownerSequenceIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ownerSequenceIndex === OWNER_MARKER_START.length) {
|
||||||
|
ownerSequenceIndex = 0;
|
||||||
|
replacingOwner = true;
|
||||||
|
replacingOwnerIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result[index] === DATA_MARKER_START[readingDataIndex]) {
|
||||||
|
readingDataIndex++;
|
||||||
|
} else {
|
||||||
|
readingDataIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (readingDataIndex === DATA_MARKER_START.length) {
|
||||||
|
dataPart = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.push(result);
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -532,8 +532,7 @@ export class IntegrityService extends BaseService {
|
|||||||
const { count } = await this.integrityRepository.getAssetCount();
|
const { count } = await this.integrityRepository.getAssetCount();
|
||||||
const checkpoint = await this.systemMetadataRepository.get(SystemMetadataKey.IntegrityChecksumCheckpoint);
|
const checkpoint = await this.systemMetadataRepository.get(SystemMetadataKey.IntegrityChecksumCheckpoint);
|
||||||
|
|
||||||
let startMarker: Date | undefined = checkpoint?.date ? new Date(checkpoint.date) : undefined;
|
const startMarker = checkpoint?.date ? new Date(checkpoint.date) : undefined;
|
||||||
let endMarker: Date | undefined;
|
|
||||||
|
|
||||||
const printStats = () => {
|
const printStats = () => {
|
||||||
const averageTime = ((Date.now() - startedAt) / processed).toFixed(2);
|
const averageTime = ((Date.now() - startedAt) / processed).toFixed(2);
|
||||||
@@ -546,31 +545,25 @@ export class IntegrityService extends BaseService {
|
|||||||
|
|
||||||
let lastCreatedAt: Date | undefined;
|
let lastCreatedAt: Date | undefined;
|
||||||
|
|
||||||
finishEarly: do {
|
this.logger.log(`Processing assets from ${startMarker?.toISOString() ?? 'beginning'}`);
|
||||||
this.logger.log(
|
|
||||||
`Processing assets in range [${startMarker?.toISOString() ?? 'beginning'}, ${endMarker?.toISOString() ?? 'end'}]`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const assets = this.integrityRepository.streamAssetChecksums(startMarker, endMarker);
|
const assets = this.integrityRepository.streamAssetChecksums(startMarker);
|
||||||
endMarker = startMarker;
|
|
||||||
startMarker = undefined;
|
|
||||||
|
|
||||||
for await (const { originalPath, checksum, createdAt, assetId, reportId } of assets) {
|
for await (const { originalPath, checksum, createdAt, assetId, reportId } of assets) {
|
||||||
await this.checkAssetChecksum(originalPath, checksum, assetId, reportId);
|
await this.checkAssetChecksum(originalPath, checksum, assetId, reportId);
|
||||||
|
|
||||||
processed++;
|
processed++;
|
||||||
|
|
||||||
if (processed % 100 === 0) {
|
if (processed % 100 === 0) {
|
||||||
printStats();
|
printStats();
|
||||||
}
|
|
||||||
|
|
||||||
if (Date.now() > startedAt + timeLimit || processed > count * percentageLimit) {
|
|
||||||
this.logger.log('Reached stop criteria.');
|
|
||||||
lastCreatedAt = createdAt;
|
|
||||||
break finishEarly;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} while (endMarker);
|
|
||||||
|
if (Date.now() > startedAt + timeLimit || processed > count * percentageLimit) {
|
||||||
|
this.logger.log('Reached stop criteria.');
|
||||||
|
lastCreatedAt = createdAt;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await this.systemMetadataRepository.set(SystemMetadataKey.IntegrityChecksumCheckpoint, {
|
await this.systemMetadataRepository.set(SystemMetadataKey.IntegrityChecksumCheckpoint, {
|
||||||
date: lastCreatedAt?.toISOString(),
|
date: lastCreatedAt?.toISOString(),
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { Kysely } from 'kysely';
|
import { Kysely } from 'kysely';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
import { createHash, randomUUID } from 'node:crypto';
|
import { createHash, randomUUID } from 'node:crypto';
|
||||||
import { Readable } from 'node:stream';
|
import { Readable } from 'node:stream';
|
||||||
import { text } from 'node:stream/consumers';
|
import { text } from 'node:stream/consumers';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { AssetFileType, IntegrityReport, JobName, JobStatus } from 'src/enum';
|
import { AssetFileType, IntegrityReport, JobName, JobStatus, SystemMetadataKey } from 'src/enum';
|
||||||
import { AssetRepository } from 'src/repositories/asset.repository';
|
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||||
import { EventRepository } from 'src/repositories/event.repository';
|
import { EventRepository } from 'src/repositories/event.repository';
|
||||||
@@ -702,6 +703,30 @@ describe(IntegrityService.name, () => {
|
|||||||
ctx.get(IntegrityRepository).getIntegrityReport({ limit: 100 }, IntegrityReport.ChecksumFail),
|
ctx.get(IntegrityRepository).getIntegrityReport({ limit: 100 }, IntegrityReport.ChecksumFail),
|
||||||
).resolves.toEqual({ items: [], nextCursor: undefined });
|
).resolves.toEqual({ items: [], nextCursor: undefined });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should continue from checkpoint', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const job = ctx.getMock(JobRepository);
|
||||||
|
job.queue.mockResolvedValue();
|
||||||
|
|
||||||
|
const { user } = await ctx.newUser();
|
||||||
|
|
||||||
|
await ctx.newAsset({
|
||||||
|
ownerId: user.id,
|
||||||
|
createdAt: DateTime.now().minus({ days: 1 }).toISO(),
|
||||||
|
originalPath: '/foo/bar',
|
||||||
|
});
|
||||||
|
await ctx.newAsset({ ownerId: user.id, originalPath: '/foo/baz' });
|
||||||
|
|
||||||
|
await ctx
|
||||||
|
.get(SystemMetadataRepository)
|
||||||
|
.set(SystemMetadataKey.IntegrityChecksumCheckpoint, { date: DateTime.now().minus({ minutes: 5 }).toISO() });
|
||||||
|
|
||||||
|
await sut.handleChecksumFiles({ refreshOnly: false });
|
||||||
|
|
||||||
|
expect(ctx.getMock(StorageRepository).createPlainReadStream).not.toHaveBeenCalledWith('/foo/bar');
|
||||||
|
expect(ctx.getMock(StorageRepository).createPlainReadStream).toHaveBeenCalledWith('/foo/baz');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('handleChecksumRefresh', () => {
|
describe('handleChecksumRefresh', () => {
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ import {
|
|||||||
fromTimelinePlainDate,
|
fromTimelinePlainDate,
|
||||||
fromTimelinePlainDateTime,
|
fromTimelinePlainDateTime,
|
||||||
fromTimelinePlainYearMonth,
|
fromTimelinePlainYearMonth,
|
||||||
fromISODateTimeUTCToObject,
|
|
||||||
getTimes,
|
getTimes,
|
||||||
setDifference,
|
setDifference,
|
||||||
type TimelineDateTime,
|
type TimelineDateTime,
|
||||||
type TimelineYearMonth,
|
type TimelineYearMonth,
|
||||||
getOrderingDate,
|
getOrderingDate,
|
||||||
|
fromISODateTimeUTC,
|
||||||
} from '$lib/utils/timeline-util';
|
} from '$lib/utils/timeline-util';
|
||||||
import { GroupInsertionCache } from './group-insertion-cache.svelte';
|
import { GroupInsertionCache } from './group-insertion-cache.svelte';
|
||||||
import { TimelineDay } from './timeline-day.svelte';
|
import { TimelineDay } from './timeline-day.svelte';
|
||||||
@@ -190,7 +190,7 @@ export class TimelineMonth {
|
|||||||
isVideo: !bucketAssets.isImage[i],
|
isVideo: !bucketAssets.isImage[i],
|
||||||
livePhotoVideoId: bucketAssets.livePhotoVideoId[i],
|
livePhotoVideoId: bucketAssets.livePhotoVideoId[i],
|
||||||
localDateTime,
|
localDateTime,
|
||||||
createdAt: fromISODateTimeUTCToObject(bucketAssets.createdAt[i]),
|
createdAt: fromISODateTimeUTC(bucketAssets.createdAt[i]).toLocal().toObject(),
|
||||||
fileCreatedAt,
|
fileCreatedAt,
|
||||||
ownerId: bucketAssets.ownerId[i],
|
ownerId: bucketAssets.ownerId[i],
|
||||||
projectionType: bucketAssets.projectionType[i],
|
projectionType: bucketAssets.projectionType[i],
|
||||||
|
|||||||
Reference in New Issue
Block a user