import 'dart:async'; import 'dart:math'; import 'package:didvan/config/auth_config.dart'; import 'package:didvan/models/category.dart'; import 'package:didvan/models/content.dart'; import 'package:didvan/models/enums.dart'; import 'package:didvan/models/overview_data.dart'; import 'package:didvan/models/radar_details_data.dart'; import 'package:didvan/models/tag.dart'; import 'package:didvan/models/requests/radar.dart'; import 'package:didvan/providers/core.dart'; import 'package:didvan/services/content/content_service.dart'; import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request_helper.dart'; import 'package:didvan/services/s3/s3_media_service.dart'; class RadarDetailsState extends CoreProvier { final List radars = []; late Timer _trackingTimer; int _trackingTimerCounter = 0; late final int initialIndex; late final RadarRequestArgs args; String? initialDescription; bool isFetchingNewItem = false; final List relatedQueue = []; bool openComments = false; bool _isRequestInProgress = false; int _currentIndex = 0; int get currentIndex => _currentIndex; RadarDetailsData get currentRadar { try { return radars[_currentIndex]!; } catch (e) { return radars[_currentIndex + 1]!; } } Future getRadarDetails(int id, {bool? isForward}) async { if (_isRequestInProgress) { return; } _isRequestInProgress = true; if (isForward == null) { appState = AppState.busy; } else { isFetchingNewItem = true; notifyListeners(); } try { if (RequestService.token == null || RequestService.token!.isEmpty) { await Future.delayed(const Duration(milliseconds: 500)); if (RequestService.token == null || RequestService.token!.isEmpty) { throw Exception('Token not available'); } } final service = RequestService(RequestHelper.radarDetails(id, args)); await service.httpGet(); _handleTracking(sendRequest: isForward != null); if (service.isSuccess) { final result = service.result; final radar = RadarDetailsData.fromJson(result['radar']); if (isForward == null && initialDescription != null) { radar.description = initialDescription; } if (isForward == null && radar.description == null) { await _fetchDescriptionFromList(radar.id, radar); } if (args.page == 0) { radars.add(radar); initialIndex = 0; appState = AppState.idle; isFetchingNewItem = false; _isRequestInProgress = false; return; } RadarDetailsData? prevRadar; if (result['prevRadar'].isNotEmpty) { prevRadar = RadarDetailsData.fromJson(result['prevRadar']); } RadarDetailsData? nextRadar; if (result['nextRadar'].isNotEmpty) { nextRadar = RadarDetailsData.fromJson(result['nextRadar']); } if (isForward == null) { radars .addAll(List.generate(max(radar.order - 2, 0), (index) => null)); if (prevRadar != null) { radars.add(prevRadar); } radars.add(radar); if (nextRadar != null) { radars.add(nextRadar); } _currentIndex = initialIndex = radar.order - 1; } else if (isForward) { if (!exists(nextRadar) && nextRadar != null) { radars.add(nextRadar); } _currentIndex++; } else if (!isForward) { if (!exists(prevRadar) && prevRadar != null) { radars[_currentIndex - 2] = prevRadar; } _currentIndex--; } isFetchingNewItem = false; if (currentRadar.contents.length == 1) { getRelatedContents(); } appState = AppState.idle; } else { isFetchingNewItem = false; if (isForward == null) { appState = AppState.failed; } else { notifyListeners(); } } } catch (e) { // ignore: avoid_print print('Error fetching radar details: $e'); isFetchingNewItem = false; if (isForward == null) { appState = AppState.failed; } notifyListeners(); } finally { _isRequestInProgress = false; } } Future _fetchDescriptionFromList( int radarId, RadarDetailsData radarItem) async { try { final service = RequestService( RequestHelper.radarOverviews( args: const RadarRequestArgs(page: 1), ), ); await service.httpGet(); if (service.isSuccess) { final radarList = service.result['radars'] as List?; if (radarList != null) { for (var item in radarList) { if (item['id'] == radarId) { final radarData = OverviewData.fromJson(item); if (radarData.description.isNotEmpty) { radarItem.description = radarData.description; } break; } } } } } catch (e) { // ignore: avoid_print print('Error fetching description from list: $e'); } } Future getRelatedContents() async { if (currentRadar.relatedContents.isNotEmpty && currentRadar.relatedContentsIsEmpty) { return; } relatedQueue.add(currentRadar.id); if (!AuthConfig.useLegacyApi) { final tagNames = currentRadar.tags .map((t) => t.label) .where((l) => l.isNotEmpty) .toList(); if (tagNames.isEmpty) { _markRelatedEmpty(); return; } final res = await ContentService.instance.getContentsByTagNames( tagNames: tagNames, page: 1, limit: 30, ); if (!res.isSuccess || res.data == null) { _markRelatedEmpty(); return; } final tagSet = tagNames.toSet(); final scored = >[]; for (final c in res.data!.data) { if (c.id.hashCode == currentRadar.id) continue; final shared = c.tagNames.where(tagSet.contains).length; if (shared > 0) scored.add(MapEntry(shared, c)); } scored.sort((a, b) => b.key.compareTo(a.key)); final target = radars.firstWhere((e) => e?.id == currentRadar.id); if (target == null) return; for (final e in scored.take(3)) { target.relatedContents.add(OverviewData.fromContentItem(e.value)); } if (target.relatedContents.isEmpty) { target.relatedContentsIsEmpty = true; } notifyListeners(); return; } final service = RequestService(RequestHelper.tag( ids: currentRadar.tags.map((tag) => tag.id).toList(), itemId: currentRadar.id, type: 'radar', )); await service.httpGet(); if (service.isSuccess) { final relateds = service.result['contents']; for (var i = 0; i < relateds.length; i++) { radars .where((element) => element != null) .firstWhere((element) => element!.id == currentRadar.id)! .relatedContents .add(OverviewData.fromJson(relateds[i])); } if (relateds.isEmpty) { radars .where((element) => element != null) .firstWhere((element) => element!.id == currentRadar.id) ?.relatedContentsIsEmpty = true; } } else { radars .where((element) => element != null) .firstWhere((element) => element!.id == currentRadar.id) ?.relatedContentsIsEmpty = true; } notifyListeners(); } /// واکشیِ جزئیات از content-service جدید (با uuid) و نمایش در همان /// UI رادار. body فرمتِ Editor.js را به بلوک‌های `Content` ترجمه می‌کند. Future getContentDetail(String uuid) async { appState = AppState.busy; notifyListeners(); try { _trackingTimer = Timer(const Duration(seconds: 0), () {}); final res = await ContentService.instance.getContent(uuid); if (!res.isSuccess || res.data == null) { appState = AppState.failed; notifyListeners(); return; } final item = res.data!; final contents = await _bodyToContents(item.body); String? podcastUrl; for (final ct in contents) { if (ct.audio != null && ct.audio!.isNotEmpty) { podcastUrl = ct.audio; break; } } int? podcastDuration; if (item.audioId != null) { podcastDuration = S3MediaService.instance.durationFor(item.audioId); } podcastDuration ??= item.mediaDuration; radars.clear(); radars.add(RadarDetailsData( id: item.id.hashCode, title: item.title, image: item.coverImage ?? '', timeToRead: 0, createdAt: item.publishedAt?.toIso8601String() ?? DateTime.now().toIso8601String(), podcast: podcastUrl, forManagers: false, marked: item.isBookmarked, comments: 0, tags: item.tagNames .where((n) => n.isNotEmpty) .map((n) => Tag(id: n.hashCode, label: n)) .toList(), contents: contents, categories: const [], order: 1, duration: podcastDuration, description: item.summary, relatedContentsIsEmpty: false, keycloakId: item.id, )); initialIndex = 0; _currentIndex = 0; appState = AppState.idle; notifyListeners(); getRelatedContents(); } catch (e) { // ignore: avoid_print print('getContentDetail error: $e'); appState = AppState.failed; notifyListeners(); } } /// همان مَپِّرِ Editor.js → List که در NewsDetailsState استفاده می‌شود. Future> _bodyToContents(dynamic body) async { final result = []; if (body is! Map) return result; final blocks = body['blocks']; if (blocks is! List) return result; var order = 0; for (final block in blocks) { if (block is! Map) continue; final type = block['type']; final data = block['data']; if (data is! Map) continue; order++; switch (type) { case 'header': final lvl = (data['level'] as num?)?.toInt() ?? 2; final t = (data['text'] ?? '').toString(); result.add(Content( order: order, text: '$t', audio: null, video: null, image: null, largeImage: null, caption: null)); break; case 'paragraph': final t = (data['text'] ?? '').toString(); if (t.trim().isEmpty) break; result.add(Content( order: order, text: t.contains('<') ? t : '

$t

', audio: null, video: null, image: null, largeImage: null, caption: null)); break; case 'image': final id = data['id']; if (id is String && id.isNotEmpty) { final url = await S3MediaService.instance.resolveUrl(id); if (url != null) { result.add(Content( order: order, text: null, audio: null, video: null, image: url, largeImage: url, caption: data['caption']?.toString())); } } break; case 'mediaLibrary': case 'medialibrary': case 'media': final id = data['id']; if (id is String && id.isNotEmpty) { final url = await S3MediaService.instance.resolveUrl(id); if (url != null) { final mime = S3MediaService.instance.mimeFor(id) ?? ''; final isAudio = mime.startsWith('audio/'); final isVideo = mime.startsWith('video/'); final isImage = mime.startsWith('image/'); result.add(Content( order: order, text: null, audio: isAudio ? url : null, video: isVideo ? url : null, image: isImage ? url : null, largeImage: isImage ? url : null, caption: data['caption']?.toString())); } } break; case 'table': final rows = data['content']; if (rows is List) { final sb = StringBuffer(''); for (var i = 0; i < rows.length; i++) { final row = rows[i]; if (row is! List) continue; sb.write(''); for (final cell in row) { final tag = i == 0 ? 'th' : 'td'; sb.write('<$tag>${cell ?? ''}'); } sb.write(''); } sb.write('
'); result.add(Content( order: order, text: sb.toString(), audio: null, video: null, image: null, largeImage: null, caption: null)); } break; default: break; } } return result; } bool exists(RadarDetailsData? radar) => radars.any((r) => radar != null && r != null && r.id == radar.id); void onCommentsChanged(int count) { radars.firstWhere((radar) => radar?.id == currentRadar.id)!.comments = count; notifyListeners(); } Future _handleTracking({bool sendRequest = true}) async { if (!sendRequest) { _trackingTimerCounter = 0; _trackingTimer = Timer.periodic(const Duration(seconds: 1), (timer) { _trackingTimerCounter++; }); return; } final service = RequestService( RequestHelper.tracking(currentRadar.id, 'radar'), body: { 'sec': _trackingTimerCounter, }, ); service.put(); _trackingTimerCounter = 0; _trackingTimer.cancel(); } void _markRelatedEmpty() { final target = radars.firstWhere( (e) => e?.id == currentRadar.id, orElse: () => null, ); if (target != null) target.relatedContentsIsEmpty = true; notifyListeners(); } @override void dispose() { _handleTracking(sendRequest: true); _trackingTimer.cancel(); super.dispose(); } }