267 lines
7.2 KiB
Dart
267 lines
7.2 KiB
Dart
// Author: fruit.com.cn
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import '../../../core/theme/app_colors.dart';
|
|
|
|
import '../../home/home_feed_channel.dart';
|
|
import '../../../l10n/app_localizations.dart';
|
|
|
|
const double _topIconSize = 36;
|
|
const double _topIconGlyph = 22;
|
|
const double _tabGap = 22;
|
|
const double _tabFontSize = 15;
|
|
const double _underlineWidth = 18;
|
|
const double _underlineHeight = 2;
|
|
|
|
/// 首页 feed 固定顶栏:刷新 / 漫剧·真人剧·推荐 / 搜索。
|
|
class HomeFeedHeader extends StatefulWidget {
|
|
const HomeFeedHeader({
|
|
super.key,
|
|
required this.l10n,
|
|
required this.channel,
|
|
required this.onChannelSelected,
|
|
required this.onRefresh,
|
|
required this.onSearch,
|
|
required this.onMore,
|
|
});
|
|
|
|
final AppLocalizations l10n;
|
|
final HomeFeedChannel channel;
|
|
final ValueChanged<HomeFeedChannel> onChannelSelected;
|
|
final Future<bool> Function() onRefresh;
|
|
final VoidCallback onSearch;
|
|
final VoidCallback onMore;
|
|
|
|
@override
|
|
State<HomeFeedHeader> createState() => _HomeFeedHeaderState();
|
|
}
|
|
|
|
class _HomeFeedHeaderState extends State<HomeFeedHeader> {
|
|
bool _refreshing = false;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Padding(
|
|
padding: const EdgeInsets.fromLTRB(8, 6, 8, 4),
|
|
child: Row(
|
|
children: [
|
|
_RefreshButton(
|
|
tooltip: widget.l10n.retry,
|
|
dimension: _topIconSize,
|
|
glyphSize: _topIconGlyph,
|
|
refreshing: _refreshing,
|
|
onTap: _refresh,
|
|
onLongPress: widget.onMore,
|
|
),
|
|
Expanded(
|
|
child: Center(
|
|
child: SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
child: Row(
|
|
children: [
|
|
for (
|
|
var index = 0;
|
|
index < HomeFeedChannel.values.length;
|
|
index++
|
|
)
|
|
_tabSlot(HomeFeedChannel.values[index], index),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
_TopIconButton(
|
|
key: const Key('home-feed-search'),
|
|
tooltip: widget.l10n.search,
|
|
icon: Icons.search_rounded,
|
|
dimension: _topIconSize,
|
|
glyphSize: _topIconGlyph,
|
|
onPressed: widget.onSearch,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _tabSlot(HomeFeedChannel channel, int index) {
|
|
final item = _FeedTabItem(
|
|
channel: channel,
|
|
label: _label(channel),
|
|
selected: widget.channel == channel,
|
|
fontSize: _tabFontSize,
|
|
underlineWidth: _underlineWidth,
|
|
underlineHeight: _underlineHeight,
|
|
onTap: () => widget.onChannelSelected(channel),
|
|
);
|
|
if (index == 0) return item;
|
|
return Padding(
|
|
padding: const EdgeInsets.only(left: _tabGap),
|
|
child: item,
|
|
);
|
|
}
|
|
|
|
Future<void> _refresh() async {
|
|
if (_refreshing) return;
|
|
setState(() => _refreshing = true);
|
|
await widget.onRefresh();
|
|
if (mounted) setState(() => _refreshing = false);
|
|
}
|
|
|
|
String _label(HomeFeedChannel channel) => switch (channel) {
|
|
HomeFeedChannel.anime => widget.l10n.feedTabAnime,
|
|
HomeFeedChannel.live => widget.l10n.feedTabReal,
|
|
HomeFeedChannel.recommend => widget.l10n.feedTabForYou,
|
|
};
|
|
}
|
|
|
|
class _TopIconButton extends StatelessWidget {
|
|
const _TopIconButton({
|
|
super.key,
|
|
required this.tooltip,
|
|
required this.icon,
|
|
required this.dimension,
|
|
required this.glyphSize,
|
|
required this.onPressed,
|
|
});
|
|
|
|
final String tooltip;
|
|
final IconData icon;
|
|
final double dimension;
|
|
final double glyphSize;
|
|
final VoidCallback onPressed;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SizedBox.square(
|
|
dimension: dimension,
|
|
child: IconButton(
|
|
tooltip: tooltip,
|
|
onPressed: onPressed,
|
|
padding: EdgeInsets.zero,
|
|
icon: Icon(
|
|
icon,
|
|
size: glyphSize,
|
|
color: AppColors.onPrimary,
|
|
shadows: const [Shadow(blurRadius: 6, color: AppColors.mediaBlack)],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 刷新入口:点击重新加载 feed,长按打开更多(清屏等)。
|
|
class _RefreshButton extends StatelessWidget {
|
|
const _RefreshButton({
|
|
required this.tooltip,
|
|
required this.dimension,
|
|
required this.glyphSize,
|
|
required this.refreshing,
|
|
required this.onTap,
|
|
required this.onLongPress,
|
|
});
|
|
|
|
final String tooltip;
|
|
final double dimension;
|
|
final double glyphSize;
|
|
final bool refreshing;
|
|
final VoidCallback onTap;
|
|
final VoidCallback onLongPress;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Tooltip(
|
|
message: tooltip,
|
|
child: GestureDetector(
|
|
key: const Key('home-feed-refresh'),
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: onTap,
|
|
onLongPress: onLongPress,
|
|
child: SizedBox.square(
|
|
dimension: dimension,
|
|
child: refreshing
|
|
? const Center(
|
|
child: SizedBox.square(
|
|
dimension: 18,
|
|
child: CircularProgressIndicator(
|
|
key: Key('home-feed-refreshing'),
|
|
strokeWidth: 2,
|
|
color: AppColors.onPrimary,
|
|
),
|
|
),
|
|
)
|
|
: Icon(
|
|
Icons.refresh_rounded,
|
|
size: glyphSize,
|
|
color: AppColors.onPrimary,
|
|
shadows: const [
|
|
Shadow(blurRadius: 6, color: AppColors.mediaBlack),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _FeedTabItem extends StatelessWidget {
|
|
const _FeedTabItem({
|
|
required this.channel,
|
|
required this.label,
|
|
required this.selected,
|
|
required this.fontSize,
|
|
required this.underlineWidth,
|
|
required this.underlineHeight,
|
|
required this.onTap,
|
|
});
|
|
|
|
final HomeFeedChannel channel;
|
|
final String label;
|
|
final bool selected;
|
|
final double fontSize;
|
|
final double underlineWidth;
|
|
final double underlineHeight;
|
|
final VoidCallback onTap;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return GestureDetector(
|
|
key: Key('home-feed-tab-${_keyName(channel)}'),
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: onTap,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
label,
|
|
maxLines: 1,
|
|
style: TextStyle(
|
|
color: selected ? AppColors.onPrimary : AppColors.muted,
|
|
fontSize: fontSize,
|
|
fontWeight: selected ? FontWeight.w800 : FontWeight.w600,
|
|
shadows: const [
|
|
Shadow(blurRadius: 6, color: AppColors.mediaBlack),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Container(
|
|
width: underlineWidth,
|
|
height: underlineHeight,
|
|
decoration: BoxDecoration(
|
|
color: selected ? AppColors.onPrimary : AppColors.transparent,
|
|
borderRadius: BorderRadius.circular(1),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
String _keyName(HomeFeedChannel channel) => switch (channel) {
|
|
HomeFeedChannel.anime => 'anime',
|
|
HomeFeedChannel.live => 'real',
|
|
HomeFeedChannel.recommend => 'recommend',
|
|
};
|
|
}
|