merge main

This commit is contained in:
Alex
2025-07-08 11:13:05 -05:00
142 changed files with 1774 additions and 422 deletions
@@ -0,0 +1,17 @@
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
class LocalAlbumService {
final DriftLocalAlbumRepository _repository;
const LocalAlbumService(this._repository);
Future<List<LocalAlbum>> getAll() {
return _repository.getAll();
}
Future<LocalAsset?> getThumbnail(String albumId) {
return _repository.getThumbnail(albumId);
}
}
@@ -42,16 +42,29 @@ class TimelineFactory {
TimelineService localAlbum({required String albumId}) => TimelineService(
assetSource: (offset, count) => _timelineRepository
.getLocalBucketAssets(albumId, offset: offset, count: count),
bucketSource: () =>
_timelineRepository.watchLocalBucket(albumId, groupBy: groupBy),
.getLocalAlbumBucketAssets(albumId, offset: offset, count: count),
bucketSource: () => _timelineRepository.watchLocalAlbumBucket(
albumId,
groupBy: groupBy,
),
);
TimelineService remoteAlbum({required String albumId}) => TimelineService(
assetSource: (offset, count) => _timelineRepository
.getRemoteBucketAssets(albumId, offset: offset, count: count),
bucketSource: () =>
_timelineRepository.watchRemoteBucket(albumId, groupBy: groupBy),
.getRemoteAlbumBucketAssets(albumId, offset: offset, count: count),
bucketSource: () => _timelineRepository.watchRemoteAlbumBucket(
albumId,
groupBy: groupBy,
),
);
TimelineService remoteAssets(String ownerId) => TimelineService(
assetSource: (offset, count) => _timelineRepository
.getRemoteBucketAssets(ownerId, offset: offset, count: count),
bucketSource: () => _timelineRepository.watchRemoteBucket(
ownerId,
groupBy: GroupAssetsBy.month,
),
);
TimelineService favorite(String userId) => TimelineService(
@@ -361,6 +361,24 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
batch.deleteWhere(_db.localAssetEntity, (f) => f.id.isIn(ids));
});
}
Future<LocalAsset?> getThumbnail(String albumId) async {
final query = _db.localAlbumAssetEntity.select().join([
innerJoin(
_db.localAssetEntity,
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
),
])
..where(_db.localAlbumAssetEntity.albumId.equals(albumId))
..orderBy([OrderingTerm.asc(_db.localAssetEntity.id)])
..limit(1);
final results = await query
.map((row) => row.readTable(_db.localAssetEntity).toDto())
.get();
return results.isNotEmpty ? results.first : null;
}
}
extension on LocalAlbumEntityData {
@@ -18,13 +18,21 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
_db.remoteAlbumAssetEntity.albumId.equalsExp(_db.remoteAlbumEntity.id),
useColumns: false,
),
leftOuterJoin(
_db.remoteAssetEntity,
_db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId),
useColumns: false,
),
leftOuterJoin(
_db.userEntity,
_db.userEntity.id.equalsExp(_db.remoteAlbumEntity.ownerId),
useColumns: false,
),
]);
query
..where(_db.remoteAssetEntity.deletedAt.isNull())
..addColumns([assetCount])
..addColumns([_db.userEntity.name])
..groupBy([_db.remoteAlbumEntity.id]);
if (sortBy.isNotEmpty) {
@@ -43,7 +51,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
.map(
(row) => row.readTable(_db.remoteAlbumEntity).toDto(
assetCount: row.read(assetCount) ?? 0,
ownerName: row.readTable(_db.userEntity).name,
ownerName: row.read(_db.userEntity.name)!,
),
)
.get();
@@ -13,6 +13,22 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
final Drift _db;
const RemoteAssetRepository(this._db) : super(_db);
/// For testing purposes
Future<List<RemoteAsset>> getSome(String userId) {
final query = _db.remoteAssetEntity.select()
..where(
(row) =>
_db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAssetEntity.visibility
.equalsValue(AssetVisibility.timeline),
)
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
..limit(10);
return query.map((row) => row.toDto()).get();
}
Stream<RemoteAsset?> watchAsset(String id) {
final query = _db.remoteAssetEntity
.select()
@@ -104,7 +104,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
).get();
}
Stream<List<Bucket>> watchLocalBucket(
Stream<List<Bucket>> watchLocalAlbumBucket(
String albumId, {
GroupAssetsBy groupBy = GroupAssetsBy.day,
}) {
@@ -124,6 +124,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
innerJoin(
_db.localAlbumAssetEntity,
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
useColumns: false,
),
])
..where(_db.localAlbumAssetEntity.albumId.equals(albumId))
@@ -137,7 +138,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
}).watch();
}
Future<List<BaseAsset>> getLocalBucketAssets(
Future<List<BaseAsset>> getLocalAlbumBucketAssets(
String albumId, {
required int offset,
required int count,
@@ -147,6 +148,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
innerJoin(
_db.localAlbumAssetEntity,
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
useColumns: false,
),
],
)
@@ -158,7 +160,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
.get();
}
Stream<List<Bucket>> watchRemoteBucket(
Stream<List<Bucket>> watchRemoteAlbumBucket(
String albumId, {
GroupAssetsBy groupBy = GroupAssetsBy.day,
}) {
@@ -179,9 +181,13 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
_db.remoteAlbumAssetEntity,
_db.remoteAlbumAssetEntity.assetId
.equalsExp(_db.remoteAssetEntity.id),
useColumns: false,
),
])
..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId))
..where(
_db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAlbumAssetEntity.albumId.equals(albumId),
)
..groupBy([dateExp])
..orderBy([OrderingTerm.desc(dateExp)]);
@@ -192,7 +198,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
}).watch();
}
Future<List<BaseAsset>> getRemoteBucketAssets(
Future<List<BaseAsset>> getRemoteAlbumBucketAssets(
String albumId, {
required int offset,
required int count,
@@ -203,10 +209,14 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
_db.remoteAlbumAssetEntity,
_db.remoteAlbumAssetEntity.assetId
.equalsExp(_db.remoteAssetEntity.id),
useColumns: false,
),
],
)
..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId))
..where(
_db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAlbumAssetEntity.albumId.equals(albumId),
)
..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)])
..limit(count, offset: offset);
return query
@@ -214,15 +224,17 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
.get();
}
Stream<List<Bucket>> watchFavoriteBucket(
String userId, {
Stream<List<Bucket>> watchRemoteBucket(
String ownerId, {
GroupAssetsBy groupBy = GroupAssetsBy.day,
}) {
if (groupBy == GroupAssetsBy.none) {
return _db.remoteAssetEntity
.count(
where: (row) =>
row.isFavorite.equals(true) & row.ownerId.equals(userId),
row.deletedAt.isNull() &
row.visibility.equalsValue(AssetVisibility.timeline) &
row.ownerId.equals(ownerId),
)
.map(_generateBuckets)
.watchSingle();
@@ -234,7 +246,63 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
final query = _db.remoteAssetEntity.selectOnly()
..addColumns([assetCountExp, dateExp])
..where(
_db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAssetEntity.visibility
.equalsValue(AssetVisibility.timeline) &
_db.remoteAssetEntity.ownerId.equals(ownerId),
)
..groupBy([dateExp])
..orderBy([OrderingTerm.desc(dateExp)]);
return query.map((row) {
final timeline = row.read(dateExp)!.dateFmt(groupBy);
final assetCount = row.read(assetCountExp)!;
return TimeBucket(date: timeline, assetCount: assetCount);
}).watch();
}
Future<List<BaseAsset>> getRemoteBucketAssets(
String ownerId, {
required int offset,
required int count,
}) {
final query = _db.remoteAssetEntity.select()
..where(
(row) =>
row.deletedAt.isNull() &
row.visibility.equalsValue(AssetVisibility.timeline) &
row.ownerId.equals(ownerId),
)
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
..limit(count, offset: offset);
return query.map((row) => row.toDto()).get();
}
Stream<List<Bucket>> watchFavoriteBucket(
String userId, {
GroupAssetsBy groupBy = GroupAssetsBy.day,
}) {
if (groupBy == GroupAssetsBy.none) {
return _db.remoteAssetEntity
.count(
where: (row) =>
row.deletedAt.isNull() &
row.isFavorite.equals(true) &
row.ownerId.equals(userId),
)
.map(_generateBuckets)
.watchSingle();
}
final assetCountExp = _db.remoteAssetEntity.id.count();
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
final query = _db.remoteAssetEntity.selectOnly()
..addColumns([assetCountExp, dateExp])
..where(
_db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.isFavorite.equals(true),
)
..groupBy([dateExp])
@@ -254,7 +322,10 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
}) {
final query = _db.remoteAssetEntity.select()
..where(
(row) => row.isFavorite.equals(true) & row.ownerId.equals(userId),
(row) =>
row.deletedAt.isNull() &
row.isFavorite.equals(true) &
row.ownerId.equals(userId),
)
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
..limit(count, offset: offset);
@@ -318,6 +389,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
return _db.remoteAssetEntity
.count(
where: (row) =>
row.deletedAt.isNull() &
row.visibility.equalsValue(AssetVisibility.archive) &
row.ownerId.equals(userId),
)
@@ -331,7 +403,8 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
final query = _db.remoteAssetEntity.selectOnly()
..addColumns([assetCountExp, dateExp])
..where(
_db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.visibility
.equalsValue(AssetVisibility.archive),
)
@@ -353,6 +426,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
final query = _db.remoteAssetEntity.select()
..where(
(row) =>
row.deletedAt.isNull() &
row.ownerId.equals(userId) &
row.visibility.equalsValue(AssetVisibility.archive),
)
@@ -370,6 +444,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
return _db.remoteAssetEntity
.count(
where: (row) =>
row.deletedAt.isNull() &
row.visibility.equalsValue(AssetVisibility.locked) &
row.ownerId.equals(userId),
)
@@ -383,7 +458,8 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
final query = _db.remoteAssetEntity.selectOnly()
..addColumns([assetCountExp, dateExp])
..where(
_db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.visibility
.equalsValue(AssetVisibility.locked),
)
@@ -405,6 +481,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
final query = _db.remoteAssetEntity.select()
..where(
(row) =>
row.deletedAt.isNull() &
row.visibility.equalsValue(AssetVisibility.locked) &
row.ownerId.equals(userId),
)
@@ -422,6 +499,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
return _db.remoteAssetEntity
.count(
where: (row) =>
row.deletedAt.isNull() &
row.type.equalsValue(AssetType.video) &
row.visibility.equalsValue(AssetVisibility.timeline) &
row.ownerId.equals(userId),
@@ -436,7 +514,8 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
final query = _db.remoteAssetEntity.selectOnly()
..addColumns([assetCountExp, dateExp])
..where(
_db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAssetEntity.ownerId.equals(userId) &
_db.remoteAssetEntity.type.equalsValue(AssetType.video) &
_db.remoteAssetEntity.visibility
.equalsValue(AssetVisibility.timeline),
@@ -459,10 +538,10 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
final query = _db.remoteAssetEntity.select()
..where(
(row) =>
_db.remoteAssetEntity.type.equalsValue(AssetType.video) &
_db.remoteAssetEntity.visibility
.equalsValue(AssetVisibility.timeline) &
_db.remoteAssetEntity.ownerId.equals(userId),
row.deletedAt.isNull() &
row.type.equalsValue(AssetType.video) &
row.visibility.equalsValue(AssetVisibility.timeline) &
row.ownerId.equals(userId),
)
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
..limit(count, offset: offset);
@@ -0,0 +1,116 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
import 'package:immich_mobile/presentation/widgets/images/local_album_thumbnail.widget.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/widgets/common/local_album_sliver_app_bar.dart';
@RoutePage()
class DriftLocalAlbumsPage extends StatelessWidget {
const DriftLocalAlbumsPage({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
body: CustomScrollView(
slivers: [
LocalAlbumsSliverAppBar(),
_AlbumList(),
],
),
);
}
}
class _AlbumList extends ConsumerWidget {
const _AlbumList();
@override
Widget build(BuildContext context, WidgetRef ref) {
final albums = ref.watch(localAlbumProvider);
return albums.when(
loading: () => const SliverToBoxAdapter(
child: Center(
child: Padding(
padding: EdgeInsets.all(20.0),
child: CircularProgressIndicator(),
),
),
),
error: (error, stack) => SliverToBoxAdapter(
child: Center(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Text(
'Error loading albums: $error, stack: $stack',
style: TextStyle(
color: context.colorScheme.error,
),
),
),
),
),
data: (albums) {
if (albums.isEmpty) {
return const SliverToBoxAdapter(
child: Center(
child: Padding(
padding: EdgeInsets.all(20.0),
child: Text('No albums found'),
),
),
);
}
return SliverPadding(
padding: const EdgeInsets.all(18.0),
sliver: SliverList.builder(
itemBuilder: (_, index) {
final album = albums[index];
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: LargeLeadingTile(
leadingPadding: const EdgeInsets.only(
right: 16,
),
leading: SizedBox(
width: 80,
height: 80,
child: LocalAlbumThumbnail(
albumId: album.id,
),
),
title: Text(
album.name,
style: context.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
subtitle: Text(
'items_count'.t(
context: context,
args: {'count': album.assetCount},
),
style: context.textTheme.bodyMedium?.copyWith(
color: context.colorScheme.onSurfaceSecondary,
),
),
onTap: () =>
context.pushRoute(LocalTimelineRoute(albumId: album.id)),
),
);
},
itemCount: albums.length,
),
);
},
);
}
}
@@ -0,0 +1,29 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
@RoutePage()
class DriftPartnerDetailPage extends StatelessWidget {
final String partnerId;
const DriftPartnerDetailPage({super.key, required this.partnerId});
@override
Widget build(BuildContext context) {
return ProviderScope(
overrides: [
timelineServiceProvider.overrideWith(
(ref) {
final timelineService =
ref.watch(timelineFactoryProvider).remoteAssets(partnerId);
ref.onDispose(timelineService.dispose);
return timelineService;
},
),
],
child: const Timeline(),
);
}
}
@@ -0,0 +1,35 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
@RoutePage()
class DriftRecentlyTakenPage extends StatelessWidget {
const DriftRecentlyTakenPage({super.key});
@override
Widget build(BuildContext context) {
return ProviderScope(
overrides: [
timelineServiceProvider.overrideWith(
(ref) {
final user = ref.watch(currentUserProvider);
if (user == null) {
throw Exception(
'User must be logged in to access recently taken',
);
}
final timelineService =
ref.watch(timelineFactoryProvider).remoteAssets(user.id);
ref.onDispose(timelineService.dispose);
return timelineService;
},
),
],
child: const Timeline(),
);
}
}
@@ -5,15 +5,43 @@ import 'package:drift/drift.dart' hide Column;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/routing/router.dart';
final _features = [
_Feature(
name: 'Selection Mode Timeline',
icon: Icons.developer_mode_rounded,
onTap: (ctx, ref) async {
final user = ref.watch(currentUserProvider);
if (user == null) {
return Future.value();
}
final assets =
await ref.read(remoteAssetRepositoryProvider).getSome(user.id);
final selectedAssets = await ctx.pushRoute<Set<BaseAsset>>(
DriftAssetSelectionTimelineRoute(
lockedSelectionAssets: assets.toSet(),
),
);
DLog.log(
"Selected ${selectedAssets?.length ?? 0} assets",
);
return Future.value();
},
),
_Feature(
name: 'Sync Local',
icon: Icons.photo_album_rounded,
@@ -104,6 +132,11 @@ final _features = [
icon: Icons.video_collection_outlined,
onTap: (ctx, _) => ctx.pushRoute(const DriftVideoRoute()),
),
_Feature(
name: 'Recently Taken',
icon: Icons.schedule_outlined,
onTap: (ctx, _) => ctx.pushRoute(const DriftRecentlyTakenRoute()),
),
];
@RoutePage()
@@ -0,0 +1,50 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
@RoutePage()
class DriftAssetSelectionTimelinePage extends ConsumerWidget {
final Set<BaseAsset> lockedSelectionAssets;
const DriftAssetSelectionTimelinePage({
super.key,
this.lockedSelectionAssets = const {},
});
@override
Widget build(BuildContext context, WidgetRef ref) {
return ProviderScope(
overrides: [
multiSelectProvider.overrideWith(
() => MultiSelectNotifier(
MultiSelectState(
selectedAssets: {},
lockedSelectionAssets: lockedSelectionAssets,
forceEnable: true,
),
),
),
timelineServiceProvider.overrideWith(
(ref) {
final user = ref.watch(currentUserProvider);
if (user == null) {
throw Exception(
'User must be logged in to access recently taken',
);
}
final timelineService =
ref.watch(timelineFactoryProvider).remoteAssets(user.id);
ref.onDispose(timelineService.dispose);
return timelineService;
},
),
],
child: const Timeline(),
);
}
}
@@ -5,14 +5,14 @@ import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:immich_mobile/presentation/widgets/images/local_album_thumbnail.widget.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/partner.provider.dart';
import 'package:immich_mobile/providers/search/people.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart';
import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart';
import 'package:immich_mobile/widgets/common/user_avatar.dart';
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
@@ -310,8 +310,7 @@ class _LocalAlbumsCollectionCard extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// TODO: Migrate to the drift after local album page
final albums = ref.watch(localAlbumsProvider);
final albums = ref.watch(localAlbumProvider);
return LayoutBuilder(
builder: (context, constraints) {
@@ -320,9 +319,7 @@ class _LocalAlbumsCollectionCard extends ConsumerWidget {
final size = context.width * widthFactor - 20.0;
return GestureDetector(
onTap: () => context.pushRoute(
const LocalAlbumsRoute(),
),
onTap: () => context.pushRoute(const DriftLocalAlbumsRoute()),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -347,12 +344,29 @@ class _LocalAlbumsCollectionCard extends ConsumerWidget {
crossAxisSpacing: 8,
mainAxisSpacing: 8,
physics: const NeverScrollableScrollPhysics(),
children: albums.take(4).map((album) {
return AlbumThumbnailCard(
album: album,
showTitle: false,
);
}).toList(),
children: albums.when(
data: (data) {
return data.take(4).map((album) {
return LocalAlbumThumbnail(
albumId: album.id,
);
}).toList();
},
error: (error, _) {
return [
Center(
child: Text('Error: $error'),
),
];
},
loading: () {
return [
const Center(
child: CircularProgressIndicator(),
),
];
},
),
),
),
),
@@ -498,7 +512,8 @@ class _PartnerList extends StatelessWidget {
fontWeight: FontWeight.w500,
),
).t(context: context, args: {'user': partner.name}),
onTap: () => context.pushRoute(PartnerDetailRoute(partner: partner)),
onTap: () =>
context.pushRoute(DriftPartnerDetailRoute(partnerId: partner.id)),
);
},
);
@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
class LocalAlbumThumbnail extends ConsumerWidget {
const LocalAlbumThumbnail({
super.key,
required this.albumId,
});
final String albumId;
@override
Widget build(BuildContext context, WidgetRef ref) {
final localAlbumThumbnail = ref.watch(localAlbumThumbnailProvider(albumId));
return localAlbumThumbnail.when(
data: (data) {
if (data == null) {
return Container(
decoration: BoxDecoration(
color: context.colorScheme.surfaceContainer,
borderRadius: const BorderRadius.all(Radius.circular(16)),
border: Border.all(
color: context.colorScheme.outline.withAlpha(50),
width: 1,
),
),
child: Icon(
Icons.collections,
size: 24,
color: context.primaryColor,
),
);
}
return ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(16)),
child: Thumbnail(
asset: data,
),
);
},
error: (error, stack) {
return const Icon(Icons.error, size: 24);
},
loading: () => const SizedBox(
width: 24,
height: 24,
child: Center(child: CircularProgressIndicator()),
),
);
}
}
@@ -12,7 +12,7 @@ class ThumbnailTile extends ConsumerWidget {
this.size = const Size.square(256),
this.fit = BoxFit.cover,
this.showStorageIndicator = true,
this.canDeselect = true,
this.lockSelection = false,
super.key,
});
@@ -20,15 +20,13 @@ class ThumbnailTile extends ConsumerWidget {
final Size size;
final BoxFit fit;
final bool showStorageIndicator;
/// If we are allowed to deselect this image
final bool canDeselect;
final bool lockSelection;
@override
Widget build(BuildContext context, WidgetRef ref) {
final assetContainerColor = context.isDarkTheme
? context.primaryColor.darken(amount: 0.6)
: context.primaryColor.lighten(amount: 0.8);
? context.primaryColor.darken(amount: 0.4)
: context.primaryColor.lighten(amount: 0.75);
final isSelected = ref.watch(
multiSelectProvider.select(
@@ -36,24 +34,29 @@ class ThumbnailTile extends ConsumerWidget {
),
);
final borderStyle = lockSelection
? BoxDecoration(
color: context.colorScheme.surfaceContainerHighest,
border: Border.all(
color: context.colorScheme.surfaceContainerHighest,
width: 6,
),
)
: isSelected
? BoxDecoration(
color: assetContainerColor,
border: Border.all(color: assetContainerColor, width: 6),
)
: const BoxDecoration();
return Stack(
children: [
AnimatedContainer(
duration: Durations.short4,
curve: Curves.decelerate,
decoration: BoxDecoration(
color: isSelected
? (canDeselect ? assetContainerColor : Colors.grey)
: null,
border: isSelected
? Border.all(
color: canDeselect ? assetContainerColor : Colors.grey,
width: 8,
)
: const Border(),
),
decoration: borderStyle,
child: ClipRRect(
borderRadius: isSelected
borderRadius: isSelected || lockSelection
? const BorderRadius.all(Radius.circular(15.0))
: BorderRadius.zero,
child: Stack(
@@ -102,14 +105,17 @@ class ThumbnailTile extends ConsumerWidget {
),
),
),
if (isSelected)
if (isSelected || lockSelection)
Padding(
padding: const EdgeInsets.all(3.0),
child: Align(
alignment: Alignment.topLeft,
child: _SelectionIndicator(
isSelected: isSelected,
color: assetContainerColor,
isLocked: lockSelection,
color: lockSelection
? context.colorScheme.surfaceContainerHighest
: assetContainerColor,
),
),
),
@@ -120,15 +126,29 @@ class ThumbnailTile extends ConsumerWidget {
class _SelectionIndicator extends StatelessWidget {
final bool isSelected;
final bool isLocked;
final Color? color;
const _SelectionIndicator({
required this.isSelected,
required this.isLocked,
this.color,
});
@override
Widget build(BuildContext context) {
if (isSelected) {
if (isLocked) {
return Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: color,
),
child: const Icon(
Icons.check_circle_rounded,
color: Colors.grey,
),
);
} else if (isSelected) {
return Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
@@ -166,22 +166,22 @@ class _AssetTileWidget extends ConsumerWidget {
BaseAsset asset,
) {
final multiSelectState = ref.read(multiSelectProvider);
if (!multiSelectState.isEnabled) {
if (multiSelectState.forceEnable || multiSelectState.isEnabled) {
ref.read(multiSelectProvider.notifier).toggleAssetSelection(asset);
} else {
ctx.pushRoute(
AssetViewerRoute(
initialIndex: assetIndex,
timelineService: ref.read(timelineServiceProvider),
),
);
return;
}
ref.read(multiSelectProvider.notifier).toggleAssetSelection(asset);
}
void _handleOnLongPress(WidgetRef ref, BaseAsset asset) {
final multiSelectState = ref.read(multiSelectProvider);
if (multiSelectState.isEnabled) {
if (multiSelectState.isEnabled || multiSelectState.forceEnable) {
return;
}
@@ -189,13 +189,35 @@ class _AssetTileWidget extends ConsumerWidget {
ref.read(multiSelectProvider.notifier).toggleAssetSelection(asset);
}
bool _getLockSelectionStatus(WidgetRef ref) {
final lockSelectionAssets = ref.read(
multiSelectProvider.select(
(state) => state.lockedSelectionAssets,
),
);
if (lockSelectionAssets.isEmpty) {
return false;
}
return lockSelectionAssets.contains(asset);
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final lockSelection = _getLockSelectionStatus(ref);
return RepaintBoundary(
child: GestureDetector(
onTap: () => _handleOnTap(context, ref, assetIndex, asset),
onLongPress: () => _handleOnLongPress(ref, asset),
child: ThumbnailTile(asset),
onTap: () => lockSelection
? null
: _handleOnTap(context, ref, assetIndex, asset),
onLongPress: () =>
lockSelection ? null : _handleOnLongPress(ref, asset),
child: ThumbnailTile(
asset,
lockSelection: lockSelection,
),
),
);
}
@@ -354,22 +354,24 @@ class ScrubberState extends ConsumerState<Scrubber>
isDragging: _isDragging,
),
),
PositionedDirectional(
top: _thumbTopOffset + widget.topPadding,
end: 0,
child: RepaintBoundary(
child: GestureDetector(
onVerticalDragStart: _onDragStart,
onVerticalDragUpdate: _onDragUpdate,
onVerticalDragEnd: _onDragEnd,
child: _Scrubber(
thumbAnimation: _thumbAnimation,
labelAnimation: _labelAnimation,
label: label,
if (_scrollController.hasClients &&
_scrollController.position.maxScrollExtent > 0)
PositionedDirectional(
top: _thumbTopOffset + widget.topPadding,
end: 0,
child: RepaintBoundary(
child: GestureDetector(
onVerticalDragStart: _onDragStart,
onVerticalDragUpdate: _onDragUpdate,
onVerticalDragEnd: _onDragEnd,
child: _Scrubber(
thumbAnimation: _thumbAnimation,
labelAnimation: _labelAnimation,
label: label,
),
),
),
),
),
],
),
);
@@ -18,6 +18,7 @@ import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart';
import 'package:immich_mobile/widgets/common/selection_sliver_app_bar.dart';
class Timeline extends StatelessWidget {
const Timeline({
@@ -96,6 +97,10 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
final asyncSegments = ref.watch(timelineSegmentProvider);
final maxHeight =
ref.watch(timelineArgsProvider.select((args) => args.maxHeight));
final isSelectionMode = ref.watch(
multiSelectProvider.select((s) => s.forceEnable),
);
return asyncSegments.widgetWhen(
onData: (segments) {
final childCount = (segments.lastOrNull?.lastIndex ?? -1) + 1;
@@ -117,12 +122,15 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
primary: true,
cacheExtent: maxHeight * 2,
slivers: [
widget.appBar ??
const ImmichSliverAppBar(
floating: true,
pinned: false,
snap: false,
),
if (isSelectionMode)
const SelectionSliverAppBar()
else
widget.appBar ??
const ImmichSliverAppBar(
floating: true,
pinned: false,
snap: false,
),
if (widget.topSliverWidget != null) widget.topSliverWidget!,
_SliverSegmentedList(
segments: segments,
@@ -147,40 +155,42 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
],
),
),
Consumer(
builder: (_, consumerRef, child) {
final isMultiSelectEnabled = consumerRef.watch(
multiSelectProvider.select(
(s) => s.isEnabled,
),
);
if (!isSelectionMode) ...[
Consumer(
builder: (_, consumerRef, child) {
final isMultiSelectEnabled = consumerRef.watch(
multiSelectProvider.select(
(s) => s.isEnabled,
),
);
if (isMultiSelectEnabled) {
return child!;
}
return const SizedBox.shrink();
},
child: const Positioned(
top: 60,
left: 25,
child: _MultiSelectStatusButton(),
if (isMultiSelectEnabled) {
return child!;
}
return const SizedBox.shrink();
},
child: const Positioned(
top: 60,
left: 25,
child: _MultiSelectStatusButton(),
),
),
),
Consumer(
builder: (_, consumerRef, child) {
final isMultiSelectEnabled = consumerRef.watch(
multiSelectProvider.select(
(s) => s.isEnabled,
),
);
Consumer(
builder: (_, consumerRef, child) {
final isMultiSelectEnabled = consumerRef.watch(
multiSelectProvider.select(
(s) => s.isEnabled,
),
);
if (isMultiSelectEnabled) {
return child!;
}
return const SizedBox.shrink();
},
child: const HomeBottomAppBar(),
),
if (isMultiSelectEnabled) {
return child!;
}
return const SizedBox.shrink();
},
child: const HomeBottomAppBar(),
),
],
],
),
);
@@ -1,4 +1,7 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/services/local_album.service.dart';
import 'package:immich_mobile/domain/services/remote_album.service.dart';
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
@@ -9,6 +12,19 @@ final localAlbumRepository = Provider<DriftLocalAlbumRepository>(
(ref) => DriftLocalAlbumRepository(ref.watch(driftProvider)),
);
final localAlbumServiceProvider = Provider<LocalAlbumService>(
(ref) => LocalAlbumService(ref.watch(localAlbumRepository)),
);
final localAlbumProvider = FutureProvider<List<LocalAlbum>>(
(ref) => LocalAlbumService(ref.watch(localAlbumRepository)).getAll(),
);
final localAlbumThumbnailProvider = FutureProvider.family<LocalAsset?, String>(
(ref, albumId) =>
LocalAlbumService(ref.watch(localAlbumRepository)).getThumbnail(albumId),
);
final remoteAlbumRepository = Provider<DriftRemoteAlbumRepository>(
(ref) => DriftRemoteAlbumRepository(ref.watch(driftProvider)),
);
@@ -1,5 +1,6 @@
import 'package:collection/collection.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/services/timeline.service.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
@@ -12,8 +13,14 @@ final multiSelectProvider =
class MultiSelectState {
final Set<BaseAsset> selectedAssets;
final Set<BaseAsset> lockedSelectionAssets;
final bool forceEnable;
const MultiSelectState({required this.selectedAssets});
const MultiSelectState({
required this.selectedAssets,
required this.lockedSelectionAssets,
this.forceEnable = false,
});
bool get isEnabled => selectedAssets.isNotEmpty;
bool get hasRemote => selectedAssets.any(
@@ -25,33 +32,54 @@ class MultiSelectState {
(asset) => asset.storage == AssetState.local,
);
MultiSelectState copyWith({Set<BaseAsset>? selectedAssets}) {
MultiSelectState copyWith({
Set<BaseAsset>? selectedAssets,
Set<BaseAsset>? lockedSelectionAssets,
bool? forceEnable,
}) {
return MultiSelectState(
selectedAssets: selectedAssets ?? this.selectedAssets,
lockedSelectionAssets:
lockedSelectionAssets ?? this.lockedSelectionAssets,
forceEnable: forceEnable ?? this.forceEnable,
);
}
@override
String toString() => 'MultiSelectState(selectedAssets: $selectedAssets)';
String toString() =>
'MultiSelectState(selectedAssets: $selectedAssets, lockedSelectionAssets: $lockedSelectionAssets, forceEnable: $forceEnable)';
@override
bool operator ==(covariant MultiSelectState other) {
if (identical(this, other)) return true;
final listEquals = const DeepCollectionEquality().equals;
final setEquals = const DeepCollectionEquality().equals;
return listEquals(other.selectedAssets, selectedAssets);
return setEquals(other.selectedAssets, selectedAssets) &&
setEquals(other.lockedSelectionAssets, lockedSelectionAssets) &&
other.forceEnable == forceEnable;
}
@override
int get hashCode => selectedAssets.hashCode;
int get hashCode =>
selectedAssets.hashCode ^
lockedSelectionAssets.hashCode ^
forceEnable.hashCode;
}
class MultiSelectNotifier extends Notifier<MultiSelectState> {
MultiSelectNotifier([this._defaultState]);
final MultiSelectState? _defaultState;
TimelineService get _timelineService => ref.read(timelineServiceProvider);
@override
MultiSelectState build() {
return const MultiSelectState(selectedAssets: {});
return _defaultState ??
const MultiSelectState(
selectedAssets: {},
lockedSelectionAssets: {},
forceEnable: false,
);
}
void selectAsset(BaseAsset asset) {
@@ -83,7 +111,11 @@ class MultiSelectNotifier extends Notifier<MultiSelectState> {
}
void reset() {
state = const MultiSelectState(selectedAssets: {});
state = const MultiSelectState(
selectedAssets: {},
lockedSelectionAssets: {},
forceEnable: false,
);
}
/// Bucket bulk operations
@@ -131,6 +163,12 @@ class MultiSelectNotifier extends Notifier<MultiSelectState> {
state = state.copyWith(selectedAssets: selectedAssets);
}
void setLockedSelectionAssets(Set<BaseAsset> assets) {
state = state.copyWith(
lockedSelectionAssets: assets,
);
}
}
final bucketSelectionProvider = Provider.family<bool, List<BaseAsset>>(
+31
View File
@@ -0,0 +1,31 @@
import 'package:auto_route/auto_route.dart';
import 'package:immich_mobile/routing/router.dart';
/// Handles duplicate navigation to this route (primarily for deep linking)
class GalleryGuard extends AutoRouteGuard {
const GalleryGuard();
@override
void onNavigation(NavigationResolver resolver, StackRouter router) async {
final newRouteName = resolver.route.name;
final currentTopRouteName =
router.stack.isNotEmpty ? router.stack.last.name : null;
if (currentTopRouteName == newRouteName) {
// Replace instead of pushing duplicate
final args = resolver.route.args as GalleryViewerRouteArgs;
router.replace(
GalleryViewerRoute(
renderList: args.renderList,
initialIndex: args.initialIndex,
heroOffset: args.heroOffset,
showStack: args.showStack,
),
);
// Prevent further navigation since we replaced the route
resolver.next(false);
return;
}
resolver.next(true);
}
}
+25 -1
View File
@@ -1,6 +1,7 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/log.model.dart';
import 'package:immich_mobile/domain/models/memory.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
@@ -67,6 +68,9 @@ import 'package:immich_mobile/pages/search/recently_taken.page.dart';
import 'package:immich_mobile/pages/search/search.page.dart';
import 'package:immich_mobile/pages/share_intent/share_intent.page.dart';
import 'package:immich_mobile/presentation/pages/dev/drift_favorite.page.dart';
import 'package:immich_mobile/presentation/pages/dev/drift_partner_detail.page.dart';
import 'package:immich_mobile/presentation/pages/dev/drift_local_album.page.dart';
import 'package:immich_mobile/presentation/pages/dev/drift_recently_taken.page.dart';
import 'package:immich_mobile/presentation/pages/dev/drift_video.page.dart';
import 'package:immich_mobile/presentation/pages/dev/drift_trash.page.dart';
import 'package:immich_mobile/presentation/pages/dev/drift_archive.page.dart';
@@ -78,6 +82,7 @@ import 'package:immich_mobile/presentation/pages/dev/media_stat.page.dart';
import 'package:immich_mobile/presentation/pages/dev/remote_timeline.page.dart';
import 'package:immich_mobile/presentation/pages/drift_album.page.dart';
import 'package:immich_mobile/presentation/pages/drift_library.page.dart';
import 'package:immich_mobile/presentation/pages/drift_asset_selection_timeline.page.dart';
import 'package:immich_mobile/presentation/pages/drift_memory.page.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart';
import 'package:immich_mobile/providers/api.provider.dart';
@@ -86,6 +91,7 @@ import 'package:immich_mobile/routing/auth_guard.dart';
import 'package:immich_mobile/routing/backup_permission_guard.dart';
import 'package:immich_mobile/routing/custom_transition_builders.dart';
import 'package:immich_mobile/routing/duplicate_guard.dart';
import 'package:immich_mobile/routing/gallery_guard.dart';
import 'package:immich_mobile/routing/locked_guard.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/local_auth.service.dart';
@@ -111,6 +117,7 @@ class AppRouter extends RootStackRouter {
late final DuplicateGuard _duplicateGuard;
late final BackupPermissionGuard _backupPermissionGuard;
late final LockedGuard _lockedGuard;
late final GalleryGuard _galleryGuard;
AppRouter(
ApiService apiService,
@@ -123,6 +130,7 @@ class AppRouter extends RootStackRouter {
_lockedGuard =
LockedGuard(apiService, secureStorageService, localAuthService);
_backupPermissionGuard = BackupPermissionGuard(galleryPermissionNotifier);
_galleryGuard = const GalleryGuard();
}
@override
@@ -192,7 +200,7 @@ class AppRouter extends RootStackRouter {
),
CustomRoute(
page: GalleryViewerRoute.page,
guards: [_authGuard, _duplicateGuard],
guards: [_authGuard, _galleryGuard],
transitionsBuilder: CustomTransitionsBuilders.zoomedPage,
),
AutoRoute(
@@ -422,6 +430,22 @@ class AppRouter extends RootStackRouter {
page: DriftLibraryRoute.page,
guards: [_authGuard, _duplicateGuard],
),
AutoRoute(
page: DriftAssetSelectionTimelineRoute.page,
guards: [_authGuard, _duplicateGuard],
),
AutoRoute(
page: DriftPartnerDetailRoute.page,
guards: [_authGuard, _duplicateGuard],
),
AutoRoute(
page: DriftRecentlyTakenRoute.page,
guards: [_authGuard, _duplicateGuard],
),
AutoRoute(
page: DriftLocalAlbumsRoute.page,
guards: [_authGuard, _duplicateGuard],
),
// required to handle all deeplinks in deep_link.service.dart
// auto_route_library#1722
RedirectRoute(path: '*', redirectTo: '/'),
+119
View File
@@ -634,6 +634,55 @@ class DriftArchiveRoute extends PageRouteInfo<void> {
);
}
/// generated route for
/// [DriftAssetSelectionTimelinePage]
class DriftAssetSelectionTimelineRoute
extends PageRouteInfo<DriftAssetSelectionTimelineRouteArgs> {
DriftAssetSelectionTimelineRoute({
Key? key,
Set<BaseAsset> lockedSelectionAssets = const {},
List<PageRouteInfo>? children,
}) : super(
DriftAssetSelectionTimelineRoute.name,
args: DriftAssetSelectionTimelineRouteArgs(
key: key,
lockedSelectionAssets: lockedSelectionAssets,
),
initialChildren: children,
);
static const String name = 'DriftAssetSelectionTimelineRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
final args = data.argsAs<DriftAssetSelectionTimelineRouteArgs>(
orElse: () => const DriftAssetSelectionTimelineRouteArgs(),
);
return DriftAssetSelectionTimelinePage(
key: args.key,
lockedSelectionAssets: args.lockedSelectionAssets,
);
},
);
}
class DriftAssetSelectionTimelineRouteArgs {
const DriftAssetSelectionTimelineRouteArgs({
this.key,
this.lockedSelectionAssets = const {},
});
final Key? key;
final Set<BaseAsset> lockedSelectionAssets;
@override
String toString() {
return 'DriftAssetSelectionTimelineRouteArgs{key: $key, lockedSelectionAssets: $lockedSelectionAssets}';
}
}
/// generated route for
/// [DriftFavoritePage]
class DriftFavoriteRoute extends PageRouteInfo<void> {
@@ -666,6 +715,22 @@ class DriftLibraryRoute extends PageRouteInfo<void> {
);
}
/// generated route for
/// [DriftLocalAlbumsPage]
class DriftLocalAlbumsRoute extends PageRouteInfo<void> {
const DriftLocalAlbumsRoute({List<PageRouteInfo>? children})
: super(DriftLocalAlbumsRoute.name, initialChildren: children);
static const String name = 'DriftLocalAlbumsRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const DriftLocalAlbumsPage();
},
);
}
/// generated route for
/// [DriftLockedFolderPage]
class DriftLockedFolderRoute extends PageRouteInfo<void> {
@@ -734,6 +799,60 @@ class DriftMemoryRouteArgs {
}
}
/// generated route for
/// [DriftPartnerDetailPage]
class DriftPartnerDetailRoute
extends PageRouteInfo<DriftPartnerDetailRouteArgs> {
DriftPartnerDetailRoute({
Key? key,
required String partnerId,
List<PageRouteInfo>? children,
}) : super(
DriftPartnerDetailRoute.name,
args: DriftPartnerDetailRouteArgs(key: key, partnerId: partnerId),
initialChildren: children,
);
static const String name = 'DriftPartnerDetailRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
final args = data.argsAs<DriftPartnerDetailRouteArgs>();
return DriftPartnerDetailPage(key: args.key, partnerId: args.partnerId);
},
);
}
class DriftPartnerDetailRouteArgs {
const DriftPartnerDetailRouteArgs({this.key, required this.partnerId});
final Key? key;
final String partnerId;
@override
String toString() {
return 'DriftPartnerDetailRouteArgs{key: $key, partnerId: $partnerId}';
}
}
/// generated route for
/// [DriftRecentlyTakenPage]
class DriftRecentlyTakenRoute extends PageRouteInfo<void> {
const DriftRecentlyTakenRoute({List<PageRouteInfo>? children})
: super(DriftRecentlyTakenRoute.name, initialChildren: children);
static const String name = 'DriftRecentlyTakenRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const DriftRecentlyTakenPage();
},
);
}
/// generated route for
/// [DriftTrashPage]
class DriftTrashRoute extends PageRouteInfo<void> {
@@ -106,7 +106,6 @@ class DeepLinkService {
Future<PageRouteInfo?> _buildAssetDeepLink(String assetId) async {
final asset = await _assetService.getAssetByRemoteId(assetId);
if (asset == null) {
return null;
}
@@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
class LocalAlbumsSliverAppBar extends StatelessWidget {
const LocalAlbumsSliverAppBar({super.key});
@override
Widget build(BuildContext context) {
return SliverAppBar(
floating: true,
pinned: true,
snap: false,
backgroundColor: context.colorScheme.surfaceContainer,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(5)),
),
automaticallyImplyLeading: true,
centerTitle: true,
title: Text(
"on_this_device".t(context: context),
),
);
}
}
@@ -0,0 +1,77 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
class SelectionSliverAppBar extends ConsumerStatefulWidget {
const SelectionSliverAppBar({
super.key,
});
@override
ConsumerState<SelectionSliverAppBar> createState() =>
_SelectionSliverAppBarState();
}
class _SelectionSliverAppBarState extends ConsumerState<SelectionSliverAppBar> {
@override
Widget build(BuildContext context) {
final selection = ref.watch(
multiSelectProvider.select((s) => s.selectedAssets),
);
final toExclude = ref.watch(
multiSelectProvider.select((s) => s.lockedSelectionAssets),
);
final filteredAssets = selection.where((asset) {
return !toExclude.contains(asset);
}).toSet();
onDone(Set<BaseAsset> selected) {
ref.read(multiSelectProvider.notifier).reset();
context.maybePop<Set<BaseAsset>>(selected);
}
return SliverAppBar(
floating: true,
pinned: true,
snap: false,
backgroundColor: context.colorScheme.surfaceContainer,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(5)),
),
automaticallyImplyLeading: false,
leading: IconButton(
icon: const Icon(Icons.close_rounded),
onPressed: () {
ref.read(multiSelectProvider.notifier).reset();
context.pop<Set<BaseAsset>>(null);
},
),
centerTitle: true,
title: Text(
"Select {count}".t(
context: context,
args: {
'count': filteredAssets.length.toString(),
},
),
),
actions: [
TextButton(
onPressed: () => onDone(filteredAssets),
child: Text(
'done'.t(context: context),
style: context.textTheme.titleSmall?.copyWith(
color: context.colorScheme.primary,
),
),
),
],
);
}
}