feat(mobile): memories view

This commit is contained in:
Ben Beckford
2026-06-01 23:05:49 -07:00
parent bb8bfcdf1e
commit eee20881dd
10 changed files with 230 additions and 13 deletions
@@ -13,6 +13,10 @@ class DriftMemoryService {
return _repository.getAll(ownerId);
}
Future<List<DriftMemory>> getAllMemories(String ownerId) {
return _repository.getAll(ownerId, onlyToday: false);
}
Future<DriftMemory?> get(String memoryId) {
return _repository.get(memoryId);
}
@@ -9,10 +9,7 @@ class DriftMemoryRepository extends DriftDatabaseRepository {
final Drift _db;
const DriftMemoryRepository(this._db) : super(_db);
Future<List<DriftMemory>> getAll(String ownerId) async {
final now = DateTime.now();
final localUtc = DateTime.utc(now.year, now.month, now.day, 0, 0, 0);
Future<List<DriftMemory>> getAll(String ownerId, {bool onlyToday = true}) async {
final query =
_db.select(_db.memoryEntity).join([
innerJoin(_db.memoryAssetEntity, _db.memoryAssetEntity.memoryId.equalsExp(_db.memoryEntity.id)),
@@ -24,10 +21,17 @@ class DriftMemoryRepository extends DriftDatabaseRepository {
),
])
..where(_db.memoryEntity.ownerId.equals(ownerId))
..where(_db.memoryEntity.deletedAt.isNull())
..where(_db.memoryEntity.showAt.isSmallerOrEqualValue(localUtc))
..where(_db.memoryEntity.hideAt.isBiggerOrEqualValue(localUtc))
..orderBy([OrderingTerm.desc(_db.memoryEntity.memoryAt), OrderingTerm.asc(_db.remoteAssetEntity.createdAt)]);
..where(_db.memoryEntity.deletedAt.isNull());
if (onlyToday) {
final now = DateTime.now();
final localUtc = DateTime.utc(now.year, now.month, now.day, 0, 0, 0);
query.where(_db.memoryEntity.showAt.isSmallerOrEqualValue(localUtc));
query.where(_db.memoryEntity.hideAt.isBiggerOrEqualValue(localUtc));
}
query.orderBy([OrderingTerm.desc(_db.memoryEntity.memoryAt), OrderingTerm.asc(_db.remoteAssetEntity.createdAt)]);
final rows = await query.get();
if (rows.isEmpty) {
+1 -1
View File
@@ -112,7 +112,7 @@ void _onNavigationSelected(TabsRouter router, int index, WidgetRef ref) {
}
if (index == kPhotoTabIndex) {
ref.invalidate(driftMemoryFutureProvider);
ref.invalidate(driftMemoryLaneProvider);
}
if (router.activeIndex != kSearchTabIndex && index == kSearchTabIndex) {
@@ -11,7 +11,7 @@ class MainTimelinePage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final hasMemories = ref.watch(driftMemoryFutureProvider.select((state) => state.value?.isNotEmpty ?? false));
final hasMemories = ref.watch(driftMemoryLaneProvider.select((state) => state.value?.isNotEmpty ?? false));
return Timeline(
topSliverWidget: const SliverToBoxAdapter(child: DriftMemoryLane()),
topSliverWidgetHeight: hasMemories ? 200 : 0,
@@ -7,8 +7,10 @@ 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/presentation/widgets/images/local_album_thumbnail.widget.dart';
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
import 'package:immich_mobile/presentation/widgets/people/partner_user_avatar.widget.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
import 'package:immich_mobile/providers/infrastructure/partner.provider.dart';
import 'package:immich_mobile/providers/infrastructure/people.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
@@ -133,7 +135,12 @@ class _CollectionCards extends StatelessWidget {
child: Wrap(
spacing: 8,
runSpacing: 8,
children: [_PeopleCollectionCard(), _PlacesCollectionCard(), _LocalAlbumsCollectionCard()],
children: [
_PeopleCollectionCard(),
_PlacesCollectionCard(),
_LocalAlbumsCollectionCard(),
_MemoriesCollectionCard(),
],
),
),
);
@@ -327,6 +334,76 @@ class _LocalAlbumsCollectionCard extends ConsumerWidget {
}
}
class _MemoriesCollectionCard extends ConsumerWidget {
const _MemoriesCollectionCard();
@override
Widget build(BuildContext context, WidgetRef ref) {
final memories = ref.watch(driftAllMemoriesProvider);
return LayoutBuilder(
builder: (context, constraints) {
final isTablet = constraints.maxWidth > 600;
final widthFactor = isTablet ? 0.25 : 0.5;
final size = context.width * widthFactor - 20.0;
return GestureDetector(
onTap: () => context.pushRoute(const DriftMemoryListRoute()),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: size,
width: size,
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(20)),
gradient: LinearGradient(
colors: [context.colorScheme.primary.withAlpha(30), context.colorScheme.primary.withAlpha(25)],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: memories.widgetWhen(
onLoading: () => const Center(child: CircularProgressIndicator()),
onData: (memories) {
return GridView.count(
crossAxisCount: 2,
padding: const EdgeInsets.all(12),
crossAxisSpacing: 8,
mainAxisSpacing: 8,
physics: const NeverScrollableScrollPhysics(),
children: memories.take(4).map((memory) {
return ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(10)),
child: Thumbnail.remote(
remoteId: memory.assets[0].id,
thumbhash: memory.assets[0].thumbHash ?? "",
fit: BoxFit.cover,
),
);
}).toList(),
);
},
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'memories'.t(context: context),
style: context.textTheme.titleSmall?.copyWith(
color: context.colorScheme.onSurface,
fontWeight: FontWeight.w500,
),
),
),
],
),
);
},
);
}
}
class _QuickAccessButtonList extends ConsumerWidget {
const _QuickAccessButtonList();
@@ -0,0 +1,104 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/presentation/pages/drift_memory.page.dart';
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
import 'package:immich_mobile/routing/router.dart';
@RoutePage()
class DriftMemoryListPage extends ConsumerStatefulWidget {
const DriftMemoryListPage({super.key});
@override
ConsumerState<DriftMemoryListPage> createState() => _DriftMemoryListPageState();
}
class _DriftMemoryListPageState extends ConsumerState<DriftMemoryListPage> {
bool _onlyFavorites = false;
@override
Widget build(BuildContext context) {
final memories = ref.watch(driftAllMemoriesProvider);
return LayoutBuilder(
builder: (context, constraints) {
return Scaffold(
appBar: AppBar(
title: Text('memories'.tr()),
actions: [
IconButton(
icon: Icon(_onlyFavorites ? Icons.favorite : Icons.favorite_outline),
onPressed: () {
setState(() => _onlyFavorites = !_onlyFavorites);
},
),
],
),
body: SafeArea(
child: memories.when(
data: (memories) {
if (_onlyFavorites) {
memories = memories.where((memory) => memory.isSaved).toList();
}
return GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: constraints.maxWidth > 600 ? 4 : 2,
childAspectRatio: 0.5625,
mainAxisSpacing: 16,
crossAxisSpacing: 16,
),
padding: const EdgeInsets.all(16),
itemCount: memories.length,
itemBuilder: (context, index) => GestureDetector(
onTap: () {
if (memories[index].assets.isNotEmpty) {
DriftMemoryPage.setMemory(ref, memories[index]);
}
context.pushRoute(DriftMemoryRoute(memories: memories, memoryIndex: index));
},
child: Stack(
children: [
ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(10)),
child: ColorFiltered(
colorFilter: ColorFilter.mode(Colors.black.withValues(alpha: 0.2), BlendMode.darken),
child: AbsorbPointer(
child: Thumbnail.remote(
remoteId: memories[index].assets[0].id,
thumbhash: memories[index].assets[0].thumbHash ?? "",
fit: BoxFit.cover,
),
),
),
),
Positioned(
bottom: 16,
left: 16,
child: Text(
DateFormat.yMMMMd().format(memories[index].memoryAt),
style: const TextStyle(fontWeight: FontWeight.w600, color: Colors.white, fontSize: 15),
),
),
if (memories[index].isSaved)
const Positioned(
top: 16,
right: 16,
child: Icon(Icons.favorite, color: Colors.white, size: 24),
),
],
),
),
);
},
error: (error, stack) => const Text("Error loading memories"),
loading: () => const Center(child: CircularProgressIndicator()),
),
),
);
},
);
}
}
@@ -14,7 +14,7 @@ class DriftMemoryLane extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final memoryLaneProvider = ref.watch(driftMemoryFutureProvider);
final memoryLaneProvider = ref.watch(driftMemoryLaneProvider);
final memories = memoryLaneProvider.value ?? const [];
if (memories.isEmpty) {
return const SizedBox.shrink();
@@ -13,7 +13,7 @@ final driftMemoryServiceProvider = Provider<DriftMemoryService>(
(ref) => DriftMemoryService(ref.watch(driftMemoryRepositoryProvider)),
);
final driftMemoryFutureProvider = FutureProvider.autoDispose<List<DriftMemory>>((ref) {
final driftMemoryLaneProvider = FutureProvider.autoDispose<List<DriftMemory>>((ref) {
final (userId, enabled) = ref.watch(currentUserProvider.select((user) => (user?.id, user?.memoryEnabled ?? true)));
if (userId == null || !enabled) {
return const [];
@@ -22,3 +22,13 @@ final driftMemoryFutureProvider = FutureProvider.autoDispose<List<DriftMemory>>(
final service = ref.watch(driftMemoryServiceProvider);
return service.getMemoryLane(userId);
});
final driftAllMemoriesProvider = FutureProvider.autoDispose<List<DriftMemory>>((ref) {
final (userId, enabled) = ref.watch(currentUserProvider.select((user) => (user?.id, user?.memoryEnabled ?? true)));
if (userId == null || !enabled) {
return const [];
}
final service = ref.watch(driftMemoryServiceProvider);
return service.getAllMemories(userId);
});
+2
View File
@@ -52,6 +52,7 @@ import 'package:immich_mobile/presentation/pages/drift_local_album.page.dart';
import 'package:immich_mobile/presentation/pages/drift_locked_folder.page.dart';
import 'package:immich_mobile/presentation/pages/drift_map.page.dart';
import 'package:immich_mobile/presentation/pages/drift_memory.page.dart';
import 'package:immich_mobile/presentation/pages/drift_memory_list.page.dart';
import 'package:immich_mobile/presentation/pages/drift_partner_detail.page.dart';
import 'package:immich_mobile/presentation/pages/drift_people_collection.page.dart';
import 'package:immich_mobile/presentation/pages/drift_person.page.dart';
@@ -191,6 +192,7 @@ class AppRouter extends RootStackRouter {
AutoRoute(page: DownloadInfoRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: CleanupPreviewRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: DriftSlideshowRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: DriftMemoryListRoute.page, guards: [_authGuard, _duplicateGuard]),
// required to handle all deeplinks in deep_link.service.dart
// auto_route_library#1722
RedirectRoute(path: '*', redirectTo: '/'),
+16
View File
@@ -754,6 +754,22 @@ class DriftMapRouteArgs {
int get hashCode => key.hashCode ^ initialLocation.hashCode;
}
/// generated route for
/// [DriftMemoryListPage]
class DriftMemoryListRoute extends PageRouteInfo<void> {
const DriftMemoryListRoute({List<PageRouteInfo>? children})
: super(DriftMemoryListRoute.name, initialChildren: children);
static const String name = 'DriftMemoryListRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const DriftMemoryListPage();
},
);
}
/// generated route for
/// [DriftMemoryPage]
class DriftMemoryRoute extends PageRouteInfo<DriftMemoryRouteArgs> {