Files
video/app/lib/services/video_service.dart
T
2026-10-15 20:36:59 +08:00

133 lines
4.4 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import '../core/network/api_client.dart';
import '../core/network/endpoints.dart';
import '../models/video_item.dart';
import '../models/watch_result.dart';
/// 视频服务:Feed / 详情 / 观看计费上报(《03-接口文档》第 5 章)
class VideoService {
VideoService._();
/// 5.1 Feed 流:type=recommend|latest|hot,游标分页
static Future<FeedPage> fetchFeed({
String type = 'recommend',
String? cursor,
int limit = 10,
int? categoryId,
}) async {
final data = await ApiClient.get<Map<String, dynamic>>(
Endpoints.feed,
queryParameters: {
'type': type,
if (cursor != null && cursor.isNotEmpty) 'cursor': cursor,
'limit': limit,
if (categoryId != null) 'category_id': categoryId,
},
);
final list = ((data['list'] as List?) ?? const [])
.whereType<Map<String, dynamic>>()
.map(VideoItem.fromJson)
.toList();
return FeedPage(items: list, nextCursor: data['next_cursor'] as String?);
}
/// 5.2 视频详情(登录返回续播进度 my_position)
static Future<Map<String, dynamic>> fetchDetail(int videoId) =>
ApiClient.get<Map<String, dynamic>>(Endpoints.videoDetail(videoId));
/// 5.3 观看上报(计费):开始播放时调用,客户端已按 video_id+日期 本地去重
static Future<WatchResult> reportWatch(int videoId, {int? position}) async {
final data = await ApiClient.post<Map<String, dynamic>>(
Endpoints.videoWatch(videoId),
data: {if (position != null) 'position': position},
);
return WatchResult.fromJson(data);
}
/// 5.4 完整观看上报:进度 ≥95% 或自然播完时调用(创作者收益计数)
/// 返回 credited:今日首次有效为 true,其余情况 false(无需 UI 处理)
static Future<bool> reportComplete(int videoId, int watchDuration) async {
final data = await ApiClient.post<Map<String, dynamic>>(
Endpoints.videoWatchComplete(videoId),
data: {'watch_duration': watchDuration},
);
return (data['credited'] as bool?) ?? false;
}
/// 5.29 点赞/取消
static Future<Map<String, dynamic>> toggleLike(int videoId) =>
ApiClient.post<Map<String, dynamic>>(Endpoints.videoLike(videoId));
/// 5.30 收藏/取消
static Future<Map<String, dynamic>> toggleFavorite(int videoId) =>
ApiClient.post<Map<String, dynamic>>(Endpoints.videoFavorite(videoId));
/// 5.12 编辑视频(仅作者):仅传需要修改的字段
static Future<void> updateVideo(
int videoId, {
String? title,
String? description,
int? categoryId,
List<String>? tags,
}) async {
await ApiClient.put<Map<String, dynamic>>(
Endpoints.videoEdit(videoId),
data: {
if (title != null) 'title': title,
if (description != null) 'description': description,
if (categoryId != null) 'category_id': categoryId,
if (tags != null) 'tags': tags,
},
);
}
/// 5.13 删除视频(仅作者,服务端软删除)
static Future<void> deleteVideo(int videoId) =>
ApiClient.delete<Map<String, dynamic>>(Endpoints.videoDelete(videoId));
/// 5.9 标签视频列表(游客可):sort=hot(默认)| latest,普通分页
static Future<TagVideosPage> fetchTagVideos(
String tagName, {
String sort = 'hot',
int page = 1,
int pageSize = 20,
}) async {
final data = await ApiClient.get<Map<String, dynamic>>(
Endpoints.tagVideos(Uri.encodeComponent(tagName)),
queryParameters: {'sort': sort, 'page': page, 'page_size': pageSize},
);
return TagVideosPage.fromJson(data, fallbackTag: tagName);
}
}
/// Feed 分页结果(游标)
class FeedPage {
FeedPage({required this.items, this.nextCursor});
final List<VideoItem> items;
/// null 表示没有更多
final String? nextCursor;
}
/// 标签视频分页结果(普通 page/page_size 分页)
class TagVideosPage {
TagVideosPage({required this.tag, required this.total, required this.items});
final String tag;
final int total;
final List<VideoItem> items;
factory TagVideosPage.fromJson(
Map<String, dynamic> json, {
String fallbackTag = '',
}) =>
TagVideosPage(
tag: (json['tag'] as String?) ?? fallbackTag,
total: (json['total'] as num?)?.toInt() ?? 0,
items: ((json['list'] as List?) ?? const [])
.whereType<Map<String, dynamic>>()
.map(VideoItem.fromJson)
.toList(),
);
}