import 'dart:convert'; import 'package:http/http.dart' as http; import '../models/models.dart'; import '../repositories/report_repository.dart'; import '../state/report_query.dart'; abstract class ReportDataSource extends ReportRepository { Future> recommended(); Future> reports(); Future> institutions(); Future institutionDetail(String institutionId); Future> listen(); Future reportDetail(String reportId); Future moduleDetail(String reportId, String moduleId); @override Future> getRecommended({String? topic}) async { final items = await recommended(); if (topic == null || topic == '全部') return items; return items.where((item) => item.topics.contains(topic)).toList(); } @override Future> getReports(ReportQuery query) async { final currentSearch = query.search.trim().toLowerCase(); final items = await reports(); final filtered = items.where((item) { final haystack = '${item.titleCn} ${item.subtitleCn} ${item.oneLiner} ' '${item.institution.nameCn} ${item.institution.nameEn} ' '${item.topics.join(' ')}' .toLowerCase(); if (currentSearch.isNotEmpty && !haystack.contains(currentSearch)) { return false; } if (query.topic != null && !item.topics.contains(query.topic)) { return false; } if (query.institutionId != null && item.institution.id != query.institutionId) { return false; } if (query.hasAudio && !item.hasAudio) { return false; } return true; }).toList(); filtered.sort((a, b) { final result = (b.releasedAt ?? '').compareTo(a.releasedAt ?? ''); return query.sort == ReportSort.oldest ? -result : result; }); return filtered; } @override Future getReportDetail(String reportId) => reportDetail(reportId); @override Future> getInstitutions() => institutions(); @override Future getInstitutionDetail(String institutionId) => institutionDetail(institutionId); @override Future> getListenItems() => listen(); @override Future getModuleDetail(String reportId, String moduleId) => moduleDetail(reportId, moduleId); } class RnbApiDataSource extends ReportDataSource { RnbApiDataSource({ http.Client? client, this.baseUrl = const String.fromEnvironment('RNB_API_BASE'), }) : client = client ?? http.Client(); final http.Client client; final String baseUrl; Future _get(String path) async { if (baseUrl.isEmpty) { throw StateError('RNB_API_BASE is required for live API requests.'); } final response = await client.get(Uri.parse('$baseUrl$path')); if (response.statusCode != 200) { throw StateError('Request failed: ${response.statusCode}'); } return jsonDecode(utf8.decode(response.bodyBytes)) as JsonMap; } List _items(JsonMap body) => asMapList(body['items']); @override Future> recommended() async { final body = await _get('/feed/recommended'); return _items(body).map(ReportCardModel.fromJson).toList(); } @override Future> reports() async { final body = await _get('/reports'); return _items(body).map(ReportCardModel.fromJson).toList(); } @override Future> institutions() async { final body = await _get('/institutions'); return _items(body).map(Institution.fromJson).toList(); } @override Future institutionDetail(String institutionId) async { return Institution.fromJson(await _get('/institutions/$institutionId')); } @override Future> listen() async { final body = await _get('/listen'); return _items(body).map(AudioItem.fromJson).toList(); } @override Future reportDetail(String reportId) async { return ReportDetail.fromJson(await _get('/reports/$reportId')); } @override Future moduleDetail(String reportId, String moduleId) async { return ModuleDetail.fromJson( await _get('/reports/$reportId/modules/$moduleId'), ); } }