Fladder/lib/screens/library_search/widgets/suggestion_search_bar.dart
PartyDonut c299492d6d
feat: Android TV support (#503)
Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
2025-09-28 21:07:49 +02:00

194 lines
7 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:page_transition/page_transition.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/providers/library_search_provider.dart';
import 'package:fladder/screens/shared/outlined_text_field.dart';
import 'package:fladder/theme.dart';
import 'package:fladder/util/debouncer.dart';
import 'package:fladder/util/fladder_image.dart';
import 'package:fladder/util/localization_helper.dart';
class SuggestionSearchBar extends ConsumerStatefulWidget {
final String? title;
final bool autoFocus;
final TextEditingController? textEditingController;
final Duration debounceDuration;
final SuggestionsController<ItemBaseModel>? suggestionsBoxController;
final Function(String value)? onSubmited;
final Function(String value)? onChanged;
final Function(ItemBaseModel value)? onItem;
const SuggestionSearchBar({
this.title,
this.autoFocus = false,
this.textEditingController,
this.debounceDuration = const Duration(milliseconds: 250),
this.suggestionsBoxController,
this.onSubmited,
this.onChanged,
this.onItem,
super.key,
});
@override
ConsumerState<ConsumerStatefulWidget> createState() => _SearchBarState();
}
class _SearchBarState extends ConsumerState<SuggestionSearchBar> {
late final Debouncer debouncer = Debouncer(widget.debounceDuration);
late final SuggestionsController<ItemBaseModel> suggestionsBoxController =
widget.suggestionsBoxController ?? SuggestionsController<ItemBaseModel>();
late final TextEditingController textEditingController = widget.textEditingController ?? TextEditingController();
bool isEmpty = true;
final FocusNode focusNode = FocusNode();
@override
void initState() {
super.initState();
if (widget.autoFocus) {
focusNode.requestFocus();
}
super.initState();
}
@override
Widget build(BuildContext context) {
ref.listen(librarySearchProvider(widget.key!).select((value) => value.searchQuery), (previous, next) {
if (textEditingController.text != next) {
setState(() {
textEditingController.text = next;
});
}
});
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: FladderTheme.smallShape.borderRadius,
),
shadowColor: Colors.transparent,
child: TypeAheadField<ItemBaseModel>(
focusNode: focusNode,
hideOnEmpty: isEmpty,
emptyBuilder: (context) => Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
"${context.localized.noSuggestionsFound}...",
style: Theme.of(context).textTheme.titleMedium,
),
),
suggestionsController: suggestionsBoxController,
decorationBuilder: (context, child) => DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.secondaryContainer,
borderRadius: FladderTheme.smallShape.borderRadius,
),
child: child,
),
builder: (context, controller, focusNode) => OutlinedTextField(
focusNode: focusNode,
controller: controller,
onSubmitted: (value) {
widget.onSubmited!(value);
suggestionsBoxController.close();
},
onChanged: (value) {
setState(() {
isEmpty = value.isEmpty;
});
},
placeHolder: widget.title ?? "${context.localized.search}...",
decoration: InputDecoration(
hintText: widget.title ?? "${context.localized.search}...",
prefixIcon: const Icon(IconsaxPlusLinear.search_normal),
contentPadding: const EdgeInsets.only(top: 13),
suffixIcon: controller.text.isNotEmpty
? IconButton(
onPressed: () {
widget.onSubmited?.call('');
controller.text = '';
suggestionsBoxController.close();
setState(() {
isEmpty = true;
});
},
icon: const Icon(Icons.clear))
: null,
border: InputBorder.none,
),
),
loadingBuilder: (context) => const SizedBox(
height: 50,
child: Center(child: CircularProgressIndicator(strokeCap: StrokeCap.round)),
),
onSelected: (suggestion) {
suggestionsBoxController.close();
},
itemBuilder: (context, suggestion) {
return ListTile(
onTap: () {
if (widget.onItem != null) {
widget.onItem?.call(suggestion);
} else {
Navigator.of(context)
.push(PageTransition(child: suggestion.detailScreenWidget, type: PageTransitionType.fade));
}
},
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
title: ConstrainedBox(
constraints: const BoxConstraints(
minHeight: 50,
maxHeight: 65,
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Card(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
child: AspectRatio(
aspectRatio: 0.8,
child: FladderImage(
image: suggestion.images?.primary,
fit: BoxFit.cover,
),
),
),
const SizedBox(width: 8),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Flexible(
child: Text(
suggestion.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
)),
if (suggestion.overview.yearAired.toString().isNotEmpty)
Flexible(
child: Opacity(
opacity: 0.45, child: Text(suggestion.overview.yearAired?.toString() ?? ""))),
],
),
),
],
),
),
),
);
},
suggestionsCallback: (pattern) async {
if (pattern.isEmpty) return [];
if (widget.key != null) {
return (await ref.read(librarySearchProvider(widget.key!).notifier).fetchSuggestions(pattern));
}
return [];
},
),
);
}
}