Compare commits

...

4 Commits

30 changed files with 11093 additions and 203 deletions

View File

@@ -7,7 +7,7 @@ sidebar_position: 3
Dev Containers provide a consistent, reproducible development environment using Docker containers. With a single click, you can get started with an Immich development environment on Mac, Linux, Windows, or in the cloud using GitHub Codespaces. Dev Containers provide a consistent, reproducible development environment using Docker containers. With a single click, you can get started with an Immich development environment on Mac, Linux, Windows, or in the cloud using GitHub Codespaces.
[![Open in VSCode Containers](https://img.shields.io/static/v1?label=VSCode%20DevContainer&message=Immich&color=blue)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/immich-app/immich/) Get started fast!
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/immich-app/immich/) [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/immich-app/immich/)
@@ -71,7 +71,7 @@ cd immich
The immich dev containers read environment variables from your shell environment, not from `.env` files. This allows them to work in cloud environments without pre-configuration. The immich dev containers read environment variables from your shell environment, not from `.env` files. This allows them to work in cloud environments without pre-configuration.
:::important Required Configuration :::important Configuration
When running locally, and if you want to create (or use an existing) DB and/or photo storage folder, you must set the `UPLOAD_LOCATION` variable in your shell environment before launching the Dev Container. This determines where uploaded files are stored and also where the DB stores it data. When running locally, and if you want to create (or use an existing) DB and/or photo storage folder, you must set the `UPLOAD_LOCATION` variable in your shell environment before launching the Dev Container. This determines where uploaded files are stored and also where the DB stores it data.
```bash ```bash
@@ -88,6 +88,10 @@ source ~/.bashrc
### Step 3: Launch the Dev Container ### Step 3: Launch the Dev Container
:::tip
Immich development makes extensive use of specialized [base images](https://github.com/immich-app/base-images) for its docker-compose based development. For this reason, you won't be able to use VSCode's **_Clone Repository in a Container Volume_** command.
:::
#### Using VS Code UI: #### Using VS Code UI:
1. Open the cloned repository in VS Code 1. Open the cloned repository in VS Code

View File

@@ -373,10 +373,12 @@
"admin_password": "Admin Password", "admin_password": "Admin Password",
"administration": "Administration", "administration": "Administration",
"advanced": "Advanced", "advanced": "Advanced",
"advanced_settings_beta_timeline_subtitle": "Try the new app experience.",
"advanced_settings_beta_timeline_title": "Beta Timeline",
"advanced_settings_enable_alternate_media_filter_subtitle": "Use this option to filter media during sync based on alternate criteria. Only try this if you have issues with the app detecting all albums.", "advanced_settings_enable_alternate_media_filter_subtitle": "Use this option to filter media during sync based on alternate criteria. Only try this if you have issues with the app detecting all albums.",
"advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTAL] Use alternate device album sync filter", "advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTAL] Use alternate device album sync filter",
"advanced_settings_log_level_title": "Log level: {level}", "advanced_settings_log_level_title": "Log level: {level}",
"advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from assets on the device. Activate this setting to load remote images instead.", "advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from local assets. Activate this setting to load remote images instead.",
"advanced_settings_prefer_remote_title": "Prefer remote images", "advanced_settings_prefer_remote_title": "Prefer remote images",
"advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request", "advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request",
"advanced_settings_proxy_headers_title": "Proxy Headers", "advanced_settings_proxy_headers_title": "Proxy Headers",

File diff suppressed because one or more lines are too long

View File

@@ -68,7 +68,9 @@ enum StoreKey<T> {
manageLocalMediaAndroid<bool>._(137), manageLocalMediaAndroid<bool>._(137),
// Experimental stuff // Experimental stuff
photoManagerCustomFilter<bool>._(1000); photoManagerCustomFilter<bool>._(1000),
betaPromptShown<bool>._(1001),
betaTimeline<bool>._(1002);
const StoreKey._(this.id); const StoreKey._(this.id);
final int id; final int id;

View File

@@ -93,6 +93,8 @@ class StoreService {
await _storeRepository.deleteAll(); await _storeRepository.deleteAll();
_cache.clear(); _cache.clear();
} }
bool get isBetaTimelineEnabled => tryGet(StoreKey.betaTimeline) ?? false;
} }
class StoreKeyNotFoundException implements Exception { class StoreKeyNotFoundException implements Exception {

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';
import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/setting.model.dart'; import 'package:immich_mobile/domain/models/setting.model.dart';
@@ -112,8 +113,19 @@ class TimelineService {
totalAssets - _bufferOffset, totalAssets - _bufferOffset,
); );
} }
_buffer = await _assetSource(offset, count);
_bufferOffset = offset; try {
_buffer = await _assetSource(offset, count);
_bufferOffset = offset;
} catch (e) {
if (e.toString().contains('database has been locked')) {
debugPrint(
"TimelineService::loadAssets - Database locked, returning cached assets",
);
return;
}
rethrow;
}
} }
// change the state's total assets count only after the buffer is reloaded // change the state's total assets count only after the buffer is reloaded
@@ -153,8 +165,22 @@ class TimelineService {
: (len > kTimelineAssetLoadBatchSize ? index : index + count - len), : (len > kTimelineAssetLoadBatchSize ? index : index + count - len),
); );
_buffer = await _assetSource(start, len); try {
_bufferOffset = start; _buffer = await _assetSource(start, len);
_bufferOffset = start;
} catch (e) {
if (e.toString().contains('database has been locked') &&
_buffer.isNotEmpty) {
debugPrint(
"TimelineService::loadAssets - Database locked, returning cached assets",
);
if (hasRange(index, count)) {
return getAssets(index, count);
}
return <BaseAsset>[];
}
rethrow;
}
return getAssets(index, count); return getAssets(index, count);
} }

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart'; import 'package:drift_flutter/drift_flutter.dart';
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/domain/interfaces/db.interface.dart'; import 'package:immich_mobile/domain/interfaces/db.interface.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
@@ -17,6 +18,7 @@ import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/stack.entity.dart'; import 'package:immich_mobile/infrastructure/entities/stack.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.steps.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import 'db.repository.drift.dart'; import 'db.repository.drift.dart';
@@ -68,10 +70,36 @@ class Drift extends $Drift implements IDatabaseRepository {
); );
@override @override
int get schemaVersion => 1; int get schemaVersion => 2;
@override @override
MigrationStrategy get migration => MigrationStrategy( MigrationStrategy get migration => MigrationStrategy(
onUpgrade: (m, from, to) async {
// Run migration steps without foreign keys and re-enable them later
await customStatement('PRAGMA foreign_keys = OFF');
await m.runMigrationSteps(
from: from,
to: to,
steps: migrationSteps(
from1To2: (m, _) async {
for (final entity in allSchemaEntities) {
await m.drop(entity);
await m.create(entity);
}
},
),
);
if (kDebugMode) {
// Fail if the migration broke foreign keys
final wrongFKs =
await customSelect('PRAGMA foreign_key_check').get();
assert(wrongFKs.isEmpty, '${wrongFKs.map((e) => e.data)}');
}
await customStatement('PRAGMA foreign_keys = ON;');
},
beforeOpen: (details) async { beforeOpen: (details) async {
await customStatement('PRAGMA foreign_keys = ON'); await customStatement('PRAGMA foreign_keys = ON');
await customStatement('PRAGMA synchronous = NORMAL'); await customStatement('PRAGMA synchronous = NORMAL');

View File

@@ -0,0 +1,864 @@
// dart format width=80
import 'package:drift/internal/versioned_schema.dart' as i0;
import 'package:drift/drift.dart' as i1;
import 'dart:typed_data' as i2;
import 'package:drift/drift.dart'; // ignore_for_file: type=lint,unused_import
// GENERATED BY drift_dev, DO NOT MODIFY.
final class Schema2 extends i0.VersionedSchema {
Schema2({required super.database}) : super(version: 2);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
userEntity,
remoteAssetEntity,
localAssetEntity,
idxLocalAssetChecksum,
uQRemoteAssetOwnerChecksum,
idxRemoteAssetChecksum,
userMetadataEntity,
partnerEntity,
localAlbumEntity,
localAlbumAssetEntity,
remoteExifEntity,
remoteAlbumEntity,
remoteAlbumAssetEntity,
remoteAlbumUserEntity,
memoryEntity,
memoryAssetEntity,
stackEntity,
];
late final Shape0 userEntity = Shape0(
source: i0.VersionedTable(
entityName: 'user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: [
'PRIMARY KEY(id)',
],
columns: [
_column_0,
_column_1,
_column_2,
_column_3,
_column_4,
_column_5,
_column_6,
_column_7,
],
attachedDatabase: database,
),
alias: null);
late final Shape1 remoteAssetEntity = Shape1(
source: i0.VersionedTable(
entityName: 'remote_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: [
'PRIMARY KEY(id)',
],
columns: [
_column_1,
_column_8,
_column_9,
_column_5,
_column_10,
_column_11,
_column_12,
_column_0,
_column_13,
_column_14,
_column_15,
_column_16,
_column_17,
_column_18,
_column_19,
_column_20,
],
attachedDatabase: database,
),
alias: null);
late final Shape2 localAssetEntity = Shape2(
source: i0.VersionedTable(
entityName: 'local_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: [
'PRIMARY KEY(id)',
],
columns: [
_column_1,
_column_8,
_column_9,
_column_5,
_column_10,
_column_11,
_column_12,
_column_0,
_column_21,
_column_14,
_column_22,
],
attachedDatabase: database,
),
alias: null);
final i1.Index idxLocalAssetChecksum = i1.Index('idx_local_asset_checksum',
'CREATE INDEX idx_local_asset_checksum ON local_asset_entity (checksum)');
final i1.Index uQRemoteAssetOwnerChecksum = i1.Index(
'UQ_remote_asset_owner_checksum',
'CREATE UNIQUE INDEX UQ_remote_asset_owner_checksum ON remote_asset_entity (checksum, owner_id)');
final i1.Index idxRemoteAssetChecksum = i1.Index('idx_remote_asset_checksum',
'CREATE INDEX idx_remote_asset_checksum ON remote_asset_entity (checksum)');
late final Shape3 userMetadataEntity = Shape3(
source: i0.VersionedTable(
entityName: 'user_metadata_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: [
'PRIMARY KEY(user_id, "key")',
],
columns: [
_column_23,
_column_24,
_column_25,
],
attachedDatabase: database,
),
alias: null);
late final Shape4 partnerEntity = Shape4(
source: i0.VersionedTable(
entityName: 'partner_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: [
'PRIMARY KEY(shared_by_id, shared_with_id)',
],
columns: [
_column_26,
_column_27,
_column_28,
],
attachedDatabase: database,
),
alias: null);
late final Shape5 localAlbumEntity = Shape5(
source: i0.VersionedTable(
entityName: 'local_album_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: [
'PRIMARY KEY(id)',
],
columns: [
_column_0,
_column_1,
_column_5,
_column_29,
_column_30,
_column_31,
],
attachedDatabase: database,
),
alias: null);
late final Shape6 localAlbumAssetEntity = Shape6(
source: i0.VersionedTable(
entityName: 'local_album_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: [
'PRIMARY KEY(asset_id, album_id)',
],
columns: [
_column_32,
_column_33,
],
attachedDatabase: database,
),
alias: null);
late final Shape7 remoteExifEntity = Shape7(
source: i0.VersionedTable(
entityName: 'remote_exif_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: [
'PRIMARY KEY(asset_id)',
],
columns: [
_column_34,
_column_35,
_column_36,
_column_37,
_column_38,
_column_39,
_column_11,
_column_10,
_column_40,
_column_41,
_column_42,
_column_43,
_column_44,
_column_45,
_column_46,
_column_47,
_column_48,
_column_49,
_column_50,
_column_51,
_column_52,
_column_53,
],
attachedDatabase: database,
),
alias: null);
late final Shape8 remoteAlbumEntity = Shape8(
source: i0.VersionedTable(
entityName: 'remote_album_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: [
'PRIMARY KEY(id)',
],
columns: [
_column_0,
_column_1,
_column_54,
_column_9,
_column_5,
_column_15,
_column_55,
_column_56,
_column_57,
],
attachedDatabase: database,
),
alias: null);
late final Shape6 remoteAlbumAssetEntity = Shape6(
source: i0.VersionedTable(
entityName: 'remote_album_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: [
'PRIMARY KEY(asset_id, album_id)',
],
columns: [
_column_34,
_column_58,
],
attachedDatabase: database,
),
alias: null);
late final Shape9 remoteAlbumUserEntity = Shape9(
source: i0.VersionedTable(
entityName: 'remote_album_user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: [
'PRIMARY KEY(album_id, user_id)',
],
columns: [
_column_58,
_column_23,
_column_59,
],
attachedDatabase: database,
),
alias: null);
late final Shape10 memoryEntity = Shape10(
source: i0.VersionedTable(
entityName: 'memory_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: [
'PRIMARY KEY(id)',
],
columns: [
_column_0,
_column_9,
_column_5,
_column_18,
_column_15,
_column_8,
_column_60,
_column_61,
_column_62,
_column_63,
_column_64,
_column_65,
],
attachedDatabase: database,
),
alias: null);
late final Shape11 memoryAssetEntity = Shape11(
source: i0.VersionedTable(
entityName: 'memory_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: [
'PRIMARY KEY(asset_id, memory_id)',
],
columns: [
_column_34,
_column_66,
],
attachedDatabase: database,
),
alias: null);
late final Shape12 stackEntity = Shape12(
source: i0.VersionedTable(
entityName: 'stack_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: [
'PRIMARY KEY(id)',
],
columns: [
_column_0,
_column_9,
_column_5,
_column_15,
_column_67,
],
attachedDatabase: database,
),
alias: null);
}
class Shape0 extends i0.VersionedTable {
Shape0({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get id =>
columnsByName['id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get name =>
columnsByName['name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<bool> get isAdmin =>
columnsByName['is_admin']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<String> get email =>
columnsByName['email']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get profileImagePath =>
columnsByName['profile_image_path']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get updatedAt =>
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<int> get quotaSizeInBytes =>
columnsByName['quota_size_in_bytes']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get quotaUsageInBytes =>
columnsByName['quota_usage_in_bytes']! as i1.GeneratedColumn<int>;
}
i1.GeneratedColumn<String> _column_0(String aliasedName) =>
i1.GeneratedColumn<String>('id', aliasedName, false,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_1(String aliasedName) =>
i1.GeneratedColumn<String>('name', aliasedName, false,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<bool> _column_2(String aliasedName) =>
i1.GeneratedColumn<bool>('is_admin', aliasedName, false,
type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("is_admin" IN (0, 1))'),
defaultValue: const CustomExpression('0'));
i1.GeneratedColumn<String> _column_3(String aliasedName) =>
i1.GeneratedColumn<String>('email', aliasedName, false,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_4(String aliasedName) =>
i1.GeneratedColumn<String>('profile_image_path', aliasedName, true,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<DateTime> _column_5(String aliasedName) =>
i1.GeneratedColumn<DateTime>('updated_at', aliasedName, false,
type: i1.DriftSqlType.dateTime,
defaultValue: const CustomExpression('CURRENT_TIMESTAMP'));
i1.GeneratedColumn<int> _column_6(String aliasedName) =>
i1.GeneratedColumn<int>('quota_size_in_bytes', aliasedName, true,
type: i1.DriftSqlType.int);
i1.GeneratedColumn<int> _column_7(String aliasedName) =>
i1.GeneratedColumn<int>('quota_usage_in_bytes', aliasedName, false,
type: i1.DriftSqlType.int, defaultValue: const CustomExpression('0'));
class Shape1 extends i0.VersionedTable {
Shape1({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get name =>
columnsByName['name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get type =>
columnsByName['type']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<DateTime> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<DateTime> get updatedAt =>
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<int> get width =>
columnsByName['width']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get height =>
columnsByName['height']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get durationInSeconds =>
columnsByName['duration_in_seconds']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get id =>
columnsByName['id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get checksum =>
columnsByName['checksum']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<bool> get isFavorite =>
columnsByName['is_favorite']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<String> get ownerId =>
columnsByName['owner_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get localDateTime =>
columnsByName['local_date_time']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<String> get thumbHash =>
columnsByName['thumb_hash']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get deletedAt =>
columnsByName['deleted_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<String> get livePhotoVideoId =>
columnsByName['live_photo_video_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get visibility =>
columnsByName['visibility']! as i1.GeneratedColumn<int>;
}
i1.GeneratedColumn<int> _column_8(String aliasedName) =>
i1.GeneratedColumn<int>('type', aliasedName, false,
type: i1.DriftSqlType.int);
i1.GeneratedColumn<DateTime> _column_9(String aliasedName) =>
i1.GeneratedColumn<DateTime>('created_at', aliasedName, false,
type: i1.DriftSqlType.dateTime,
defaultValue: const CustomExpression('CURRENT_TIMESTAMP'));
i1.GeneratedColumn<int> _column_10(String aliasedName) =>
i1.GeneratedColumn<int>('width', aliasedName, true,
type: i1.DriftSqlType.int);
i1.GeneratedColumn<int> _column_11(String aliasedName) =>
i1.GeneratedColumn<int>('height', aliasedName, true,
type: i1.DriftSqlType.int);
i1.GeneratedColumn<int> _column_12(String aliasedName) =>
i1.GeneratedColumn<int>('duration_in_seconds', aliasedName, true,
type: i1.DriftSqlType.int);
i1.GeneratedColumn<String> _column_13(String aliasedName) =>
i1.GeneratedColumn<String>('checksum', aliasedName, false,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<bool> _column_14(String aliasedName) =>
i1.GeneratedColumn<bool>('is_favorite', aliasedName, false,
type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("is_favorite" IN (0, 1))'),
defaultValue: const CustomExpression('0'));
i1.GeneratedColumn<String> _column_15(String aliasedName) =>
i1.GeneratedColumn<String>('owner_id', aliasedName, false,
type: i1.DriftSqlType.string,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'REFERENCES user_entity (id) ON DELETE CASCADE'));
i1.GeneratedColumn<DateTime> _column_16(String aliasedName) =>
i1.GeneratedColumn<DateTime>('local_date_time', aliasedName, true,
type: i1.DriftSqlType.dateTime);
i1.GeneratedColumn<String> _column_17(String aliasedName) =>
i1.GeneratedColumn<String>('thumb_hash', aliasedName, true,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<DateTime> _column_18(String aliasedName) =>
i1.GeneratedColumn<DateTime>('deleted_at', aliasedName, true,
type: i1.DriftSqlType.dateTime);
i1.GeneratedColumn<String> _column_19(String aliasedName) =>
i1.GeneratedColumn<String>('live_photo_video_id', aliasedName, true,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<int> _column_20(String aliasedName) =>
i1.GeneratedColumn<int>('visibility', aliasedName, false,
type: i1.DriftSqlType.int);
class Shape2 extends i0.VersionedTable {
Shape2({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get name =>
columnsByName['name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get type =>
columnsByName['type']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<DateTime> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<DateTime> get updatedAt =>
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<int> get width =>
columnsByName['width']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get height =>
columnsByName['height']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get durationInSeconds =>
columnsByName['duration_in_seconds']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get id =>
columnsByName['id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get checksum =>
columnsByName['checksum']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<bool> get isFavorite =>
columnsByName['is_favorite']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<int> get orientation =>
columnsByName['orientation']! as i1.GeneratedColumn<int>;
}
i1.GeneratedColumn<String> _column_21(String aliasedName) =>
i1.GeneratedColumn<String>('checksum', aliasedName, true,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<int> _column_22(String aliasedName) =>
i1.GeneratedColumn<int>('orientation', aliasedName, false,
type: i1.DriftSqlType.int, defaultValue: const CustomExpression('0'));
class Shape3 extends i0.VersionedTable {
Shape3({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get userId =>
columnsByName['user_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get key =>
columnsByName['key']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<i2.Uint8List> get value =>
columnsByName['value']! as i1.GeneratedColumn<i2.Uint8List>;
}
i1.GeneratedColumn<String> _column_23(String aliasedName) =>
i1.GeneratedColumn<String>('user_id', aliasedName, false,
type: i1.DriftSqlType.string,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'REFERENCES user_entity (id) ON DELETE CASCADE'));
i1.GeneratedColumn<int> _column_24(String aliasedName) =>
i1.GeneratedColumn<int>('key', aliasedName, false,
type: i1.DriftSqlType.int);
i1.GeneratedColumn<i2.Uint8List> _column_25(String aliasedName) =>
i1.GeneratedColumn<i2.Uint8List>('value', aliasedName, false,
type: i1.DriftSqlType.blob);
class Shape4 extends i0.VersionedTable {
Shape4({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get sharedById =>
columnsByName['shared_by_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get sharedWithId =>
columnsByName['shared_with_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<bool> get inTimeline =>
columnsByName['in_timeline']! as i1.GeneratedColumn<bool>;
}
i1.GeneratedColumn<String> _column_26(String aliasedName) =>
i1.GeneratedColumn<String>('shared_by_id', aliasedName, false,
type: i1.DriftSqlType.string,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'REFERENCES user_entity (id) ON DELETE CASCADE'));
i1.GeneratedColumn<String> _column_27(String aliasedName) =>
i1.GeneratedColumn<String>('shared_with_id', aliasedName, false,
type: i1.DriftSqlType.string,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'REFERENCES user_entity (id) ON DELETE CASCADE'));
i1.GeneratedColumn<bool> _column_28(String aliasedName) =>
i1.GeneratedColumn<bool>('in_timeline', aliasedName, false,
type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("in_timeline" IN (0, 1))'),
defaultValue: const CustomExpression('0'));
class Shape5 extends i0.VersionedTable {
Shape5({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get id =>
columnsByName['id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get name =>
columnsByName['name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get updatedAt =>
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<int> get backupSelection =>
columnsByName['backup_selection']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<bool> get isIosSharedAlbum =>
columnsByName['is_ios_shared_album']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get marker_ =>
columnsByName['marker']! as i1.GeneratedColumn<bool>;
}
i1.GeneratedColumn<int> _column_29(String aliasedName) =>
i1.GeneratedColumn<int>('backup_selection', aliasedName, false,
type: i1.DriftSqlType.int);
i1.GeneratedColumn<bool> _column_30(String aliasedName) =>
i1.GeneratedColumn<bool>('is_ios_shared_album', aliasedName, false,
type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("is_ios_shared_album" IN (0, 1))'),
defaultValue: const CustomExpression('0'));
i1.GeneratedColumn<bool> _column_31(String aliasedName) =>
i1.GeneratedColumn<bool>('marker', aliasedName, true,
type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("marker" IN (0, 1))'));
class Shape6 extends i0.VersionedTable {
Shape6({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get assetId =>
columnsByName['asset_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get albumId =>
columnsByName['album_id']! as i1.GeneratedColumn<String>;
}
i1.GeneratedColumn<String> _column_32(String aliasedName) =>
i1.GeneratedColumn<String>('asset_id', aliasedName, false,
type: i1.DriftSqlType.string,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'REFERENCES local_asset_entity (id) ON DELETE CASCADE'));
i1.GeneratedColumn<String> _column_33(String aliasedName) =>
i1.GeneratedColumn<String>('album_id', aliasedName, false,
type: i1.DriftSqlType.string,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'REFERENCES local_album_entity (id) ON DELETE CASCADE'));
class Shape7 extends i0.VersionedTable {
Shape7({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get assetId =>
columnsByName['asset_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get city =>
columnsByName['city']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get state =>
columnsByName['state']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get country =>
columnsByName['country']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get dateTimeOriginal =>
columnsByName['date_time_original']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<String> get description =>
columnsByName['description']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get height =>
columnsByName['height']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get width =>
columnsByName['width']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get exposureTime =>
columnsByName['exposure_time']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<double> get fNumber =>
columnsByName['f_number']! as i1.GeneratedColumn<double>;
i1.GeneratedColumn<int> get fileSize =>
columnsByName['file_size']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<double> get focalLength =>
columnsByName['focal_length']! as i1.GeneratedColumn<double>;
i1.GeneratedColumn<double> get latitude =>
columnsByName['latitude']! as i1.GeneratedColumn<double>;
i1.GeneratedColumn<double> get longitude =>
columnsByName['longitude']! as i1.GeneratedColumn<double>;
i1.GeneratedColumn<int> get iso =>
columnsByName['iso']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get make =>
columnsByName['make']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get model =>
columnsByName['model']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get lens =>
columnsByName['lens']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get orientation =>
columnsByName['orientation']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get timeZone =>
columnsByName['time_zone']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get rating =>
columnsByName['rating']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get projectionType =>
columnsByName['projection_type']! as i1.GeneratedColumn<String>;
}
i1.GeneratedColumn<String> _column_34(String aliasedName) =>
i1.GeneratedColumn<String>('asset_id', aliasedName, false,
type: i1.DriftSqlType.string,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'REFERENCES remote_asset_entity (id) ON DELETE CASCADE'));
i1.GeneratedColumn<String> _column_35(String aliasedName) =>
i1.GeneratedColumn<String>('city', aliasedName, true,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_36(String aliasedName) =>
i1.GeneratedColumn<String>('state', aliasedName, true,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_37(String aliasedName) =>
i1.GeneratedColumn<String>('country', aliasedName, true,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<DateTime> _column_38(String aliasedName) =>
i1.GeneratedColumn<DateTime>('date_time_original', aliasedName, true,
type: i1.DriftSqlType.dateTime);
i1.GeneratedColumn<String> _column_39(String aliasedName) =>
i1.GeneratedColumn<String>('description', aliasedName, true,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_40(String aliasedName) =>
i1.GeneratedColumn<String>('exposure_time', aliasedName, true,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<double> _column_41(String aliasedName) =>
i1.GeneratedColumn<double>('f_number', aliasedName, true,
type: i1.DriftSqlType.double);
i1.GeneratedColumn<int> _column_42(String aliasedName) =>
i1.GeneratedColumn<int>('file_size', aliasedName, true,
type: i1.DriftSqlType.int);
i1.GeneratedColumn<double> _column_43(String aliasedName) =>
i1.GeneratedColumn<double>('focal_length', aliasedName, true,
type: i1.DriftSqlType.double);
i1.GeneratedColumn<double> _column_44(String aliasedName) =>
i1.GeneratedColumn<double>('latitude', aliasedName, true,
type: i1.DriftSqlType.double);
i1.GeneratedColumn<double> _column_45(String aliasedName) =>
i1.GeneratedColumn<double>('longitude', aliasedName, true,
type: i1.DriftSqlType.double);
i1.GeneratedColumn<int> _column_46(String aliasedName) =>
i1.GeneratedColumn<int>('iso', aliasedName, true,
type: i1.DriftSqlType.int);
i1.GeneratedColumn<String> _column_47(String aliasedName) =>
i1.GeneratedColumn<String>('make', aliasedName, true,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_48(String aliasedName) =>
i1.GeneratedColumn<String>('model', aliasedName, true,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_49(String aliasedName) =>
i1.GeneratedColumn<String>('lens', aliasedName, true,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_50(String aliasedName) =>
i1.GeneratedColumn<String>('orientation', aliasedName, true,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_51(String aliasedName) =>
i1.GeneratedColumn<String>('time_zone', aliasedName, true,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<int> _column_52(String aliasedName) =>
i1.GeneratedColumn<int>('rating', aliasedName, true,
type: i1.DriftSqlType.int);
i1.GeneratedColumn<String> _column_53(String aliasedName) =>
i1.GeneratedColumn<String>('projection_type', aliasedName, true,
type: i1.DriftSqlType.string);
class Shape8 extends i0.VersionedTable {
Shape8({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get id =>
columnsByName['id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get name =>
columnsByName['name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get description =>
columnsByName['description']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<DateTime> get updatedAt =>
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<String> get ownerId =>
columnsByName['owner_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get thumbnailAssetId =>
columnsByName['thumbnail_asset_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<bool> get isActivityEnabled =>
columnsByName['is_activity_enabled']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<int> get order =>
columnsByName['order']! as i1.GeneratedColumn<int>;
}
i1.GeneratedColumn<String> _column_54(String aliasedName) =>
i1.GeneratedColumn<String>('description', aliasedName, false,
type: i1.DriftSqlType.string,
defaultValue: const CustomExpression('\'\''));
i1.GeneratedColumn<String> _column_55(String aliasedName) =>
i1.GeneratedColumn<String>('thumbnail_asset_id', aliasedName, true,
type: i1.DriftSqlType.string,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'REFERENCES remote_asset_entity (id) ON DELETE SET NULL'));
i1.GeneratedColumn<bool> _column_56(String aliasedName) =>
i1.GeneratedColumn<bool>('is_activity_enabled', aliasedName, false,
type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("is_activity_enabled" IN (0, 1))'),
defaultValue: const CustomExpression('1'));
i1.GeneratedColumn<int> _column_57(String aliasedName) =>
i1.GeneratedColumn<int>('order', aliasedName, false,
type: i1.DriftSqlType.int);
i1.GeneratedColumn<String> _column_58(String aliasedName) =>
i1.GeneratedColumn<String>('album_id', aliasedName, false,
type: i1.DriftSqlType.string,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'REFERENCES remote_album_entity (id) ON DELETE CASCADE'));
class Shape9 extends i0.VersionedTable {
Shape9({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get albumId =>
columnsByName['album_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get userId =>
columnsByName['user_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get role =>
columnsByName['role']! as i1.GeneratedColumn<int>;
}
i1.GeneratedColumn<int> _column_59(String aliasedName) =>
i1.GeneratedColumn<int>('role', aliasedName, false,
type: i1.DriftSqlType.int);
class Shape10 extends i0.VersionedTable {
Shape10({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get id =>
columnsByName['id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<DateTime> get updatedAt =>
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<DateTime> get deletedAt =>
columnsByName['deleted_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<String> get ownerId =>
columnsByName['owner_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get type =>
columnsByName['type']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get data =>
columnsByName['data']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<bool> get isSaved =>
columnsByName['is_saved']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<DateTime> get memoryAt =>
columnsByName['memory_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<DateTime> get seenAt =>
columnsByName['seen_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<DateTime> get showAt =>
columnsByName['show_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<DateTime> get hideAt =>
columnsByName['hide_at']! as i1.GeneratedColumn<DateTime>;
}
i1.GeneratedColumn<String> _column_60(String aliasedName) =>
i1.GeneratedColumn<String>('data', aliasedName, false,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<bool> _column_61(String aliasedName) =>
i1.GeneratedColumn<bool>('is_saved', aliasedName, false,
type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("is_saved" IN (0, 1))'),
defaultValue: const CustomExpression('0'));
i1.GeneratedColumn<DateTime> _column_62(String aliasedName) =>
i1.GeneratedColumn<DateTime>('memory_at', aliasedName, false,
type: i1.DriftSqlType.dateTime);
i1.GeneratedColumn<DateTime> _column_63(String aliasedName) =>
i1.GeneratedColumn<DateTime>('seen_at', aliasedName, true,
type: i1.DriftSqlType.dateTime);
i1.GeneratedColumn<DateTime> _column_64(String aliasedName) =>
i1.GeneratedColumn<DateTime>('show_at', aliasedName, true,
type: i1.DriftSqlType.dateTime);
i1.GeneratedColumn<DateTime> _column_65(String aliasedName) =>
i1.GeneratedColumn<DateTime>('hide_at', aliasedName, true,
type: i1.DriftSqlType.dateTime);
class Shape11 extends i0.VersionedTable {
Shape11({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get assetId =>
columnsByName['asset_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get memoryId =>
columnsByName['memory_id']! as i1.GeneratedColumn<String>;
}
i1.GeneratedColumn<String> _column_66(String aliasedName) =>
i1.GeneratedColumn<String>('memory_id', aliasedName, false,
type: i1.DriftSqlType.string,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'REFERENCES memory_entity (id) ON DELETE CASCADE'));
class Shape12 extends i0.VersionedTable {
Shape12({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get id =>
columnsByName['id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<DateTime> get updatedAt =>
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<String> get ownerId =>
columnsByName['owner_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get primaryAssetId =>
columnsByName['primary_asset_id']! as i1.GeneratedColumn<String>;
}
i1.GeneratedColumn<String> _column_67(String aliasedName) =>
i1.GeneratedColumn<String>('primary_asset_id', aliasedName, false,
type: i1.DriftSqlType.string,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'REFERENCES remote_asset_entity (id)'));
i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
}) {
return (currentVersion, database) async {
switch (currentVersion) {
case 1:
final schema = Schema2(database: database);
final migrator = i1.Migrator(database, schema);
await from1To2(migrator, schema);
return 2;
default:
throw ArgumentError.value('Unknown migration from $currentVersion');
}
};
}
i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
}) =>
i0.VersionedSchema.stepByStepHelper(
step: migrationSteps(
from1To2: from1To2,
));

View File

@@ -254,42 +254,44 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
required int offset, required int offset,
required int count, required int count,
}) async { }) async {
final albumData = await (_db.remoteAlbumEntity.select() return await transaction(() async {
..where((row) => row.id.equals(albumId))) final albumData = await (_db.remoteAlbumEntity.select()
.getSingleOrNull(); ..where((row) => row.id.equals(albumId)))
.getSingleOrNull();
// If album doesn't exist (was deleted), return empty list // If album doesn't exist (was deleted), return empty list
if (albumData == null) { if (albumData == null) {
return <BaseAsset>[]; return <BaseAsset>[];
} }
final isAscending = albumData.order == AlbumAssetOrder.asc; final isAscending = albumData.order == AlbumAssetOrder.asc;
final query = _db.remoteAssetEntity.select().join( final query = _db.remoteAssetEntity.select().join(
[ [
innerJoin( innerJoin(
_db.remoteAlbumAssetEntity, _db.remoteAlbumAssetEntity,
_db.remoteAlbumAssetEntity.assetId _db.remoteAlbumAssetEntity.assetId
.equalsExp(_db.remoteAssetEntity.id), .equalsExp(_db.remoteAssetEntity.id),
useColumns: false, useColumns: false,
), ),
], ],
)..where( )..where(
_db.remoteAssetEntity.deletedAt.isNull() & _db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAlbumAssetEntity.albumId.equals(albumId), _db.remoteAlbumAssetEntity.albumId.equals(albumId),
); );
if (isAscending) { if (isAscending) {
query.orderBy([OrderingTerm.asc(_db.remoteAssetEntity.createdAt)]); query.orderBy([OrderingTerm.asc(_db.remoteAssetEntity.createdAt)]);
} else { } else {
query.orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)]); query.orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)]);
} }
query.limit(count, offset: offset); query.limit(count, offset: offset);
return query return query
.map((row) => row.readTable(_db.remoteAssetEntity).toDto()) .map((row) => row.readTable(_db.remoteAssetEntity).toDto())
.get(); .get();
});
} }
TimelineQuery remote(String ownerId, GroupAssetsBy groupBy) => TimelineQuery remote(String ownerId, GroupAssetsBy groupBy) =>
@@ -462,13 +464,15 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
required Expression<bool> Function($RemoteAssetEntityTable row) filter, required Expression<bool> Function($RemoteAssetEntityTable row) filter,
required int offset, required int offset,
required int count, required int count,
}) { }) async {
final query = _db.remoteAssetEntity.select() return await transaction(() async {
..where(filter) final query = _db.remoteAssetEntity.select()
..orderBy([(row) => OrderingTerm.desc(row.createdAt)]) ..where(filter)
..limit(count, offset: offset); ..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
..limit(count, offset: offset);
return query.map((row) => row.toDto()).get(); return query.map((row) => row.toDto()).get();
});
} }
} }

View File

@@ -0,0 +1,126 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/migration.dart';
import 'package:permission_handler/permission_handler.dart';
@RoutePage()
class ChangeExperiencePage extends ConsumerStatefulWidget {
final bool switchingToBeta;
const ChangeExperiencePage({super.key, required this.switchingToBeta});
@override
ConsumerState createState() => _ChangeExperiencePageState();
}
class _ChangeExperiencePageState extends ConsumerState<ChangeExperiencePage> {
bool hasMigrated = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => _handleMigration());
}
Future<void> _handleMigration() async {
if (widget.switchingToBeta) {
final assetNotifier = ref.read(assetProvider.notifier);
if (assetNotifier.mounted) {
assetNotifier.dispose();
}
final albumNotifier = ref.read(albumProvider.notifier);
if (albumNotifier.mounted) {
albumNotifier.dispose();
}
final permission = await ref
.read(galleryPermissionNotifier.notifier)
.requestGalleryPermission();
if (permission.isGranted) {
await ref.read(backgroundSyncProvider).syncLocal(full: true);
await migrateDeviceAssetToSqlite(
ref.read(isarProvider),
ref.read(driftProvider),
);
}
} else {
await ref.read(backgroundSyncProvider).cancel();
}
Future.delayed(const Duration(seconds: 3), () {
context.replaceRoute(
widget.switchingToBeta
? const TabShellRoute()
: const TabControllerRoute(),
);
});
if (mounted) {
setState(() {
HapticFeedback.heavyImpact();
hasMigrated = true;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedSwitcher(
duration: Durations.long4,
child: hasMigrated
? const Icon(
Icons.check_circle_rounded,
color: Colors.green,
size: 48.0,
)
: const SizedBox(
width: 50.0,
height: 50.0,
child: CircularProgressIndicator(),
),
),
const SizedBox(height: 16.0),
Center(
child: Column(
children: [
SizedBox(
width: 300.0,
child: AnimatedSwitcher(
duration: Durations.long4,
child: hasMigrated
? Text(
"Migration success. Navigating to the new timeline...",
style: context.textTheme.titleMedium,
textAlign: TextAlign.center,
)
: Text(
"Data migration in progress...\nPlease wait and don't close this page",
style: context.textTheme.titleMedium,
textAlign: TextAlign.center,
),
),
),
],
),
),
],
),
),
);
}
}

View File

@@ -8,6 +8,7 @@ import 'package:immich_mobile/widgets/settings/advanced_settings.dart';
import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_settings.dart'; import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_settings.dart';
import 'package:immich_mobile/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart'; import 'package:immich_mobile/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart';
import 'package:immich_mobile/widgets/settings/backup_settings/backup_settings.dart'; import 'package:immich_mobile/widgets/settings/backup_settings/backup_settings.dart';
import 'package:immich_mobile/widgets/settings/beta_timeline_list_tile.dart';
import 'package:immich_mobile/widgets/settings/language_settings.dart'; import 'package:immich_mobile/widgets/settings/language_settings.dart';
import 'package:immich_mobile/widgets/settings/networking_settings/networking_settings.dart'; import 'package:immich_mobile/widgets/settings/networking_settings/networking_settings.dart';
import 'package:immich_mobile/widgets/settings/notification_setting.dart'; import 'package:immich_mobile/widgets/settings/notification_setting.dart';
@@ -94,55 +95,59 @@ class _MobileLayout extends StatelessWidget {
const _MobileLayout(); const _MobileLayout();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final List<Widget> settings = SettingSection.values
.map(
(setting) => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
),
child: Card(
elevation: 0,
clipBehavior: Clip.antiAlias,
color: context.colorScheme.surfaceContainer,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(16)),
),
margin: const EdgeInsets.symmetric(vertical: 4.0),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 16.0,
),
leading: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(16)),
color: context.isDarkTheme
? Colors.black26
: Colors.white.withAlpha(100),
),
padding: const EdgeInsets.all(16.0),
child: Icon(setting.icon, color: context.primaryColor),
),
title: Text(
setting.title,
style: context.textTheme.titleMedium!.copyWith(
fontWeight: FontWeight.w600,
color: context.primaryColor,
),
).tr(),
subtitle: Text(
setting.subtitle,
style: context.textTheme.labelLarge,
).tr(),
onTap: () =>
context.pushRoute(SettingsSubRoute(section: setting)),
),
),
),
)
.toList();
return ListView( return ListView(
physics: const ClampingScrollPhysics(), physics: const ClampingScrollPhysics(),
padding: const EdgeInsets.symmetric(vertical: 10.0), padding: const EdgeInsets.symmetric(vertical: 10.0),
children: SettingSection.values children: [
.map( const BetaTimelineListTile(),
(setting) => Padding( ...settings,
padding: const EdgeInsets.symmetric( ],
horizontal: 16.0,
),
child: Card(
elevation: 0,
clipBehavior: Clip.antiAlias,
color: context.colorScheme.surfaceContainer,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(16)),
),
margin: const EdgeInsets.symmetric(vertical: 4.0),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 16.0,
),
leading: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(16)),
color: context.isDarkTheme
? Colors.black26
: Colors.white.withAlpha(100),
),
padding: const EdgeInsets.all(16.0),
child: Icon(setting.icon, color: context.primaryColor),
),
title: Text(
setting.title,
style: context.textTheme.titleMedium!.copyWith(
fontWeight: FontWeight.w600,
color: context.primaryColor,
),
).tr(),
subtitle: Text(
setting.subtitle,
style: context.textTheme.labelLarge,
).tr(),
onTap: () =>
context.pushRoute(SettingsSubRoute(section: setting)),
),
),
),
)
.toList(),
); );
} }
} }

View File

@@ -73,7 +73,15 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
} }
if (context.router.current.name == SplashScreenRoute.name) { if (context.router.current.name == SplashScreenRoute.name) {
context.replaceRoute(const TabControllerRoute()); context.replaceRoute(
Store.isBetaTimelineEnabled
? const TabShellRoute()
: const TabControllerRoute(),
);
}
if (Store.isBetaTimelineEnabled) {
return;
} }
final hasPermission = final hasPermission =

View File

@@ -10,13 +10,28 @@ import 'package:immich_mobile/providers/search/search_input_focus.provider.dart'
import 'package:immich_mobile/providers/tab.provider.dart'; import 'package:immich_mobile/providers/tab.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/migration.dart';
@RoutePage() @RoutePage()
class TabShellPage extends ConsumerWidget { class TabShellPage extends ConsumerStatefulWidget {
const TabShellPage({super.key}); const TabShellPage({super.key});
@override @override
Widget build(BuildContext context, WidgetRef ref) { ConsumerState<TabShellPage> createState() => _TabShellPageState();
}
class _TabShellPageState extends ConsumerState<TabShellPage> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
runNewSync(ref, full: true);
});
}
@override
Widget build(BuildContext context) {
final isScreenLandscape = context.orientation == Orientation.landscape; final isScreenLandscape = context.orientation == Orientation.landscape;
Widget buildIcon({required Widget icon, required bool isProcessing}) { Widget buildIcon({required Widget icon, required bool isProcessing}) {

View File

@@ -3,6 +3,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart'; import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart';
import 'package:immich_mobile/pages/common/large_leading_tile.dart'; import 'package:immich_mobile/pages/common/large_leading_tile.dart';
@@ -75,7 +76,9 @@ class ShareIntentPage extends HookConsumerWidget {
leading: IconButton( leading: IconButton(
onPressed: () { onPressed: () {
context.navigateTo( context.navigateTo(
const TabControllerRoute(), Store.isBetaTimelineEnabled
? const TabShellRoute()
: const TabControllerRoute(),
); );
}, },
icon: const Icon(Icons.arrow_back), icon: const Icon(Icons.arrow_back),

View File

@@ -3,10 +3,12 @@ import 'dart:async';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/domain/services/log.service.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/models/backup/backup_state.model.dart'; import 'package:immich_mobile/models/backup/backup_state.model.dart';
import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart'; import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart';
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
@@ -18,6 +20,7 @@ import 'package:immich_mobile/providers/tab.provider.dart';
import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart';
import 'package:immich_mobile/services/background.service.dart'; import 'package:immich_mobile/services/background.service.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
enum AppLifeCycleEnum { enum AppLifeCycleEnum {
@@ -57,29 +60,59 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
debugPrint("Using server URL: $endpoint"); debugPrint("Using server URL: $endpoint");
} }
final permission = _ref.watch(galleryPermissionNotifier); if (!Store.isBetaTimelineEnabled) {
if (permission.isGranted || permission.isLimited) { final permission = _ref.watch(galleryPermissionNotifier);
await _ref.read(backupProvider.notifier).resumeBackup(); if (permission.isGranted || permission.isLimited) {
await _ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); await _ref.read(backupProvider.notifier).resumeBackup();
await _ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
}
} }
await _ref.read(serverInfoProvider.notifier).getServerVersion(); await _ref.read(serverInfoProvider.notifier).getServerVersion();
} }
switch (_ref.read(tabProvider)) { if (!Store.isBetaTimelineEnabled) {
case TabEnum.home: switch (_ref.read(tabProvider)) {
await _ref.read(assetProvider.notifier).getAllAsset(); case TabEnum.home:
break; await _ref.read(assetProvider.notifier).getAllAsset();
case TabEnum.search: break;
// nothing to do case TabEnum.search:
break; // nothing to do
break;
case TabEnum.albums: case TabEnum.albums:
await _ref.read(albumProvider.notifier).refreshRemoteAlbums(); await _ref.read(albumProvider.notifier).refreshRemoteAlbums();
break; break;
case TabEnum.library: case TabEnum.library:
// nothing to do // nothing to do
break; break;
}
} else {
_ref.read(backupProvider.notifier).cancelBackup();
final backgroundManager = _ref.read(backgroundSyncProvider);
// Ensure proper cleanup before starting new background tasks
try {
await Future.wait([
backgroundManager.syncLocal().then(
(_) {
Logger("AppLifeCycleNotifier")
.fine("Hashing assets after syncLocal");
// Check if app is still active before hashing
if (state == AppLifeCycleEnum.resumed) {
backgroundManager.hashAssets();
}
},
),
backgroundManager.syncRemote(),
]);
} catch (e, stackTrace) {
Logger("AppLifeCycleNotifier").severe(
"Error during background sync",
e,
stackTrace,
);
}
} }
_ref.read(websocketProvider.notifier).connect(); _ref.read(websocketProvider.notifier).connect();
@@ -92,9 +125,11 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
.read(galleryPermissionNotifier.notifier) .read(galleryPermissionNotifier.notifier)
.getGalleryPermissionStatus(); .getGalleryPermissionStatus();
await _ref.read(iOSBackgroundSettingsProvider.notifier).refresh(); if (!Store.isBetaTimelineEnabled) {
await _ref.read(iOSBackgroundSettingsProvider.notifier).refresh();
_ref.invalidate(memoryFutureProvider); _ref.invalidate(memoryFutureProvider);
}
} }
void handleAppInactivity() { void handleAppInactivity() {
@@ -106,7 +141,8 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
state = AppLifeCycleEnum.paused; state = AppLifeCycleEnum.paused;
_wasPaused = true; _wasPaused = true;
if (_ref.read(authProvider).isAuthenticated) { if (!Store.isBetaTimelineEnabled &&
_ref.read(authProvider).isAuthenticated) {
// Do not cancel backup if manual upload is in progress // Do not cancel backup if manual upload is in progress
if (_ref.read(backupProvider.notifier).backupProgress != if (_ref.read(backupProvider.notifier).backupProgress !=
BackUpProgressEnum.manualInProgress) { BackUpProgressEnum.manualInProgress) {
@@ -115,15 +151,43 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
_ref.read(websocketProvider.notifier).disconnect(); _ref.read(websocketProvider.notifier).disconnect();
} }
LogService.I.flush(); try {
LogService.I.flush();
} catch (e) {
// Ignore flush errors during pause
}
} }
Future<void> handleAppDetached() async { Future<void> handleAppDetached() async {
state = AppLifeCycleEnum.detached; state = AppLifeCycleEnum.detached;
LogService.I.flush();
await Isar.getInstance()?.close(); // Flush logs before closing database
try {
LogService.I.flush();
} catch (e) {
// Ignore flush errors during shutdown
}
// Close Isar database safely
try {
final isar = Isar.getInstance();
if (isar != null && isar.isOpen) {
await isar.close();
}
} catch (e) {
// Ignore close errors during shutdown
}
if (Store.isBetaTimelineEnabled) {
return;
}
// no guarantee this is called at all // no guarantee this is called at all
_ref.read(manualUploadProvider.notifier).cancelBackup(); try {
_ref.read(manualUploadProvider.notifier).cancelBackup();
} catch (e) {
// Ignore errors during shutdown
}
} }
void handleAppHidden() { void handleAppHidden() {

View File

@@ -13,5 +13,6 @@ Isar isar(Ref ref) => throw UnimplementedError('isar');
final driftProvider = Provider<Drift>((ref) { final driftProvider = Provider<Drift>((ref) {
final drift = Drift(); final drift = Drift();
ref.onDispose(() => unawaited(drift.close())); ref.onDispose(() => unawaited(drift.close()));
ref.keepAlive();
return drift; return drift;
}); });

View File

@@ -24,7 +24,25 @@ class AuthRepository extends DatabaseRepository {
const AuthRepository(super.db, this._drift); const AuthRepository(super.db, this._drift);
Future<void> clearLocalData() { Future<void> clearLocalData() async {
// Drift deletions - child entities first (those with foreign keys)
await Future.wait([
_drift.memoryAssetEntity.deleteAll(),
_drift.remoteAlbumAssetEntity.deleteAll(),
_drift.remoteAlbumUserEntity.deleteAll(),
_drift.remoteExifEntity.deleteAll(),
_drift.userMetadataEntity.deleteAll(),
_drift.partnerEntity.deleteAll(),
_drift.stackEntity.deleteAll(),
]);
// Drift deletions - parent entities
await Future.wait([
_drift.memoryEntity.deleteAll(),
_drift.remoteAlbumEntity.deleteAll(),
_drift.remoteAssetEntity.deleteAll(),
_drift.userEntity.deleteAll(),
]);
return db.writeTxn(() { return db.writeTxn(() {
return Future.wait([ return Future.wait([
db.assets.clear(), db.assets.clear(),
@@ -32,17 +50,6 @@ class AuthRepository extends DatabaseRepository {
db.albums.clear(), db.albums.clear(),
db.eTags.clear(), db.eTags.clear(),
db.users.clear(), db.users.clear(),
_drift.remoteAssetEntity.deleteAll(),
_drift.remoteExifEntity.deleteAll(),
_drift.userEntity.deleteAll(),
_drift.userMetadataEntity.deleteAll(),
_drift.partnerEntity.deleteAll(),
_drift.remoteAlbumEntity.deleteAll(),
_drift.remoteAlbumAssetEntity.deleteAll(),
_drift.remoteAlbumUserEntity.deleteAll(),
_drift.memoryEntity.deleteAll(),
_drift.memoryAssetEntity.deleteAll(),
_drift.stackEntity.deleteAll(),
]); ]);
}); });
} }

View File

@@ -29,6 +29,7 @@ import 'package:immich_mobile/pages/backup/failed_backup_status.page.dart';
import 'package:immich_mobile/pages/common/activities.page.dart'; import 'package:immich_mobile/pages/common/activities.page.dart';
import 'package:immich_mobile/pages/common/app_log.page.dart'; import 'package:immich_mobile/pages/common/app_log.page.dart';
import 'package:immich_mobile/pages/common/app_log_detail.page.dart'; import 'package:immich_mobile/pages/common/app_log_detail.page.dart';
import 'package:immich_mobile/pages/common/change_experience.page.dart';
import 'package:immich_mobile/pages/common/create_album.page.dart'; import 'package:immich_mobile/pages/common/create_album.page.dart';
import 'package:immich_mobile/pages/common/gallery_viewer.page.dart'; import 'package:immich_mobile/pages/common/gallery_viewer.page.dart';
import 'package:immich_mobile/pages/common/headers_settings.page.dart'; import 'package:immich_mobile/pages/common/headers_settings.page.dart';
@@ -69,27 +70,27 @@ import 'package:immich_mobile/pages/search/person_result.page.dart';
import 'package:immich_mobile/pages/search/recently_taken.page.dart'; import 'package:immich_mobile/pages/search/recently_taken.page.dart';
import 'package:immich_mobile/pages/search/search.page.dart'; import 'package:immich_mobile/pages/search/search.page.dart';
import 'package:immich_mobile/pages/share_intent/share_intent.page.dart'; import 'package:immich_mobile/pages/share_intent/share_intent.page.dart';
import 'package:immich_mobile/presentation/pages/dev/feat_in_development.page.dart';
import 'package:immich_mobile/presentation/pages/dev/main_timeline.page.dart';
import 'package:immich_mobile/presentation/pages/dev/media_stat.page.dart';
import 'package:immich_mobile/presentation/pages/drift_album.page.dart';
import 'package:immich_mobile/presentation/pages/drift_archive.page.dart';
import 'package:immich_mobile/presentation/pages/drift_asset_selection_timeline.page.dart';
import 'package:immich_mobile/presentation/pages/drift_create_album.page.dart';
import 'package:immich_mobile/presentation/pages/drift_favorite.page.dart'; import 'package:immich_mobile/presentation/pages/drift_favorite.page.dart';
import 'package:immich_mobile/presentation/pages/drift_partner_detail.page.dart'; import 'package:immich_mobile/presentation/pages/drift_library.page.dart';
import 'package:immich_mobile/presentation/pages/drift_local_album.page.dart'; import 'package:immich_mobile/presentation/pages/drift_local_album.page.dart';
import 'package:immich_mobile/presentation/pages/drift_locked_folder.page.dart';
import 'package:immich_mobile/presentation/pages/drift_memory.page.dart';
import 'package:immich_mobile/presentation/pages/drift_partner_detail.page.dart';
import 'package:immich_mobile/presentation/pages/drift_place.page.dart'; import 'package:immich_mobile/presentation/pages/drift_place.page.dart';
import 'package:immich_mobile/presentation/pages/drift_place_detail.page.dart'; import 'package:immich_mobile/presentation/pages/drift_place_detail.page.dart';
import 'package:immich_mobile/presentation/pages/drift_recently_taken.page.dart'; import 'package:immich_mobile/presentation/pages/drift_recently_taken.page.dart';
import 'package:immich_mobile/presentation/pages/drift_user_selection.page.dart'; import 'package:immich_mobile/presentation/pages/drift_user_selection.page.dart';
import 'package:immich_mobile/presentation/pages/drift_video.page.dart';
import 'package:immich_mobile/presentation/pages/drift_trash.page.dart';
import 'package:immich_mobile/presentation/pages/drift_archive.page.dart';
import 'package:immich_mobile/presentation/pages/drift_locked_folder.page.dart';
import 'package:immich_mobile/presentation/pages/dev/feat_in_development.page.dart';
import 'package:immich_mobile/presentation/pages/local_timeline.page.dart';
import 'package:immich_mobile/presentation/pages/dev/main_timeline.page.dart';
import 'package:immich_mobile/presentation/pages/dev/media_stat.page.dart';
import 'package:immich_mobile/presentation/pages/drift_remote_album.page.dart'; import 'package:immich_mobile/presentation/pages/drift_remote_album.page.dart';
import 'package:immich_mobile/presentation/pages/drift_album.page.dart'; import 'package:immich_mobile/presentation/pages/drift_trash.page.dart';
import 'package:immich_mobile/presentation/pages/drift_library.page.dart'; import 'package:immich_mobile/presentation/pages/drift_video.page.dart';
import 'package:immich_mobile/presentation/pages/drift_asset_selection_timeline.page.dart'; import 'package:immich_mobile/presentation/pages/local_timeline.page.dart';
import 'package:immich_mobile/presentation/pages/drift_create_album.page.dart';
import 'package:immich_mobile/presentation/pages/drift_memory.page.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart';
import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart';
@@ -103,7 +104,6 @@ import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/local_auth.service.dart'; import 'package:immich_mobile/services/local_auth.service.dart';
import 'package:immich_mobile/services/secure_storage.service.dart'; import 'package:immich_mobile/services/secure_storage.service.dart';
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
import 'package:maplibre_gl/maplibre_gl.dart'; import 'package:maplibre_gl/maplibre_gl.dart';
part 'router.gr.dart'; part 'router.gr.dart';
@@ -469,6 +469,10 @@ class AppRouter extends RootStackRouter {
guards: [_authGuard, _duplicateGuard], guards: [_authGuard, _duplicateGuard],
), ),
AutoRoute(
page: ChangeExperienceRoute.page,
guards: [_authGuard, _duplicateGuard],
),
// required to handle all deeplinks in deep_link.service.dart // required to handle all deeplinks in deep_link.service.dart
// auto_route_library#1722 // auto_route_library#1722
RedirectRoute(path: '*', redirectTo: '/'), RedirectRoute(path: '*', redirectTo: '/'),

View File

@@ -503,6 +503,49 @@ class BackupOptionsRoute extends PageRouteInfo<void> {
); );
} }
/// generated route for
/// [ChangeExperiencePage]
class ChangeExperienceRoute extends PageRouteInfo<ChangeExperienceRouteArgs> {
ChangeExperienceRoute({
Key? key,
required bool switchingToBeta,
List<PageRouteInfo>? children,
}) : super(
ChangeExperienceRoute.name,
args: ChangeExperienceRouteArgs(
key: key,
switchingToBeta: switchingToBeta,
),
initialChildren: children,
);
static const String name = 'ChangeExperienceRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
final args = data.argsAs<ChangeExperienceRouteArgs>();
return ChangeExperiencePage(
key: args.key,
switchingToBeta: args.switchingToBeta,
);
},
);
}
class ChangeExperienceRouteArgs {
const ChangeExperienceRouteArgs({this.key, required this.switchingToBeta});
final Key? key;
final bool switchingToBeta;
@override
String toString() {
return 'ChangeExperienceRouteArgs{key: $key, switchingToBeta: $switchingToBeta}';
}
}
/// generated route for /// generated route for
/// [ChangePasswordPage] /// [ChangePasswordPage]
class ChangePasswordRoute extends PageRouteInfo<void> { class ChangePasswordRoute extends PageRouteInfo<void> {

View File

@@ -90,6 +90,7 @@ enum AppSettingsEnum<T> {
null, null,
true, true,
), ),
betaTimeline<bool>(StoreKey.betaTimeline, null, false),
; ;
const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue); const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue);

View File

@@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/material.dart';
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/domain/services/log.service.dart'; import 'package:immich_mobile/domain/services/log.service.dart';
@@ -59,9 +60,18 @@ Cancelable<T?> runInIsolateGentle<T>({
stack, stack,
); );
} finally { } finally {
await LogService.I.flushBuffer(); try {
ref.read(driftProvider).close(); await LogService.I.flushBuffer();
ref.read(isarProvider).close(); await ref.read(driftProvider).close();
await ref.read(isarProvider).close();
ref.dispose();
} catch (error) {
debugPrint("Error closing resources in isolate: $error");
} finally {
ref.dispose();
// Delay to ensure all resources are released
await Future.delayed(const Duration(seconds: 2));
}
} }
return null; return null;
}); });

View File

@@ -4,6 +4,7 @@ import 'dart:io';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/utils/background_sync.dart'; import 'package:immich_mobile/domain/utils/background_sync.dart';
import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/album.entity.dart';
@@ -18,12 +19,15 @@ import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.d
import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/utils/diff.dart'; import 'package:immich_mobile/utils/diff.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
// ignore: import_rule_photo_manager // ignore: import_rule_photo_manager
import 'package:photo_manager/photo_manager.dart'; import 'package:photo_manager/photo_manager.dart';
const int targetVersion = 13; const int targetVersion = 14;
Future<void> migrateDatabaseIfNeeded(Isar db) async { Future<void> migrateDatabaseIfNeeded(Isar db) async {
final int version = Store.get(StoreKey.version, targetVersion); final int version = Store.get(StoreKey.version, targetVersion);
@@ -48,18 +52,22 @@ Future<void> migrateDatabaseIfNeeded(Isar db) async {
await _migrateDeviceAsset(db); await _migrateDeviceAsset(db);
} }
if (version < 12 && (!kReleaseMode)) { if (version < 13) {
await Store.put(StoreKey.photoManagerCustomFilter, true);
}
if (version < 14) {
if (!Store.isBetaTimelineEnabled) {
// Try again when beta timeline is enabled and the app is restarted
return;
}
final backgroundSync = BackgroundSyncManager(); final backgroundSync = BackgroundSyncManager();
await backgroundSync.syncLocal(); await backgroundSync.syncLocal();
final drift = Drift(); final drift = Drift();
await _migrateDeviceAssetToSqlite(db, drift); await migrateDeviceAssetToSqlite(db, drift);
await drift.close(); await drift.close();
} }
if (version < 13) {
await Store.put(StoreKey.photoManagerCustomFilter, true);
}
if (targetVersion >= 12) { if (targetVersion >= 12) {
await Store.put(StoreKey.version, targetVersion); await Store.put(StoreKey.version, targetVersion);
return; return;
@@ -175,33 +183,24 @@ Future<void> _migrateDeviceAsset(Isar db) async {
}); });
} }
Future<void> _migrateDeviceAssetToSqlite(Isar db, Drift drift) async { Future<void> migrateDeviceAssetToSqlite(Isar db, Drift drift) async {
try { try {
final isarDeviceAssets = final isarDeviceAssets = await db.deviceAssetEntitys.where().findAll();
await db.deviceAssetEntitys.where().sortByAssetId().findAll();
await drift.batch((batch) { await drift.batch((batch) {
for (final deviceAsset in isarDeviceAssets) { for (final deviceAsset in isarDeviceAssets) {
final companion = LocalAssetEntityCompanion( batch.update(
updatedAt: Value(deviceAsset.modifiedTime),
id: Value(deviceAsset.assetId),
checksum: Value(base64.encode(deviceAsset.hash)),
);
batch.insert<$LocalAssetEntityTable, LocalAssetEntityData>(
drift.localAssetEntity, drift.localAssetEntity,
companion, LocalAssetEntityCompanion(
onConflict: DoUpdate( checksum: Value(base64.encode(deviceAsset.hash)),
(_) => companion,
where: (old) => old.updatedAt.equals(deviceAsset.modifiedTime),
), ),
where: (t) => t.id.equals(deviceAsset.assetId),
); );
} }
}); });
} catch (error) { } catch (error) {
if (kDebugMode) { debugPrint(
debugPrint( "[MIGRATION] Error while migrating device assets to SQLite: $error",
"[MIGRATION] Error while migrating device assets to SQLite: $error", );
);
}
} }
} }
@@ -212,3 +211,18 @@ class _DeviceAsset {
const _DeviceAsset({required this.assetId, this.hash, this.dateTime}); const _DeviceAsset({required this.assetId, this.hash, this.dateTime});
} }
Future<void> runNewSync(WidgetRef ref, {bool full = false}) async {
ref.read(backupProvider.notifier).cancelBackup();
final backgroundManager = ref.read(backgroundSyncProvider);
Future.wait([
backgroundManager.syncLocal(full: full).then(
(_) {
Logger("runNewSync").fine("Hashing assets after syncLocal");
backgroundManager.hashAssets();
},
),
backgroundManager.syncRemote(),
]);
}

View File

@@ -6,7 +6,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/backup/backup_state.model.dart'; import 'package:immich_mobile/models/backup/backup_state.model.dart';
import 'package:immich_mobile/models/server_info/server_info.model.dart'; import 'package:immich_mobile/models/server_info/server_info.model.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/providers/cast.provider.dart'; import 'package:immich_mobile/providers/cast.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart';
@@ -51,7 +50,6 @@ class ImmichSliverAppBar extends ConsumerWidget {
pinned: pinned, pinned: pinned,
snap: snap, snap: snap,
expandedHeight: expandedHeight, expandedHeight: expandedHeight,
backgroundColor: context.colorScheme.surfaceContainer,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all( borderRadius: BorderRadius.all(
Radius.circular(5), Radius.circular(5),
@@ -68,24 +66,6 @@ class ImmichSliverAppBar extends ConsumerWidget {
child: action, child: action,
), ),
), ),
IconButton(
icon: const Icon(Icons.swipe_left_alt_rounded),
onPressed: () => context.pop(),
),
IconButton(
onPressed: () {
ref.read(backgroundSyncProvider).syncLocal(full: true);
ref.read(backgroundSyncProvider).syncRemote();
Future.delayed(
const Duration(seconds: 10),
() => ref.read(backgroundSyncProvider).hashAssets(),
);
},
icon: const Icon(
Icons.sync,
),
),
if (isCasting) if (isCasting)
Padding( Padding(
padding: const EdgeInsets.only(right: 12), padding: const EdgeInsets.only(right: 12),
@@ -127,13 +107,30 @@ class _ImmichLogoWithText extends StatelessWidget {
children: [ children: [
Builder( Builder(
builder: (context) { builder: (context) {
return Padding( return Badge(
padding: const EdgeInsets.only(top: 3.0), padding:
child: SvgPicture.asset( const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
context.isDarkTheme backgroundColor: context.primaryColor,
? 'assets/immich-logo-inline-dark.svg' alignment: Alignment.centerRight,
: 'assets/immich-logo-inline-light.svg', offset: const Offset(16, -8),
height: 40, label: Text(
'β',
style: TextStyle(
fontSize: 11,
color: context.colorScheme.onPrimary,
fontWeight: FontWeight.bold,
fontFamily: 'OverpassMono',
height: 1.2,
),
),
child: Padding(
padding: const EdgeInsets.only(top: 3.0),
child: SvgPicture.asset(
context.isDarkTheme
? 'assets/immich-logo-inline-dark.svg'
: 'assets/immich-logo-inline-light.svg',
height: 40,
),
), ),
); );
}, },

View File

@@ -10,6 +10,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart';
@@ -17,6 +18,7 @@ import 'package:immich_mobile/providers/gallery_permission.provider.dart';
import 'package:immich_mobile/providers/oauth.provider.dart'; import 'package:immich_mobile/providers/oauth.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/migration.dart';
import 'package:immich_mobile/utils/provider_utils.dart'; import 'package:immich_mobile/utils/provider_utils.dart';
import 'package:immich_mobile/utils/url_helper.dart'; import 'package:immich_mobile/utils/url_helper.dart';
import 'package:immich_mobile/utils/version_compatibility.dart'; import 'package:immich_mobile/utils/version_compatibility.dart';
@@ -192,6 +194,15 @@ class LoginForm extends HookConsumerWidget {
if (result.shouldChangePassword && !result.isAdmin) { if (result.shouldChangePassword && !result.isAdmin) {
context.pushRoute(const ChangePasswordRoute()); context.pushRoute(const ChangePasswordRoute());
} else { } else {
final isBeta = Store.isBetaTimelineEnabled;
if (isBeta) {
await ref
.read(galleryPermissionNotifier.notifier)
.requestGalleryPermission();
await runNewSync(ref);
context.replaceRoute(const TabShellRoute());
return;
}
context.replaceRoute(const TabControllerRoute()); context.replaceRoute(const TabControllerRoute());
} }
} catch (error) { } catch (error) {
@@ -292,9 +303,18 @@ class LoginForm extends HookConsumerWidget {
if (isSuccess) { if (isSuccess) {
isLoading.value = false; isLoading.value = false;
final permission = ref.watch(galleryPermissionNotifier); final permission = ref.watch(galleryPermissionNotifier);
if (permission.isGranted || permission.isLimited) { final isBeta = Store.isBetaTimelineEnabled;
if (!isBeta && (permission.isGranted || permission.isLimited)) {
ref.watch(backupProvider.notifier).resumeBackup(); ref.watch(backupProvider.notifier).resumeBackup();
} }
if (isBeta) {
await ref
.read(galleryPermissionNotifier.notifier)
.requestGalleryPermission();
await runNewSync(ref);
context.replaceRoute(const TabShellRoute());
return;
}
context.replaceRoute(const TabControllerRoute()); context.replaceRoute(const TabControllerRoute());
} }
} catch (error, stack) { } catch (error, stack) {

View File

@@ -0,0 +1,269 @@
import 'dart:math' as math;
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
class BetaTimelineListTile extends ConsumerStatefulWidget {
const BetaTimelineListTile({
super.key,
});
@override
ConsumerState<BetaTimelineListTile> createState() =>
_BetaTimelineListTileState();
}
class _BetaTimelineListTileState extends ConsumerState<BetaTimelineListTile>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _rotationAnimation;
late Animation<double> _pulseAnimation;
late Animation<double> _gradientAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(seconds: 3),
vsync: this,
);
_rotationAnimation = Tween<double>(begin: 0, end: 2 * math.pi).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.linear,
),
);
_pulseAnimation = Tween<double>(begin: 1, end: 1.1).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
),
);
_gradientAnimation = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
),
);
_animationController.repeat(reverse: true);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final betaTimelineValue = ref
.watch(appSettingsServiceProvider)
.getSetting<bool>(AppSettingsEnum.betaTimeline);
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
void onSwitchChanged(bool value) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: value
? const Text("Enable Beta Timeline")
: const Text("Disable Beta Timeline"),
content: value
? const Text(
"Are you sure you want to enable the beta timeline?",
)
: const Text(
"Are you sure you want to disable the beta timeline?",
),
actions: [
TextButton(
onPressed: () async {
Navigator.of(context).pop();
await ref.read(appSettingsServiceProvider).setSetting(
AppSettingsEnum.betaTimeline,
value,
);
context.router.replaceAll(
[ChangeExperienceRoute(switchingToBeta: value)],
);
},
child: const Text("Yes"),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text("No"),
),
],
);
},
);
}
final gradientColors = [
Color.lerp(
context.primaryColor.withValues(alpha: 0.3),
context.primaryColor.withValues(alpha: 0.1),
_gradientAnimation.value,
)!,
Color.lerp(
context.primaryColor.withValues(alpha: 0.2),
context.primaryColor.withValues(alpha: 0.4),
_gradientAnimation.value,
)!,
Color.lerp(
context.primaryColor.withValues(alpha: 0.1),
context.primaryColor.withValues(alpha: 0.3),
_gradientAnimation.value,
)!,
];
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(12)),
gradient: LinearGradient(
colors: gradientColors,
stops: const [0.0, 0.5, 1.0],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
transform: GradientRotation(_rotationAnimation.value * 0.1),
),
boxShadow: [
BoxShadow(
color: context.primaryColor.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Container(
margin: const EdgeInsets.all(1.5),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(10.5)),
color: context.scaffoldBackgroundColor,
),
child: Material(
color: Colors.transparent,
borderRadius: const BorderRadius.all(Radius.circular(10.5)),
child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(10.5)),
onTap: () => onSwitchChanged(!betaTimelineValue),
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
child: Row(
children: [
Transform.scale(
scale: _pulseAnimation.value,
child: Transform.rotate(
angle: _rotationAnimation.value * 0.02,
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [
context.primaryColor.withValues(alpha: 0.2),
context.primaryColor.withValues(alpha: 0.1),
],
),
),
child: Icon(
Icons.auto_awesome,
color: context.primaryColor,
size: 20,
),
),
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"advanced_settings_beta_timeline_title"
.t(context: context),
style:
context.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
gradient: LinearGradient(
colors: [
context.primaryColor
.withValues(alpha: 0.8),
context.primaryColor
.withValues(alpha: 0.6),
],
),
),
child: Text(
'NEW',
style:
context.textTheme.labelSmall?.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 10,
height: 1.2,
),
),
),
],
),
const SizedBox(height: 4),
Text(
"advanced_settings_beta_timeline_subtitle"
.t(context: context),
style: context.textTheme.labelLarge?.copyWith(
color: context.textTheme.labelLarge?.color
?.withValues(alpha: 0.7),
),
),
],
),
),
Switch.adaptive(
value: betaTimelineValue,
onChanged: onSwitchChanged,
activeColor: context.primaryColor,
),
],
),
),
),
),
),
);
},
);
}
}

View File

@@ -139,7 +139,6 @@ flutter:
- family: OverpassMono - family: OverpassMono
fonts: fonts:
- asset: fonts/overpass/OverpassMono.ttf - asset: fonts/overpass/OverpassMono.ttf
flutter_launcher_icons: flutter_launcher_icons:
image_path_android: 'assets/immich-logo.png' image_path_android: 'assets/immich-logo.png'
adaptive_icon_background: '#ffffff' adaptive_icon_background: '#ffffff'

View File

@@ -0,0 +1,23 @@
// dart format width=80
// GENERATED CODE, DO NOT EDIT BY HAND.
// ignore_for_file: type=lint
import 'package:drift/drift.dart';
import 'package:drift/internal/migrations.dart';
import 'schema_v1.dart' as v1;
import 'schema_v2.dart' as v2;
class GeneratedHelper implements SchemaInstantiationHelper {
@override
GeneratedDatabase databaseForVersion(QueryExecutor db, int version) {
switch (version) {
case 1:
return v1.DatabaseAtV1(db);
case 2:
return v2.DatabaseAtV2(db);
default:
throw MissingSchemaException(version, versions);
}
}
static const versions = const [1, 2];
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,38 @@
// dart format width=80
// ignore_for_file: unused_local_variable, unused_import
import 'package:drift/drift.dart';
import 'package:drift_dev/api/migrations_native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'generated/schema.dart';
import 'generated/schema_v1.dart' as v1;
import 'generated/schema_v2.dart' as v2;
void main() {
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
late SchemaVerifier verifier;
setUpAll(() {
verifier = SchemaVerifier(GeneratedHelper());
});
group('simple database migrations', () {
// These simple tests verify all possible schema updates with a simple (no
// data) migration. This is a quick way to ensure that written database
// migrations properly alter the schema.
const versions = GeneratedHelper.versions;
for (final (i, fromVersion) in versions.indexed) {
group('from $fromVersion', () {
for (final toVersion in versions.skip(i + 1)) {
test('to $toVersion', () async {
final schema = await verifier.schemaAt(fromVersion);
final db = Drift(schema.newConnection());
await verifier.migrateAndValidate(db, toVersion);
await db.close();
});
}
});
}
});
}