fix(favorite): 统一收藏状态与互动图标
This commit is contained in:
1 parent
4b97f89c0b
commit
d3dbc9f8dc
13 files changed
+347
-213
No files matched your search
@@ -119,7 +119,6 @@ class PlayerLogic extends ChangeNotifier {
|
||||
final Map<String, Future<void>> _episodeStatsInFlight = {};
|
||||
final Map<String, int> _episodeStatsRevision = {};
|
||||
final Map<String, int> _dramaStatsRevision = {};
|
||||
final Map<String, int> _dramaFavoriteCounts = {};
|
||||
|
||||
/// 解锁中的集 id 与失败文案;空表示没有进行中的解锁。
|
||||
String? unlockingEpisodeId;
|
||||
@@ -185,8 +184,6 @@ class PlayerLogic extends ChangeNotifier {
|
||||
|
||||
EpisodeStats? episodeStats(String episodeId) => _episodeStats[episodeId];
|
||||
|
||||
int? dramaFavoriteCount(String dramaId) => _dramaFavoriteCounts[dramaId];
|
||||
|
||||
void notify() {
|
||||
if (!_disposed) notifyListeners();
|
||||
}
|
||||
@@ -621,9 +618,6 @@ class PlayerLogic extends ChangeNotifier {
|
||||
);
|
||||
_episodeStats[episodeId] = stats;
|
||||
_episodeStatsFetchedAt[episodeId] = _clock();
|
||||
if (stats.dramaId.isNotEmpty) {
|
||||
_dramaFavoriteCounts[stats.dramaId] = stats.favoriteCount;
|
||||
}
|
||||
final episodeIndex = sessionEpisodes.indexWhere(
|
||||
(episode) => episode.id == episodeId,
|
||||
);
|
||||
@@ -703,29 +697,13 @@ class PlayerLogic extends ChangeNotifier {
|
||||
notify();
|
||||
}
|
||||
|
||||
/// 收藏成功时同时修正剧级计数和所有已缓存的同剧互动状态。
|
||||
void applyDramaFavoriteState(
|
||||
String dramaId, {
|
||||
required bool favorited,
|
||||
int? favoriteCount,
|
||||
}) {
|
||||
/// 收藏成功后让旧 stats 请求失效,计数本身只写 FavoriteController。
|
||||
void markDramaFavoriteChanged(String dramaId) {
|
||||
_dramaStatsRevision.update(
|
||||
dramaId,
|
||||
(value) => value + 1,
|
||||
ifAbsent: () => 1,
|
||||
);
|
||||
if (favoriteCount != null) {
|
||||
_dramaFavoriteCounts[dramaId] = favoriteCount;
|
||||
}
|
||||
for (final entry in _episodeStats.entries.toList()) {
|
||||
final stats = entry.value;
|
||||
if (stats.dramaId != dramaId) continue;
|
||||
_episodeStats[entry.key] = stats.copyWith(
|
||||
favorited: favorited,
|
||||
favoriteCount: favoriteCount,
|
||||
);
|
||||
}
|
||||
notify();
|
||||
}
|
||||
|
||||
String _fileUrl(String path) =>
|
||||
|
||||
@@ -564,6 +564,7 @@ class _PlayerPageState extends State<PlayerPage>
|
||||
context.read<FavoriteController>().applyServerState(
|
||||
stats.dramaId,
|
||||
saved: stats.favorited,
|
||||
favoriteCount: stats.favoriteCount,
|
||||
);
|
||||
}
|
||||
setState(() {
|
||||
@@ -571,21 +572,6 @@ class _PlayerPageState extends State<PlayerPage>
|
||||
});
|
||||
}
|
||||
|
||||
/// 收藏状态确认后同步右侧剧级计数。
|
||||
void _applyFavoriteDelta(Drama drama, {required bool nowSaved}) {
|
||||
final current = logic.dramaFavoriteCount(drama.id) ?? drama.favoriteCount;
|
||||
final next = current == null
|
||||
? null
|
||||
: nowSaved
|
||||
? current + 1
|
||||
: (current > 0 ? current - 1 : 0);
|
||||
logic.applyDramaFavoriteState(
|
||||
drama.id,
|
||||
favorited: nowSaved,
|
||||
favoriteCount: next,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _openComments(
|
||||
BuildContext context,
|
||||
Drama drama,
|
||||
@@ -1026,6 +1012,7 @@ class _PlayerPageState extends State<PlayerPage>
|
||||
InteractionController interactions,
|
||||
) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final favorites = context.watch<FavoriteController>();
|
||||
final entry = logic.queue.entryAt(index);
|
||||
final drama = entry.drama;
|
||||
final episode = _resolvedEpisode(index);
|
||||
@@ -1194,13 +1181,12 @@ class _PlayerPageState extends State<PlayerPage>
|
||||
liked: interactions.isEpisodeLiked(episode.id),
|
||||
likeCount:
|
||||
interactions.episodeLikeCount(episode.id) ?? episode.likeCount,
|
||||
favoriteCount:
|
||||
logic.dramaFavoriteCount(drama.id) ?? drama.favoriteCount,
|
||||
favoriteCount: favorites.favoriteCountFor(drama),
|
||||
commentCount:
|
||||
logic.episodeStats(episode.id)?.commentCount ??
|
||||
_commentCounts[episode.id] ??
|
||||
episode.commentCount,
|
||||
saved: context.watch<FavoriteController>().isSaved(drama.id),
|
||||
saved: favorites.isSaved(drama.id),
|
||||
onLike: () async {
|
||||
await _toggleLike(context, interactions, episode);
|
||||
},
|
||||
@@ -1277,6 +1263,7 @@ class _PlayerPageState extends State<PlayerPage>
|
||||
) {
|
||||
if (logic.pip || logic.pipPreparing) return const SizedBox.expand();
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final favorites = context.watch<FavoriteController>();
|
||||
final entry = logic.queue.entryAt(index);
|
||||
final drama = entry.drama;
|
||||
final episode = _resolvedEpisode(index);
|
||||
@@ -1306,9 +1293,8 @@ class _PlayerPageState extends State<PlayerPage>
|
||||
logic.episodeStats(episode.id)?.commentCount ??
|
||||
_commentCounts[episode.id] ??
|
||||
episode.commentCount,
|
||||
favoriteCount:
|
||||
logic.dramaFavoriteCount(drama.id) ?? drama.favoriteCount,
|
||||
saved: context.watch<FavoriteController>().isSaved(drama.id),
|
||||
favoriteCount: favorites.favoriteCountFor(drama),
|
||||
saved: favorites.isSaved(drama.id),
|
||||
playing: logic.session.userWantsPlay,
|
||||
playbackSpeed: _playbackSpeed,
|
||||
totalEpisodes: logic.visibleIndices.length,
|
||||
|
||||
@@ -166,7 +166,7 @@ extension _PlayerViewActions on _PlayerPageState {
|
||||
if (ok) {
|
||||
final nowSaved = favorites.isSaved(drama.id);
|
||||
if (nowSaved != wasSaved) {
|
||||
_applyFavoriteDelta(drama, nowSaved: nowSaved);
|
||||
logic.markDramaFavoriteChanged(drama.id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -269,8 +269,11 @@ class _HomeFeedOverlayState extends State<HomeFeedOverlay> {
|
||||
liked: widget.liked,
|
||||
favoriteCount: widget.favoriteCount,
|
||||
likeCount: widget.likeCount,
|
||||
commentCount: widget.commentCount,
|
||||
onSave: widget.onSave,
|
||||
onLike: widget.onLike,
|
||||
onComments: widget.onComments,
|
||||
onShare: widget.onShare,
|
||||
onEpisodes: widget.onEpisodes ?? widget.onOpenFullPlayer,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,19 +5,16 @@ import 'dart:math' as math;
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../core/data/models/drama.dart';
|
||||
import '../../../core/formatting/count_format.dart';
|
||||
import '../../../core/formatting/localized_content.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../../l10n/app_localizations.dart';
|
||||
import '../../home/widgets/home_drama_classification.dart';
|
||||
import 'interaction_rail.dart';
|
||||
|
||||
const double _seriesFont = 19;
|
||||
const double _tagFont = 13;
|
||||
const double _synopsisFont = 13;
|
||||
const double _watchAllHeight = 44;
|
||||
const double _actionGap = 28;
|
||||
const double _actionIcon = 32;
|
||||
const double _actionWidth = 64;
|
||||
const List<Shadow> _textShadows = [
|
||||
Shadow(color: AppColors.shadow, blurRadius: 6, offset: Offset(0, 1)),
|
||||
];
|
||||
@@ -307,112 +304,71 @@ class HomeFeedActionRail extends StatelessWidget {
|
||||
required this.liked,
|
||||
required this.onSave,
|
||||
required this.onLike,
|
||||
required this.onComments,
|
||||
required this.onShare,
|
||||
required this.onEpisodes,
|
||||
this.favoriteCount,
|
||||
this.likeCount,
|
||||
this.commentCount,
|
||||
});
|
||||
|
||||
final bool saved;
|
||||
final bool liked;
|
||||
final int? favoriteCount;
|
||||
final int? likeCount;
|
||||
final int? commentCount;
|
||||
final VoidCallback onSave;
|
||||
final VoidCallback onLike;
|
||||
final VoidCallback onComments;
|
||||
final VoidCallback onShare;
|
||||
final VoidCallback onEpisodes;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return SizedBox(
|
||||
width: _actionWidth,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_action(
|
||||
key: const Key('rail-save'),
|
||||
icon: saved
|
||||
? Icons.bookmark_rounded
|
||||
: Icons.bookmark_border_rounded,
|
||||
label: favoriteCount == null
|
||||
? l10n.favorite
|
||||
: formatInteractionCount(favoriteCount!),
|
||||
labelKey: const Key('rail-save-count'),
|
||||
onTap: onSave,
|
||||
),
|
||||
const SizedBox(height: _actionGap),
|
||||
_action(
|
||||
key: const Key('rail-like'),
|
||||
icon: liked
|
||||
? Icons.favorite_rounded
|
||||
: Icons.favorite_border_rounded,
|
||||
iconColor: liked
|
||||
? AppColors.doubleTapHeartRed
|
||||
: AppColors.textPrimary,
|
||||
label: likeCount == null
|
||||
? l10n.like
|
||||
: formatInteractionCount(likeCount!),
|
||||
labelKey: const Key('rail-like-count'),
|
||||
onTap: onLike,
|
||||
),
|
||||
const SizedBox(height: _actionGap),
|
||||
_action(
|
||||
key: const Key('rail-episodes'),
|
||||
icon: Icons.layers_rounded,
|
||||
label: l10n.episodes,
|
||||
labelKey: const Key('rail-episodes-label'),
|
||||
labelFontSize: 11,
|
||||
onTap: onEpisodes,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _action({
|
||||
required Key key,
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required Key labelKey,
|
||||
required VoidCallback onTap,
|
||||
Color iconColor = AppColors.textPrimary,
|
||||
double labelFontSize = 12,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
key: key,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onTap,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: _actionIcon,
|
||||
color: iconColor,
|
||||
shadows: const [
|
||||
Shadow(
|
||||
color: AppColors.shadowSoft,
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
label,
|
||||
key: labelKey,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: labelFontSize,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1,
|
||||
shadows: _textShadows,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
return InteractionRail(
|
||||
items: [
|
||||
InteractionRailItem(
|
||||
key: const Key('rail-save'),
|
||||
asset: 'assets/icon/rail/rail_save.png',
|
||||
label: l10n.favorite,
|
||||
count: favoriteCount,
|
||||
countKey: const Key('rail-save-count'),
|
||||
selected: saved,
|
||||
onTap: onSave,
|
||||
),
|
||||
InteractionRailItem(
|
||||
key: const Key('rail-comments'),
|
||||
asset: 'assets/icon/rail/rail_comment.png',
|
||||
label: l10n.comments,
|
||||
count: commentCount,
|
||||
countKey: const Key('rail-comments-count'),
|
||||
onTap: onComments,
|
||||
),
|
||||
InteractionRailItem(
|
||||
key: const Key('rail-like'),
|
||||
asset: 'assets/icon/rail/rail_like.png',
|
||||
label: l10n.like,
|
||||
count: likeCount,
|
||||
countKey: const Key('rail-like-count'),
|
||||
selected: liked,
|
||||
selectedColor: AppColors.doubleTapHeartRed,
|
||||
onTap: onLike,
|
||||
),
|
||||
InteractionRailItem(
|
||||
key: const Key('rail-share'),
|
||||
asset: 'assets/icon/rail/rail_share.png',
|
||||
label: l10n.share,
|
||||
onTap: onShare,
|
||||
),
|
||||
InteractionRailItem(
|
||||
key: const Key('rail-episodes'),
|
||||
icon: Icons.layers_rounded,
|
||||
label: l10n.episodes,
|
||||
countKey: const Key('rail-episodes-label'),
|
||||
onTap: onEpisodes,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -13,17 +13,19 @@ const double _itemGap = 24;
|
||||
class InteractionRailItem {
|
||||
const InteractionRailItem({
|
||||
required this.key,
|
||||
required this.asset,
|
||||
required this.label,
|
||||
required this.onTap,
|
||||
this.asset,
|
||||
this.icon,
|
||||
this.count,
|
||||
this.countKey,
|
||||
this.selected = false,
|
||||
this.selectedColor = AppColors.blue,
|
||||
});
|
||||
}) : assert((asset == null) != (icon == null));
|
||||
|
||||
final Key key;
|
||||
final String asset;
|
||||
final String? asset;
|
||||
final IconData? icon;
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
final int? count;
|
||||
@@ -68,14 +70,24 @@ class _RailButton extends StatelessWidget {
|
||||
child: IconButton(
|
||||
onPressed: item.onTap,
|
||||
padding: EdgeInsets.zero,
|
||||
icon: Image.asset(
|
||||
item.asset,
|
||||
width: _iconSize,
|
||||
height: _iconSize,
|
||||
fit: BoxFit.contain,
|
||||
color: item.selected ? item.selectedColor : AppColors.onPrimary,
|
||||
colorBlendMode: BlendMode.srcIn,
|
||||
),
|
||||
icon: item.asset != null
|
||||
? Image.asset(
|
||||
item.asset!,
|
||||
width: _iconSize,
|
||||
height: _iconSize,
|
||||
fit: BoxFit.contain,
|
||||
color: item.selected
|
||||
? item.selectedColor
|
||||
: AppColors.onPrimary,
|
||||
colorBlendMode: BlendMode.srcIn,
|
||||
)
|
||||
: Icon(
|
||||
item.icon,
|
||||
size: _iconSize,
|
||||
color: item.selected
|
||||
? item.selectedColor
|
||||
: AppColors.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
|
||||
Reference in new issue
Block a user