Explore only reached search through an app-bar icon that pushed a separate screen. Touch and pointer builds now carry the field inline under the app bar: results replace the shelves while the query is non-empty and the shelves return when it clears. TV keeps pushing CatalogSearchScreen, since a text field cannot share the spotlight scaffold with the bottom-pinned browse rail and the on-screen keyboard. Pull-to-refresh and the toolbar refresh action re-run the live query instead of reloading hidden rows, and switching catalog source re-runs the query against the new source rather than leaving the previous source's results under its name.
104 lines
3.6 KiB
Dart
104 lines
3.6 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:material_symbols_icons/symbols.dart';
|
|
|
|
import '../i18n/strings.g.dart';
|
|
import '../media/media_item.dart';
|
|
import '../mixins/debounced_media_search.dart';
|
|
import '../services/catalog/catalog_source.dart';
|
|
import '../utils/focus_utils.dart';
|
|
import '../utils/platform_detector.dart';
|
|
import '../widgets/focusable_media_card.dart';
|
|
import '../widgets/focused_scroll_scaffold.dart';
|
|
import '../widgets/loading_indicator_box.dart';
|
|
import '../widgets/search_input_field.dart';
|
|
import 'libraries/state_messages.dart';
|
|
|
|
/// Free-text search of one catalog source (the Explore tab's active source),
|
|
/// pushed from the Explore TV toolbar — touch/pointer builds search inline on
|
|
/// the Explore page instead. Results are catalog items rendered through the
|
|
/// synthesized-MediaItem card stack, so taps land on the catalog detail
|
|
/// screen with library matching, exactly like the Explore rows.
|
|
class CatalogSearchScreen extends StatefulWidget {
|
|
final CatalogSource source;
|
|
|
|
const CatalogSearchScreen({super.key, required this.source});
|
|
|
|
@override
|
|
State<CatalogSearchScreen> createState() => _CatalogSearchScreenState();
|
|
}
|
|
|
|
class _CatalogSearchScreenState extends State<CatalogSearchScreen> with DebouncedMediaSearch {
|
|
@override
|
|
String get searchDebugLabel => 'CatalogSearch';
|
|
|
|
@override
|
|
Future<List<MediaItem>> performSearchQuery(String query) async {
|
|
final items = await widget.source.search(query);
|
|
return [for (final item in items) item.toMediaItem()];
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
FocusUtils.requestFocusAfterBuild(this, searchFocusNode);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final sourceName = widget.source.displayName;
|
|
return FocusedScrollScaffold(
|
|
title: Text(t.explore.searchHint(source: sourceName)),
|
|
slivers: [
|
|
SliverToBoxAdapter(
|
|
child: SearchInputField(
|
|
controller: searchController,
|
|
focusNode: searchFocusNode,
|
|
debugLabel: searchDebugLabel,
|
|
hintText: t.explore.searchHint(source: sourceName),
|
|
onNavigateDown: searchResults.isNotEmpty && !isSearching ? firstResultFocusNode.requestFocus : null,
|
|
onEditingComplete: PlatformDetector.isTV() ? handleSearchSubmit : null,
|
|
),
|
|
),
|
|
if (isSearching)
|
|
LoadingIndicatorBox.sliver
|
|
else if (!hasSearched)
|
|
SliverFillRemaining(
|
|
child: StateMessageWidget(
|
|
message: t.explore.searchPrompt(source: sourceName),
|
|
icon: Symbols.search_rounded,
|
|
iconSize: 80,
|
|
),
|
|
)
|
|
else if (lastSearchFailed)
|
|
SliverFillRemaining(
|
|
child: StateMessageWidget(message: t.explore.searchFailed, icon: Symbols.error_rounded, iconSize: 80),
|
|
)
|
|
else if (searchResults.isEmpty)
|
|
SliverFillRemaining(
|
|
child: StateMessageWidget(
|
|
message: t.explore.searchEmpty(query: lastSearchedQuery),
|
|
icon: Symbols.search_off_rounded,
|
|
iconSize: 80,
|
|
),
|
|
)
|
|
else
|
|
_buildResultsList(),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildResultsList() {
|
|
return buildResultsSliver((context, index) {
|
|
final item = searchResults[index];
|
|
return FocusableMediaCard(
|
|
key: Key(item.globalKey),
|
|
item: item,
|
|
forceListMode: true,
|
|
disableScale: true,
|
|
focusNode: index == 0 ? firstResultFocusNode : null,
|
|
onNavigateUp: index == 0 ? searchFocusNode.requestFocus : null,
|
|
);
|
|
});
|
|
}
|
|
}
|