From 13dda540741c04115e74a9069f33b5e8e77633fb Mon Sep 17 00:00:00 2001 From: Jiexin Guo Date: Thu, 7 Mar 2019 15:07:28 -0500 Subject: [PATCH] Add image refinement to a separate class. Fixed https://github.com/guojiex/flutter_app_cse/issues/20 --- README.md | 2 +- lib/custom_search_search_delegate.dart | 190 +++++------------- lib/search_data_source.dart | 3 +- lib/ui/custom_search_demo_page.dart | 4 +- lib/ui/image_search_result_page.dart | 177 ++++++++++++++++ ..._page.dart => web_search_result_page.dart} | 172 ++++++++-------- 6 files changed, 312 insertions(+), 236 deletions(-) create mode 100644 lib/ui/image_search_result_page.dart rename lib/ui/{search_result_page.dart => web_search_result_page.dart} (50%) diff --git a/README.md b/README.md index 22fe302..fd1a97d 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/F0SX3IMYbv8/hqdefault.jpg)](https://youtu.be/F0SX3IMYbv8) +[![demo_on_youtube](https://img.youtube.com/vi/dXPWOY69XuI/hqdefault.jpg)](https://youtu.be/dXPWOY69XuI) diff --git a/lib/custom_search_search_delegate.dart b/lib/custom_search_search_delegate.dart index ae09180..16ca144 100644 --- a/lib/custom_search_search_delegate.dart +++ b/lib/custom_search_search_delegate.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_app_cse/search_data_source.dart'; -import 'ui/search_result_page.dart'; +import 'ui/web_search_result_page.dart'; import 'ui/no_result_card.dart'; import 'ui/web_search_result_card.dart'; import 'ui/image_search_result_card.dart'; import 'ui/pagination_tab.dart'; +import 'ui/image_search_result_page.dart'; import 'shared_constant.dart'; /// A [SearchDelegate] for search using CSE API. @@ -240,154 +241,59 @@ class CustomSearchInfiniteSearchDelegate extends CustomSearchSearchDelegate { void close(BuildContext context, SearchResult result) { this.currentSearchResults = null; this.currentResultLength = 0; - this.refinementBar = null; super.close(context, result); } - _loadNextPage() { - dataSource.search(currentSearchResults.nextPage).then((value) { - this.currentSearchResults = value; - this.currentResultLength += - this.currentSearchResults.searchResults.length; - debugPrint( - 'current result length ${this.currentSearchResults.searchResults - .length}'); - }); - } - - Widget _buildImageGridPage(BuildContext context, SearchQuery searchQuery) { - return GridView.builder( - shrinkWrap: true, - primary: false, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 1, - ), - itemCount: 99, - itemBuilder: (_, index) { - if (this.currentSearchResults == null) { - return FutureBuilder( - future: dataSource.search(searchQuery), - builder: (context, snapshot) { - switch (snapshot.connectionState) { - case ConnectionState.none: - case ConnectionState.active: - case ConnectionState.waiting: - return SizedBox( - height: MediaQuery - .of(context) - .size - .height * 2, - child: Align( - alignment: Alignment.topCenter, - child: Padding( - padding: EdgeInsets.only(top: 5.0), - child: CircularProgressIndicator(), - )), - ); - case ConnectionState.done: - if (snapshot.hasError) { - return Text('Error: ${snapshot.error}'); - } - this.currentSearchResults = snapshot.data; - if (currentSearchResults.searchResults.isEmpty) { - return GridView.count( - crossAxisCount: 1, - mainAxisSpacing: 4.0, - crossAxisSpacing: 4.0, - padding: const EdgeInsets.all(4.0), - children: [new NoResultCard()], - ); - } - return ImageSearchResultCard( - searchResult: this.currentSearchResults.searchResults[ - index % - this - .currentSearchResults - .searchResults - .length]); - } - }); - } - if (index >= currentResultLength) { - this._loadNextPage(); - } - return ImageSearchResultCard( - searchResult: this.currentSearchResults.searchResults[ - index % this.currentSearchResults.searchResults.length]); - }); - } - - Widget refinementBar; - - Widget _buildWebSearchResultSubList(SearchResults searchResults, - Widget injectedBar) { - if (injectedBar != null) { - return Column( - children: [ - injectedBar, - ListView( - shrinkWrap: true, - primary: false, - children: searchResults.searchResults.map((searchResult) { - return WebSearchResultCard(searchResult: searchResult); - }).toList()) - ], - ); - } else { - return ListView( - shrinkWrap: true, - primary: false, - children: searchResults.searchResults.map((searchResult) { - return WebSearchResultCard(searchResult: searchResult); - }).toList()); - } + Widget _buildSearchResultPage(BuildContext context, SearchQuery searchQuery) { + return FutureBuilder( + future: dataSource.search(searchQuery), + builder: (context, snapshot) { + switch (snapshot.connectionState) { + case ConnectionState.none: + return Text('Press button to start.'); + case ConnectionState.active: + case ConnectionState.waiting: + return SizedBox( + height: MediaQuery + .of(context) + .size + .height * 2, + child: Align( + alignment: Alignment.topCenter, + child: Padding( + padding: EdgeInsets.only(top: 5.0), + child: CircularProgressIndicator(), + )), + ); + case ConnectionState.done: + if (snapshot.hasError) { + return Text('Error: ${snapshot.error}'); + } + if (snapshot.data.searchResults.isEmpty) { + return GridView.count( + crossAxisCount: 1, + mainAxisSpacing: 4.0, + crossAxisSpacing: 4.0, + padding: const EdgeInsets.all(4.0), + children: [new NoResultCard()], + ); + } + switch (this.searchType) { + case SearchType.image: + return ImageSearchResultPage( + dataSource, snapshot.data, searchQuery); + case SearchType.web: + return WebSearchResultPage( + dataSource, snapshot.data, searchQuery); + } + } + }, + ); } @override Widget buildResultsFromQuery(BuildContext context, SearchQuery searchQuery) { - switch (this.searchType) { - case SearchType.image: - return _buildImageGridPage(context, searchQuery); - case SearchType.web: - return FutureBuilder( - future: dataSource.search(searchQuery), - builder: (context, snapshot) { - switch (snapshot.connectionState) { - case ConnectionState.none: - return Text('Press button to start.'); - case ConnectionState.active: - case ConnectionState.waiting: - return SizedBox( - height: MediaQuery - .of(context) - .size - .height * 2, - child: Align( - alignment: Alignment.topCenter, - child: Padding( - padding: EdgeInsets.only(top: 5.0), - child: CircularProgressIndicator(), - )), - ); - case ConnectionState.done: - if (snapshot.hasError) { - return Text('Error: ${snapshot.error}'); - } - if (snapshot.data.searchResults.isEmpty) { - return GridView.count( - crossAxisCount: 1, - mainAxisSpacing: 4.0, - crossAxisSpacing: 4.0, - padding: const EdgeInsets.all(4.0), - children: [new NoResultCard()], - ); - } - return SearchResultPage( - dataSource, searchType, snapshot.data, searchQuery); - } - }, - ); - } + return _buildSearchResultPage(context, searchQuery); } } diff --git a/lib/search_data_source.dart b/lib/search_data_source.dart index 664a258..fabe486 100644 --- a/lib/search_data_source.dart +++ b/lib/search_data_source.dart @@ -449,7 +449,8 @@ class CustomSearchDataSource implements SearchDataSource { final ExpireCache _cache = ExpireCache(); - CustomSearchDataSource({@required this.cx, @required this.apiKey}) { + CustomSearchDataSource({@required this.cx, @required this.apiKey}) + :assert(apiKey.isNotEmpty) { var client = auth.clientViaApiKey(apiKey); this.api = new customsearch.CustomsearchApi(client); } diff --git a/lib/ui/custom_search_demo_page.dart b/lib/ui/custom_search_demo_page.dart index 5b9d2ab..285b012 100644 --- a/lib/ui/custom_search_demo_page.dart +++ b/lib/ui/custom_search_demo_page.dart @@ -66,10 +66,10 @@ class _CustomSearchDemoPageState extends State { } _CustomSearchDemoPageState.customImageSearch() { - // Pokemon db. + // Pokemon db with refinement. this.delegate = new CustomSearchInfiniteSearchDelegate.imageSearch( dataSource: CustomSearchDataSource( - cx: '013098254965507895640:wyytcpldjbw', + cx: '013098254965507895640:g-r0nurxf2g', apiKey: '')); this.hintText = 'Google Custom Image Search'; otherRoutes = [ diff --git a/lib/ui/image_search_result_page.dart b/lib/ui/image_search_result_page.dart new file mode 100644 index 0000000..897a111 --- /dev/null +++ b/lib/ui/image_search_result_page.dart @@ -0,0 +1,177 @@ +import 'package:flutter/material.dart'; + +import '../search_data_source.dart'; +import '../shared_constant.dart'; +import 'no_result_card.dart'; +import 'image_search_result_card.dart'; + +@immutable +class ImageSearchResultPage extends StatefulWidget { + final SearchDataSource dataSource; + final SearchResults initialSearchResult; + SearchQuery searchQuery; + final Refinement currentRefinement; + final bool withRefinementTabBar; + + ImageSearchResultPage( + this.dataSource, this.initialSearchResult, this.searchQuery, + {this.currentRefinement, this.withRefinementTabBar = true}) { + this.searchQuery = this.searchQuery.copyWith(searchType: 'image'); + } + + @override + _ImageSearchResultPageState createState() => + new _ImageSearchResultPageState(initialSearchResult); +} + +class _ImageSearchResultPageState extends State + with SingleTickerProviderStateMixin { + SearchResults initialSearchResult; + SearchResults currentSearchResults; + TabController _tabController; + List _refinementTabs = new List(); + int currentResultLength = 0; + + _ImageSearchResultPageState(this.initialSearchResult) { + if (initialSearchResult.refinements.isNotEmpty) { + _refinementTabs.add(Tab(text: 'all')); + initialSearchResult.refinements.forEach( + (refinement) => _refinementTabs.add(Tab(text: refinement.label))); + _tabController = + TabController(vsync: this, length: _refinementTabs.length); + } + } + + @override + void dispose() { + currentSearchResults = null; + this.currentSearchResults = null; + this._refinementTabs.clear(); + _tabController?.dispose(); + super.dispose(); + } + + _loadNextPage() { + widget.dataSource.search(currentSearchResults.nextPage).then((value) { + this.currentSearchResults = value; + this.currentResultLength += + this.currentSearchResults.searchResults.length; + debugPrint( + 'current result length ${this.currentSearchResults.searchResults.length}'); + }); + } + + Widget _buildImageGridPage(BuildContext context) { + return GridView.builder( + shrinkWrap: true, + primary: false, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 1, + ), + itemCount: 99, + itemBuilder: (_, index) { + if (index == 0) { + this.currentSearchResults = initialSearchResult; + currentResultLength += + this.currentSearchResults.searchResults.length; + if (currentSearchResults.searchResults.isEmpty) { + return GridView.count( + crossAxisCount: 1, + mainAxisSpacing: 4.0, + crossAxisSpacing: 4.0, + padding: const EdgeInsets.all(4.0), + children: [new NoResultCard()], + ); + } + } + if (index >= currentResultLength) { + this._loadNextPage(); + } + return ImageSearchResultCard( + searchResult: this.currentSearchResults.searchResults[ + index % this.currentSearchResults.searchResults.length]); + }); + } + + @override + Widget build(BuildContext context) { + if (this._refinementTabs.isEmpty || !widget.withRefinementTabBar) { + return _buildImageGridPage(context); + } + if (widget.withRefinementTabBar) { + return Scaffold( + appBar: new PreferredSize( + preferredSize: Size.fromHeight(kToolbarHeight), + child: new Container( + color: Colors.blue, + child: new SafeArea( + child: Column( + children: [ + new TabBar( + isScrollable: true, + controller: _tabController, + tabs: _refinementTabs, + ), + ], + ), + ), + ), + ), + body: TabBarView( + controller: _tabController, + children: _refinementTabs.map((Tab tab) { + if (tab.text == 'all') { + return _buildImageGridPage(context); + } else { + final currentRefinement = initialSearchResult.refinements + .firstWhere((element) => element.label == tab.text); + final query = + widget.searchQuery.q + " " + currentRefinement.labelWithOp; + print(query); + return FutureBuilder( + future: widget.dataSource + .search(widget.searchQuery.copyWith(q: query)), + builder: (context, snapshot) { + switch (snapshot.connectionState) { + case ConnectionState.none: + return Text('Press button to start.'); + case ConnectionState.active: + case ConnectionState.waiting: + return SizedBox( + height: MediaQuery.of(context).size.height * 2, + child: Align( + alignment: Alignment.topCenter, + child: Padding( + padding: EdgeInsets.only(top: 5.0), + child: CircularProgressIndicator(), + )), + ); + case ConnectionState.done: + if (snapshot.hasError) { + return Text('Error: ${snapshot.error}'); + } + if (snapshot.data.searchResults.isEmpty) { + return GridView.count( + crossAxisCount: 1, + mainAxisSpacing: 4.0, + crossAxisSpacing: 4.0, + padding: const EdgeInsets.all(4.0), + children: [new NoResultCard()], + ); + } + return ImageSearchResultPage( + widget.dataSource, + snapshot.data, + widget.searchQuery, + currentRefinement: currentRefinement, + withRefinementTabBar: false, + ); + } + }, + ); + } + }).toList(), + )); + } + } +} diff --git a/lib/ui/search_result_page.dart b/lib/ui/web_search_result_page.dart similarity index 50% rename from lib/ui/search_result_page.dart rename to lib/ui/web_search_result_page.dart index 4ebcbb2..07f7ab3 100644 --- a/lib/ui/search_result_page.dart +++ b/lib/ui/web_search_result_page.dart @@ -6,31 +6,30 @@ import 'no_result_card.dart'; import 'web_search_result_card.dart'; @immutable -class SearchResultPage extends StatefulWidget { +class WebSearchResultPage extends StatefulWidget { final SearchDataSource dataSource; - final SearchType searchType; final SearchResults initialSearchResult; final SearchQuery searchQuery; final Refinement currentRefinement; final bool withRefinementTabBar; - SearchResultPage(this.dataSource, this.searchType, this.initialSearchResult, + WebSearchResultPage(this.dataSource, this.initialSearchResult, this.searchQuery, {this.currentRefinement, this.withRefinementTabBar = true}); @override - _SearchResultPageState createState() => - new _SearchResultPageState(initialSearchResult); + _WebSearchResultPageState createState() => + new _WebSearchResultPageState(initialSearchResult); } -class _SearchResultPageState extends State +class _WebSearchResultPageState extends State with SingleTickerProviderStateMixin { SearchResults initialSearchResult; SearchResults currentSearchResults; TabController _tabController; List _refinementTabs = new List(); - _SearchResultPageState(this.initialSearchResult) { + _WebSearchResultPageState(this.initialSearchResult) { if (initialSearchResult.refinements.isNotEmpty) { _refinementTabs.add(Tab(text: 'all')); initialSearchResult.refinements.forEach( @@ -120,93 +119,86 @@ class _SearchResultPageState extends State @override Widget build(BuildContext context) { - switch (widget.searchType) { - case SearchType.image: - return Text('not implemented'); - case SearchType.web: - if (this._refinementTabs.isEmpty || !widget.withRefinementTabBar) { - return _buildWebListPage(context); - } - if (widget.withRefinementTabBar) { - return Scaffold( - appBar: new PreferredSize( - preferredSize: Size.fromHeight(kToolbarHeight), - child: new Container( - color: Colors.blue, - child: new SafeArea( - child: Column( - children: [ - new TabBar( - isScrollable: true, - controller: _tabController, - tabs: _refinementTabs, - ), - ], + if (this._refinementTabs.isEmpty || !widget.withRefinementTabBar) { + return _buildWebListPage(context); + } + if (widget.withRefinementTabBar) { + return Scaffold( + appBar: new PreferredSize( + preferredSize: Size.fromHeight(kToolbarHeight), + child: new Container( + color: Colors.blue, + child: new SafeArea( + child: Column( + children: [ + new TabBar( + isScrollable: true, + controller: _tabController, + tabs: _refinementTabs, ), - ), + ], ), ), - body: TabBarView( - controller: _tabController, - children: _refinementTabs.map((Tab tab) { - if (tab.text == 'all') { - return _buildWebListPage(context); - } else { - final currentRefinement = initialSearchResult.refinements - .firstWhere((element) => element.label == tab.text); - final query = widget.searchQuery.q + - " " + - currentRefinement.labelWithOp; - print(query); - return FutureBuilder( - future: widget.dataSource - .search(widget.searchQuery.copyWith(q: query)), - builder: (context, snapshot) { - switch (snapshot.connectionState) { - case ConnectionState.none: - return Text('Press button to start.'); - case ConnectionState.active: - case ConnectionState.waiting: - return SizedBox( - height: MediaQuery - .of(context) - .size - .height * 2, - child: Align( - alignment: Alignment.topCenter, - child: Padding( - padding: EdgeInsets.only(top: 5.0), - child: CircularProgressIndicator(), - )), - ); - case ConnectionState.done: - if (snapshot.hasError) { - return Text('Error: ${snapshot.error}'); - } - if (snapshot.data.searchResults.isEmpty) { - return GridView.count( - crossAxisCount: 1, - mainAxisSpacing: 4.0, - crossAxisSpacing: 4.0, - padding: const EdgeInsets.all(4.0), - children: [new NoResultCard()], - ); - } - return SearchResultPage( - widget.dataSource, - widget.searchType, - snapshot.data, - widget.searchQuery, - currentRefinement: currentRefinement, - withRefinementTabBar: false, - ); + ), + ), + body: TabBarView( + controller: _tabController, + children: _refinementTabs.map((Tab tab) { + if (tab.text == 'all') { + return _buildWebListPage(context); + } else { + final currentRefinement = initialSearchResult.refinements + .firstWhere((element) => element.label == tab.text); + final query = + widget.searchQuery.q + " " + currentRefinement.labelWithOp; + print(query); + return FutureBuilder( + future: widget.dataSource + .search(widget.searchQuery.copyWith(q: query)), + builder: (context, snapshot) { + switch (snapshot.connectionState) { + case ConnectionState.none: + return Text('Press button to start.'); + case ConnectionState.active: + case ConnectionState.waiting: + return SizedBox( + height: MediaQuery + .of(context) + .size + .height * 2, + child: Align( + alignment: Alignment.topCenter, + child: Padding( + padding: EdgeInsets.only(top: 5.0), + child: CircularProgressIndicator(), + )), + ); + case ConnectionState.done: + if (snapshot.hasError) { + return Text('Error: ${snapshot.error}'); } - }, - ); - } - }).toList(), - )); - } + if (snapshot.data.searchResults.isEmpty) { + return GridView.count( + crossAxisCount: 1, + mainAxisSpacing: 4.0, + crossAxisSpacing: 4.0, + padding: const EdgeInsets.all(4.0), + children: [new NoResultCard()], + ); + } + return WebSearchResultPage( + widget.dataSource, + snapshot.data, + widget.searchQuery, + currentRefinement: currentRefinement, + withRefinementTabBar: false, + ); + } + }, + ); + } + }).toList(), + )); } } }