optimizations

This commit is contained in:
mertalev 2025-07-30 19:30:42 -04:00
parent fb510e9853
commit e416c971e7
No known key found for this signature in database
GPG Key ID: DF6ABC77AAD98C95
24 changed files with 7022 additions and 329 deletions

File diff suppressed because one or more lines are too long

View File

@ -4,10 +4,7 @@ class Marker {
final LatLng location; final LatLng location;
final String assetId; final String assetId;
const Marker({ const Marker({required this.location, required this.assetId});
required this.location,
required this.assetId,
});
@override @override
bool operator ==(covariant Marker other) { bool operator ==(covariant Marker other) {

View File

@ -2,37 +2,22 @@ import 'package:immich_mobile/domain/models/map.model.dart';
import 'package:immich_mobile/infrastructure/repositories/map.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/map.repository.dart';
import 'package:maplibre_gl/maplibre_gl.dart'; import 'package:maplibre_gl/maplibre_gl.dart';
typedef MapMarkerSource = Stream<List<Marker>> Function(LatLngBounds? bounds); typedef MapMarkerSource = Future<List<Marker>> Function(LatLngBounds? bounds);
typedef MapQuery = ({ typedef MapQuery = ({MapMarkerSource markerSource});
MapMarkerSource markerSource,
});
class MapFactory { class MapFactory {
final DriftMapRepository _mapRepository; final DriftMapRepository _mapRepository;
const MapFactory({ const MapFactory({required DriftMapRepository mapRepository}) : _mapRepository = mapRepository;
required DriftMapRepository mapRepository,
}) : _mapRepository = mapRepository;
MapService remote(String ownerId) => MapService remote(String ownerId) => MapService(_mapRepository.remote(ownerId));
MapService(_mapRepository.remote(ownerId));
} }
class MapService { class MapService {
final MapMarkerSource _markerSource; final MapMarkerSource _markerSource;
MapService(MapQuery query) MapService(MapQuery query) : _markerSource = query.markerSource;
: this._(
markerSource: query.markerSource,
);
MapService._({ Future<List<Marker>> Function(LatLngBounds? bounds) get getMarkers => _markerSource;
required MapMarkerSource markerSource,
}) : _markerSource = markerSource;
Stream<List<Marker>> Function(LatLngBounds? bounds) get watchMarkers =>
_markerSource;
Future<void> dispose() async {}
} }

View File

@ -59,8 +59,7 @@ class TimelineFactory {
TimelineService fromAssets(List<BaseAsset> assets) => TimelineService(_timelineRepository.fromAssets(assets)); TimelineService fromAssets(List<BaseAsset> assets) => TimelineService(_timelineRepository.fromAssets(assets));
TimelineService map(LatLngBounds bounds) => TimelineService map(LatLngBounds bounds) => TimelineService(_timelineRepository.map(bounds, groupBy));
TimelineService(_timelineRepository.map(bounds, groupBy));
} }
class TimelineService { class TimelineService {

View File

@ -95,6 +95,7 @@ class ExifInfo {
); );
} }
@TableIndex(name: 'idx_lat_lng', columns: {#latitude, #longitude})
class RemoteExifEntity extends Table with DriftDefaultsMixin { class RemoteExifEntity extends Table with DriftDefaultsMixin {
const RemoteExifEntity(); const RemoteExifEntity();

View File

@ -21,7 +21,7 @@ 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:immich_mobile/infrastructure/repositories/db.repository.steps.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart' hide Index;
import 'db.repository.drift.dart'; import 'db.repository.drift.dart';
@ -66,7 +66,7 @@ class Drift extends $Drift implements IDatabaseRepository {
: super(executor ?? driftDatabase(name: 'immich', native: const DriftNativeOptions(shareAcrossIsolates: true))); : super(executor ?? driftDatabase(name: 'immich', native: const DriftNativeOptions(shareAcrossIsolates: true)));
@override @override
int get schemaVersion => 6; int get schemaVersion => 7;
@override @override
MigrationStrategy get migration => MigrationStrategy( MigrationStrategy get migration => MigrationStrategy(
@ -112,6 +112,9 @@ class Drift extends $Drift implements IDatabaseRepository {
await m.create(v6.uQRemoteAssetsOwnerChecksum); await m.create(v6.uQRemoteAssetsOwnerChecksum);
await m.create(v6.uQRemoteAssetsOwnerLibraryChecksum); await m.create(v6.uQRemoteAssetsOwnerLibraryChecksum);
}, },
from6To7: (m, v7) async {
await m.createIndex(v7.idxLatLng);
},
), ),
); );

View File

@ -2705,12 +2705,357 @@ i1.GeneratedColumn<String> _column_86(String aliasedName) =>
true, true,
type: i1.DriftSqlType.string, type: i1.DriftSqlType.string,
); );
final class Schema7 extends i0.VersionedSchema {
Schema7({required super.database}) : super(version: 7);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
userEntity,
remoteAssetEntity,
stackEntity,
localAssetEntity,
localAlbumEntity,
localAlbumAssetEntity,
idxLocalAssetChecksum,
idxRemoteAssetOwnerChecksum,
uQRemoteAssetsOwnerChecksum,
uQRemoteAssetsOwnerLibraryChecksum,
idxRemoteAssetChecksum,
userMetadataEntity,
partnerEntity,
remoteExifEntity,
remoteAlbumEntity,
remoteAlbumAssetEntity,
remoteAlbumUserEntity,
memoryEntity,
memoryAssetEntity,
personEntity,
assetFaceEntity,
idxLatLng,
];
late final Shape16 userEntity = Shape16(
source: i0.VersionedTable(
entityName: 'user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_1,
_column_2,
_column_3,
_column_84,
_column_85,
_column_5,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape17 remoteAssetEntity = Shape17(
source: i0.VersionedTable(
entityName: 'remote_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_1,
_column_8,
_column_9,
_column_5,
_column_10,
_column_11,
_column_12,
_column_0,
_column_13,
_column_14,
_column_15,
_column_16,
_column_17,
_column_18,
_column_19,
_column_20,
_column_21,
_column_86,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape3 stackEntity = Shape3(
source: i0.VersionedTable(
entityName: 'stack_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [_column_0, _column_9, _column_5, _column_15, _column_75],
attachedDatabase: database,
),
alias: null,
);
late final Shape2 localAssetEntity = Shape2(
source: i0.VersionedTable(
entityName: 'local_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_1,
_column_8,
_column_9,
_column_5,
_column_10,
_column_11,
_column_12,
_column_0,
_column_22,
_column_14,
_column_23,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape6 localAlbumEntity = Shape6(
source: i0.VersionedTable(
entityName: 'local_album_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_1,
_column_5,
_column_31,
_column_32,
_column_33,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape7 localAlbumAssetEntity = Shape7(
source: i0.VersionedTable(
entityName: 'local_album_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
columns: [_column_34, _column_35],
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 idxRemoteAssetOwnerChecksum = i1.Index(
'idx_remote_asset_owner_checksum',
'CREATE INDEX idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)',
);
final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index(
'UQ_remote_assets_owner_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
);
final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index(
'UQ_remote_assets_owner_library_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)',
);
final i1.Index idxRemoteAssetChecksum = i1.Index(
'idx_remote_asset_checksum',
'CREATE INDEX idx_remote_asset_checksum ON remote_asset_entity (checksum)',
);
late final Shape4 userMetadataEntity = Shape4(
source: i0.VersionedTable(
entityName: 'user_metadata_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(user_id, "key")'],
columns: [_column_25, _column_26, _column_27],
attachedDatabase: database,
),
alias: null,
);
late final Shape5 partnerEntity = Shape5(
source: i0.VersionedTable(
entityName: 'partner_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'],
columns: [_column_28, _column_29, _column_30],
attachedDatabase: database,
),
alias: null,
);
late final Shape8 remoteExifEntity = Shape8(
source: i0.VersionedTable(
entityName: 'remote_exif_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id)'],
columns: [
_column_36,
_column_37,
_column_38,
_column_39,
_column_40,
_column_41,
_column_11,
_column_10,
_column_42,
_column_43,
_column_44,
_column_45,
_column_46,
_column_47,
_column_48,
_column_49,
_column_50,
_column_51,
_column_52,
_column_53,
_column_54,
_column_55,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape9 remoteAlbumEntity = Shape9(
source: i0.VersionedTable(
entityName: 'remote_album_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_1,
_column_56,
_column_9,
_column_5,
_column_15,
_column_57,
_column_58,
_column_59,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape7 remoteAlbumAssetEntity = Shape7(
source: i0.VersionedTable(
entityName: 'remote_album_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
columns: [_column_36, _column_60],
attachedDatabase: database,
),
alias: null,
);
late final Shape10 remoteAlbumUserEntity = Shape10(
source: i0.VersionedTable(
entityName: 'remote_album_user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(album_id, user_id)'],
columns: [_column_60, _column_25, _column_61],
attachedDatabase: database,
),
alias: null,
);
late final Shape11 memoryEntity = Shape11(
source: i0.VersionedTable(
entityName: 'memory_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_9,
_column_5,
_column_18,
_column_15,
_column_8,
_column_62,
_column_63,
_column_64,
_column_65,
_column_66,
_column_67,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape12 memoryAssetEntity = Shape12(
source: i0.VersionedTable(
entityName: 'memory_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'],
columns: [_column_36, _column_68],
attachedDatabase: database,
),
alias: null,
);
late final Shape14 personEntity = Shape14(
source: i0.VersionedTable(
entityName: 'person_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_9,
_column_5,
_column_15,
_column_1,
_column_69,
_column_71,
_column_72,
_column_73,
_column_74,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape15 assetFaceEntity = Shape15(
source: i0.VersionedTable(
entityName: 'asset_face_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_36,
_column_76,
_column_77,
_column_78,
_column_79,
_column_80,
_column_81,
_column_82,
_column_83,
],
attachedDatabase: database,
),
alias: null,
);
final i1.Index idxLatLng = i1.Index(
'idx_lat_lng',
'CREATE INDEX idx_lat_lng ON remote_exif_entity (latitude, longitude)',
);
}
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,
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4, required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
required Future<void> Function(i1.Migrator m, Schema5 schema) from4To5, required Future<void> Function(i1.Migrator m, Schema5 schema) from4To5,
required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6, required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6,
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
}) { }) {
return (currentVersion, database) async { return (currentVersion, database) async {
switch (currentVersion) { switch (currentVersion) {
@ -2739,6 +3084,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema); final migrator = i1.Migrator(database, schema);
await from5To6(migrator, schema); await from5To6(migrator, schema);
return 6; return 6;
case 6:
final schema = Schema7(database: database);
final migrator = i1.Migrator(database, schema);
await from6To7(migrator, schema);
return 7;
default: default:
throw ArgumentError.value('Unknown migration from $currentVersion'); throw ArgumentError.value('Unknown migration from $currentVersion');
} }
@ -2751,6 +3101,7 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4, required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
required Future<void> Function(i1.Migrator m, Schema5 schema) from4To5, required Future<void> Function(i1.Migrator m, Schema5 schema) from4To5,
required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6, required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6,
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
}) => i0.VersionedSchema.stepByStepHelper( }) => i0.VersionedSchema.stepByStepHelper(
step: migrationSteps( step: migrationSteps(
from1To2: from1To2, from1To2: from1To2,
@ -2758,5 +3109,6 @@ i1.OnUpgrade stepByStep({
from3To4: from3To4, from3To4: from3To4,
from4To5: from4To5, from4To5: from4To5,
from5To6: from5To6, from5To6: from5To6,
from6To7: from6To7,
), ),
); );

View File

@ -6,7 +6,6 @@ import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:maplibre_gl/maplibre_gl.dart'; import 'package:maplibre_gl/maplibre_gl.dart';
import 'package:stream_transform/stream_transform.dart';
class DriftMapRepository extends DriftDatabaseRepository { class DriftMapRepository extends DriftDatabaseRepository {
final Drift _db; final Drift _db;
@ -14,88 +13,59 @@ class DriftMapRepository extends DriftDatabaseRepository {
const DriftMapRepository(super._db) : _db = _db; const DriftMapRepository(super._db) : _db = _db;
MapQuery remote(String ownerId) => _mapQueryBuilder( MapQuery remote(String ownerId) => _mapQueryBuilder(
assetFilter: (row) => assetFilter: (row) =>
row.deletedAt.isNull() & row.deletedAt.isNull() & row.visibility.equalsValue(AssetVisibility.timeline) & row.ownerId.equals(ownerId),
row.visibility.equalsValue(AssetVisibility.timeline) & );
row.ownerId.equals(ownerId),
);
MapQuery _mapQueryBuilder({ MapQuery _mapQueryBuilder({Expression<bool> Function($RemoteAssetEntityTable row)? assetFilter}) {
Expression<bool> Function($RemoteAssetEntityTable row)? assetFilter, return (markerSource: (bounds) => _watchMapMarker(assetFilter: assetFilter, bounds: bounds));
Expression<bool> Function($RemoteExifEntityTable row)? exifFilter,
}) {
return (
markerSource: (bounds) => _watchMapMarker(
assetFilter: assetFilter,
exifFilter: exifFilter,
bounds: bounds,
)
);
} }
Stream<List<Marker>> _watchMapMarker({ Future<List<Marker>> _watchMapMarker({
Expression<bool> Function($RemoteAssetEntityTable row)? assetFilter, Expression<bool> Function($RemoteAssetEntityTable row)? assetFilter,
Expression<bool> Function($RemoteExifEntityTable row)? exifFilter,
LatLngBounds? bounds, LatLngBounds? bounds,
}) { }) {
final query = _db.remoteExifEntity.select().join([ final query = _db.remoteExifEntity.selectOnly()
innerJoin( ..addColumns([_db.remoteExifEntity.assetId, _db.remoteExifEntity.latitude, _db.remoteExifEntity.longitude])
_db.remoteAssetEntity, ..join([
_db.remoteAssetEntity.id.equalsExp(_db.remoteExifEntity.assetId), innerJoin(
useColumns: false, _db.remoteAssetEntity,
), _db.remoteAssetEntity.id.equalsExp(_db.remoteExifEntity.assetId),
]) useColumns: false,
..where( ),
_db.remoteExifEntity.latitude.isNotNull() & ])
_db.remoteExifEntity.longitude.isNotNull(), ..limit(10000);
);
if (assetFilter != null) { if (assetFilter != null) {
query.where(assetFilter(_db.remoteAssetEntity)); query.where(assetFilter(_db.remoteAssetEntity));
} }
if (exifFilter != null) {
query.where(exifFilter(_db.remoteExifEntity));
}
if (bounds != null) { if (bounds != null) {
query.where(_db.remoteExifEntity.inBounds(bounds)); query.where(_db.remoteExifEntity.inBounds(bounds));
} else {
query.where(_db.remoteExifEntity.latitude.isNotNull() & _db.remoteExifEntity.longitude.isNotNull());
} }
return query return query.map((row) {
.map((row) => row.readTable(_db.remoteExifEntity).toMarker()) return Marker(
.watch() assetId: row.read(_db.remoteExifEntity.assetId)!,
.throttle(const Duration(seconds: 3)); location: LatLng(row.read(_db.remoteExifEntity.latitude)!, row.read(_db.remoteExifEntity.longitude)!),
);
}).get();
} }
} }
extension MapBounds on $RemoteExifEntityTable { extension MapBounds on $RemoteExifEntityTable {
Expression<bool> inBounds(LatLngBounds bounds) { Expression<bool> inBounds(LatLngBounds bounds) {
final isLatitudeInBounds = final southwest = bounds.southwest;
latitude.isBiggerOrEqualValue(bounds.southwest.latitude) & final northeast = bounds.northeast;
latitude.isSmallerOrEqualValue(bounds.northeast.latitude);
final Expression<bool> isLongitudeInBounds; if (southwest.longitude <= northeast.longitude) {
return latitude.isBetweenValues(southwest.latitude, northeast.latitude) &
if (bounds.southwest.longitude <= bounds.northeast.longitude) { longitude.isBetweenValues(southwest.longitude, northeast.longitude);
isLongitudeInBounds =
longitude.isBiggerOrEqualValue(bounds.southwest.longitude) &
longitude.isSmallerOrEqualValue(bounds.northeast.longitude);
} else { } else {
isLongitudeInBounds = return latitude.isBetweenValues(southwest.latitude, northeast.latitude) &
longitude.isBiggerOrEqualValue(bounds.southwest.longitude) | (longitude.isBiggerOrEqualValue(southwest.longitude) | longitude.isSmallerOrEqualValue(northeast.longitude));
longitude.isSmallerOrEqualValue(bounds.northeast.longitude);
} }
return isLatitudeInBounds & isLongitudeInBounds;
}
}
extension on RemoteExifEntityData {
Marker toMarker() {
return Marker(
assetId: assetId,
location: LatLng(latitude!, longitude!),
);
} }
} }

View File

@ -430,26 +430,14 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
} }
TimelineQuery map(LatLngBounds bounds, GroupAssetsBy groupBy) => ( TimelineQuery map(LatLngBounds bounds, GroupAssetsBy groupBy) => (
bucketSource: () => _watchMapBucket( bucketSource: () => _watchMapBucket(bounds, groupBy: groupBy),
bounds, assetSource: (offset, count) => _getMapBucketAssets(bounds, offset: offset, count: count),
groupBy: groupBy, );
),
assetSource: (offset, count) => _getMapBucketAssets(
bounds,
offset: offset,
count: count,
),
);
Stream<List<Bucket>> _watchMapBucket( Stream<List<Bucket>> _watchMapBucket(LatLngBounds bounds, {GroupAssetsBy groupBy = GroupAssetsBy.day}) {
LatLngBounds bounds, {
GroupAssetsBy groupBy = GroupAssetsBy.day,
}) {
if (groupBy == GroupAssetsBy.none) { if (groupBy == GroupAssetsBy.none) {
// TODO: Support GroupAssetsBy.none // TODO: Support GroupAssetsBy.none
throw UnsupportedError( throw UnsupportedError("GroupAssetsBy.none is not supported for _watchMapBucket");
"GroupAssetsBy.none is not supported for _watchMapBucket",
);
} }
final assetCountExp = _db.remoteAssetEntity.id.count(); final assetCountExp = _db.remoteAssetEntity.id.count();
@ -468,8 +456,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
_db.remoteExifEntity.latitude.isNotNull() & _db.remoteExifEntity.latitude.isNotNull() &
_db.remoteExifEntity.longitude.isNotNull() & _db.remoteExifEntity.longitude.isNotNull() &
_db.remoteExifEntity.inBounds(bounds) & _db.remoteExifEntity.inBounds(bounds) &
_db.remoteAssetEntity.visibility _db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) &
.equalsValue(AssetVisibility.timeline) &
_db.remoteAssetEntity.deletedAt.isNull(), _db.remoteAssetEntity.deletedAt.isNull(),
) )
..groupBy([dateExp]) ..groupBy([dateExp])
@ -482,33 +469,25 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
}).watch(); }).watch();
} }
Future<List<BaseAsset>> _getMapBucketAssets( Future<List<BaseAsset>> _getMapBucketAssets(LatLngBounds bounds, {required int offset, required int count}) {
LatLngBounds bounds, { final query =
required int offset, _db.remoteAssetEntity.select().join([
required int count, innerJoin(
}) { _db.remoteExifEntity,
final query = _db.remoteAssetEntity.select().join( _db.remoteExifEntity.assetId.equalsExp(_db.remoteAssetEntity.id),
[ useColumns: false,
innerJoin( ),
_db.remoteExifEntity, ])
_db.remoteExifEntity.assetId.equalsExp(_db.remoteAssetEntity.id), ..where(
useColumns: false, _db.remoteExifEntity.latitude.isNotNull() &
), _db.remoteExifEntity.longitude.isNotNull() &
], _db.remoteExifEntity.inBounds(bounds) &
) _db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) &
..where( _db.remoteAssetEntity.deletedAt.isNull(),
_db.remoteExifEntity.latitude.isNotNull() & )
_db.remoteExifEntity.longitude.isNotNull() & ..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)])
_db.remoteExifEntity.inBounds(bounds) & ..limit(count, offset: offset);
_db.remoteAssetEntity.visibility return query.map((row) => row.readTable(_db.remoteAssetEntity).toDto()).get();
.equalsValue(AssetVisibility.timeline) &
_db.remoteAssetEntity.deletedAt.isNull(),
)
..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)])
..limit(count, offset: offset);
return query
.map((row) => row.readTable(_db.remoteAssetEntity).toDto())
.get();
} }
TimelineQuery _remoteQueryBuilder({ TimelineQuery _remoteQueryBuilder({

View File

@ -22,11 +22,7 @@ final _features = [
icon: Icons.timeline_rounded, icon: Icons.timeline_rounded,
onTap: (ctx, _) => ctx.pushRoute(const TabShellRoute()), onTap: (ctx, _) => ctx.pushRoute(const TabShellRoute()),
), ),
_Feature( _Feature(name: 'Map', icon: Icons.map_outlined, onTap: (ctx, _) => ctx.pushRoute(const DriftMapRoute())),
name: 'Map',
icon: Icons.map_outlined,
onTap: (ctx, _) => ctx.pushRoute(const DriftMapRoute()),
),
_Feature( _Feature(
name: 'Selection Mode Timeline', name: 'Selection Mode Timeline',
icon: Icons.developer_mode_rounded, icon: Icons.developer_mode_rounded,

View File

@ -8,9 +8,6 @@ class DriftMapPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const Scaffold( return const Scaffold(extendBodyBehindAppBar: true, body: DriftMap());
extendBodyBehindAppBar: true,
body: DriftMap(),
);
} }
} }

View File

@ -94,7 +94,11 @@ class _Map extends StatelessWidget {
width: context.width, width: context.width,
// TODO: migrate to DriftMapRoute after merging #19898 // TODO: migrate to DriftMapRoute after merging #19898
child: MapThumbnail( child: MapThumbnail(
onTap: (_, __) => context.pushRoute(MapRoute(initialLocation: currentLocation)), onTap: (_, __) => context.pushRoute(
const DriftMapRoute(
// initialLocation: currentLocation
),
),
zoom: 8, zoom: 8,
centre: currentLocation ?? const LatLng(21.44950, -157.91959), centre: currentLocation ?? const LatLng(21.44950, -157.91959),
showAttribution: false, showAttribution: false,

View File

@ -16,15 +16,14 @@ class MapBottomSheet extends ConsumerWidget {
return BaseBottomSheet( return BaseBottomSheet(
initialChildSize: 0.25, initialChildSize: 0.25,
shouldCloseOnMinExtent: false, shouldCloseOnMinExtent: false,
actions: [], actions: const [],
slivers: [ slivers: [
SliverFillRemaining( SliverFillRemaining(
child: ProviderScope( child: ProviderScope(
key: ObjectKey(bounds), key: ObjectKey(bounds),
overrides: [ overrides: [
timelineServiceProvider.overrideWith((ref) { timelineServiceProvider.overrideWith((ref) {
final timelineService = final timelineService = ref.watch(timelineFactoryProvider).map(bounds);
ref.watch(timelineFactoryProvider).map(bounds);
ref.onDispose(timelineService.dispose); ref.onDispose(timelineService.dispose);
return timelineService; return timelineService;
}), }),

View File

@ -1,5 +1,4 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/presentation/widgets/map/marker_build.dart';
import 'package:immich_mobile/providers/infrastructure/map.provider.dart'; import 'package:immich_mobile/providers/infrastructure/map.provider.dart';
import 'package:maplibre_gl/maplibre_gl.dart'; import 'package:maplibre_gl/maplibre_gl.dart';
@ -24,35 +23,39 @@ class MapState {
class MapStateNotifier extends Notifier<MapState> { class MapStateNotifier extends Notifier<MapState> {
MapStateNotifier(); MapStateNotifier();
void setBounds(LatLngBounds bounds) { bool setBounds(LatLngBounds bounds) {
if (state.bounds == bounds) {
return false;
}
state = state.copyWith(bounds: bounds); state = state.copyWith(bounds: bounds);
return true;
} }
@override @override
MapState build() => MapState( MapState build() => MapState(
// TODO: set default bounds // TODO: set default bounds
bounds: LatLngBounds( bounds: LatLngBounds(northeast: const LatLng(0, 0), southwest: const LatLng(0, 0)),
northeast: const LatLng(0, 0), );
southwest: const LatLng(0, 0),
),
);
} }
// This provider watches the markers from the map service and serves the markers. // This provider watches the markers from the map service and serves the markers.
// It should be used only after the map service provider is overridden // It should be used only after the map service provider is overridden
final mapMarkerProvider = final mapMarkerProvider = FutureProvider.family<Map<String, dynamic>, LatLngBounds?>((ref, bounds) async {
StreamProvider.family<Map<String, dynamic>, LatLngBounds?>( final mapService = ref.watch(mapServiceProvider);
(ref, bounds) async* { final markers = await mapService.getMarkers(bounds);
final mapService = ref.watch(mapServiceProvider); final features = List.filled(markers.length, const <String, dynamic>{});
yield* mapService.watchMarkers(bounds).map((markers) { for (int i = 0; i < markers.length; i++) {
return MarkerBuilder( final marker = markers[i];
markers: markers, features[i] = {
).generate(); 'type': 'Feature',
}); 'id': marker.assetId,
}, 'geometry': {
dependencies: [mapServiceProvider], 'type': 'Point',
); 'coordinates': [marker.location.longitude, marker.location.latitude],
},
};
}
return {'type': 'FeatureCollection', 'features': features};
}, dependencies: [mapServiceProvider]);
final mapStateProvider = NotifierProvider<MapStateNotifier, MapState>( final mapStateProvider = NotifierProvider<MapStateNotifier, MapState>(MapStateNotifier.new);
MapStateNotifier.new,
);

View File

@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
@ -11,10 +10,29 @@ import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/bottom_sheet/map_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/map_bottom_sheet.widget.dart';
import 'package:immich_mobile/presentation/widgets/map/map_utils.dart'; import 'package:immich_mobile/presentation/widgets/map/map_utils.dart';
import 'package:immich_mobile/presentation/widgets/map/map.state.dart'; import 'package:immich_mobile/presentation/widgets/map/map.state.dart';
import 'package:immich_mobile/utils/async_mutex.dart';
import 'package:immich_mobile/utils/debounce.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:immich_mobile/widgets/map/map_theme_override.dart'; import 'package:immich_mobile/widgets/map/map_theme_override.dart';
import 'package:maplibre_gl/maplibre_gl.dart'; import 'package:maplibre_gl/maplibre_gl.dart';
class CustomSourceProperties implements SourceProperties {
final Map<String, dynamic> data;
const CustomSourceProperties({required this.data});
@override
Map<String, dynamic> toJson() {
return {
"type": "geojson",
"data": data,
// "cluster": true,
// "clusterRadius": 1,
// "clusterMinPoints": 5,
// "tolerance": 0.1,
};
}
}
class DriftMap extends ConsumerStatefulWidget { class DriftMap extends ConsumerStatefulWidget {
const DriftMap({super.key}); const DriftMap({super.key});
@ -24,7 +42,8 @@ class DriftMap extends ConsumerStatefulWidget {
class _DriftMapState extends ConsumerState<DriftMap> { class _DriftMapState extends ConsumerState<DriftMap> {
MapLibreMapController? mapController; MapLibreMapController? mapController;
bool loadAllMarkers = false; final _reloadMutex = AsyncMutex();
final _debouncer = Debouncer(interval: const Duration(milliseconds: 250), maxWaitTime: const Duration(seconds: 2));
@override @override
void initState() { void initState() {
@ -33,76 +52,69 @@ class _DriftMapState extends ConsumerState<DriftMap> {
@override @override
void dispose() { void dispose() {
mapController?.removeListener(onMapMoved);
mapController?.dispose();
_debouncer.dispose();
super.dispose(); super.dispose();
} }
Future<void> onMapCreated(MapLibreMapController controller) async { void onMapCreated(MapLibreMapController controller) {
mapController = controller; mapController = controller;
await setBounds();
} }
Future<void> onMapMoved() async { Future<void> onMapReady() async {
await setBounds(); final controller = mapController;
if (controller == null) {
return;
}
await controller.addSource(
MapUtils.defaultSourceId,
const CustomSourceProperties(data: {'type': 'FeatureCollection', 'features': []}),
);
await controller.addHeatmapLayer(
MapUtils.defaultSourceId,
MapUtils.defaultHeatMapLayerId,
MapUtils.defaultHeatmapLayerProperties,
);
controller.addListener(onMapMoved);
}
void onMapMoved() {
if (mapController!.isCameraMoving || !mounted) {
return;
}
_debouncer.run(setBounds);
} }
Future<void> setBounds() async { Future<void> setBounds() async {
if (mapController == null) return; final controller = mapController;
final bounds = await mapController!.getVisibleRegion(); if (controller == null || !mounted) {
ref.read(mapStateProvider.notifier).setBounds(bounds); return;
}
final bounds = await controller.getVisibleRegion();
_reloadMutex.run(() async {
if (mounted && ref.read(mapStateProvider.notifier).setBounds(bounds)) {
final markers = await ref.read(mapMarkerProvider(bounds).future);
await reloadMarkers(markers);
}
});
} }
Future<void> reloadMarkers( Future<void> reloadMarkers(Map<String, dynamic> markers) async {
Map<String, dynamic> markers, { final controller = mapController;
bool isLoadAllMarkers = false, if (controller == null || !mounted) {
}) async { return;
if (mapController == null || loadAllMarkers) return;
// Wait for previous reload to complete
if (!MapUtils.markerCompleter.isCompleted) {
return MapUtils.markerCompleter.future;
}
MapUtils.markerCompleter = Completer();
// !! Make sure to remove layers before sources else the native
// maplibre library would crash when removing the source saying that
// the source is still in use
final existingLayers = await mapController!.getLayerIds();
if (existingLayers.contains(MapUtils.defaultHeatMapLayerId)) {
await mapController!.removeLayer(MapUtils.defaultHeatMapLayerId);
} }
final existingSources = await mapController!.getSourceIds(); await controller.setGeoJsonSource(MapUtils.defaultSourceId, markers);
if (existingSources.contains(MapUtils.defaultSourceId)) {
await mapController!.removeSource(MapUtils.defaultSourceId);
}
await mapController!.addSource(
MapUtils.defaultSourceId,
GeojsonSourceProperties(data: markers),
);
if (Platform.isAndroid) {
await mapController!.addCircleLayer(
MapUtils.defaultSourceId,
MapUtils.defaultHeatMapLayerId,
MapUtils.defaultCircleLayerLayerProperties,
);
} else if (Platform.isIOS) {
await mapController!.addHeatmapLayer(
MapUtils.defaultSourceId,
MapUtils.defaultHeatMapLayerId,
MapUtils.defaultHeatmapLayerProperties,
);
}
if (isLoadAllMarkers) loadAllMarkers = true;
MapUtils.markerCompleter.complete();
} }
Future<void> onZoomToLocation() async { Future<void> onZoomToLocation() async {
final (location, error) = final (location, error) = await MapUtils.checkPermAndGetLocation(context: context);
await MapUtils.checkPermAndGetLocation(context: context);
if (error != null) { if (error != null) {
if (error == LocationPermission.unableToDetermine && context.mounted) { if (error == LocationPermission.unableToDetermine && context.mounted) {
ImmichToast.show( ImmichToast.show(
@ -115,12 +127,10 @@ class _DriftMapState extends ConsumerState<DriftMap> {
return; return;
} }
if (mapController != null && location != null) { final controller = mapController;
mapController!.animateCamera( if (controller != null && location != null) {
CameraUpdate.newLatLngZoom( controller.animateCamera(
LatLng(location.latitude, location.longitude), CameraUpdate.newLatLngZoom(LatLng(location.latitude, location.longitude), MapUtils.mapZoomToAssetLevel),
MapUtils.mapZoomToAssetLevel,
),
duration: const Duration(milliseconds: 800), duration: const Duration(milliseconds: 800),
); );
} }
@ -128,28 +138,9 @@ class _DriftMapState extends ConsumerState<DriftMap> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bounds = ref.watch(mapStateProvider.select((s) => s.bounds));
AsyncValue<Map<String, dynamic>> markers =
ref.watch(mapMarkerProvider(bounds));
AsyncValue<Map<String, dynamic>> allMarkers =
ref.watch(mapMarkerProvider(null));
ref.listen(mapStateProvider, (_, __) async {
if (!loadAllMarkers) {
markers = ref.watch(mapMarkerProvider(bounds));
}
});
markers.whenData((markers) => reloadMarkers(markers));
allMarkers
.whenData((markers) => reloadMarkers(markers, isLoadAllMarkers: true));
return Stack( return Stack(
children: [ children: [
_Map( _Map(onMapCreated: onMapCreated, onMapReady: onMapReady),
onMapCreated: onMapCreated,
onMapMoved: onMapMoved,
),
_MyLocationButton(onZoomToLocation: onZoomToLocation), _MyLocationButton(onZoomToLocation: onZoomToLocation),
const MapBottomSheet(), const MapBottomSheet(),
], ],
@ -158,26 +149,21 @@ class _DriftMapState extends ConsumerState<DriftMap> {
} }
class _Map extends StatelessWidget { class _Map extends StatelessWidget {
const _Map({ const _Map({required this.onMapCreated, required this.onMapReady});
required this.onMapCreated,
required this.onMapMoved,
});
final MapCreatedCallback onMapCreated; final MapCreatedCallback onMapCreated;
final OnCameraIdleCallback onMapMoved;
final VoidCallback onMapReady;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MapThemeOverride( return MapThemeOverride(
mapBuilder: (style) => style.widgetWhen( mapBuilder: (style) => style.widgetWhen(
onData: (style) => MapLibreMap( onData: (style) => MapLibreMap(
initialCameraPosition: const CameraPosition( initialCameraPosition: const CameraPosition(target: LatLng(0, 0), zoom: 0),
target: LatLng(0, 0),
zoom: 0,
),
styleString: style, styleString: style,
onMapCreated: onMapCreated, onMapCreated: onMapCreated,
onCameraIdle: onMapMoved, onStyleLoadedCallback: onMapReady,
), ),
), ),
); );
@ -196,9 +182,7 @@ class _MyLocationButton extends StatelessWidget {
bottom: context.padding.bottom + 16, bottom: context.padding.bottom + 16,
child: ElevatedButton( child: ElevatedButton(
onPressed: onZoomToLocation, onPressed: onZoomToLocation,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(shape: const CircleBorder()),
shape: const CircleBorder(),
),
child: const Icon(Icons.my_location), child: const Icon(Icons.my_location),
), ),
); );

View File

@ -73,10 +73,7 @@ class MapUtils {
try { try {
bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled && !silent) { if (!serviceEnabled && !silent) {
showDialog( showDialog(context: context, builder: (context) => _LocationServiceDisabledDialog(context));
context: context,
builder: (context) => _LocationServiceDisabledDialog(context),
);
return (null, LocationPermission.deniedForever); return (null, LocationPermission.deniedForever);
} }
@ -93,12 +90,9 @@ class MapUtils {
} }
} }
if (permission == LocationPermission.denied || if (permission == LocationPermission.denied || permission == LocationPermission.deniedForever) {
permission == LocationPermission.deniedForever) {
// Open app settings only if you did not request for permission before // Open app settings only if you did not request for permission before
if (permission == LocationPermission.deniedForever && if (permission == LocationPermission.deniedForever && !shouldRequestPermission && !silent) {
!shouldRequestPermission &&
!silent) {
await Geolocator.openAppSettings(); await Geolocator.openAppSettings();
} }
return (null, LocationPermission.deniedForever); return (null, LocationPermission.deniedForever);
@ -121,24 +115,24 @@ class MapUtils {
class _LocationServiceDisabledDialog extends ConfirmDialog { class _LocationServiceDisabledDialog extends ConfirmDialog {
_LocationServiceDisabledDialog(BuildContext context) _LocationServiceDisabledDialog(BuildContext context)
: super( : super(
title: 'map_location_service_disabled_title'.t(context: context), title: 'map_location_service_disabled_title'.t(context: context),
content: 'map_location_service_disabled_content'.t(context: context), content: 'map_location_service_disabled_content'.t(context: context),
cancel: 'cancel'.t(context: context), cancel: 'cancel'.t(context: context),
ok: 'yes'.t(context: context), ok: 'yes'.t(context: context),
onOk: () async { onOk: () async {
await Geolocator.openLocationSettings(); await Geolocator.openLocationSettings();
}, },
); );
} }
class _LocationPermissionDisabledDialog extends ConfirmDialog { class _LocationPermissionDisabledDialog extends ConfirmDialog {
_LocationPermissionDisabledDialog(BuildContext context) _LocationPermissionDisabledDialog(BuildContext context)
: super( : super(
title: 'map_no_location_permission_title'.t(context: context), title: 'map_no_location_permission_title'.t(context: context),
content: 'map_no_location_permission_content'.t(context: context), content: 'map_no_location_permission_content'.t(context: context),
cancel: 'cancel'.t(context: context), cancel: 'cancel'.t(context: context),
ok: 'yes'.t(context: context), ok: 'yes'.t(context: context),
onOk: () {}, onOk: () {},
); );
} }

View File

@ -1,21 +0,0 @@
import 'package:immich_mobile/domain/models/map.model.dart';
class MarkerBuilder {
final List<Marker> markers;
const MarkerBuilder({required this.markers});
static Map<String, dynamic> addFeature(Marker marker) => {
'type': 'Feature',
'id': marker.assetId,
'geometry': {
'type': 'Point',
'coordinates': [marker.location.longitude, marker.location.latitude],
},
};
Map<String, dynamic> generate() => {
'type': 'FeatureCollection',
'features': markers.map(addFeature).toList(),
};
}

View File

@ -4,9 +4,7 @@ import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/domain/services/map.service.dart'; import 'package:immich_mobile/domain/services/map.service.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
final mapRepositoryProvider = Provider<DriftMapRepository>( final mapRepositoryProvider = Provider<DriftMapRepository>((ref) => DriftMapRepository(ref.watch(driftProvider)));
(ref) => DriftMapRepository(ref.watch(driftProvider)),
);
final mapServiceProvider = Provider<MapService>( final mapServiceProvider = Provider<MapService>(
(ref) { (ref) {
@ -16,16 +14,11 @@ final mapServiceProvider = Provider<MapService>(
} }
final mapService = ref.watch(mapFactoryProvider).remote(user.id); final mapService = ref.watch(mapFactoryProvider).remote(user.id);
ref.onDispose(mapService.dispose);
return mapService; return mapService;
}, },
// Empty dependencies to inform the framework that this provider // Empty dependencies to inform the framework that this provider
// might be used in a ProviderScope // might be used in a ProviderScope
dependencies: [], dependencies: const [],
); );
final mapFactoryProvider = Provider<MapFactory>( final mapFactoryProvider = Provider<MapFactory>((ref) => MapFactory(mapRepository: ref.watch(mapRepositoryProvider)));
(ref) => MapFactory(
mapRepository: ref.watch(mapRepositoryProvider),
),
);

View File

@ -330,7 +330,7 @@ class AppRouter extends RootStackRouter {
AutoRoute(page: DriftPeopleCollectionRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftPeopleCollectionRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: DriftPersonRoute.page, guards: [_authGuard]), AutoRoute(page: DriftPersonRoute.page, guards: [_authGuard]),
AutoRoute(page: DriftBackupOptionsRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftBackupOptionsRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: DriftMapRoute.page,guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftMapRoute.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

@ -880,7 +880,7 @@ class DriftLockedFolderRoute extends PageRouteInfo<void> {
/// [DriftMapPage] /// [DriftMapPage]
class DriftMapRoute extends PageRouteInfo<void> { class DriftMapRoute extends PageRouteInfo<void> {
const DriftMapRoute({List<PageRouteInfo>? children}) const DriftMapRoute({List<PageRouteInfo>? children})
: super(DriftMapRoute.name, initialChildren: children); : super(DriftMapRoute.name, initialChildren: children);
static const String name = 'DriftMapRoute'; static const String name = 'DriftMapRoute';

View File

@ -5,7 +5,7 @@ class AsyncMutex {
Future _running = Future.value(null); Future _running = Future.value(null);
int _enqueued = 0; int _enqueued = 0;
get enqueued => _enqueued; int get enqueued => _enqueued;
/// Execute [operation] exclusively, after any currently running operations. /// Execute [operation] exclusively, after any currently running operations.
/// Returns a [Future] with the result of the [operation]. /// Returns a [Future] with the result of the [operation].

View File

@ -27,8 +27,9 @@ class Debouncer {
} }
Future<void>? drain() { Future<void>? drain() {
if (_timer != null && _timer!.isActive) { final timer = _timer;
_timer!.cancel(); if (timer != null && timer.isActive) {
timer.cancel();
if (_lastAction != null) { if (_lastAction != null) {
_callAndRest(); _callAndRest();
} }

View File

@ -9,6 +9,7 @@ import 'schema_v3.dart' as v3;
import 'schema_v4.dart' as v4; import 'schema_v4.dart' as v4;
import 'schema_v5.dart' as v5; import 'schema_v5.dart' as v5;
import 'schema_v6.dart' as v6; import 'schema_v6.dart' as v6;
import 'schema_v7.dart' as v7;
class GeneratedHelper implements SchemaInstantiationHelper { class GeneratedHelper implements SchemaInstantiationHelper {
@override @override
@ -26,10 +27,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
return v5.DatabaseAtV5(db); return v5.DatabaseAtV5(db);
case 6: case 6:
return v6.DatabaseAtV6(db); return v6.DatabaseAtV6(db);
case 7:
return v7.DatabaseAtV7(db);
default: default:
throw MissingSchemaException(version, versions); throw MissingSchemaException(version, versions);
} }
} }
static const versions = const [1, 2, 3, 4, 5, 6]; static const versions = const [1, 2, 3, 4, 5, 6, 7];
} }

File diff suppressed because it is too large Load Diff