didvan-app/lib/views/podcasts/studio_details/studio_details_state.dart

506 lines
15 KiB
Dart

// 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<int> 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<void> 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<void> _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<void> 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<void> 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<void> _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<StudioDetailsData?> 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<void> 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 = <MapEntry<int, dynamic>>[];
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<void> 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();
}
}