Merge pull request #413 from ente-io/mobile-search

Mobile search
This commit is contained in:
Neeraj Gupta 2022-07-28 10:39:26 +05:30 committed by GitHub
commit 9facc73fb1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 296 additions and 235 deletions

View file

@ -19,6 +19,7 @@ import 'package:photos/events/force_reload_home_gallery_event.dart';
import 'package:photos/events/local_photos_updated_event.dart';
import 'package:photos/models/collection.dart';
import 'package:photos/models/collection_file_item.dart';
import 'package:photos/models/collection_items.dart';
import 'package:photos/models/file.dart';
import 'package:photos/models/magic_metadata.dart';
import 'package:photos/services/app_lifecycle_service.dart';
@ -170,6 +171,46 @@ class CollectionsService {
.toList();
}
// getFilteredCollectionsWithThumbnail removes deleted or archived or
// collections which don't have a file from search result
Future<List<CollectionWithThumbnail>> getFilteredCollectionsWithThumbnail(
String query,
) async {
// identify collections which have at least one file as we don't display
// empty collection
List<File> latestCollectionFiles = await getLatestCollectionFiles();
Map<int, File> collectionIDToLatestFileMap = {
for (File file in latestCollectionFiles) file.collectionID: file
};
/* Identify collections whose name matches the search query
and is not archived
and is not deleted
and has at-least one file
*/
List<Collection> matchedCollection = _collectionIDToCollections.values
.where(
(c) =>
!c.isDeleted && // not deleted
!c.isArchived() // not archived
&&
collectionIDToLatestFileMap.containsKey(c.id) && // the
// collection is not empty
c.name.contains(RegExp(query, caseSensitive: false)),
)
.toList();
List<CollectionWithThumbnail> result = [];
for (Collection collection in matchedCollection) {
result.add(CollectionWithThumbnail(
collection,
collectionIDToLatestFileMap[collection.id],
));
}
return result;
}
Future<List<User>> getSharees(int collectionID) {
return _dio
.get(

View file

@ -3,6 +3,7 @@ import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/core/constants.dart';
import 'package:photos/core/network.dart';
import 'package:shared_preferences/shared_preferences.dart';
@ -83,6 +84,11 @@ class FeatureFlagService {
}
}
bool enableSearchFeature() {
String email = Configuration.instance.getEmail();
return (email != null && email.endsWith("@ente.io")) || kDebugMode;
}
Future<void> fetchFeatureFlags() async {
try {
final response = await Network.instance

View file

@ -591,7 +591,6 @@ class DeviceFolderIcon extends StatelessWidget {
child: SizedBox(
height: 140,
width: 120,
// padding: const EdgeInsets.all(8.0),
child: Column(
children: <Widget>[
ClipRRect(
@ -709,7 +708,7 @@ class CollectionItem extends StatelessWidget {
),
);
} else {
return Container();
return const SizedBox.shrink();
}
},
),

View file

@ -63,9 +63,7 @@ class DynamicFAB extends StatelessWidget {
height: 56,
padding: const EdgeInsets.symmetric(horizontal: 20),
child: OutlinedButton(
onPressed: isFormValid //var here
? onPressedFunction
: null,
onPressed: isFormValid ? onPressedFunction : null,
child: Text(buttonText),
),
);

View file

@ -4,8 +4,10 @@ import 'package:flutter/material.dart';
import 'package:photos/core/event_bus.dart';
import 'package:photos/ente_theme_data.dart';
import 'package:photos/events/sync_status_update_event.dart';
import 'package:photos/services/feature_flag_service.dart';
import 'package:photos/services/sync_service.dart';
import 'package:photos/ui/header_error_widget.dart';
import 'package:photos/ui/viewer/search/search_widget.dart';
const double kContainerHeight = 36;
@ -209,21 +211,34 @@ class StatusBarBrandingWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
height: kContainerHeight,
padding: const EdgeInsets.only(left: 12),
child: const Align(
alignment: Alignment.centerLeft,
child: Text(
"ente",
style: TextStyle(
fontWeight: FontWeight.bold,
fontFamily: 'Montserrat',
fontSize: 24,
height: 1,
return Stack(
children: [
Container(
height: kContainerHeight,
padding: const EdgeInsets.only(left: 12),
child: const Align(
alignment: Alignment.centerLeft,
child: Text(
"ente",
style: TextStyle(
fontWeight: FontWeight.bold,
fontFamily: 'Montserrat',
fontSize: 24,
height: 1,
),
),
),
),
),
FeatureFlagService.instance.enableSearchFeature()
? SizedBox(
width: MediaQuery.of(context).size.width,
child: const Align(
alignment: Alignment.centerRight,
child: SearchIconWidget(),
),
)
: const SizedBox.shrink(),
],
);
}
}

View file

@ -0,0 +1,75 @@
import 'package:flutter/material.dart';
import 'package:photos/db/files_db.dart';
import 'package:photos/ente_theme_data.dart';
import 'package:photos/models/collection_items.dart';
import 'package:photos/ui/viewer/file/thumbnail_widget.dart';
import 'package:photos/ui/viewer/gallery/collection_page.dart';
import 'package:photos/utils/navigation_util.dart';
class CollectionResultWidget extends StatelessWidget {
final CollectionWithThumbnail c;
const CollectionResultWidget(this.c, {Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return GestureDetector(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Album',
style: TextStyle(fontSize: 12),
),
const SizedBox(height: 8),
Text(
c.collection.name,
style: const TextStyle(fontSize: 18),
),
FutureBuilder<int>(
future: FilesDB.instance.collectionFileCount(
c.collection.id,
),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data > 0) {
int noOfMemories = snapshot.data;
return RichText(
text: TextSpan(
style: TextStyle(
color:
Theme.of(context).colorScheme.defaultTextColor,
),
children: [
TextSpan(text: noOfMemories.toString()),
TextSpan(
text: noOfMemories != 1 ? ' memories' : ' memory',
),
],
),
);
} else {
return const SizedBox.shrink();
}
},
),
],
),
SizedBox(
height: 50,
width: 50,
child: ThumbnailWidget(c.thumbnail),
)
],
),
),
onTap: () {
routeToPage(context, CollectionPage(c), forceCustomPageRoute: true);
},
);
}
}

View file

@ -1,69 +0,0 @@
import 'package:flutter/material.dart';
import 'package:photos/models/file.dart';
import 'package:photos/models/location.dart';
import 'package:photos/models/selected_files.dart';
import 'package:photos/ui/viewer/gallery/gallery.dart';
class ViewPort {
final Location northEast;
final Location southWest;
ViewPort(this.northEast, this.southWest);
@override
String toString() => 'ViewPort(northEast: $northEast, southWest: $southWest)';
}
class LocationSearchResultsPage extends StatefulWidget {
final ViewPort viewPort;
final String name;
const LocationSearchResultsPage(this.viewPort, this.name, {Key key})
: super(key: key);
@override
State<LocationSearchResultsPage> createState() =>
_LocationSearchResultsPageState();
}
class _LocationSearchResultsPageState extends State<LocationSearchResultsPage> {
final _selectedFiles = SelectedFiles();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.name),
),
body: Gallery(
tagPrefix: "location_search",
selectedFiles: _selectedFiles,
footer: const SizedBox(height: 120),
),
);
}
List<File> _getResult() {
List<File> files = [];
final Map<String, dynamic> args = <String, dynamic>{};
args['files'] = files;
args['viewPort'] = widget.viewPort;
return _filterPhotos(args);
}
static List<File> _filterPhotos(Map<String, dynamic> args) {
List<File> files = args['files'];
ViewPort viewPort = args['viewPort'];
final result = <File>[];
for (final file in files) {
if (file.location != null &&
viewPort.northEast.latitude > file.location.latitude &&
viewPort.southWest.latitude < file.location.latitude &&
viewPort.northEast.longitude > file.location.longitude &&
viewPort.southWest.longitude < file.location.longitude) {
result.add(file);
} else {}
}
return result;
}
}

View file

@ -1,121 +0,0 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/core/network.dart';
import 'package:photos/models/location.dart';
import 'package:photos/ui/common/loading_widget.dart';
import 'package:photos/ui/viewer/search/location_search_results_page.dart';
class LocationSearchWidget extends StatefulWidget {
const LocationSearchWidget({
Key key,
}) : super(key: key);
@override
State<LocationSearchWidget> createState() => _LocationSearchWidgetState();
}
class _LocationSearchWidgetState extends State<LocationSearchWidget> {
String _searchString;
@override
Widget build(BuildContext context) {
return TypeAheadField(
textFieldConfiguration: const TextFieldConfiguration(
autofocus: true,
decoration: InputDecoration(
border: InputBorder.none,
hintText: 'Eg: Rome, Paris, New York',
contentPadding: EdgeInsets.all(0.0),
),
),
hideOnEmpty: true,
loadingBuilder: (context) {
return const EnteLoadingWidget();
},
suggestionsCallback: (pattern) async {
if (pattern.isEmpty || pattern.length < 2) {
return null;
}
_searchString = pattern;
return Network.instance
.getDio()
.get(
Configuration.instance.getHttpEndpoint() + "/search/location",
queryParameters: {
"query": pattern,
},
options: Options(
headers: {"X-Auth-Token": Configuration.instance.getToken()},
),
)
.then((response) {
if (_searchString == pattern) {
// Query has not changed
return response.data["results"];
}
return null;
});
},
itemBuilder: (context, suggestion) {
return LocationSearchResultWidget(suggestion['name']);
},
onSuggestionSelected: (suggestion) {
Navigator.pop(context);
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => LocationSearchResultsPage(
ViewPort(
Location(
suggestion['geometry']['viewport']['northeast']['lat'],
suggestion['geometry']['viewport']['northeast']['lng'],
),
Location(
suggestion['geometry']['viewport']['southwest']['lat'],
suggestion['geometry']['viewport']['southwest']['lng'],
),
),
suggestion['name'],
),
),
);
},
);
}
}
class LocationSearchResultWidget extends StatelessWidget {
final String name;
const LocationSearchResultWidget(
this.name, {
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 6.0, horizontal: 6.0),
margin: const EdgeInsets.symmetric(vertical: 6.0),
child: Column(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
const Icon(
Icons.location_on,
),
const Padding(padding: EdgeInsets.only(left: 20.0)),
Flexible(
child: Text(
name,
overflow: TextOverflow.clip,
),
),
],
),
],
),
);
}
}

View file

@ -1,27 +0,0 @@
import 'package:flutter/material.dart';
import 'package:photos/ui/viewer/search/location_search_widget.dart';
class SearchPage extends StatefulWidget {
const SearchPage({Key key}) : super(key: key);
@override
State<SearchPage> createState() => _SearchPageState();
}
class _SearchPageState extends State<SearchPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const LocationSearchWidget(),
actions: <Widget>[
IconButton(
icon: const Icon(Icons.search),
onPressed: () {},
)
],
),
body: Container(),
);
}
}

View file

@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:photos/models/collection_items.dart';
import 'package:photos/ui/viewer/search/collection_result_widget.dart';
class SearchResultsSuggestions extends StatelessWidget {
final List<CollectionWithThumbnail> collectionsWithThumbnail;
const SearchResultsSuggestions({Key key, this.collectionsWithThumbnail})
: super(key: key);
@override
Widget build(BuildContext context) {
List<Widget> suggestions = [];
for (CollectionWithThumbnail c in collectionsWithThumbnail) {
suggestions.add(CollectionResultWidget(c));
}
return Container(
constraints:
BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.6),
child: ListView.builder(
itemCount: suggestions.length,
itemBuilder: (context, index) {
return suggestions[index];
},
),
);
}
}

View file

@ -0,0 +1,115 @@
import 'package:flutter/material.dart';
import 'package:photos/ente_theme_data.dart';
import 'package:photos/models/collection_items.dart';
import 'package:photos/services/collections_service.dart';
import 'package:photos/ui/viewer/search/search_results_suggestions.dart';
class SearchIconWidget extends StatefulWidget {
const SearchIconWidget({Key key}) : super(key: key);
@override
State<SearchIconWidget> createState() => _SearchIconWidgetState();
}
class _SearchIconWidgetState extends State<SearchIconWidget> {
bool showSearchWidget;
@override
void initState() {
super.initState();
showSearchWidget = false;
}
@override
Widget build(BuildContext context) {
return showSearchWidget
? Searchwidget(showSearchWidget)
: IconButton(
onPressed: () {
setState(
() {
showSearchWidget = !showSearchWidget;
},
);
},
icon: const Icon(Icons.search),
);
}
}
// ignore: must_be_immutable
class Searchwidget extends StatefulWidget {
bool openSearch;
final String searchQuery = '';
Searchwidget(this.openSearch, {Key key}) : super(key: key);
@override
State<Searchwidget> createState() => _SearchwidgetState();
}
class _SearchwidgetState extends State<Searchwidget> {
final ValueNotifier<String> _searchQ = ValueNotifier('');
@override
Widget build(BuildContext context) {
List<CollectionWithThumbnail> matchedCollections;
return widget.openSearch
? Column(
children: [
Row(
children: [
const SizedBox(width: 12),
Flexible(
child: Container(
color:
Theme.of(context).colorScheme.defaultBackgroundColor,
child: TextFormField(
style: Theme.of(context).textTheme.subtitle1,
decoration: InputDecoration(
filled: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
border: UnderlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(8),
),
prefixIcon: const Icon(Icons.search),
),
onChanged: (value) async {
matchedCollections = await CollectionsService.instance
.getFilteredCollectionsWithThumbnail(value);
_searchQ.value = value;
},
autofocus: true,
),
),
),
IconButton(
onPressed: () {
setState(() {
widget.openSearch = !widget.openSearch;
});
},
icon: const Icon(Icons.close),
),
],
),
const SizedBox(height: 20),
ValueListenableBuilder(
valueListenable: _searchQ,
builder: (
BuildContext context,
String newQuery,
Widget child,
) {
return newQuery != ''
? SearchResultsSuggestions(
collectionsWithThumbnail: matchedCollections,
)
: const SizedBox.shrink();
},
),
],
)
: SearchIconWidget();
}
}