[mob][photos] Album UI improvements (#6176)

## Description

### Changes

1. Album corner radius (114 size - 12px corner radius) (100 size - 8px
corner radius)
2. Thumbnail view size 60 -  corner radius 4
3. Padding between album thumbnail and Album name is 6
4. Padding between Album name and number of photos is 2
5. Album name (text size) - Small regular , Full black (color)
6. Number of photos (text size) - Mini Regular  , Text muted (color)
7. New design for Add album thumbnail
8. Distance between album vertically & horizontally 8px
9. List view border radius 6px and thumbnail radius 4px
This commit is contained in:
Ashil
2025-07-01 14:27:53 +05:30
committed by GitHub
30 changed files with 720 additions and 478 deletions

View File

@@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.75 6.75H21.75M6.75 12H21.75M6.75 17.25H21.75" stroke="white" stroke-opacity="0.6" stroke-width="2.25" stroke-linejoin="round"/>
<path d="M3 6H4.5V7.5H3V6ZM3 11.25H4.5V12.75H3V11.25ZM3 16.5H4.5V18H3V16.5Z" stroke="white" stroke-opacity="0.6" stroke-width="1.5" stroke-linecap="square" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 425 B

View File

@@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.75 6.75H21.75M6.75 12H21.75M6.75 17.25H21.75" stroke="black" stroke-opacity="0.6" stroke-width="2.25" stroke-linejoin="round"/>
<path d="M3 6H4.5V7.5H3V6ZM3 11.25H4.5V12.75H3V11.25ZM3 16.5H4.5V18H3V16.5Z" stroke="black" stroke-opacity="0.6" stroke-width="1.5" stroke-linecap="square" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 425 B

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.7548 14.394L12.1277 10.7669C13.0009 9.60436 13.4723 8.18933 13.4707 6.73536C13.4707 3.02151 10.4492 0 6.73536 0C3.02151 0 0 3.02151 0 6.73536C0 10.4492 3.02151 13.4707 6.73536 13.4707C8.18933 13.4723 9.60436 13.0009 10.7669 12.1277L14.394 15.7548C14.5776 15.9189 14.8171 16.0065 15.0632 15.9996C15.3094 15.9927 15.5436 15.8919 15.7177 15.7177C15.8919 15.5436 15.9927 15.3094 15.9996 15.0632C16.0065 14.8171 15.9189 14.5776 15.7548 14.394ZM1.92439 6.73536C1.92439 5.78384 2.20655 4.85369 2.73518 4.06253C3.26382 3.27137 4.01519 2.65473 4.89428 2.2906C5.77337 1.92647 6.7407 1.8312 7.67393 2.01683C8.60717 2.20246 9.4644 2.66066 10.1372 3.33349C10.8101 4.00632 11.2683 4.86355 11.4539 5.79679C11.6395 6.73002 11.5442 7.69735 11.1801 8.57644C10.816 9.45553 10.1994 10.2069 9.40819 10.7355C8.61703 11.2642 7.68688 11.5463 6.73536 11.5463C5.45988 11.5448 4.23708 11.0374 3.33518 10.1355C2.43328 9.23364 1.92592 8.01084 1.92439 6.73536Z" fill="white" fill-opacity="0.6"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.7548 14.394L12.1277 10.7669C13.0009 9.60436 13.4723 8.18933 13.4707 6.73536C13.4707 3.02151 10.4492 0 6.73536 0C3.02151 0 0 3.02151 0 6.73536C0 10.4492 3.02151 13.4707 6.73536 13.4707C8.18933 13.4723 9.60436 13.0009 10.7669 12.1277L14.394 15.7548C14.5776 15.9189 14.8171 16.0065 15.0632 15.9996C15.3094 15.9927 15.5436 15.8919 15.7177 15.7177C15.8919 15.5436 15.9927 15.3094 15.9996 15.0632C16.0065 14.8171 15.9189 14.5776 15.7548 14.394ZM1.92439 6.73536C1.92439 5.78384 2.20655 4.85369 2.73518 4.06253C3.26382 3.27137 4.01519 2.65473 4.89428 2.2906C5.77337 1.92647 6.7407 1.8312 7.67393 2.01683C8.60717 2.20246 9.4644 2.66066 10.1372 3.33349C10.8101 4.00632 11.2683 4.86355 11.4539 5.79679C11.6395 6.73002 11.5442 7.69735 11.1801 8.57644C10.816 9.45553 10.1994 10.2069 9.40819 10.7355C8.61703 11.2642 7.68688 11.5463 6.73536 11.5463C5.45988 11.5448 4.23708 11.0374 3.33518 10.1355C2.43328 9.23364 1.92592 8.01084 1.92439 6.73536Z" fill="black" fill-opacity="0.6"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -463,7 +463,7 @@ SPEC CHECKSUMS:
flutter_native_splash: 6cad9122ea0fad137d23137dd14b937f3e90b145
flutter_secure_storage: 2c2ff13db9e0a5647389bff88b0ecac56e3f3418
flutter_sodium: 7e4621538491834eba53bd524547854bcbbd6987
flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544
flutter_timezone: ac3da59ac941ff1c98a2e1f0293420e020120282
fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1

View File

@@ -0,0 +1,8 @@
import "package:photos/events/event.dart";
import "package:photos/models/collection/collection.dart";
class CreateNewAlbumEvent extends Event {
final Collection collection;
CreateNewAlbumEvent(this.collection);
}

View File

@@ -1690,5 +1690,14 @@
"onTheRoad": "På veien igjen",
"food": "Kulinær glede",
"pets": "Pelsvenner",
"wishThemAHappyBirthday": "Wish ${name} a happy birthday! 🎉"
"cLIcon": "Nytt ikon",
"cLIconDesc": "Endelig er et nytt appikon, som vi tror best representerer arbeidet vårt. Vi har også lagt til en icon-switcher slik at du kan fortsette å bruke det gamle ikonet.",
"cLMemories": "Minner",
"cLMemoriesDesc": "Gjenoppdag dine spesielle øyeblikk - fremhev dine favorittpersoner, dine turer og ferier, de beste bildene dine, og mye mer. Skru på maskinlæring, merk deg selv og navngi vennene dine for best mulig opplevelse.",
"cLWidgets": "Widgeter",
"cLWidgetsDesc": "Hjemmeskjermwidgeter som er integrert med minner er nå tilgjengelige. De vil vise dine spesielle øyeblikk uten å åpne appen.",
"cLFamilyPlan": "Begrensninger for familieabonnement",
"cLFamilyPlanDesc": "Du kan nå sette grenser for hvor mye lagringsplass familiemedlemmer kan bruke.",
"cLBulkEdit": "Masseendring av datoer",
"cLBulkEditDesc": "Du kan nå velge flere bilder, og redigere dato/klokkeslett for alle med en rask handling. Forskyving av datoer støttes også."
}

View File

@@ -10,7 +10,7 @@ import "package:photos/ui/components/buttons/icon_button_widget.dart";
import 'package:photos/ui/viewer/file/no_thumbnail_widget.dart';
import 'package:photos/ui/viewer/file/thumbnail_widget.dart';
///https://www.figma.com/file/SYtMyLBs5SAOkTbfMMzhqt/ente-Visual-Design?node-id=7480%3A33462&t=H5AvR79OYDnB9ekw-4
///https://www.figma.com/design/SYtMyLBs5SAOkTbfMMzhqt/Ente-Visual-Design?node-id=39181-172145&t=3qmSZWpXF3ZC4JGN-1
class AlbumColumnItemWidget extends StatelessWidget {
final Collection collection;
final List<Collection> selectedCollections;
@@ -36,7 +36,7 @@ class AlbumColumnItemWidget extends StatelessWidget {
isSelected ? colorScheme.strokeMuted : colorScheme.strokeFainter,
),
borderRadius: const BorderRadius.all(
Radius.circular(4),
Radius.circular(6),
),
),
child: Row(
@@ -47,9 +47,7 @@ class AlbumColumnItemWidget extends StatelessWidget {
child: Row(
children: [
ClipRRect(
borderRadius: const BorderRadius.horizontal(
left: Radius.circular(4),
),
borderRadius: BorderRadius.circular(4),
child: SizedBox(
height: sideOfThumbnail,
width: sideOfThumbnail,
@@ -66,7 +64,7 @@ class AlbumColumnItemWidget extends StatelessWidget {
);
} else {
return const NoThumbnailWidget(
addBorder: false,
addBorder: false,
);
}
},
@@ -94,9 +92,7 @@ class AlbumColumnItemWidget extends StatelessWidget {
snapshot.data!,
NumberFormat().format(snapshot.data!),
),
style: textTheme.small.copyWith(
color: colorScheme.textMuted,
),
style: textTheme.miniMuted,
);
} else {
if (snapshot.hasError) {

View File

@@ -1,4 +1,5 @@
import "dart:async";
import "dart:math";
import "package:flutter/cupertino.dart";
import "package:logging/logging.dart";
@@ -29,6 +30,10 @@ class _AlbumHorizontalListState extends State<AlbumHorizontalList> {
_collectionUpdatesSubscription;
late Logger _logger;
static const maxThumbnailWidth = 224.0;
static const crossAxisSpacing = 8.0;
static const horizontalPadding = 16.0;
@override
void initState() {
super.initState();
@@ -47,6 +52,12 @@ class _AlbumHorizontalListState extends State<AlbumHorizontalList> {
@override
Widget build(BuildContext context) {
final double screenWidth = MediaQuery.sizeOf(context).width;
final int albumsCountInRow = max(screenWidth ~/ maxThumbnailWidth, 3);
final totalHorizontalPadding = (albumsCountInRow - 1) * crossAxisSpacing;
final sideOfThumbnail =
(screenWidth - totalHorizontalPadding - horizontalPadding) /
albumsCountInRow;
debugPrint('$runtimeType widget build');
return FutureBuilder<List<Collection>>(
future: widget.collectionsFuture(),
@@ -75,20 +86,25 @@ class _AlbumHorizontalListState extends State<AlbumHorizontalList> {
Align(
alignment: Alignment.centerLeft,
child: SizedBox(
height: 147, //139 + 8 (calculated from figma design)
child: ListView.separated(
separatorBuilder: (context, index) =>
const SizedBox(width: 4),
height: sideOfThumbnail + 46,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: collections.length,
padding: const EdgeInsets.symmetric(horizontal: 8),
padding: const EdgeInsets.symmetric(
horizontal: horizontalPadding / 2,
),
itemBuilder: (context, index) {
final item = collections[index];
return AlbumRowItemWidget(
item,
120,
showFileCount: false,
hasVerifiedLock: widget.hasVerifiedLock,
return Padding(
padding: const EdgeInsets.only(
right: horizontalPadding / 2,
),
child: AlbumRowItemWidget(
item,
sideOfThumbnail,
showFileCount: true,
hasVerifiedLock: widget.hasVerifiedLock,
),
);
},
),

View File

@@ -36,9 +36,7 @@ class AlbumListItemWidget extends StatelessWidget {
child: Row(
children: [
ClipRRect(
borderRadius: const BorderRadius.horizontal(
left: Radius.circular(4),
),
borderRadius: BorderRadius.circular(4),
child: SizedBox(
height: sideOfThumbnail,
width: sideOfThumbnail,
@@ -53,7 +51,9 @@ class AlbumListItemWidget extends StatelessWidget {
shouldShowOwnerAvatar: false,
);
} else {
return const NoThumbnailWidget(addBorder: false);
return const NoThumbnailWidget(
addBorder: false,
);
}
},
),
@@ -63,13 +63,15 @@ class AlbumListItemWidget extends StatelessWidget {
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
collection.displayName,
overflow: TextOverflow.ellipsis,
),
FutureBuilder<int>(
future: CollectionsService.instance.getFileCount(collection),
future:
CollectionsService.instance.getFileCount(collection),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(
@@ -123,7 +125,7 @@ class AlbumListItemWidget extends StatelessWidget {
? colorScheme.strokeMuted
: colorScheme.strokeFainter,
),
borderRadius: const BorderRadius.all(Radius.circular(4)),
borderRadius: const BorderRadius.all(Radius.circular(6)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,

View File

@@ -3,7 +3,8 @@ import 'package:flutter/material.dart';
import "package:photos/generated/l10n.dart";
import 'package:photos/theme/ente_theme.dart';
///https://www.figma.com/file/SYtMyLBs5SAOkTbfMMzhqt/ente-Visual-Design?node-id=10854%3A57947&t=H5AvR79OYDnB9ekw-4
//https://www.figma.com/design/SYtMyLBs5SAOkTbfMMzhqt/Ente-Visual-Design?node-id=39181-172209&t=3qmSZWpXF3ZC4JGN-1
class NewAlbumListItemWidget extends StatelessWidget {
const NewAlbumListItemWidget({
super.key,
@@ -22,12 +23,13 @@ class NewAlbumListItemWidget extends StatelessWidget {
Row(
children: [
ClipRRect(
borderRadius: const BorderRadius.horizontal(
left: Radius.circular(4),
),
child: SizedBox(
borderRadius: BorderRadius.circular(4),
child: Container(
height: sideOfThumbnail,
width: sideOfThumbnail,
color: Theme.of(context).brightness == Brightness.light
? colorScheme.backdropBase
: colorScheme.backdropFaint,
child: Icon(
Icons.add_outlined,
color: colorScheme.strokeMuted,

View File

@@ -23,6 +23,7 @@ class NewAlbumRowItemWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colorScheme = getEnteColorScheme(context);
return GestureDetector(
onTap: () async {
final result = await showTextInputDialog(
@@ -67,25 +68,31 @@ class NewAlbumRowItemWidget extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: height,
width: width,
child: DottedBorder(
borderType: BorderType.RRect,
strokeWidth: 1.5,
dashPattern: const [3.75, 3.75],
radius: const Radius.circular(2.35),
padding: EdgeInsets.zero,
color: getEnteColorScheme(context).strokeMuted,
child: Center(
child: Icon(
Icons.add,
color: getEnteColorScheme(context).strokeMuted,
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Container(
height: height,
width: width,
color: Theme.of(context).brightness == Brightness.light
? colorScheme.backdropBase
: colorScheme.backdropFaint,
child: DottedBorder(
borderType: BorderType.RRect,
strokeWidth: 1.75,
dashPattern: const [3.75, 3.75],
radius: const Radius.circular(12),
padding: EdgeInsets.zero,
color: colorScheme.strokeFaint,
child: Center(
child: Icon(
Icons.add,
color: colorScheme.strokeFaint,
),
),
),
),
),
const SizedBox(height: 4),
const SizedBox(height: 6),
Text(
S.of(context).addNew,
style: getEnteTextTheme(context).smallFaint,

View File

@@ -1,3 +1,4 @@
import "package:figma_squircle/figma_squircle.dart";
import 'package:flutter/material.dart';
import "package:intl/intl.dart";
import "package:photos/core/configuration.dart";
@@ -24,6 +25,9 @@ class AlbumRowItemWidget extends StatelessWidget {
final void Function(Collection)? onTapCallback;
final void Function(Collection)? onLongPressCallback;
final SelectedAlbums? selectedAlbums;
static const _borderWidth = 1.0;
static const _cornerRadius = 12.0;
static const _cornerSmoothing = 1.0;
const AlbumRowItemWidget(
this.c,
@@ -56,125 +60,152 @@ class AlbumRowItemWidget extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(1),
child: SizedBox(
height: sideOfThumbnail,
width: sideOfThumbnail,
child: Stack(
children: [
FutureBuilder<EnteFile?>(
future: CollectionsService.instance.getCover(c),
builder: (context, snapshot) {
EnteFile? thumbnail;
if (snapshot.hasData) {
thumbnail = snapshot.data!;
} else {
//Need to use cached thumbnail so that the hero
//animation works as expected.
thumbnail =
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: isSelected
? ColorFiltered(
colorFilter: ColorFilter.mode(
Colors.black.withOpacity(
0.4,
),
BlendMode.darken,
),
child: thumbnailWidget,
)
: thumbnailWidget,
);
} else {
return const NoThumbnailWidget();
}
},
),
if (isOwner && (c.hasSharees || c.hasLink))
Hero(
tag: tagPrefix + "_sharees",
transitionOnUserGestures: true,
child: Align(
alignment: Alignment.topLeft,
child: AlbumSharesIcons(
padding: const EdgeInsets.only(left: 4, top: 4),
sharees: c.getSharees(),
type: AvatarType.mini,
trailingWidget: linkIcon,
),
),
),
Positioned(
top: 5,
right: 5,
child: Hero(
tag: tagPrefix + "_album_selection",
transitionOnUserGestures: true,
child: ListenableBuilder(
listenable: selectedAlbums ?? ValueNotifier(false),
builder: (context, _) {
SizedBox(
height: sideOfThumbnail,
width: sideOfThumbnail,
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
ClipSmoothRect(
radius: SmoothBorderRadius(
cornerRadius: _cornerRadius + _borderWidth,
cornerSmoothing: _cornerSmoothing,
),
child: Container(
color: getEnteColorScheme(context).strokeFaint,
width: sideOfThumbnail,
height: sideOfThumbnail,
),
),
ClipSmoothRect(
radius: SmoothBorderRadius(
cornerRadius: _cornerRadius,
cornerSmoothing: _cornerSmoothing,
),
child: SizedBox(
height: sideOfThumbnail - _borderWidth * 2,
width: sideOfThumbnail - _borderWidth * 2,
child: Stack(
children: [
FutureBuilder<EnteFile?>(
future: CollectionsService.instance.getCover(c),
builder: (context, snapshot) {
EnteFile? thumbnail;
if (snapshot.hasData) {
thumbnail = snapshot.data!;
} else {
//Need to use cached thumbnail so that the hero
//animation works as expected.
thumbnail =
CollectionsService.instance.getCoverCache(c);
}
if (thumbnail != null) {
final bool isSelected =
selectedAlbums?.isAlbumSelected(c) ?? false;
return AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
switchInCurve: Curves.easeOut,
switchOutCurve: Curves.easeIn,
child: isSelected
? const Icon(
Icons.check_circle_rounded,
color: Colors.white,
size: 22,
)
: null,
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: isSelected
? ColorFiltered(
colorFilter: ColorFilter.mode(
Colors.black.withOpacity(
0.4,
),
BlendMode.darken,
),
child: thumbnailWidget,
)
: thumbnailWidget,
);
} else {
return Container(
color: getEnteColorScheme(context).backdropBase,
child: const NoThumbnailWidget(
borderRadius: 12,
addBorder: false,
),
);
}
},
),
),
if (!isOwner)
Align(
alignment: Alignment.bottomRight,
child: Hero(
tag: tagPrefix + "_owner_other",
if (isOwner && (c.hasSharees || c.hasLink))
Hero(
tag: tagPrefix + "_sharees",
transitionOnUserGestures: true,
child: Padding(
padding:
const EdgeInsets.only(right: 4, bottom: 4),
child: UserAvatarWidget(
c.owner,
thumbnailView: true,
child: Align(
alignment: Alignment.topLeft,
child: AlbumSharesIcons(
padding: const EdgeInsets.only(left: 4, top: 4),
sharees: c.getSharees(),
type: AvatarType.mini,
trailingWidget: linkIcon,
),
),
),
Positioned(
top: 5,
right: 5,
child: Hero(
tag: tagPrefix + "_album_selection",
transitionOnUserGestures: true,
child: ListenableBuilder(
listenable:
selectedAlbums ?? ValueNotifier(false),
builder: (context, _) {
final bool isSelected =
selectedAlbums?.isAlbumSelected(c) ?? false;
return AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
switchInCurve: Curves.easeOut,
switchOutCurve: Curves.easeIn,
child: isSelected
? const Icon(
Icons.check_circle_rounded,
color: Colors.white,
size: 22,
)
: null,
);
},
),
),
),
],
if (!isOwner)
Align(
alignment: Alignment.bottomRight,
child: Hero(
tag: tagPrefix + "_owner_other",
transitionOnUserGestures: true,
child: Padding(
padding:
const EdgeInsets.only(right: 4, bottom: 4),
child: UserAvatarWidget(
c.owner,
thumbnailView: true,
),
),
),
),
],
),
),
),
),
],
],
),
),
const SizedBox(height: 4),
const SizedBox(height: 6),
Hero(
tag: tagPrefix + "_title",
transitionOnUserGestures: true,
@@ -214,7 +245,7 @@ class AlbumRowItemWidget extends StatelessWidget {
const SizedBox(height: 2),
RichText(
text: TextSpan(
style: enteTextTheme.tinyMuted,
style: enteTextTheme.miniMuted,
children: [
TextSpan(text: textCount),
],

View File

@@ -5,6 +5,7 @@ import "package:flutter/services.dart";
import 'package:logging/logging.dart';
import 'package:photos/core/configuration.dart';
import "package:photos/core/event_bus.dart";
import "package:photos/events/create_new_album_event.dart";
import "package:photos/events/tab_changed_event.dart";
import "package:photos/generated/l10n.dart";
import 'package:photos/models/collection/collection.dart';
@@ -183,31 +184,35 @@ class _AlbumVerticalListWidgetState extends State<AlbumVerticalListWidget> {
}
if (collection != null) {
if (await _runCollectionAction(
context,
collection,
showProgressDialog: false,
)) {
if (widget.actionType == CollectionActionType.restoreFiles) {
showShortToast(
context,
'Restored files to album ' + albumName,
);
} else {
showShortToast(
context,
"Album '" + albumName + "' created.",
);
}
Navigator.pop(context);
Navigator.pop(context);
await _navigateToCollection(
if (widget.enableSelection) {
Bus.instance.fire(CreateNewAlbumEvent(collection));
} else {
if (await _runCollectionAction(
context,
collection,
hasVerifiedLock: hasVerifiedLock,
);
showProgressDialog: false,
)) {
if (widget.actionType == CollectionActionType.restoreFiles) {
showShortToast(
context,
'Restored files to album ' + albumName,
);
} else {
showShortToast(
context,
"Album '" + albumName + "' created.",
);
}
Navigator.pop(context);
Navigator.pop(context);
await _navigateToCollection(
context,
collection,
hasVerifiedLock: hasVerifiedLock,
);
}
}
}
}

View File

@@ -5,6 +5,8 @@ import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import "package:photos/core/configuration.dart";
import "package:photos/core/event_bus.dart";
import "package:photos/events/create_new_album_event.dart";
import "package:photos/generated/l10n.dart";
import 'package:photos/models/collection/collection.dart';
import 'package:photos/models/selected_files.dart';
@@ -34,6 +36,12 @@ enum CollectionActionType {
moveToHiddenCollection,
}
extension CollectionActionTypeExtension on CollectionActionType {
bool get isHiddenAction =>
this == CollectionActionType.moveToHiddenCollection ||
this == CollectionActionType.addToHiddenAlbum;
}
String _actionName(
BuildContext context,
CollectionActionType type,
@@ -119,16 +127,29 @@ class _CollectionActionSheetState extends State<CollectionActionSheet> {
static const int okButtonSize = 80;
String _searchQuery = "";
final _selectedCollections = <Collection>[];
final _recentlyCreatedCollections = <Collection>[];
late StreamSubscription<CreateNewAlbumEvent> _createNewAlbumSubscription;
@override
void initState() {
super.initState();
_showOnlyHiddenCollections =
widget.actionType == CollectionActionType.moveToHiddenCollection ||
widget.actionType == CollectionActionType.addToHiddenAlbum;
_showOnlyHiddenCollections = widget.actionType.isHiddenAction;
_enableSelection = (widget.actionType == CollectionActionType.addFiles ||
widget.actionType == CollectionActionType.addToHiddenAlbum) &&
(widget.sharedFiles == null || widget.sharedFiles!.isEmpty);
_createNewAlbumSubscription =
Bus.instance.on<CreateNewAlbumEvent>().listen((event) {
setState(() {
_recentlyCreatedCollections.insert(0, event.collection);
_selectedCollections.add(event.collection);
});
});
}
@override
void dispose() {
_createNewAlbumSubscription.cancel();
super.dispose();
}
@override
@@ -319,15 +340,25 @@ class _CollectionActionSheetState extends State<CollectionActionSheet> {
Future<List<Collection>> _getCollections() async {
if (_showOnlyHiddenCollections) {
final List<Collection> recentlyCreated = [];
final List<Collection> hidden = [];
final hiddenCollections = CollectionsService.instance
.getHiddenCollections(includeDefaultHidden: false);
hiddenCollections.sort((first, second) {
for (final collection in hiddenCollections) {
if (_recentlyCreatedCollections.contains(collection)) {
recentlyCreated.add(collection);
} else {
hidden.add(collection);
}
}
hidden.sort((first, second) {
return compareAsciiLowerCaseNatural(
first.displayName,
second.displayName,
);
});
return hiddenCollections;
return recentlyCreated + hidden;
} else {
final List<Collection> collections =
CollectionsService.instance.getCollectionsForUI(
@@ -344,6 +375,7 @@ class _CollectionActionSheetState extends State<CollectionActionSheet> {
});
final List<Collection> pinned = [];
final List<Collection> unpinned = [];
final List<Collection> recentlyCreated = [];
// show uncategorized collection only for restore files action
Collection? uncategorized;
for (final collection in collections) {
@@ -355,13 +387,20 @@ class _CollectionActionSheetState extends State<CollectionActionSheet> {
}
continue;
}
if (_recentlyCreatedCollections.contains(collection)) {
recentlyCreated.add(collection);
continue;
}
if (collection.isPinned) {
pinned.add(collection);
} else {
unpinned.add(collection);
}
}
return (uncategorized != null ? [uncategorized] + pinned + unpinned : []);
return uncategorized != null
? [uncategorized] + recentlyCreated + pinned + unpinned
: recentlyCreated + pinned + unpinned;
}
}

View File

@@ -1,6 +1,7 @@
import "dart:async";
import 'package:flutter/material.dart';
import "package:flutter_svg/flutter_svg.dart";
import "package:photos/core/event_bus.dart";
import "package:photos/events/album_sort_order_change_event.dart";
import "package:photos/events/collection_updated_event.dart";
@@ -142,7 +143,7 @@ class _CollectionListPageState extends State<CollectionListPage> {
Widget _sortMenu(List<Collection> collections) {
final colorTheme = getEnteColorScheme(context);
final isLightMode = Theme.of(context).brightness == Brightness.light;
Widget sortOptionText(AlbumSortKey key) {
String text = key.toString();
switch (key) {
@@ -180,12 +181,7 @@ class _CollectionListPageState extends State<CollectionListPage> {
),
child: Row(
children: [
IconButtonWidget(
icon: albumViewType == AlbumViewType.grid
? Icons.view_list_outlined
: Icons.grid_view_outlined,
iconButtonType: IconButtonType.secondary,
iconColor: colorTheme.blurStrokePressed,
GestureDetector(
onTap: () async {
setState(() {
albumViewType = albumViewType == AlbumViewType.grid
@@ -194,6 +190,24 @@ class _CollectionListPageState extends State<CollectionListPage> {
});
await localSettings.setAlbumViewType(albumViewType!);
},
child: Padding(
padding: const EdgeInsets.all(12.0),
child: SizedBox(
height: 24,
width: 24,
child: albumViewType == AlbumViewType.grid
? SvgPicture.asset(
isLightMode
? "assets/icons/list_view_icon_light.svg"
: "assets/icons/list_view_icon_dark.svg",
)
: Icon(
Icons.grid_view,
color: colorTheme.textMuted,
size: 22,
),
),
),
),
GestureDetector(
onTapDown: (TapDownDetails details) async {
@@ -226,9 +240,9 @@ class _CollectionListPageState extends State<CollectionListPage> {
}
},
child: IconButtonWidget(
icon: Icons.sort_outlined,
icon: Icons.sort_rounded,
iconButtonType: IconButtonType.secondary,
iconColor: colorTheme.blurStrokePressed,
iconColor: colorTheme.textMuted,
),
),
],

View File

@@ -1,6 +1,8 @@
import "package:figma_squircle/figma_squircle.dart";
import 'package:flutter/material.dart';
import 'package:photos/ente_theme_data.dart';
import 'package:photos/models/device_collection.dart';
import "package:photos/theme/ente_theme.dart";
import 'package:photos/ui/viewer/file/file_icons_widget.dart';
import 'package:photos/ui/viewer/file/thumbnail_widget.dart';
import 'package:photos/ui/viewer/gallery/device_folder_page.dart';
@@ -9,6 +11,11 @@ import 'package:photos/utils/navigation_util.dart';
class DeviceFolderItem extends StatelessWidget {
final DeviceCollection deviceCollection;
final double sideOfThumbnail;
static const _cornerRadius = 12.0;
static const _cornerSmoothing = 1.0;
static const _borderWidth = 1.0;
const DeviceFolderItem(
this.deviceCollection, {
///120 is default for the 'on device' scrollview in albums section
@@ -23,36 +30,60 @@ class DeviceFolderItem extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
ClipRRect(
borderRadius: BorderRadius.circular(1),
child: SizedBox(
height: sideOfThumbnail,
width: sideOfThumbnail,
child: Hero(
tag: "device_folder:" +
deviceCollection.name +
deviceCollection.thumbnail!.tag,
transitionOnUserGestures: true,
child: Stack(
children: [
ThumbnailWidget(
deviceCollection.thumbnail!,
shouldShowSyncStatus: false,
key: Key(
"device_folder:" +
deviceCollection.name +
deviceCollection.thumbnail!.tag,
SizedBox(
height: sideOfThumbnail,
width: sideOfThumbnail,
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
ClipSmoothRect(
radius: SmoothBorderRadius(
cornerRadius: _cornerRadius + _borderWidth,
cornerSmoothing: _cornerSmoothing,
),
child: Container(
color: getEnteColorScheme(context).strokeFaint,
width: sideOfThumbnail,
height: sideOfThumbnail,
),
),
ClipSmoothRect(
radius: SmoothBorderRadius(
cornerRadius: _cornerRadius,
cornerSmoothing: _cornerSmoothing,
),
child: SizedBox(
height: sideOfThumbnail - _borderWidth * 2,
width: sideOfThumbnail - _borderWidth * 2,
child: Hero(
tag: "device_folder:" +
deviceCollection.name +
deviceCollection.thumbnail!.tag,
transitionOnUserGestures: true,
child: Stack(
children: [
ThumbnailWidget(
deviceCollection.thumbnail!,
shouldShowSyncStatus: false,
key: Key(
"device_folder:" +
deviceCollection.name +
deviceCollection.thumbnail!.tag,
),
),
isBackedUp
? const SizedBox.shrink()
: const UnSyncedIcon(),
],
),
),
isBackedUp ? const SizedBox.shrink() : const UnSyncedIcon(),
],
),
),
),
],
),
),
const SizedBox(
height: 2,
),
const SizedBox(height: 6),
SizedBox(
width: sideOfThumbnail,
child: Text(
@@ -62,6 +93,17 @@ class DeviceFolderItem extends StatelessWidget {
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(height: 2),
SizedBox(
width: sideOfThumbnail,
child: Text(
deviceCollection.count.toString(),
textAlign: TextAlign.left,
style:
Theme.of(context).colorScheme.enteTheme.textTheme.miniMuted,
overflow: TextOverflow.ellipsis,
),
),
],
),
onTap: () {

View File

@@ -1,4 +1,5 @@
import 'dart:async';
import "dart:math";
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
@@ -33,6 +34,9 @@ class _DeviceFoldersGridViewState extends State<DeviceFoldersGridView> {
executionInterval: const Duration(seconds: 5),
leading: true,
);
static const maxThumbnailWidth = 224.0;
static const horizontalPadding = 16.0;
static const crossAxisSpacing = 8.0;
@override
void initState() {
@@ -57,9 +61,18 @@ class _DeviceFoldersGridViewState extends State<DeviceFoldersGridView> {
@override
Widget build(BuildContext context) {
final double screenWidth = MediaQuery.sizeOf(context).width;
final int albumsCountInCrossAxis = max(screenWidth ~/ maxThumbnailWidth, 3);
final double totalCrossAxisSpacing =
(albumsCountInCrossAxis - 1) * crossAxisSpacing;
final double sideOfThumbnail =
(screenWidth - totalCrossAxisSpacing - horizontalPadding) /
albumsCountInCrossAxis;
debugPrint("${(DeviceFoldersGridView).toString()} - $_loadReason");
return SizedBox(
height: 170,
height: sideOfThumbnail + 46,
child: Align(
alignment: Alignment.centerLeft,
child: FutureBuilder<List<DeviceCollection>>(
@@ -72,19 +85,28 @@ class _DeviceFoldersGridViewState extends State<DeviceFoldersGridView> {
padding: EdgeInsets.all(22),
child: EmptyState(),
)
: ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.fromLTRB(8, 0, 8, 0),
physics: const ScrollPhysics(),
// to disable GridView's scrolling
itemBuilder: (context, index) {
final deviceCollection = snapshot.data![index];
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 2),
child: DeviceFolderItem(deviceCollection),
);
},
itemCount: snapshot.data!.length,
: Padding(
padding: const EdgeInsets.symmetric(
horizontal: horizontalPadding / 2,
),
child: ListView.builder(
scrollDirection: Axis.horizontal,
physics: const ScrollPhysics(),
// to disable GridView's scrolling
itemBuilder: (context, index) {
final deviceCollection = snapshot.data![index];
return Padding(
padding: const EdgeInsets.only(
right: horizontalPadding / 2,
),
child: DeviceFolderItem(
deviceCollection,
sideOfThumbnail: sideOfThumbnail,
),
);
},
itemCount: snapshot.data!.length,
),
);
} else if (snapshot.hasError) {
_logger.severe("failed to load device gallery", snapshot.error);

View File

@@ -62,13 +62,12 @@ class _DeviceFolderVerticalGridViewBodyState
executionInterval: const Duration(seconds: 4),
);
/*
Aspect ratio 1:1 Max width 224 Fixed gap 8
Width changes dynamically with screen width such that we can fit 2 in one row.
Keep the width integral (center the albums to distribute excess pixels)
*/
static const maxThumbnailWidth = 170.0;
static const fixedGapBetweenAlbum = 2.0;
static const minGapForHorizontalPadding = 8.0;
Aspect ratio 1:1
Width changes dynamically with screen width
*/
static const maxThumbnailWidth = 224.0;
static const horizontalPadding = 16.0;
static const crossAxisSpacing = 8.0;
@override
void initState() {
@@ -101,30 +100,28 @@ class _DeviceFolderVerticalGridViewBodyState
FilesDB.instance.getDeviceCollections(includeCoverThumbnail: true),
builder: (context, snapshot) {
if (snapshot.hasData) {
final double screenWidth = MediaQuery.of(context).size.width;
final int albumsCountInOneRow =
final double screenWidth = MediaQuery.sizeOf(context).width;
final int albumsCountInCrossAxis =
max(screenWidth ~/ maxThumbnailWidth, 3);
final double gapBetweenAlbums =
(albumsCountInOneRow - 1) * fixedGapBetweenAlbum;
final double gapOnSizeOfAlbums = minGapForHorizontalPadding +
(screenWidth -
gapBetweenAlbums -
(2 * minGapForHorizontalPadding)) %
albumsCountInOneRow;
final double totalCrossAxisSpacing =
(albumsCountInCrossAxis - 1) * crossAxisSpacing;
final double sideOfThumbnail =
(screenWidth - gapOnSizeOfAlbums - gapBetweenAlbums) /
albumsCountInOneRow;
(screenWidth - totalCrossAxisSpacing - horizontalPadding) /
albumsCountInCrossAxis;
return snapshot.data!.isEmpty
? const SliverFillRemaining(child: EmptyState())
: SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 8),
padding: const EdgeInsets.only(
left: horizontalPadding / 2,
right: horizontalPadding / 2,
),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: albumsCountInOneRow,
mainAxisSpacing: 4,
crossAxisSpacing: gapBetweenAlbums,
crossAxisCount: albumsCountInCrossAxis,
mainAxisSpacing: 8,
crossAxisSpacing: crossAxisSpacing,
childAspectRatio:
sideOfThumbnail / (sideOfThumbnail + 46),
),

View File

@@ -186,7 +186,7 @@ class _CollectionsFlexiGridViewWidgetState
),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: albumsCountInCrossAxis,
mainAxisSpacing: 2,
mainAxisSpacing: 8,
crossAxisSpacing: CollectionsFlexiGridViewWidget.crossAxisSpacing,
childAspectRatio: sideOfThumbnail / (sideOfThumbnail + 46),
),

View File

@@ -1,6 +1,6 @@
import "package:flutter/material.dart";
import "package:flutter_svg/flutter_svg.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/components/buttons/icon_button_widget.dart";
class SearchableAppBar extends StatefulWidget {
final Widget title;
@@ -56,6 +56,7 @@ class _SearchableAppBarState extends State<SearchableAppBar> {
@override
Widget build(BuildContext context) {
final isLightMode = Theme.of(context).brightness == Brightness.light;
return SliverAppBar(
floating: true,
elevation: 0,
@@ -81,11 +82,20 @@ class _SearchableAppBarState extends State<SearchableAppBar> {
actions: _isSearchActive
? null
: [
IconButtonWidget(
icon: Icons.search,
iconButtonType: IconButtonType.secondary,
GestureDetector(
onTap: _activateSearch,
iconColor: getEnteColorScheme(context).blurStrokePressed,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: SizedBox(
height: 18,
width: 18,
child: SvgPicture.asset(
isLightMode
? "assets/icons/search_icon_light.svg"
: "assets/icons/search_icon_dark.svg",
),
),
),
),
...?widget.actions,
],

View File

@@ -35,7 +35,7 @@ class QuickLinkAlbumItem extends StatelessWidget {
isSelected ? colorScheme.strokeMuted : colorScheme.strokeFainter,
),
borderRadius: const BorderRadius.all(
Radius.circular(2),
Radius.circular(6),
),
),
child: Row(
@@ -57,9 +57,7 @@ class QuickLinkAlbumItem extends StatelessWidget {
return Hero(
tag: heroTag,
child: ClipRRect(
borderRadius: const BorderRadius.horizontal(
left: Radius.circular(2),
),
borderRadius: BorderRadius.circular(4),
child: ThumbnailWidget(
snapshot.data!,
key: ValueKey(heroTag),

View File

@@ -60,6 +60,10 @@ class _SharedCollectionsTabState extends State<SharedCollectionsTab>
const Duration(milliseconds: 500),
);
static const maxThumbnailWidth = 224.0;
static const crossAxisSpacing = 8.0;
static const horizontalPadding = 16.0;
@override
void initState() {
super.initState();
@@ -135,9 +139,14 @@ class _SharedCollectionsTabState extends State<SharedCollectionsTab>
}
Widget _getSharedCollectionsGallery(SharedCollections collections) {
const maxThumbnailWidth = 160.0;
const maxQuickLinks = 4;
final numberOfQuickLinks = collections.quickLinks.length;
final double screenWidth = MediaQuery.sizeOf(context).width;
final int albumsCountInRow = max(screenWidth ~/ maxThumbnailWidth, 3);
final totalHorizontalPadding = (albumsCountInRow - 1) * crossAxisSpacing;
final sideOfThumbnail =
(screenWidth - totalHorizontalPadding - horizontalPadding) /
albumsCountInRow;
const quickLinkTitleHeroTag = "quick_link_title";
final SectionTitle sharedWithYou =
SectionTitle(title: S.of(context).sharedWithYou);
@@ -150,195 +159,188 @@ class _SharedCollectionsTabState extends State<SharedCollectionsTab>
margin: const EdgeInsets.only(bottom: 50),
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SectionOptions(
onTap: collections.incoming.isNotEmpty
? () {
unawaited(
routeToPage(
context,
CollectionListPage(
collections.incoming,
sectionType:
UISectionType.incomingCollections,
tag: "incoming",
appTitle: sharedWithYou,
),
Column(
mainAxisSize: MainAxisSize.min,
children: [
SectionOptions(
onTap: collections.incoming.isNotEmpty
? () {
unawaited(
routeToPage(
context,
CollectionListPage(
collections.incoming,
sectionType:
UISectionType.incomingCollections,
tag: "incoming",
appTitle: sharedWithYou,
),
),
);
}
: null,
Hero(tag: "incoming", child: sharedWithYou),
trailingWidget: collections.incoming.isNotEmpty
? IconButtonWidget(
icon: Icons.chevron_right,
iconButtonType: IconButtonType.secondary,
iconColor: colorTheme.blurStrokePressed,
)
: null,
),
const SizedBox(height: 2),
collections.incoming.isNotEmpty
? SizedBox(
height: sideOfThumbnail + 46,
child: ListView.builder(
padding: const EdgeInsets.symmetric(
horizontal: horizontalPadding / 2,
),
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.only(
right: horizontalPadding / 2,
),
child: AlbumRowItemWidget(
collections.incoming[index],
sideOfThumbnail,
tag: "incoming",
showFileCount: true,
),
);
}
: null,
Hero(tag: "incoming", child: sharedWithYou),
trailingWidget: collections.incoming.isNotEmpty
? IconButtonWidget(
icon: Icons.chevron_right,
iconButtonType: IconButtonType.secondary,
iconColor: colorTheme.blurStrokePressed,
)
: null,
),
const SizedBox(height: 2),
collections.incoming.isNotEmpty
? SizedBox(
height: maxThumbnailWidth + 24,
child: ListView.builder(
padding: const EdgeInsets.symmetric(
horizontal: 8,
),
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.only(right: 8.0),
child: AlbumRowItemWidget(
collections.incoming[index],
maxThumbnailWidth,
tag: "incoming",
showFileCount: false,
),
);
},
itemCount: collections.incoming.length,
),
)
: const IncomingAlbumEmptyState(),
],
),
},
itemCount: collections.incoming.length,
),
)
: const IncomingAlbumEmptyState(),
],
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SectionOptions(
onTap: collections.outgoing.isNotEmpty
? () {
unawaited(
routeToPage(
context,
CollectionListPage(
collections.outgoing,
sectionType:
UISectionType.outgoingCollections,
tag: "outgoing",
appTitle: sharedByYou,
),
Column(
mainAxisSize: MainAxisSize.min,
children: [
SectionOptions(
onTap: collections.outgoing.isNotEmpty
? () {
unawaited(
routeToPage(
context,
CollectionListPage(
collections.outgoing,
sectionType:
UISectionType.outgoingCollections,
tag: "outgoing",
appTitle: sharedByYou,
),
),
);
}
: null,
Hero(tag: "outgoing", child: sharedByYou),
trailingWidget: collections.outgoing.isNotEmpty
? IconButtonWidget(
icon: Icons.chevron_right,
iconButtonType: IconButtonType.secondary,
iconColor: colorTheme.blurStrokePressed,
)
: null,
),
const SizedBox(height: 2),
collections.outgoing.isNotEmpty
? SizedBox(
height: sideOfThumbnail + 46,
child: ListView.builder(
padding: const EdgeInsets.symmetric(
horizontal: 8,
),
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.only(
right: horizontalPadding / 2,
),
child: AlbumRowItemWidget(
collections.outgoing[index],
sideOfThumbnail,
tag: "outgoing",
showFileCount: true,
),
);
}
: null,
Hero(tag: "outgoing", child: sharedByYou),
trailingWidget: collections.outgoing.isNotEmpty
? IconButtonWidget(
icon: Icons.chevron_right,
iconButtonType: IconButtonType.secondary,
iconColor: colorTheme.blurStrokePressed,
)
: null,
),
const SizedBox(height: 2),
collections.outgoing.isNotEmpty
? SizedBox(
height: maxThumbnailWidth + 24,
child: ListView.builder(
padding: const EdgeInsets.symmetric(
horizontal: 8,
),
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.only(right: 8.0),
child: AlbumRowItemWidget(
collections.outgoing[index],
maxThumbnailWidth,
tag: "outgoing",
showFileCount: false,
),
);
},
itemCount: collections.outgoing.length,
),
)
: const OutgoingAlbumEmptyState(),
],
),
},
itemCount: collections.outgoing.length,
),
)
: const OutgoingAlbumEmptyState(),
],
),
numberOfQuickLinks > 0
? Padding(
padding: const EdgeInsets.symmetric(
vertical: 8.0,
),
child: Column(
children: [
SectionOptions(
onTap: numberOfQuickLinks > maxQuickLinks
? () {
unawaited(
routeToPage(
context,
AllQuickLinksPage(
titleHeroTag: quickLinkTitleHeroTag,
quickLinks: collections.quickLinks,
),
),
);
}
: null,
Hero(
tag: quickLinkTitleHeroTag,
child: SectionTitle(
title: S.of(context).quickLinks,
),
),
trailingWidget: numberOfQuickLinks > maxQuickLinks
? IconButtonWidget(
icon: Icons.chevron_right,
iconButtonType: IconButtonType.secondary,
iconColor: colorTheme.blurStrokePressed,
)
: null,
),
const SizedBox(height: 2),
ListView.separated(
shrinkWrap: true,
padding: const EdgeInsets.only(
bottom: 12,
left: 12,
right: 12,
),
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
return GestureDetector(
onTap: () async {
final thumbnail = await CollectionsService
.instance
.getCover(collections.quickLinks[index]);
final page = CollectionPage(
CollectionWithThumbnail(
collections.quickLinks[index],
thumbnail,
? Column(
children: [
SectionOptions(
onTap: numberOfQuickLinks > maxQuickLinks
? () {
unawaited(
routeToPage(
context,
AllQuickLinksPage(
titleHeroTag: quickLinkTitleHeroTag,
quickLinks: collections.quickLinks,
),
tagPrefix: heroTagPrefix,
);
// ignore: unawaited_futures
routeToPage(context, page);
},
child: QuickLinkAlbumItem(
c: collections.quickLinks[index],
),
);
},
separatorBuilder: (context, index) {
return const SizedBox(height: 4);
},
itemCount: min(numberOfQuickLinks, maxQuickLinks),
),
);
}
: null,
Hero(
tag: quickLinkTitleHeroTag,
child: SectionTitle(
title: S.of(context).quickLinks,
),
],
),
trailingWidget: numberOfQuickLinks > maxQuickLinks
? IconButtonWidget(
icon: Icons.chevron_right,
iconButtonType: IconButtonType.secondary,
iconColor: colorTheme.blurStrokePressed,
)
: null,
),
)
const SizedBox(height: 2),
ListView.separated(
shrinkWrap: true,
padding: const EdgeInsets.only(
bottom: 12,
left: 12,
right: 12,
),
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
return GestureDetector(
onTap: () async {
final thumbnail = await CollectionsService
.instance
.getCover(collections.quickLinks[index]);
final page = CollectionPage(
CollectionWithThumbnail(
collections.quickLinks[index],
thumbnail,
),
tagPrefix: heroTagPrefix,
);
// ignore: unawaited_futures
routeToPage(context, page);
},
child: QuickLinkAlbumItem(
c: collections.quickLinks[index],
),
);
},
separatorBuilder: (context, index) {
return const SizedBox(height: 4);
},
itemCount: min(numberOfQuickLinks, maxQuickLinks),
),
],
)
: const SizedBox.shrink(),
const SizedBox(height: 2),
ValueListenableBuilder(
@@ -368,7 +370,6 @@ class _SharedCollectionsTabState extends State<SharedCollectionsTab>
: const EnteLoadingWidget();
},
),
const SizedBox(height: 4),
const CollectPhotosCardWidget(),
const SizedBox(height: 32),
],

View File

@@ -7,6 +7,7 @@ import 'package:photos/models/collection/collection.dart';
import 'package:photos/models/file/file.dart';
import 'package:photos/services/collections_service.dart';
import "package:photos/services/sync/remote_sync_service.dart";
import "package:photos/theme/ente_theme.dart";
import 'package:photos/ui/components/action_sheet_widget.dart';
import 'package:photos/ui/components/buttons/button_widget.dart';
import 'package:photos/ui/components/models/button_type.dart';
@@ -59,16 +60,33 @@ class _DeleteEmptyAlbumsState extends State<DeleteEmptyAlbums> {
}
Widget _buildDeleteButton() {
final colorScheme = getEnteColorScheme(context);
final textTheme = getEnteTextTheme(context);
return Padding(
padding: const EdgeInsets.fromLTRB(8.5, 4, 8, 12),
child: Align(
alignment: Alignment.centerLeft,
child: ButtonWidget(
buttonSize: ButtonSize.small,
buttonType: ButtonType.secondary,
labelText: S.of(context).deleteEmptyAlbums,
icon: Icons.delete_sweep_outlined,
shouldSurfaceExecutionStates: false,
child: GestureDetector(
child: Container(
decoration: BoxDecoration(
color: colorScheme.fillFaint,
borderRadius: const BorderRadius.all(Radius.circular(4)),
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.delete_sweep_outlined, size: 18),
const SizedBox(width: 8),
Text(
S.of(context).deleteEmptyAlbums,
style: textTheme.smallBold,
),
],
),
),
),
onTap: () async {
await showActionSheet(
context: context,

View File

@@ -3,14 +3,19 @@ import 'package:photos/theme/ente_theme.dart';
class NoThumbnailWidget extends StatelessWidget {
final bool addBorder;
const NoThumbnailWidget({this.addBorder = true, super.key});
final double borderRadius;
const NoThumbnailWidget({
this.addBorder = true,
this.borderRadius = 1,
super.key,
});
@override
Widget build(BuildContext context) {
final enteColorScheme = getEnteColorScheme(context);
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(1),
borderRadius: BorderRadius.circular(borderRadius),
border: addBorder
? Border.all(
color: enteColorScheme.strokeFaint,

View File

@@ -129,8 +129,7 @@ class _AddLocationSheetState extends State<AddLocationSheet> {
children: [
Expanded(
child: TextInputWidget(
hintText: S.of(context).locationName,
borderRadius: 2,
hintText: S.of(context).locationName,
focusNode: _focusNode,
submitNotifier: _submitNotifer,
cancelNotifier: _cancelNotifier,

View File

@@ -119,8 +119,7 @@ class _EditLocationSheetState extends State<EditLocationSheet> {
children: [
Expanded(
child: TextInputWidget(
hintText: S.of(context).locationName,
borderRadius: 2,
hintText: S.of(context).locationName,
focusNode: _focusNode,
submitNotifier: _submitNotifer,
cancelNotifier: _cancelNotifier,

View File

@@ -32,7 +32,7 @@ class SearchThumbnailWidget extends StatelessWidget {
height: 60,
width: 60,
child: ClipRRect(
borderRadius: const BorderRadius.horizontal(left: Radius.circular(4)),
borderRadius: BorderRadius.circular(4),
child: file != null
? (searchResult != null &&
searchResult!.type() == ResultType.faces)

View File

@@ -64,7 +64,7 @@ class SearchableItemWidget extends StatelessWidget {
decoration: BoxDecoration(
border: Border.all(color: colorScheme.strokeFainter),
borderRadius: const BorderRadius.all(
Radius.circular(4),
Radius.circular(6),
),
),
child: Row(

View File

@@ -1,7 +1,6 @@
import "dart:async";
import "package:dotted_border/dotted_border.dart";
import "package:figma_squircle/figma_squircle.dart";
import "package:flutter/material.dart";
import "package:photos/core/constants.dart";
import "package:photos/events/event.dart";
@@ -156,9 +155,8 @@ class AlbumRecommendation extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipSmoothRect(
radius:
SmoothBorderRadius(cornerRadius: 2.35, cornerSmoothing: 1),
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: SizedBox(
width: _width,
height: 100,
@@ -171,10 +169,12 @@ class AlbumRecommendation extends StatelessWidget {
shouldShowSyncStatus: false,
),
)
: const NoThumbnailWidget(),
: const NoThumbnailWidget(
borderRadius: 8,
),
),
),
const SizedBox(height: 2),
const SizedBox(height: 6),
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: _width),
child: Column(
@@ -187,7 +187,7 @@ class AlbumRecommendation extends StatelessWidget {
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 3),
const SizedBox(height: 2),
FutureBuilder(
future: CollectionsService.instance.getFileCount(
albumSearchResult.collectionWithThumbnail.collection,
@@ -198,7 +198,7 @@ class AlbumRecommendation extends StatelessWidget {
snapshot.data != 0) {
return Text(
snapshot.data.toString(),
style: enteTextTheme.smallMuted,
style: enteTextTheme.miniMuted,
);
} else {
return const SizedBox.shrink();
@@ -228,20 +228,28 @@ class AlbumCTA extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DottedBorder(
borderType: BorderType.RRect,
strokeWidth: 1.5,
borderPadding: const EdgeInsets.all(0.75),
dashPattern: const [3.75, 3.75],
radius: const Radius.circular(2.35),
padding: EdgeInsets.zero,
color: enteColorScheme.strokeFaint,
child: SizedBox(
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
color: Theme.of(context).brightness == Brightness.light
? enteColorScheme.backdropBase
: enteColorScheme.backdropFaint,
height: 100,
width: 100,
child: Icon(
Icons.add,
child: DottedBorder(
borderType: BorderType.RRect,
strokeWidth: 1.5,
borderPadding: const EdgeInsets.all(0.75),
dashPattern: const [3.75, 3.75],
radius: const Radius.circular(8),
padding: EdgeInsets.zero,
color: enteColorScheme.strokeFaint,
child: Center(
child: Icon(
Icons.add,
color: enteColorScheme.strokeFaint,
),
),
),
),
),