Files
ReelShort/lib/pages/player/widgets/home_feed_overlay.dart
T

424 lines
12 KiB
Dart

// Author: fruit.com.cn
import 'dart:async';
import 'package:flutter/material.dart';
import '../../../core/data/models/drama.dart';
import '../../../core/formatting/localized_content.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_dimens.dart';
import '../../../core/widgets/glass_panel.dart';
import '../../../l10n/app_localizations.dart';
/// Home feed layer from `docs/移动端设计预览.html` screen 01.
class HomeFeedOverlay extends StatelessWidget {
const HomeFeedOverlay({
super.key,
required this.l10n,
required this.drama,
required this.episode,
required this.liked,
this.likeCount,
required this.saved,
required this.onLike,
required this.onSave,
required this.onComments,
required this.onShare,
required this.onEpisodes,
required this.onDownload,
required this.onClear,
required this.onSearch,
this.onPip,
});
final AppLocalizations l10n;
final Drama drama;
final Episode episode;
final bool liked;
final int? likeCount;
final bool saved;
final VoidCallback onLike;
final VoidCallback onSave;
final VoidCallback onComments;
final VoidCallback onShare;
final VoidCallback onEpisodes;
final VoidCallback onDownload;
final VoidCallback onClear;
final VoidCallback onSearch;
final VoidCallback? onPip;
@override
Widget build(BuildContext context) {
return SafeArea(
bottom: false,
child: Column(
children: [
_header(context),
Expanded(
child: Align(
alignment: const Alignment(1.0, 0.45),
child: Padding(
padding: const EdgeInsets.fromLTRB(0, 0, 12, 16),
child: _rail(),
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(14, 0, 14, 14),
child: _infoCard(),
),
],
),
);
}
Widget _header(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(14, 6, 10, 4),
child: Row(
children: [
Expanded(
child: Text(
localizedDramaTitle(l10n, drama),
key: const Key('home-feed-title'),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontFamily: 'Outfit',
color: AppColors.onSurface,
fontSize: 13,
fontWeight: FontWeight.w800,
shadows: [Shadow(blurRadius: 8, color: Colors.black)],
),
),
),
_RoundIcon(
key: const Key('home-feed-search'),
tooltip: l10n.search,
icon: Icons.search_rounded,
onPressed: onSearch,
),
const SizedBox(width: 8),
_RoundIcon(
key: const Key('home-feed-clear-screen'),
tooltip: l10n.clearScreen,
icon: Icons.fullscreen_rounded,
onPressed: onClear,
),
const SizedBox(width: 8),
_RoundIcon(
key: const Key('home-feed-more'),
tooltip: l10n.more,
icon: Icons.menu_rounded,
onPressed: () => _openMore(context),
),
],
),
);
}
Widget _rail() {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
_RailItem(
key: const Key('rail-like'),
icon: liked ? Icons.favorite_rounded : Icons.favorite_border_rounded,
label: likeCount?.toString() ?? l10n.like,
countKey: likeCount == null ? null : const Key('rail-like-count'),
selected: liked,
onTap: onLike,
),
_RailItem(
key: const Key('rail-comments'),
icon: Icons.chat_bubble_outline_rounded,
label: l10n.comments,
onTap: onComments,
),
_RailItem(
key: const Key('rail-share'),
icon: Icons.ios_share_rounded,
label: l10n.share,
onTap: onShare,
),
_RailItem(
key: const Key('rail-save'),
icon: saved ? Icons.star_rounded : Icons.star_border_rounded,
label: l10n.favorite,
selected: saved,
onTap: onSave,
),
],
);
}
Widget _infoCard() {
final genre = drama.genre.isEmpty ? drama.category : drama.genre;
return GlassPanel(
key: const Key('home-feed-info-card'),
radius: 14,
blur: 6,
padding: const EdgeInsets.all(12),
borderColor: AppColors.border,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
spacing: 6,
runSpacing: 4,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
if (genre.isNotEmpty)
_tag(
genre,
background: AppColors.blue,
foreground: AppColors.onPrimary,
),
if (drama.category.isNotEmpty && drama.category != genre)
_tag(
drama.category,
background: AppColors.raised,
foreground: AppColors.onSurface,
),
Text(
l10n.episodeOfTotal(episode.number, drama.episodeCount),
key: const Key('home-feed-episode'),
style: const TextStyle(color: AppColors.muted, fontSize: 9),
),
],
),
const SizedBox(height: 8),
Text(
episode.title.trim().isEmpty
? localizedDramaTitle(l10n, drama)
: episode.title,
key: const Key('home-feed-episode-title'),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.onSurface,
fontSize: 13,
fontWeight: FontWeight.w800,
),
),
const SizedBox(height: 10),
Row(
children: [
Expanded(
child: SizedBox(
height: 34,
child: FilledButton.icon(
key: const Key('home-feed-follow-series'),
onPressed: onSave,
style: FilledButton.styleFrom(
backgroundColor: AppColors.blue,
foregroundColor: AppColors.onPrimary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
AppDimens.buttonRadius,
),
),
padding: EdgeInsets.zero,
textStyle: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
),
),
icon: const Icon(Icons.play_arrow_rounded, size: 16),
label: Text(
saved ? l10n.followingTab : l10n.followSeries,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
),
const SizedBox(width: 8),
Expanded(
child: SizedBox(
height: 34,
child: FilledButton(
key: const Key('home-feed-episodes'),
onPressed: onEpisodes,
style: FilledButton.styleFrom(
backgroundColor: AppColors.raised,
foregroundColor: AppColors.onSurface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
AppDimens.buttonRadius,
),
),
padding: EdgeInsets.zero,
textStyle: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
child: Text(
l10n.episodes,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
),
],
),
],
),
);
}
Widget _tag(
String label, {
required Color background,
required Color foreground,
}) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: background,
borderRadius: BorderRadius.circular(6),
),
child: Text(
label,
maxLines: 1,
style: TextStyle(
color: foreground,
fontSize: 8,
fontWeight: FontWeight.w800,
),
),
);
}
Future<void> _openMore(BuildContext context) async {
await showModalBottomSheet<void>(
context: context,
backgroundColor: AppColors.card,
showDragHandle: true,
builder: (sheetContext) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
key: const Key('home-feed-download'),
leading: const Icon(
Icons.download_outlined,
color: AppColors.secondary,
),
title: Text(l10n.downloads),
onTap: () {
Navigator.pop(sheetContext);
onDownload();
},
),
if (onPip != null)
ListTile(
key: const Key('home-feed-pip'),
leading: const Icon(
Icons.picture_in_picture_alt_outlined,
color: AppColors.secondary,
),
title: Text(l10n.pictureInPicture),
onTap: () {
Navigator.pop(sheetContext);
onPip!();
},
),
],
),
),
);
}
}
class _RoundIcon extends StatelessWidget {
const _RoundIcon({
super.key,
required this.tooltip,
required this.icon,
required this.onPressed,
});
final String tooltip;
final IconData icon;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return SizedBox(
width: 36,
height: 36,
child: IconButton(
tooltip: tooltip,
onPressed: onPressed,
padding: EdgeInsets.zero,
style: IconButton.styleFrom(
backgroundColor: AppColors.card.withValues(alpha: 0.94),
foregroundColor: AppColors.onSurface,
),
icon: Icon(icon, size: 18),
),
);
}
}
class _RailItem extends StatelessWidget {
const _RailItem({
super.key,
required this.icon,
required this.label,
required this.onTap,
this.selected = false,
this.countKey,
});
final IconData icon;
final String label;
final VoidCallback onTap;
final bool selected;
final Key? countKey;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 14),
child: Semantics(
button: true,
label: label,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(22),
child: SizedBox(
width: 44,
child: Column(
children: [
Icon(
icon,
size: 22,
color: selected ? AppColors.blue : AppColors.onSurface,
),
const SizedBox(height: 2),
Text(
label,
key: countKey,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.cream,
fontSize: 9,
shadows: [Shadow(blurRadius: 6, color: Colors.black)],
),
),
],
),
),
),
),
);
}
}