From 596a3bd689fb36cd9f357f39f77436b651793e0f Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 8 Jul 2025 12:58:38 -0500 Subject: [PATCH] adapt to more pages --- .../lib/domain/models/album/album.model.dart | 6 +- .../domain/services/remote_album.service.dart | 12 +- mobile/lib/domain/utils/background_sync.dart | 119 +++----- .../repositories/remote_album.repository.dart | 7 +- .../pages/dev/drift_archive.page.dart | 3 +- .../pages/dev/drift_favorite.page.dart | 1 + .../pages/dev/drift_local_album.page.dart | 2 +- .../pages/dev/drift_trash.page.dart | 13 +- .../pages/dev/local_timeline.page.dart | 15 +- .../pages/dev/media_stat.page.dart | 4 +- .../pages/dev/remote_timeline.page.dart | 17 +- .../presentation/pages/drift_album.page.dart | 31 ++- .../infrastructure/remote_album.provider.dart | 12 +- mobile/lib/routing/router.dart | 2 + mobile/lib/routing/router.gr.dart | 24 +- mobile/lib/utils/remote_album.utils.dart | 21 +- .../common/mesmerizing_sliver_app_bar.dart | 257 ++++++++---------- 17 files changed, 248 insertions(+), 298 deletions(-) diff --git a/mobile/lib/domain/models/album/album.model.dart b/mobile/lib/domain/models/album/album.model.dart index 29f75f29ef..7cafca9116 100644 --- a/mobile/lib/domain/models/album/album.model.dart +++ b/mobile/lib/domain/models/album/album.model.dart @@ -11,7 +11,7 @@ enum AlbumUserRole { } // Model for an album stored in the server -class Album { +class RemoteAlbum { final String id; final String name; final String ownerId; @@ -24,7 +24,7 @@ class Album { final int assetCount; final String ownerName; - const Album({ + const RemoteAlbum({ required this.id, required this.name, required this.ownerId, @@ -57,7 +57,7 @@ class Album { @override bool operator ==(Object other) { - if (other is! Album) return false; + if (other is! RemoteAlbum) return false; if (identical(this, other)) return true; return id == other.id && name == other.name && diff --git a/mobile/lib/domain/services/remote_album.service.dart b/mobile/lib/domain/services/remote_album.service.dart index f3106470d2..9ff00e1ce3 100644 --- a/mobile/lib/domain/services/remote_album.service.dart +++ b/mobile/lib/domain/services/remote_album.service.dart @@ -8,26 +8,26 @@ class RemoteAlbumService { const RemoteAlbumService(this._repository); - Future> getAll() { + Future> getAll() { return _repository.getAll(); } - List sortAlbums( - List albums, + List sortAlbums( + List albums, RemoteAlbumSortMode sortMode, { bool isReverse = false, }) { return sortMode.sortFn(albums, isReverse); } - List searchAlbums( - List albums, + List searchAlbums( + List albums, String query, String? userId, [ QuickFilterMode filterMode = QuickFilterMode.all, ]) { final lowerQuery = query.toLowerCase(); - List filtered = albums; + List filtered = albums; // Apply text search filter if (query.isNotEmpty) { diff --git a/mobile/lib/domain/utils/background_sync.dart b/mobile/lib/domain/utils/background_sync.dart index b2bf37b2bf..c8d2e2b624 100644 --- a/mobile/lib/domain/utils/background_sync.dart +++ b/mobile/lib/domain/utils/background_sync.dart @@ -9,57 +9,8 @@ class BackgroundSyncManager { Cancelable? _deviceAlbumSyncTask; Cancelable? _hashTask; - Completer? _localSyncMutex; - Completer? _remoteSyncMutex; - Completer? _hashMutex; - BackgroundSyncManager(); - Future _withMutex( - Completer? Function() getMutex, - void Function(Completer?) setMutex, - Future Function() operation, - ) async { - while (getMutex() != null) { - await getMutex()!.future; - } - - final mutex = Completer(); - setMutex(mutex); - - try { - final result = await operation(); - return result; - } finally { - setMutex(null); - mutex.complete(); - } - } - - Future _withLocalSyncMutex(Future Function() operation) { - return _withMutex( - () => _localSyncMutex, - (mutex) => _localSyncMutex = mutex, - operation, - ); - } - - Future _withRemoteSyncMutex(Future Function() operation) { - return _withMutex( - () => _remoteSyncMutex, - (mutex) => _remoteSyncMutex = mutex, - operation, - ); - } - - Future _withHashMutex(Future Function() operation) { - return _withMutex( - () => _hashMutex, - (mutex) => _hashMutex = mutex, - operation, - ); - } - Future cancel() { final futures = []; @@ -74,57 +25,51 @@ class BackgroundSyncManager { // No need to cancel the task, as it can also be run when the user logs out Future syncLocal({bool full = false}) { - return _withLocalSyncMutex(() async { - if (_deviceAlbumSyncTask != null) { - return _deviceAlbumSyncTask!.future; - } + if (_deviceAlbumSyncTask != null) { + return _deviceAlbumSyncTask!.future; + } - // We use a ternary operator to avoid [_deviceAlbumSyncTask] from being - // captured by the closure passed to [runInIsolateGentle]. - _deviceAlbumSyncTask = full - ? runInIsolateGentle( - computation: (ref) => - ref.read(localSyncServiceProvider).sync(full: true), - ) - : runInIsolateGentle( - computation: (ref) => - ref.read(localSyncServiceProvider).sync(full: false), - ); + // We use a ternary operator to avoid [_deviceAlbumSyncTask] from being + // captured by the closure passed to [runInIsolateGentle]. + _deviceAlbumSyncTask = full + ? runInIsolateGentle( + computation: (ref) => + ref.read(localSyncServiceProvider).sync(full: true), + ) + : runInIsolateGentle( + computation: (ref) => + ref.read(localSyncServiceProvider).sync(full: false), + ); - return _deviceAlbumSyncTask!.whenComplete(() { - _deviceAlbumSyncTask = null; - }); + return _deviceAlbumSyncTask!.whenComplete(() { + _deviceAlbumSyncTask = null; }); } // No need to cancel the task, as it can also be run when the user logs out Future hashAssets() { - return _withHashMutex(() async { - if (_hashTask != null) { - return _hashTask!.future; - } + if (_hashTask != null) { + return _hashTask!.future; + } - _hashTask = runInIsolateGentle( - computation: (ref) => ref.read(hashServiceProvider).hashAssets(), - ); - return _hashTask!.whenComplete(() { - _hashTask = null; - }); + _hashTask = runInIsolateGentle( + computation: (ref) => ref.read(hashServiceProvider).hashAssets(), + ); + return _hashTask!.whenComplete(() { + _hashTask = null; }); } Future syncRemote() { - return _withRemoteSyncMutex(() async { - if (_syncTask != null) { - return _syncTask!.future; - } + if (_syncTask != null) { + return _syncTask!.future; + } - _syncTask = runInIsolateGentle( - computation: (ref) => ref.read(syncStreamServiceProvider).sync(), - ); - return _syncTask!.whenComplete(() { - _syncTask = null; - }); + _syncTask = runInIsolateGentle( + computation: (ref) => ref.read(syncStreamServiceProvider).sync(), + ); + return _syncTask!.whenComplete(() { + _syncTask = null; }); } } diff --git a/mobile/lib/infrastructure/repositories/remote_album.repository.dart b/mobile/lib/infrastructure/repositories/remote_album.repository.dart index b1b73b4cdc..8050f3b63e 100644 --- a/mobile/lib/infrastructure/repositories/remote_album.repository.dart +++ b/mobile/lib/infrastructure/repositories/remote_album.repository.dart @@ -9,7 +9,8 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { final Drift _db; const DriftRemoteAlbumRepository(this._db) : super(_db); - Future> getAll({Set sortBy = const {}}) { + Future> getAll( + {Set sortBy = const {}}) { final assetCount = _db.remoteAlbumAssetEntity.assetId.count(); final query = _db.remoteAlbumEntity.select().join([ @@ -59,8 +60,8 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { } extension on RemoteAlbumEntityData { - Album toDto({int assetCount = 0, required String ownerName}) { - return Album( + RemoteAlbum toDto({int assetCount = 0, required String ownerName}) { + return RemoteAlbum( id: id, name: name, ownerId: ownerId, diff --git a/mobile/lib/presentation/pages/dev/drift_archive.page.dart b/mobile/lib/presentation/pages/dev/drift_archive.page.dart index 07788bc8f9..afde5df5a8 100644 --- a/mobile/lib/presentation/pages/dev/drift_archive.page.dart +++ b/mobile/lib/presentation/pages/dev/drift_archive.page.dart @@ -1,5 +1,5 @@ import 'package:auto_route/auto_route.dart'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; @@ -32,6 +32,7 @@ class DriftArchivePage extends StatelessWidget { child: Timeline( appBar: MesmerizingSliverAppBar( title: 'archive'.t(context: context), + icon: Icons.archive_outlined, // Icon for the archive page ), ), ); diff --git a/mobile/lib/presentation/pages/dev/drift_favorite.page.dart b/mobile/lib/presentation/pages/dev/drift_favorite.page.dart index 58196a1832..43f270574b 100644 --- a/mobile/lib/presentation/pages/dev/drift_favorite.page.dart +++ b/mobile/lib/presentation/pages/dev/drift_favorite.page.dart @@ -32,6 +32,7 @@ class DriftFavoritePage extends StatelessWidget { child: Timeline( appBar: MesmerizingSliverAppBar( title: 'favorites'.t(context: context), + icon: Icons.favorite_outline, ), ), ); diff --git a/mobile/lib/presentation/pages/dev/drift_local_album.page.dart b/mobile/lib/presentation/pages/dev/drift_local_album.page.dart index f47811b6da..0ad9abd2fa 100644 --- a/mobile/lib/presentation/pages/dev/drift_local_album.page.dart +++ b/mobile/lib/presentation/pages/dev/drift_local_album.page.dart @@ -103,7 +103,7 @@ class _AlbumList extends ConsumerWidget { ), ), onTap: () => - context.pushRoute(LocalTimelineRoute(albumId: album.id)), + context.pushRoute(LocalTimelineRoute(album: album)), ), ); }, diff --git a/mobile/lib/presentation/pages/dev/drift_trash.page.dart b/mobile/lib/presentation/pages/dev/drift_trash.page.dart index cbcfe50112..86b00f2282 100644 --- a/mobile/lib/presentation/pages/dev/drift_trash.page.dart +++ b/mobile/lib/presentation/pages/dev/drift_trash.page.dart @@ -1,6 +1,8 @@ import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/translate_extensions.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'; @@ -27,7 +29,16 @@ class DriftTrashPage extends StatelessWidget { }, ), ], - child: const Timeline(), + child: Timeline( + appBar: SliverAppBar( + title: Text('trash'.t(context: context)), + floating: true, + snap: true, + pinned: true, + centerTitle: true, + elevation: 0, + ), + ), ); } } diff --git a/mobile/lib/presentation/pages/dev/local_timeline.page.dart b/mobile/lib/presentation/pages/dev/local_timeline.page.dart index 3a98a81e9e..f966109289 100644 --- a/mobile/lib/presentation/pages/dev/local_timeline.page.dart +++ b/mobile/lib/presentation/pages/dev/local_timeline.page.dart @@ -1,14 +1,16 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/album/local_album.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/widgets/common/mesmerizing_sliver_app_bar.dart'; @RoutePage() class LocalTimelinePage extends StatelessWidget { - final String albumId; + final LocalAlbum album; - const LocalTimelinePage({super.key, required this.albumId}); + const LocalTimelinePage({super.key, required this.album}); @override Widget build(BuildContext context) { @@ -16,14 +18,17 @@ class LocalTimelinePage extends StatelessWidget { overrides: [ timelineServiceProvider.overrideWith( (ref) { - final timelineService = - ref.watch(timelineFactoryProvider).localAlbum(albumId: albumId); + final timelineService = ref + .watch(timelineFactoryProvider) + .localAlbum(albumId: album.id); ref.onDispose(timelineService.dispose); return timelineService; }, ), ], - child: const Timeline(), + child: Timeline( + appBar: MesmerizingSliverAppBar(title: album.name), + ), ); } } diff --git a/mobile/lib/presentation/pages/dev/media_stat.page.dart b/mobile/lib/presentation/pages/dev/media_stat.page.dart index e5745fa629..0a77d9dfe8 100644 --- a/mobile/lib/presentation/pages/dev/media_stat.page.dart +++ b/mobile/lib/presentation/pages/dev/media_stat.page.dart @@ -125,7 +125,7 @@ class LocalMediaSummaryPage extends StatelessWidget { name: album.name, countFuture: countFuture, onTap: () => context.router.push( - LocalTimelineRoute(albumId: album.id), + LocalTimelineRoute(album: album), ), ); }, @@ -226,7 +226,7 @@ class RemoteMediaSummaryPage extends StatelessWidget { name: album.name, countFuture: countFuture, onTap: () => context.router.push( - RemoteTimelineRoute(albumId: album.id), + RemoteTimelineRoute(album: album), ), ); }, diff --git a/mobile/lib/presentation/pages/dev/remote_timeline.page.dart b/mobile/lib/presentation/pages/dev/remote_timeline.page.dart index 6568f0f74f..2930a3c3d8 100644 --- a/mobile/lib/presentation/pages/dev/remote_timeline.page.dart +++ b/mobile/lib/presentation/pages/dev/remote_timeline.page.dart @@ -1,14 +1,16 @@ import 'package:auto_route/auto_route.dart'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/album/album.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/widgets/common/mesmerizing_sliver_app_bar.dart'; @RoutePage() class RemoteTimelinePage extends StatelessWidget { - final String albumId; + final RemoteAlbum album; - const RemoteTimelinePage({super.key, required this.albumId}); + const RemoteTimelinePage({super.key, required this.album}); @override Widget build(BuildContext context) { @@ -18,13 +20,18 @@ class RemoteTimelinePage extends StatelessWidget { (ref) { final timelineService = ref .watch(timelineFactoryProvider) - .remoteAlbum(albumId: albumId); + .remoteAlbum(albumId: album.id); ref.onDispose(timelineService.dispose); return timelineService; }, ), ], - child: const Timeline(), + child: Timeline( + appBar: MesmerizingSliverAppBar( + title: album.name, + icon: Icons.photo_album_outlined, + ), + ), ); } } diff --git a/mobile/lib/presentation/pages/drift_album.page.dart b/mobile/lib/presentation/pages/drift_album.page.dart index 3298f12b65..4a7b12126a 100644 --- a/mobile/lib/presentation/pages/drift_album.page.dart +++ b/mobile/lib/presentation/pages/drift_album.page.dart @@ -475,7 +475,7 @@ class _AlbumList extends StatelessWidget { final bool isLoading; final String? error; - final List albums; + final List albums; final String? userId; @override @@ -555,7 +555,7 @@ class _AlbumList extends StatelessWidget { ), ), onTap: () => context.router.push( - RemoteTimelineRoute(albumId: album.id), + RemoteTimelineRoute(album: album), ), leadingPadding: const EdgeInsets.only( right: 16, @@ -573,13 +573,24 @@ class _AlbumList extends StatelessWidget { ), ), ) - : const SizedBox( + : SizedBox( width: 80, height: 80, - child: Icon( - Icons.photo_album_rounded, - size: 40, - color: Colors.grey, + child: 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: const Icon( + Icons.photo_album_rounded, + size: 24, + color: Colors.grey, + ), ), ), ), @@ -599,7 +610,7 @@ class _AlbumGrid extends StatelessWidget { required this.error, }); - final List albums; + final List albums; final String? userId; final bool isLoading; final String? error; @@ -674,14 +685,14 @@ class _GridAlbumCard extends StatelessWidget { required this.userId, }); - final Album album; + final RemoteAlbum album; final String? userId; @override Widget build(BuildContext context) { return GestureDetector( onTap: () => context.router.push( - RemoteTimelineRoute(albumId: album.id), + RemoteTimelineRoute(album: album), ), child: Card( elevation: 0, diff --git a/mobile/lib/providers/infrastructure/remote_album.provider.dart b/mobile/lib/providers/infrastructure/remote_album.provider.dart index 2e5a475b9c..b80d791b0a 100644 --- a/mobile/lib/providers/infrastructure/remote_album.provider.dart +++ b/mobile/lib/providers/infrastructure/remote_album.provider.dart @@ -8,21 +8,21 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'album.provider.dart'; class RemoteAlbumState { - final List albums; - final List filteredAlbums; + final List albums; + final List filteredAlbums; final bool isLoading; final String? error; const RemoteAlbumState({ required this.albums, - List? filteredAlbums, + List? filteredAlbums, this.isLoading = false, this.error, }) : filteredAlbums = filteredAlbums ?? albums; RemoteAlbumState copyWith({ - List? albums, - List? filteredAlbums, + List? albums, + List? filteredAlbums, bool? isLoading, String? error, }) { @@ -66,7 +66,7 @@ class RemoteAlbumNotifier extends Notifier { return const RemoteAlbumState(albums: [], filteredAlbums: []); } - Future> getAll() async { + Future> getAll() async { state = state.copyWith(isLoading: true, error: null); try { diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 18aa937a9d..7becbd4804 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -1,6 +1,8 @@ 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/album/album.model.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/models/log.model.dart'; import 'package:immich_mobile/domain/models/memory.model.dart'; diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index b13320b1c0..8e8da4d8d0 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -1211,11 +1211,11 @@ class LocalMediaSummaryRoute extends PageRouteInfo { class LocalTimelineRoute extends PageRouteInfo { LocalTimelineRoute({ Key? key, - required String albumId, + required LocalAlbum album, List? children, }) : super( LocalTimelineRoute.name, - args: LocalTimelineRouteArgs(key: key, albumId: albumId), + args: LocalTimelineRouteArgs(key: key, album: album), initialChildren: children, ); @@ -1225,21 +1225,21 @@ class LocalTimelineRoute extends PageRouteInfo { name, builder: (data) { final args = data.argsAs(); - return LocalTimelinePage(key: args.key, albumId: args.albumId); + return LocalTimelinePage(key: args.key, album: args.album); }, ); } class LocalTimelineRouteArgs { - const LocalTimelineRouteArgs({this.key, required this.albumId}); + const LocalTimelineRouteArgs({this.key, required this.album}); final Key? key; - final String albumId; + final LocalAlbum album; @override String toString() { - return 'LocalTimelineRouteArgs{key: $key, albumId: $albumId}'; + return 'LocalTimelineRouteArgs{key: $key, album: $album}'; } } @@ -1765,11 +1765,11 @@ class RemoteMediaSummaryRoute extends PageRouteInfo { class RemoteTimelineRoute extends PageRouteInfo { RemoteTimelineRoute({ Key? key, - required String albumId, + required RemoteAlbum album, List? children, }) : super( RemoteTimelineRoute.name, - args: RemoteTimelineRouteArgs(key: key, albumId: albumId), + args: RemoteTimelineRouteArgs(key: key, album: album), initialChildren: children, ); @@ -1779,21 +1779,21 @@ class RemoteTimelineRoute extends PageRouteInfo { name, builder: (data) { final args = data.argsAs(); - return RemoteTimelinePage(key: args.key, albumId: args.albumId); + return RemoteTimelinePage(key: args.key, album: args.album); }, ); } class RemoteTimelineRouteArgs { - const RemoteTimelineRouteArgs({this.key, required this.albumId}); + const RemoteTimelineRouteArgs({this.key, required this.album}); final Key? key; - final String albumId; + final RemoteAlbum album; @override String toString() { - return 'RemoteTimelineRouteArgs{key: $key, albumId: $albumId}'; + return 'RemoteTimelineRouteArgs{key: $key, album: $album}'; } } diff --git a/mobile/lib/utils/remote_album.utils.dart b/mobile/lib/utils/remote_album.utils.dart index 4fc7ba5f74..d1934b4b52 100644 --- a/mobile/lib/utils/remote_album.utils.dart +++ b/mobile/lib/utils/remote_album.utils.dart @@ -1,38 +1,44 @@ import 'package:collection/collection.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; -typedef AlbumSortFn = List Function(List albums, bool isReverse); +typedef AlbumSortFn = List Function( + List albums, bool isReverse); class _RemoteAlbumSortHandlers { const _RemoteAlbumSortHandlers._(); static const AlbumSortFn created = _sortByCreated; - static List _sortByCreated(List albums, bool isReverse) { + static List _sortByCreated( + List albums, bool isReverse) { final sorted = albums.sortedBy((album) => album.createdAt); return (isReverse ? sorted.reversed : sorted).toList(); } static const AlbumSortFn title = _sortByTitle; - static List _sortByTitle(List albums, bool isReverse) { + static List _sortByTitle( + List albums, bool isReverse) { final sorted = albums.sortedBy((album) => album.name); return (isReverse ? sorted.reversed : sorted).toList(); } static const AlbumSortFn lastModified = _sortByLastModified; - static List _sortByLastModified(List albums, bool isReverse) { + static List _sortByLastModified( + List albums, bool isReverse) { final sorted = albums.sortedBy((album) => album.updatedAt); return (isReverse ? sorted.reversed : sorted).toList(); } static const AlbumSortFn assetCount = _sortByAssetCount; - static List _sortByAssetCount(List albums, bool isReverse) { + static List _sortByAssetCount( + List albums, bool isReverse) { final sorted = albums.sorted((a, b) => a.assetCount.compareTo(b.assetCount)); return (isReverse ? sorted.reversed : sorted).toList(); } static const AlbumSortFn mostRecent = _sortByMostRecent; - static List _sortByMostRecent(List albums, bool isReverse) { + static List _sortByMostRecent( + List albums, bool isReverse) { final sorted = albums.sorted((a, b) { // For most recent, we sort by updatedAt in descending order return b.updatedAt.compareTo(a.updatedAt); @@ -41,7 +47,8 @@ class _RemoteAlbumSortHandlers { } static const AlbumSortFn mostOldest = _sortByMostOldest; - static List _sortByMostOldest(List albums, bool isReverse) { + static List _sortByMostOldest( + List albums, bool isReverse) { final sorted = albums.sorted((a, b) { // For oldest, we sort by createdAt in ascending order return a.createdAt.compareTo(b.createdAt); diff --git a/mobile/lib/widgets/common/mesmerizing_sliver_app_bar.dart b/mobile/lib/widgets/common/mesmerizing_sliver_app_bar.dart index 52ec6e0058..d259683bad 100644 --- a/mobile/lib/widgets/common/mesmerizing_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/mesmerizing_sliver_app_bar.dart @@ -12,9 +12,11 @@ class MesmerizingSliverAppBar extends ConsumerWidget { const MesmerizingSliverAppBar({ super.key, required this.title, + this.icon = Icons.camera, }); final String title; + final IconData icon; double _calculateScrollProgress(FlexibleSpaceBarSettings? settings) { if (settings?.maxExtent == null || settings?.minExtent == null) { @@ -70,6 +72,7 @@ class MesmerizingSliverAppBar extends ConsumerWidget { assetCount: assetCount, scrollProgress: scrollProgress, title: title, + icon: icon, ), ); }, @@ -83,11 +86,13 @@ class _ExpandedBackground extends ConsumerWidget { final int assetCount; final double scrollProgress; final String title; + final IconData icon; const _ExpandedBackground({ required this.assetCount, required this.scrollProgress, required this.title, + required this.icon, }); @override @@ -101,7 +106,10 @@ class _ExpandedBackground extends ConsumerWidget { offset: Offset(0, scrollProgress * 50), child: Transform.scale( scale: 1.4 - (scrollProgress * 0.2), - child: _RandomAssetBackground(timelineService: timelineService), + child: _RandomAssetBackground( + timelineService: timelineService, + icon: icon, + ), ), ), Container( @@ -175,8 +183,12 @@ class _ExpandedBackground extends ConsumerWidget { class _RandomAssetBackground extends StatefulWidget { final TimelineService timelineService; + final IconData icon; - const _RandomAssetBackground({required this.timelineService}); + const _RandomAssetBackground({ + required this.timelineService, + required this.icon, + }); @override State<_RandomAssetBackground> createState() => _RandomAssetBackgroundState(); @@ -192,6 +204,18 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground> BaseAsset? _currentAsset; BaseAsset? _nextAsset; + final LinearGradient gradient = LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.pink.shade300.withValues(alpha: 0.9), + Colors.purple.shade400.withValues(alpha: 0.8), + Colors.indigo.shade400.withValues(alpha: 0.9), + Colors.blue.shade500.withValues(alpha: 0.8), + ], + stops: const [0.0, 0.3, 0.7, 1.0], + ); + @override void initState() { super.initState(); @@ -237,7 +261,7 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground> ); Future.delayed( - const Duration(milliseconds: 100), + Durations.medium1, () => _loadRandomAsset(), ); } @@ -275,6 +299,7 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground> : 0; final assets = widget.timelineService.getAssets(randomIndex, 1); + if (assets.isEmpty) { return; } @@ -330,119 +355,14 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground> @override Widget build(BuildContext context) { if (widget.timelineService.totalAssets == 0) { - return Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: context.isDarkTheme - ? [ - Colors.deepPurple.withValues(alpha: 0.8), - Colors.indigo.withValues(alpha: 0.9), - Colors.purple.withValues(alpha: 0.8), - Colors.pink.withValues(alpha: 0.7), - ] - : [ - Colors.pink.shade300.withValues(alpha: 0.9), - Colors.purple.shade400.withValues(alpha: 0.8), - Colors.indigo.shade400.withValues(alpha: 0.9), - Colors.blue.shade500.withValues(alpha: 0.8), - ], - stops: const [0.0, 0.3, 0.7, 1.0], - ), - ), - child: Stack( - children: [ - Positioned( - top: 40, - right: 30, - child: Container( - width: 80, - height: 80, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: context.isDarkTheme - ? Colors.white.withValues(alpha: 0.1) - : Colors.white.withValues(alpha: 0.2), - ), - ), - ), - Positioned( - bottom: 100, - left: 50, - child: Container( - width: 60, - height: 60, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: context.isDarkTheme - ? Colors.white.withValues(alpha: 0.08) - : Colors.white.withValues(alpha: 0.15), - ), - ), - ), - Positioned( - top: 120, - left: 20, - child: Container( - width: 40, - height: 40, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: context.isDarkTheme - ? Colors.white.withValues(alpha: 0.06) - : Colors.white.withValues(alpha: 0.12), - ), - ), - ), - // Heart icon for empty favorites - Center( - child: Icon( - Icons.favorite_outline, - size: 100, - color: context.isDarkTheme - ? Colors.white.withValues(alpha: 0.15) - : Colors.white.withValues(alpha: 0.25), - ), - ), - ], - ), + return _EmptyPageExtendedBackground( + gradient: gradient, + icon: widget.icon, ); } if (_currentAsset == null) { - return AnimatedContainer( - duration: const Duration(milliseconds: 300), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: context.isDarkTheme - ? [ - Colors.deepPurple.withValues(alpha: 0.4), - Colors.indigo.withValues(alpha: 0.5), - Colors.purple.withValues(alpha: 0.4), - ] - : [ - Colors.blue.shade200.withValues(alpha: 0.6), - Colors.purple.shade300.withValues(alpha: 0.5), - Colors.indigo.shade300.withValues(alpha: 0.6), - ], - ), - ), - child: const Center( - child: SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation( - Colors.white70, - ), - ), - ), - ), - ); + return const SizedBox.shrink(); } return AnimatedBuilder( @@ -451,7 +371,7 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground> builder: (context, child) { return Transform.translate( offset: Offset( - _panAnimation.value.dx * 100, // Convert to pixel offset + _panAnimation.value.dx * 100, _panAnimation.value.dy * 100, ), child: Transform.scale( @@ -469,47 +389,14 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground> if (wasSynchronouslyLoaded || frame != null) { return child; } - // Show a subtle loading state while the full image loads + return Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: context.isDarkTheme - ? [ - Colors.deepPurple.withValues(alpha: 0.3), - Colors.indigo.withValues(alpha: 0.4), - Colors.purple.withValues(alpha: 0.3), - ] - : [ - Colors.blue.shade200.withValues(alpha: 0.5), - Colors.purple.shade300.withValues(alpha: 0.4), - Colors.indigo.shade300.withValues(alpha: 0.5), - ], - ), - ), + decoration: BoxDecoration(gradient: gradient), ); }, errorBuilder: (context, error, stackTrace) { - // Fallback to a gradient if image fails to load return Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: context.isDarkTheme - ? [ - Colors.deepPurple.withValues(alpha: 0.6), - Colors.indigo.withValues(alpha: 0.7), - Colors.purple.withValues(alpha: 0.6), - ] - : [ - Colors.blue.shade300.withValues(alpha: 0.7), - Colors.purple.shade400.withValues(alpha: 0.6), - Colors.indigo.shade400.withValues(alpha: 0.7), - ], - ), - ), + decoration: BoxDecoration(gradient: gradient), ); }, ), @@ -521,3 +408,75 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground> ); } } + +class _EmptyPageExtendedBackground extends StatelessWidget { + const _EmptyPageExtendedBackground({ + required this.gradient, + required this.icon, + }); + + final LinearGradient gradient; + final IconData icon; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration(gradient: gradient), + child: Stack( + children: [ + Positioned( + top: 40, + right: 30, + child: Container( + width: 80, + height: 80, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: context.isDarkTheme + ? Colors.white.withValues(alpha: 0.1) + : Colors.white.withValues(alpha: 0.2), + ), + ), + ), + Positioned( + bottom: 100, + left: 50, + child: Container( + width: 60, + height: 60, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: context.isDarkTheme + ? Colors.white.withValues(alpha: 0.08) + : Colors.white.withValues(alpha: 0.15), + ), + ), + ), + Positioned( + top: 120, + left: 20, + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: context.isDarkTheme + ? Colors.white.withValues(alpha: 0.06) + : Colors.white.withValues(alpha: 0.12), + ), + ), + ), + Center( + child: Icon( + icon, + size: 100, + color: context.isDarkTheme + ? Colors.white.withValues(alpha: 0.15) + : Colors.white.withValues(alpha: 0.25), + ), + ), + ], + ), + ); + } +}