169 lines
5.0 KiB
Dart
169 lines
5.0 KiB
Dart
// Author: fruit.com.cn
|
|
|
|
import 'package:dio/dio.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
import '../../core/data/models/drama.dart';
|
|
import '../../core/data/models/watch_progress.dart';
|
|
import '../../core/data/remote/api_client.dart';
|
|
import '../../core/data/repositories/watch_history_repository.dart';
|
|
import '../../core/data/repositories/playback_repository.dart';
|
|
import '../../core/data/services/favorite_controller.dart';
|
|
import '../../core/data/services/download_controller.dart';
|
|
import '../../core/data/services/playback_position_cache.dart';
|
|
import '../../core/data/services/wallet_controller.dart';
|
|
import '../player/player_view.dart';
|
|
|
|
class DramaDetailLogic extends ChangeNotifier {
|
|
DramaDetailLogic({
|
|
required this.drama,
|
|
required this.history,
|
|
required this.playback,
|
|
required this.favorites,
|
|
required this.positionCache,
|
|
});
|
|
|
|
/// 先用列表卡片渲染,取到详情后换成带完整剧集的版本。
|
|
Drama drama;
|
|
final WatchHistoryRepository history;
|
|
final PlaybackRepository playback;
|
|
final FavoriteController favorites;
|
|
final PlaybackPositionCache positionCache;
|
|
bool loading = false;
|
|
bool failed = false;
|
|
|
|
/// 接口给出的失败原因;为空时界面回退到本地文案。
|
|
String? failureMessage;
|
|
int? continueIndex;
|
|
WatchProgress? progress;
|
|
String? progressError;
|
|
int _favoriteCountDelta = 0;
|
|
bool _disposed = false;
|
|
|
|
void notify() {
|
|
if (!_disposed) notifyListeners();
|
|
}
|
|
|
|
/// 列表卡片只带一集可播预览,进详情时补一次完整剧集列表。
|
|
Future<void> load(Future<Drama> Function() fetch) async {
|
|
if (drama.detailLoaded) {
|
|
await loadContinue();
|
|
return;
|
|
}
|
|
loading = true;
|
|
failed = false;
|
|
failureMessage = null;
|
|
notify();
|
|
try {
|
|
drama = await fetch();
|
|
await loadContinue();
|
|
} on ApiException catch (exception) {
|
|
failed = true;
|
|
failureMessage = exception.message;
|
|
} on DioException {
|
|
// 断网/超时:交给 l10n 文案,不把 HTTP 细节丢给用户
|
|
failed = true;
|
|
failureMessage = 'networkError';
|
|
} catch (_) {
|
|
failed = true;
|
|
} finally {
|
|
loading = false;
|
|
notify();
|
|
}
|
|
}
|
|
|
|
Future<void> loadContinue() async {
|
|
final cached = positionCache.latestForDrama(drama.id);
|
|
if (cached != null) {
|
|
final index = _indexOf(cached.episodeId);
|
|
if (index != null) {
|
|
progress = WatchProgress(
|
|
dramaId: cached.dramaId,
|
|
episodeId: cached.episodeId,
|
|
position: cached.position,
|
|
duration: cached.duration,
|
|
updatedAt: cached.updatedAt,
|
|
);
|
|
continueIndex = index;
|
|
progressError = null;
|
|
notify();
|
|
return;
|
|
}
|
|
}
|
|
try {
|
|
final record = await history.progressFor(drama.id);
|
|
if (_disposed) return;
|
|
progress = record;
|
|
progressError = null;
|
|
continueIndex = record == null
|
|
? null
|
|
: _indexOf(record.episodeId) ?? continueIndex;
|
|
notify();
|
|
} catch (exception) {
|
|
if (_disposed) return;
|
|
// 拿不到续看位置不影响看第一集,但要让用户知道(可重试)
|
|
progressError = exception is ApiException
|
|
? exception.message
|
|
: 'progressError';
|
|
notify();
|
|
}
|
|
}
|
|
|
|
int? _indexOf(String episodeId) {
|
|
final index = drama.episodes.indexWhere(
|
|
(episode) => episode.id == episodeId,
|
|
);
|
|
return index >= 0 ? index : null;
|
|
}
|
|
|
|
/// 该集的续看进度(0~1),用于剧集行上的进度条;没有记录返回 null。
|
|
double? progressFractionFor(String episodeId) {
|
|
final record = progress;
|
|
if (record == null || record.episodeId != episodeId) return null;
|
|
if (!record.hasPosition) return null;
|
|
return record.fraction;
|
|
}
|
|
|
|
Future<void> openPlayer(BuildContext context, int index) async {
|
|
await Navigator.of(context).push(
|
|
MaterialPageRoute<void>(
|
|
builder: (_) => PlayerPage(
|
|
drama: drama,
|
|
initialIndex: index,
|
|
wallet: context.read<WalletController>(),
|
|
downloads: context.read<DownloadController>(),
|
|
),
|
|
),
|
|
);
|
|
await loadContinue();
|
|
}
|
|
|
|
/// 详情页展示用的收藏数;没有服务端计数时继续保持缺省展示。
|
|
int? get adjustedFavoriteCount {
|
|
final base = drama.favoriteCount;
|
|
if (base == null) return null;
|
|
final adjusted = base + _favoriteCountDelta;
|
|
return adjusted < 0 ? 0 : adjusted;
|
|
}
|
|
|
|
/// 收藏切换:乐观状态由控制器负责回滚,这里只在成功后同步计数。
|
|
Future<bool> toggleFavorite() async {
|
|
final wasSaved = favorites.isSaved(drama.id);
|
|
final ok = await favorites.toggle(drama.id, card: drama);
|
|
if (!ok) return false;
|
|
final nowSaved = favorites.isSaved(drama.id);
|
|
if (nowSaved != wasSaved) {
|
|
_favoriteCountDelta += nowSaved ? 1 : -1;
|
|
notify();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_disposed = true;
|
|
super.dispose();
|
|
}
|
|
}
|