424 lines
12 KiB
Dart
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)],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|