mirror of
https://github.com/immich-app/immich.git
synced 2025-08-11 09:16:31 -04:00
optimizations
This commit is contained in:
parent
fb510e9853
commit
e416c971e7
1
mobile/drift_schemas/main/drift_schema_v7.json
generated
Normal file
1
mobile/drift_schemas/main/drift_schema_v7.json
generated
Normal file
File diff suppressed because one or more lines are too long
@ -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) {
|
||||||
|
@ -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 {}
|
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -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;
|
||||||
@ -15,87 +14,58 @@ class DriftMapRepository extends DriftDatabaseRepository {
|
|||||||
|
|
||||||
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()
|
||||||
|
..addColumns([_db.remoteExifEntity.assetId, _db.remoteExifEntity.latitude, _db.remoteExifEntity.longitude])
|
||||||
|
..join([
|
||||||
innerJoin(
|
innerJoin(
|
||||||
_db.remoteAssetEntity,
|
_db.remoteAssetEntity,
|
||||||
_db.remoteAssetEntity.id.equalsExp(_db.remoteExifEntity.assetId),
|
_db.remoteAssetEntity.id.equalsExp(_db.remoteExifEntity.assetId),
|
||||||
useColumns: false,
|
useColumns: false,
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
..where(
|
..limit(10000);
|
||||||
_db.remoteExifEntity.latitude.isNotNull() &
|
|
||||||
_db.remoteExifEntity.longitude.isNotNull(),
|
|
||||||
);
|
|
||||||
|
|
||||||
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!),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
|
||||||
}) {
|
|
||||||
final query = _db.remoteAssetEntity.select().join(
|
|
||||||
[
|
|
||||||
innerJoin(
|
innerJoin(
|
||||||
_db.remoteExifEntity,
|
_db.remoteExifEntity,
|
||||||
_db.remoteExifEntity.assetId.equalsExp(_db.remoteAssetEntity.id),
|
_db.remoteExifEntity.assetId.equalsExp(_db.remoteAssetEntity.id),
|
||||||
useColumns: false,
|
useColumns: false,
|
||||||
),
|
),
|
||||||
],
|
])
|
||||||
)
|
|
||||||
..where(
|
..where(
|
||||||
_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(),
|
||||||
)
|
)
|
||||||
..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)])
|
..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)])
|
||||||
..limit(count, offset: offset);
|
..limit(count, offset: offset);
|
||||||
return query
|
return query.map((row) => row.readTable(_db.remoteAssetEntity).toDto()).get();
|
||||||
.map((row) => row.readTable(_db.remoteAssetEntity).toDto())
|
|
||||||
.get();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TimelineQuery _remoteQueryBuilder({
|
TimelineQuery _remoteQueryBuilder({
|
||||||
|
@ -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,
|
||||||
|
@ -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(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
}),
|
}),
|
||||||
|
@ -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?>(
|
|
||||||
(ref, bounds) async* {
|
|
||||||
final mapService = ref.watch(mapServiceProvider);
|
final mapService = ref.watch(mapServiceProvider);
|
||||||
yield* mapService.watchMarkers(bounds).map((markers) {
|
final markers = await mapService.getMarkers(bounds);
|
||||||
return MarkerBuilder(
|
final features = List.filled(markers.length, const <String, dynamic>{});
|
||||||
markers: markers,
|
for (int i = 0; i < markers.length; i++) {
|
||||||
).generate();
|
final marker = markers[i];
|
||||||
});
|
features[i] = {
|
||||||
|
'type': 'Feature',
|
||||||
|
'id': marker.assetId,
|
||||||
|
'geometry': {
|
||||||
|
'type': 'Point',
|
||||||
|
'coordinates': [marker.location.longitude, marker.location.latitude],
|
||||||
},
|
},
|
||||||
dependencies: [mapServiceProvider],
|
};
|
||||||
);
|
}
|
||||||
|
return {'type': 'FeatureCollection', 'features': features};
|
||||||
|
}, dependencies: [mapServiceProvider]);
|
||||||
|
|
||||||
final mapStateProvider = NotifierProvider<MapStateNotifier, MapState>(
|
final mapStateProvider = NotifierProvider<MapStateNotifier, MapState>(MapStateNotifier.new);
|
||||||
MapStateNotifier.new,
|
|
||||||
);
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setBounds() async {
|
await controller.addSource(
|
||||||
if (mapController == null) return;
|
|
||||||
final bounds = await mapController!.getVisibleRegion();
|
|
||||||
ref.read(mapStateProvider.notifier).setBounds(bounds);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> reloadMarkers(
|
|
||||||
Map<String, dynamic> markers, {
|
|
||||||
bool isLoadAllMarkers = false,
|
|
||||||
}) async {
|
|
||||||
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();
|
|
||||||
if (existingSources.contains(MapUtils.defaultSourceId)) {
|
|
||||||
await mapController!.removeSource(MapUtils.defaultSourceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
await mapController!.addSource(
|
|
||||||
MapUtils.defaultSourceId,
|
MapUtils.defaultSourceId,
|
||||||
GeojsonSourceProperties(data: markers),
|
const CustomSourceProperties(data: {'type': 'FeatureCollection', 'features': []}),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (Platform.isAndroid) {
|
await controller.addHeatmapLayer(
|
||||||
await mapController!.addCircleLayer(
|
|
||||||
MapUtils.defaultSourceId,
|
|
||||||
MapUtils.defaultHeatMapLayerId,
|
|
||||||
MapUtils.defaultCircleLayerLayerProperties,
|
|
||||||
);
|
|
||||||
} else if (Platform.isIOS) {
|
|
||||||
await mapController!.addHeatmapLayer(
|
|
||||||
MapUtils.defaultSourceId,
|
MapUtils.defaultSourceId,
|
||||||
MapUtils.defaultHeatMapLayerId,
|
MapUtils.defaultHeatMapLayerId,
|
||||||
MapUtils.defaultHeatmapLayerProperties,
|
MapUtils.defaultHeatmapLayerProperties,
|
||||||
);
|
);
|
||||||
|
controller.addListener(onMapMoved);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoadAllMarkers) loadAllMarkers = true;
|
void onMapMoved() {
|
||||||
|
if (mapController!.isCameraMoving || !mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
MapUtils.markerCompleter.complete();
|
_debouncer.run(setBounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setBounds() async {
|
||||||
|
final controller = mapController;
|
||||||
|
if (controller == null || !mounted) {
|
||||||
|
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(Map<String, dynamic> markers) async {
|
||||||
|
final controller = mapController;
|
||||||
|
if (controller == null || !mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await controller.setGeoJsonSource(MapUtils.defaultSourceId, markers);
|
||||||
}
|
}
|
||||||
|
|
||||||
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),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -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);
|
||||||
|
@ -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(),
|
|
||||||
};
|
|
||||||
}
|
|
@ -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),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
@ -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: '/'),
|
||||||
|
@ -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].
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
5
mobile/test/drift/main/generated/schema.dart
generated
5
mobile/test/drift/main/generated/schema.dart
generated
@ -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];
|
||||||
}
|
}
|
||||||
|
6453
mobile/test/drift/main/generated/schema_v7.dart
generated
Normal file
6453
mobile/test/drift/main/generated/schema_v7.dart
generated
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user