commit
9facc73fb1
11 changed files with 296 additions and 235 deletions
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
},
|
||||
),
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
75
lib/ui/viewer/search/collection_result_widget.dart
Normal file
75
lib/ui/viewer/search/collection_result_widget.dart
Normal 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);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
29
lib/ui/viewer/search/search_results_suggestions.dart
Normal file
29
lib/ui/viewer/search/search_results_suggestions.dart
Normal 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];
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
115
lib/ui/viewer/search/search_widget.dart
Normal file
115
lib/ui/viewer/search/search_widget.dart
Normal 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();
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue