feat(flutter): 剧场页按参考图重做
This commit is contained in:
1 parent
2abb8c7032
commit
5a51af4264
13 files changed
+499
-258
No files matched your search
@@ -22,6 +22,16 @@ import 'widgets/discover_header.dart';
|
||||
import 'widgets/discover_search_results.dart';
|
||||
import 'widgets/drama_grid.dart';
|
||||
|
||||
const Color _filterIconColor = Color(0xFF7B61FF);
|
||||
const Color _newIconColor = Color(0xFF14B8A6);
|
||||
const double _toolbarHeight = 36;
|
||||
const double _toolbarGap = 10;
|
||||
const double _toolbarRadius = 10;
|
||||
const double _categoryTabHeight = 44;
|
||||
const double _categoryTabGap = 20;
|
||||
const double _categorySelectedFontSize = 16;
|
||||
const double _categoryFontSize = 15;
|
||||
|
||||
/// K28-A2 剧场页:搜索、分类/语言 chips 与双列剧库。
|
||||
class DiscoverPage extends StatefulWidget {
|
||||
const DiscoverPage({
|
||||
@@ -96,7 +106,7 @@ class _DiscoverPageState extends State<DiscoverPage> {
|
||||
focusNode: logic.searchFocus,
|
||||
onChanged: (_) => logic.onSearchChanged(),
|
||||
loading: logic.searchLoading,
|
||||
onProfile: _openProfile,
|
||||
onIdentify: () => _showComingSoon(l10n),
|
||||
),
|
||||
Expanded(child: _feed(l10n, categories)),
|
||||
if (!embedded) _bottomNav(l10n),
|
||||
@@ -174,7 +184,8 @@ class _DiscoverPageState extends State<DiscoverPage> {
|
||||
? categories.feed.dramas
|
||||
: categories.dramasOf(logic.selectedCategoryId!);
|
||||
return [
|
||||
_categoryChips(l10n, categories),
|
||||
_categoryTabs(l10n, categories),
|
||||
_toolbar(l10n, categories),
|
||||
if (dramas.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 40),
|
||||
@@ -190,73 +201,199 @@ class _DiscoverPageState extends State<DiscoverPage> {
|
||||
];
|
||||
}
|
||||
|
||||
Widget _categoryChips(AppLocalizations l10n, CategoryController categories) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
AppDimens.pageHorizontal,
|
||||
AppDimens.spaceMd,
|
||||
AppDimens.pageHorizontal,
|
||||
0,
|
||||
Widget _categoryTabs(AppLocalizations l10n, CategoryController categories) {
|
||||
return SizedBox(
|
||||
key: const Key('theater-category-chips'),
|
||||
height: _categoryTabHeight,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||||
itemCount: categories.categories.length + 1,
|
||||
separatorBuilder: (_, _) => const SizedBox(width: _categoryTabGap),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0) {
|
||||
return _categoryTab(
|
||||
key: const Key('theater-category-all'),
|
||||
label: l10n.all,
|
||||
selected: logic.selectedCategoryId == null,
|
||||
onTap: () => logic.selectCategory(null),
|
||||
);
|
||||
}
|
||||
final category = categories.categories[index - 1];
|
||||
return _categoryTab(
|
||||
key: Key('theater-category-${category.id}'),
|
||||
label: category.name,
|
||||
selected: logic.selectedCategoryId == category.id,
|
||||
onTap: () => logic.selectCategory(category.id),
|
||||
);
|
||||
},
|
||||
),
|
||||
child: Wrap(
|
||||
key: const Key('theater-category-chips'),
|
||||
spacing: 10,
|
||||
runSpacing: 10,
|
||||
children: [
|
||||
_filterChip(
|
||||
key: const Key('theater-category-all'),
|
||||
label: l10n.all,
|
||||
selected: logic.selectedCategoryId == null,
|
||||
onSelected: () => logic.selectCategory(null),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _categoryTab({
|
||||
required Key key,
|
||||
required String label,
|
||||
required bool selected,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
key: key,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onTap,
|
||||
child: Center(
|
||||
child: Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: selected ? AppColors.onSurface : AppColors.muted,
|
||||
fontSize: selected ? _categorySelectedFontSize : _categoryFontSize,
|
||||
fontWeight: selected ? FontWeight.w800 : FontWeight.w500,
|
||||
),
|
||||
for (final category in categories.categories)
|
||||
_filterChip(
|
||||
key: Key('theater-category-${category.id}'),
|
||||
label: category.name,
|
||||
selected: logic.selectedCategoryId == category.id,
|
||||
onSelected: () => logic.selectCategory(category.id),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _toolbar(AppLocalizations l10n, CategoryController categories) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(14, 10, 14, 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _toolButton(
|
||||
key: const Key('theater-toolbar-filter'),
|
||||
icon: Icons.tune_rounded,
|
||||
color: _filterIconColor,
|
||||
label: l10n.filter,
|
||||
onTap: () => _openFilterSheet(l10n, categories),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: _toolbarGap),
|
||||
Expanded(
|
||||
child: _toolButton(
|
||||
key: const Key('theater-toolbar-ranking'),
|
||||
icon: Icons.local_fire_department_rounded,
|
||||
color: AppColors.blue,
|
||||
label: l10n.rankingTab,
|
||||
onTap: () => _showComingSoon(l10n),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: _toolbarGap),
|
||||
Expanded(
|
||||
child: _toolButton(
|
||||
key: const Key('theater-toolbar-new'),
|
||||
icon: Icons.play_circle_fill_rounded,
|
||||
color: _newIconColor,
|
||||
label: l10n.newDramas,
|
||||
onTap: () => _showComingSoon(l10n),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: _toolbarGap),
|
||||
Expanded(
|
||||
child: _toolButton(
|
||||
key: const Key('theater-toolbar-reserve'),
|
||||
icon: Icons.calendar_month_rounded,
|
||||
color: AppColors.blue,
|
||||
label: l10n.reserve,
|
||||
onTap: () => _showComingSoon(l10n),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _filterChip({
|
||||
Widget _toolButton({
|
||||
required Key key,
|
||||
required IconData icon,
|
||||
required Color color,
|
||||
required String label,
|
||||
required bool selected,
|
||||
required VoidCallback onSelected,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return ChoiceChip(
|
||||
key: key,
|
||||
label: Text(label),
|
||||
selected: selected,
|
||||
showCheckmark: false,
|
||||
onSelected: (_) => onSelected(),
|
||||
backgroundColor: AppColors.card,
|
||||
selectedColor: AppColors.blue,
|
||||
side: BorderSide(color: selected ? AppColors.blue : AppColors.border),
|
||||
labelStyle: TextStyle(
|
||||
color: selected ? AppColors.onPrimary : AppColors.onSurface,
|
||||
fontSize: AppDimens.caption,
|
||||
fontWeight: FontWeight.w700,
|
||||
return Material(
|
||||
color: AppColors.card,
|
||||
borderRadius: BorderRadius.circular(_toolbarRadius),
|
||||
child: InkWell(
|
||||
key: key,
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(_toolbarRadius),
|
||||
child: Container(
|
||||
height: _toolbarHeight,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(_toolbarRadius),
|
||||
border: Border.all(color: AppColors.border),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, size: 16, color: color),
|
||||
const SizedBox(width: 4),
|
||||
Flexible(
|
||||
child: Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: AppColors.onSurface,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
shape: const StadiumBorder(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _openFilterSheet(
|
||||
AppLocalizations l10n,
|
||||
CategoryController categories,
|
||||
) async {
|
||||
await showModalBottomSheet<void>(
|
||||
context: context,
|
||||
backgroundColor: AppColors.card,
|
||||
showDragHandle: true,
|
||||
builder: (sheetContext) => SafeArea(
|
||||
child: ListView(
|
||||
key: const Key('theater-filter-sheet'),
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
children: [
|
||||
ListTile(
|
||||
key: const Key('theater-filter-option-all'),
|
||||
title: Text(l10n.all),
|
||||
onTap: () {
|
||||
Navigator.pop(sheetContext);
|
||||
logic.selectCategory(null);
|
||||
},
|
||||
),
|
||||
for (final category in categories.categories)
|
||||
ListTile(
|
||||
key: Key('theater-filter-option-${category.id}'),
|
||||
title: Text(category.name),
|
||||
onTap: () {
|
||||
Navigator.pop(sheetContext);
|
||||
logic.selectCategory(category.id);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showComingSoon(AppLocalizations l10n) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(l10n.comingSoon)));
|
||||
}
|
||||
|
||||
Widget _block(Widget child) =>
|
||||
SizedBox(height: 280, child: Center(child: child));
|
||||
|
||||
void _openProfile() {
|
||||
final callback = widget.callbacks.onProfile;
|
||||
if (callback != null) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
_act(DiscoverPageAction.my);
|
||||
}
|
||||
|
||||
void _act(DiscoverPageAction action) {
|
||||
logic.act(
|
||||
context,
|
||||
|
||||
Reference in new issue
Block a user