Redesigned Discovery Tab (#1755)

This commit is contained in:
Vishnu Mohandas 2024-02-29 23:46:37 +05:30 committed by GitHub
commit ef6890dcdb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
62 changed files with 1983 additions and 334 deletions

BIN
assets/2.0x/map_world.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
assets/2.0x/type_AVI.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
assets/2.0x/type_GIF.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
assets/2.0x/type_HEIC.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
assets/2.0x/type_JPEG.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
assets/2.0x/type_JPG.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
assets/2.0x/type_MKV.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
assets/2.0x/type_MP4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
assets/2.0x/type_PNG.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
assets/2.0x/type_WEBP.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
assets/2.0x/type_live.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
assets/2.0x/type_photos.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
assets/2.0x/type_videos.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
assets/3.0x/map_world.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

BIN
assets/3.0x/type_AVI.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
assets/3.0x/type_GIF.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
assets/3.0x/type_HEIC.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
assets/3.0x/type_JPEG.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
assets/3.0x/type_JPG.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
assets/3.0x/type_MKV.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
assets/3.0x/type_MP4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
assets/3.0x/type_PNG.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
assets/3.0x/type_WEBP.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
assets/3.0x/type_live.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
assets/3.0x/type_photos.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
assets/3.0x/type_videos.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
assets/earth_blurred.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
assets/map_world.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
assets/type_AVI.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
assets/type_GIF.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

BIN
assets/type_HEIC.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

BIN
assets/type_JPEG.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

BIN
assets/type_JPG.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
assets/type_MKV.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
assets/type_MP4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

BIN
assets/type_PNG.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
assets/type_WEBP.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

BIN
assets/type_live.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

BIN
assets/type_photos.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
assets/type_unknown.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
assets/type_videos.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View file

@ -65,7 +65,7 @@ const defaultCityRadius = 10.0;
const galleryGridSpacing = 2.0;
const searchSectionLimit = 7;
const kSearchSectionLimit = 7;
const blackThumbnailBase64 = '/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEB' +
'AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQ' +

View file

@ -12,7 +12,7 @@ class RecentSearches with ChangeNotifier {
void add(String query) {
searches.add(query);
while (searches.length > searchSectionLimit) {
while (searches.length > kSearchSectionLimit) {
searches.remove(searches.first);
}
//buffer for not surfacing a new recent search before going to the next

View file

@ -43,10 +43,10 @@ enum SectionType {
content,
// includes year, month , day, event ResultType
moment,
// People section shows the files shared by other persons
contacts,
fileCaption,
album,
// People section shows the files shared by other persons
fileCaption,
contacts,
fileTypesAndExtension,
}

View file

@ -84,7 +84,7 @@ class _AllSectionsExamplesProviderState
continue;
}
allSectionsExamples.add(
sectionType.getData(context, limit: searchSectionLimit),
sectionType.getData(context, limit: kSearchSectionLimit),
);
}
allSectionsExamplesFuture =

View file

@ -47,13 +47,13 @@ import 'package:photos/ui/home/landing_page_widget.dart';
import "package:photos/ui/home/loading_photos_widget.dart";
import 'package:photos/ui/home/start_backup_hook_widget.dart';
import 'package:photos/ui/notification/update/change_log_page.dart';
import "package:photos/ui/search_tab.dart";
import 'package:photos/ui/settings/app_update_dialog.dart';
import "package:photos/ui/settings/app_update_dialog.dart";
import "package:photos/ui/settings_page.dart";
import "package:photos/ui/tabs/shared_collections_tab.dart";
import "package:photos/ui/tabs/user_collections_tab.dart";
import "package:photos/ui/viewer/gallery/collection_page.dart";
import 'package:photos/ui/viewer/search/search_widget.dart';
import "package:photos/ui/viewer/search/search_widget.dart";
import 'package:photos/ui/viewer/search_tab/search_tab.dart';
import 'package:photos/utils/dialog_util.dart';
import "package:photos/utils/navigation_util.dart";
import 'package:receive_sharing_intent/receive_sharing_intent.dart';

View file

@ -33,6 +33,7 @@ class ThumbnailWidget extends StatefulWidget {
final Duration? serverLoadDeferDuration;
final int thumbnailSize;
final bool shouldShowOwnerAvatar;
final bool shouldShowFavoriteIcon;
ThumbnailWidget(
this.file, {
@ -47,6 +48,7 @@ class ThumbnailWidget extends StatefulWidget {
this.diskLoadDeferDuration,
this.serverLoadDeferDuration,
this.thumbnailSize = thumbnailSmallSize,
this.shouldShowFavoriteIcon = true,
}) : super(key: key ?? Key(file.tag));
@override
@ -128,12 +130,15 @@ class _ThumbnailWidgetState extends State<ThumbnailWidget> {
Widget? content;
if (image != null) {
final List<Widget> contentChildren = [image];
if (FavoritesService.instance.isFavoriteCache(
widget.file,
checkOnlyAlbum: widget.showFavForAlbumOnly,
)) {
contentChildren.add(const FavoriteOverlayIcon());
if (widget.shouldShowFavoriteIcon) {
if (FavoritesService.instance.isFavoriteCache(
widget.file,
checkOnlyAlbum: widget.showFavForAlbumOnly,
)) {
contentChildren.add(const FavoriteOverlayIcon());
}
}
if (widget.file.fileType == FileType.video) {
contentChildren.add(const VideoOverlayIcon());
} else if (widget.shouldShowLivePhotoOverlay &&
@ -183,6 +188,7 @@ class _ThumbnailWidgetState extends State<ThumbnailWidget> {
}
return Stack(
clipBehavior: Clip.none,
fit: StackFit.expand,
children: viewChildren,
);

View file

@ -5,19 +5,12 @@ import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/map/enable_map.dart";
import "package:photos/ui/map/map_screen.dart";
class GoToMapWidget extends StatelessWidget {
const GoToMapWidget({super.key});
//Used for empty state of location section
class GoToMap extends StatelessWidget {
const GoToMap({super.key});
@override
Widget build(BuildContext context) {
final textScaleFactor = MediaQuery.textScaleFactorOf(context);
late final double width;
if (textScaleFactor <= 1.0) {
width = 85.0;
} else {
width = 85.0 + ((textScaleFactor - 1.0) * 64);
}
return GestureDetector(
onTap: () async {
final bool result = await requestForMapEnable(context);
@ -32,30 +25,31 @@ class GoToMapWidget extends StatelessWidget {
);
}
},
child: SizedBox(
width: width,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 10),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset(
"assets/map.png",
child: Padding(
padding: const EdgeInsets.fromLTRB(8, 14, 8, 0),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Transform.scale(
scale: 1.2,
child: Image.asset(
"assets/map_world.png",
width: 64,
height: 64,
),
const SizedBox(
height: 10,
),
Text(
S.of(context).yourMap,
maxLines: 2,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
style: getEnteTextTheme(context).mini,
),
],
),
),
const SizedBox(
height: 11.5,
),
Text(
S.of(context).yourMap,
maxLines: 2,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
style: getEnteTextTheme(context).mini,
),
],
),
),
);

View file

@ -1,278 +0,0 @@
import "dart:async";
import "package:collection/collection.dart";
import "package:flutter/material.dart";
import "package:photos/core/constants.dart";
import "package:photos/events/event.dart";
import "package:photos/models/search/album_search_result.dart";
import "package:photos/models/search/generic_search_result.dart";
import "package:photos/models/search/recent_searches.dart";
import "package:photos/models/search/search_result.dart";
import "package:photos/models/search/search_types.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/viewer/file/no_thumbnail_widget.dart";
import "package:photos/ui/viewer/file/thumbnail_widget.dart";
import "package:photos/ui/viewer/gallery/collection_page.dart";
import "package:photos/ui/viewer/search/result/go_to_map_widget.dart";
import "package:photos/ui/viewer/search/result/search_result_page.dart";
import 'package:photos/ui/viewer/search/result/search_section_all_page.dart';
import "package:photos/ui/viewer/search/search_section_cta.dart";
import "package:photos/utils/navigation_util.dart";
class SearchSection extends StatefulWidget {
final SectionType sectionType;
final List<SearchResult> examples;
final int limit;
const SearchSection({
Key? key,
required this.sectionType,
required this.examples,
required this.limit,
}) : super(key: key);
@override
State<SearchSection> createState() => _SearchSectionState();
}
class _SearchSectionState extends State<SearchSection> {
late List<SearchResult> _examples;
final streamSubscriptions = <StreamSubscription>[];
@override
void initState() {
super.initState();
_examples = widget.examples;
final streamsToListenTo = widget.sectionType.sectionUpdateEvents();
for (Stream<Event> stream in streamsToListenTo) {
streamSubscriptions.add(
stream.listen((event) async {
_examples = await widget.sectionType.getData(
context,
limit: searchSectionLimit,
);
setState(() {});
}),
);
}
}
@override
void dispose() {
for (var subscriptions in streamSubscriptions) {
subscriptions.cancel();
}
super.dispose();
}
@override
void didUpdateWidget(covariant SearchSection oldWidget) {
super.didUpdateWidget(oldWidget);
_examples = widget.examples;
}
@override
Widget build(BuildContext context) {
debugPrint("Building section for ${widget.sectionType.name}");
final shouldShowMore = _examples.length >= widget.limit - 1;
final textTheme = getEnteTextTheme(context);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: _examples.isNotEmpty
? GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (shouldShowMore) {
routeToPage(
context,
SearchSectionAllPage(
sectionType: widget.sectionType,
),
);
}
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.all(12),
child: Text(
widget.sectionType.sectionTitle(context),
style: textTheme.largeBold,
),
),
shouldShowMore
? Padding(
padding: const EdgeInsets.all(12),
child: Icon(
Icons.chevron_right_outlined,
color: getEnteColorScheme(context).strokeMuted,
),
)
: const SizedBox.shrink(),
],
),
const SizedBox(height: 2),
SearchExampleRow(_examples, widget.sectionType),
],
),
)
: Padding(
padding: const EdgeInsets.only(left: 16, right: 8),
child: Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.sectionType.sectionTitle(context),
style: textTheme.largeBold,
),
const SizedBox(height: 24),
Text(
widget.sectionType.getEmptyStateText(context),
style: textTheme.smallMuted,
),
],
),
),
),
const SizedBox(width: 8),
SearchSectionEmptyCTAIcon(widget.sectionType),
widget.sectionType == SectionType.location
? const Padding(
padding: EdgeInsets.fromLTRB(8, 24, 8, 0),
child: GoToMapWidget(),
)
: const SizedBox.shrink(),
],
),
),
);
}
}
class SearchExampleRow extends StatelessWidget {
final SectionType sectionType;
final List<SearchResult> examples;
const SearchExampleRow(this.examples, this.sectionType, {super.key});
@override
Widget build(BuildContext context) {
//Cannot use listView.builder here
final scrollableExamples = <Widget>[];
if (sectionType == SectionType.location) {
scrollableExamples.add(const GoToMapWidget());
}
examples.forEachIndexed((index, element) {
scrollableExamples.add(
SearchExample(
searchResult: examples.elementAt(index),
),
);
});
scrollableExamples.add(SearchSectionCTAIcon(sectionType));
return SizedBox(
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
scrollDirection: Axis.horizontal,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: scrollableExamples,
),
),
);
}
}
class SearchExample extends StatelessWidget {
final SearchResult searchResult;
const SearchExample({required this.searchResult, super.key});
@override
Widget build(BuildContext context) {
final textScaleFactor = MediaQuery.textScaleFactorOf(context);
late final double width;
if (textScaleFactor <= 1.0) {
width = 85.0;
} else {
width = 85.0 + ((textScaleFactor - 1.0) * 64);
}
final heroTag =
searchResult.heroTag() + (searchResult.previewThumbnail()?.tag ?? "");
return GestureDetector(
onTap: () {
RecentSearches().add(searchResult.name());
if (searchResult is GenericSearchResult) {
final genericSearchResult = searchResult as GenericSearchResult;
if (genericSearchResult.onResultTap != null) {
genericSearchResult.onResultTap!(context);
} else {
routeToPage(
context,
SearchResultPage(searchResult),
);
}
} else if (searchResult is AlbumSearchResult) {
final albumSearchResult = searchResult as AlbumSearchResult;
routeToPage(
context,
CollectionPage(
albumSearchResult.collectionWithThumbnail,
tagPrefix: albumSearchResult.heroTag(),
),
);
}
},
child: SizedBox(
width: width,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 10),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 64,
height: 64,
child: searchResult.previewThumbnail() != null
? Hero(
tag: heroTag,
child: ClipOval(
child: ThumbnailWidget(
searchResult.previewThumbnail()!,
shouldShowSyncStatus: false,
),
),
)
: const ClipOval(
child: NoThumbnailWidget(
addBorder: false,
),
),
),
const SizedBox(
height: 10,
),
Text(
searchResult.name(),
maxLines: 2,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
style: getEnteTextTheme(context).mini,
),
],
),
),
),
);
}
}

View file

@ -72,7 +72,7 @@ class SearchSectionEmptyCTAIcon extends StatelessWidget {
return GestureDetector(
onTap: sectionType.ctaOnTap(context),
child: Padding(
padding: const EdgeInsets.fromLTRB(8, 24, 8, 0),
padding: const EdgeInsets.fromLTRB(8, 14, 8, 0),
child: Column(
children: [
DottedBorder(

View file

@ -0,0 +1,254 @@
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";
import "package:photos/generated/l10n.dart";
import "package:photos/models/search/album_search_result.dart";
import "package:photos/models/search/recent_searches.dart";
import "package:photos/models/search/search_types.dart";
import "package:photos/services/collections_service.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/viewer/file/no_thumbnail_widget.dart";
import "package:photos/ui/viewer/file/thumbnail_widget.dart";
import "package:photos/ui/viewer/gallery/collection_page.dart";
import "package:photos/ui/viewer/search/search_section_cta.dart";
import "package:photos/ui/viewer/search_tab/section_header.dart";
import "package:photos/utils/navigation_util.dart";
class AlbumsSection extends StatefulWidget {
final List<AlbumSearchResult> albumSearchResults;
const AlbumsSection(this.albumSearchResults, {super.key});
@override
State<AlbumsSection> createState() => _AlbumsSectionState();
}
class _AlbumsSectionState extends State<AlbumsSection> {
late List<AlbumSearchResult> _albumSearchResults;
final streamSubscriptions = <StreamSubscription>[];
@override
void initState() {
super.initState();
_albumSearchResults = widget.albumSearchResults;
final streamsToListenTo = SectionType.album.sectionUpdateEvents();
for (Stream<Event> stream in streamsToListenTo) {
streamSubscriptions.add(
stream.listen((event) async {
_albumSearchResults = (await SectionType.album.getData(
context,
limit: kSearchSectionLimit,
)) as List<AlbumSearchResult>;
setState(() {});
}),
);
}
}
@override
void dispose() {
for (var subscriptions in streamSubscriptions) {
subscriptions.cancel();
}
super.dispose();
}
@override
void didUpdateWidget(covariant AlbumsSection oldWidget) {
super.didUpdateWidget(oldWidget);
_albumSearchResults = widget.albumSearchResults;
}
@override
Widget build(BuildContext context) {
if (_albumSearchResults.isEmpty) {
final textTheme = getEnteTextTheme(context);
return Padding(
padding: const EdgeInsets.only(left: 12, right: 8),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
SectionType.album.sectionTitle(context),
style: textTheme.largeBold,
),
const SizedBox(height: 24),
Padding(
padding: const EdgeInsets.only(left: 4),
child: Text(
SectionType.album.getEmptyStateText(context),
style: textTheme.smallMuted,
),
),
],
),
),
const SizedBox(width: 8),
const SearchSectionEmptyCTAIcon(SectionType.album),
],
),
);
} else {
final recommendations = <Widget>[
..._albumSearchResults.map(
(albumSearchResult) => AlbumRecommendation(albumSearchResult),
),
const AlbumCTA(),
];
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SectionHeader(
SectionType.album,
hasMore: (_albumSearchResults.length >= kSearchSectionLimit - 1),
),
const SizedBox(height: 2),
SizedBox(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 4.5),
physics: const BouncingScrollPhysics(),
scrollDirection: Axis.horizontal,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: recommendations,
),
),
),
],
),
);
}
}
}
class AlbumRecommendation extends StatelessWidget {
final AlbumSearchResult albumSearchResult;
const AlbumRecommendation(this.albumSearchResult, {super.key});
@override
Widget build(BuildContext context) {
final heroTag = albumSearchResult.heroTag() +
(albumSearchResult.previewThumbnail()?.tag ?? "");
final enteTextTheme = getEnteTextTheme(context);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 2.5),
child: GestureDetector(
onTap: () {
RecentSearches().add(albumSearchResult.name());
routeToPage(
context,
CollectionPage(
albumSearchResult.collectionWithThumbnail,
tagPrefix: albumSearchResult.heroTag(),
),
);
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipSmoothRect(
radius:
SmoothBorderRadius(cornerRadius: 2.35, cornerSmoothing: 1),
child: SizedBox(
width: 100,
height: 100,
child: albumSearchResult.previewThumbnail() != null
? Hero(
tag: heroTag,
child: ThumbnailWidget(
albumSearchResult.previewThumbnail()!,
shouldShowArchiveStatus: false,
shouldShowSyncStatus: false,
),
)
: const NoThumbnailWidget(),
),
),
const SizedBox(height: 2),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
albumSearchResult.name(),
style: enteTextTheme.small,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 3),
FutureBuilder(
future: CollectionsService.instance.getFileCount(
albumSearchResult.collectionWithThumbnail.collection,
),
builder: (context, snapshot) {
if (snapshot.hasData &&
snapshot.data != null &&
snapshot.data != 0) {
return Text(
snapshot.data.toString(),
style: enteTextTheme.smallMuted,
);
} else {
return const SizedBox.shrink();
}
},
),
],
),
],
),
),
);
}
}
class AlbumCTA extends StatelessWidget {
const AlbumCTA({super.key});
@override
Widget build(BuildContext context) {
final enteColorScheme = getEnteColorScheme(context);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 2.5),
child: GestureDetector(
onTap: SectionType.album.ctaOnTap(context),
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(
height: 100,
width: 100,
child: Icon(
Icons.add,
color: enteColorScheme.strokeFaint,
),
),
),
const SizedBox(height: 2),
Text(
S.of(context).addNew,
style: getEnteTextTheme(context).smallFaint,
),
],
),
),
);
}
}

View file

@ -0,0 +1,262 @@
import "dart:async";
import "package:dotted_border/dotted_border.dart";
import "package:flutter/material.dart";
import "package:photos/core/constants.dart";
import "package:photos/events/event.dart";
import "package:photos/generated/l10n.dart";
import "package:photos/models/search/generic_search_result.dart";
import "package:photos/models/search/recent_searches.dart";
import "package:photos/models/search/search_types.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/viewer/file/no_thumbnail_widget.dart";
import "package:photos/ui/viewer/file/thumbnail_widget.dart";
import "package:photos/ui/viewer/search/result/search_result_page.dart";
import "package:photos/ui/viewer/search/search_section_cta.dart";
import "package:photos/ui/viewer/search_tab/section_header.dart";
import "package:photos/utils/navigation_util.dart";
class ContactsSection extends StatefulWidget {
final List<GenericSearchResult> contactSearchResults;
const ContactsSection(this.contactSearchResults, {super.key});
@override
State<ContactsSection> createState() => _ContactsSectionState();
}
class _ContactsSectionState extends State<ContactsSection> {
late List<GenericSearchResult> _contactSearchResults;
final streamSubscriptions = <StreamSubscription>[];
@override
void initState() {
super.initState();
_contactSearchResults = widget.contactSearchResults;
final streamsToListenTo = SectionType.contacts.sectionUpdateEvents();
for (Stream<Event> stream in streamsToListenTo) {
streamSubscriptions.add(
stream.listen((event) async {
_contactSearchResults = (await SectionType.contacts.getData(
context,
limit: kSearchSectionLimit,
)) as List<GenericSearchResult>;
setState(() {});
}),
);
}
}
@override
void dispose() {
for (var subscriptions in streamSubscriptions) {
subscriptions.cancel();
}
super.dispose();
}
@override
void didUpdateWidget(covariant ContactsSection oldWidget) {
super.didUpdateWidget(oldWidget);
_contactSearchResults = widget.contactSearchResults;
}
@override
Widget build(BuildContext context) {
if (_contactSearchResults.isEmpty) {
final textTheme = getEnteTextTheme(context);
return Padding(
padding: const EdgeInsets.only(left: 12, right: 8),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
SectionType.contacts.sectionTitle(context),
style: textTheme.largeBold,
),
const SizedBox(height: 24),
Padding(
padding: const EdgeInsets.only(left: 4),
child: Text(
SectionType.contacts.getEmptyStateText(context),
style: textTheme.smallMuted,
),
),
],
),
),
const SizedBox(width: 8),
const SearchSectionEmptyCTAIcon(SectionType.contacts),
],
),
);
} else {
final recommendations = <Widget>[
..._contactSearchResults.map(
(contactSearchResult) => ContactRecommendation(contactSearchResult),
),
const ContactCTA(),
];
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SectionHeader(
SectionType.contacts,
hasMore:
(_contactSearchResults.length >= kSearchSectionLimit - 1),
),
const SizedBox(height: 2),
SizedBox(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 4.5),
physics: const BouncingScrollPhysics(),
scrollDirection: Axis.horizontal,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: recommendations,
),
),
),
],
),
);
}
}
}
class ContactRecommendation extends StatelessWidget {
final GenericSearchResult contactSearchResult;
const ContactRecommendation(this.contactSearchResult, {super.key});
@override
Widget build(BuildContext context) {
final heroTag = contactSearchResult.heroTag() +
(contactSearchResult.previewThumbnail()?.tag ?? "");
final enteTextTheme = getEnteTextTheme(context);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 2.5),
child: GestureDetector(
onTap: () {
RecentSearches().add(contactSearchResult.name());
if (contactSearchResult.onResultTap != null) {
contactSearchResult.onResultTap!(context);
} else {
routeToPage(
context,
SearchResultPage(contactSearchResult),
);
}
},
child: ConstrainedBox(
constraints: const BoxConstraints(
maxHeight: double.infinity,
minHeight: 115.5,
maxWidth: 100,
minWidth: 100,
),
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 4.25, vertical: 10.5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
ClipOval(
child: SizedBox(
width: 67.75,
height: 67.75,
child: contactSearchResult.previewThumbnail() != null
? Hero(
tag: heroTag,
child: ThumbnailWidget(
contactSearchResult.previewThumbnail()!,
shouldShowArchiveStatus: false,
shouldShowSyncStatus: false,
),
)
: const NoThumbnailWidget(),
),
),
const SizedBox(height: 10.5),
SizedBox(
width: 91.5,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
contactSearchResult.name(),
style: enteTextTheme.small,
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
],
),
),
],
),
),
),
),
);
}
}
class ContactCTA extends StatelessWidget {
const ContactCTA({super.key});
@override
Widget build(BuildContext context) {
final enteColorScheme = getEnteColorScheme(context);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 2.5),
child: GestureDetector(
onTap: SectionType.contacts.ctaOnTap(context),
child: ConstrainedBox(
constraints: const BoxConstraints(
maxHeight: double.infinity,
minHeight: 115.5,
maxWidth: 100,
minWidth: 100,
),
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 4.25, vertical: 10.5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
DottedBorder(
borderType: BorderType.Circle,
strokeWidth: 1.5,
borderPadding: const EdgeInsets.all(0.75),
dashPattern: const [4, 4],
radius: const Radius.circular(2.35),
padding: EdgeInsets.zero,
color: enteColorScheme.strokeFaint,
child: SizedBox(
height: 67.75,
width: 67.75,
child: Icon(
Icons.adaptive.share,
color: enteColorScheme.strokeFaint,
),
),
),
const SizedBox(height: 10.5),
Text(
S.of(context).invite,
style: getEnteTextTheme(context).smallFaint,
),
],
),
),
),
),
);
}
}

View file

@ -0,0 +1,239 @@
import "dart:async";
import "package:figma_squircle/figma_squircle.dart";
import "package:flutter/material.dart";
import "package:photos/core/constants.dart";
import "package:photos/events/event.dart";
import "package:photos/models/search/generic_search_result.dart";
import "package:photos/models/search/recent_searches.dart";
import "package:photos/models/search/search_types.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/viewer/file/no_thumbnail_widget.dart";
import "package:photos/ui/viewer/file/thumbnail_widget.dart";
import "package:photos/ui/viewer/search/result/search_result_page.dart";
import "package:photos/ui/viewer/search/search_section_cta.dart";
import "package:photos/ui/viewer/search_tab/section_header.dart";
import "package:photos/utils/navigation_util.dart";
class DescriptionsSection extends StatefulWidget {
final List<GenericSearchResult> descriptionsSearchResults;
const DescriptionsSection(this.descriptionsSearchResults, {super.key});
@override
State<DescriptionsSection> createState() => _DescriptionsSectionState();
}
class _DescriptionsSectionState extends State<DescriptionsSection> {
late List<GenericSearchResult> _descriptionsSearchResults;
final streamSubscriptions = <StreamSubscription>[];
@override
void initState() {
super.initState();
_descriptionsSearchResults = widget.descriptionsSearchResults;
final streamsToListenTo = SectionType.fileCaption.sectionUpdateEvents();
for (Stream<Event> stream in streamsToListenTo) {
streamSubscriptions.add(
stream.listen((event) async {
_descriptionsSearchResults = (await SectionType.fileCaption.getData(
context,
limit: kSearchSectionLimit,
)) as List<GenericSearchResult>;
setState(() {});
}),
);
}
}
@override
void dispose() {
for (var subscriptions in streamSubscriptions) {
subscriptions.cancel();
}
super.dispose();
}
@override
void didUpdateWidget(covariant DescriptionsSection oldWidget) {
super.didUpdateWidget(oldWidget);
_descriptionsSearchResults = widget.descriptionsSearchResults;
}
@override
Widget build(BuildContext context) {
if (_descriptionsSearchResults.isEmpty) {
final textTheme = getEnteTextTheme(context);
return Padding(
padding: const EdgeInsets.only(left: 12, right: 8),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
SectionType.fileCaption.sectionTitle(context),
style: textTheme.largeBold,
),
const SizedBox(height: 24),
Padding(
padding: const EdgeInsets.only(left: 4),
child: Text(
SectionType.fileCaption.getEmptyStateText(context),
style: textTheme.smallMuted,
),
),
],
),
),
const SizedBox(width: 8),
const SearchSectionEmptyCTAIcon(SectionType.fileCaption),
],
),
);
} else {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SectionHeader(
SectionType.fileCaption,
hasMore: (_descriptionsSearchResults.length >=
kSearchSectionLimit - 1),
),
const SizedBox(height: 2),
SizedBox(
child: SingleChildScrollView(
clipBehavior: Clip.none,
padding: const EdgeInsets.symmetric(horizontal: 4.5),
physics: const BouncingScrollPhysics(),
scrollDirection: Axis.horizontal,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: _descriptionsSearchResults
.map(
(descriptionSearchResult) =>
DescriptionRecommendation(descriptionSearchResult),
)
.toList(),
),
),
),
],
),
);
}
}
}
class DescriptionRecommendation extends StatelessWidget {
final GenericSearchResult descriptionSearchResult;
const DescriptionRecommendation(this.descriptionSearchResult, {super.key});
@override
Widget build(BuildContext context) {
final heroTag = descriptionSearchResult.heroTag() +
(descriptionSearchResult.previewThumbnail()?.tag ?? "");
final enteTextTheme = getEnteTextTheme(context);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 2.5),
child: GestureDetector(
onTap: () {
RecentSearches().add(descriptionSearchResult.name());
if (descriptionSearchResult.onResultTap != null) {
descriptionSearchResult.onResultTap!(context);
} else {
routeToPage(
context,
SearchResultPage(descriptionSearchResult),
);
}
},
child: SizedBox(
width: 100,
height: 150,
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
Container(
decoration: const BoxDecoration(
boxShadow: [
BoxShadow(
blurRadius: 15,
offset: Offset(0, 7.5),
color: Color.fromRGBO(68, 68, 68, 0.1),
),
],
),
child: ClipSmoothRect(
radius: SmoothBorderRadius(
cornerRadius: 7.5,
cornerSmoothing: 1,
),
child: Container(
color: Theme.of(context).colorScheme.brightness ==
Brightness.light
? const Color(0xFFFFFFFF)
: const Color(0xFF181818),
child: Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(9, 9, 9, 0),
child: SizedBox(
width: 82,
height: 82,
child: descriptionSearchResult.previewThumbnail() !=
null
? Hero(
tag: heroTag,
child: ClipSmoothRect(
radius: SmoothBorderRadius(
cornerRadius: 7.5,
cornerSmoothing: 1,
),
child: ThumbnailWidget(
descriptionSearchResult
.previewThumbnail()!,
shouldShowArchiveStatus: false,
shouldShowSyncStatus: false,
),
),
)
: const NoThumbnailWidget(),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 5,
vertical: 10,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
descriptionSearchResult.name(),
style: enteTextTheme.smallMuted,
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
],
),
),
),
],
),
),
),
),
],
),
),
),
);
}
}

View file

@ -0,0 +1,215 @@
import "dart:async";
import "package:flutter/material.dart";
import "package:photos/core/constants.dart";
import "package:photos/events/event.dart";
import "package:photos/models/search/generic_search_result.dart";
import "package:photos/models/search/recent_searches.dart";
import "package:photos/models/search/search_types.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/viewer/search/result/search_result_page.dart";
import "package:photos/ui/viewer/search/search_section_cta.dart";
import "package:photos/ui/viewer/search_tab/section_header.dart";
import "package:photos/utils/navigation_util.dart";
class FileTypeSection extends StatefulWidget {
final List<GenericSearchResult> fileTypesSearchResults;
const FileTypeSection(this.fileTypesSearchResults, {super.key});
@override
State<FileTypeSection> createState() => _FileTypeSectionState();
}
class _FileTypeSectionState extends State<FileTypeSection> {
late List<GenericSearchResult> _fileTypesSearchResults;
final streamSubscriptions = <StreamSubscription>[];
@override
void initState() {
super.initState();
_fileTypesSearchResults = widget.fileTypesSearchResults;
final streamsToListenTo =
SectionType.fileTypesAndExtension.sectionUpdateEvents();
for (Stream<Event> stream in streamsToListenTo) {
streamSubscriptions.add(
stream.listen((event) async {
_fileTypesSearchResults =
(await SectionType.fileTypesAndExtension.getData(
context,
limit: kSearchSectionLimit,
)) as List<GenericSearchResult>;
setState(() {});
}),
);
}
}
@override
void dispose() {
for (var subscriptions in streamSubscriptions) {
subscriptions.cancel();
}
super.dispose();
}
@override
void didUpdateWidget(covariant FileTypeSection oldWidget) {
super.didUpdateWidget(oldWidget);
_fileTypesSearchResults = widget.fileTypesSearchResults;
}
@override
Widget build(BuildContext context) {
if (_fileTypesSearchResults.isEmpty) {
final textTheme = getEnteTextTheme(context);
return Padding(
padding: const EdgeInsets.only(left: 12, right: 8),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
SectionType.fileTypesAndExtension.sectionTitle(context),
style: textTheme.largeBold,
),
const SizedBox(height: 24),
Padding(
padding: const EdgeInsets.only(left: 4),
child: Text(
SectionType.fileTypesAndExtension
.getEmptyStateText(context),
style: textTheme.smallMuted,
),
),
],
),
),
const SizedBox(width: 8),
const SearchSectionEmptyCTAIcon(SectionType.fileTypesAndExtension),
],
),
);
} else {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SectionHeader(
SectionType.fileTypesAndExtension,
hasMore:
(_fileTypesSearchResults.length >= kSearchSectionLimit - 1),
),
const SizedBox(height: 2),
SizedBox(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 4.5),
physics: const BouncingScrollPhysics(),
scrollDirection: Axis.horizontal,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: _fileTypesSearchResults
.map(
(fileTypeSearchResult) =>
FileTypeRecommendation(fileTypeSearchResult),
)
.toList(),
),
),
),
],
),
);
}
}
}
class FileTypeRecommendation extends StatelessWidget {
static const knownTypesToAssetPath = {
"PHOTO": "assets/type_photos.png",
"VIDEO": "assets/type_videos.png",
"LIVE": "assets/type_live.png",
"AVI": "assets/type_AVI.png",
"GIF": "assets/type_GIF.png",
"HEIC": "assets/type_HEIC.png",
"JPEG": "assets/type_JPEG.png",
"JPG": "assets/type_JPG.png",
"MKV": "assets/type_MKV.png",
"MP4": "assets/type_MP4.png",
"PNG": "assets/type_PNG.png",
"WEBP": "assets/type_WEBP.png",
};
final GenericSearchResult fileTypeSearchResult;
const FileTypeRecommendation(this.fileTypeSearchResult, {super.key});
@override
Widget build(BuildContext context) {
final fileTypeKey =
fileTypeKeyFromSearchResult(fileTypeSearchResult.name.call());
final assetPath = knownTypesToAssetPath[fileTypeKey];
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 68),
child: GestureDetector(
onTap: () {
RecentSearches().add(fileTypeSearchResult.name());
if (fileTypeSearchResult.onResultTap != null) {
fileTypeSearchResult.onResultTap!(context);
} else {
routeToPage(
context,
SearchResultPage(fileTypeSearchResult),
);
}
},
child: assetPath != null
? Image.asset(assetPath)
: Stack(
alignment: Alignment.center,
children: [
Image.asset(
"assets/type_unknown.png",
),
Positioned(
bottom: 18,
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 48),
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
fileTypeKey,
style: const TextStyle(
fontSize: 14,
fontFamily: "Inter",
color: Colors.white,
letterSpacing: 0.75,
),
textAlign: TextAlign.center,
maxLines: 1,
),
),
),
),
],
),
),
),
);
}
String fileTypeKeyFromSearchResult(String name) {
String fileTypeKey = "";
//remove 's' at the end of string
if (RegExp(r's$').hasMatch(name)) {
fileTypeKey = name.substring(0, name.length - 1);
}
//use only 1st word if there exists multiple words
fileTypeKey = fileTypeKey.split(" ").first.toUpperCase();
return fileTypeKey;
}
}

View file

@ -0,0 +1,603 @@
import "dart:async";
import "dart:math";
import "dart:ui";
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";
import "package:photos/models/search/generic_search_result.dart";
import "package:photos/models/search/recent_searches.dart";
import "package:photos/models/search/search_types.dart";
import "package:photos/services/search_service.dart";
import "package:photos/theme/colors.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/map/enable_map.dart";
import "package:photos/ui/map/map_screen.dart";
import "package:photos/ui/viewer/file/no_thumbnail_widget.dart";
import "package:photos/ui/viewer/file/thumbnail_widget.dart";
import "package:photos/ui/viewer/search/result/go_to_map_widget.dart";
import "package:photos/ui/viewer/search/result/search_result_page.dart";
import "package:photos/ui/viewer/search/search_section_cta.dart";
import "package:photos/ui/viewer/search_tab/section_header.dart";
import "package:photos/utils/navigation_util.dart";
class LocationsSection extends StatefulWidget {
final List<GenericSearchResult> locationsSearchResults;
const LocationsSection(this.locationsSearchResults, {super.key});
@override
State<LocationsSection> createState() => _LocationsSectionState();
}
class _LocationsSectionState extends State<LocationsSection> {
late List<GenericSearchResult> _locationsSearchResults;
final streamSubscriptions = <StreamSubscription>[];
@override
void initState() {
super.initState();
_locationsSearchResults = widget.locationsSearchResults;
final streamsToListenTo = SectionType.location.sectionUpdateEvents();
for (Stream<Event> stream in streamsToListenTo) {
streamSubscriptions.add(
stream.listen((event) async {
_locationsSearchResults = (await SectionType.location.getData(
context,
limit: kSearchSectionLimit,
)) as List<GenericSearchResult>;
setState(() {});
}),
);
}
}
@override
void dispose() {
for (var subscriptions in streamSubscriptions) {
subscriptions.cancel();
}
super.dispose();
}
@override
void didUpdateWidget(covariant LocationsSection oldWidget) {
super.didUpdateWidget(oldWidget);
_locationsSearchResults = widget.locationsSearchResults;
}
@override
Widget build(BuildContext context) {
if (_locationsSearchResults.isEmpty) {
final textTheme = getEnteTextTheme(context);
return Padding(
padding: const EdgeInsets.only(left: 12, right: 8),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
SectionType.location.sectionTitle(context),
style: textTheme.largeBold,
),
const SizedBox(height: 24),
Padding(
padding: const EdgeInsets.only(left: 4),
child: Text(
SectionType.location.getEmptyStateText(context),
style: textTheme.smallMuted,
),
),
],
),
),
const SizedBox(width: 8),
const SearchSectionEmptyCTAIcon(SectionType.location),
const GoToMap(),
],
),
);
} else {
final recommendations = <Widget>[
const RepaintBoundary(child: GoToMapWithBG()),
..._locationsSearchResults.map(
(locationSearchResult) =>
LocationRecommendation(locationSearchResult),
),
const RepaintBoundary(child: LocationCTA()),
];
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SectionHeader(
SectionType.location,
hasMore:
(_locationsSearchResults.length >= kSearchSectionLimit - 1),
),
const SizedBox(height: 2),
SizedBox(
child: SingleChildScrollView(
clipBehavior: Clip.none,
padding: const EdgeInsets.symmetric(horizontal: 4.5),
physics: const BouncingScrollPhysics(),
scrollDirection: Axis.horizontal,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: recommendations,
),
),
),
],
),
);
}
}
}
class LocationRecommendation extends StatelessWidget {
static const width = 100.0;
static const height = 123.0;
static const thumbnailBorderWidth = 1.0;
static const outerCornerRadius = 12.0;
static const cornerSmoothing = 1.0;
static const sideOfThumbnail = 90.0;
static const outerStrokeWidth = 1.0;
//This is the space between this widget's boundary and the border stroke of
//thumbnail.
static const outerPadding = 4.0;
final GenericSearchResult locationSearchResult;
const LocationRecommendation(this.locationSearchResult, {super.key});
@override
Widget build(BuildContext context) {
final heroTag = locationSearchResult.heroTag() +
(locationSearchResult.previewThumbnail()?.tag ?? "");
final enteTextTheme = getEnteTextTheme(context);
return Padding(
padding: EdgeInsets.symmetric(horizontal: max(0, 2.5 - outerStrokeWidth)),
child: GestureDetector(
onTap: () {
RecentSearches().add(locationSearchResult.name());
if (locationSearchResult.onResultTap != null) {
locationSearchResult.onResultTap!(context);
} else {
routeToPage(
context,
SearchResultPage(locationSearchResult),
);
}
},
child: RepaintBoundary(
child: Stack(
alignment: Alignment.center,
clipBehavior: Clip.none,
children: [
ClipSmoothRect(
radius: SmoothBorderRadius(
cornerRadius: outerCornerRadius + outerStrokeWidth,
cornerSmoothing: cornerSmoothing,
),
child: Container(
color: Colors.white.withOpacity(0.1),
width: width + outerStrokeWidth * 2,
height: height + outerStrokeWidth * 2,
),
),
SizedBox(
width: width,
height: height,
child: Container(
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(12)),
boxShadow: [
BoxShadow(
blurRadius: 1,
offset: Offset(0, 0),
color: Color.fromRGBO(0, 0, 0, 0.09),
),
BoxShadow(
blurRadius: 1,
offset: Offset(1, 2),
color: Color.fromRGBO(0, 0, 0, 0.05),
),
],
),
child: ClipSmoothRect(
radius: SmoothBorderRadius(
cornerRadius: outerCornerRadius,
cornerSmoothing: cornerSmoothing,
),
child: Stack(
clipBehavior: Clip.none,
children: [
Stack(
children: [
ImageFiltered(
imageFilter:
ImageFilter.blur(sigmaX: 24, sigmaY: 24),
child: locationSearchResult.previewThumbnail() !=
null
? ThumbnailWidget(
locationSearchResult.previewThumbnail()!,
shouldShowArchiveStatus: false,
shouldShowSyncStatus: false,
shouldShowFavoriteIcon: false,
)
: const NoThumbnailWidget(),
),
Container(
color: Colors.black.withOpacity(0.2),
),
],
),
Padding(
padding: const EdgeInsets.fromLTRB(
outerPadding,
outerPadding,
outerPadding,
outerPadding,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Stack(
alignment: Alignment.center,
children: [
ClipSmoothRect(
radius: SmoothBorderRadius(
cornerRadius:
outerCornerRadius - outerPadding,
cornerSmoothing: cornerSmoothing,
),
child: Container(
color: Colors.black.withOpacity(0.1),
width: sideOfThumbnail +
thumbnailBorderWidth * 2,
height: sideOfThumbnail +
thumbnailBorderWidth * 2,
),
),
SizedBox(
width: sideOfThumbnail,
height: sideOfThumbnail,
child: locationSearchResult
.previewThumbnail() !=
null
? Hero(
tag: heroTag,
child: ClipSmoothRect(
radius: SmoothBorderRadius(
cornerRadius:
outerCornerRadius -
outerPadding -
thumbnailBorderWidth,
cornerSmoothing:
cornerSmoothing,
),
clipBehavior:
Clip.antiAliasWithSaveLayer,
child: ThumbnailWidget(
locationSearchResult
.previewThumbnail()!,
shouldShowArchiveStatus: false,
shouldShowSyncStatus: false,
shouldShowFavoriteIcon: false,
),
),
)
: const NoThumbnailWidget(),
),
],
),
const SizedBox(height: 4),
Expanded(
child: SizedBox(
width: 90,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
locationSearchResult.name(),
style: enteTextTheme.mini.copyWith(
color: Colors.white,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
],
),
),
),
],
),
),
],
),
),
),
),
Positioned(
left: 8,
top: 8,
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
ClipOval(
child: Container(
color: const Color.fromRGBO(0, 0, 0, 0.6),
width: 15,
height: 15,
),
),
Container(
width: 16,
height: 16,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
width: 0.5,
color: strokeSolidMutedLight,
),
),
),
const Icon(
Icons.location_on_sharp,
color: Colors.white,
size: 11,
),
],
),
),
],
),
),
),
);
}
}
//Used for non-empty state of location section.
class GoToMapWithBG extends StatelessWidget {
const GoToMapWithBG({super.key});
@override
Widget build(BuildContext context) {
final enteTextTheme = getEnteTextTheme(context);
return Padding(
padding: EdgeInsets.symmetric(
horizontal: max(0, 2.5 - LocationRecommendation.outerStrokeWidth),
),
child: GestureDetector(
onTap: () async {
final bool result = await requestForMapEnable(context);
if (result) {
// ignore: unawaited_futures
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => MapScreen(
filesFutureFn: SearchService.instance.getAllFiles,
),
),
);
}
},
child: Stack(
alignment: Alignment.center,
children: [
ClipSmoothRect(
radius: SmoothBorderRadius(
cornerRadius: LocationRecommendation.outerCornerRadius +
LocationRecommendation.outerStrokeWidth,
cornerSmoothing: LocationRecommendation.cornerSmoothing,
),
child: Container(
color: Colors.white.withOpacity(0.1),
width: LocationRecommendation.width +
LocationRecommendation.outerStrokeWidth * 2,
height: LocationRecommendation.height +
LocationRecommendation.outerStrokeWidth * 2,
),
),
SizedBox(
width: LocationRecommendation.width,
height: LocationRecommendation.height,
child: Container(
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(12)),
boxShadow: [
BoxShadow(
blurRadius: 1,
offset: Offset(0, 0),
color: Color.fromRGBO(0, 0, 0, 0.09),
blurStyle: BlurStyle.outer,
),
BoxShadow(
blurRadius: 1,
offset: Offset(1, 2),
color: Color.fromRGBO(0, 0, 0, 0.05),
blurStyle: BlurStyle.outer,
),
],
),
child: ClipSmoothRect(
radius: SmoothBorderRadius(
cornerRadius: LocationRecommendation.outerCornerRadius,
cornerSmoothing: LocationRecommendation.cornerSmoothing,
),
child: Stack(
clipBehavior: Clip.none,
children: [
Image.asset("assets/earth_blurred.png"),
Padding(
padding: const EdgeInsets.fromLTRB(
LocationRecommendation.outerPadding,
LocationRecommendation.outerPadding,
LocationRecommendation.outerPadding,
LocationRecommendation.outerPadding,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: LocationRecommendation.sideOfThumbnail,
height: LocationRecommendation.sideOfThumbnail,
child: Image.asset("assets/map_world.png"),
),
const SizedBox(height: 4),
Expanded(
child: SizedBox(
width: 90,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Your Map",
style: enteTextTheme.mini.copyWith(
color: Colors.white,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
],
),
),
),
],
),
),
],
),
),
),
),
],
),
),
);
}
}
class LocationCTA extends StatelessWidget {
const LocationCTA({super.key});
@override
Widget build(BuildContext context) {
final enteTextTheme = getEnteTextTheme(context);
final enteColorScheme = getEnteColorScheme(context);
return Padding(
padding: EdgeInsets.symmetric(
horizontal: max(0, 2.5 - LocationRecommendation.outerStrokeWidth),
),
child: GestureDetector(
onTap: SectionType.location.ctaOnTap(context),
child: Stack(
alignment: Alignment.center,
children: [
ClipSmoothRect(
radius: SmoothBorderRadius(
cornerRadius: LocationRecommendation.outerCornerRadius +
LocationRecommendation.outerStrokeWidth,
cornerSmoothing: LocationRecommendation.cornerSmoothing,
),
child: Container(
color: Colors.white.withOpacity(0.1),
width: LocationRecommendation.width +
LocationRecommendation.outerStrokeWidth * 2,
height: LocationRecommendation.height +
LocationRecommendation.outerStrokeWidth * 2,
),
),
SizedBox(
width: LocationRecommendation.width,
height: LocationRecommendation.height,
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(12)),
boxShadow: const [
BoxShadow(
blurRadius: 1,
offset: Offset(0, 0),
color: Color.fromRGBO(0, 0, 0, 0.09),
blurStyle: BlurStyle.outer,
),
BoxShadow(
blurRadius: 1,
offset: Offset(1, 2),
color: Color.fromRGBO(0, 0, 0, 0.05),
blurStyle: BlurStyle.outer,
),
],
color: enteColorScheme.backgroundElevated,
),
child: ClipSmoothRect(
radius: SmoothBorderRadius(
cornerRadius: LocationRecommendation.outerCornerRadius,
cornerSmoothing: LocationRecommendation.cornerSmoothing,
),
child: Padding(
padding: const EdgeInsets.fromLTRB(
LocationRecommendation.outerPadding + 2,
LocationRecommendation.outerPadding + 3,
LocationRecommendation.outerPadding + 2,
LocationRecommendation.outerPadding,
),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
DottedBorder(
dashPattern: const [2, 2],
color: enteColorScheme.strokeFaint,
strokeWidth: 1,
padding: const EdgeInsets.all(0),
borderType: BorderType.RRect,
radius: const Radius.circular(4.5),
child: SizedBox(
width: 90,
height: 84,
child: Icon(
Icons.add_location_alt_outlined,
color: enteColorScheme.strokeFaint,
size: 28,
),
),
),
const SizedBox(height: 8),
Expanded(
child: SizedBox(
width: 90,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Add new",
style: enteTextTheme.miniFaint,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
],
),
),
),
],
),
),
),
),
),
],
),
),
);
}
}

View file

@ -0,0 +1,259 @@
import "dart:async";
import "dart:math";
import "package:figma_squircle/figma_squircle.dart";
import "package:flutter/material.dart";
import "package:photos/core/constants.dart";
import "package:photos/events/event.dart";
import "package:photos/models/search/generic_search_result.dart";
import "package:photos/models/search/recent_searches.dart";
import "package:photos/models/search/search_types.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/viewer/file/no_thumbnail_widget.dart";
import "package:photos/ui/viewer/file/thumbnail_widget.dart";
import "package:photos/ui/viewer/search/result/search_result_page.dart";
import "package:photos/ui/viewer/search/search_section_cta.dart";
import "package:photos/ui/viewer/search_tab/section_header.dart";
import "package:photos/utils/navigation_util.dart";
class MomentsSection extends StatefulWidget {
final List<GenericSearchResult> momentsSearchResults;
const MomentsSection(this.momentsSearchResults, {super.key});
@override
State<MomentsSection> createState() => _MomentsSectionState();
}
class _MomentsSectionState extends State<MomentsSection> {
late List<GenericSearchResult> _momentsSearchResults;
final streamSubscriptions = <StreamSubscription>[];
@override
void initState() {
super.initState();
_momentsSearchResults = widget.momentsSearchResults;
final streamsToListenTo = SectionType.moment.sectionUpdateEvents();
for (Stream<Event> stream in streamsToListenTo) {
streamSubscriptions.add(
stream.listen((event) async {
_momentsSearchResults = (await SectionType.moment.getData(
context,
limit: kSearchSectionLimit,
)) as List<GenericSearchResult>;
setState(() {});
}),
);
}
}
@override
void dispose() {
for (var subscriptions in streamSubscriptions) {
subscriptions.cancel();
}
super.dispose();
}
@override
void didUpdateWidget(covariant MomentsSection oldWidget) {
super.didUpdateWidget(oldWidget);
_momentsSearchResults = widget.momentsSearchResults;
}
@override
Widget build(BuildContext context) {
if (_momentsSearchResults.isEmpty) {
final textTheme = getEnteTextTheme(context);
return Padding(
padding: const EdgeInsets.only(left: 12, right: 8),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
SectionType.moment.sectionTitle(context),
style: textTheme.largeBold,
),
const SizedBox(height: 24),
Padding(
padding: const EdgeInsets.only(left: 4),
child: Text(
SectionType.moment.getEmptyStateText(context),
style: textTheme.smallMuted,
),
),
],
),
),
const SizedBox(width: 8),
const SearchSectionEmptyCTAIcon(SectionType.moment),
],
),
);
} else {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SectionHeader(
SectionType.moment,
hasMore:
(_momentsSearchResults.length >= kSearchSectionLimit - 1),
),
const SizedBox(height: 2),
SizedBox(
child: SingleChildScrollView(
clipBehavior: Clip.none,
padding: const EdgeInsets.symmetric(horizontal: 4.5),
physics: const BouncingScrollPhysics(),
scrollDirection: Axis.horizontal,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: _momentsSearchResults
.map(
(momentSearchResult) =>
MomentRecommendation(momentSearchResult),
)
.toList(),
),
),
),
],
),
);
}
}
}
class MomentRecommendation extends StatelessWidget {
static const _width = 100.0;
static const _height = 145.0;
static const _borderWidth = 1.0;
static const _cornerRadius = 5.0;
static const _cornerSmoothing = 1.0;
final GenericSearchResult momentSearchResult;
const MomentRecommendation(this.momentSearchResult, {super.key});
@override
Widget build(BuildContext context) {
final heroTag = momentSearchResult.heroTag() +
(momentSearchResult.previewThumbnail()?.tag ?? "");
final enteTextTheme = getEnteTextTheme(context);
return Padding(
padding: EdgeInsets.symmetric(horizontal: max(2.5 - _borderWidth, 0)),
child: GestureDetector(
onTap: () {
RecentSearches().add(momentSearchResult.name());
if (momentSearchResult.onResultTap != null) {
momentSearchResult.onResultTap!(context);
} else {
routeToPage(
context,
SearchResultPage(momentSearchResult),
);
}
},
child: SizedBox(
width: _width + _borderWidth * 2,
height: _height + _borderWidth * 2,
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
ClipSmoothRect(
radius: SmoothBorderRadius(
cornerRadius: _cornerRadius + _borderWidth,
cornerSmoothing: _cornerSmoothing,
),
child: Container(
color: Colors.white.withOpacity(0.16),
width: _width + _borderWidth * 2,
height: _height + _borderWidth * 2,
),
),
Container(
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 6.25,
offset: const Offset(-1.25, 2.5),
),
],
),
child: ClipSmoothRect(
radius: SmoothBorderRadius(
cornerRadius: _cornerRadius,
cornerSmoothing: _cornerSmoothing,
),
child: Stack(
alignment: Alignment.bottomCenter,
clipBehavior: Clip.none,
children: [
SizedBox(
width: _width,
height: _height,
child: momentSearchResult.previewThumbnail() != null
? Hero(
tag: heroTag,
child: ThumbnailWidget(
momentSearchResult.previewThumbnail()!,
shouldShowArchiveStatus: false,
shouldShowSyncStatus: false,
),
)
: const NoThumbnailWidget(),
),
Container(
height: 145,
width: 100,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withOpacity(0),
Colors.black.withOpacity(0),
Colors.black.withOpacity(0.5),
],
stops: const [
0,
0.1,
1,
],
),
),
),
ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 76,
),
child: Padding(
padding: const EdgeInsets.only(
bottom: 8,
),
child: Text(
momentSearchResult.name(),
style: enteTextTheme.small.copyWith(
color: Colors.white,
),
maxLines: 3,
overflow: TextOverflow.fade,
),
),
),
],
),
),
),
],
),
),
),
);
}
}

View file

@ -1,15 +1,21 @@
import "package:fade_indexed_stack/fade_indexed_stack.dart";
import "package:flutter/material.dart";
import "package:flutter_animate/flutter_animate.dart";
import "package:photos/core/constants.dart";
import "package:photos/models/search/album_search_result.dart";
import "package:photos/models/search/generic_search_result.dart";
import "package:photos/models/search/index_of_indexed_stack.dart";
import "package:photos/models/search/search_types.dart";
import "package:photos/states/all_sections_examples_state.dart";
import "package:photos/ui/common/loading_widget.dart";
import "package:photos/ui/viewer/search/result/no_result_widget.dart";
import "package:photos/ui/viewer/search/search_section.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/descriptions_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/moments_section.dart";
class SearchTab extends StatefulWidget {
const SearchTab({Key? key}) : super(key: key);
@ -93,11 +99,40 @@ class _AllSearchSectionsState extends State<AllSearchSections> {
physics: const BouncingScrollPhysics(),
itemCount: searchTypes.length,
itemBuilder: (context, index) {
return SearchSection(
sectionType: searchTypes[index],
examples: snapshot.data!.elementAt(index),
limit: searchSectionLimit,
);
switch (searchTypes[index]) {
case SectionType.album:
return AlbumsSection(
snapshot.data!.elementAt(index)
as List<AlbumSearchResult>,
);
case SectionType.moment:
return MomentsSection(
snapshot.data!.elementAt(index)
as List<GenericSearchResult>,
);
case SectionType.fileCaption:
return DescriptionsSection(
snapshot.data!.elementAt(index)
as List<GenericSearchResult>,
);
case SectionType.location:
return LocationsSection(
snapshot.data!.elementAt(index)
as List<GenericSearchResult>,
);
case SectionType.contacts:
return ContactsSection(
snapshot.data!.elementAt(index)
as List<GenericSearchResult>,
);
case SectionType.fileTypesAndExtension:
return FileTypeSection(
snapshot.data!.elementAt(index)
as List<GenericSearchResult>,
);
default:
const SizedBox.shrink();
}
},
)
.animate(

View file

@ -0,0 +1,51 @@
import "package:flutter/material.dart";
import "package:photos/models/search/search_types.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/viewer/search/result/search_section_all_page.dart";
import "package:photos/utils/navigation_util.dart";
class SectionHeader extends StatelessWidget {
final SectionType sectionType;
final bool hasMore;
const SectionHeader(this.sectionType, {required this.hasMore, super.key});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
if (hasMore) {
routeToPage(
context,
SearchSectionAllPage(
sectionType: sectionType,
),
);
}
},
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.all(12),
child: Text(
sectionType.sectionTitle(context),
style: getEnteTextTheme(context).largeBold,
),
),
hasMore
? Container(
color: Colors.transparent,
child: Padding(
padding: const EdgeInsets.fromLTRB(24, 12, 12, 12),
child: Icon(
Icons.chevron_right_outlined,
color: getEnteColorScheme(context).strokeMuted,
),
),
)
: const SizedBox.shrink(),
],
),
);
}
}

View file

@ -407,10 +407,10 @@ packages:
dependency: "direct main"
description:
name: dotted_border
sha256: "07a5c5e8d4e6e992279e190e0352be8faa5b8f96d81c77a78b2d42f060279840"
sha256: "108837e11848ca776c53b30bc870086f84b62ed6e01c503ed976e8f8c7df9c04"
url: "https://pub.dev"
source: hosted
version: "2.0.0+3"
version: "2.1.0"
dropdown_button2:
dependency: "direct main"
description:
@ -515,6 +515,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.0"
figma_squircle:
dependency: "direct main"
description:
name: figma_squircle
sha256: "790b91a9505e90d246f6efe2fa065ff7fffe658c7b44fe9b5b20c7b0ad3818c0"
url: "https://pub.dev"
source: hosted
version: "0.5.3"
file:
dependency: transitive
description:

View file

@ -46,7 +46,7 @@ dependencies:
device_info_plus: ^9.0.3
dio: ^4.0.6
dots_indicator: ^2.0.0
dotted_border: ^2.0.0+2
dotted_border: ^2.1.0
dropdown_button2: ^2.0.0
email_validator: ^2.0.1
equatable: ^2.0.5
@ -58,6 +58,7 @@ dependencies:
fade_indexed_stack: ^0.2.2
fast_base58: ^0.2.1
figma_squircle: ^0.5.3
file_saver:
# Use forked version till this PR is merged: https://github.com/incrediblezayed/file_saver/pull/87
git: https://github.com/jesims/file_saver.git