make search page type to search
This commit is contained in:
parent
1ebf6f32e0
commit
8b54b3ba1b
5 changed files with 103 additions and 1 deletions
|
|
@ -41,7 +41,7 @@ class _SearchState extends State<Search> {
|
|||
Positioned.fill(
|
||||
child: Observer(
|
||||
builder: (context) {
|
||||
if (_searchStore.textEditingController.text.isEmpty) {
|
||||
if (_searchStore.searchText.isEmpty) {
|
||||
if (_searchStore.searchHistory.isEmpty) {
|
||||
// Keep controller attached so borders stay in sync
|
||||
return CustomScrollView(
|
||||
|
|
@ -202,6 +202,7 @@ class _SearchState extends State<Search> {
|
|||
controller: _searchStore.textEditingController,
|
||||
focusNode: _searchStore.textFieldFocusNode,
|
||||
autocorrect: false,
|
||||
onChanged: _searchStore.onSearchTextChanged,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.search_rounded),
|
||||
hintText: 'Search channels or categories',
|
||||
|
|
@ -218,6 +219,7 @@ class _SearchState extends State<Search> {
|
|||
_searchStore.textFieldFocusNode.unfocus();
|
||||
}
|
||||
_searchStore.textEditingController.clear();
|
||||
_searchStore.onSearchTextChanged('');
|
||||
// Ensure borders reset: scroll back to top and trigger listeners
|
||||
if (widget.scrollController.hasClients) {
|
||||
widget.scrollController.animateTo(
|
||||
|
|
|
|||
|
|
@ -18,7 +18,14 @@ class SearchResultsCategories extends StatelessWidget {
|
|||
builder: (context) {
|
||||
final future = searchStore.categoryFuture;
|
||||
|
||||
// Show skeletons immediately while waiting for debounce.
|
||||
if (future == null) {
|
||||
if (searchStore.isSearching) {
|
||||
return SliverList.builder(
|
||||
itemCount: 8,
|
||||
itemBuilder: (context, index) => const CategorySkeletonLoader(),
|
||||
);
|
||||
}
|
||||
return const SliverToBoxAdapter(child: SizedBox.shrink());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -72,7 +72,15 @@ class _SearchResultsChannelsState extends State<SearchResultsChannels> {
|
|||
builder: (context) {
|
||||
final future = widget.searchStore.channelFuture;
|
||||
|
||||
// Show skeletons immediately while waiting for debounce.
|
||||
if (future == null) {
|
||||
if (widget.searchStore.isSearching) {
|
||||
return SliverList.builder(
|
||||
itemCount: 8,
|
||||
itemBuilder: (context, index) =>
|
||||
ChannelSkeletonLoader(index: index),
|
||||
);
|
||||
}
|
||||
return const SliverToBoxAdapter(child: SizedBox.shrink());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:frosty/apis/twitch_api.dart';
|
||||
import 'package:frosty/models/category.dart';
|
||||
|
|
@ -19,12 +21,17 @@ abstract class SearchStoreBase with Store {
|
|||
|
||||
final textFieldFocusNode = FocusNode();
|
||||
|
||||
Timer? _debounceTimer;
|
||||
|
||||
@readonly
|
||||
var _searchText = '';
|
||||
|
||||
@readonly
|
||||
var _searchHistory = ObservableList<String>();
|
||||
|
||||
@readonly
|
||||
var _isSearching = false;
|
||||
|
||||
@readonly
|
||||
ObservableFuture<List<ChannelQuery>>? _channelFuture;
|
||||
|
||||
|
|
@ -57,15 +64,48 @@ abstract class SearchStoreBase with Store {
|
|||
);
|
||||
}
|
||||
|
||||
/// Debounced handler for search-as-you-type.
|
||||
@action
|
||||
void onSearchTextChanged(String query) {
|
||||
_debounceTimer?.cancel();
|
||||
|
||||
if (query.isEmpty) {
|
||||
_isSearching = false;
|
||||
_channelFuture = null;
|
||||
_categoryFuture = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading state immediately for responsive feedback.
|
||||
_isSearching = true;
|
||||
|
||||
_debounceTimer = Timer(const Duration(milliseconds: 300), () {
|
||||
runInAction(() => _performSearch(query));
|
||||
});
|
||||
}
|
||||
|
||||
/// Obtain the channels and categories that match the provided [query].
|
||||
/// Adds the query to search history.
|
||||
@action
|
||||
void handleQuery(String query) {
|
||||
if (query.isEmpty) return;
|
||||
|
||||
// Cancel any pending debounce since we're searching immediately.
|
||||
_debounceTimer?.cancel();
|
||||
|
||||
// Move the query to the most recent result (top of the stack).
|
||||
_searchHistory.remove(query);
|
||||
_searchHistory.insert(0, query);
|
||||
|
||||
_performSearch(query);
|
||||
}
|
||||
|
||||
/// Performs the actual search API calls.
|
||||
@action
|
||||
void _performSearch(String query) {
|
||||
// Futures are now pending, so isSearching can be cleared.
|
||||
_isSearching = false;
|
||||
|
||||
// Fetch the matching channels, sort it by live status, and then set it.
|
||||
_channelFuture = twitchApi.searchChannels(query: query).then((channels) {
|
||||
channels.sort((c1, c2) => c2.isLive ? 1 : -1);
|
||||
|
|
@ -96,6 +136,7 @@ abstract class SearchStoreBase with Store {
|
|||
}
|
||||
|
||||
void dispose() {
|
||||
_debounceTimer?.cancel();
|
||||
textEditingController.dispose();
|
||||
textFieldFocusNode.dispose();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,26 @@ mixin _$SearchStore on SearchStoreBase, Store {
|
|||
});
|
||||
}
|
||||
|
||||
late final _$_isSearchingAtom = Atom(
|
||||
name: 'SearchStoreBase._isSearching',
|
||||
context: context,
|
||||
);
|
||||
|
||||
bool get isSearching {
|
||||
_$_isSearchingAtom.reportRead();
|
||||
return super._isSearching;
|
||||
}
|
||||
|
||||
@override
|
||||
bool get _isSearching => isSearching;
|
||||
|
||||
@override
|
||||
set _isSearching(bool value) {
|
||||
_$_isSearchingAtom.reportWrite(value, super._isSearching, () {
|
||||
super._isSearching = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$_channelFutureAtom = Atom(
|
||||
name: 'SearchStoreBase._channelFuture',
|
||||
context: context,
|
||||
|
|
@ -104,6 +124,18 @@ mixin _$SearchStore on SearchStoreBase, Store {
|
|||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
void onSearchTextChanged(String query) {
|
||||
final _$actionInfo = _$SearchStoreBaseActionController.startAction(
|
||||
name: 'SearchStoreBase.onSearchTextChanged',
|
||||
);
|
||||
try {
|
||||
return super.onSearchTextChanged(query);
|
||||
} finally {
|
||||
_$SearchStoreBaseActionController.endAction(_$actionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void handleQuery(String query) {
|
||||
final _$actionInfo = _$SearchStoreBaseActionController.startAction(
|
||||
|
|
@ -116,6 +148,18 @@ mixin _$SearchStore on SearchStoreBase, Store {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void _performSearch(String query) {
|
||||
final _$actionInfo = _$SearchStoreBaseActionController.startAction(
|
||||
name: 'SearchStoreBase._performSearch',
|
||||
);
|
||||
try {
|
||||
return super._performSearch(query);
|
||||
} finally {
|
||||
_$SearchStoreBaseActionController.endAction(_$actionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue