506 lines
15 KiB
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();
|
|
}
|
|
}
|