Frontent dev env (#247)

* Added frontend development files/environment

* More items-categories related removals

* Improvements in pages templates (inc. static pages)

* Improvements in video player

* Added empty home page message + cta

* Updates in media, playlist and management pages

* Improvements in material icons font loading

* Replaced media & playlists links in frontend dev-env

* frontend package version update

* chnaged frontend dev url port

* static files update

* Changed default position of theme switcher

* enabled frontend docker container
This commit is contained in:
Yiannis Stergiou
2021-07-11 18:01:34 +03:00
committed by GitHub
parent 060bb45725
commit aa6520daac
555 changed files with 201927 additions and 66002 deletions

View File

@@ -0,0 +1,58 @@
import React, { useState, useEffect } from 'react';
import { TextsContext } from '../../utils/contexts/';
import { MediaPageStore } from '../../utils/stores/';
import { formatViewsNumber } from '../../utils/helpers/';
import { PageActions, MediaPageActions } from '../../utils/actions/';
import { CircleIconButton, MaterialIcon } from '../_shared/';
export function MediaDislikeIcon() {
const [dislikedMedia, setDislikedMedia] = useState(MediaPageStore.get('user-liked-media'));
const [dislikesCounter, setDislikesCounter] = useState(formatViewsNumber(MediaPageStore.get('media-likes'), false));
function updateStateValues() {
setDislikedMedia(MediaPageStore.get('user-disliked-media'));
setDislikesCounter(formatViewsNumber(MediaPageStore.get('media-dislikes'), false));
}
function onCompleteMediaDislike() {
updateStateValues();
PageActions.addNotification(TextsContext._currentValue.messages.addToDisliked, 'mediaDislike');
}
function onCompleteMediaDislikeCancel() {
updateStateValues();
PageActions.addNotification(TextsContext._currentValue.messages.removeFromDisliked, 'cancelMediaDislike');
}
function onFailMediaDislikeRequest() {
PageActions.addNotification('Action failed', 'mediaDislikeRequestFail');
}
function toggleDislike(ev) {
ev.preventDefault();
ev.stopPropagation();
MediaPageActions[dislikedMedia ? 'undislikeMedia' : 'dislikeMedia']();
}
useEffect(() => {
MediaPageStore.on('disliked_media', onCompleteMediaDislike);
MediaPageStore.on('undisliked_media', onCompleteMediaDislikeCancel);
MediaPageStore.on('disliked_media_failed_request', onFailMediaDislikeRequest);
return () => {
MediaPageStore.removeListener('disliked_media', onCompleteMediaDislike);
MediaPageStore.removeListener('undisliked_media', onCompleteMediaDislikeCancel);
MediaPageStore.removeListener('disliked_media_failed_request', onFailMediaDislikeRequest);
};
}, []);
return (
<div className="like">
<button onClick={toggleDislike}>
<CircleIconButton type="span">
<MaterialIcon type="thumb_down" />
</CircleIconButton>
<span className="dislikes-counter">{dislikesCounter}</span>
</button>
</div>
);
}

View File

@@ -0,0 +1,58 @@
import React, { useState, useEffect } from 'react';
import { TextsContext } from '../../utils/contexts/';
import { MediaPageStore } from '../../utils/stores/';
import { formatViewsNumber } from '../../utils/helpers/';
import { PageActions, MediaPageActions } from '../../utils/actions/';
import { CircleIconButton, MaterialIcon } from '../_shared/';
export function MediaLikeIcon() {
const [likedMedia, setLikedMedia] = useState(MediaPageStore.get('user-liked-media'));
const [likesCounter, setLikesCounter] = useState(formatViewsNumber(MediaPageStore.get('media-likes'), false));
function updateStateValues() {
setLikedMedia(MediaPageStore.get('user-liked-media'));
setLikesCounter(formatViewsNumber(MediaPageStore.get('media-likes'), false));
}
function onCompleteMediaLike() {
updateStateValues();
PageActions.addNotification(TextsContext._currentValue.addToLiked, 'likedMedia');
}
function onCompleteMediaLikeCancel() {
updateStateValues();
PageActions.addNotification(TextsContext._currentValue.removeFromLiked, 'unlikedMedia');
}
function onFailMediaLikeRequest() {
PageActions.addNotification('Action failed', 'likedMediaRequestFail');
}
function toggleLike(ev) {
ev.preventDefault();
ev.stopPropagation();
MediaPageActions[likedMedia ? 'unlikeMedia' : 'likeMedia']();
}
useEffect(() => {
MediaPageStore.on('liked_media', onCompleteMediaLike);
MediaPageStore.on('unliked_media', onCompleteMediaLikeCancel);
MediaPageStore.on('liked_media_failed_request', onFailMediaLikeRequest);
return () => {
MediaPageStore.removeListener('liked_media', onCompleteMediaLike);
MediaPageStore.removeListener('unliked_media', onCompleteMediaLikeCancel);
MediaPageStore.removeListener('liked_media_failed_request', onFailMediaLikeRequest);
};
}, []);
return (
<div className="like">
<button onClick={toggleLike}>
<CircleIconButton type="span">
<MaterialIcon type="thumb_up" />
</CircleIconButton>
<span className="likes-counter">{likesCounter}</span>
</button>
</div>
);
}

View File

@@ -0,0 +1,350 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { formatInnerLink } from '../../utils/helpers/';
import { usePopup, useUser } from '../../utils/hooks/';
import { SiteContext } from '../../utils/contexts/';
import { PageActions, MediaPageActions } from '../../utils/actions/';
import { MediaPageStore } from '../../utils/stores/';
import { CircleIconButton, MaterialIcon, NavigationContentApp, NavigationMenuList, PopupMain } from '../_shared/';
import { ReportForm } from '../report-form/ReportForm';
function downloadOptions(mediaData, allowDownload) {
const site = SiteContext._currentValue;
const encodingsInfo = mediaData.encodings_info;
const options = {};
let k, g;
for (k in encodingsInfo) {
if (encodingsInfo.hasOwnProperty(k)) {
if (Object.keys(encodingsInfo[k]).length) {
for (g in encodingsInfo[k]) {
if (encodingsInfo[k].hasOwnProperty(g)) {
if ('success' === encodingsInfo[k][g].status && 100 === encodingsInfo[k][g].progress) {
options[encodingsInfo[k][g].title] = {
text: k + ' - ' + g.toUpperCase() + ' (' + encodingsInfo[k][g].size + ')',
link: formatInnerLink(encodingsInfo[k][g].url, site.url),
linkAttr: {
target: '_blank',
download: mediaData.title + '_' + k + '_' + g.toUpperCase(),
},
};
}
}
}
}
}
}
options.original_media_url = {
text: 'Original file (' + mediaData.size + ')',
link: formatInnerLink(mediaData.original_media_url, site.url),
linkAttr: {
target: '_blank',
download: mediaData.title,
},
};
return Object.values(options);
}
function optionsItems(userCan, mediaData, allowDownload, downloadLink, mediaReported) {
const items = [];
const mediaType = mediaData.media_type;
const mediaIsVideo = 'video' === mediaType;
const mediaReportedTimes = mediaData.reported_times;
if (allowDownload && userCan.downloadMedia) {
if (!mediaIsVideo) {
if (downloadLink) {
items.push({
itemType: 'link',
link: downloadLink,
text: 'Download',
icon: 'arrow_downward',
itemAttr: {
className: 'visible-only-in-small',
},
linkAttr: {
target: '_blank',
download: mediaData.title,
},
});
}
} else {
items.push({
itemType: 'open-subpage',
text: 'Download',
icon: 'arrow_downward',
itemAttr: {
className: 'visible-only-in-small',
},
buttonAttr: {
className: 'change-page',
'data-page-id': 'videoDownloadOptions',
},
});
}
}
if (mediaIsVideo && userCan.editMedia) {
items.push({
itemType: 'open-subpage',
text: 'Status info',
icon: 'info',
buttonAttr: {
className: 'change-page',
'data-page-id': 'mediaStatusInfo',
},
});
}
if (userCan.reportMedia) {
if (mediaReported) {
items.push({
itemType: 'div',
text: 'Reported',
icon: 'flag',
divAttr: {
className: 'reported-label loggedin-media-reported',
},
});
} else {
items.push({
itemType: 'open-subpage',
text: 'Report',
icon: 'flag',
buttonAttr: {
className: 'change-page' + (mediaReportedTimes ? ' loggedin-media-reported' : ''),
'data-page-id': 'loggedInReportMedia',
},
});
}
}
return items;
}
function getPopupPages(userCan, mediaData, allowDownload, downloadLink, mediaReported, submitReportForm, cancelReportForm) {
const mediaUrl = mediaData.url;
const mediaType = mediaData.media_type;
const mediaState = mediaData.state || 'N/A';
const mediaEncodingStatus = mediaData.encoding_status || 'N/A';
const mediaReportedTimes = mediaData.reported_times;
const mediaIsReviewed = mediaData.is_reviewed;
const mediaIsVideo = 'video' === mediaType;
const navItems = optionsItems(userCan, mediaData, allowDownload, downloadLink, mediaReported);
const pages = {};
if (navItems.length) {
pages.main = (
<div className="main-options">
<PopupMain>
<NavigationMenuList items={navItems} />
</PopupMain>
</div>
);
}
if (userCan.reportMedia) {
pages.loggedInReportMedia = mediaReported ? null : (
<div className="popup-fullscreen">
<PopupMain>
<span className="popup-fullscreen-overlay"></span>
<div>
<ReportForm mediaUrl={mediaUrl} submitReportForm={submitReportForm} cancelReportForm={cancelReportForm} />
</div>
</PopupMain>
</div>
);
}
if (userCan.editMedia) {
pages.mediaStatusInfo = (
<div className="main-options">
<PopupMain>
<ul className="media-status-info">
<li>
Media type: <span>{mediaType}</span>
</li>
<li>
State: <span>{mediaState}</span>
</li>
<li>
Review state: <span>{mediaIsReviewed ? 'Is reviewed' : 'Pending review'}</span>
</li>
{mediaIsVideo ? (
<li>
Encoding Status: <span>{mediaEncodingStatus}</span>
</li>
) : null}
{mediaReportedTimes ? (
<li className="reports">
Reports: <span>{mediaReportedTimes}</span>
</li>
) : null}
</ul>
</PopupMain>
</div>
);
}
if (allowDownload && userCan.downloadMedia && mediaIsVideo) {
pages.videoDownloadOptions = (
<div className="video-download-options">
<PopupMain>
<NavigationMenuList items={downloadOptions(mediaData, allowDownload)} />
</PopupMain>
</div>
);
}
return pages;
}
const defaultContainerClassname = 'more-options active-options';
export function MediaMoreOptionsIcon(props) {
const { userCan } = useUser();
const site = SiteContext._currentValue;
const downloadLink = formatInnerLink(MediaPageStore.get('media-original-url'), site.url);
const mediaData = MediaPageStore.get('media-data');
const mediaIsVideo = 'video' === mediaData.media_type;
const [popupContentRef, PopupContent, PopupTrigger] = usePopup();
const [visible, setVisible] = useState(false);
const [reported, setReported] = useState(false);
const [popupPages, setPopupPages] = useState({});
const [popupCurrentPage, setPopupCurrentPage] = useState('main');
const [containerClassname, setContainerClassname] = useState(defaultContainerClassname);
function submitReportForm(reportDescription) {
MediaPageActions.reportMedia(reportDescription);
}
function cancelReportFormSubmission() {
popupContentRef.current.toggle();
}
function onPopupPageChange(newPage) {
setPopupCurrentPage(newPage);
}
function onPopupHide() {
setPopupCurrentPage('main');
}
function onCompleteMediaReport() {
popupContentRef.current.tryToHide();
// FIXME: Without delay creates conflict [ Uncaught Error: Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch. ].
setTimeout(function () {
PageActions.addNotification('Media Reported', 'reportedMedia');
setReported(true);
MediaPageStore.removeListener('reported_media', onCompleteMediaReport);
}, 100);
}
useEffect(() => {
if (!reported) {
if (visible) {
MediaPageStore.on('reported_media', onCompleteMediaReport);
} else {
MediaPageStore.removeListener('reported_media', onCompleteMediaReport);
}
}
}, [visible]);
useEffect(() => {
setVisible(Object.keys(popupPages).length && props.allowDownload && userCan.downloadMedia);
}, [popupPages]);
useEffect(() => {
let classname = defaultContainerClassname;
if (props.allowDownload && userCan.downloadMedia && 'videoDownloadOptions' === popupCurrentPage) {
classname += ' video-downloads';
}
if (
1 === Object.keys(popupPages).length &&
props.allowDownload &&
userCan.downloadMedia &&
(mediaIsVideo || downloadLink)
) {
classname += ' visible-only-in-small';
}
setContainerClassname(classname);
}, [popupCurrentPage]);
useEffect(() => {
setPopupPages(
getPopupPages(
userCan,
mediaData,
props.allowDownload,
downloadLink,
reported,
submitReportForm,
cancelReportFormSubmission
)
);
}, [reported]);
useEffect(() => {
setPopupPages(
getPopupPages(
userCan,
mediaData,
props.allowDownload,
downloadLink,
reported,
submitReportForm,
cancelReportFormSubmission
)
);
return () => {
if (visible && !reported) {
MediaPageStore.removeListener('reported_media', onCompleteMediaReport);
}
};
}, []);
return !visible ? null : (
<div className={containerClassname}>
<PopupTrigger contentRef={popupContentRef}>
<span>
<CircleIconButton type="button">
<MaterialIcon type="more_horiz" />
</CircleIconButton>
</span>
</PopupTrigger>
<div className={'nav-page-' + popupCurrentPage}>
<PopupContent contentRef={popupContentRef} hideCallback={onPopupHide}>
<NavigationContentApp
pageChangeCallback={onPopupPageChange}
initPage={popupCurrentPage}
focusFirstItemOnPageChange={false}
pages={popupPages}
pageChangeSelector={'.change-page'}
pageIdSelectorAttr={'data-page-id'}
/>
</PopupContent>
</div>
</div>
);
}
MediaMoreOptionsIcon.propTypes = {
allowDownload: PropTypes.bool.isRequired,
};
MediaMoreOptionsIcon.defaultProps = {
allowDownload: false,
};

View File

@@ -0,0 +1,58 @@
import React, { useState } from 'react';
import { usePopup } from '../../utils/hooks/';
import { CircleIconButton, MaterialIcon, NavigationContentApp, PopupMain } from '../_shared/';
import { PlaylistsSelection } from '../playlists-selection/PlaylistsSelection';
function mediaSavePopupPages(onTriggerPopupClose) {
return {
selectPlaylist: (
<div className="popup-fullscreen">
<PopupMain>
<span className="popup-fullscreen-overlay"></span>
<PlaylistsSelection triggerPopupClose={onTriggerPopupClose} />
</PopupMain>
</div>
),
createPlaylist: (
<div className="popup-fullscreen">
<PopupMain>
<span className="popup-fullscreen-overlay"></span>
</PopupMain>
</div>
),
};
}
export function MediaSaveButton(props) {
const [popupContentRef, PopupContent, PopupTrigger] = usePopup();
const [popupCurrentPage, setPopupCurrentPage] = useState('selectPlaylist');
function triggerPopupClose() {
popupContentRef.current.toggle();
}
return (
<div className="save">
<PopupTrigger contentRef={popupContentRef}>
<button>
<CircleIconButton type="span">
<MaterialIcon type="playlist_add" />
</CircleIconButton>
<span>SAVE</span>
</button>
</PopupTrigger>
<PopupContent contentRef={popupContentRef}>
<NavigationContentApp
initPage={popupCurrentPage}
pageChangeSelector={'.change-page'}
pageIdSelectorAttr={'data-page-id'}
pages={mediaSavePopupPages(triggerPopupClose)}
focusFirstItemOnPageChange={false}
pageChangeCallback={setPopupCurrentPage}
/>
</PopupContent>
</div>
);
}

View File

@@ -0,0 +1,73 @@
import React, { useState } from 'react';
import { usePopup } from '../../utils/hooks/';
import { CircleIconButton, MaterialIcon, NavigationContentApp, PopupMain } from '../_shared/';
import { MediaShareEmbed } from './MediaShareEmbed';
import { MediaShareOptions } from './MediaShareOptions';
function mediaSharePopupPages() {
return {
shareOptions: (
<div className="popup-fullscreen">
<PopupMain>
<span className="popup-fullscreen-overlay"></span>
<MediaShareOptions />
</PopupMain>
</div>
),
};
}
function videoSharePopupPages(onTriggerPopupClose) {
return {
...mediaSharePopupPages(),
shareEmbed: (
<div className="popup-fullscreen share-embed-popup">
<PopupMain>
<span className="popup-fullscreen-overlay"></span>
<MediaShareEmbed triggerPopupClose={onTriggerPopupClose} />
</PopupMain>
</div>
),
};
}
export function MediaShareButton(props) {
const [popupContentRef, PopupContent, PopupTrigger] = usePopup();
const [popupCurrentPage, setPopupCurrentPage] = useState('shareOptions');
function triggerPopupClose() {
popupContentRef.current.toggle();
}
function onPopupPageChange(newPage) {
setPopupCurrentPage(newPage);
}
function onPopupHide() {
setPopupCurrentPage('shareOptions');
}
return (
<div className="share">
<PopupTrigger contentRef={popupContentRef}>
<button>
<CircleIconButton type="span">
<MaterialIcon type="share" />
</CircleIconButton>
<span>SHARE</span>
</button>
</PopupTrigger>
<PopupContent contentRef={popupContentRef} hideCallback={onPopupHide}>
<NavigationContentApp
initPage={popupCurrentPage}
pageChangeSelector={'.change-page'}
pageIdSelectorAttr={'data-page-id'}
pages={props.isVideo ? videoSharePopupPages(triggerPopupClose) : mediaSharePopupPages()}
focusFirstItemOnPageChange={false}
pageChangeCallback={onPopupPageChange}
/>
</PopupContent>
</div>
);
}

View File

@@ -0,0 +1,249 @@
import React, { useRef, useState, useEffect, useContext } from 'react';
import PropTypes from 'prop-types';
import { LinksContext } from '../../utils/contexts/';
import { PageStore, MediaPageStore } from '../../utils/stores/';
import { PageActions, MediaPageActions } from '../../utils/actions/';
import { CircleIconButton, MaterialIcon, NumericInputWithUnit } from '../_shared/';
import VideoViewer from '../media-viewer/VideoViewer';
export function MediaShareEmbed(props) {
const embedVideoDimensions = PageStore.get('config-options').embedded.video.dimensions;
const links = useContext(LinksContext);
const aspectRatioValueRef = useRef(null);
const onRightRef = useRef(null);
const onRightTopRef = useRef(null);
const onRightMiddleRef = useRef(null);
const onRightBottomRef = useRef(null);
const [maxHeight, setMaxHeight] = useState(window.innerHeight - 144 + 56);
const [keepAspectRatio, setKeepAspectRatio] = useState(false);
const [aspectRatio, setAspectRatio] = useState('16:9');
const [embedWidthValue, setEmbedWidthValue] = useState(embedVideoDimensions.width);
const [embedWidthUnit, setEmbedWidthUnit] = useState(embedVideoDimensions.widthUnit);
const [embedHeightValue, setEmbedHeightValue] = useState(embedVideoDimensions.height);
const [embedHeightUnit, setEmbedHeightUnit] = useState(embedVideoDimensions.heightUnit);
const [rightMiddlePositionTop, setRightMiddlePositionTop] = useState(60);
const [rightMiddlePositionBottom, setRightMiddlePositionBottom] = useState(60);
const [unitOptions, setUnitOptions] = useState([
{ key: 'px', label: 'px' },
{ key: 'percent', label: '%' },
]);
function onClickCopyMediaLink() {
MediaPageActions.copyEmbedMediaCode(onRightMiddleRef.current.querySelector('textarea'));
}
function onClickEmbedShareExit() {
if (void 0 !== props.triggerPopupClose) {
props.triggerPopupClose();
}
}
function onEmbedWidthValueChange(newVal) {
newVal = '' === newVal ? 0 : newVal;
const arr = aspectRatio.split(':');
const x = arr[0];
const y = arr[1];
setEmbedWidthValue(newVal);
setEmbedHeightValue(keepAspectRatio ? parseInt((newVal * y) / x, 10) : embedHeightValue);
}
function onEmbedWidthUnitChange(newVal) {
setEmbedWidthUnit(newVal);
}
function onEmbedHeightValueChange(newVal) {
newVal = '' === newVal ? 0 : newVal;
const arr = aspectRatio.split(':');
const x = arr[0];
const y = arr[1];
setEmbedHeightValue(newVal);
setEmbedWidthValue(keepAspectRatio ? parseInt((newVal * x) / y, 10) : embedWidthValue);
}
function onEmbedHeightUnitChange(newVal) {
setEmbedHeightUnit(newVal);
}
function onKeepAspectRatioChange() {
const newVal = !keepAspectRatio;
const arr = aspectRatio.split(':');
const x = arr[0];
const y = arr[1];
setKeepAspectRatio(newVal);
setEmbedWidthUnit(newVal ? 'px' : embedWidthUnit);
setEmbedHeightUnit(newVal ? 'px' : embedHeightUnit);
setEmbedHeightValue(newVal ? parseInt((embedWidthValue * y) / x, 10) : embedHeightValue);
setUnitOptions(
newVal
? [{ key: 'px', label: 'px' }]
: [
{ key: 'px', label: 'px' },
{ key: 'percent', label: '%' },
]
);
}
function onAspectRatioChange() {
const newVal = aspectRatioValueRef.current.value;
const arr = newVal.split(':');
const x = arr[0];
const y = arr[1];
setAspectRatio(newVal);
setEmbedHeightValue(keepAspectRatio ? parseInt((embedWidthValue * y) / x, 10) : embedHeightValue);
}
function onWindowResize() {
setMaxHeight(window.innerHeight - 144 + 56);
setRightMiddlePositionTop(onRightTopRef.current.offsetHeight);
setRightMiddlePositionBottom(onRightBottomRef.current.offsetHeight);
}
function onCompleteCopyMediaLink() {
setTimeout(function () {
PageActions.addNotification('Embed media code copied to clipboard', 'clipboardEmbedMediaCodeCopy');
}, 100);
}
useEffect(() => {
setMaxHeight(window.innerHeight - 144 + 56);
setRightMiddlePositionTop(onRightTopRef.current.offsetHeight);
setRightMiddlePositionBottom(onRightBottomRef.current.offsetHeight);
});
useEffect(() => {
PageStore.on('window_resize', onWindowResize);
MediaPageStore.on('copied_embed_media_code', onCompleteCopyMediaLink);
return () => {
PageStore.removeListener('window_resize', onWindowResize);
MediaPageStore.removeListener('copied_embed_media_code', onCompleteCopyMediaLink);
};
}, []);
return (
<div className="share-embed" style={{ maxHeight: maxHeight + 'px' }}>
<div className="share-embed-inner">
<div className="on-left">
<div className="media-embed-wrap">
<VideoViewer data={MediaPageStore.get('media-data')} inEmbed={true} />
</div>
</div>
<div ref={onRightRef} className="on-right">
<div ref={onRightTopRef} className="on-right-top">
<div className="on-right-top-inner">
<span className="ttl">Embed Video</span>
<CircleIconButton type="button" onClick={onClickEmbedShareExit}>
<MaterialIcon type="close" />
</CircleIconButton>
</div>
</div>
<div
ref={onRightMiddleRef}
className="on-right-middle"
style={{ top: rightMiddlePositionTop + 'px', bottom: rightMiddlePositionBottom + 'px' }}
>
<textarea
readOnly
value={
'<iframe width="' +
('percent' === embedWidthUnit ? embedWidthValue + '%' : embedWidthValue) +
'" height="' +
('percent' === embedHeightUnit ? embedHeightValue + '%' : embedHeightValue) +
'" src="' +
links.embed +
MediaPageStore.get('media-id') +
'" frameborder="0" allowfullscreen></iframe>'
}
></textarea>
<div className="iframe-config">
<div className="iframe-config-options-title">Embed options</div>
<div className="iframe-config-option">
{/*<div className="option-title">
<span>Dimensions</span>
</div>*/}
<div className="option-content">
<div className="ratio-options">
<div className="options-group">
<label style={{ minHeight: '36px' }}>
<input type="checkbox" checked={keepAspectRatio} onChange={onKeepAspectRatioChange} />
Keep aspect ratio
</label>
</div>
{!keepAspectRatio ? null : (
<div className="options-group">
<select ref={aspectRatioValueRef} onChange={onAspectRatioChange} value={aspectRatio}>
<optgroup label="Horizontal orientation">
<option value="16:9">16:9</option>
<option value="4:3">4:3</option>
<option value="3:2">3:2</option>
</optgroup>
<optgroup label="Vertical orientation">
<option value="9:16">9:16</option>
<option value="3:4">3:4</option>
<option value="2:3">2:3</option>
</optgroup>
</select>
</div>
)}
</div>
<br />
<div className="options-group">
<NumericInputWithUnit
valueCallback={onEmbedWidthValueChange}
unitCallback={onEmbedWidthUnitChange}
label={'Width'}
defaultValue={parseInt(embedWidthValue, 10)}
defaultUnit={embedWidthUnit}
minValue={1}
maxValue={99999}
units={unitOptions}
/>
</div>
<div className="options-group">
<NumericInputWithUnit
valueCallback={onEmbedHeightValueChange}
unitCallback={onEmbedHeightUnitChange}
label={'Height'}
defaultValue={parseInt(embedHeightValue, 10)}
defaultUnit={embedHeightUnit}
minValue={1}
maxValue={99999}
units={unitOptions}
/>
</div>
</div>
</div>
</div>
</div>
<div ref={onRightBottomRef} className="on-right-bottom">
<button onClick={onClickCopyMediaLink}>COPY</button>
</div>
</div>
</div>
</div>
);
}
MediaShareEmbed.propTypes = {
triggerPopupClose: PropTypes.func,
};

View File

@@ -0,0 +1,282 @@
import React, { useRef, useState, useEffect } from 'react';
import { ShareOptionsContext } from '../../utils/contexts/';
import { PageStore, MediaPageStore } from '../../utils/stores/';
import { PageActions, MediaPageActions } from '../../utils/actions/';
import ItemsInlineSlider from '../item-list/includes/itemLists/ItemsInlineSlider';
import { CircleIconButton } from '../_shared/';
function shareOptionsList() {
const socialMedia = ShareOptionsContext._currentValue;
const mediaUrl = MediaPageStore.get('media-url');
const mediaTitle = MediaPageStore.get('media-data').title;
const ret = {};
let i = 0;
while (i < socialMedia.length) {
switch (socialMedia[i]) {
case 'embed':
if ('video' === MediaPageStore.get('media-data').media_type) {
ret[socialMedia[i]] = {};
}
break;
case 'email':
ret[socialMedia[i]] = {
title: 'Email',
shareUrl: 'mailto:?body=' + mediaUrl,
};
break;
case 'fb':
ret[socialMedia[i]] = {
title: 'Facebook',
shareUrl: 'https://www.facebook.com/sharer.php?u=' + mediaUrl,
};
break;
case 'tw':
ret[socialMedia[i]] = {
title: 'Twitter',
shareUrl: 'https://twitter.com/intent/tweet?url=' + mediaUrl,
};
break;
case 'reddit':
ret[socialMedia[i]] = {
title: 'reddit',
shareUrl: 'https://reddit.com/submit?url=' + mediaUrl + '&title=' + mediaTitle,
};
break;
case 'tumblr':
ret[socialMedia[i]] = {
title: 'Tumblr',
shareUrl: 'https://www.tumblr.com/widgets/share/tool?canonicalUrl=' + mediaUrl + '&title=' + mediaTitle,
};
break;
case 'pinterest':
ret[socialMedia[i]] = {
title: 'Pinterest',
shareUrl: 'http://pinterest.com/pin/create/link/?url=' + mediaUrl,
};
break;
case 'vk':
ret[socialMedia[i]] = {
title: 'ВКонтакте',
shareUrl: 'http://vk.com/share.php?url=' + mediaUrl + '&title=' + mediaTitle,
};
break;
case 'linkedin':
ret[socialMedia[i]] = {
title: 'LinkedIn',
shareUrl: 'https://www.linkedin.com/shareArticle?mini=true&url=' + mediaUrl,
};
break;
case 'mix':
ret[socialMedia[i]] = {
title: 'Mix',
shareUrl: 'https://mix.com/add?url=' + mediaUrl,
};
break;
case 'whatsapp':
ret[socialMedia[i]] = {
title: 'WhatsApp',
shareUrl: 'whatsapp://send?text=' + mediaUrl,
};
break;
case 'telegram':
ret[socialMedia[i]] = {
title: 'Telegram',
shareUrl: 'https://t.me/share/url?url=' + mediaUrl + '&text=' + mediaTitle,
};
break;
}
i += 1;
}
return ret;
}
function ShareOptions() {
const shareOptions = shareOptionsList();
const compList = [];
for (let k in shareOptions) {
if (shareOptions.hasOwnProperty(k)) {
if (k === 'embed') {
compList.push(
<div key={'share-' + k} className={'sh-option share-' + k + '-opt'}>
<button className="sh-option change-page" data-page-id="shareEmbed">
<span>
<i className="material-icons">code</i>
</span>
<span>Embed</span>
</button>
</div>
);
} else if (k === 'whatsapp') {
compList.push(
<div key={'share-' + k} className={'sh-option share-' + k}>
<a
href={shareOptions[k].shareUrl}
title=""
target="_blank"
data-action="share/whatsapp/share"
rel="noreferrer"
>
<span></span>
<span>{shareOptions[k].title}</span>
</a>
</div>
);
} else if (k === 'email') {
compList.push(
<div key="share-email" className="sh-option share-email">
<a href={shareOptions[k].shareUrl} title="">
<span>
<i className="material-icons">email</i>
</span>
<span>{shareOptions[k].title}</span>
</a>
</div>
);
} else {
compList.push(
<div key={'share-' + k} className={'sh-option share-' + k}>
<a href={shareOptions[k].shareUrl} title="" target="_blank" rel="noreferrer">
<span></span>
<span>{shareOptions[k].title}</span>
</a>
</div>
);
}
}
}
return compList;
}
function NextSlideButton({ onClick }) {
return (
<span className="next-slide">
<CircleIconButton buttonShadow={true} onClick={onClick}>
<i className="material-icons">keyboard_arrow_right</i>
</CircleIconButton>
</span>
);
}
function PreviousSlideButton({ onClick }) {
return (
<span className="previous-slide">
<CircleIconButton buttonShadow={true} onClick={onClick}>
<i className="material-icons">keyboard_arrow_left</i>
</CircleIconButton>
</span>
);
}
function updateDimensions() {
return {
maxFormContentHeight: window.innerHeight - (56 + 4 * 24 + 44),
maxPopupWidth: 518 > window.innerWidth - 2 * 40 ? window.innerWidth - 2 * 40 : null,
};
}
export function MediaShareOptions(props) {
const containerRef = useRef(null);
const shareOptionsInnerRef = useRef(null);
const [inlineSlider, setInlineSlider] = useState(null);
const [sliderButtonsVisible, setSliderButtonsVisible] = useState({ prev: false, next: false });
const [dimensions, setDimensions] = useState(updateDimensions());
const [shareOptions] = useState(ShareOptions());
function onWindowResize() {
setDimensions(updateDimensions());
}
function onClickCopyMediaLink() {
MediaPageActions.copyShareLink(containerRef.current.querySelector('.copy-field input'));
}
function onCompleteCopyMediaLink() {
// FIXME: Without delay throws conflict error [ Uncaught Error: Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch. ].
setTimeout(function () {
PageActions.addNotification('Link copied to clipboard', 'clipboardLinkCopy');
}, 100);
}
function updateSlider() {
inlineSlider.scrollToCurrentSlide();
updateSliderButtonsView();
}
function updateSliderButtonsView() {
setSliderButtonsVisible({
prev: inlineSlider.hasPreviousSlide(),
next: inlineSlider.hasNextSlide(),
});
}
function nextSlide() {
inlineSlider.nextSlide();
updateSlider();
}
function prevSlide() {
inlineSlider.previousSlide();
updateSlider();
}
useEffect(() => {
setInlineSlider(new ItemsInlineSlider(shareOptionsInnerRef.current, '.sh-option'));
}, [shareOptions]);
useEffect(() => {
if (inlineSlider) {
inlineSlider.updateDataStateOnResize(shareOptions.length, true, true);
updateSlider();
}
}, [dimensions, inlineSlider]);
useEffect(() => {
PageStore.on('window_resize', onWindowResize);
MediaPageStore.on('copied_media_link', onCompleteCopyMediaLink);
return () => {
PageStore.removeListener('window_resize', onWindowResize);
MediaPageStore.removeListener('copied_media_link', onCompleteCopyMediaLink);
setInlineSlider(null);
};
}, []);
return (
<div
ref={containerRef}
style={null !== dimensions.maxPopupWidth ? { maxWidth: dimensions.maxPopupWidth + 'px' } : null}
>
<div
className="scrollable-content"
style={null !== dimensions.maxFormContentHeight ? { maxHeight: dimensions.maxFormContentHeight + 'px' } : null}
>
<div className="share-popup-title">Share media</div>
{shareOptions.length ? (
<div className="share-options">
{sliderButtonsVisible.prev ? <PreviousSlideButton onClick={prevSlide} /> : null}
<div ref={shareOptionsInnerRef} className="share-options-inner">
{shareOptions}
</div>
{sliderButtonsVisible.next ? <NextSlideButton onClick={nextSlide} /> : null}
</div>
) : null}
</div>
<div className="copy-field">
<div>
<input type="text" readOnly value={MediaPageStore.get('media-url')} />
<button onClick={onClickCopyMediaLink}>COPY</button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
import { CircleIconButton, MaterialIcon } from '../_shared/';
export function OtherMediaDownloadLink(props) {
return (
<div className="download hidden-only-in-small">
<a href={props.link} target="_blank" download={props.title} title="Download" rel="noreferrer">
<CircleIconButton type="span">
<MaterialIcon type="arrow_downward" />
</CircleIconButton>
<span>DOWNLOAD</span>
</a>
</div>
);
}
OtherMediaDownloadLink.propTypes = {
link: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
};

View File

@@ -0,0 +1,92 @@
import React, { useState } from 'react';
import { usePopup } from '../../utils/hooks/';
import { SiteContext } from '../../utils/contexts/';
import { MediaPageStore } from '../../utils/stores/';
import { formatInnerLink } from '../../utils/helpers/';
import { CircleIconButton, MaterialIcon, NavigationContentApp, NavigationMenuList, PopupMain } from '../_shared/';
function downloadOptionsList() {
const media_data = MediaPageStore.get('media-data');
const title = media_data.title;
const encodings_info = media_data.encodings_info;
const optionsList = {};
let k, g;
for (k in encodings_info) {
if (encodings_info.hasOwnProperty(k)) {
if (Object.keys(encodings_info[k]).length) {
for (g in encodings_info[k]) {
if (encodings_info[k].hasOwnProperty(g)) {
if ('success' === encodings_info[k][g].status && 100 === encodings_info[k][g].progress) {
optionsList[encodings_info[k][g].title] = {
text: k + ' - ' + g.toUpperCase() + ' (' + encodings_info[k][g].size + ')',
link: formatInnerLink(encodings_info[k][g].url, SiteContext._currentValue.url),
linkAttr: {
target: '_blank',
download: media_data.title + '_' + k + '_' + g.toUpperCase(),
},
};
}
}
}
}
}
}
optionsList.original_media_url = {
text: 'Original file (' + media_data.size + ')',
link: formatInnerLink(media_data.original_media_url, SiteContext._currentValue.url),
linkAttr: {
target: '_blank',
download: media_data.title,
},
};
return Object.values(optionsList);
}
function downloadOptionsPages() {
return {
main: (
<div className="main-options">
<PopupMain>
<NavigationMenuList items={downloadOptionsList()} />
</PopupMain>
</div>
),
};
}
export function VideoMediaDownloadLink(props) {
const [popupContentRef, PopupContent, PopupTrigger] = usePopup();
const [downloadOptionsCurrentPage, setDownloadOptionsCurrentPage] = useState('main');
return (
<div className="video-downloads hidden-only-in-small">
<PopupTrigger contentRef={popupContentRef}>
<button>
<CircleIconButton type="span">
<MaterialIcon type="arrow_downward" />
</CircleIconButton>
<span>DOWNLOAD</span>
</button>
</PopupTrigger>
<div className={'nav-page-' + downloadOptionsCurrentPage}>
<PopupContent contentRef={popupContentRef}>
<NavigationContentApp
pageChangeCallback={null}
initPage="main"
focusFirstItemOnPageChange={false}
pages={downloadOptionsPages()}
pageChangeSelector={'.change-page'}
pageIdSelectorAttr={'data-page-id'}
/>
</PopupContent>
</div>
</div>
);
}

View File

@@ -0,0 +1,9 @@
export * from './MediaDislikeIcon.jsx';
export * from './MediaLikeIcon.jsx';
export * from './MediaMoreOptionsIcon.jsx';
export * from './MediaSaveButton.jsx';
export * from './MediaShareButton.jsx';
export * from './MediaShareEmbed.jsx';
export * from './MediaShareOptions.jsx';
export * from './OtherMediaDownloadLink.jsx';
export * from './VideoMediaDownloadLink.jsx';