[mob][photos] feat: implement album selection management and action overlay components

This commit is contained in:
Aman Raj Singh Mourya
2025-04-19 00:13:10 +05:30
parent 28f03d3514
commit 8e552c57bb
6 changed files with 359 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
import "package:photos/events/event.dart";
class ClearAlbumSelectionsEvent extends Event {}

View File

@@ -0,0 +1,53 @@
import 'package:collection/collection.dart' show IterableExtension;
import 'package:flutter/foundation.dart';
import 'package:photos/core/event_bus.dart';
import "package:photos/events/clear_album_selections_event.dart";
import 'package:photos/models/collection/collection.dart';
class SelectedAlbums extends ChangeNotifier {
final albums = <Collection>{};
void toggleSelection(Collection albumToToggle) {
final Collection? alreadySelected = albums.firstWhereOrNull(
(element) => element.id == albumToToggle.id,
);
if (alreadySelected != null) {
albums.remove(alreadySelected);
} else {
albums.add(albumToToggle);
}
if (alreadySelected != null) {
print('Album "${alreadySelected.displayName}" removed.');
} else {
print('Album "${albumToToggle.displayName}" added.');
}
notifyListeners();
}
void selectAll(Set<Collection> albumsToSelect) {
albums.addAll(albumsToSelect);
notifyListeners();
}
void unSelectAll(
Set<Collection> albumsToUnselect, {
bool skipNotify = false,
}) {
albums.removeWhere((album) => albumsToUnselect.contains(album));
if (!skipNotify) {
notifyListeners();
}
}
bool isAlbumSelected(Collection album) {
return albums.any((element) => element.id == album.id);
}
void clearAll() {
Bus.instance.fire(ClearAlbumSelectionsEvent());
albums.clear();
notifyListeners();
}
}

View File

@@ -0,0 +1,84 @@
import "package:flutter/material.dart";
import "package:photos/generated/l10n.dart";
import "package:photos/models/selected_albums.dart";
import "package:photos/theme/ente_theme.dart";
class AlbumActionBarWidget extends StatefulWidget {
final SelectedAlbums? selectedAlbums;
final VoidCallback? onCancel;
const AlbumActionBarWidget({
super.key,
this.selectedAlbums,
this.onCancel,
});
@override
State<AlbumActionBarWidget> createState() => _AlbumActionBarWidgetState();
}
class _AlbumActionBarWidgetState extends State<AlbumActionBarWidget> {
final ValueNotifier<int> _selectedAlbumNotifier = ValueNotifier(0);
@override
void initState() {
widget.selectedAlbums?.addListener(_selectedAlbumListener);
super.initState();
}
@override
void dispose() {
widget.selectedAlbums?.removeListener(_selectedAlbumListener);
_selectedAlbumNotifier.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final textTheme = getEnteTextTheme(context);
return SizedBox(
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 8, 20, 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
flex: 1,
child: ValueListenableBuilder(
valueListenable: _selectedAlbumNotifier,
builder: (context, value, child) {
return Text(
"${widget.selectedAlbums?.albums.length ?? 0} selected",
style: textTheme.miniMuted,
);
},
),
),
Flexible(
flex: 1,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
widget.onCancel?.call();
},
child: Align(
alignment: Alignment.centerRight,
child: Text(
S.of(context).cancel,
style: textTheme.mini,
),
),
),
),
),
],
),
),
);
}
void _selectedAlbumListener() {
_selectedAlbumNotifier.value = widget.selectedAlbums?.albums.length ?? 0;
}
}

View File

@@ -0,0 +1,56 @@
import "package:flutter/material.dart";
import "package:photos/core/constants.dart";
import "package:photos/models/selected_albums.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/components/bottom_action_bar/album_action_bar_widget.dart";
import "package:photos/ui/components/divider_widget.dart";
import "package:photos/ui/viewer/actions/album_selection_action_widget.dart";
class AlbumBottomActionBarWidget extends StatelessWidget {
final SelectedAlbums selectedAlbums;
final VoidCallback? onCancel;
final Color? backgroundColor;
const AlbumBottomActionBarWidget(
this.selectedAlbums, {
super.key,
this.backgroundColor,
this.onCancel,
});
@override
Widget build(BuildContext context) {
final bottomPadding = MediaQuery.paddingOf(context).bottom;
final widthOfScreen = MediaQuery.of(context).size.width;
final colorScheme = getEnteColorScheme(context);
final double leftRightPadding = widthOfScreen > restrictedMaxWidth
? (widthOfScreen - restrictedMaxWidth) / 2
: 0;
return Container(
decoration: BoxDecoration(
color: backgroundColor ?? colorScheme.backgroundElevated2,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
),
padding: EdgeInsets.only(
top: 4,
bottom: bottomPadding,
right: leftRightPadding,
left: leftRightPadding,
),
child: Column(
children: [
const SizedBox(height: 8),
AlbumSelectionActionWidget(selectedAlbums),
const DividerWidget(dividerType: DividerType.bottomBar),
AlbumActionBarWidget(
selectedAlbums: selectedAlbums,
onCancel: onCancel,
),
],
),
);
}
}

View File

@@ -0,0 +1,85 @@
import "package:flutter/material.dart";
import "package:photos/generated/l10n.dart";
import "package:photos/models/selected_albums.dart";
import "package:photos/ui/components/bottom_action_bar/selection_action_button_widget.dart";
class AlbumSelectionActionWidget extends StatefulWidget {
final SelectedAlbums selectedAlbums;
const AlbumSelectionActionWidget(
this.selectedAlbums, {
super.key,
});
@override
State<AlbumSelectionActionWidget> createState() =>
_AlbumSelectionActionWidgetState();
}
class _AlbumSelectionActionWidgetState
extends State<AlbumSelectionActionWidget> {
@override
Widget build(BuildContext context) {
if (widget.selectedAlbums.albums.isEmpty) {
return const SizedBox();
}
final List<SelectionActionButton> items = [];
items.add(
SelectionActionButton(
labelText: S.of(context).share,
icon: Icons.adaptive.share,
onTap: () {},
),
);
items.add(
SelectionActionButton(
labelText: "Pin",
icon: Icons.push_pin_rounded,
onTap: () {},
),
);
items.add(
SelectionActionButton(
labelText: S.of(context).delete,
icon: Icons.delete_outline,
onTap: () {},
),
);
items.add(
SelectionActionButton(
labelText: S.of(context).hide,
icon: Icons.visibility_off_outlined,
onTap: () {},
),
);
final scrollController = ScrollController();
return MediaQuery(
data: MediaQuery.of(context).removePadding(removeBottom: true),
child: SafeArea(
child: Scrollbar(
radius: const Radius.circular(1),
thickness: 2,
controller: scrollController,
thumbVisibility: true,
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(
decelerationRate: ScrollDecelerationRate.fast,
),
scrollDirection: Axis.horizontal,
child: Container(
padding: const EdgeInsets.only(bottom: 24),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(width: 4),
...items,
const SizedBox(width: 4),
],
),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
import "package:photos/models/selected_albums.dart";
import "package:photos/theme/effects.dart";
import "package:photos/ui/components/bottom_action_bar/album_bottom_action_bar_widget.dart";
class AlbumSelectionOverlayBar extends StatefulWidget {
final VoidCallback? onClose;
final SelectedAlbums selectedAlbum;
final Color? backgroundColor;
const AlbumSelectionOverlayBar(
this.selectedAlbum, {
super.key,
this.onClose,
this.backgroundColor,
});
@override
State<AlbumSelectionOverlayBar> createState() =>
_AlbumSelectionOverlayBarState();
}
class _AlbumSelectionOverlayBarState extends State<AlbumSelectionOverlayBar> {
final ValueNotifier<bool> _hasSelectedAlbumsNotifier = ValueNotifier(false);
@override
void initState() {
super.initState();
widget.selectedAlbum.addListener(_selectedAlbumsListener);
}
@override
void dispose() {
widget.selectedAlbum.removeListener(_selectedAlbumsListener);
super.dispose();
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: _hasSelectedAlbumsNotifier,
builder: (context, value, child) {
return AnimatedCrossFade(
firstCurve: Curves.easeInOutExpo,
secondCurve: Curves.easeInOutExpo,
sizeCurve: Curves.easeInOutExpo,
crossFadeState: _hasSelectedAlbumsNotifier.value
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
duration: const Duration(milliseconds: 400),
firstChild: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Container(
decoration: BoxDecoration(boxShadow: shadowFloatFaintLight),
child: AlbumBottomActionBarWidget(
widget.selectedAlbum,
onCancel: () {
if (widget.selectedAlbum.albums.isNotEmpty) {
widget.selectedAlbum.clearAll();
}
},
backgroundColor: widget.backgroundColor,
),
),
],
),
secondChild: const SizedBox(width: double.infinity),
);
},
);
}
_selectedAlbumsListener() {
_hasSelectedAlbumsNotifier.value = widget.selectedAlbum.albums.isNotEmpty;
}
}