1
0
mirror of https://github.com/immich-app/immich.git synced 2024-11-28 09:33:27 +02: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(
children: [ child: Stack(
Container( clipBehavior: Clip.hardEdge,
width: 200, children: [
height: 200, ColorFiltered(
decoration: BoxDecoration( colorFilter: buildImageFilter(),
borderRadius: const BorderRadius.only( child: Image(
topLeft: Radius.circular(12), width: double.infinity,
topRight: Radius.circular(12), height: double.infinity,
),
image: DecorationImage(
colorFilter: buildImageFilter(),
image: imageData != null image: imageData != null
? MemoryImage(imageData!) ? MemoryImage(imageData!)
: const AssetImage( : const AssetImage(
@ -172,58 +170,56 @@ class AlbumInfoCard extends HookConsumerWidget {
fit: BoxFit.cover, fit: BoxFit.cover,
), ),
), ),
child: null, Positioned(
), bottom: 10,
Positioned( right: 25,
bottom: 10, child: buildSelectedTextBox(),
left: 25, )
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: Column(
child: Padding( crossAxisAlignment: CrossAxisAlignment.start,
padding: const EdgeInsets.only(left: 25.0), mainAxisAlignment: MainAxisAlignment.center,
child: Column( children: [
crossAxisAlignment: CrossAxisAlignment.start, Text(
children: [ albumInfo.name,
Text( style: TextStyle(
albumInfo.name, fontSize: 14,
style: TextStyle( color: Theme.of(context).primaryColor,
fontSize: 14, fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
),
), ),
Padding( ),
padding: const EdgeInsets.only(top: 2.0), Padding(
child: FutureBuilder( padding: const EdgeInsets.only(top: 2.0),
builder: ((context, snapshot) { child: FutureBuilder(
if (snapshot.hasData) { builder: ((context, snapshot) {
return Text( if (snapshot.hasData) {
snapshot.data.toString() + return Text(
(albumInfo.isAll snapshot.data.toString() +
? " (${'backup_all'.tr()})" (albumInfo.isAll
: ""), ? " (${'backup_all'.tr()})"
style: TextStyle( : ""),
fontSize: 12, style: TextStyle(
color: Colors.grey[600], fontSize: 12,
), color: Colors.grey[600],
); ),
} );
return const Text("0"); }
}), return const Text("0");
future: albumInfo.assetCount, }),
), future: albumInfo.assetCount,
) ),
], )
), ],
), ),
), ),
IconButton( IconButton(

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: ImmichLoadingIndicator(), child: Center(
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(), var thumbnailData = albums[index].thumbnailData;
itemBuilder: ((context, index) { return AlbumInfoListTile(
var thumbnailData = albums[index].thumbnailData;
return Padding(
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 {
.where( filteredAlbums.value = allAlbums
(album) => album.name .where(
.toLowerCase() (album) => album.name
.contains(searchValue.toLowerCase()), .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,143 +220,162 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
).tr(), ).tr(),
elevation: 0, elevation: 0,
), ),
body: ListView( body: CustomScrollView(
physics: const ClampingScrollPhysics(), physics: const ClampingScrollPhysics(),
children: [ slivers: [
Padding( SliverToBoxAdapter(
padding: const EdgeInsets.symmetric( child: Column(
vertical: 8.0, crossAxisAlignment: CrossAxisAlignment.start,
horizontal: 16.0,
),
child: const Text(
"backup_album_selection_page_selection_info",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
).tr(),
),
// Selected Album Chips
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Wrap(
children: [ children: [
...buildSelectedAlbumNameChip(), Padding(
...buildExcludedAlbumNameChip() padding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 16.0,
),
child: const Text(
"backup_album_selection_page_selection_info",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
).tr(),
),
// Selected Album Chips
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Wrap(
children: [
...buildSelectedAlbumNameChip(),
...buildExcludedAlbumNameChip()
],
),
),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
child: Card(
margin: const EdgeInsets.all(0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
side: BorderSide(
color: isDarkTheme
? const Color.fromARGB(255, 0, 0, 0)
: const Color.fromARGB(255, 235, 235, 235),
width: 1,
),
),
elevation: 0,
borderOnForeground: false,
child: Column(
children: [
ListTile(
visualDensity: VisualDensity.compact,
title: const Text(
"backup_album_selection_page_total_assets",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
).tr(),
trailing: Text(
ref
.watch(backupProvider)
.allUniqueAssets
.length
.toString(),
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
],
),
),
),
ListTile(
title: Text(
"backup_album_selection_page_albums_device".tr(
args: [
ref
.watch(backupProvider)
.availableAlbums
.length
.toString()
],
),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
subtitle: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Text(
"backup_album_selection_page_albums_tap",
style: TextStyle(
fontSize: 12,
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
),
).tr(),
),
trailing: IconButton(
splashRadius: 16,
icon: Icon(
Icons.info,
size: 20,
color: Theme.of(context).primaryColor,
),
onPressed: () {
// show the dialog
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
elevation: 5,
title: Text(
'backup_album_selection_page_selection_info',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor,
),
).tr(),
content: SingleChildScrollView(
child: ListBody(
children: [
const Text(
'backup_album_selection_page_assets_scatter',
style: TextStyle(
fontSize: 14,
),
).tr(),
],
),
),
);
},
);
},
),
),
buildSearchBar(),
], ],
), ),
), ),
SliverLayoutBuilder(
Padding( builder: (context, constraints) {
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), if (constraints.crossAxisExtent > 600) {
child: Card( return buildAlbumSelectionGrid();
margin: const EdgeInsets.all(0), } else {
shape: RoundedRectangleBorder( return buildAlbumSelectionList();
borderRadius: BorderRadius.circular(10), }
side: BorderSide( },
color: isDarkTheme
? const Color.fromARGB(255, 0, 0, 0)
: const Color.fromARGB(255, 235, 235, 235),
width: 1,
),
),
elevation: 0,
borderOnForeground: false,
child: Column(
children: [
ListTile(
visualDensity: VisualDensity.compact,
title: const Text(
"backup_album_selection_page_total_assets",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
).tr(),
trailing: Text(
ref
.watch(backupProvider)
.allUniqueAssets
.length
.toString(),
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
],
),
),
),
ListTile(
title: Text(
"backup_album_selection_page_albums_device".tr(
args: [
ref.watch(backupProvider).availableAlbums.length.toString()
],
),
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
),
subtitle: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Text(
"backup_album_selection_page_albums_tap",
style: TextStyle(
fontSize: 12,
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
),
).tr(),
),
trailing: IconButton(
splashRadius: 16,
icon: Icon(
Icons.info,
size: 20,
color: Theme.of(context).primaryColor,
),
onPressed: () {
// show the dialog
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
elevation: 5,
title: Text(
'backup_album_selection_page_selection_info',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor,
),
).tr(),
content: SingleChildScrollView(
child: ListBody(
children: [
const Text(
'backup_album_selection_page_assets_scatter',
style: TextStyle(
fontSize: 14,
),
).tr(),
],
),
),
);
},
);
},
),
),
buildSearchBar(),
Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: buildAlbumSelectionList(),
), ),
], ],
), ),