Redesigned Discovery Tab (#1755)
BIN
assets/2.0x/map_world.png
Normal file
After Width: | Height: | Size: 42 KiB |
BIN
assets/2.0x/type_AVI.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
assets/2.0x/type_GIF.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
assets/2.0x/type_HEIC.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
assets/2.0x/type_JPEG.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
assets/2.0x/type_JPG.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
assets/2.0x/type_MKV.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
assets/2.0x/type_MP4.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
assets/2.0x/type_PNG.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
assets/2.0x/type_WEBP.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
assets/2.0x/type_live.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
assets/2.0x/type_photos.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
assets/2.0x/type_unknown.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
assets/2.0x/type_videos.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
assets/3.0x/map_world.png
Normal file
After Width: | Height: | Size: 83 KiB |
BIN
assets/3.0x/type_AVI.png
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
assets/3.0x/type_GIF.png
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
assets/3.0x/type_HEIC.png
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
assets/3.0x/type_JPEG.png
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
assets/3.0x/type_JPG.png
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
assets/3.0x/type_MKV.png
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
assets/3.0x/type_MP4.png
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
assets/3.0x/type_PNG.png
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
assets/3.0x/type_WEBP.png
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
assets/3.0x/type_live.png
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
assets/3.0x/type_photos.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
assets/3.0x/type_unknown.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
assets/3.0x/type_videos.png
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
assets/earth_blurred.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
assets/map_world.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
assets/type_AVI.png
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
assets/type_GIF.png
Normal file
After Width: | Height: | Size: 5.6 KiB |
BIN
assets/type_HEIC.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
assets/type_JPEG.png
Normal file
After Width: | Height: | Size: 5.6 KiB |
BIN
assets/type_JPG.png
Normal file
After Width: | Height: | Size: 5.5 KiB |
BIN
assets/type_MKV.png
Normal file
After Width: | Height: | Size: 5.5 KiB |
BIN
assets/type_MP4.png
Normal file
After Width: | Height: | Size: 5.6 KiB |
BIN
assets/type_PNG.png
Normal file
After Width: | Height: | Size: 5.7 KiB |
BIN
assets/type_WEBP.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
assets/type_live.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
assets/type_photos.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
assets/type_unknown.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
assets/type_videos.png
Normal file
After Width: | Height: | Size: 5.5 KiB |
|
@ -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' +
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -84,7 +84,7 @@ class _AllSectionsExamplesProviderState
|
|||
continue;
|
||||
}
|
||||
allSectionsExamples.add(
|
||||
sectionType.getData(context, limit: searchSectionLimit),
|
||||
sectionType.getData(context, limit: kSearchSectionLimit),
|
||||
);
|
||||
}
|
||||
allSectionsExamplesFuture =
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
254
lib/ui/viewer/search_tab/albums_section.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
262
lib/ui/viewer/search_tab/contacts_section.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
239
lib/ui/viewer/search_tab/descriptions_section.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
215
lib/ui/viewer/search_tab/file_type_section.dart
Normal 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;
|
||||
}
|
||||
}
|
603
lib/ui/viewer/search_tab/locations_section.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
259
lib/ui/viewer/search_tab/moments_section.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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(
|
51
lib/ui/viewer/search_tab/section_header.dart
Normal 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(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
12
pubspec.lock
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|