diff --git a/mobile/lib/domain/services/timeline.service.dart b/mobile/lib/domain/services/timeline.service.dart index a055f8bcae..eaf196a0fd 100644 --- a/mobile/lib/domain/services/timeline.service.dart +++ b/mobile/lib/domain/services/timeline.service.dart @@ -56,6 +56,8 @@ class TimelineFactory { TimelineService localAlbum({required String albumId}) => TimelineService(_timelineRepository.localAlbum(albumId, groupBy)); + TimelineService allLocal() => TimelineService(_timelineRepository.allLocal(groupBy)); + TimelineService remoteAlbum({required String albumId}) => TimelineService(_timelineRepository.remoteAlbum(albumId, groupBy)); diff --git a/mobile/lib/infrastructure/repositories/timeline.repository.dart b/mobile/lib/infrastructure/repositories/timeline.repository.dart index 65788f07e2..c5afd9ebef 100644 --- a/mobile/lib/infrastructure/repositories/timeline.repository.dart +++ b/mobile/lib/infrastructure/repositories/timeline.repository.dart @@ -118,6 +118,40 @@ class DriftTimelineRepository extends DriftDatabaseRepository { origin: TimelineOrigin.localAlbum, ); + TimelineQuery allLocal(GroupAssetsBy groupBy) => ( + bucketSource: () => _watchAllLocalBucket(groupBy: groupBy), + assetSource: (offset, count) => _getAllLocalAssets(offset: offset, count: count), + origin: TimelineOrigin.localAlbum, + ); + + Stream> _watchAllLocalBucket({GroupAssetsBy groupBy = GroupAssetsBy.day}) { + if (groupBy == GroupAssetsBy.none) { + return _db.localAssetEntity.count().map(_generateBuckets).watchSingle(); + } + + final assetCountExp = _db.localAssetEntity.id.count(); + final dateExp = _db.localAssetEntity.createdAt.dateFmt(groupBy, toLocal: true); + + final query = _db.localAssetEntity.selectOnly() + ..addColumns([assetCountExp, dateExp]) + ..groupBy([dateExp]) + ..orderBy([OrderingTerm.desc(dateExp)]); + + return query.map((row) { + final timeline = row.read(dateExp)!.truncateDate(groupBy); + final assetCount = row.read(assetCountExp)!; + return TimeBucket(date: timeline, assetCount: assetCount); + }).watch(); + } + + Future> _getAllLocalAssets({required int offset, required int count}) { + final query = _db.localAssetEntity.select() + ..orderBy([(row) => OrderingTerm.desc(row.createdAt)]) + ..limit(count, offset: offset); + + return query.map((row) => row.toDto()).get(); + } + Stream> _watchLocalAlbumBucket(String albumId, {GroupAssetsBy groupBy = GroupAssetsBy.day}) { if (groupBy == GroupAssetsBy.none) { return _db.localAlbumAssetEntity diff --git a/mobile/lib/pages/common/splash_screen.page.dart b/mobile/lib/pages/common/splash_screen.page.dart index 725f7f9e85..15e400a16d 100644 --- a/mobile/lib/pages/common/splash_screen.page.dart +++ b/mobile/lib/pages/common/splash_screen.page.dart @@ -355,9 +355,9 @@ class SplashScreenPageState extends ConsumerState { ), ); } else { - log.severe('Missing crucial offline login info - Logging out completely'); + log.info('No stored credentials - showing local gallery in guest mode'); unawaited(ref.read(authProvider.notifier).logout()); - unawaited(context.replaceRoute(const LoginRoute())); + unawaited(context.replaceRoute(const GuestGalleryRoute())); return; } diff --git a/mobile/lib/presentation/pages/guest_gallery.page.dart b/mobile/lib/presentation/pages/guest_gallery.page.dart new file mode 100644 index 0000000000..cbff48c6c7 --- /dev/null +++ b/mobile/lib/presentation/pages/guest_gallery.page.dart @@ -0,0 +1,32 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.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/routing/router.dart'; + +@RoutePage() +class GuestGalleryPage extends StatelessWidget { + const GuestGalleryPage({super.key}); + + @override + Widget build(BuildContext context) { + return ProviderScope( + overrides: [timelineServiceProvider.overrideWith((ref) => ref.watch(localOnlyTimelineServiceProvider))], + child: Timeline( + appBar: SliverAppBar( + title: const Text('On this device'), + floating: true, + actions: [ + TextButton.icon( + onPressed: () => context.replaceRoute(const LoginRoute()), + icon: const Icon(Icons.login), + label: const Text('Sign in'), + ), + ], + ), + showStorageIndicator: false, + ), + ); + } +} diff --git a/mobile/lib/providers/infrastructure/timeline.provider.dart b/mobile/lib/providers/infrastructure/timeline.provider.dart index 06ec0242b2..81ca2e78ab 100644 --- a/mobile/lib/providers/infrastructure/timeline.provider.dart +++ b/mobile/lib/providers/infrastructure/timeline.provider.dart @@ -26,6 +26,12 @@ final timelineServiceProvider = Provider( dependencies: [], ); +final localOnlyTimelineServiceProvider = Provider((ref) { + final timelineService = ref.watch(timelineFactoryProvider).allLocal(); + ref.onDispose(timelineService.dispose); + return timelineService; +}); + final timelineFactoryProvider = Provider( (ref) => TimelineFactory( timelineRepository: ref.watch(timelineRepositoryProvider), diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 0a9f6c4199..bfab501f59 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -63,6 +63,7 @@ import 'package:immich_mobile/presentation/pages/drift_trash.page.dart'; import 'package:immich_mobile/presentation/pages/drift_user_selection.page.dart'; import 'package:immich_mobile/presentation/pages/drift_video.page.dart'; import 'package:immich_mobile/presentation/pages/edit/drift_edit.page.dart'; +import 'package:immich_mobile/presentation/pages/guest_gallery.page.dart'; import 'package:immich_mobile/presentation/pages/local_timeline.page.dart'; import 'package:immich_mobile/presentation/pages/profile/profile_picture_crop.page.dart'; import 'package:immich_mobile/presentation/pages/search/drift_search.page.dart'; @@ -114,6 +115,7 @@ class AppRouter extends RootStackRouter { @override late final List routes = [ AutoRoute(page: SplashScreenRoute.page, initial: true), + AutoRoute(page: GuestGalleryRoute.page), AutoRoute(page: LoginRoute.page), AutoRoute(page: ChangePasswordRoute.page), AutoRoute( diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index c025da0f73..36e883f794 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -1224,6 +1224,22 @@ class FolderRouteArgs { int get hashCode => key.hashCode ^ folder.hashCode; } +/// generated route for +/// [GuestGalleryPage] +class GuestGalleryRoute extends PageRouteInfo { + const GuestGalleryRoute({List? children}) + : super(GuestGalleryRoute.name, initialChildren: children); + + static const String name = 'GuestGalleryRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const GuestGalleryPage(); + }, + ); +} + /// generated route for /// [HeaderSettingsPage] class HeaderSettingsRoute extends PageRouteInfo {