feat: A3-A6 数据层(内容/播放/记录收藏/互动的 HTTP 对接)
- AppCatalog 模型(feed/section/card/detail/episode/banner/category) - HttpCatalogRepository 对接首页/发现/分类/详情 - PlaybackRepository 播放票据、WatchRepository 记录收藏 - InteractionRepository 评论点赞举报、WalletRepository 钱包解锁
This commit is contained in:
1 parent
a7ac3b7fc7
commit
88a401512d
6 files changed
+382
No files matched your search
@@ -0,0 +1,188 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
// 与后端 /app/** 内容接口对齐的数据模型。
|
||||
|
||||
class AppEpisode {
|
||||
AppEpisode({
|
||||
required this.id,
|
||||
required this.number,
|
||||
required this.title,
|
||||
required this.durationSec,
|
||||
required this.locked,
|
||||
required this.unlockCoin,
|
||||
this.playUrl,
|
||||
});
|
||||
|
||||
final int id;
|
||||
final int number;
|
||||
final String title;
|
||||
final int durationSec;
|
||||
final bool locked;
|
||||
final int unlockCoin;
|
||||
final String? playUrl;
|
||||
|
||||
factory AppEpisode.fromJson(Map<String, dynamic> json) => AppEpisode(
|
||||
id: json['id'] as int,
|
||||
number: json['number'] as int,
|
||||
title: json['title'] as String? ?? '',
|
||||
durationSec: json['durationSec'] as int? ?? 0,
|
||||
locked: json['locked'] as bool? ?? false,
|
||||
unlockCoin: json['unlockCoin'] as int? ?? 0,
|
||||
playUrl: json['playUrl'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
class AppDramaCard {
|
||||
AppDramaCard({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.cover,
|
||||
required this.synopsis,
|
||||
required this.genre,
|
||||
required this.category,
|
||||
required this.totalEpisodes,
|
||||
required this.isHot,
|
||||
required this.viewCount,
|
||||
this.preview,
|
||||
});
|
||||
|
||||
final int id;
|
||||
final String title;
|
||||
final String cover;
|
||||
final String synopsis;
|
||||
final String genre;
|
||||
final String category;
|
||||
final int totalEpisodes;
|
||||
final bool isHot;
|
||||
final int viewCount;
|
||||
final AppEpisode? preview;
|
||||
|
||||
factory AppDramaCard.fromJson(Map<String, dynamic> json) => AppDramaCard(
|
||||
id: json['id'] as int,
|
||||
title: json['title'] as String? ?? '',
|
||||
cover: json['cover'] as String? ?? '',
|
||||
synopsis: json['synopsis'] as String? ?? '',
|
||||
genre: json['genre'] as String? ?? '',
|
||||
category: json['category'] as String? ?? '',
|
||||
totalEpisodes: json['totalEpisodes'] as int? ?? 0,
|
||||
isHot: json['isHot'] as bool? ?? false,
|
||||
viewCount: json['viewCount'] as int? ?? 0,
|
||||
preview: json['preview'] == null
|
||||
? null
|
||||
: AppEpisode.fromJson(json['preview'] as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
|
||||
class AppDramaDetail {
|
||||
AppDramaDetail({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.synopsis,
|
||||
required this.cover,
|
||||
required this.genre,
|
||||
required this.category,
|
||||
required this.totalEpisodes,
|
||||
required this.freeEpisodes,
|
||||
required this.unlockCoin,
|
||||
required this.isHot,
|
||||
required this.viewCount,
|
||||
required this.episodes,
|
||||
});
|
||||
|
||||
final int id;
|
||||
final String title;
|
||||
final String synopsis;
|
||||
final String cover;
|
||||
final String genre;
|
||||
final String category;
|
||||
final int totalEpisodes;
|
||||
final int freeEpisodes;
|
||||
final int unlockCoin;
|
||||
final bool isHot;
|
||||
final int viewCount;
|
||||
final List<AppEpisode> episodes;
|
||||
|
||||
factory AppDramaDetail.fromJson(Map<String, dynamic> json) => AppDramaDetail(
|
||||
id: json['id'] as int,
|
||||
title: json['title'] as String? ?? '',
|
||||
synopsis: json['synopsis'] as String? ?? '',
|
||||
cover: json['cover'] as String? ?? '',
|
||||
genre: json['genre'] as String? ?? '',
|
||||
category: json['category'] as String? ?? '',
|
||||
totalEpisodes: json['totalEpisodes'] as int? ?? 0,
|
||||
freeEpisodes: json['freeEpisodes'] as int? ?? 0,
|
||||
unlockCoin: json['unlockCoin'] as int? ?? 0,
|
||||
isHot: json['isHot'] as bool? ?? false,
|
||||
viewCount: json['viewCount'] as int? ?? 0,
|
||||
episodes: (json['episodes'] as List? ?? const [])
|
||||
.map((e) => AppEpisode.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
class AppBanner {
|
||||
AppBanner({
|
||||
required this.id,
|
||||
required this.imageUrl,
|
||||
required this.linkType,
|
||||
required this.linkValue,
|
||||
});
|
||||
|
||||
final int id;
|
||||
final String imageUrl;
|
||||
final String linkType;
|
||||
final String linkValue;
|
||||
|
||||
factory AppBanner.fromJson(Map<String, dynamic> json) => AppBanner(
|
||||
id: json['id'] as int,
|
||||
imageUrl: json['imageUrl'] as String? ?? '',
|
||||
linkType: json['linkType'] as String? ?? '',
|
||||
linkValue: json['linkValue'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
class AppCategory {
|
||||
AppCategory({required this.id, required this.name});
|
||||
|
||||
final int id;
|
||||
final String name;
|
||||
|
||||
factory AppCategory.fromJson(Map<String, dynamic> json) =>
|
||||
AppCategory(id: json['id'] as int, name: json['name'] as String? ?? '');
|
||||
}
|
||||
|
||||
class AppSection {
|
||||
AppSection({
|
||||
required this.code,
|
||||
required this.title,
|
||||
required this.dramas,
|
||||
});
|
||||
|
||||
final String code;
|
||||
final String title;
|
||||
final List<AppDramaCard> dramas;
|
||||
|
||||
factory AppSection.fromJson(Map<String, dynamic> json) => AppSection(
|
||||
code: json['code'] as String? ?? '',
|
||||
title: json['title'] as String? ?? '',
|
||||
dramas: (json['dramas'] as List? ?? const [])
|
||||
.map((e) => AppDramaCard.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
class AppFeed {
|
||||
AppFeed({required this.banners, required this.sections});
|
||||
|
||||
final List<AppBanner> banners;
|
||||
final List<AppSection> sections;
|
||||
|
||||
factory AppFeed.fromJson(Map<String, dynamic> json) => AppFeed(
|
||||
banners: (json['banners'] as List? ?? const [])
|
||||
.map((e) => AppBanner.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
sections: (json['sections'] as List? ?? const [])
|
||||
.map((e) => AppSection.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import '../models/app_catalog.dart';
|
||||
import '../services/api_client.dart';
|
||||
|
||||
/// 对接后端 `/app/**` 内容接口(A3)。
|
||||
class HttpCatalogRepository {
|
||||
HttpCatalogRepository(this._client);
|
||||
|
||||
final ApiClient _client;
|
||||
|
||||
Future<AppFeed> fetchHomeFeed() async {
|
||||
final data = await _client.get('/app/home/feed');
|
||||
return AppFeed.fromJson(data as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
Future<AppFeed> fetchDiscoverFeed() async {
|
||||
final data = await _client.get('/app/discover/feed');
|
||||
return AppFeed.fromJson(data as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
Future<List<AppCategory>> fetchCategories() async {
|
||||
final data = await _client.get('/app/categories');
|
||||
return (data as List)
|
||||
.map((e) => AppCategory.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<AppDramaDetail> fetchDramaDetail(int id) async {
|
||||
final data = await _client.get('/app/dramas/$id');
|
||||
return AppDramaDetail.fromJson(data as Map<String, dynamic>);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import '../services/api_client.dart';
|
||||
|
||||
/// 评论、点赞、举报(A6)。
|
||||
class InteractionRepository {
|
||||
InteractionRepository(this._client);
|
||||
|
||||
final ApiClient _client;
|
||||
|
||||
Future<void> postComment({
|
||||
required int dramaId,
|
||||
required String content,
|
||||
int? rootId,
|
||||
}) async {
|
||||
await _client.post('/app/comments', data: {
|
||||
'dramaId': dramaId,
|
||||
'content': content,
|
||||
if (rootId != null) 'rootId': rootId,
|
||||
});
|
||||
}
|
||||
|
||||
Future<List<dynamic>> fetchComments({
|
||||
required int dramaId,
|
||||
int? cursor,
|
||||
int size = 20,
|
||||
}) async {
|
||||
final data = await _client.get('/app/comments', query: {
|
||||
'dramaId': dramaId,
|
||||
if (cursor != null) 'cursor': cursor,
|
||||
'size': size,
|
||||
});
|
||||
return data as List;
|
||||
}
|
||||
|
||||
Future<void> deleteComment(int id) async {
|
||||
await _client.delete('/app/comments/$id');
|
||||
}
|
||||
|
||||
Future<void> toggleLike({
|
||||
required String targetType,
|
||||
required int targetId,
|
||||
}) async {
|
||||
await _client.post('/app/likes/toggle', data: {
|
||||
'targetType': targetType,
|
||||
'targetId': targetId,
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> report({
|
||||
required String targetType,
|
||||
required int targetId,
|
||||
required String reason,
|
||||
}) async {
|
||||
await _client.post('/app/reports', data: {
|
||||
'targetType': targetType,
|
||||
'targetId': targetId,
|
||||
'reason': reason,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import '../services/api_client.dart';
|
||||
|
||||
/// 播放票据(A4)。
|
||||
class PlaybackRepository {
|
||||
PlaybackRepository(this._client);
|
||||
|
||||
final ApiClient _client;
|
||||
|
||||
/// 签发播放票据与签名地址,客户端原样使用 playUrl。
|
||||
Future<Map<String, dynamic>> fetchPlayTicket(int episodeId) async {
|
||||
final data = await _client.get('/app/episodes/$episodeId/play');
|
||||
return data as Map<String, dynamic>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import '../services/api_client.dart';
|
||||
|
||||
/// 钱包、金币解锁(09 收费支付)。
|
||||
class WalletRepository {
|
||||
WalletRepository(this._client);
|
||||
|
||||
final ApiClient _client;
|
||||
|
||||
Future<Map<String, dynamic>> fetchWallet() async {
|
||||
final data = await _client.get('/app/wallet');
|
||||
return data as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> unlockEpisode(int episodeId) async {
|
||||
final data = await _client.post('/app/episodes/$episodeId/unlock');
|
||||
return data as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
Future<List<dynamic>> fetchOrders() async {
|
||||
final data = await _client.get('/app/orders');
|
||||
return data as List;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import '../services/api_client.dart';
|
||||
|
||||
/// 观看记录与收藏(A5)。
|
||||
class WatchRepository {
|
||||
WatchRepository(this._client);
|
||||
|
||||
final ApiClient _client;
|
||||
|
||||
Future<void> reportProgress({
|
||||
required int episodeId,
|
||||
required int positionSec,
|
||||
required int durationSec,
|
||||
}) async {
|
||||
await _client.post('/app/watch/progress', data: {
|
||||
'episodeId': episodeId,
|
||||
'positionSec': positionSec,
|
||||
'durationSec': durationSec,
|
||||
});
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> fetchProgress(int dramaId) async {
|
||||
final data = await _client.get('/app/watch/progress', query: {
|
||||
'dramaId': dramaId,
|
||||
});
|
||||
return data as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
Future<List<dynamic>> fetchRecent({int limit = 20}) async {
|
||||
final data = await _client.get('/app/watch/recent', query: {'limit': limit});
|
||||
return data as List;
|
||||
}
|
||||
|
||||
Future<void> deleteProgress(int dramaId) async {
|
||||
await _client.delete('/app/watch/progress/$dramaId');
|
||||
}
|
||||
|
||||
Future<void> clearProgress() async {
|
||||
await _client.delete('/app/watch/progress');
|
||||
}
|
||||
|
||||
Future<bool> toggleFavorite(int dramaId) async {
|
||||
final data = await _client.post('/app/favorites/toggle', data: {
|
||||
'dramaId': dramaId,
|
||||
});
|
||||
final map = data as Map<String, dynamic>;
|
||||
return map['favorited'] as bool? ?? false;
|
||||
}
|
||||
|
||||
Future<List<dynamic>> fetchFavorites() async {
|
||||
final data = await _client.get('/app/favorites');
|
||||
return data as List;
|
||||
}
|
||||
|
||||
Future<void> deleteFavorite(int dramaId) async {
|
||||
await _client.delete('/app/favorites/$dramaId');
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user