// Author: fruit.com.cn import 'dart:async'; import 'package:flutter/material.dart'; import '../../core/data/models/drama.dart'; import '../../core/data/remote/api_exception.dart'; import '../../core/data/repositories/search_repository.dart'; import 'discover_types.dart'; /// 剧场页搜索状态:服务端检索、防抖与错误态。 class DiscoverLogic extends ChangeNotifier { DiscoverLogic({this.searchRepository}); final SearchRepository? searchRepository; final TextEditingController search = TextEditingController(); final FocusNode searchFocus = FocusNode(); final ScrollController scroll = ScrollController(); /// 搜索防抖与分页口径:与后端 `/app/search` 一致。 Duration searchDebounce = const Duration(milliseconds: 350); static const int searchPageSize = 20; List searchResults = const []; bool searchLoading = false; String? searchError; Timer? _searchTimer; String _lastRequestedQuery = ''; int _searchRequest = 0; bool _disposed = false; String? selectedCategoryId; bool get hasSearchQuery => search.text.trim().isNotEmpty; void notify() { if (!_disposed) notifyListeners(); } void onSearchChanged() { final query = search.text.trim(); if (query.isEmpty) { _resetSearch(); notify(); return; } notify(); _scheduleSearch(query); } void openSearch() => searchFocus.requestFocus(); /// 选择分类;空值表示全部。 void selectCategory(String? categoryId) { if (selectedCategoryId == categoryId) return; selectedCategoryId = categoryId; notify(); } void retrySearch() { final query = search.text.trim(); if (query.isNotEmpty) unawaited(_runSearch(query)); } void _resetSearch() { _searchTimer?.cancel(); _searchRequest++; searchResults = const []; searchLoading = false; searchError = null; _lastRequestedQuery = ''; } void _scheduleSearch(String query) { final alreadyRequested = query == _lastRequestedQuery; if (alreadyRequested && (searchLoading || searchResults.isNotEmpty || searchError != null)) { return; } _lastRequestedQuery = query; _searchTimer?.cancel(); if (searchDebounce == Duration.zero) { unawaited(_runSearch(query)); return; } _searchTimer = Timer(searchDebounce, () => unawaited(_runSearch(query))); } Future _runSearch(String query) async { final repository = searchRepository; if (repository == null) return; final request = ++_searchRequest; searchLoading = true; searchError = null; notify(); try { final result = await repository.search( query, pageIndex: 1, pageSize: searchPageSize, ); if (request != _searchRequest) return; searchResults = result.list; } on ApiException catch (exception) { _applySearchError(request, exception.message); } catch (_) { _applySearchError(request, 'searchFailed'); } finally { if (request == _searchRequest) { searchLoading = false; notify(); } } } void _applySearchError(int request, String message) { if (request != _searchRequest) return; searchError = message; searchResults = const []; } /// 从播放器发起的搜索请求只在剧场页可见时抢焦点。 void syncSearch({ required bool active, required int request, required int previousRequest, required bool Function() isActive, }) { if (!active || request == previousRequest) return; WidgetsBinding.instance.addPostFrameCallback((_) { if (isActive()) searchFocus.requestFocus(); }); WidgetsBinding.instance.scheduleFrame(); } void act( BuildContext context, DiscoverPageAction action, { required bool embedded, ValueChanged? onAction, }) { if (embedded) { onAction?.call(action); } else { Navigator.pop(context, action); } } @override void dispose() { _disposed = true; _searchTimer?.cancel(); search.dispose(); searchFocus.dispose(); scroll.dispose(); super.dispose(); } }