feat(flutter): 播放页互动数量与评论面板收紧
This commit is contained in:
1 parent
e39aa766ac
commit
bed6bbb75b
13 files changed
+265
-59
No files matched your search
@@ -65,6 +65,9 @@ class Drama {
|
||||
this.freeEpisodes = 0,
|
||||
this.isHot = false,
|
||||
this.viewCount = 0,
|
||||
this.likeCount,
|
||||
this.favoriteCount,
|
||||
this.commentCount,
|
||||
this.language = '',
|
||||
this.detailLoaded = false,
|
||||
});
|
||||
@@ -81,6 +84,11 @@ class Drama {
|
||||
final bool isHot;
|
||||
final int viewCount;
|
||||
|
||||
/// 详情下发的互动数量;卡片或旧接口没有这些字段时为 null(客户端不显示数字)。
|
||||
final int? likeCount;
|
||||
final int? favoriteCount;
|
||||
final int? commentCount;
|
||||
|
||||
/// 卡片语种(如 zh-CN / id),用于剧场语言筛选。
|
||||
final String language;
|
||||
|
||||
@@ -108,6 +116,9 @@ class Drama {
|
||||
isHot: json['isHot'] == true,
|
||||
// viewCount 后端是 Long,按字符串下发(见 json_numbers.dart)
|
||||
viewCount: jsonToInt(json['viewCount']),
|
||||
likeCount: _optionalInt(json['likeCount']),
|
||||
favoriteCount: _optionalInt(json['favoriteCount']),
|
||||
commentCount: _optionalInt(json['commentCount']),
|
||||
language: json['language'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
@@ -125,6 +136,9 @@ class Drama {
|
||||
freeEpisodes: (json['freeEpisodes'] as num?)?.toInt() ?? 0,
|
||||
isHot: json['isHot'] == true,
|
||||
viewCount: jsonToInt(json['viewCount']),
|
||||
likeCount: _optionalInt(json['likeCount']),
|
||||
favoriteCount: _optionalInt(json['favoriteCount']),
|
||||
commentCount: _optionalInt(json['commentCount']),
|
||||
language: json['language'] as String? ?? '',
|
||||
detailLoaded: true,
|
||||
episodes: (json['episodes'] as List? ?? const [])
|
||||
@@ -134,3 +148,5 @@ class Drama {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
int? _optionalInt(dynamic value) => value == null ? null : jsonToInt(value);
|
||||
@@ -0,0 +1,9 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
/// 互动数量展示:不足一万显示原数,一万及以上显示一位小数的「万」。
|
||||
///
|
||||
/// 例:`3200` → `3200`,`12400` → `1.2万`。
|
||||
String formatInteractionCount(int count) {
|
||||
if (count < 10000) return '$count';
|
||||
return '${(count / 10000).toStringAsFixed(1)}万';
|
||||
}
|
||||
@@ -83,6 +83,9 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
|
||||
bool _chromeVisible = true;
|
||||
double _playbackSpeed = 1.0;
|
||||
|
||||
/// 收藏切换后的本地增量:基准是详情快照,切换成功按服务端状态 ±1。
|
||||
final Map<String, int> _favoriteDelta = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -127,6 +130,14 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
|
||||
setState(() => _chromeVisible = false);
|
||||
}
|
||||
|
||||
/// 收藏状态确认后同步右侧计数(总数没有独立计数接口,按 ±1 跟随)。
|
||||
void _applyFavoriteDelta(String dramaId, {required bool nowSaved}) {
|
||||
setState(() {
|
||||
_favoriteDelta[dramaId] =
|
||||
(_favoriteDelta[dramaId] ?? 0) + (nowSaved ? 1 : -1);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListenableBuilder(
|
||||
@@ -377,9 +388,11 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
|
||||
episode: episode,
|
||||
controller: ready ? videoPort!.controller : null,
|
||||
liked: interactions.isLiked(drama.id),
|
||||
likeCount: interactions.likeCount(drama.id),
|
||||
commentCount: null,
|
||||
favoriteCount: null,
|
||||
likeCount: interactions.likeCount(drama.id) ?? drama.likeCount,
|
||||
commentCount: drama.commentCount,
|
||||
favoriteCount: drama.favoriteCount == null
|
||||
? null
|
||||
: drama.favoriteCount! + (_favoriteDelta[drama.id] ?? 0),
|
||||
saved: context.watch<FavoriteController>().isSaved(drama.id),
|
||||
playing: logic.session.userWantsPlay,
|
||||
playbackSpeed: _playbackSpeed,
|
||||
|
||||
@@ -100,8 +100,16 @@ extension _PlayerViewActions on _PlayerPageState {
|
||||
final favorites = context.read<FavoriteController>();
|
||||
if (!await ensureSignedIn(context) || !context.mounted) return;
|
||||
unawaited(context.read<Haptics>().lightImpact());
|
||||
final wasSaved = favorites.isSaved(drama.id);
|
||||
final ok = await favorites.toggle(drama.id, card: drama);
|
||||
if (ok || !context.mounted) return;
|
||||
if (!context.mounted) return;
|
||||
if (ok) {
|
||||
final nowSaved = favorites.isSaved(drama.id);
|
||||
if (nowSaved != wasSaved) {
|
||||
_applyFavoriteDelta(drama.id, nowSaved: nowSaved);
|
||||
}
|
||||
return;
|
||||
}
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.favoriteToggleFailed),
|
||||
|
||||
@@ -107,7 +107,11 @@ class CommentTile extends StatelessWidget {
|
||||
),
|
||||
Text(
|
||||
comment.content,
|
||||
style: const TextStyle(color: AppColors.cream, fontSize: 14),
|
||||
style: const TextStyle(
|
||||
color: AppColors.cream,
|
||||
fontSize: 13,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
if (comment.isPending)
|
||||
Padding(
|
||||
@@ -153,14 +157,14 @@ class CommentTile extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _avatar() {
|
||||
final size = isReply ? 24.0 : 32.0;
|
||||
final size = isReply ? 22.0 : 28.0;
|
||||
if (comment.avatarUrl.isEmpty) {
|
||||
return CircleAvatar(
|
||||
radius: size / 2,
|
||||
backgroundColor: AppColors.raised,
|
||||
child: Icon(
|
||||
Icons.person_rounded,
|
||||
size: isReply ? 14 : 18,
|
||||
size: isReply ? 13 : 16,
|
||||
color: AppColors.cream,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -22,7 +22,9 @@ import 'comment_tile.dart';
|
||||
const double _minSheetHeight = 280;
|
||||
const double _maxSheetHeight = 520;
|
||||
const int _maxCommentLength = 300;
|
||||
const double _composerRadius = 22;
|
||||
const double _composerRadius = 20;
|
||||
const double _composerHeight = 40;
|
||||
const double _sendButtonSize = 32;
|
||||
const double _sheetRadius = 20;
|
||||
|
||||
/// 服务端评论底部弹窗:匿名列表、游标翻页、回复、点赞和本人删除。
|
||||
@@ -70,6 +72,7 @@ class _DramaCommentsSheetState extends State<DramaCommentsSheet> {
|
||||
if (result.isSuccess) {
|
||||
_input.clear();
|
||||
setState(() => _replyTo = null);
|
||||
FocusScope.of(context).unfocus();
|
||||
return;
|
||||
}
|
||||
_showMessage(
|
||||
@@ -174,7 +177,7 @@ class _DramaCommentsSheetState extends State<DramaCommentsSheet> {
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 4, 20, 12),
|
||||
padding: const EdgeInsets.fromLTRB(12, 4, 12, 12),
|
||||
child: ListenableBuilder(
|
||||
listenable: _controller,
|
||||
builder: (context, _) => _content(context),
|
||||
@@ -200,7 +203,7 @@ class _DramaCommentsSheetState extends State<DramaCommentsSheet> {
|
||||
key: count == null ? null : const Key('comment-count-label'),
|
||||
style: const TextStyle(
|
||||
color: AppColors.onSurface,
|
||||
fontSize: 20,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
@@ -223,7 +226,7 @@ class _DramaCommentsSheetState extends State<DramaCommentsSheet> {
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(child: _list(l10n)),
|
||||
if (_replyTo != null) _replyBanner(l10n, _replyTo!),
|
||||
_composer(l10n),
|
||||
@@ -323,11 +326,12 @@ class _DramaCommentsSheetState extends State<DramaCommentsSheet> {
|
||||
);
|
||||
return InputDecoration(
|
||||
hintText: hint,
|
||||
hintStyle: const TextStyle(color: AppColors.muted, fontSize: 14),
|
||||
hintStyle: const TextStyle(color: AppColors.muted, fontSize: 13),
|
||||
counterText: '',
|
||||
filled: true,
|
||||
fillColor: AppColors.raised,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
isDense: true,
|
||||
border: clear,
|
||||
enabledBorder: clear,
|
||||
focusedBorder: clear,
|
||||
@@ -337,29 +341,37 @@ class _DramaCommentsSheetState extends State<DramaCommentsSheet> {
|
||||
Widget _composer(AppLocalizations l10n) => Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
key: const Key('comment-input'),
|
||||
controller: _input,
|
||||
focusNode: _inputFocus,
|
||||
maxLength: _maxCommentLength,
|
||||
maxLines: 1,
|
||||
style: const TextStyle(color: AppColors.cream, fontSize: 14),
|
||||
cursorColor: AppColors.blue,
|
||||
decoration: _composerDecoration(l10n.writeComment),
|
||||
onSubmitted: (_) => _post(),
|
||||
child: SizedBox(
|
||||
height: _composerHeight,
|
||||
child: TextField(
|
||||
key: const Key('comment-input'),
|
||||
controller: _input,
|
||||
focusNode: _inputFocus,
|
||||
maxLength: _maxCommentLength,
|
||||
maxLines: 1,
|
||||
style: const TextStyle(color: AppColors.cream, fontSize: 13),
|
||||
cursorColor: AppColors.blue,
|
||||
decoration: _composerDecoration(l10n.writeComment),
|
||||
onSubmitted: (_) => _post(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
IconButton.filled(
|
||||
key: const Key('comment-post'),
|
||||
tooltip: l10n.postComment,
|
||||
onPressed: _controller.submitting ? null : _post,
|
||||
icon: _controller.submitting
|
||||
? const SizedBox.square(
|
||||
dimension: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.send_rounded),
|
||||
SizedBox.square(
|
||||
dimension: _sendButtonSize,
|
||||
child: IconButton.filled(
|
||||
key: const Key('comment-post'),
|
||||
tooltip: l10n.postComment,
|
||||
padding: EdgeInsets.zero,
|
||||
iconSize: 18,
|
||||
onPressed: _controller.submitting ? null : _post,
|
||||
icon: _controller.submitting
|
||||
? const SizedBox.square(
|
||||
dimension: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.send_rounded),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../core/formatting/count_format.dart';
|
||||
import '../../../core/formatting/localized_content.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../../core/theme/app_dimens.dart';
|
||||
|
||||
@@ -138,7 +138,9 @@ class _Rail extends StatelessWidget {
|
||||
_RailButton(
|
||||
key: const Key('player-rail-save'),
|
||||
icon: saved ? Icons.star_rounded : Icons.star_border_rounded,
|
||||
label: favoriteCount?.toString() ?? l10n.favorite,
|
||||
label: favoriteCount == null
|
||||
? l10n.favorite
|
||||
: formatInteractionCount(favoriteCount!),
|
||||
active: saved,
|
||||
onTap: onSave,
|
||||
),
|
||||
@@ -146,14 +148,18 @@ class _Rail extends StatelessWidget {
|
||||
_RailButton(
|
||||
key: const Key('player-rail-comments'),
|
||||
icon: Icons.chat_bubble_outline_rounded,
|
||||
label: commentCount?.toString() ?? l10n.comments,
|
||||
label: commentCount == null
|
||||
? l10n.comments
|
||||
: formatInteractionCount(commentCount!),
|
||||
onTap: onComments,
|
||||
),
|
||||
const SizedBox(height: _railGap),
|
||||
_RailButton(
|
||||
key: const Key('player-rail-like'),
|
||||
icon: liked ? Icons.favorite_rounded : Icons.favorite_border_rounded,
|
||||
label: likeCount?.toString() ?? l10n.like,
|
||||
label: likeCount == null
|
||||
? l10n.like
|
||||
: formatInteractionCount(likeCount!),
|
||||
active: liked,
|
||||
onTap: onLike,
|
||||
),
|
||||
|
||||
Reference in new issue
Block a user