fix(search): recover omitted Plex media categories

close #1598
This commit is contained in:
edde746
2026-07-26 14:58:38 +02:00
parent 8ef977d890
commit 1fea9ef6e3
30 changed files with 512 additions and 50 deletions
+17 -2
View File
@@ -1,6 +1,7 @@
import 'dart:async';
import 'package:flutter/widgets.dart';
import '../exceptions/media_server_exceptions.dart';
import '../media/media_item.dart';
import '../utils/app_logger.dart';
@@ -39,7 +40,7 @@ mixin DebouncedMediaSearch<T extends StatefulWidget> on State<T> {
/// Names the focus nodes and log lines.
String get searchDebugLabel => widget.runtimeType.toString();
/// Run the actual search. Thrown errors flip [lastSearchFailed].
/// Run the actual search. Non-cancellation errors flip [lastSearchFailed].
Future<List<MediaItem>> performSearchQuery(String query);
/// A failed search was applied to the state (e.g. show a snackbar).
@@ -51,6 +52,11 @@ mixin DebouncedMediaSearch<T extends StatefulWidget> on State<T> {
/// The field was cleared and the state reset.
void onSearchCleared() {}
/// The active query was superseded or the search scope is being disposed.
/// Implementations may cancel transport work here; the generation guard
/// remains authoritative for preventing stale UI commits.
void onSearchInvalidated() {}
@override
void initState() {
super.initState();
@@ -60,6 +66,7 @@ mixin DebouncedMediaSearch<T extends StatefulWidget> on State<T> {
@override
void dispose() {
_debounceTimer?.cancel();
onSearchInvalidated();
searchController.removeListener(_onSearchTextChanged);
searchController.dispose();
searchFocusNode.dispose();
@@ -88,6 +95,7 @@ mixin DebouncedMediaSearch<T extends StatefulWidget> on State<T> {
if (query.isEmpty) {
_debounceTimer?.cancel();
_searchGeneration++;
if (_inFlightQuery != null) onSearchInvalidated();
_inFlightQuery = null;
setState(() {
searchResults = [];
@@ -118,6 +126,7 @@ mixin DebouncedMediaSearch<T extends StatefulWidget> on State<T> {
bool _invalidateStaleInFlight(String current) {
if (_inFlightQuery == null || _inFlightQuery == current) return false;
_searchGeneration++;
onSearchInvalidated();
_inFlightQuery = null;
return true;
}
@@ -125,6 +134,7 @@ mixin DebouncedMediaSearch<T extends StatefulWidget> on State<T> {
/// Run [query] now, bypassing the debounce (submit, external refresh).
Future<void> runSearch(String query) async {
if (!mounted || query.isEmpty) return;
if (_inFlightQuery != null) onSearchInvalidated();
final generation = ++_searchGeneration;
_inFlightQuery = query;
setState(() {
@@ -143,8 +153,13 @@ mixin DebouncedMediaSearch<T extends StatefulWidget> on State<T> {
});
onSearchCompleted(query, results);
} catch (e) {
appLogger.w('$searchDebugLabel: search failed', error: e);
if (!mounted || generation != _searchGeneration) return;
if (e is MediaServerHttpException && e.isCancellation) {
_inFlightQuery = null;
setState(() => isSearching = false);
return;
}
appLogger.w('$searchDebugLabel: search failed', error: e);
_inFlightQuery = null;
setState(() {
searchResults = [];