feat(ui): 更新品牌启动页与选集交互

This commit is contained in:
Zhou Weidong committed 2026-10-19 09:28:16 +08:00
1 parent 4778ce2c75
commit c1cb7d1c11
87 files changed
+919 -552

No files matched your search

+3 -3
View File
@@ -52,11 +52,11 @@ class CommentTile extends StatelessWidget {
children: [
CircleAvatar(
radius: isReply ? 12 : 16,
backgroundColor: AppColors.blue.withValues(alpha: 0.2),
backgroundColor: AppColors.raised,
child: Icon(
Icons.person_rounded,
size: isReply ? 14 : 18,
color: AppColors.blueSoft,
color: AppColors.cream,
),
),
const SizedBox(width: 12),
@@ -101,7 +101,7 @@ class CommentTile extends StatelessWidget {
),
Text(
comment.content,
style: const TextStyle(color: Colors.white, fontSize: 14),
style: const TextStyle(color: AppColors.cream, fontSize: 14),
),
if (comment.isPending)
Padding(
@@ -19,9 +19,10 @@ import '../../../l10n/app_localizations.dart';
import '../../interaction/widgets/report_dialog.dart';
import 'comment_tile.dart';
const double _minSheetHeight = 240;
const double _maxSheetHeight = 460;
const double _minSheetHeight = 280;
const double _maxSheetHeight = 520;
const int _maxCommentLength = 300;
const double _composerRadius = 22;
/// 服务端评论底部弹窗:匿名列表、游标翻页、回复、点赞和本人删除。
class DramaCommentsSheet extends StatefulWidget {
@@ -164,7 +165,7 @@ class _DramaCommentsSheetState extends State<DramaCommentsSheet> {
child: SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
padding: const EdgeInsets.fromLTRB(20, 4, 20, 12),
child: ListenableBuilder(
listenable: _controller,
builder: (context, _) => _content(context),
@@ -182,9 +183,13 @@ class _DramaCommentsSheetState extends State<DramaCommentsSheet> {
children: [
Text(
l10n.comments,
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w700),
style: const TextStyle(
color: AppColors.cream,
fontSize: 18,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 8),
const SizedBox(height: 12),
Expanded(child: _list(l10n)),
if (_replyTo != null) _replyBanner(l10n, _replyTo!),
_composer(l10n),
@@ -199,24 +204,14 @@ class _DramaCommentsSheetState extends State<DramaCommentsSheet> {
);
}
if (_controller.error != null && _controller.comments.isEmpty) {
return Center(
return EmptyState(
key: const Key('comment-list-error'),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
errorText(l10n, _controller.error, l10n.commentsLoadFailed),
textAlign: TextAlign.center,
style: const TextStyle(color: AppColors.muted),
),
const SizedBox(height: 8),
TextButton(
key: const Key('comment-list-retry'),
onPressed: _controller.load,
child: Text(l10n.retry),
),
],
),
icon: Icons.cloud_off_rounded,
title: errorText(l10n, _controller.error, l10n.commentsLoadFailed),
actionLabel: l10n.retry,
actionKey: const Key('comment-list-retry'),
onAction: _controller.load,
compact: true,
);
}
if (_controller.comments.isEmpty) {
@@ -272,7 +267,7 @@ class _DramaCommentsSheetState extends State<DramaCommentsSheet> {
l10n.replyingTo(
replyTo.nickname.isEmpty ? l10n.anonymousUser : replyTo.nickname,
),
style: const TextStyle(color: AppColors.blueSoft, fontSize: 12),
style: const TextStyle(color: AppColors.muted, fontSize: 12),
),
),
IconButton(
@@ -286,6 +281,25 @@ class _DramaCommentsSheetState extends State<DramaCommentsSheet> {
),
);
InputDecoration _composerDecoration(String hint) {
const radius = BorderRadius.all(Radius.circular(_composerRadius));
const clear = OutlineInputBorder(
borderRadius: radius,
borderSide: BorderSide.none,
);
return InputDecoration(
hintText: hint,
hintStyle: const TextStyle(color: AppColors.muted, fontSize: 14),
counterText: '',
filled: true,
fillColor: AppColors.raised,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
border: clear,
enabledBorder: clear,
focusedBorder: clear,
);
}
Widget _composer(AppLocalizations l10n) => Row(
children: [
Expanded(
@@ -295,16 +309,9 @@ class _DramaCommentsSheetState extends State<DramaCommentsSheet> {
focusNode: _inputFocus,
maxLength: _maxCommentLength,
maxLines: 1,
decoration: InputDecoration(
hintText: l10n.writeComment,
counterText: '',
filled: true,
fillColor: const Color(0xFF35353A),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide.none,
),
),
style: const TextStyle(color: AppColors.cream, fontSize: 14),
cursorColor: AppColors.gold,
decoration: _composerDecoration(l10n.writeComment),
onSubmitted: (_) => _post(),
),
),
@@ -387,7 +394,7 @@ class _CommentThread extends StatelessWidget {
],
),
),
const Divider(height: 1),
const Divider(height: 1, color: AppColors.outlineVariantDark),
],
);
}
@@ -0,0 +1,116 @@
// Author: fruit.com.cn
import 'package:flutter/material.dart';
import '../../../core/data/models/drama.dart';
import '../../../core/theme/app_colors.dart';
const double _cellRadius = 8;
const double _numberSize = 15;
const double _lockSize = 16;
const double _lockGap = 4;
const double _lockWeight = 300;
const double _coverAlpha = 0.45;
/// 选集格子。需要解锁时铺一层透明底,锁在上,集数在锁下面。
class EpisodePickerCell extends StatelessWidget {
const EpisodePickerCell({
required this.episode,
required this.selected,
required this.lockLabel,
required this.onTap,
super.key,
});
final Episode episode;
final bool selected;
final String lockLabel;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return Material(
clipBehavior: Clip.antiAlias,
color: selected ? AppColors.bluePressed : AppColors.raised,
borderRadius: BorderRadius.circular(_cellRadius),
child: InkWell(
onTap: onTap,
child: Stack(
fit: StackFit.expand,
children: [
if (!episode.locked) Center(child: _EpisodeNumber(episode.number)),
if (episode.locked)
_LockCover(
episodeId: episode.id,
number: episode.number,
label: lockLabel,
),
],
),
),
);
}
}
class _EpisodeNumber extends StatelessWidget {
const _EpisodeNumber(this.number);
final int number;
@override
Widget build(BuildContext context) {
return Text(
'$number',
style: const TextStyle(
color: AppColors.cream,
fontSize: _numberSize,
fontWeight: FontWeight.w600,
height: 1,
),
);
}
}
/// 透明底铺满格子,锁在上,集数在锁下面。
class _LockCover extends StatelessWidget {
const _LockCover({
required this.episodeId,
required this.number,
required this.label,
});
final String episodeId;
final int number;
final String label;
@override
Widget build(BuildContext context) {
return Stack(
fit: StackFit.expand,
children: [
ColoredBox(
key: ValueKey('episode-lock-cover-$episodeId'),
color: AppColors.ink.withValues(alpha: _coverAlpha),
),
Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.lock_outline,
key: ValueKey('episode-lock-$episodeId'),
size: _lockSize,
weight: _lockWeight,
fill: 0,
color: AppColors.gold,
semanticLabel: label,
),
const SizedBox(height: _lockGap),
_EpisodeNumber(number),
],
),
),
],
);
}
}
@@ -120,7 +120,7 @@ class _SeekProgressBarState extends State<SeekProgressBar> {
child: SliderTheme(
data: SliderTheme.of(context).copyWith(
trackHeight: scrubbing ? 4 : 3,
activeTrackColor: AppColors.blueSoft,
activeTrackColor: AppColors.gold,
inactiveTrackColor: Colors.white30,
thumbColor: Colors.white,
thumbShape: RoundSliderThumbShape(