feat(app): 剧场页九宫格与极简我的页
This commit is contained in:
1 parent
dc8ca7b743
commit
35338cbfc8
46 files changed
+2353
-4358
No files matched your search
@@ -5,39 +5,23 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../core/data/models/drama.dart';
|
||||
import '../../core/data/models/watch_progress.dart';
|
||||
import '../../core/data/remote/api_exception.dart';
|
||||
import '../../core/data/repositories/search_repository.dart';
|
||||
import '../../core/data/repositories/watch_history_repository.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
|
||||
enum DiscoverPageAction { home, my, watchlist, detail, resume }
|
||||
import 'discover_types.dart';
|
||||
|
||||
/// 剧场页搜索状态:服务端检索、防抖与错误态。
|
||||
class DiscoverLogic extends ChangeNotifier {
|
||||
DiscoverLogic({
|
||||
required this.history,
|
||||
required this.drama,
|
||||
this.searchRepository,
|
||||
}) {
|
||||
refreshPosition();
|
||||
}
|
||||
DiscoverLogic({this.searchRepository});
|
||||
|
||||
final WatchHistoryRepository history;
|
||||
final SearchRepository? searchRepository;
|
||||
Drama drama;
|
||||
late Future<Duration?> position;
|
||||
WatchProgress? resumeRecord;
|
||||
final TextEditingController search = TextEditingController();
|
||||
final FocusNode searchFocus = FocusNode();
|
||||
final ScrollController scroll = ScrollController();
|
||||
bool searchOpen = false;
|
||||
|
||||
/// 搜索防抖与分页口径:与后端 `/app/search` 一致。
|
||||
Duration searchDebounce = const Duration(milliseconds: 350);
|
||||
static const int searchPageSize = 20;
|
||||
List<Drama> searchResults = const [];
|
||||
String? selectedLanguage;
|
||||
int searchTotal = 0;
|
||||
bool searchLoading = false;
|
||||
String? searchError;
|
||||
Timer? _searchTimer;
|
||||
@@ -45,36 +29,12 @@ class DiscoverLogic extends ChangeNotifier {
|
||||
int _searchRequest = 0;
|
||||
bool _disposed = false;
|
||||
|
||||
bool get hasSearchQuery => search.text.trim().isNotEmpty;
|
||||
|
||||
void notify() {
|
||||
if (!_disposed) notifyListeners();
|
||||
}
|
||||
|
||||
void refreshPosition() {
|
||||
// 续看位置来自服务端(每剧一条记录);未登录或没有记录时给 null,UI 显示"从头看"
|
||||
position = history
|
||||
.resumePointFor(drama.id)
|
||||
.then((point) => point?.position)
|
||||
.catchError((Object _) => null);
|
||||
refreshResume();
|
||||
}
|
||||
|
||||
Future<void> refreshResume() async {
|
||||
try {
|
||||
final recent = await history.recent(limit: 1);
|
||||
if (recent.isEmpty || recent.first.finished) {
|
||||
resumeRecord = null;
|
||||
} else {
|
||||
resumeRecord = recent.first;
|
||||
}
|
||||
} catch (_) {
|
||||
resumeRecord = null;
|
||||
}
|
||||
notify();
|
||||
}
|
||||
|
||||
bool get hasSearchQuery => search.text.trim().isNotEmpty;
|
||||
|
||||
/// 输入变化只触发后端查询;本地不再做标题/简介匹配。
|
||||
void onSearchChanged() {
|
||||
final query = search.text.trim();
|
||||
if (query.isEmpty) {
|
||||
@@ -86,47 +46,17 @@ class DiscoverLogic extends ChangeNotifier {
|
||||
_scheduleSearch(query);
|
||||
}
|
||||
|
||||
/// 搜索态展示后端结果,非搜索态展示剧场自身的可见短剧。
|
||||
List<Drama> visibleDramas(List<Drama> fallback) {
|
||||
if (!hasSearchQuery) return _filterByLanguage(fallback);
|
||||
return _filterByLanguage(searchResults);
|
||||
}
|
||||
void openSearch() => searchFocus.requestFocus();
|
||||
|
||||
/// 当前 feed 中出现过的语种,按首次出现顺序去重。
|
||||
List<String> languagesOf(List<Drama> dramas) {
|
||||
final languages = <String>[];
|
||||
for (final drama in dramas) {
|
||||
final language = drama.language;
|
||||
if (language.isNotEmpty && !languages.contains(language)) {
|
||||
languages.add(language);
|
||||
}
|
||||
}
|
||||
return languages;
|
||||
}
|
||||
|
||||
/// 按语种筛选;null 表示全部。
|
||||
void selectLanguage(String? code) {
|
||||
selectedLanguage = code;
|
||||
notify();
|
||||
}
|
||||
|
||||
List<Drama> _filterByLanguage(List<Drama> dramas) {
|
||||
final code = selectedLanguage;
|
||||
if (code == null) return dramas;
|
||||
return dramas.where((drama) => drama.language == code).toList();
|
||||
}
|
||||
|
||||
Future<void> retrySearch() async {
|
||||
void retrySearch() {
|
||||
final query = search.text.trim();
|
||||
if (query.isEmpty) return;
|
||||
await _runSearch(query);
|
||||
if (query.isNotEmpty) unawaited(_runSearch(query));
|
||||
}
|
||||
|
||||
void _resetSearch() {
|
||||
_searchTimer?.cancel();
|
||||
_searchRequest++;
|
||||
searchResults = const [];
|
||||
searchTotal = 0;
|
||||
searchLoading = false;
|
||||
searchError = null;
|
||||
_lastRequestedQuery = '';
|
||||
@@ -162,7 +92,6 @@ class DiscoverLogic extends ChangeNotifier {
|
||||
);
|
||||
if (request != _searchRequest) return;
|
||||
searchResults = result.list;
|
||||
searchTotal = result.total;
|
||||
} on ApiException catch (exception) {
|
||||
_applySearchError(request, exception.message);
|
||||
} catch (_) {
|
||||
@@ -179,47 +108,20 @@ class DiscoverLogic extends ChangeNotifier {
|
||||
if (request != _searchRequest) return;
|
||||
searchError = message;
|
||||
searchResults = const [];
|
||||
searchTotal = 0;
|
||||
}
|
||||
|
||||
Drama? resumeDrama(List<Drama> dramas) {
|
||||
final record = resumeRecord;
|
||||
if (record == null) return null;
|
||||
for (final drama in dramas) {
|
||||
if (drama.id == record.dramaId) return drama;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
bool get showSearch => searchOpen || search.text.isNotEmpty;
|
||||
|
||||
void openSearch() {
|
||||
searchOpen = true;
|
||||
notify();
|
||||
searchFocus.requestFocus();
|
||||
}
|
||||
|
||||
void syncDrama({
|
||||
/// 从播放器发起的搜索请求只在剧场页可见时抢焦点。
|
||||
void syncSearch({
|
||||
required bool active,
|
||||
required bool wasActive,
|
||||
required Drama nextDrama,
|
||||
required bool dramaChanged,
|
||||
required int searchRequest,
|
||||
required int previousSearchRequest,
|
||||
required int request,
|
||||
required int previousRequest,
|
||||
required bool Function() isActive,
|
||||
}) {
|
||||
if (active && (!wasActive || dramaChanged)) {
|
||||
drama = nextDrama;
|
||||
refreshPosition();
|
||||
notify();
|
||||
}
|
||||
if (active && searchRequest != previousSearchRequest) {
|
||||
searchOpen = true;
|
||||
notify();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (isActive()) searchFocus.requestFocus();
|
||||
});
|
||||
}
|
||||
if (!active || request == previousRequest) return;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (isActive()) searchFocus.requestFocus();
|
||||
});
|
||||
WidgetsBinding.instance.scheduleFrame();
|
||||
}
|
||||
|
||||
void act(
|
||||
@@ -235,48 +137,6 @@ class DiscoverLogic extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Drama? dramaFor(String? dramaId, List<Drama> dramas) {
|
||||
for (final item in dramas) {
|
||||
if (item.id == dramaId) return item;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void openCard(
|
||||
BuildContext context,
|
||||
Drama? selected, {
|
||||
required bool embedded,
|
||||
ValueChanged<Drama>? onDramaSelected,
|
||||
ValueChanged<DiscoverPageAction>? onAction,
|
||||
}) {
|
||||
if (selected == null) {
|
||||
unavailable(context);
|
||||
} else if (embedded && onDramaSelected != null) {
|
||||
onDramaSelected(selected);
|
||||
} else {
|
||||
act(
|
||||
context,
|
||||
DiscoverPageAction.detail,
|
||||
embedded: embedded,
|
||||
onAction: onAction,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void unavailable(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(l10n.artworkOnlyNotice)));
|
||||
}
|
||||
|
||||
void catalogInfo(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(l10n.demoCatalogNotice)));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_disposed = true;
|
||||
|
||||
Reference in new issue
Block a user