Merge branch 'album_UI_revamp' of https://github.com/ente-io/auth into album_UI_revamp

This commit is contained in:
Neeraj Gupta
2025-04-25 16:14:57 +05:30
19 changed files with 490 additions and 282 deletions

View File

@@ -390,6 +390,7 @@ class CollectionsService {
Future<SharedCollections> getSharedCollections() async {
final AlbumSortKey sortKey = localSettings.albumSortKey();
final AlbumSortDirection sortDirection = localSettings.albumSortDirection();
final List<Collection> outgoing = [];
final List<Collection> incoming = [];
final List<Collection> quickLinks = [];
@@ -406,9 +407,6 @@ class CollectionsService {
incoming.add(c);
}
}
incoming.sort((first, second) {
return second.updationTime.compareTo(first.updationTime);
});
late Map<int, int> collectionIDToNewestPhotoTime;
if (sortKey == AlbumSortKey.newestPhoto) {
@@ -418,37 +416,47 @@ class CollectionsService {
incoming.sort(
(first, second) {
int comparison;
if (sortKey == AlbumSortKey.albumName) {
return compareAsciiLowerCaseNatural(
comparison = compareAsciiLowerCaseNatural(
first.displayName,
second.displayName,
);
} else if (sortKey == AlbumSortKey.newestPhoto) {
return (collectionIDToNewestPhotoTime[second.id] ?? -1 * intMaxValue)
.compareTo(
comparison =
(collectionIDToNewestPhotoTime[second.id] ?? -1 * intMaxValue)
.compareTo(
collectionIDToNewestPhotoTime[first.id] ?? -1 * intMaxValue,
);
} else {
return second.updationTime.compareTo(first.updationTime);
comparison = second.updationTime.compareTo(first.updationTime);
}
return sortDirection == AlbumSortDirection.ascending
? comparison
: -comparison;
},
);
outgoing.sort(
(first, second) {
int comparison;
if (sortKey == AlbumSortKey.albumName) {
return compareAsciiLowerCaseNatural(
comparison = compareAsciiLowerCaseNatural(
first.displayName,
second.displayName,
);
} else if (sortKey == AlbumSortKey.newestPhoto) {
return (collectionIDToNewestPhotoTime[second.id] ?? -1 * intMaxValue)
.compareTo(
comparison =
(collectionIDToNewestPhotoTime[second.id] ?? -1 * intMaxValue)
.compareTo(
collectionIDToNewestPhotoTime[first.id] ?? -1 * intMaxValue,
);
} else {
return second.updationTime.compareTo(first.updationTime);
comparison = second.updationTime.compareTo(first.updationTime);
}
return sortDirection == AlbumSortDirection.ascending
? comparison
: -comparison;
},
);
@@ -457,6 +465,7 @@ class CollectionsService {
Future<List<Collection>> getCollectionForOnEnteSection() async {
final AlbumSortKey sortKey = localSettings.albumSortKey();
final AlbumSortDirection sortDirection = localSettings.albumSortDirection();
final List<Collection> collections =
CollectionsService.instance.getCollectionsForUI();
final bool hasFavorites = FavoritesService.instance.hasFavorites();
@@ -467,19 +476,24 @@ class CollectionsService {
}
collections.sort(
(first, second) {
int comparison;
if (sortKey == AlbumSortKey.albumName) {
return compareAsciiLowerCaseNatural(
comparison = compareAsciiLowerCaseNatural(
first.displayName,
second.displayName,
);
} else if (sortKey == AlbumSortKey.newestPhoto) {
return (collectionIDToNewestPhotoTime[second.id] ?? -1 * intMaxValue)
.compareTo(
comparison =
(collectionIDToNewestPhotoTime[second.id] ?? -1 * intMaxValue)
.compareTo(
collectionIDToNewestPhotoTime[first.id] ?? -1 * intMaxValue,
);
} else {
return second.updationTime.compareTo(first.updationTime);
comparison = second.updationTime.compareTo(first.updationTime);
}
return sortDirection == AlbumSortDirection.ascending
? comparison
: -comparison;
},
);
final List<Collection> favorites = [];

View File

@@ -27,10 +27,8 @@ import 'package:photos/ui/components/dialog_widget.dart';
import 'package:photos/ui/components/models/button_type.dart';
import 'package:photos/ui/notification/toast.dart';
import 'package:photos/ui/payment/subscription.dart';
import "package:photos/ui/sharing/add_participant_page.dart";
import 'package:photos/utils/dialog_util.dart';
import 'package:photos/utils/email_util.dart';
import "package:photos/utils/navigation_util.dart";
import 'package:photos/utils/share_util.dart';
import 'package:photos/utils/standalone/date_time.dart';
import "package:styled_text/styled_text.dart";
@@ -335,72 +333,6 @@ class CollectionActions {
}
}
Future<bool> shareMultipleCollectionSheet(
BuildContext bContext,
List<Collection> collection,
) async {
final textTheme = getEnteTextTheme(bContext);
final actionResult = await showActionSheet(
context: bContext,
buttons: [
ButtonWidget(
labelText: S.of(bContext).addViewer,
buttonType: ButtonType.primary,
buttonSize: ButtonSize.large,
buttonAction: ButtonAction.first,
shouldStickToDarkTheme: true,
isInAlert: true,
onTap: () async {
await routeToPage(
bContext,
AddParticipantPage(collection[0], true),
);
},
),
ButtonWidget(
labelText: S.of(bContext).addCollaborator,
buttonType: ButtonType.neutral,
buttonSize: ButtonSize.large,
buttonAction: ButtonAction.second,
shouldStickToDarkTheme: true,
isInAlert: true,
onTap: () async {
await routeToPage(
bContext,
AddParticipantPage(collection[0], false),
);
},
),
],
bodyWidget: StyledText(
text:
"<bold>Share ${collection.length} ${collection.length == 1 ? 'album' : 'albums'}</bold>",
style: textTheme.body.copyWith(color: textMutedDark),
tags: {
'bold': StyledTextTag(
style: textTheme.body.copyWith(color: textBaseDark),
),
},
),
actionSheetType: ActionSheetType.defaultActionSheet,
);
if (actionResult?.action != null &&
actionResult!.action == ButtonAction.error) {
await showGenericErrorDialog(
context: bContext,
error: actionResult.exception,
);
return false;
}
if ((actionResult?.action != null) &&
(actionResult!.action == ButtonAction.first ||
actionResult.action == ButtonAction.second)) {
return true;
}
return false;
}
Future<bool> deleteMultipleCollectionSheet(
BuildContext bContext,
List<Collection> collections,

View File

@@ -30,7 +30,6 @@ class AlbumListItemWidget extends StatelessWidget {
final textTheme = getEnteTextTheme(context);
final colorScheme = getEnteColorScheme(context);
const sideOfThumbnail = 60.0;
final bool isFavCollection = collection.type == CollectionType.favorites;
return GestureDetector(
onTap: () {
@@ -54,7 +53,7 @@ class AlbumListItemWidget extends StatelessWidget {
duration: const Duration(milliseconds: 200),
decoration: BoxDecoration(
border: Border.all(
color: isSelected & !isFavCollection
color: isSelected
? colorScheme.strokeMuted
: colorScheme.strokeFainter,
),
@@ -153,17 +152,18 @@ class AlbumListItemWidget extends StatelessWidget {
duration: const Duration(milliseconds: 200),
switchInCurve: Curves.easeOut,
switchOutCurve: Curves.easeIn,
child: isSelected & !isFavCollection
child: isSelected
? IconButtonWidget(
key: const ValueKey("selected"),
icon: Icons.check_circle_rounded,
iconButtonType: IconButtonType.secondary,
iconColor: colorScheme.blurStrokeBase,
)
: const IconButtonWidget(
key: ValueKey("unselected"),
: IconButtonWidget(
key: const ValueKey("unselected"),
icon: Icons.chevron_right_outlined,
iconButtonType: IconButtonType.secondary,
iconColor: colorScheme.blurStrokePressed,
),
);
},

View File

@@ -73,13 +73,13 @@ class NewAlbumRowItemWidget extends StatelessWidget {
dashPattern: const [3.75, 3.75],
radius: const Radius.circular(2.35),
padding: EdgeInsets.zero,
color: enteColorScheme.strokeFaint,
color: enteColorScheme.blurStrokePressed,
child: SizedBox(
height: height,
width: width,
child: Icon(
Icons.add,
color: enteColorScheme.strokeFaint,
color: enteColorScheme.blurStrokePressed,
),
),
),

View File

@@ -8,7 +8,6 @@ import "package:photos/models/selected_albums.dart";
import "package:photos/services/collections_service.dart";
import "package:photos/theme/colors.dart";
import 'package:photos/theme/ente_theme.dart';
import "package:photos/ui/components/buttons/icon_button_widget.dart";
import "package:photos/ui/sharing/album_share_info_widget.dart";
import "package:photos/ui/sharing/user_avator_widget.dart";
import 'package:photos/ui/viewer/file/no_thumbnail_widget.dart';
@@ -40,7 +39,6 @@ class AlbumRowItemWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colorScheme = getEnteColorScheme(context);
final bool isOwner = c.isOwner(Configuration.instance.getUserID()!);
final String tagPrefix = (isOwner ? "collection" : "shared_collection") +
tag +
@@ -53,7 +51,6 @@ class AlbumRowItemWidget extends StatelessWidget {
color: c.publicURLs.first.isExpired ? warning500 : strokeBaseDark,
)
: null;
final bool isFavCollection = c.type == CollectionType.favorites;
return GestureDetector(
child: Column(
@@ -81,20 +78,33 @@ class AlbumRowItemWidget extends StatelessWidget {
CollectionsService.instance.getCoverCache(c);
}
if (thumbnail != null) {
final bool isSelected =
selectedAlbums?.isAlbumSelected(c) ?? false;
final String heroTag = tagPrefix + thumbnail.tag;
final thumbnailWidget = ThumbnailWidget(
thumbnail,
shouldShowArchiveStatus: isOwner
? c.isArchived()
: c.hasShareeArchived(),
showFavForAlbumOnly: true,
shouldShowSyncStatus: false,
shouldShowPinIcon: isOwner && c.isPinned,
key: Key(heroTag),
);
return Hero(
tag: heroTag,
transitionOnUserGestures: true,
child: ThumbnailWidget(
thumbnail,
shouldShowArchiveStatus: isOwner
? c.isArchived()
: c.hasShareeArchived(),
showFavForAlbumOnly: true,
shouldShowSyncStatus: false,
shouldShowPinIcon: isOwner && c.isPinned,
key: Key(heroTag),
),
child: isSelected
? ColorFiltered(
colorFilter: ColorFilter.mode(
Colors.black.withOpacity(
0.4,
),
BlendMode.darken,
),
child: thumbnailWidget,
)
: thumbnailWidget,
);
} else {
return const NoThumbnailWidget();
@@ -114,8 +124,9 @@ class AlbumRowItemWidget extends StatelessWidget {
),
),
),
Align(
alignment: Alignment.topRight,
Positioned(
top: 6,
right: 6,
child: Hero(
tag: tagPrefix + "_album_selection",
transitionOnUserGestures: true,
@@ -128,13 +139,11 @@ class AlbumRowItemWidget extends StatelessWidget {
duration: const Duration(milliseconds: 200),
switchInCurve: Curves.easeOut,
switchOutCurve: Curves.easeIn,
child: isSelected && !isFavCollection
? IconButtonWidget(
key: const ValueKey("selected"),
icon: Icons.check_circle_rounded,
iconButtonType:
IconButtonType.secondary,
iconColor: colorScheme.blurStrokeBase,
child: isSelected
? const Icon(
Icons.check_circle_rounded,
color: Colors.white,
size: 22,
)
: null,
);

View File

@@ -10,6 +10,7 @@ import 'package:photos/models/collection/collection_items.dart';
import "package:photos/models/selected_albums.dart";
import "package:photos/service_locator.dart";
import "package:photos/services/collections_service.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/collections/flex_grid_view.dart";
import "package:photos/ui/components/buttons/icon_button_widget.dart";
import "package:photos/ui/components/searchable_appbar.dart";
@@ -48,6 +49,7 @@ class _CollectionListPageState extends State<CollectionListPage> {
List<Collection>? collections;
AlbumSortKey? sortKey;
AlbumViewType? albumViewType;
AlbumSortDirection? albumSortDirection;
String _searchQuery = "";
final _selectedAlbum = SelectedAlbums();
@@ -61,6 +63,7 @@ class _CollectionListPageState extends State<CollectionListPage> {
});
sortKey = localSettings.albumSortKey();
albumViewType = localSettings.albumViewType();
albumSortDirection = localSettings.albumSortDirection();
}
@override
@@ -104,7 +107,6 @@ class _CollectionListPageState extends State<CollectionListPage> {
refreshCollections();
},
actions: [
const SizedBox(width: 8),
_sortMenu(collections!),
],
),
@@ -114,8 +116,6 @@ class _CollectionListPageState extends State<CollectionListPage> {
tag: widget.tag,
enableSelectionMode: enableSelectionMode,
albumViewType: albumViewType ?? AlbumViewType.grid,
shouldShowCreateAlbum:
widget.sectionType == UISectionType.homeCollections,
selectedAlbums: _selectedAlbum,
),
],
@@ -132,24 +132,39 @@ class _CollectionListPageState extends State<CollectionListPage> {
}
Widget _sortMenu(List<Collection> collections) {
Text sortOptionText(AlbumSortKey key) {
final colorTheme = getEnteColorScheme(context);
Widget sortOptionText(AlbumSortKey key) {
String text = key.toString();
switch (key) {
case AlbumSortKey.albumName:
text = S.of(context).name;
break;
case AlbumSortKey.newestPhoto:
text = S.of(context).newest;
text = "Date Created";
break;
case AlbumSortKey.lastUpdated:
text = S.of(context).lastUpdated;
text = "Date Modified";
}
return Text(
text,
style: Theme.of(context).textTheme.titleMedium!.copyWith(
fontSize: 14,
color: Theme.of(context).iconTheme.color!.withOpacity(0.7),
),
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
text,
style: Theme.of(context).textTheme.titleMedium!.copyWith(
fontSize: 14,
color: Theme.of(context).iconTheme.color!.withOpacity(0.7),
),
),
Icon(
sortKey == key
? (albumSortDirection == AlbumSortDirection.ascending
? Icons.arrow_upward_outlined
: Icons.arrow_downward_outlined)
: null,
color: Theme.of(context).iconTheme.color!.withOpacity(0.7),
),
],
);
}
@@ -165,6 +180,7 @@ class _CollectionListPageState extends State<CollectionListPage> {
? Icons.view_list_outlined
: Icons.grid_view_outlined,
iconButtonType: IconButtonType.secondary,
iconColor: colorTheme.blurStrokePressed,
onTap: () async {
setState(() {
albumViewType = albumViewType == AlbumViewType.grid
@@ -174,7 +190,6 @@ class _CollectionListPageState extends State<CollectionListPage> {
await localSettings.setAlbumViewType(albumViewType!);
},
),
const SizedBox(width: 8),
GestureDetector(
onTapDown: (TapDownDetails details) async {
final int? selectedValue = await showMenu<int>(
@@ -195,14 +210,20 @@ class _CollectionListPageState extends State<CollectionListPage> {
if (selectedValue != null) {
sortKey = AlbumSortKey.values[selectedValue];
await localSettings.setAlbumSortKey(sortKey!);
albumSortDirection =
albumSortDirection == AlbumSortDirection.ascending
? AlbumSortDirection.descending
: AlbumSortDirection.ascending;
await localSettings.setAlbumSortDirection(albumSortDirection!);
await refreshCollections();
setState(() {});
Bus.instance.fire(AlbumSortOrderChangeEvent());
}
},
child: const IconButtonWidget(
child: IconButtonWidget(
icon: Icons.sort_outlined,
iconButtonType: IconButtonType.secondary,
iconColor: colorTheme.blurStrokePressed,
),
),
],

View File

@@ -88,9 +88,6 @@ class _CollectionsFlexiGridViewWidgetState
Future<void> _toggleAlbumSelection(Collection c) async {
await HapticFeedback.lightImpact();
if (c.type == CollectionType.favorites) {
return;
}
widget.selectedAlbums!.toggleSelection(c);
setState(() {
isAnyAlbumSelected = widget.selectedAlbums!.albums.isNotEmpty;
@@ -136,8 +133,7 @@ class _CollectionsFlexiGridViewWidgetState
albumsCountInOneRow;
final int totalCollections = widget.collections!.length;
final bool showCreateAlbum =
widget.shouldShowCreateAlbum && !isAnyAlbumSelected;
final bool showCreateAlbum = widget.shouldShowCreateAlbum;
final int totalItemCount = totalCollections + (showCreateAlbum ? 1 : 0);
final int displayItemCount = min(totalItemCount, widget.displayLimitCount);
@@ -160,6 +156,7 @@ class _CollectionsFlexiGridViewWidgetState
sideOfThumbnail,
tag: widget.tag,
selectedAlbums: widget.selectedAlbums,
showFileCount: false,
onTapCallback: (c) {
isAnyAlbumSelected
? _toggleAlbumSelection(c)

View File

@@ -85,6 +85,7 @@ class _SearchableAppBarState extends State<SearchableAppBar> {
icon: Icons.search,
iconButtonType: IconButtonType.secondary,
onTap: _activateSearch,
iconColor: getEnteColorScheme(context).blurStrokePressed,
),
...?widget.actions,
],

View File

@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:photos/core/event_bus.dart';
import 'package:photos/events/tab_changed_event.dart';
import "package:photos/models/selected_albums.dart";
import 'package:photos/models/selected_files.dart';
import "package:photos/theme/colors.dart";
import 'package:photos/theme/ente_theme.dart';
@@ -10,12 +11,14 @@ import 'package:photos/ui/tabs/nav_bar.dart';
class HomeBottomNavigationBar extends StatefulWidget {
const HomeBottomNavigationBar(
this.selectedFiles, {
this.selectedFiles,
this.selectedAlbums, {
required this.selectedTabIndex,
super.key,
});
final SelectedFiles selectedFiles;
final SelectedAlbums selectedAlbums;
final int selectedTabIndex;
@override
@@ -32,6 +35,7 @@ class _HomeBottomNavigationBarState extends State<HomeBottomNavigationBar> {
super.initState();
currentTabIndex = widget.selectedTabIndex;
widget.selectedFiles.addListener(_selectedFilesListener);
widget.selectedAlbums.addListener(_selectedAlbumsListener);
_tabChangedEventSubscription =
Bus.instance.on<TabChangedEvent>().listen((event) {
if (event.source != TabChangedEventSource.tabBar) {
@@ -56,6 +60,7 @@ class _HomeBottomNavigationBarState extends State<HomeBottomNavigationBar> {
void dispose() {
_tabChangedEventSubscription.cancel();
widget.selectedFiles.removeListener(_selectedFilesListener);
widget.selectedAlbums.removeListener(_selectedAlbumsListener);
super.dispose();
}
@@ -65,6 +70,12 @@ class _HomeBottomNavigationBarState extends State<HomeBottomNavigationBar> {
}
}
void _selectedAlbumsListener() {
if (mounted) {
setState(() {});
}
}
void _onTabChange(int index, {String mode = 'tabChanged'}) {
debugPrint("_TabChanged called via method $mode");
Bus.instance.fire(
@@ -78,6 +89,7 @@ class _HomeBottomNavigationBarState extends State<HomeBottomNavigationBar> {
@override
Widget build(BuildContext context) {
final bool filesAreSelected = widget.selectedFiles.files.isNotEmpty;
final bool albumsAreSelected = widget.selectedAlbums.albums.isNotEmpty;
final enteColorScheme = getEnteColorScheme(context);
return SafeArea(
@@ -85,9 +97,9 @@ class _HomeBottomNavigationBarState extends State<HomeBottomNavigationBar> {
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
height: filesAreSelected ? 0 : 56,
height: filesAreSelected || albumsAreSelected ? 0 : 56,
child: IgnorePointer(
ignoring: filesAreSelected,
ignoring: filesAreSelected || albumsAreSelected,
child: ListView(
physics: const NeverScrollableScrollPhysics(),
children: [

View File

@@ -35,7 +35,7 @@ class AddParticipantPage extends StatefulWidget {
);
bool get isMultipleCollections =>
collections != null && collections!.length > 1;
collections != null && collections!.isNotEmpty;
@override
State<StatefulWidget> createState() => _AddParticipantPage();
@@ -84,9 +84,11 @@ class _AddParticipantPage extends State<AddParticipantPage> {
resizeToAvoidBottomInset: isKeypadOpen,
appBar: AppBar(
title: Text(
widget.isAddingViewer
? S.of(context).addViewer
: S.of(context).addCollaborator,
widget.isMultipleCollections
? "Add participants"
: widget.isAddingViewer
? S.of(context).addViewer
: S.of(context).addCollaborator,
),
),
body: Column(
@@ -228,52 +230,9 @@ class _AddParticipantPage extends State<AddParticipantPage> {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 8),
ButtonWidget(
buttonType: ButtonType.primary,
buttonSize: ButtonSize.large,
labelText: widget.isAddingViewer
? S.of(context).addViewers(_selectedEmails.length)
: S
.of(context)
.addCollaborators(_selectedEmails.length),
isDisabled: _selectedEmails.isEmpty,
onTap: () async {
final results = <bool>[];
final collections = getAllCollections();
for (String email in _selectedEmails) {
for (Collection collection in collections) {
results.add(
await collectionActions.addEmailToCollection(
context,
collection,
email,
widget.isAddingViewer
? CollectionParticipantRole.viewer
: CollectionParticipantRole.collaborator,
),
);
}
}
final noOfSuccessfullAdds =
results.where((e) => e).length;
showToast(
context,
widget.isAddingViewer
? S
.of(context)
.viewersSuccessfullyAdded(noOfSuccessfullAdds)
: S.of(context).collaboratorsSuccessfullyAdded(
noOfSuccessfullAdds,
),
);
if (!results.any((e) => e == false) && mounted) {
Navigator.of(context).pop(true);
}
},
),
widget.isMultipleCollections
? _multipleActionButton()
: _singleActionButton(),
const SizedBox(height: 12),
],
),
@@ -284,6 +243,124 @@ class _AddParticipantPage extends State<AddParticipantPage> {
);
}
Widget _singleActionButton() {
return ButtonWidget(
buttonType: ButtonType.primary,
buttonSize: ButtonSize.large,
labelText: widget.isAddingViewer
? S.of(context).addViewers(_selectedEmails.length)
: S.of(context).addCollaborators(_selectedEmails.length),
isDisabled: _selectedEmails.isEmpty,
onTap: () async {
final results = <bool>[];
final collections = getAllCollections();
for (String email in _selectedEmails) {
for (Collection collection in collections) {
results.add(
await collectionActions.addEmailToCollection(
context,
collection,
email,
widget.isAddingViewer
? CollectionParticipantRole.viewer
: CollectionParticipantRole.collaborator,
),
);
}
}
final noOfSuccessfullAdds = results.where((e) => e).length;
showToast(
context,
widget.isAddingViewer
? S.of(context).viewersSuccessfullyAdded(noOfSuccessfullAdds)
: S.of(context).collaboratorsSuccessfullyAdded(
noOfSuccessfullAdds,
),
);
if (!results.any((e) => e == false) && mounted) {
Navigator.of(context).pop(true);
}
},
);
}
Widget _multipleActionButton() {
return Column(
children: [
ButtonWidget(
buttonType: ButtonType.primary,
buttonSize: ButtonSize.large,
labelText: S.of(context).addViewers(_selectedEmails.length),
isDisabled: _selectedEmails.isEmpty,
onTap: () async {
final results = <bool>[];
final collections = getAllCollections();
for (String email in _selectedEmails) {
bool result = false;
for (Collection collection in collections) {
result = await collectionActions.addEmailToCollection(
context,
collection,
email,
CollectionParticipantRole.viewer,
);
}
results.add(result);
}
final noOfSuccessfullAdds = results.where((e) => e).length;
showToast(
context,
S.of(context).viewersSuccessfullyAdded(noOfSuccessfullAdds),
);
if (!results.any((e) => e == false) && mounted) {
Navigator.of(context).pop(true);
}
},
),
const SizedBox(height: 8),
ButtonWidget(
buttonType: ButtonType.primary,
buttonSize: ButtonSize.large,
labelText: S.of(context).addCollaborators(_selectedEmails.length),
isDisabled: _selectedEmails.isEmpty,
onTap: () async {
final results = <bool>[];
final collections = getAllCollections();
for (String email in _selectedEmails) {
bool result = false;
for (Collection collection in collections) {
result = await collectionActions.addEmailToCollection(
context,
collection,
email,
CollectionParticipantRole.collaborator,
);
}
results.add(result);
}
final noOfSuccessfullAdds = results.where((e) => e).length;
showToast(
context,
S.of(context).collaboratorsSuccessfullyAdded(noOfSuccessfullAdds),
);
if (!results.any((e) => e == false) && mounted) {
Navigator.of(context).pop(true);
}
},
),
],
);
}
void clearFocus() {
_textController.clear();
_newEmail = _textController.text;

View File

@@ -33,6 +33,7 @@ import "package:photos/l10n/l10n.dart";
import "package:photos/models/collection/collection.dart";
import 'package:photos/models/collection/collection_items.dart';
import "package:photos/models/file/file.dart";
import "package:photos/models/selected_albums.dart";
import 'package:photos/models/selected_files.dart';
import "package:photos/service_locator.dart";
import 'package:photos/services/account/user_service.dart';
@@ -84,7 +85,6 @@ class HomeWidget extends StatefulWidget {
}
class _HomeWidgetState extends State<HomeWidget> {
static const _userCollectionsTab = UserCollectionsTab();
static const _sharedCollectionTab = SharedCollectionsTab();
static const _searchTab = SearchTab();
static final _settingsPage = SettingsPage(
@@ -92,6 +92,7 @@ class _HomeWidgetState extends State<HomeWidget> {
);
final _logger = Logger("HomeWidgetState");
final _selectedAlbums = SelectedAlbums();
final _selectedFiles = SelectedFiles();
final PageController _pageController = PageController();
@@ -708,7 +709,7 @@ class _HomeWidgetState extends State<HomeWidget> {
),
selectedFiles: _selectedFiles,
),
_userCollectionsTab,
UserCollectionsTab(selectedAlbums: _selectedAlbums),
_sharedCollectionTab,
_searchTab,
],
@@ -755,6 +756,7 @@ class _HomeWidgetState extends State<HomeWidget> {
: const SizedBox.shrink(),
HomeBottomNavigationBar(
_selectedFiles,
_selectedAlbums,
selectedTabIndex: _selectedTabIndex,
),
],

View File

@@ -150,10 +150,11 @@ class QuickLinkAlbumItem extends StatelessWidget {
iconButtonType: IconButtonType.secondary,
iconColor: colorScheme.blurStrokeBase,
)
: const IconButtonWidget(
key: ValueKey("unselected"),
: IconButtonWidget(
key: const ValueKey("unselected"),
icon: Icons.chevron_right_outlined,
iconButtonType: IconButtonType.secondary,
iconColor: colorScheme.blurStrokePressed,
),
),
),

View File

@@ -3,13 +3,17 @@ import "dart:math";
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import "package:photos/core/constants.dart";
import 'package:photos/core/event_bus.dart';
import 'package:photos/events/collection_updated_event.dart';
import 'package:photos/events/local_photos_updated_event.dart';
import 'package:photos/events/user_logged_out_event.dart';
import "package:photos/generated/l10n.dart";
import 'package:photos/models/collection/collection_items.dart';
import "package:photos/models/search/generic_search_result.dart";
import 'package:photos/services/collections_service.dart';
import "package:photos/services/search_service.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/collections/album/row_item.dart";
import "package:photos/ui/collections/collection_list_page.dart";
import 'package:photos/ui/common/loading_widget.dart';
@@ -20,6 +24,7 @@ import "package:photos/ui/tabs/shared/empty_state.dart";
import "package:photos/ui/tabs/shared/quick_link_album_item.dart";
import "package:photos/ui/viewer/gallery/collect_photos_card_widget.dart";
import "package:photos/ui/viewer/gallery/collection_page.dart";
import "package:photos/ui/viewer/search_tab/contacts_section.dart";
import "package:photos/utils/navigation_util.dart";
import "package:photos/utils/standalone/debouncer.dart";
@@ -106,6 +111,7 @@ class _SharedCollectionsTabState extends State<SharedCollectionsTab>
SectionTitle(title: S.of(context).sharedWithYou);
final SectionTitle sharedByYou =
SectionTitle(title: S.of(context).sharedByYou);
final colorTheme = getEnteColorScheme(context);
return SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Container(
@@ -136,9 +142,10 @@ class _SharedCollectionsTabState extends State<SharedCollectionsTab>
: null,
Hero(tag: "incoming", child: sharedWithYou),
trailingWidget: collections.incoming.isNotEmpty
? const IconButtonWidget(
? IconButtonWidget(
icon: Icons.chevron_right,
iconButtonType: IconButtonType.secondary,
iconColor: colorTheme.blurStrokePressed,
)
: null,
),
@@ -158,6 +165,7 @@ class _SharedCollectionsTabState extends State<SharedCollectionsTab>
collections.incoming[index],
maxThumbnailWidth,
tag: "incoming",
showFileCount: false,
),
);
},
@@ -192,9 +200,10 @@ class _SharedCollectionsTabState extends State<SharedCollectionsTab>
: null,
Hero(tag: "outgoing", child: sharedByYou),
trailingWidget: collections.outgoing.isNotEmpty
? const IconButtonWidget(
? IconButtonWidget(
icon: Icons.chevron_right,
iconButtonType: IconButtonType.secondary,
iconColor: colorTheme.blurStrokePressed,
)
: null,
),
@@ -214,6 +223,7 @@ class _SharedCollectionsTabState extends State<SharedCollectionsTab>
collections.outgoing[index],
maxThumbnailWidth,
tag: "outgoing",
showFileCount: false,
),
);
},
@@ -252,9 +262,10 @@ class _SharedCollectionsTabState extends State<SharedCollectionsTab>
),
),
trailingWidget: numberOfQuickLinks > maxQuickLinks
? const IconButtonWidget(
? IconButtonWidget(
icon: Icons.chevron_right,
iconButtonType: IconButtonType.secondary,
iconColor: colorTheme.blurStrokePressed,
)
: null,
),
@@ -297,6 +308,27 @@ class _SharedCollectionsTabState extends State<SharedCollectionsTab>
),
)
: const SizedBox.shrink(),
const SizedBox(height: 2),
FutureBuilder(
future: SearchService.instance
.getAllContactsSearchResults(kSearchSectionLimit),
builder: (context, snapshot) {
if (snapshot.hasData) {
return ContactsSection(
snapshot.data as List<GenericSearchResult>,
);
} else if (snapshot.hasError) {
_logger.severe(
"failed to load contacts section",
snapshot.error,
snapshot.stackTrace,
);
return const EnteLoadingWidget();
} else {
return const EnteLoadingWidget();
}
},
),
const SizedBox(height: 4),
const CollectPhotosCardWidget(),
const SizedBox(height: 32),

View File

@@ -11,6 +11,7 @@ import 'package:photos/events/local_photos_updated_event.dart';
import 'package:photos/events/user_logged_out_event.dart';
import "package:photos/generated/l10n.dart";
import 'package:photos/models/collection/collection.dart';
import "package:photos/models/selected_albums.dart";
import 'package:photos/services/collections_service.dart';
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/collections/button/archived_button.dart";
@@ -24,13 +25,16 @@ import "package:photos/ui/collections/flex_grid_view.dart";
import 'package:photos/ui/common/loading_widget.dart';
import 'package:photos/ui/components/buttons/icon_button_widget.dart';
import "package:photos/ui/tabs/section_title.dart";
import "package:photos/ui/viewer/actions/album_selection_overlay_bar.dart";
import "package:photos/ui/viewer/actions/delete_empty_albums.dart";
import "package:photos/ui/viewer/gallery/empty_state.dart";
import "package:photos/utils/navigation_util.dart";
import "package:photos/utils/standalone/debouncer.dart";
class UserCollectionsTab extends StatefulWidget {
const UserCollectionsTab({super.key});
const UserCollectionsTab({super.key, this.selectedAlbums});
final SelectedAlbums? selectedAlbums;
@override
State<UserCollectionsTab> createState() => _UserCollectionsTabState();
@@ -55,7 +59,8 @@ class _UserCollectionsTabState extends State<UserCollectionsTab>
leading: true,
);
static const int _kOnEnteItemLimitCount = 9;
static const int _kOnEnteItemLimitCount = 12;
@override
void initState() {
super.initState();
@@ -123,95 +128,109 @@ class _UserCollectionsTabState extends State<UserCollectionsTab>
.withOpacity(0.5),
);
return CustomScrollView(
physics: const BouncingScrollPhysics(),
controller: _scrollController,
slivers: [
SliverToBoxAdapter(
child: SectionOptions(
onTap: () {
unawaited(
routeToPage(
context,
DeviceFolderVerticalGridView(
appTitle: SectionTitle(
title: S.of(context).onDevice,
return Stack(
alignment: Alignment.bottomCenter,
children: [
CustomScrollView(
physics: const BouncingScrollPhysics(),
controller: _scrollController,
slivers: [
SliverToBoxAdapter(
child: SectionOptions(
onTap: () {
unawaited(
routeToPage(
context,
DeviceFolderVerticalGridView(
appTitle: SectionTitle(
title: S.of(context).onDevice,
),
tag: "OnDeviceAppTitle",
),
),
tag: "OnDeviceAppTitle",
),
);
},
Hero(
tag: "OnDeviceAppTitle",
child: SectionTitle(title: S.of(context).onDevice),
),
);
},
Hero(
tag: "OnDeviceAppTitle",
child: SectionTitle(title: S.of(context).onDevice),
trailingWidget: IconButtonWidget(
icon: Icons.chevron_right,
iconButtonType: IconButtonType.secondary,
iconColor: getEnteColorScheme(context).blurStrokePressed,
),
),
),
trailingWidget: const IconButtonWidget(
icon: Icons.chevron_right,
iconButtonType: IconButtonType.secondary,
const SliverToBoxAdapter(child: DeviceFoldersGridView()),
SliverToBoxAdapter(
child: SectionOptions(
onTap: () {
unawaited(
routeToPage(
context,
CollectionListPage(
collections,
sectionType: UISectionType.homeCollections,
appTitle: SectionTitle(
titleWithBrand: getOnEnteSection(context),
),
initialScrollOffset: _scrollController.offset,
),
),
);
},
SectionTitle(titleWithBrand: getOnEnteSection(context)),
trailingWidget: IconButtonWidget(
icon: Icons.chevron_right,
iconButtonType: IconButtonType.secondary,
iconColor: getEnteColorScheme(context).blurStrokePressed,
),
padding: const EdgeInsets.only(left: 12, right: 6),
),
),
),
),
const SliverToBoxAdapter(child: DeviceFoldersGridView()),
SliverToBoxAdapter(
child: SectionOptions(
onTap: () {
unawaited(
routeToPage(
context,
CollectionListPage(
SliverToBoxAdapter(child: DeleteEmptyAlbums(collections)),
Configuration.instance.hasConfiguredAccount()
? CollectionsFlexiGridViewWidget(
collections,
sectionType: UISectionType.homeCollections,
appTitle: SectionTitle(
titleWithBrand: getOnEnteSection(context),
),
initialScrollOffset: _scrollController.offset,
),
displayLimitCount: _kOnEnteItemLimitCount,
selectedAlbums: widget.selectedAlbums,
shrinkWrap: true,
shouldShowCreateAlbum: true,
enableSelectionMode: true,
)
: const SliverToBoxAdapter(child: EmptyState()),
SliverToBoxAdapter(
child: Divider(
color: getEnteColorScheme(context).strokeFaint,
),
),
const SliverToBoxAdapter(child: SizedBox(height: 12)),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Column(
children: [
UnCategorizedCollections(trashAndHiddenTextStyle),
const SizedBox(height: 12),
ArchivedCollectionsButton(trashAndHiddenTextStyle),
const SizedBox(height: 12),
HiddenCollectionsButtonWidget(trashAndHiddenTextStyle),
const SizedBox(height: 12),
TrashSectionButton(trashAndHiddenTextStyle),
],
),
);
},
SectionTitle(titleWithBrand: getOnEnteSection(context)),
trailingWidget: const IconButtonWidget(
icon: Icons.chevron_right,
iconButtonType: IconButtonType.secondary,
),
),
padding: const EdgeInsets.only(left: 12, right: 6),
),
),
SliverToBoxAdapter(child: DeleteEmptyAlbums(collections)),
Configuration.instance.hasConfiguredAccount()
? CollectionsFlexiGridViewWidget(
collections,
displayLimitCount: _kOnEnteItemLimitCount,
shrinkWrap: true,
shouldShowCreateAlbum: true,
enableSelectionMode: false,
)
: const SliverToBoxAdapter(child: EmptyState()),
SliverToBoxAdapter(
child: Divider(
color: getEnteColorScheme(context).strokeFaint,
),
),
const SliverToBoxAdapter(child: SizedBox(height: 12)),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Column(
children: [
UnCategorizedCollections(trashAndHiddenTextStyle),
const SizedBox(height: 12),
ArchivedCollectionsButton(trashAndHiddenTextStyle),
const SizedBox(height: 12),
HiddenCollectionsButtonWidget(trashAndHiddenTextStyle),
const SizedBox(height: 12),
TrashSectionButton(trashAndHiddenTextStyle),
],
SliverToBoxAdapter(
child:
SizedBox(height: 64 + MediaQuery.of(context).padding.bottom),
),
),
],
),
SliverToBoxAdapter(
child: SizedBox(height: 64 + MediaQuery.of(context).padding.bottom),
AlbumSelectionOverlayBar(
widget.selectedAlbums!,
UISectionType.homeCollections,
collections,
),
],
);

View File

@@ -1,3 +1,4 @@
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
import "package:logging/logging.dart";
import "package:photos/db/files_db.dart";
@@ -12,8 +13,11 @@ import "package:photos/ui/components/action_sheet_widget.dart";
import "package:photos/ui/components/bottom_action_bar/selection_action_button_widget.dart";
import "package:photos/ui/components/buttons/button_widget.dart";
import "package:photos/ui/components/models/button_type.dart";
import "package:photos/ui/notification/toast.dart";
import "package:photos/ui/sharing/add_participant_page.dart";
import "package:photos/utils/dialog_util.dart";
import "package:photos/utils/magic_util.dart";
import "package:photos/utils/navigation_util.dart";
class AlbumSelectionActionWidget extends StatefulWidget {
final SelectedAlbums selectedAlbums;
@@ -34,19 +38,31 @@ class _AlbumSelectionActionWidgetState
extends State<AlbumSelectionActionWidget> {
final _logger = Logger("AlbumSelectionActionWidgetState");
late CollectionActions collectionActions;
bool hasFavorites = false;
@override
initState() {
collectionActions = CollectionActions(CollectionsService.instance);
widget.selectedAlbums.addListener(_selectionChangedListener);
super.initState();
}
@override
void dispose() {
widget.selectedAlbums.removeListener(_selectionChangedListener);
super.dispose();
}
@override
Widget build(BuildContext context) {
if (widget.selectedAlbums.albums.isEmpty) {
return const SizedBox();
}
final List<SelectionActionButton> items = [];
final hasPinnedAlbum =
widget.selectedAlbums.albums.any((album) => album.isPinned);
final hasUnpinnedAlbum =
widget.selectedAlbums.albums.any((album) => !album.isPinned);
if (widget.sectionType == UISectionType.homeCollections ||
widget.sectionType == UISectionType.outgoingCollections) {
@@ -62,8 +78,19 @@ class _AlbumSelectionActionWidgetState
labelText: "Pin",
icon: Icons.push_pin_rounded,
onTap: _onPinClick,
shouldShow: hasUnpinnedAlbum,
),
);
items.add(
SelectionActionButton(
labelText: "Unpin",
icon: CupertinoIcons.pin_slash,
onTap: _onUnpinClick,
shouldShow: hasPinnedAlbum,
),
);
items.add(
SelectionActionButton(
labelText: S.of(context).delete,
@@ -131,10 +158,17 @@ class _AlbumSelectionActionWidgetState
}
Future<void> _shareCollection() async {
await collectionActions.shareMultipleCollectionSheet(
await routeToPage(
context,
widget.selectedAlbums.albums.toList(),
AddParticipantPage(
widget.selectedAlbums.albums.toList().first,
false,
collections: widget.selectedAlbums.albums.toList(),
),
);
if (hasFavorites) {
_showFavToast();
}
widget.selectedAlbums.clearAll();
}
@@ -170,6 +204,9 @@ class _AlbumSelectionActionWidgetState
debugPrint("No pop");
}
}
if (hasFavorites) {
_showFavToast();
}
widget.selectedAlbums.clearAll();
}
@@ -185,6 +222,27 @@ class _AlbumSelectionActionWidgetState
collection.isPinned ? 1 : 1,
);
}
if (hasFavorites) {
_showFavToast();
}
widget.selectedAlbums.clearAll();
}
Future<void> _onUnpinClick() async {
for (final collection in widget.selectedAlbums.albums) {
if (collection.type == CollectionType.favorites || !collection.isPinned) {
continue;
}
await updateOrder(
context,
collection,
collection.isPinned ? 0 : 0,
);
}
if (hasFavorites) {
_showFavToast();
}
widget.selectedAlbums.clearAll();
}
@@ -204,6 +262,9 @@ class _AlbumSelectionActionWidgetState
prevVisibility: prevVisiblity,
);
}
if (hasFavorites) {
_showFavToast();
}
widget.selectedAlbums.clearAll();
}
@@ -240,6 +301,9 @@ class _AlbumSelectionActionWidgetState
prevVisibility: prevVisiblity,
);
}
if (hasFavorites) {
_showFavToast();
}
if (mounted) {
setState(() {});
}
@@ -290,4 +354,19 @@ class _AlbumSelectionActionWidgetState
}
}
}
void _selectionChangedListener() {
if (mounted) {
hasFavorites = widget.selectedAlbums.albums
.any((album) => album.type == CollectionType.favorites);
setState(() {});
}
}
void _showFavToast() {
showShortToast(
context,
"The Favorites album cannot be modified",
);
}
}

View File

@@ -133,11 +133,12 @@ class SearchableItemWidget extends StatelessWidget {
],
),
),
const Flexible(
Flexible(
flex: 1,
child: IconButtonWidget(
icon: Icons.chevron_right_outlined,
iconButtonType: IconButtonType.secondary,
iconColor: colorScheme.blurStrokePressed,
),
),
],

View File

@@ -16,7 +16,6 @@ import "package:photos/ui/viewer/search/result/no_result_widget.dart";
import "package:photos/ui/viewer/search/search_suggestions.dart";
import "package:photos/ui/viewer/search/tab_empty_state.dart";
import 'package:photos/ui/viewer/search_tab/albums_section.dart';
import "package:photos/ui/viewer/search_tab/contacts_section.dart";
import "package:photos/ui/viewer/search_tab/file_type_section.dart";
import "package:photos/ui/viewer/search_tab/locations_section.dart";
import "package:photos/ui/viewer/search_tab/magic_section.dart";
@@ -142,10 +141,7 @@ class _AllSearchSectionsState extends State<AllSearchSections> {
as List<GenericSearchResult>,
);
case SectionType.contacts:
return ContactsSection(
snapshot.data!.elementAt(index)
as List<GenericSearchResult>,
);
return const SizedBox.shrink();
case SectionType.fileTypesAndExtension:
return FileTypeSection(
snapshot.data!.elementAt(index)

View File

@@ -39,7 +39,7 @@ class SectionHeader extends StatelessWidget {
padding: const EdgeInsets.fromLTRB(24, 12, 12, 12),
child: Icon(
Icons.chevron_right_outlined,
color: getEnteColorScheme(context).strokeMuted,
color: getEnteColorScheme(context).blurStrokePressed,
),
),
)

View File

@@ -8,6 +8,11 @@ enum AlbumSortKey {
lastUpdated,
}
enum AlbumSortDirection {
ascending,
descending,
}
enum AlbumViewType {
grid,
list,
@@ -29,6 +34,7 @@ class LocalSettings {
static const _hideSharedItemsFromHomeGalleryTag =
"hide_shared_items_from_home_gallery";
static const kCollectionViewType = "collection_view_type";
static const kCollectionSortDirection = "collection_sort_direction";
final SharedPreferences _prefs;
@@ -51,6 +57,15 @@ class LocalSettings {
return AlbumViewType.values[index];
}
AlbumSortDirection albumSortDirection() {
return AlbumSortDirection
.values[_prefs.getInt(kCollectionSortDirection) ?? 1];
}
Future<bool> setAlbumSortDirection(AlbumSortDirection direction) {
return _prefs.setInt(kCollectionSortDirection, direction.index);
}
int getPhotoGridSize() {
if (_prefs.containsKey(kPhotoGridSize)) {
return _prefs.getInt(kPhotoGridSize)!;