276 lines
7.0 KiB
Dart
276 lines
7.0 KiB
Dart
// Author: fruit.com.cn
|
|
|
|
import 'package:flutter/material.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.onRefresh,
|
|
required this.onSearch,
|
|
required this.onMore,
|
|
});
|
|
|
|
final AppLocalizations l10n;
|
|
final Future<bool> Function() onRefresh;
|
|
final VoidCallback onSearch;
|
|
final VoidCallback onMore;
|
|
|
|
@override
|
|
State<HomeFeedHeader> createState() => _HomeFeedHeaderState();
|
|
}
|
|
|
|
class _HomeFeedHeaderState extends State<HomeFeedHeader> {
|
|
_FeedTab _tab = _FeedTab.recommend;
|
|
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 < _FeedTab.values.length; index++)
|
|
_tabSlot(_FeedTab.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(_FeedTab tab, int index) {
|
|
final item = _FeedTabItem(
|
|
tab: tab,
|
|
label: tab.label(widget.l10n),
|
|
selected: _tab == tab,
|
|
fontSize: _tabFontSize,
|
|
underlineWidth: _underlineWidth,
|
|
underlineHeight: _underlineHeight,
|
|
onTap: () => _selectTab(tab),
|
|
);
|
|
if (index == 0) return item;
|
|
return Padding(
|
|
padding: const EdgeInsets.only(left: _tabGap),
|
|
child: item,
|
|
);
|
|
}
|
|
|
|
void _selectTab(_FeedTab tab) {
|
|
if (tab.comingSoon) {
|
|
_showComingSoon();
|
|
return;
|
|
}
|
|
setState(() => _tab = tab);
|
|
}
|
|
|
|
Future<void> _refresh() async {
|
|
if (_refreshing) return;
|
|
setState(() => _refreshing = true);
|
|
await widget.onRefresh();
|
|
if (mounted) setState(() => _refreshing = false);
|
|
}
|
|
|
|
void _showComingSoon() {
|
|
ScaffoldMessenger.of(
|
|
context,
|
|
).showSnackBar(SnackBar(content: Text(widget.l10n.comingSoon)));
|
|
}
|
|
}
|
|
|
|
/// 顶部频道 tab:漫剧点击提示即将上线。
|
|
enum _FeedTab {
|
|
anime,
|
|
real,
|
|
recommend;
|
|
|
|
bool get comingSoon => this == _FeedTab.anime;
|
|
|
|
String get keyName => switch (this) {
|
|
_FeedTab.anime => 'anime',
|
|
_FeedTab.real => 'real',
|
|
_FeedTab.recommend => 'recommend',
|
|
};
|
|
|
|
String label(AppLocalizations l10n) => switch (this) {
|
|
_FeedTab.anime => l10n.feedTabAnime,
|
|
_FeedTab.real => l10n.feedTabReal,
|
|
_FeedTab.recommend => 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: Colors.white,
|
|
shadows: const [Shadow(blurRadius: 6, color: Colors.black)],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 刷新入口:点击重新加载 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: Colors.white,
|
|
),
|
|
),
|
|
)
|
|
: Icon(
|
|
Icons.refresh_rounded,
|
|
size: glyphSize,
|
|
color: Colors.white,
|
|
shadows: const [Shadow(blurRadius: 6, color: Colors.black)],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _FeedTabItem extends StatelessWidget {
|
|
const _FeedTabItem({
|
|
required this.tab,
|
|
required this.label,
|
|
required this.selected,
|
|
required this.fontSize,
|
|
required this.underlineWidth,
|
|
required this.underlineHeight,
|
|
required this.onTap,
|
|
});
|
|
|
|
final _FeedTab tab;
|
|
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-${tab.keyName}'),
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: onTap,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
label,
|
|
maxLines: 1,
|
|
style: TextStyle(
|
|
color: selected ? Colors.white : Colors.white70,
|
|
fontSize: fontSize,
|
|
fontWeight: selected ? FontWeight.w800 : FontWeight.w600,
|
|
shadows: const [Shadow(blurRadius: 6, color: Colors.black)],
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Container(
|
|
width: underlineWidth,
|
|
height: underlineHeight,
|
|
decoration: BoxDecoration(
|
|
color: selected ? Colors.white : Colors.transparent,
|
|
borderRadius: BorderRadius.circular(1),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|