feat: 统一首页与推荐搜索入口
This commit is contained in:
1 parent
933333b0fe
commit
9aa797504b
18 files changed
+325
-107
No files matched your search
@@ -118,20 +118,6 @@ class DiscoverLogic extends ChangeNotifier {
|
||||
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, {
|
||||
|
||||
@@ -56,14 +56,10 @@ class DiscoverPage extends StatefulWidget {
|
||||
const DiscoverPage({
|
||||
super.key,
|
||||
this.active = true,
|
||||
this.searchRequest = 0,
|
||||
this.focusSearch = false,
|
||||
this.callbacks = const DiscoverPageCallbacks(),
|
||||
});
|
||||
|
||||
final bool active;
|
||||
final int searchRequest;
|
||||
final bool focusSearch;
|
||||
final DiscoverPageCallbacks callbacks;
|
||||
|
||||
@override
|
||||
@@ -89,7 +85,6 @@ class _DiscoverPageState extends State<DiscoverPage> {
|
||||
logic.scroll.addListener(_onScroll);
|
||||
theater.addListener(_onTheaterChanged);
|
||||
unawaited(theater.select(null));
|
||||
if (widget.focusSearch) _focusOnFirstFrame();
|
||||
}
|
||||
|
||||
void _onScroll() {
|
||||
@@ -106,23 +101,6 @@ class _DiscoverPageState extends State<DiscoverPage> {
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
void _focusOnFirstFrame() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted && widget.active) logic.openSearch();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant DiscoverPage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
logic.syncSearch(
|
||||
active: widget.active,
|
||||
request: widget.searchRequest,
|
||||
previousRequest: oldWidget.searchRequest,
|
||||
isActive: () => mounted && widget.active,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
logic.scroll.removeListener(_onScroll);
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../core/data/repositories/search_repository.dart';
|
||||
import '../../core/formatting/error_text.dart';
|
||||
import '../../core/theme/app_colors.dart';
|
||||
import '../../core/theme/app_dimens.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../core/widgets/empty_state.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import '../home/widgets/home_feed_card.dart';
|
||||
import '../player/open_player.dart';
|
||||
import 'discover_binding.dart';
|
||||
import 'discover_logic.dart';
|
||||
|
||||
class SearchPage extends StatefulWidget {
|
||||
const SearchPage({super.key});
|
||||
|
||||
@override
|
||||
State<SearchPage> createState() => _SearchPageState();
|
||||
}
|
||||
|
||||
class _SearchPageState extends State<SearchPage> {
|
||||
late final DiscoverLogic _logic;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_logic = DiscoverBinding().create(search: context.read<SearchRepository>());
|
||||
_logic.addListener(_rebuild);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) _logic.searchFocus.requestFocus();
|
||||
});
|
||||
}
|
||||
|
||||
void _rebuild() {
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_logic
|
||||
..removeListener(_rebuild)
|
||||
..dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: SystemUiOverlayStyle.light,
|
||||
child: Theme(
|
||||
data: AppTheme.dark(),
|
||||
child: Scaffold(
|
||||
key: const Key('search-page'),
|
||||
backgroundColor: AppColors.appBackground,
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
_header(l10n),
|
||||
Expanded(child: _body(l10n)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _header(AppLocalizations l10n) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 6, 14, 8),
|
||||
child: SizedBox(
|
||||
height: AppDimens.searchHeight,
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox.square(
|
||||
dimension: AppDimens.buttonMedium,
|
||||
child: IconButton(
|
||||
key: const Key('search-back'),
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () => Navigator.of(context).maybePop(),
|
||||
icon: const Icon(
|
||||
Icons.arrow_back_ios_new_rounded,
|
||||
color: AppColors.textPrimary,
|
||||
size: 22,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppDimens.spaceSm),
|
||||
Expanded(child: _field(l10n)),
|
||||
const SizedBox(width: AppDimens.spaceSm),
|
||||
TextButton(
|
||||
key: const Key('search-cancel'),
|
||||
onPressed: () => Navigator.of(context).maybePop(),
|
||||
child: Text(l10n.searchCancel),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _field(AppLocalizations l10n) {
|
||||
final hasText = _logic.search.text.isNotEmpty;
|
||||
return Container(
|
||||
key: const Key('search-field'),
|
||||
height: AppDimens.searchHeight,
|
||||
padding: const EdgeInsets.symmetric(horizontal: AppDimens.spaceMd),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.contentSurface,
|
||||
borderRadius: BorderRadius.circular(AppDimens.searchRadius),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.search_rounded,
|
||||
color: AppColors.textSecondary,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: AppDimens.spaceSm),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
key: const Key('discover-search'),
|
||||
controller: _logic.search,
|
||||
focusNode: _logic.searchFocus,
|
||||
autofocus: true,
|
||||
onChanged: (_) => _logic.onSearchChanged(),
|
||||
textInputAction: TextInputAction.search,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: 14,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
hintText: l10n.searchDiscover,
|
||||
hintStyle: const TextStyle(
|
||||
color: AppColors.textSecondary,
|
||||
fontSize: 13,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_logic.searchLoading)
|
||||
const SizedBox.square(
|
||||
dimension: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
)
|
||||
else if (hasText)
|
||||
IconButton(
|
||||
key: const Key('search-clear'),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints.tightFor(width: 24, height: 24),
|
||||
onPressed: () {
|
||||
_logic.search.clear();
|
||||
_logic.onSearchChanged();
|
||||
_logic.searchFocus.requestFocus();
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.close_rounded,
|
||||
color: AppColors.textSecondary,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _body(AppLocalizations l10n) {
|
||||
if (!_logic.hasSearchQuery) {
|
||||
return EmptyState(
|
||||
key: const Key('search-idle'),
|
||||
icon: Icons.search_rounded,
|
||||
title: l10n.searchDiscover,
|
||||
);
|
||||
}
|
||||
if (_logic.searchLoading) {
|
||||
return const Center(
|
||||
child: SizedBox.square(
|
||||
key: Key('search-loading'),
|
||||
dimension: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (_logic.searchError != null) {
|
||||
return EmptyState(
|
||||
key: const Key('search-error'),
|
||||
icon: Icons.cloud_off_rounded,
|
||||
title: errorText(l10n, _logic.searchError, l10n.searchFailed),
|
||||
actionLabel: l10n.retry,
|
||||
onAction: _logic.retrySearch,
|
||||
);
|
||||
}
|
||||
if (_logic.searchResults.isEmpty) {
|
||||
return EmptyState(
|
||||
key: const Key('search-empty'),
|
||||
icon: Icons.search_off_rounded,
|
||||
title: l10n.noSearchResults,
|
||||
subtitle: l10n.emptyHint,
|
||||
);
|
||||
}
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final width =
|
||||
(constraints.maxWidth -
|
||||
AppDimens.feedPageHorizontal * 2 -
|
||||
AppDimens.homeGridGap * 2) /
|
||||
3;
|
||||
final itemExtent =
|
||||
width / AppDimens.cardCoverAspectRatio +
|
||||
AppDimens.homeCardBodyExtent;
|
||||
return GridView.builder(
|
||||
key: const Key('search-results-grid'),
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
AppDimens.feedPageHorizontal,
|
||||
AppDimens.spaceMd,
|
||||
AppDimens.feedPageHorizontal,
|
||||
AppDimens.spaceXl,
|
||||
),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
crossAxisSpacing: AppDimens.homeGridGap,
|
||||
mainAxisSpacing: AppDimens.homeGridRowGap,
|
||||
mainAxisExtent: itemExtent,
|
||||
),
|
||||
itemCount: _logic.searchResults.length,
|
||||
itemBuilder: (context, index) {
|
||||
final drama = _logic.searchResults[index];
|
||||
return HomeFeedCard(
|
||||
drama: drama,
|
||||
onTap: () => unawaited(playDramaFromCatalog(context, drama)),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../core/data/models/drama.dart';
|
||||
import '../../../core/formatting/error_text.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../../core/widgets/empty_state.dart';
|
||||
import '../../../l10n/app_localizations.dart';
|
||||
import 'drama_grid.dart';
|
||||
@@ -38,7 +39,15 @@ class DiscoverSearchResults extends StatelessWidget {
|
||||
if (loading) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 80),
|
||||
child: Center(child: CircularProgressIndicator(strokeWidth: 2)),
|
||||
child: Center(
|
||||
child: SizedBox.square(
|
||||
dimension: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
final message = error;
|
||||
|
||||
@@ -17,7 +17,6 @@ class HomeLogic extends ChangeNotifier {
|
||||
final scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
Drama currentDrama;
|
||||
int tab = 0;
|
||||
int searchRequest = 0;
|
||||
int resumeRequest = 0;
|
||||
int backRequest = 0;
|
||||
int followingHistoryRequest = 0;
|
||||
@@ -31,10 +30,9 @@ class HomeLogic extends ChangeNotifier {
|
||||
if (!_disposed) notifyListeners();
|
||||
}
|
||||
|
||||
void select(int nextTab, {bool search = false, bool resume = false}) {
|
||||
void select(int nextTab, {bool resume = false}) {
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
tab = nextTab;
|
||||
if (search) searchRequest++;
|
||||
if (resume) resumeRequest++;
|
||||
notify();
|
||||
}
|
||||
@@ -81,7 +79,7 @@ class HomeLogic extends ChangeNotifier {
|
||||
case DiscoverPageAction.home:
|
||||
select(0);
|
||||
case DiscoverPageAction.recommend:
|
||||
select(1, search: true);
|
||||
select(1);
|
||||
case DiscoverPageAction.following:
|
||||
select(2);
|
||||
case DiscoverPageAction.mine:
|
||||
@@ -112,7 +110,7 @@ class HomeLogic extends ChangeNotifier {
|
||||
case MyPageAction.home:
|
||||
select(0);
|
||||
case MyPageAction.recommend:
|
||||
select(1, search: true);
|
||||
select(1);
|
||||
case MyPageAction.following:
|
||||
select(2);
|
||||
case MyPageAction.mine:
|
||||
|
||||
@@ -22,7 +22,7 @@ import '../../core/widgets/app_root_tab.dart';
|
||||
import '../../core/widgets/catalog_endpoint_editor.dart';
|
||||
import '../../core/widgets/empty_state.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import '../discover/discover_view.dart';
|
||||
import '../discover/search_page.dart';
|
||||
import '../following/following_page.dart';
|
||||
import '../filter/filter_page.dart';
|
||||
import '../mine/mine_types.dart';
|
||||
@@ -292,9 +292,9 @@ class _HomeTabsState extends State<_HomeTabs> {
|
||||
}
|
||||
|
||||
void _openSearch(BuildContext context) {
|
||||
Navigator.of(context).push<void>(
|
||||
MaterialPageRoute(builder: (_) => const DiscoverPage(focusSearch: true)),
|
||||
);
|
||||
Navigator.of(
|
||||
context,
|
||||
).push<void>(MaterialPageRoute(builder: (_) => const SearchPage()));
|
||||
}
|
||||
|
||||
void _pushPage(BuildContext context, Widget page) {
|
||||
|
||||
@@ -26,8 +26,7 @@ import '../../core/formatting/localized_content.dart';
|
||||
import '../../core/theme/app_colors.dart';
|
||||
import '../../core/navigation/fade_page_route.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import '../discover/discover_types.dart';
|
||||
import '../discover/discover_view.dart';
|
||||
import '../discover/search_page.dart';
|
||||
import '../drama_detail/drama_detail_view.dart';
|
||||
import '../mine/mine_types.dart';
|
||||
import '../mine/mine_view.dart';
|
||||
@@ -1127,13 +1126,10 @@ class PlayerLogic extends ChangeNotifier {
|
||||
onSearch?.call();
|
||||
return;
|
||||
}
|
||||
await showDiscover(context, focusSearch: true);
|
||||
await showDiscover(context);
|
||||
}
|
||||
|
||||
Future<void> showDiscover(
|
||||
BuildContext context, {
|
||||
bool focusSearch = false,
|
||||
}) async {
|
||||
Future<void> showDiscover(BuildContext context) async {
|
||||
if (embedded) {
|
||||
onDiscover?.call();
|
||||
return;
|
||||
@@ -1142,13 +1138,10 @@ class PlayerLogic extends ChangeNotifier {
|
||||
await session.playbackSettled;
|
||||
await saveCurrentProgress();
|
||||
if (!context.mounted) return;
|
||||
final action = await Navigator.of(context).push<DiscoverPageAction>(
|
||||
MaterialPageRoute(builder: (_) => DiscoverPage(focusSearch: focusSearch)),
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
if (action == DiscoverPageAction.mine) {
|
||||
await showMy(context);
|
||||
} else {
|
||||
await Navigator.of(
|
||||
context,
|
||||
).push<void>(MaterialPageRoute(builder: (_) => const SearchPage()));
|
||||
if (context.mounted) {
|
||||
session.setRouteVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user