mirror of
https://github.com/immich-app/immich.git
synced 2025-07-09 03:04:16 -04:00
feat(mobile): drift local albums page (#19817)
* feat(mobile): drift local albums page * fix: lint * refactor: use AsyncValue * fix: lint * local album thumbnail --------- Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
parent
e703685d8d
commit
a556de67b0
17
mobile/lib/domain/services/local_album.service.dart
Normal file
17
mobile/lib/domain/services/local_album.service.dart
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -361,6 +361,24 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
|||||||
batch.deleteWhere(_db.localAssetEntity, (f) => f.id.isIn(ids));
|
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 {
|
extension on LocalAlbumEntityData {
|
||||||
|
116
mobile/lib/presentation/pages/dev/drift_local_album.page.dart
Normal file
116
mobile/lib/presentation/pages/dev/drift_local_album.page.dart
Normal file
@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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/asyncvalue_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_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/partner.provider.dart';
|
||||||
import 'package:immich_mobile/providers/search/people.provider.dart';
|
import 'package:immich_mobile/providers/search/people.provider.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:immich_mobile/utils/image_url_builder.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/immich_sliver_app_bar.dart';
|
||||||
import 'package:immich_mobile/widgets/common/user_avatar.dart';
|
import 'package:immich_mobile/widgets/common/user_avatar.dart';
|
||||||
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
||||||
@ -305,8 +305,7 @@ class _LocalAlbumsCollectionCard extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
// TODO: Migrate to the drift after local album page
|
final albums = ref.watch(localAlbumProvider);
|
||||||
final albums = ref.watch(localAlbumsProvider);
|
|
||||||
|
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
@ -315,9 +314,7 @@ class _LocalAlbumsCollectionCard extends ConsumerWidget {
|
|||||||
final size = context.width * widthFactor - 20.0;
|
final size = context.width * widthFactor - 20.0;
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => context.pushRoute(
|
onTap: () => context.pushRoute(const DriftLocalAlbumsRoute()),
|
||||||
const LocalAlbumsRoute(),
|
|
||||||
),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@ -342,12 +339,29 @@ class _LocalAlbumsCollectionCard extends ConsumerWidget {
|
|||||||
crossAxisSpacing: 8,
|
crossAxisSpacing: 8,
|
||||||
mainAxisSpacing: 8,
|
mainAxisSpacing: 8,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
children: albums.take(4).map((album) {
|
children: albums.when(
|
||||||
return AlbumThumbnailCard(
|
data: (data) {
|
||||||
album: album,
|
return data.take(4).map((album) {
|
||||||
showTitle: false,
|
return LocalAlbumThumbnail(
|
||||||
);
|
albumId: album.id,
|
||||||
}).toList(),
|
);
|
||||||
|
}).toList();
|
||||||
|
},
|
||||||
|
error: (error, _) {
|
||||||
|
return [
|
||||||
|
Center(
|
||||||
|
child: Text('Error: $error'),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
loading: () {
|
||||||
|
return [
|
||||||
|
const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,7 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
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/domain/services/remote_album.service.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/remote_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)),
|
(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>(
|
final remoteAlbumRepository = Provider<DriftRemoteAlbumRepository>(
|
||||||
(ref) => DriftRemoteAlbumRepository(ref.watch(driftProvider)),
|
(ref) => DriftRemoteAlbumRepository(ref.watch(driftProvider)),
|
||||||
);
|
);
|
||||||
|
@ -69,6 +69,7 @@ import 'package:immich_mobile/pages/search/search.page.dart';
|
|||||||
import 'package:immich_mobile/pages/share_intent/share_intent.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_favorite.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/dev/drift_partner_detail.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_recently_taken.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/dev/drift_video.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_trash.page.dart';
|
||||||
@ -438,6 +439,10 @@ class AppRouter extends RootStackRouter {
|
|||||||
page: DriftRecentlyTakenRoute.page,
|
page: DriftRecentlyTakenRoute.page,
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
),
|
),
|
||||||
|
AutoRoute(
|
||||||
|
page: DriftLocalAlbumsRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
),
|
||||||
// required to handle all deeplinks in deep_link.service.dart
|
// required to handle all deeplinks in deep_link.service.dart
|
||||||
// auto_route_library#1722
|
// auto_route_library#1722
|
||||||
RedirectRoute(path: '*', redirectTo: '/'),
|
RedirectRoute(path: '*', redirectTo: '/'),
|
||||||
|
@ -715,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
|
/// generated route for
|
||||||
/// [DriftLockedFolderPage]
|
/// [DriftLockedFolderPage]
|
||||||
class DriftLockedFolderRoute extends PageRouteInfo<void> {
|
class DriftLockedFolderRoute extends PageRouteInfo<void> {
|
||||||
|
25
mobile/lib/widgets/common/local_album_sliver_app_bar.dart
Normal file
25
mobile/lib/widgets/common/local_album_sliver_app_bar.dart
Normal file
@ -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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user