1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-03-01 15:11:21 +01:00

feat(mobile): Responsive list and grid view of backup album selection and fixes search filter (#1895)

* rebuilding gridview

* adds listview, gridview and responsive display to backup album selection

* aligned selection info title and chips to the left

* fixed search

* style: album tile

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
martyfuhry 2023-02-28 22:10:53 -05:00 committed by GitHub
parent 12217bde8a
commit 9d57039274
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 442 additions and 221 deletions

View file

@ -137,6 +137,7 @@ class AlbumInfoCard extends HookConsumerWidget {
} }
}, },
child: Card( child: Card(
clipBehavior: Clip.hardEdge,
margin: const EdgeInsets.all(1), margin: const EdgeInsets.all(1),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), // if you need this borderRadius: BorderRadius.circular(12), // if you need this
@ -150,20 +151,17 @@ class AlbumInfoCard extends HookConsumerWidget {
elevation: 0, elevation: 0,
borderOnForeground: false, borderOnForeground: false,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Stack( Expanded(
child: Stack(
clipBehavior: Clip.hardEdge,
children: [ children: [
Container( ColorFiltered(
width: 200,
height: 200,
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
),
image: DecorationImage(
colorFilter: buildImageFilter(), colorFilter: buildImageFilter(),
child: Image(
width: double.infinity,
height: double.infinity,
image: imageData != null image: imageData != null
? MemoryImage(imageData!) ? MemoryImage(imageData!)
: const AssetImage( : const AssetImage(
@ -172,26 +170,25 @@ class AlbumInfoCard extends HookConsumerWidget {
fit: BoxFit.cover, fit: BoxFit.cover,
), ),
), ),
child: null,
),
Positioned( Positioned(
bottom: 10, bottom: 10,
left: 25, right: 25,
child: buildSelectedTextBox(), child: buildSelectedTextBox(),
) )
], ],
), ),
),
Padding( Padding(
padding: const EdgeInsets.only(top: 8.0), padding: const EdgeInsets.only(
left: 25,
),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
SizedBox( Expanded(
width: 140,
child: Padding(
padding: const EdgeInsets.only(left: 25.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Text(
albumInfo.name, albumInfo.name,
@ -225,7 +222,6 @@ class AlbumInfoCard extends HookConsumerWidget {
], ],
), ),
), ),
),
IconButton( IconButton(
onPressed: () { onPressed: () {
AutoRouter.of(context).push( AutoRouter.of(context).push(

View file

@ -0,0 +1,176 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
class AlbumInfoListTile extends HookConsumerWidget {
final Uint8List? imageData;
final AvailableAlbum albumInfo;
const AlbumInfoListTile({Key? key, this.imageData, required this.albumInfo})
: super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final bool isSelected =
ref.watch(backupProvider).selectedBackupAlbums.contains(albumInfo);
final bool isExcluded =
ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo);
ColorFilter selectedFilter = ColorFilter.mode(
Theme.of(context).primaryColor.withAlpha(100),
BlendMode.darken,
);
ColorFilter excludedFilter =
ColorFilter.mode(Colors.red.withAlpha(75), BlendMode.darken);
ColorFilter unselectedFilter =
const ColorFilter.mode(Colors.black, BlendMode.color);
var isDarkTheme = Theme.of(context).brightness == Brightness.dark;
var assetCount = useState(0);
useEffect(
() {
albumInfo.assetCount.then((value) => assetCount.value = value);
return null;
},
[],
);
buildImageFilter() {
if (isSelected) {
return selectedFilter;
} else if (isExcluded) {
return excludedFilter;
} else {
return unselectedFilter;
}
}
buildTileColor() {
if (isSelected) {
return isDarkTheme
? Theme.of(context).primaryColor.withAlpha(100)
: Theme.of(context).primaryColor.withAlpha(25);
} else if (isExcluded) {
return isDarkTheme
? Colors.red[300]?.withAlpha(150)
: Colors.red[100]?.withAlpha(150);
} else {
return Colors.transparent;
}
}
return GestureDetector(
onDoubleTap: () {
HapticFeedback.selectionClick();
if (isExcluded) {
// Remove from exclude album list
ref
.watch(backupProvider.notifier)
.removeExcludedAlbumForBackup(albumInfo);
} else {
// Add to exclude album list
if (ref.watch(backupProvider).selectedBackupAlbums.length == 1 &&
ref
.watch(backupProvider)
.selectedBackupAlbums
.contains(albumInfo)) {
ImmichToast.show(
context: context,
msg: "backup_err_only_album".tr(),
toastType: ToastType.error,
gravity: ToastGravity.BOTTOM,
);
return;
}
if (albumInfo.id == 'isAll' || albumInfo.name == 'Recents') {
ImmichToast.show(
context: context,
msg: 'Cannot exclude album contains all assets',
toastType: ToastType.error,
gravity: ToastGravity.BOTTOM,
);
return;
}
ref
.watch(backupProvider.notifier)
.addExcludedAlbumForBackup(albumInfo);
}
},
child: ListTile(
tileColor: buildTileColor(),
contentPadding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
onTap: () {
HapticFeedback.selectionClick();
if (isSelected) {
if (ref.watch(backupProvider).selectedBackupAlbums.length == 1) {
ImmichToast.show(
context: context,
msg: "backup_err_only_album".tr(),
toastType: ToastType.error,
gravity: ToastGravity.BOTTOM,
);
return;
}
ref.watch(backupProvider.notifier).removeAlbumForBackup(albumInfo);
} else {
ref.watch(backupProvider.notifier).addAlbumForBackup(albumInfo);
}
},
leading: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: SizedBox(
height: 80,
width: 80,
child: ColorFiltered(
colorFilter: buildImageFilter(),
child: Image(
width: double.infinity,
height: double.infinity,
image: imageData != null
? MemoryImage(imageData!)
: const AssetImage(
'assets/immich-logo-no-outline.png',
) as ImageProvider,
fit: BoxFit.cover,
),
),
),
),
title: Text(
albumInfo.name,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
subtitle: Text(assetCount.value.toString()),
trailing: IconButton(
onPressed: () {
AutoRouter.of(context).push(
AlbumPreviewRoute(album: albumInfo.albumEntity),
);
},
icon: Icon(
Icons.image_outlined,
color: Theme.of(context).primaryColor,
size: 24,
),
splashRadius: 25,
),
),
);
}
}

View file

@ -7,6 +7,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/immich_colors.dart'; import 'package:immich_mobile/constants/immich_colors.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
import 'package:immich_mobile/modules/backup/ui/album_info_card.dart'; import 'package:immich_mobile/modules/backup/ui/album_info_card.dart';
import 'package:immich_mobile/modules/backup/ui/album_info_list_tile.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart'; import 'package:immich_mobile/shared/ui/immich_toast.dart';
@ -18,7 +19,12 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums; final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums;
final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums; final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums;
final isDarkTheme = Theme.of(context).brightness == Brightness.dark; final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
final albums = ref.watch(backupProvider).availableAlbums; final allAlbums = ref.watch(backupProvider).availableAlbums;
// Albums which are displayed to the user
// by filtering out based on search
final filteredAlbums = useState(allAlbums);
final albums = filteredAlbums.value;
useEffect( useEffect(
() { () {
@ -30,27 +36,53 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
buildAlbumSelectionList() { buildAlbumSelectionList() {
if (albums.isEmpty) { if (albums.isEmpty) {
return const Center( return const SliverToBoxAdapter(
child: Center(
child: ImmichLoadingIndicator(), child: ImmichLoadingIndicator(),
),
); );
} }
return SizedBox( return SliverPadding(
height: 265, padding: const EdgeInsets.symmetric(vertical: 12.0),
child: ListView.builder( sliver: SliverList(
scrollDirection: Axis.horizontal, delegate: SliverChildBuilderDelegate(
itemCount: albums.length, ((context, index) {
physics: const BouncingScrollPhysics(),
itemBuilder: ((context, index) {
var thumbnailData = albums[index].thumbnailData; var thumbnailData = albums[index].thumbnailData;
return Padding( return AlbumInfoListTile(
padding: index == 0
? const EdgeInsets.only(left: 16.00)
: const EdgeInsets.all(0),
child: AlbumInfoCard(
imageData: thumbnailData, imageData: thumbnailData,
albumInfo: albums[index], albumInfo: albums[index],
);
}),
childCount: albums.length,
), ),
),
);
}
buildAlbumSelectionGrid() {
if (albums.isEmpty) {
return const SliverToBoxAdapter(
child: Center(
child: ImmichLoadingIndicator(),
),
);
}
return SliverPadding(
padding: const EdgeInsets.all(12.0),
sliver: SliverGrid.builder(
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 300,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
),
itemCount: albums.length,
itemBuilder: ((context, index) {
var thumbnailData = albums[index].thumbnailData;
return AlbumInfoCard(
imageData: thumbnailData,
albumInfo: albums[index],
); );
}), }),
), ),
@ -139,19 +171,17 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 8.0), padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 8.0),
child: TextFormField( child: TextFormField(
onChanged: (searchValue) { onChanged: (searchValue) {
var avaialbleAlbums = ref if (searchValue.isEmpty) {
.watch(backupProvider) filteredAlbums.value = allAlbums;
.availableAlbums } else {
filteredAlbums.value = allAlbums
.where( .where(
(album) => album.name (album) => album.name
.toLowerCase() .toLowerCase()
.contains(searchValue.toLowerCase()), .contains(searchValue.toLowerCase()),
) )
.toList(); .toList();
}
ref
.read(backupProvider.notifier)
.setAvailableAlbums(avaialbleAlbums);
}, },
decoration: InputDecoration( decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
@ -190,8 +220,12 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
).tr(), ).tr(),
elevation: 0, elevation: 0,
), ),
body: ListView( body: CustomScrollView(
physics: const ClampingScrollPhysics(), physics: const ClampingScrollPhysics(),
slivers: [
SliverToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@ -219,7 +253,8 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
), ),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), padding:
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
child: Card( child: Card(
margin: const EdgeInsets.all(0), margin: const EdgeInsets.all(0),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
@ -262,10 +297,17 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
title: Text( title: Text(
"backup_album_selection_page_albums_device".tr( "backup_album_selection_page_albums_device".tr(
args: [ args: [
ref.watch(backupProvider).availableAlbums.length.toString() ref
.watch(backupProvider)
.availableAlbums
.length
.toString()
], ],
), ),
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14), style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
), ),
subtitle: Padding( subtitle: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0), padding: const EdgeInsets.symmetric(vertical: 8.0),
@ -323,10 +365,17 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
), ),
buildSearchBar(), buildSearchBar(),
],
Padding( ),
padding: const EdgeInsets.only(bottom: 16.0), ),
child: buildAlbumSelectionList(), SliverLayoutBuilder(
builder: (context, constraints) {
if (constraints.crossAxisExtent > 600) {
return buildAlbumSelectionGrid();
} else {
return buildAlbumSelectionList();
}
},
), ),
], ],
), ),