// ignore_for_file: unnecessary_this import 'dart:async'; import 'package:didvan/config/auth_config.dart'; import 'package:didvan/models/enums.dart'; import 'package:didvan/models/overview_data.dart'; import 'package:didvan/models/requests/studio.dart'; import 'package:didvan/models/studio_details_data.dart'; import 'package:didvan/models/tag.dart'; import 'package:didvan/providers/core.dart'; import 'package:didvan/services/content/content_service.dart'; import 'package:didvan/services/media/media.dart'; import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request_helper.dart'; class StudioDetailsState extends CoreProvier { late StudioDetailsData studio; bool isStudioLoaded = false; StudioDetailsData? nextStudio; StudioDetailsData? prevStudio; late int initialIndex; StudioRequestArgs? args; StudioRequestArgs? podcastArgs; final List relatedQueue = []; AppState alongSideState = AppState.idle; int _trackingTimerCounter = 0; Timer? _trackingTimer; StreamSubscription? _positionSubscription; int _selectedDetailsIndex = 0; Timer? timer; int timerValue = 10; bool stopOnPodcastEnds = false; int get selectedDetailsIndex => _selectedDetailsIndex; set selectedDetailsIndex(int value) { _selectedDetailsIndex = value; if (value == 2) { getRelatedContents(); } notifyListeners(); } Future getStudioDetails( int id, { StudioRequestArgs? args, bool? isForward, bool fetchOnly = false, }) async { try { if (args != null) { this.args = args; } if (this.args?.type == 'podcast') { podcastArgs = this.args; } if (MediaService.currentPodcast?.id == id && this.args?.type == 'podcast' && !fetchOnly) { return; } _selectedDetailsIndex = 0; if (isForward != null) { if (isForward) { prevStudio = studio; studio = nextStudio!; nextStudio = null; } else { nextStudio = studio; studio = prevStudio!; prevStudio = null; } isStudioLoaded = true; notifyListeners(); _handlePodcastPlayback(studio); } if (isForward == null) { if (this.args?.type == 'podcast') { MediaService.audioPlayerTag = 'podcast-${MediaService.currentPodcast?.id ?? ''}'; } appState = AppState.busy; } else { alongSideState = AppState.busy; notifyListeners(); } final service = RequestService( RequestHelper.studioDetails( id, this.args ?? const StudioRequestArgs(page: 0), ), ); await service.httpGet(); nextStudio = null; prevStudio = null; if (stopOnPodcastEnds) { timerValue = 10; } stopOnPodcastEnds = false; if (service.isSuccess) { if (args?.type == 'video') { handleTracking(id: id, sendRequest: false); } final result = service.result; studio = StudioDetailsData.fromJson(result['studio']); isStudioLoaded = true; if (args?.page == 0) { initialIndex = 0; appState = AppState.idle; notifyListeners(); return; } if (result['nextStudio'].isNotEmpty && this.args?.page != 0) { nextStudio = StudioDetailsData.fromJson(result['nextStudio']); } if (result['prevStudio'].isNotEmpty && this.args?.page != 0) { prevStudio = StudioDetailsData.fromJson(result['prevStudio']); } if (isForward == null && !fetchOnly) { await _handlePodcastPlayback(studio); } alongSideState = AppState.idle; appState = AppState.idle; notifyListeners(); return; } if (isForward == null) { appState = AppState.failed; } else { alongSideState = AppState.failed; notifyListeners(); } } catch (e) { update(); appState = AppState.idle; } } Future _handlePodcastPlayback(StudioDetailsData studio) async { _positionSubscription?.cancel(); if (args?.type == 'podcast') { MediaService.currentPodcast = studio; MediaService.podcastPlaylistArgs = args; await MediaService.handleAudioPlayback( audioSource: studio.link, id: studio.id, isVoiceMessage: false, onTrackChanged: (isNext) { if (isNext && nextStudio != null) { getStudioDetails(nextStudio!.id); } else if (!isNext && prevStudio != null) { getStudioDetails(prevStudio!.id); } }, ); if (nextStudio != null) { _positionSubscription = MediaService.audioPlayer.positionStream.listen((event) { if (this.args?.type != 'podcast' || MediaService.audioPlayerTag?.contains('message') == true) { return; } final duration = MediaService.duration ?? Duration(seconds: this.studio.duration); if (event.compareTo(duration) > 0 && this.nextStudio != null) { if (this.stopOnPodcastEnds) { MediaService.resetAudioPlayer(); return; } this.getStudioDetails(this.nextStudio!.id, isForward: true); } }); } } else { MediaService.audioPlayer.pause(); } } Future setStudioFromPrefetched( OverviewData ov, { required bool isVideo, }) async { studio = StudioDetailsData( id: ov.id, duration: ov.duration ?? 0, title: ov.title, description: ov.description, image: ov.image, link: ov.link ?? '', iframe: ov.iframe, createdAt: ov.createdAt, order: 1, marked: ov.marked, comments: ov.comments, tags: const [], type: isVideo ? 'video' : 'podcast', relatedContentsIsEmpty: true, ); isStudioLoaded = true; initialIndex = 0; args = StudioRequestArgs( page: 0, type: isVideo ? 'video' : 'podcast', ); if (!isVideo) { podcastArgs = args; MediaService.currentPodcast = studio; } appState = AppState.idle; notifyListeners(); if (ov.keycloakId != null) { // ignore: discarded_futures ContentService.instance.getContent(ov.keycloakId!).then((res) { if (!res.isSuccess || res.data == null) return; final item = res.data!; final tags = [ for (var i = 0; i < (item.tagNames.isNotEmpty ? item.tagNames : item.keywords) .length; i++) Tag( id: i + 1, label: (item.tagNames.isNotEmpty ? item.tagNames : item.keywords)[i], ), ]; studio = StudioDetailsData( id: studio.id, duration: item.mediaDuration ?? studio.duration, title: studio.title, description: studio.description, image: studio.image, link: studio.link, iframe: studio.iframe, createdAt: studio.createdAt, order: studio.order, marked: studio.marked, comments: studio.comments, tags: tags, type: studio.type, relatedContentsIsEmpty: true, ); notifyListeners(); }); } } Future getStudioDetailFromContentService( String uuid, { required bool isVideo, int idForUi = 0, }) async { appState = AppState.busy; notifyListeners(); try { final res = await ContentService.instance.getContent(uuid); if (!res.isSuccess || res.data == null) { appState = AppState.failed; notifyListeners(); return; } final item = res.data!; final mediaUrl = isVideo ? (item.videoUrl ?? '') : (item.audioUrl ?? ''); studio = StudioDetailsData( id: idForUi != 0 ? idForUi : item.id.hashCode, keycloakId: item.id, duration: item.mediaDuration ?? 0, title: item.title, description: item.summary ?? '', image: item.coverImage ?? '', link: mediaUrl, iframe: null, createdAt: item.publishedAt?.toIso8601String() ?? DateTime.now().toIso8601String(), order: 1, marked: item.isBookmarked, comments: 0, tags: [ for (var i = 0; i < (item.tagNames.isNotEmpty ? item.tagNames : item.keywords) .length; i++) Tag( id: i + 1, label: (item.tagNames.isNotEmpty ? item.tagNames : item.keywords)[i], ), ], type: isVideo ? 'video' : 'podcast', relatedContentsIsEmpty: true, ); initialIndex = 0; args = StudioRequestArgs( page: 0, type: isVideo ? 'video' : 'podcast', ); if (!isVideo) { podcastArgs = args; MediaService.currentPodcast = studio; } await _loadPrevNextFromList(uuid, isVideo: isVideo); isStudioLoaded = true; appState = AppState.idle; notifyListeners(); } catch (e) { appState = AppState.failed; notifyListeners(); } } static const String _podcastCategoryIdSt = '7cbe250a-d0c4-44c2-970b-c4abbdf0d6c2'; static const String _videocastCategoryIdSt = '321a1cc9-b1e0-4787-a8db-6faa9a3477c4'; Future _loadPrevNextFromList( String currentUuid, { required bool isVideo, }) async { try { final categoryId = isVideo ? _videocastCategoryIdSt : _podcastCategoryIdSt; final listRes = await ContentService.instance.getContents( page: 1, limit: 100, categoryId: categoryId, sortBy: const [MapEntry('createdAt', 'DESC')], ); if (!listRes.isSuccess || listRes.data == null) return; final items = listRes.data!.data; final idx = items.indexWhere((c) => c.id == currentUuid); if (idx < 0) return; StudioDetailsData? buildFrom(int i) { if (i < 0 || i >= items.length) return null; final c = items[i]; return StudioDetailsData( id: c.id.hashCode, keycloakId: c.id, duration: c.mediaDuration ?? 0, title: c.title, description: c.summary ?? '', image: c.coverImage ?? '', link: (isVideo ? c.videoUrl : c.audioUrl) ?? '', iframe: null, createdAt: (c.publishedAt ?? c.createdAt)?.toIso8601String() ?? DateTime.now().toIso8601String(), order: 1, marked: c.isBookmarked, comments: 0, tags: const [], type: isVideo ? 'video' : 'podcast', relatedContentsIsEmpty: true, ); } nextStudio = buildFrom(idx - 1); prevStudio = buildFrom(idx + 1); Future hydrate(StudioDetailsData? s) async { if (s == null) return null; final pos = items.indexWhere((c) => c.id.hashCode == s.id); if (pos < 0) return s; final detail = await ContentService.instance.getContent(items[pos].id); if (!detail.isSuccess || detail.data == null) return s; final c = detail.data!; return StudioDetailsData( id: s.id, keycloakId: c.id, duration: c.mediaDuration ?? s.duration, title: c.title, description: c.summary ?? s.description, image: c.coverImage ?? s.image, link: (isVideo ? c.videoUrl : c.audioUrl) ?? s.link, iframe: s.iframe, createdAt: s.createdAt, order: s.order, marked: c.isBookmarked, comments: s.comments, tags: const [], type: s.type, relatedContentsIsEmpty: true, ); } final hydrated = await Future.wait([ hydrate(nextStudio), hydrate(prevStudio), ]); nextStudio = hydrated[0]; prevStudio = hydrated[1]; } catch (_) {} } Future getRelatedContents() async { if (studio.relatedContents.isNotEmpty) return; if (!AuthConfig.useLegacyApi) { final tagNames = studio.tags.map((t) => t.label).where((l) => l.isNotEmpty).toList(); if (tagNames.isEmpty) { studio.relatedContentsIsEmpty = true; notifyListeners(); return; } try { final res = await ContentService.instance.getContentsByTagNames( tagNames: tagNames, page: 1, limit: 30, ); if (!res.isSuccess || res.data == null) { studio.relatedContentsIsEmpty = true; notifyListeners(); return; } final currentUuid = studio.keycloakId; final tagSet = tagNames.toSet(); final scored = >[]; for (final c in res.data!.data) { if (currentUuid != null && c.id == currentUuid) continue; int score = 0; for (final t in c.tagNames) { if (tagSet.contains(t)) score++; } scored.add(MapEntry(score, c)); } scored.sort((a, b) => b.key.compareTo(a.key)); for (final e in scored.take(3)) { studio.relatedContents.add(OverviewData.fromContentItem(e.value)); } studio.relatedContentsIsEmpty = studio.relatedContents.isEmpty; notifyListeners(); } catch (e) { studio.relatedContentsIsEmpty = true; notifyListeners(); } return; } relatedQueue.add(studio.id); final service = RequestService(RequestHelper.tag( ids: studio.tags.map((tag) => tag.id).toList(), itemId: studio.id, type: args?.type, )); await service.httpGet(); if (service.isSuccess) { final relateds = service.result['contents']; for (var i = 0; i < relateds.length; i++) { studio.relatedContents.add(OverviewData.fromJson(relateds[i])); } if (relateds.isEmpty) { studio.relatedContentsIsEmpty = true; } } else { studio.relatedContentsIsEmpty = true; } notifyListeners(); } void onCommentsChanged(int count) { studio.comments = count; notifyListeners(); } Future handleTracking({ required int id, bool sendRequest = true, }) async { if (!sendRequest) { _trackingTimerCounter = 0; _trackingTimer = Timer.periodic(const Duration(seconds: 1), (timer) { _trackingTimerCounter++; notifyListeners(); }); return; } if (!AuthConfig.useLegacyApi) { _trackingTimerCounter = 0; _trackingTimer?.cancel(); return; } final service = RequestService( RequestHelper.tracking(id, 'studio'), body: { 'sec': _trackingTimerCounter, }, ); service.put(); _trackingTimerCounter = 0; _trackingTimer?.cancel(); } @override void dispose() { _trackingTimer?.cancel(); _positionSubscription?.cancel(); super.dispose(); } }