133 lines
4.4 KiB
Dart
133 lines
4.4 KiB
Dart
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(),
|
||
);
|
||
}
|