diff --git a/lib/core/data/models/app_catalog.dart b/lib/core/data/models/app_catalog.dart new file mode 100644 index 0000000..ba898b6 --- /dev/null +++ b/lib/core/data/models/app_catalog.dart @@ -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 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 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), + ); +} + +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 episodes; + + factory AppDramaDetail.fromJson(Map 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)) + .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 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 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 dramas; + + factory AppSection.fromJson(Map 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)) + .toList(), + ); +} + +class AppFeed { + AppFeed({required this.banners, required this.sections}); + + final List banners; + final List sections; + + factory AppFeed.fromJson(Map json) => AppFeed( + banners: (json['banners'] as List? ?? const []) + .map((e) => AppBanner.fromJson(e as Map)) + .toList(), + sections: (json['sections'] as List? ?? const []) + .map((e) => AppSection.fromJson(e as Map)) + .toList(), + ); +} diff --git a/lib/core/data/repositories/http_catalog_repository.dart b/lib/core/data/repositories/http_catalog_repository.dart new file mode 100644 index 0000000..0e9bb37 --- /dev/null +++ b/lib/core/data/repositories/http_catalog_repository.dart @@ -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 fetchHomeFeed() async { + final data = await _client.get('/app/home/feed'); + return AppFeed.fromJson(data as Map); + } + + Future fetchDiscoverFeed() async { + final data = await _client.get('/app/discover/feed'); + return AppFeed.fromJson(data as Map); + } + + Future> fetchCategories() async { + final data = await _client.get('/app/categories'); + return (data as List) + .map((e) => AppCategory.fromJson(e as Map)) + .toList(); + } + + Future fetchDramaDetail(int id) async { + final data = await _client.get('/app/dramas/$id'); + return AppDramaDetail.fromJson(data as Map); + } +} diff --git a/lib/core/data/repositories/interaction_repository.dart b/lib/core/data/repositories/interaction_repository.dart new file mode 100644 index 0000000..eb58428 --- /dev/null +++ b/lib/core/data/repositories/interaction_repository.dart @@ -0,0 +1,61 @@ +// Author: fruit.com.cn + +import '../services/api_client.dart'; + +/// 评论、点赞、举报(A6)。 +class InteractionRepository { + InteractionRepository(this._client); + + final ApiClient _client; + + Future 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> 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 deleteComment(int id) async { + await _client.delete('/app/comments/$id'); + } + + Future toggleLike({ + required String targetType, + required int targetId, + }) async { + await _client.post('/app/likes/toggle', data: { + 'targetType': targetType, + 'targetId': targetId, + }); + } + + Future report({ + required String targetType, + required int targetId, + required String reason, + }) async { + await _client.post('/app/reports', data: { + 'targetType': targetType, + 'targetId': targetId, + 'reason': reason, + }); + } +} diff --git a/lib/core/data/repositories/playback_repository.dart b/lib/core/data/repositories/playback_repository.dart new file mode 100644 index 0000000..1510d86 --- /dev/null +++ b/lib/core/data/repositories/playback_repository.dart @@ -0,0 +1,16 @@ +// Author: fruit.com.cn + +import '../services/api_client.dart'; + +/// 播放票据(A4)。 +class PlaybackRepository { + PlaybackRepository(this._client); + + final ApiClient _client; + + /// 签发播放票据与签名地址,客户端原样使用 playUrl。 + Future> fetchPlayTicket(int episodeId) async { + final data = await _client.get('/app/episodes/$episodeId/play'); + return data as Map; + } +} diff --git a/lib/core/data/repositories/wallet_repository.dart b/lib/core/data/repositories/wallet_repository.dart new file mode 100644 index 0000000..6eeccbe --- /dev/null +++ b/lib/core/data/repositories/wallet_repository.dart @@ -0,0 +1,25 @@ +// Author: fruit.com.cn + +import '../services/api_client.dart'; + +/// 钱包、金币解锁(09 收费支付)。 +class WalletRepository { + WalletRepository(this._client); + + final ApiClient _client; + + Future> fetchWallet() async { + final data = await _client.get('/app/wallet'); + return data as Map; + } + + Future> unlockEpisode(int episodeId) async { + final data = await _client.post('/app/episodes/$episodeId/unlock'); + return data as Map; + } + + Future> fetchOrders() async { + final data = await _client.get('/app/orders'); + return data as List; + } +} diff --git a/lib/core/data/repositories/watch_repository.dart b/lib/core/data/repositories/watch_repository.dart new file mode 100644 index 0000000..5ab59b0 --- /dev/null +++ b/lib/core/data/repositories/watch_repository.dart @@ -0,0 +1,59 @@ +// Author: fruit.com.cn + +import '../services/api_client.dart'; + +/// 观看记录与收藏(A5)。 +class WatchRepository { + WatchRepository(this._client); + + final ApiClient _client; + + Future 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> fetchProgress(int dramaId) async { + final data = await _client.get('/app/watch/progress', query: { + 'dramaId': dramaId, + }); + return data as Map; + } + + Future> fetchRecent({int limit = 20}) async { + final data = await _client.get('/app/watch/recent', query: {'limit': limit}); + return data as List; + } + + Future deleteProgress(int dramaId) async { + await _client.delete('/app/watch/progress/$dramaId'); + } + + Future clearProgress() async { + await _client.delete('/app/watch/progress'); + } + + Future toggleFavorite(int dramaId) async { + final data = await _client.post('/app/favorites/toggle', data: { + 'dramaId': dramaId, + }); + final map = data as Map; + return map['favorited'] as bool? ?? false; + } + + Future> fetchFavorites() async { + final data = await _client.get('/app/favorites'); + return data as List; + } + + Future deleteFavorite(int dramaId) async { + await _client.delete('/app/favorites/$dramaId'); + } +}