mirror of
https://github.com/immich-app/immich.git
synced 2026-07-08 13:27:46 -07:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 49714a3b30 |
@@ -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@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.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@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.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@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.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@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
|
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||||
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@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
|
uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||||
|
|
||||||
# ℹ️ 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@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
|
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||||
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@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.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@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.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 with a single Docker Compose command.
|
All the services are packaged to run as with 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 with a 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 - `mise x -- pnpm i`
|
4. Install dependencies - `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`
|
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`
|
||||||
|
|
||||||
**Notes:**
|
**Notes:**
|
||||||
|
|
||||||
|
|||||||
@@ -1462,6 +1462,7 @@
|
|||||||
"move_to_lock_folder_action_prompt": "{count} added to the locked folder",
|
"move_to_lock_folder_action_prompt": "{count} added to the locked folder",
|
||||||
"move_to_locked_folder": "Move to locked folder",
|
"move_to_locked_folder": "Move to locked folder",
|
||||||
"move_to_locked_folder_confirmation": "These photos and video will be removed from all albums, and only viewable from the locked folder",
|
"move_to_locked_folder_confirmation": "These photos and video will be removed from all albums, and only viewable from the locked folder",
|
||||||
|
"move_to_locked_folder_local_ios": "These items will be moved to Recently Deleted but still be available on the Immich server",
|
||||||
"moved_to_trash": "Moved to trash",
|
"moved_to_trash": "Moved to trash",
|
||||||
"mute_memories": "Mute Memories",
|
"mute_memories": "Mute Memories",
|
||||||
"my_albums": "My albums",
|
"my_albums": "My albums",
|
||||||
|
|||||||
-3626
File diff suppressed because it is too large
Load Diff
@@ -70,8 +70,7 @@
|
|||||||
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,65 +1,19 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class CropAspectRatio {
|
enum AspectRatioPreset {
|
||||||
final int? numerator;
|
free(ratio: null, label: 'Free', icon: Icons.crop_free_rounded),
|
||||||
final int? denominator;
|
square(ratio: 1.0, label: '1:1', icon: Icons.crop_square_rounded),
|
||||||
|
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 String? customLabel;
|
final double? ratio;
|
||||||
final IconData? icon;
|
final String label;
|
||||||
|
final IconData icon;
|
||||||
|
final bool iconRotated;
|
||||||
|
|
||||||
const CropAspectRatio({this.numerator, this.denominator, this.customLabel, this.icon});
|
const AspectRatioPreset({required this.ratio, required this.label, required this.icon, this.iconRotated = false});
|
||||||
|
|
||||||
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,6 +27,7 @@ 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({
|
||||||
@@ -39,15 +40,32 @@ 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 => playbackStyle == AssetPlaybackStyle.livePhoto;
|
bool get isMotionPhoto => livePhotoVideoId != null;
|
||||||
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) {
|
||||||
@@ -68,7 +86,6 @@ 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,6 +25,7 @@ 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,7 +10,6 @@ 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;
|
||||||
|
|
||||||
@@ -30,7 +29,7 @@ class RemoteAsset extends BaseAsset {
|
|||||||
super.isFavorite = false,
|
super.isFavorite = false,
|
||||||
this.thumbHash,
|
this.thumbHash,
|
||||||
this.visibility = AssetVisibility.timeline,
|
this.visibility = AssetVisibility.timeline,
|
||||||
this.livePhotoVideoId,
|
super.livePhotoVideoId,
|
||||||
this.stackId,
|
this.stackId,
|
||||||
required super.isEdited,
|
required super.isEdited,
|
||||||
this.deletedAt,
|
this.deletedAt,
|
||||||
@@ -39,23 +38,6 @@ 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;
|
||||||
|
|
||||||
@@ -108,7 +90,6 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -122,7 +103,6 @@ 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,10 +26,6 @@ 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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,12 +85,8 @@ class TimelineFactory {
|
|||||||
TimelineService fromAssetsWithBuckets(List<BaseAsset> assets, TimelineOrigin type) =>
|
TimelineService fromAssetsWithBuckets(List<BaseAsset> assets, TimelineOrigin type) =>
|
||||||
TimelineService(_timelineRepository.fromAssetsWithBuckets(assets, type));
|
TimelineService(_timelineRepository.fromAssetsWithBuckets(assets, type));
|
||||||
|
|
||||||
/// Creates a TimelineService for serving geographical map queries, such assets within bounded locations
|
TimelineService map(List<String> userIds, TimelineMapOptions options) =>
|
||||||
TimelineService geographicMap(
|
TimelineService(_timelineRepository.map(userIds, options, groupBy));
|
||||||
List<String> userIds,
|
|
||||||
TimelineMapOptions Function() currentOptions,
|
|
||||||
Stream<TimelineMapOptions> optionsStream,
|
|
||||||
) => TimelineService(_timelineRepository.geographicMap(userIds, currentOptions, optionsStream, groupBy));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class TimelineService {
|
class TimelineService {
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ 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,7 +1779,3 @@ 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 => 31;
|
int get schemaVersion => 30;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration => MigrationStrategy(
|
MigrationStrategy get migration => MigrationStrategy(
|
||||||
@@ -128,209 +128,199 @@ 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');
|
||||||
|
|
||||||
try {
|
await m.runMigrationSteps(
|
||||||
await transaction(
|
from: from,
|
||||||
() => m.runMigrationSteps(
|
to: to,
|
||||||
from: from,
|
steps: migrationSteps(
|
||||||
to: to,
|
from1To2: (m, v2) async {
|
||||||
steps: migrationSteps(
|
for (final entity in v2.entities) {
|
||||||
from1To2: (m, v2) async {
|
await m.drop(entity);
|
||||||
for (final entity in v2.entities) {
|
await m.create(entity);
|
||||||
await m.drop(entity);
|
}
|
||||||
await m.create(entity);
|
},
|
||||||
}
|
from2To3: (m, v3) async {
|
||||||
},
|
// Removed foreign key constraint on stack.primaryAssetId
|
||||||
from2To3: (m, v3) async {
|
await m.alterTable(TableMigration(v3.stackEntity));
|
||||||
// Removed foreign key constraint on stack.primaryAssetId
|
},
|
||||||
await m.alterTable(TableMigration(v3.stackEntity));
|
from3To4: (m, v4) async {
|
||||||
},
|
// Thumbnail path column got removed from person_entity
|
||||||
from3To4: (m, v4) async {
|
await m.alterTable(TableMigration(v4.personEntity));
|
||||||
// Thumbnail path column got removed from person_entity
|
// asset_face_entity is added
|
||||||
await m.alterTable(TableMigration(v4.personEntity));
|
await m.create(v4.assetFaceEntity);
|
||||||
// asset_face_entity is added
|
},
|
||||||
await m.create(v4.assetFaceEntity);
|
from4To5: (m, v5) async {
|
||||||
},
|
await m.alterTable(
|
||||||
from4To5: (m, v5) async {
|
TableMigration(
|
||||||
await m.alterTable(
|
v5.userEntity,
|
||||||
TableMigration(
|
newColumns: [v5.userEntity.hasProfileImage, v5.userEntity.profileChangedAt],
|
||||||
v5.userEntity,
|
columnTransformer: {v5.userEntity.profileChangedAt: currentDateAndTime},
|
||||||
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)
|
||||||
from5To6: (m, v6) async {
|
await customStatement('DROP INDEX IF EXISTS UQ_remote_asset_owner_checksum');
|
||||||
// Drops the (checksum, ownerId) and adds it back as (ownerId, checksum)
|
await m.drop(v6.idxRemoteAssetOwnerChecksum);
|
||||||
await customStatement('DROP INDEX IF EXISTS UQ_remote_asset_owner_checksum');
|
await m.create(v6.idxRemoteAssetOwnerChecksum);
|
||||||
await m.drop(v6.idxRemoteAssetOwnerChecksum);
|
// Adds libraryId to remote_asset_entity
|
||||||
await m.create(v6.idxRemoteAssetOwnerChecksum);
|
await m.addColumn(v6.remoteAssetEntity, v6.remoteAssetEntity.libraryId);
|
||||||
// Adds libraryId to remote_asset_entity
|
await m.drop(v6.uQRemoteAssetsOwnerChecksum);
|
||||||
await m.addColumn(v6.remoteAssetEntity, v6.remoteAssetEntity.libraryId);
|
await m.create(v6.uQRemoteAssetsOwnerChecksum);
|
||||||
await m.drop(v6.uQRemoteAssetsOwnerChecksum);
|
await m.drop(v6.uQRemoteAssetsOwnerLibraryChecksum);
|
||||||
await m.create(v6.uQRemoteAssetsOwnerChecksum);
|
await m.create(v6.uQRemoteAssetsOwnerLibraryChecksum);
|
||||||
await m.drop(v6.uQRemoteAssetsOwnerLibraryChecksum);
|
},
|
||||||
await m.create(v6.uQRemoteAssetsOwnerLibraryChecksum);
|
from6To7: (m, v7) async {
|
||||||
},
|
await m.createIndex(v7.idxLatLng);
|
||||||
from6To7: (m, v7) async {
|
},
|
||||||
await m.createIndex(v7.idxLatLng);
|
from7To8: (m, v8) async {
|
||||||
},
|
await m.create(v8.storeEntity);
|
||||||
from7To8: (m, v8) async {
|
},
|
||||||
await m.create(v8.storeEntity);
|
from8To9: (m, v9) async {
|
||||||
},
|
await m.addColumn(v9.localAlbumEntity, v9.localAlbumEntity.linkedRemoteAlbumId);
|
||||||
from8To9: (m, v9) async {
|
},
|
||||||
await m.addColumn(v9.localAlbumEntity, v9.localAlbumEntity.linkedRemoteAlbumId);
|
from9To10: (m, v10) async {
|
||||||
},
|
await m.createTable(v10.authUserEntity);
|
||||||
from9To10: (m, v10) async {
|
await m.addColumn(v10.userEntity, v10.userEntity.avatarColor);
|
||||||
await m.createTable(v10.authUserEntity);
|
await m.alterTable(TableMigration(v10.userEntity));
|
||||||
await m.addColumn(v10.userEntity, v10.userEntity.avatarColor);
|
},
|
||||||
await m.alterTable(TableMigration(v10.userEntity));
|
from10To11: (m, v11) async {
|
||||||
},
|
await m.addColumn(v11.localAlbumAssetEntity, v11.localAlbumAssetEntity.marker_);
|
||||||
from10To11: (m, v11) async {
|
},
|
||||||
await m.addColumn(v11.localAlbumAssetEntity, v11.localAlbumAssetEntity.marker_);
|
from11To12: (m, v12) async {
|
||||||
},
|
final localToUTCMapping = {
|
||||||
from11To12: (m, v12) async {
|
v12.localAssetEntity: [v12.localAssetEntity.createdAt, v12.localAssetEntity.updatedAt],
|
||||||
final localToUTCMapping = {
|
v12.localAlbumEntity: [v12.localAlbumEntity.updatedAt],
|
||||||
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(
|
RemoteAssetEntityCompanion.custom(durationMs: v23.remoteAssetEntity.durationMs * const Constant(1000)),
|
||||||
durationMs: v23.remoteAssetEntity.durationMs * const Constant(1000),
|
);
|
||||||
),
|
await trashedLocalAssetEntity.update().write(
|
||||||
);
|
TrashedLocalAssetEntityCompanion.custom(
|
||||||
await trashedLocalAssetEntity.update().write(
|
durationMs: v23.trashedLocalAssetEntity.durationMs * const Constant(1000),
|
||||||
TrashedLocalAssetEntityCompanion.custom(
|
),
|
||||||
durationMs: v23.trashedLocalAssetEntity.durationMs * const Constant(1000),
|
);
|
||||||
),
|
},
|
||||||
);
|
from23To24: (m, v24) async {
|
||||||
},
|
await customStatement('DROP INDEX IF EXISTS idx_remote_album_owner_id');
|
||||||
from23To24: (m, v24) async {
|
await m.alterTable(TableMigration(v24.remoteAlbumEntity));
|
||||||
await customStatement('DROP INDEX IF EXISTS idx_remote_album_owner_id');
|
},
|
||||||
await m.alterTable(TableMigration(v24.remoteAlbumEntity));
|
from24To25: (m, v25) async {
|
||||||
},
|
await m.createTable(v25.metadata);
|
||||||
from24To25: (m, v25) async {
|
await customStatement('DROP INDEX IF EXISTS idx_remote_asset_owner_checksum');
|
||||||
await m.createTable(v25.metadata);
|
await customStatement('DROP INDEX IF EXISTS idx_remote_asset_local_date_time_day');
|
||||||
await customStatement('DROP INDEX IF EXISTS idx_remote_asset_owner_checksum');
|
await customStatement('DROP INDEX IF EXISTS idx_remote_asset_local_date_time_month');
|
||||||
await customStatement('DROP INDEX IF EXISTS idx_remote_asset_local_date_time_day');
|
await m.createIndex(v25.idxRemoteAssetOwnerVisibilityDeletedCreated);
|
||||||
await customStatement('DROP INDEX IF EXISTS idx_remote_asset_local_date_time_month');
|
await m.createIndex(v25.idxRemoteExifCity);
|
||||||
await m.createIndex(v25.idxRemoteAssetOwnerVisibilityDeletedCreated);
|
await m.createIndex(v25.idxAssetFaceVisiblePerson);
|
||||||
await m.createIndex(v25.idxRemoteExifCity);
|
},
|
||||||
await m.createIndex(v25.idxAssetFaceVisiblePerson);
|
from25To26: (m, v26) async {
|
||||||
},
|
await m.addColumn(v26.remoteAssetEntity, v26.remoteAssetEntity.uploadedAt);
|
||||||
from25To26: (m, v26) async {
|
},
|
||||||
await m.addColumn(v26.remoteAssetEntity, v26.remoteAssetEntity.uploadedAt);
|
from26To27: (m, v27) async {
|
||||||
},
|
await customStatement('ALTER TABLE metadata RENAME TO settings');
|
||||||
from26To27: (m, v27) async {
|
},
|
||||||
await customStatement('ALTER TABLE metadata RENAME TO settings');
|
from27To28: (m, v28) async {
|
||||||
},
|
await m.createIndex(v28.idxLocalAssetCreatedAt);
|
||||||
from27To28: (m, v28) async {
|
},
|
||||||
await m.createIndex(v28.idxLocalAssetCreatedAt);
|
from28To29: (m, v29) async {
|
||||||
},
|
await m.createTable(v29.assetOcrEntity);
|
||||||
from28To29: (m, v29) async {
|
await m.createIndex(v29.idxAssetOcrAssetId);
|
||||||
await m.createTable(v29.assetOcrEntity);
|
},
|
||||||
await m.createIndex(v29.idxAssetOcrAssetId);
|
from29To30: (m, v30) async {
|
||||||
},
|
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,7 +124,6 @@ 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,592 +15920,6 @@ 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,
|
||||||
@@ -16536,7 +15950,6 @@ 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) {
|
||||||
@@ -16685,11 +16098,6 @@ 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');
|
||||||
}
|
}
|
||||||
@@ -16726,7 +16134,6 @@ 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,
|
||||||
@@ -16758,6 +16165,5 @@ i1.OnUpgrade stepByStep({
|
|||||||
from27To28: from27To28,
|
from27To28: from27To28,
|
||||||
from28To29: from28To29,
|
from28To29: from28To29,
|
||||||
from29To30: from29To30,
|
from29To30: from29To30,
|
||||||
from30To31: from30To31,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ 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');
|
||||||
|
|||||||
@@ -509,21 +509,9 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
return query.map((row) => row.toDto()).get();
|
return query.map((row) => row.toDto()).get();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a geographic map query that can dynamically filter on changing [TimelineMapOptions]
|
TimelineQuery map(List<String> userIds, TimelineMapOptions options, GroupAssetsBy groupBy) => (
|
||||||
/// (most notably the active map bounds)
|
bucketSource: () => _watchMapBucket(userIds, options, groupBy: groupBy),
|
||||||
TimelineQuery geographicMap(
|
assetSource: (offset, count) => _getMapBucketAssets(userIds, options, offset: offset, count: count),
|
||||||
List<String> userIds,
|
|
||||||
TimelineMapOptions Function() currentOptions,
|
|
||||||
Stream<TimelineMapOptions> optionsStream,
|
|
||||||
GroupAssetsBy groupBy,
|
|
||||||
) => (
|
|
||||||
bucketSource: () => Stream.value(currentOptions())
|
|
||||||
.followedBy(optionsStream)
|
|
||||||
.switchMap(
|
|
||||||
// Any error would kill the stream for all options; make sure the stream stays alive
|
|
||||||
(options) => _watchMapBucket(userIds, options, groupBy: groupBy).handleError((_) {}),
|
|
||||||
),
|
|
||||||
assetSource: (offset, count) => _getMapBucketAssets(userIds, currentOptions(), offset: offset, count: count),
|
|
||||||
origin: TimelineOrigin.map,
|
origin: TimelineOrigin.map,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -626,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, sortBy: sortBy),
|
_getRemoteAssets(filter: filter, offset: offset, count: count, joinLocal: joinLocal),
|
||||||
origin: origin,
|
origin: origin,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -663,7 +651,6 @@ 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 =
|
||||||
@@ -676,11 +663,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
])
|
])
|
||||||
..addColumns([_db.localAssetEntity.id])
|
..addColumns([_db.localAssetEntity.id])
|
||||||
..where(filter(_db.remoteAssetEntity))
|
..where(filter(_db.remoteAssetEntity))
|
||||||
..orderBy([
|
..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)])
|
||||||
OrderingTerm.desc(
|
|
||||||
sortBy == SortAssetsBy.uploaded ? _db.remoteAssetEntity.uploadedAt : _db.remoteAssetEntity.createdAt,
|
|
||||||
),
|
|
||||||
])
|
|
||||||
..limit(count, offset: offset);
|
..limit(count, offset: offset);
|
||||||
|
|
||||||
return query
|
return query
|
||||||
@@ -689,7 +672,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(sortBy == SortAssetsBy.uploaded ? row.uploadedAt : row.createdAt)])
|
..orderBy([(row) => OrderingTerm.desc(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,6 +114,7 @@ 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()),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -150,7 +151,6 @@ 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 CropAspectRatio ratio;
|
final AspectRatioPreset ratio;
|
||||||
final bool isSelected;
|
final bool isSelected;
|
||||||
final VoidCallback onPressed;
|
final VoidCallback onPressed;
|
||||||
|
|
||||||
@@ -162,16 +162,15 @@ 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: ratio.ratio != null
|
icon: Transform.rotate(
|
||||||
? _AspectRatioRect(ratio: ratio.ratio!, color: color)
|
angle: ratio.iconRotated ? pi / 2 : 0,
|
||||||
: Icon(ratio.icon, color: color),
|
child: Icon(ratio.icon, color: isSelected ? context.primaryColor : context.themeData.iconTheme.color),
|
||||||
|
),
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
),
|
),
|
||||||
Text(ratio.label, style: context.textTheme.displayMedium),
|
Text(ratio.label, style: context.textTheme.displayMedium),
|
||||||
@@ -180,32 +179,6 @@ 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();
|
||||||
|
|
||||||
@@ -214,16 +187,22 @@ 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: aspectRatioPresets.map((entry) {
|
children: AspectRatioPreset.values.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: editorState.aspectRatio == entry,
|
isSelected: selectedAspectRatio == entry.ratio,
|
||||||
onPressed: () => editorNotifier.setAspectRatio(entry),
|
onPressed: () => editorNotifier.setAspectRatio(entry.ratio),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
@@ -378,22 +357,8 @@ 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, (previous, current) {
|
ref.listen(editorStateProvider, (_, current) {
|
||||||
// Only re-apply the aspect ratio when it changes, otherwise the crop rect will shrink on every rotation
|
cropController.aspectRatio = current.aspectRatio;
|
||||||
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;
|
||||||
@@ -421,9 +386,7 @@ class _EditorPreviewState extends ConsumerState<_EditorPreview> with TickerProvi
|
|||||||
1.0,
|
1.0,
|
||||||
1.0,
|
1.0,
|
||||||
),
|
),
|
||||||
child: AnimatedContainer(
|
child: Container(
|
||||||
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,6 +1,5 @@
|
|||||||
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';
|
||||||
@@ -61,8 +60,13 @@ class EditorProvider extends Notifier<EditorState> {
|
|||||||
state = state.copyWith(crop: crop, hasUnsavedEdits: true);
|
state = state.copyWith(crop: crop, hasUnsavedEdits: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setAspectRatio(CropAspectRatio preset) {
|
void setAspectRatio(double? aspectRatio) {
|
||||||
state = state.copyWith(aspectRatio: preset, hasUnsavedEdits: true);
|
if (aspectRatio != null && state.rotationAngle % 180 != 0) {
|
||||||
|
// 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() {
|
||||||
@@ -72,19 +76,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: CropAspectRatio.free,
|
aspectRatio: null,
|
||||||
hasUnsavedEdits: true,
|
hasUnsavedEdits: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void rotateCCW() {
|
void rotateCCW() {
|
||||||
_animateRotation(state.rotationAngle - 90);
|
_animateRotation(state.rotationAngle - 90);
|
||||||
state = state.copyWith(aspectRatio: state.aspectRatio.flipped, hasUnsavedEdits: true);
|
state = state.copyWith(hasUnsavedEdits: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void rotateCW() {
|
void rotateCW() {
|
||||||
_animateRotation(state.rotationAngle + 90);
|
_animateRotation(state.rotationAngle + 90);
|
||||||
state = state.copyWith(aspectRatio: state.aspectRatio.flipped, hasUnsavedEdits: true);
|
state = state.copyWith(hasUnsavedEdits: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void flipHorizontally() {
|
void flipHorizontally() {
|
||||||
@@ -113,7 +117,7 @@ class EditorState {
|
|||||||
final bool flipHorizontal;
|
final bool flipHorizontal;
|
||||||
final bool flipVertical;
|
final bool flipVertical;
|
||||||
final Rect crop;
|
final Rect crop;
|
||||||
final CropAspectRatio aspectRatio;
|
final double? aspectRatio;
|
||||||
|
|
||||||
final int originalWidth;
|
final int originalWidth;
|
||||||
final int originalHeight;
|
final int originalHeight;
|
||||||
@@ -128,7 +132,7 @@ class EditorState {
|
|||||||
bool? flipHorizontal,
|
bool? flipHorizontal,
|
||||||
bool? flipVertical,
|
bool? flipVertical,
|
||||||
Rect? crop,
|
Rect? crop,
|
||||||
CropAspectRatio? aspectRatio,
|
this.aspectRatio,
|
||||||
int? originalWidth,
|
int? originalWidth,
|
||||||
int? originalHeight,
|
int? originalHeight,
|
||||||
Duration? animationDuration,
|
Duration? animationDuration,
|
||||||
@@ -141,7 +145,6 @@ 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({
|
||||||
@@ -149,7 +152,7 @@ class EditorState {
|
|||||||
int? rotationAngle,
|
int? rotationAngle,
|
||||||
bool? flipHorizontal,
|
bool? flipHorizontal,
|
||||||
bool? flipVertical,
|
bool? flipVertical,
|
||||||
CropAspectRatio? aspectRatio,
|
double? aspectRatio = double.infinity,
|
||||||
int? originalWidth,
|
int? originalWidth,
|
||||||
int? originalHeight,
|
int? originalHeight,
|
||||||
Duration? animationDuration,
|
Duration? animationDuration,
|
||||||
@@ -161,7 +164,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 ?? this.aspectRatio,
|
aspectRatio: aspectRatio == double.infinity ? this.aspectRatio : 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,
|
||||||
|
|||||||
+5
-1
@@ -16,11 +16,15 @@ Future<void> performMoveToLockFolderAction(BuildContext context, WidgetRef ref,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final result = await ref.read(actionProvider.notifier).moveToLockFolder(source, context);
|
||||||
|
if (result == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (source == ActionSource.viewer) {
|
if (source == ActionSource.viewer) {
|
||||||
EventStream.shared.emit(const ViewerReloadAssetEvent());
|
EventStream.shared.emit(const ViewerReloadAssetEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
final result = await ref.read(actionProvider.notifier).moveToLockFolder(source);
|
|
||||||
ref.read(multiSelectProvider.notifier).reset();
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
final successMessage = 'move_to_lock_folder_action_prompt'.t(
|
final successMessage = 'move_to_lock_folder_action_prompt'.t(
|
||||||
|
|||||||
@@ -122,14 +122,9 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to retrieve LocalAsset, falling back to remote if it cannot be found
|
if (videoAsset.hasLocal && videoAsset.livePhotoVideoId == null) {
|
||||||
final localAsset = await _localPlaybackAsset(videoAsset);
|
final id = videoAsset is LocalAsset ? videoAsset.id : (videoAsset as RemoteAsset).localId!;
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
@@ -146,13 +141,14 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final remoteAsset = videoAsset as RemoteAsset;
|
final remoteId = (videoAsset as RemoteAsset).id;
|
||||||
|
|
||||||
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 assetId = remoteAsset.livePhotoVideoId ?? remoteAsset.id;
|
final String videoUrl = videoAsset.livePhotoVideoId != null
|
||||||
final String videoUrl = '$serverEndpoint/assets/$assetId/$postfixUrl';
|
? '$serverEndpoint/assets/${videoAsset.livePhotoVideoId}/$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) {
|
||||||
@@ -161,43 +157,6 @@ 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;
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ class _ScopedMapTimeline extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
// TODO: this causes the timeline to switch to flicker to "loading" state and back. This is both janky and inefficient.
|
||||||
return ProviderScope(
|
return ProviderScope(
|
||||||
overrides: [
|
overrides: [
|
||||||
timelineServiceProvider.overrideWith((ref) {
|
timelineServiceProvider.overrideWith((ref) {
|
||||||
@@ -43,16 +44,13 @@ class _ScopedMapTimeline extends StatelessWidget {
|
|||||||
throw Exception('User must be logged in to access archive');
|
throw Exception('User must be logged in to access archive');
|
||||||
}
|
}
|
||||||
|
|
||||||
final withPartners = ref.watch(mapStateProvider.select((s) => s.withPartners));
|
final users = ref.watch(mapStateProvider).withPartners
|
||||||
final users = withPartners ? ref.watch(timelineUsersProvider).valueOrNull ?? [user.id] : [user.id];
|
? ref.watch(timelineUsersProvider).valueOrNull ?? [user.id]
|
||||||
|
: [user.id];
|
||||||
|
|
||||||
final timelineService = ref
|
final timelineService = ref
|
||||||
.watch(timelineFactoryProvider)
|
.watch(timelineFactoryProvider)
|
||||||
.geographicMap(
|
.map(users, ref.watch(mapStateProvider).toOptions());
|
||||||
users,
|
|
||||||
() => ref.read(mapStateProvider).toOptions(),
|
|
||||||
ref.read(mapStateProvider.notifier).optionsStream,
|
|
||||||
);
|
|
||||||
ref.onDispose(timelineService.dispose);
|
ref.onDispose(timelineService.dispose);
|
||||||
return timelineService;
|
return timelineService;
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -297,7 +297,8 @@ class _AssetTypeIcons extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isLivePhoto = asset.isMotionPhoto;
|
final remoteAsset = asset is RemoteAsset ? asset as RemoteAsset : null;
|
||||||
|
final isLivePhoto = remoteAsset?.livePhotoVideoId != null;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/events.model.dart';
|
import 'package:immich_mobile/domain/models/events.model.dart';
|
||||||
@@ -65,16 +63,11 @@ class MapState {
|
|||||||
class MapStateNotifier extends Notifier<MapState> {
|
class MapStateNotifier extends Notifier<MapState> {
|
||||||
MapStateNotifier();
|
MapStateNotifier();
|
||||||
|
|
||||||
final StreamController<TimelineMapOptions> _optionsController = StreamController.broadcast();
|
|
||||||
|
|
||||||
Stream<TimelineMapOptions> get optionsStream => _optionsController.stream;
|
|
||||||
|
|
||||||
bool setBounds(LatLngBounds bounds) {
|
bool setBounds(LatLngBounds bounds) {
|
||||||
if (state.bounds == bounds) {
|
if (state.bounds == bounds) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
state = state.copyWith(bounds: bounds);
|
state = state.copyWith(bounds: bounds);
|
||||||
_optionsController.add(state.toOptions());
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,14 +82,12 @@ class MapStateNotifier extends Notifier<MapState> {
|
|||||||
void switchFavoriteOnly(bool isFavoriteOnly) {
|
void switchFavoriteOnly(bool isFavoriteOnly) {
|
||||||
ref.read(settingsProvider).write(.mapShowFavoriteOnly, isFavoriteOnly);
|
ref.read(settingsProvider).write(.mapShowFavoriteOnly, isFavoriteOnly);
|
||||||
state = state.copyWith(onlyFavorites: isFavoriteOnly);
|
state = state.copyWith(onlyFavorites: isFavoriteOnly);
|
||||||
_optionsController.add(state.toOptions());
|
|
||||||
EventStream.shared.emit(const MapMarkerReloadEvent());
|
EventStream.shared.emit(const MapMarkerReloadEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
void switchIncludeArchived(bool isIncludeArchived) {
|
void switchIncludeArchived(bool isIncludeArchived) {
|
||||||
ref.read(settingsProvider).write(.mapIncludeArchived, isIncludeArchived);
|
ref.read(settingsProvider).write(.mapIncludeArchived, isIncludeArchived);
|
||||||
state = state.copyWith(includeArchived: isIncludeArchived);
|
state = state.copyWith(includeArchived: isIncludeArchived);
|
||||||
_optionsController.add(state.toOptions());
|
|
||||||
EventStream.shared.emit(const MapMarkerReloadEvent());
|
EventStream.shared.emit(const MapMarkerReloadEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,13 +100,11 @@ class MapStateNotifier extends Notifier<MapState> {
|
|||||||
void setRelativeTime(int relativeDays) {
|
void setRelativeTime(int relativeDays) {
|
||||||
ref.read(settingsProvider).write(.mapRelativeDate, relativeDays);
|
ref.read(settingsProvider).write(.mapRelativeDate, relativeDays);
|
||||||
state = state.copyWith(relativeDays: relativeDays);
|
state = state.copyWith(relativeDays: relativeDays);
|
||||||
_optionsController.add(state.toOptions());
|
|
||||||
EventStream.shared.emit(const MapMarkerReloadEvent());
|
EventStream.shared.emit(const MapMarkerReloadEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MapState build() {
|
MapState build() {
|
||||||
ref.onDispose(_optionsController.close);
|
|
||||||
final mapConfig = ref.read(appConfigProvider.select((config) => config.map));
|
final mapConfig = ref.read(appConfigProvider.select((config) => config.map));
|
||||||
return MapState(
|
return MapState(
|
||||||
themeMode: mapConfig.themeMode,
|
themeMode: mapConfig.themeMode,
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ class _SliverTimeline extends ConsumerStatefulWidget {
|
|||||||
ConsumerState createState() => _SliverTimelineState();
|
ConsumerState createState() => _SliverTimelineState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SliverTimelineState extends ConsumerState<_SliverTimeline> with WidgetsBindingObserver {
|
class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
||||||
late final ScrollController _scrollController;
|
late final ScrollController _scrollController;
|
||||||
StreamSubscription? _eventSubscription;
|
StreamSubscription? _eventSubscription;
|
||||||
|
|
||||||
@@ -153,7 +153,6 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> with WidgetsBi
|
|||||||
@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);
|
||||||
|
|
||||||
@@ -179,14 +178,17 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> with WidgetsBi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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():
|
||||||
@@ -244,24 +246,11 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> with WidgetsBi
|
|||||||
|
|
||||||
@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);
|
||||||
@@ -392,9 +381,6 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> with WidgetsBi
|
|||||||
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(
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.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/services/asset.service.dart';
|
import 'package:immich_mobile/domain/services/asset.service.dart';
|
||||||
import 'package:immich_mobile/domain/services/remote_album.service.dart';
|
import 'package:immich_mobile/domain/services/remote_album.service.dart';
|
||||||
|
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||||
import 'package:immich_mobile/models/download/livephotos_medatada.model.dart';
|
import 'package:immich_mobile/models/download/livephotos_medatada.model.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart';
|
import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart';
|
||||||
@@ -27,6 +28,7 @@ import 'package:immich_mobile/services/download.service.dart';
|
|||||||
import 'package:immich_mobile/services/foreground_upload.service.dart';
|
import 'package:immich_mobile/services/foreground_upload.service.dart';
|
||||||
import 'package:immich_mobile/utils/semver.dart';
|
import 'package:immich_mobile/utils/semver.dart';
|
||||||
import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart';
|
import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
@@ -200,9 +202,24 @@ class ActionNotifier extends Notifier<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ActionResult> moveToLockFolder(ActionSource source) async {
|
Future<ActionResult?> moveToLockFolder(ActionSource source, BuildContext context) async {
|
||||||
final ids = _getOwnedRemoteIdsForSource(source);
|
final ids = _getOwnedRemoteIdsForSource(source);
|
||||||
final localIds = _getLocalIdsForSource(source, ignoreLocalOnly: true);
|
final localIds = _getLocalIdsForSource(source, ignoreLocalOnly: true);
|
||||||
|
|
||||||
|
if (localIds.isNotEmpty) {
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => ConfirmDialog(
|
||||||
|
title: "move_to_locked_folder",
|
||||||
|
content: CurrentPlatform.isAndroid ? "delete_dialog_alert_local" : "move_to_locked_folder_local_ios",
|
||||||
|
ok: "confirm",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (confirmed != true) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await _service.moveToLockFolder(ids, localIds);
|
await _service.moveToLockFolder(ids, localIds);
|
||||||
return ActionResult(count: ids.length, success: true);
|
return ActionResult(count: ids.length, success: true);
|
||||||
|
|||||||
@@ -45,9 +45,9 @@ class AssetMediaRepository {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<String>> deleteAll(List<String> ids) async {
|
Future<List<String>> deleteAll(List<String> ids, {bool trash = true}) async {
|
||||||
if (CurrentPlatform.isAndroid) {
|
if (CurrentPlatform.isAndroid) {
|
||||||
if (await _androidSupportsTrash()) {
|
if (trash && await _androidSupportsTrash()) {
|
||||||
return PhotoManager.editor.android.moveToTrash(
|
return PhotoManager.editor.android.moveToTrash(
|
||||||
ids.map((e) => AssetEntity(id: e, width: 1, height: 1, typeInt: 0)).toList(),
|
ids.map((e) => AssetEntity(id: e, width: 1, height: 1, typeInt: 0)).toList(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -92,9 +92,9 @@ class ActionService {
|
|||||||
await _assetApiRepository.updateVisibility(remoteIds, AssetVisibilityEnum.locked);
|
await _assetApiRepository.updateVisibility(remoteIds, AssetVisibilityEnum.locked);
|
||||||
await _remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.locked);
|
await _remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.locked);
|
||||||
|
|
||||||
// Ask user if they want to delete local copies
|
// Locked assets stay on the server, so permanently delete the local copies instead of trashing them
|
||||||
if (localIds.isNotEmpty) {
|
if (localIds.isNotEmpty) {
|
||||||
await _deleteLocalAssets(localIds);
|
await _deleteLocalAssets(localIds, trash: false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,12 +313,12 @@ class ActionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> _deleteLocalAssets(List<String> localIds) async {
|
Future<int> _deleteLocalAssets(List<String> localIds, {bool trash = true}) async {
|
||||||
final deletedIds = await _assetMediaRepository.deleteAll(localIds);
|
final deletedIds = await _assetMediaRepository.deleteAll(localIds, trash: trash);
|
||||||
if (deletedIds.isEmpty) {
|
if (deletedIds.isEmpty) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) {
|
if (trash && CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) {
|
||||||
await _trashedLocalAssetRepository.applyTrashedAssets(deletedIds);
|
await _trashedLocalAssetRepository.applyTrashedAssets(deletedIds);
|
||||||
} else {
|
} else {
|
||||||
await _localAssetRepository.delete(deletedIds);
|
await _localAssetRepository.delete(deletedIds);
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ 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) {
|
||||||
@@ -336,8 +335,7 @@ class BackgroundUploadService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Visibility hidden on upload to prevent the server from running regular jobs on the live photo asset
|
final fields = {'livePhotoVideoId': livePhotoVideoId};
|
||||||
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' hide AssetVisibility;
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/entities/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,7 +19,6 @@ 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;
|
||||||
|
|
||||||
@@ -339,8 +338,7 @@ class ForegroundUploadService {
|
|||||||
final livePhotoResult = await _uploadRepository.uploadFile(
|
final livePhotoResult = await _uploadRepository.uploadFile(
|
||||||
file: livePhotoFile,
|
file: livePhotoFile,
|
||||||
originalFileName: livePhotoTitle,
|
originalFileName: livePhotoTitle,
|
||||||
// Visibility hidden on upload to prevent the server from running regular jobs on the live photo asset
|
fields: fields,
|
||||||
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: 1
|
/// Maximum value: 9007199254740991
|
||||||
double percentageLimit;
|
int 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<double>(json, r'percentageLimit')!,
|
percentageLimit: mapValueOfType<int>(json, r'percentageLimit')!,
|
||||||
timeLimit: mapValueOfType<int>(json, r'timeLimit')!,
|
timeLimit: mapValueOfType<int>(json, r'timeLimit')!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
-4
@@ -34,7 +34,6 @@ 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
|
||||||
@@ -100,8 +99,6 @@ 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);
|
||||||
}
|
}
|
||||||
@@ -138,6 +135,5 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
|||||||
28,
|
28,
|
||||||
29,
|
29,
|
||||||
30,
|
30,
|
||||||
31,
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
-10032
File diff suppressed because it is too large
Load Diff
@@ -70,36 +70,4 @@ 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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import 'package:drift/drift.dart' as drift;
|
|||||||
import 'package:drift/native.dart';
|
import 'package:drift/native.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
@@ -147,13 +149,13 @@ void main() {
|
|||||||
await Store.put(StoreKey.manageLocalMediaAndroid, true);
|
await Store.put(StoreKey.manageLocalMediaAndroid, true);
|
||||||
const ids = ['a', 'b'];
|
const ids = ['a', 'b'];
|
||||||
|
|
||||||
when(() => assetMediaRepository.deleteAll(ids)).thenAnswer((_) async => ids);
|
when(() => assetMediaRepository.deleteAll(ids, trash: true)).thenAnswer((_) async => ids);
|
||||||
when(() => trashedLocalAssetRepository.applyTrashedAssets(ids)).thenAnswer((_) async {});
|
when(() => trashedLocalAssetRepository.applyTrashedAssets(ids)).thenAnswer((_) async {});
|
||||||
|
|
||||||
final result = await sut.deleteLocal(ids);
|
final result = await sut.deleteLocal(ids);
|
||||||
|
|
||||||
expect(result, ids.length);
|
expect(result, ids.length);
|
||||||
verify(() => assetMediaRepository.deleteAll(ids)).called(1);
|
verify(() => assetMediaRepository.deleteAll(ids, trash: true)).called(1);
|
||||||
verify(() => trashedLocalAssetRepository.applyTrashedAssets(ids)).called(1);
|
verify(() => trashedLocalAssetRepository.applyTrashedAssets(ids)).called(1);
|
||||||
verifyNever(() => localAssetRepository.delete(any()));
|
verifyNever(() => localAssetRepository.delete(any()));
|
||||||
});
|
});
|
||||||
@@ -162,13 +164,13 @@ void main() {
|
|||||||
await Store.put(StoreKey.manageLocalMediaAndroid, false);
|
await Store.put(StoreKey.manageLocalMediaAndroid, false);
|
||||||
const ids = ['c'];
|
const ids = ['c'];
|
||||||
|
|
||||||
when(() => assetMediaRepository.deleteAll(ids)).thenAnswer((_) async => ids);
|
when(() => assetMediaRepository.deleteAll(ids, trash: true)).thenAnswer((_) async => ids);
|
||||||
when(() => localAssetRepository.delete(ids)).thenAnswer((_) async {});
|
when(() => localAssetRepository.delete(ids)).thenAnswer((_) async {});
|
||||||
|
|
||||||
final result = await sut.deleteLocal(ids);
|
final result = await sut.deleteLocal(ids);
|
||||||
|
|
||||||
expect(result, ids.length);
|
expect(result, ids.length);
|
||||||
verify(() => assetMediaRepository.deleteAll(ids)).called(1);
|
verify(() => assetMediaRepository.deleteAll(ids, trash: true)).called(1);
|
||||||
verify(() => localAssetRepository.delete(ids)).called(1);
|
verify(() => localAssetRepository.delete(ids)).called(1);
|
||||||
verifyNever(() => trashedLocalAssetRepository.applyTrashedAssets(any()));
|
verifyNever(() => trashedLocalAssetRepository.applyTrashedAssets(any()));
|
||||||
});
|
});
|
||||||
@@ -177,14 +179,46 @@ void main() {
|
|||||||
await Store.put(StoreKey.manageLocalMediaAndroid, true);
|
await Store.put(StoreKey.manageLocalMediaAndroid, true);
|
||||||
const ids = ['x'];
|
const ids = ['x'];
|
||||||
|
|
||||||
when(() => assetMediaRepository.deleteAll(ids)).thenAnswer((_) async => <String>[]);
|
when(() => assetMediaRepository.deleteAll(ids, trash: true)).thenAnswer((_) async => <String>[]);
|
||||||
|
|
||||||
final result = await sut.deleteLocal(ids);
|
final result = await sut.deleteLocal(ids);
|
||||||
|
|
||||||
expect(result, 0);
|
expect(result, 0);
|
||||||
verify(() => assetMediaRepository.deleteAll(ids)).called(1);
|
verify(() => assetMediaRepository.deleteAll(ids, trash: true)).called(1);
|
||||||
verifyNever(() => trashedLocalAssetRepository.applyTrashedAssets(any()));
|
verifyNever(() => trashedLocalAssetRepository.applyTrashedAssets(any()));
|
||||||
verifyNever(() => localAssetRepository.delete(any()));
|
verifyNever(() => localAssetRepository.delete(any()));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('ActionService.moveToLockFolder', () {
|
||||||
|
const remoteIds = ['r1', 'r2'];
|
||||||
|
const localIds = ['l1', 'l2'];
|
||||||
|
|
||||||
|
test('permanently deletes local copies without trashing, even when Android trash handling is on', () async {
|
||||||
|
await Store.put(StoreKey.manageLocalMediaAndroid, true);
|
||||||
|
|
||||||
|
when(() => assetApiRepository.updateVisibility(remoteIds, AssetVisibilityEnum.locked)).thenAnswer((_) async {});
|
||||||
|
when(() => remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.locked)).thenAnswer((_) async {});
|
||||||
|
when(() => assetMediaRepository.deleteAll(localIds, trash: false)).thenAnswer((_) async => localIds);
|
||||||
|
when(() => localAssetRepository.delete(localIds)).thenAnswer((_) async {});
|
||||||
|
|
||||||
|
await sut.moveToLockFolder(remoteIds, localIds);
|
||||||
|
|
||||||
|
verify(() => assetApiRepository.updateVisibility(remoteIds, AssetVisibilityEnum.locked)).called(1);
|
||||||
|
verify(() => remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.locked)).called(1);
|
||||||
|
verify(() => assetMediaRepository.deleteAll(localIds, trash: false)).called(1);
|
||||||
|
verify(() => localAssetRepository.delete(localIds)).called(1);
|
||||||
|
verifyNever(() => trashedLocalAssetRepository.applyTrashedAssets(any()));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('locks remote assets without touching local media when there are no local copies', () async {
|
||||||
|
when(() => assetApiRepository.updateVisibility(remoteIds, AssetVisibilityEnum.locked)).thenAnswer((_) async {});
|
||||||
|
when(() => remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.locked)).thenAnswer((_) async {});
|
||||||
|
|
||||||
|
await sut.moveToLockFolder(remoteIds, const []);
|
||||||
|
|
||||||
|
verify(() => assetApiRepository.updateVisibility(remoteIds, AssetVisibilityEnum.locked)).called(1);
|
||||||
|
verifyNever(() => assetMediaRepository.deleteAll(any(), trash: any(named: 'trash')));
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,7 +140,6 @@ 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);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -334,7 +333,6 @@ 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,10 +25917,9 @@
|
|||||||
},
|
},
|
||||||
"percentageLimit": {
|
"percentageLimit": {
|
||||||
"description": "Percentage limit of the integrity checksum job",
|
"description": "Percentage limit of the integrity checksum job",
|
||||||
"format": "double",
|
"maximum": 9007199254740991,
|
||||||
"maximum": 1,
|
|
||||||
"minimum": 0,
|
"minimum": 0,
|
||||||
"type": "number"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"timeLimit": {
|
"timeLimit": {
|
||||||
"description": "How long the integrity checksum job may run for",
|
"description": "How long the integrity checksum job may run for",
|
||||||
|
|||||||
@@ -74,12 +74,7 @@ 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
|
percentageLimit: z.int().nonnegative().describe('Percentage limit of the integrity checksum job'),
|
||||||
.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,9 +130,10 @@ from
|
|||||||
where
|
where
|
||||||
"asset"."deletedAt" is null
|
"asset"."deletedAt" is null
|
||||||
and "asset"."isExternal" = false
|
and "asset"."isExternal" = false
|
||||||
and "asset"."createdAt" >= $2
|
and "integrity_report"."createdAt" >= $2
|
||||||
|
and "integrity_report"."createdAt" <= $3
|
||||||
order by
|
order by
|
||||||
"asset"."createdAt" asc
|
"integrity_report"."createdAt" asc
|
||||||
|
|
||||||
-- IntegrityRepository.streamIntegrityReports
|
-- IntegrityRepository.streamIntegrityReports
|
||||||
select
|
select
|
||||||
|
|||||||
@@ -160,8 +160,8 @@ export class IntegrityRepository {
|
|||||||
>;
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.DATE], stream: true })
|
@GenerateSql({ params: [DummyValue.DATE, DummyValue.DATE], stream: true })
|
||||||
streamAssetChecksums(startMarker?: Date) {
|
streamAssetChecksums(startMarker?: Date, endMarker?: Date) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('asset')
|
.selectFrom('asset')
|
||||||
.where('asset.deletedAt', 'is', null)
|
.where('asset.deletedAt', 'is', null)
|
||||||
@@ -178,8 +178,9 @@ 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('asset.createdAt', '>=', startMarker!))
|
.$if(startMarker !== undefined, (qb) => qb.where('integrity_report.createdAt', '>=', startMarker!))
|
||||||
.orderBy('asset.createdAt', 'asc')
|
.$if(endMarker !== undefined, (qb) => qb.where('integrity_report.createdAt', '<=', endMarker!))
|
||||||
|
.orderBy('integrity_report.createdAt', 'asc')
|
||||||
.stream();
|
.stream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 && sid) {
|
if (auth.session) {
|
||||||
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, createSqlOwnerTransformStream(databaseUsername), progressSource, psql, progressSink);
|
await pipeline(sqlStream, progressSource, psql, progressSink);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
progressCb?.('migrations', 0.9);
|
progressCb?.('migrations', 0.9);
|
||||||
@@ -572,69 +572,3 @@ 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,7 +532,8 @@ 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);
|
||||||
|
|
||||||
const startMarker = checkpoint?.date ? new Date(checkpoint.date) : undefined;
|
let startMarker: Date | undefined = 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);
|
||||||
@@ -545,25 +546,31 @@ export class IntegrityService extends BaseService {
|
|||||||
|
|
||||||
let lastCreatedAt: Date | undefined;
|
let lastCreatedAt: Date | undefined;
|
||||||
|
|
||||||
this.logger.log(`Processing assets from ${startMarker?.toISOString() ?? 'beginning'}`);
|
finishEarly: do {
|
||||||
|
this.logger.log(
|
||||||
|
`Processing assets in range [${startMarker?.toISOString() ?? 'beginning'}, ${endMarker?.toISOString() ?? 'end'}]`,
|
||||||
|
);
|
||||||
|
|
||||||
const assets = this.integrityRepository.streamAssetChecksums(startMarker);
|
const assets = this.integrityRepository.streamAssetChecksums(startMarker, endMarker);
|
||||||
|
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,10 +1,9 @@
|
|||||||
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, SystemMetadataKey } from 'src/enum';
|
import { AssetFileType, IntegrityReport, JobName, JobStatus } 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';
|
||||||
@@ -703,30 +702,6 @@ 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: fromISODateTimeUTC(bucketAssets.createdAt[i]).toLocal().toObject(),
|
createdAt: fromISODateTimeUTCToObject(bucketAssets.createdAt[i]),
|
||||||
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