diff --git a/README.md b/README.md index fd1a97d..ad4d5ca 100644 --- a/README.md +++ b/README.md @@ -22,4 +22,4 @@ samples, guidance on mobile development, and a full API reference. ### watch demo on youtube. -[![demo_on_youtube](https://img.youtube.com/vi/dXPWOY69XuI/hqdefault.jpg)](https://youtu.be/dXPWOY69XuI) +[![demo_on_youtube](https://img.youtube.com/vi/IW1yq-cZSxg/hqdefault.jpg)](https://youtu.be/IW1yq-cZSxg) diff --git a/lib/custom_search_search_delegate.dart b/lib/custom_search_search_delegate.dart index 16ca144..8f583c7 100644 --- a/lib/custom_search_search_delegate.dart +++ b/lib/custom_search_search_delegate.dart @@ -283,6 +283,7 @@ class CustomSearchInfiniteSearchDelegate extends CustomSearchSearchDelegate { return ImageSearchResultPage( dataSource, snapshot.data, searchQuery); case SearchType.web: + print('here'); return WebSearchResultPage( dataSource, snapshot.data, searchQuery); } diff --git a/lib/main.dart b/lib/main.dart index 3e73165..78dc0c8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -16,7 +16,8 @@ class SearchDemoApp extends StatelessWidget { '/websearch': (context) => webSearchDemo, '/imagesearch': (context) => CustomSearchDemoPage(CustomSearchDemoType.imageSearch), - + '/promotionwebsearch': (context) => + CustomSearchDemoPage(CustomSearchDemoType.promotionWebSearch), }, title: 'Custom Search Engine Flutter Demo'); } diff --git a/lib/search_data_source.dart b/lib/search_data_source.dart index fabe486..ab10b56 100644 --- a/lib/search_data_source.dart +++ b/lib/search_data_source.dart @@ -49,10 +49,54 @@ class SearchResult { : result.image.contextLink.hashCode; } +class PromotionImage { + int height; + String source; + int width; + + PromotionImage(customsearch.PromotionImage promotionImage) { + this.height = promotionImage.height; + this.width = promotionImage.width; + this.source = promotionImage.source; + } +} + +class PromotionBodyLines { + String htmlTitle; + String link; + String title; + String url; + + PromotionBodyLines(customsearch.PromotionBodyLines bodyLines) { + this.htmlTitle = bodyLines.htmlTitle; + this.link = bodyLines.link; + this.title = bodyLines.title; + this.url = bodyLines.url; + } +} + class Promotion { - final customsearch.Promotion promotion; + String title; + String link; + String displayLink; + PromotionImage promotionImage; + List promotionBodyLines = new List(); + + Promotion(customsearch.Promotion promotion) { + print('here'); + this.title = promotion.title; + this.link = promotion.link; + this.displayLink = promotion.displayLink; + this.promotionImage = PromotionImage(promotion.image); + promotion.bodyLines.forEach((bodyLines) => + this.promotionBodyLines.add(PromotionBodyLines(bodyLines))); + print(this); + } - Promotion(this.promotion); + @override + String toString() { + return 'Promotion{title: $title, link: $link, displayLink: $displayLink, promotionImage: $promotionImage, promotionBodyLines: $promotionBodyLines}'; + } } class Refinement { @@ -85,18 +129,25 @@ class SearchResults { var results = new List(); search.items.forEach( (item) => results.add(SearchResult.escapeLineBreakInSnippet(item))); + // Deduplicate search result. this.searchResults = Set.from(results).toList(); - search.context.facets.forEach((listOfFacet) { - this.refinements.add(Refinement(listOfFacet[0])); - }); + print(search.context.facets); + if (search.context.facets != null) { + search.context.facets.forEach((listOfFacet) { + this.refinements.add(Refinement(listOfFacet[0])); + }); + } + if (search.promotions != null) { + search.promotions + .forEach((promotion) => this.promotions.add(Promotion(promotion))); + } } @override String toString() { return 'SearchResults{searchResults: $searchResults, refinements: $refinements, nextPage: $nextPage}'; } - } /// A wrapper class for search request, to make caching search request possible. @@ -450,7 +501,7 @@ class CustomSearchDataSource implements SearchDataSource { ExpireCache(); CustomSearchDataSource({@required this.cx, @required this.apiKey}) - :assert(apiKey.isNotEmpty) { + : assert(apiKey.isNotEmpty) { var client = auth.clientViaApiKey(apiKey); this.api = new customsearch.CustomsearchApi(client); } @@ -469,7 +520,6 @@ class CustomSearchDataSource implements SearchDataSource { } else { return await _cache.get(searchQuery); } - final result = await searchQuery.runSearch(this.api); _cache.set(searchQuery, result); print('call search backend'); diff --git a/lib/ui/custom_search_demo_page.dart b/lib/ui/custom_search_demo_page.dart index 285b012..b1c6fa7 100644 --- a/lib/ui/custom_search_demo_page.dart +++ b/lib/ui/custom_search_demo_page.dart @@ -16,6 +16,7 @@ enum CustomSearchDemoType { /// Search for image. imageSearch, + promotionWebSearch, } class CustomSearchDemoPage extends StatefulWidget { @@ -34,6 +35,8 @@ class CustomSearchDemoPage extends StatefulWidget { return _CustomSearchDemoPageState.customWebSearch(); case CustomSearchDemoType.imageSearch: return _CustomSearchDemoPageState.customImageSearch(); + case CustomSearchDemoType.promotionWebSearch: + return _CustomSearchDemoPageState.customPromotionWebSearch(); default: return null; } @@ -73,7 +76,9 @@ class _CustomSearchDemoPageState extends State { apiKey: '')); this.hintText = 'Google Custom Image Search'; otherRoutes = [ - Tuple2('Custom Web Search Demo', '/websearch') + Tuple2('Custom Web Search Demo', '/websearch'), + Tuple2( + 'Custom Web Search Promotion Demo', '/promotionwebsearch') ]; } @@ -85,10 +90,24 @@ class _CustomSearchDemoPageState extends State { apiKey: '')); this.hintText = 'Google Custom Web Search'; otherRoutes = [ - Tuple2('Custom Image Search Demo', '/imagesearch') + Tuple2('Custom Image Search Demo', '/imagesearch'), + Tuple2( + 'Custom Web Search Promotion Demo', '/promotionwebsearch') ]; } + _CustomSearchDemoPageState.customPromotionWebSearch() { + // flutter with promotion + this.delegate = new CustomSearchInfiniteSearchDelegate( + dataSource: CustomSearchDataSource( + cx: '013098254965507895640:ebp1trsjo0a', + apiKey: '')); + this.hintText = 'Google Custom Web Search with Promotion'; + otherRoutes = [ + Tuple2('Custom Web Search Demo', '/websearch'), + Tuple2('Custom Image Search Demo', '/imagesearch') + ]; + } final GlobalKey _scaffoldKey = new GlobalKey(); diff --git a/lib/ui/promotion_card.dart b/lib/ui/promotion_card.dart new file mode 100644 index 0000000..fb5fa64 --- /dev/null +++ b/lib/ui/promotion_card.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_app_cse/search_data_source.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class PromotionCard extends StatelessWidget { + const PromotionCard({@required this.promotion}); + + final Promotion promotion; + + Widget _generateTitleTile(BuildContext context) { + final ThemeData theme = Theme.of(context); + return ListTile( + title: Text( + '[promotion] ' + this.promotion.title, + style: theme.textTheme.headline.copyWith( + fontSize: 15.0, fontWeight: FontWeight.bold, color: Colors.blue), + ), + subtitle: new Text( + this.promotion.displayLink, + style: + theme.textTheme.body1.copyWith(fontSize: 14.0, color: Colors.green), + ), + ); + } + + Widget _generateBodyTile(BuildContext context) { + final ThemeData theme = Theme.of(context); + return Container( + padding: const EdgeInsets.only( + left: 8.0, + bottom: 8.0, + ), + child: new Row(children: [ + // TODO: wait until CSE fix their API. +// Expanded( +// flex: 1, +// child: +// Image.network('https:' + this.promotion.promotionImage.source)), + Expanded( +// flex: 4, + child: Container( + padding: const EdgeInsets.only(left: 4.0, right: 8.0), + child: Text( + '[promotion] ' + this.promotion.promotionBodyLines[0].title, + style: theme.textTheme.body1, + textAlign: TextAlign.left, + ))), + ]), + ); + } + + @override + Widget build(BuildContext context) { + return new GestureDetector( + onTap: () async { + if (await canLaunch(this.promotion.link)) { + await launch(this.promotion.link); + } + }, + child: Container( + decoration: new BoxDecoration(boxShadow: [ + new BoxShadow( + color: Colors.grey, + blurRadius: 1.0, + ), + ]), + child: new Card( + color: Colors.lightGreen[50], + child: Column( + children: [ + _generateTitleTile(context), + new Divider(color: Colors.black26), + _generateBodyTile(context), + ], + ), + ), + )); + } +} diff --git a/lib/ui/web_search_result_page.dart b/lib/ui/web_search_result_page.dart index 07f7ab3..4e51a31 100644 --- a/lib/ui/web_search_result_page.dart +++ b/lib/ui/web_search_result_page.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import '../search_data_source.dart'; -import '../shared_constant.dart'; import 'no_result_card.dart'; import 'web_search_result_card.dart'; +import 'promotion_card.dart'; @immutable class WebSearchResultPage extends StatefulWidget { @@ -51,9 +51,16 @@ class _WebSearchResultPageState extends State return ListView( shrinkWrap: true, primary: false, - children: searchResults.searchResults.map((searchResult) { - return WebSearchResultCard(searchResult: searchResult); - }).toList()); + children: _buildWebResultWidgetList(searchResults)); + } + + List _buildWebResultWidgetList(SearchResults searchResults) { + List _results = new List(); + _results.addAll(searchResults.promotions + .map((promotion) => PromotionCard(promotion: promotion))); + _results.addAll(searchResults.searchResults.map( + (searchResult) => WebSearchResultCard(searchResult: searchResult))); + return _results; } Widget _buildWebListPage(BuildContext context) {