[mob][photos] feat: implement album selection management and action overlay components
This commit is contained in:
3
mobile/lib/events/clear_album_selections_event.dart
Normal file
3
mobile/lib/events/clear_album_selections_event.dart
Normal file
@@ -0,0 +1,3 @@
|
||||
import "package:photos/events/event.dart";
|
||||
|
||||
class ClearAlbumSelectionsEvent extends Event {}
|
||||
53
mobile/lib/models/selected_albums.dart
Normal file
53
mobile/lib/models/selected_albums.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user