import { action, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { Doc, DocListCastAsync } from '../../../fields/Doc'; import { InkTool } from '../../../fields/InkField'; import { Cast, NumCast, StrCast } from '../../../fields/Types'; import { Utils } from '../../../Utils'; import { DocServer } from '../../DocServer'; import { Docs } from '../../documents/Documents'; import { DocumentView } from '../../views/nodes/DocumentView'; import { FieldView, FieldViewProps } from '../../views/nodes/FieldView'; import '../../views/nodes/WebBox.scss'; import './YoutubeBox.scss'; import * as React from 'react'; import { SnappingManager } from '../../util/SnappingManager'; interface VideoTemplate { thumbnailUrl: string; videoTitle: string; videoId: string; duration: string; channelTitle: string; viewCount: string; publishDate: string; videoDescription: string; } /** * This class models the youtube search document that can be dropped on to canvas. */ @observer export class YoutubeBox extends React.Component { @observable YoutubeSearchElement: HTMLInputElement | undefined; @observable searchResultsFound: boolean = false; @observable searchResults: any[] = []; @observable videoClicked: boolean = false; @observable selectedVideoUrl: string = ''; @observable lisOfBackUp: JSX.Element[] = []; @observable videoIds: string | undefined; @observable videoDetails: any[] = []; @observable curVideoTemplates: VideoTemplate[] = []; public static LayoutString(fieldKey: string) { return FieldView.LayoutString(YoutubeBox, fieldKey); } /** * When component mounts, last search's results are laoded in based on the back up stored * in the document of the props. */ async componentDidMount() { //DocServer.getYoutubeChannels(); const castedSearchBackUp = Cast(this.props.Document.cachedSearchResults, Doc); const awaitedBackUp = await castedSearchBackUp; const castedDetailBackUp = Cast(this.props.Document.cachedDetails, Doc); const awaitedDetails = await castedDetailBackUp; if (awaitedBackUp) { const jsonList = await DocListCastAsync(awaitedBackUp.json); const jsonDetailList = await DocListCastAsync(awaitedDetails!.json); if (jsonList!.length !== 0) { runInAction(() => (this.searchResultsFound = true)); let index = 0; //getting the necessary information from backUps and building templates that will be used to map in render for (const video of jsonList!) { const videoId = await Cast(video.id, Doc); const id = StrCast(videoId!.videoId); const snippet = await Cast(video.snippet, Doc); const videoTitle = this.filterYoutubeTitleResult(StrCast(snippet!.title)); const thumbnail = await Cast(snippet!.thumbnails, Doc); const thumbnailMedium = await Cast(thumbnail!.medium, Doc); const thumbnailUrl = StrCast(thumbnailMedium!.url); const videoDescription = StrCast(snippet!.description); const pusblishDate = this.roundPublishTime(StrCast(snippet!.publishedAt))!; const channelTitle = StrCast(snippet!.channelTitle); let duration: string = ''; let viewCount: string = ''; if (jsonDetailList!.length !== 0) { const contentDetails = await Cast(jsonDetailList![index].contentDetails, Doc); const statistics = await Cast(jsonDetailList![index].statistics, Doc); duration = this.convertIsoTimeToDuration(StrCast(contentDetails!.duration)); viewCount = this.abbreviateViewCount(parseInt(StrCast(statistics!.viewCount)))!; } index = index + 1; const newTemplate: VideoTemplate = { videoId: id, videoTitle: videoTitle, thumbnailUrl: thumbnailUrl, publishDate: pusblishDate, channelTitle: channelTitle, videoDescription: videoDescription, duration: duration, viewCount: viewCount, }; runInAction(() => this.curVideoTemplates.push(newTemplate)); } } } } _ignore = 0; onPreWheel = (e: React.WheelEvent) => { this._ignore = e.timeStamp; }; onPrePointer = (e: React.PointerEvent) => { this._ignore = e.timeStamp; }; onPostPointer = (e: React.PointerEvent) => { if (this._ignore !== e.timeStamp) { e.stopPropagation(); } }; onPostWheel = (e: React.WheelEvent) => { if (this._ignore !== e.timeStamp) { e.stopPropagation(); } }; /** * Function that submits the title entered by user on enter press. */ onEnterKeyDown = (e: React.KeyboardEvent) => { if (e.keyCode === 13) { const submittedTitle = this.YoutubeSearchElement!.value; this.YoutubeSearchElement!.value = ''; this.YoutubeSearchElement!.blur(); DocServer.getYoutubeVideos(submittedTitle, this.processesVideoResults); } }; /** * The callback that is passed in to server, which functions as a way to * get videos that is returned by search. It also makes a call to server * to get details for the videos found. */ @action processesVideoResults = (videos: any[]) => { this.searchResults = videos; if (this.searchResults.length > 0) { this.searchResultsFound = true; this.videoIds = ''; videos.forEach(video => { if (this.videoIds === '') { this.videoIds = video.id.videoId; } else { this.videoIds = this.videoIds! + ', ' + video.id.videoId; } }); //Asking for details that include duration and viewCount from server for videoIds DocServer.getYoutubeVideoDetails(this.videoIds, this.processVideoDetails); this.backUpSearchResults(videos); if (this.videoClicked) { this.videoClicked = false; } } }; /** * The callback that is given to server to process and receive returned details about the videos. */ @action processVideoDetails = (videoDetails: any[]) => { this.videoDetails = videoDetails; this.props.Document.cachedDetails = Doc.Get.FromJson({ data: videoDetails, title: 'detailBackUp' }); }; /** * The function that stores the search results in the props document. */ backUpSearchResults = (videos: any[]) => { this.props.Document.cachedSearchResults = Doc.Get.FromJson({ data: videos, title: 'videosBackUp' }); }; /** * The function that filters out escaped characters returned by the api * in the title of the videos. */ filterYoutubeTitleResult = (resultTitle: string) => { let processedTitle: string = resultTitle.replace(/&/g, '&'); //.ReplaceAll("&", "&"); processedTitle = processedTitle.replace(/"'/g, "'"); processedTitle = processedTitle.replace(/"/g, '"'); return processedTitle; }; /** * The function that converts ISO date, which is passed in, to normal date and finds the * difference between today's date and that date, in terms of "ago" to imitate youtube. */ roundPublishTime = (publishTime: string) => { const date = new Date(publishTime).getTime(); const curDate = new Date().getTime(); const timeDif = curDate - date; const totalSeconds = timeDif / 1000; const totalMin = totalSeconds / 60; const totalHours = totalMin / 60; const totalDays = totalHours / 24; const totalMonths = totalDays / 30.417; const totalYears = totalMonths / 12; const truncYears = Math.trunc(totalYears); const truncMonths = Math.trunc(totalMonths); const truncDays = Math.trunc(totalDays); const truncHours = Math.trunc(totalHours); const truncMin = Math.trunc(totalMin); const truncSec = Math.trunc(totalSeconds); let pluralCase = ''; if (truncYears !== 0) { truncYears > 1 ? (pluralCase = 's') : (pluralCase = ''); return truncYears + ' year' + pluralCase + ' ago'; } else if (truncMonths !== 0) { truncMonths > 1 ? (pluralCase = 's') : (pluralCase = ''); return truncMonths + ' month' + pluralCase + ' ago'; } else if (truncDays !== 0) { truncDays > 1 ? (pluralCase = 's') : (pluralCase = ''); return truncDays + ' day' + pluralCase + ' ago'; } else if (truncHours !== 0) { truncHours > 1 ? (pluralCase = 's') : (pluralCase = ''); return truncHours + ' hour' + pluralCase + ' ago'; } else if (truncMin !== 0) { truncMin > 1 ? (pluralCase = 's') : (pluralCase = ''); return truncMin + ' minute' + pluralCase + ' ago'; } else if (truncSec !== 0) { truncSec > 1 ? (pluralCase = 's') : (pluralCase = ''); return truncSec + ' second' + pluralCase + ' ago'; } }; /** * The function that converts the passed in ISO time to normal duration time. */ convertIsoTimeToDuration = (isoDur: string) => { const convertedTime = isoDur.replace(/D|H|M/g, ':').replace(/P|T|S/g, '').split(':'); if (1 === convertedTime.length) { 2 !== convertedTime[0].length && (convertedTime[0] = '0' + convertedTime[0]), (convertedTime[0] = '0:' + convertedTime[0]); } else { for (var r = 1, l = convertedTime.length - 1; l >= r; r++) { 2 !== convertedTime[r].length && (convertedTime[r] = '0' + convertedTime[r]); } } return convertedTime.join(':'); }; /** * The function that rounds the viewCount to the nearest * thousand, million or billion, given a viewCount number. */ abbreviateViewCount = (viewCount: number) => { if (viewCount < 1000) { return viewCount.toString(); } else if (viewCount >= 1000 && viewCount < 1000000) { return Math.trunc(viewCount / 1000) + 'K'; } else if (viewCount >= 1000000 && viewCount < 1000000000) { return Math.trunc(viewCount / 1000000) + 'M'; } else if (viewCount >= 1000000000) { return Math.trunc(viewCount / 1000000000) + 'B'; } }; /** * The function that is called to decide on what'll be rendered by the component. * It renders search Results if found. If user didn't do a new search, it renders from the videoTemplates * generated by the backUps. If none present, renders nothing. */ renderSearchResultsOrVideo = () => { if (this.searchResultsFound) { if (this.searchResults.length !== 0) { return ( ); } else if (this.curVideoTemplates.length !== 0) { return ( ); } } else { return null; } }; /** * Given a videoId and title, creates a new youtube embedded url, and uses that * to create a new video document. */ @action embedVideoOnClick = (videoId: string, filteredTitle: string) => { const embeddedUrl = 'https://www.youtube.com/embed/' + videoId; this.selectedVideoUrl = embeddedUrl; const addFunction = this.props.addDocument!; const newVideoX = NumCast(this.props.Document.x); const newVideoY = NumCast(this.props.Document.y) + NumCast(this.props.Document.height); addFunction(Docs.Create.VideoDocument(embeddedUrl, { title: filteredTitle, _width: 400, _height: 315, x: newVideoX, y: newVideoY })); this.videoClicked = true; }; render() { const content = (
(this.YoutubeSearchElement = e!)} /> {this.renderSearchResultsOrVideo()}
); const frozen = !this.props.isSelected() || SnappingManager.IsResizing; const classname = 'webBox-cont' + (this.props.isSelected() && Doc.ActiveTool === InkTool.None && !SnappingManager.IsResizing ? '-interactive' : ''); return ( <>
{content}
{!frozen ? null :
} ); } }