installed plugin Infinite Uploads version 2.0.8

This commit is contained in:
2025-05-02 12:03:21 +00:00
committed by Gitium
parent 7ca941b591
commit 8fefb19ab4
1179 changed files with 99739 additions and 0 deletions

View File

@ -0,0 +1,31 @@
import {__, _x, _n, _nx} from '@wordpress/i18n';
import {useEffect, useState} from '@wordpress/element';
import Button from "react-bootstrap/Button";
import Row from "react-bootstrap/Row";
import Col from "react-bootstrap/Col";
import {ChromePicker} from "react-color";
export default function ColorPick({settings, setSettings}) {
const [showPicker, setShowPicker] = useState(false);
return (
<Row className="justify-content-start">
<Col xs={2}>
<Button style={{backgroundColor: settings.PlayerKeyColor}} variant="secondary" className="rounded-pill px-4" onClick={() => {
setShowPicker(!showPicker);
}}>{settings.PlayerKeyColor}</Button>
</Col>
{showPicker && (
<Col>
<ChromePicker
color={settings.PlayerKeyColor}
onChangeComplete={(color) => {
setSettings({...settings, PlayerKeyColor: color.hex});
}}
disableAlpha
/>
</Col>
)}
</Row>
);
}

View File

@ -0,0 +1,78 @@
import {__, _x, _n, _nx} from '@wordpress/i18n';
import {useState, useEffect} from '@wordpress/element';
import Modal from 'react-bootstrap/Modal';
import Button from 'react-bootstrap/Button';
export default function DeleteModal({video, setVideos}) {
const [show, setShow] = useState(false);
const handleClose = () => setShow(false);
const handleShow = () => setShow(true);
function deleteVideo() {
const formData = new FormData();
formData.append('video_id', video.guid);
formData.append('nonce', IUP_VIDEO.nonce);
const options = {
method: 'POST',
headers: {
Accept: 'application/json',
},
body: formData,
};
fetch(`${ajaxurl}?action=infinite-uploads-video-delete`, options)
.then((response) => response.json())
.then((data) => {
console.log(data);
if (data.success) {
setVideos((videos) =>
videos.filter((v) => v.guid !== video.guid)
);
handleClose();
} else {
console.error(data.data);
}
})
.catch((error) => {
console.log('Error:', error);
});
}
return (
<>
<Button
variant="outline-danger"
size="sm"
onClick={handleShow}
className="rounded-4"
>
{__('Delete Video', 'infinite-uploads')}
</Button>
<Modal show={show} onHide={handleClose}>
<Modal.Header closeButton>
<Modal.Title>
{__('Delete Video:', 'infinite-uploads')}{' '}
{video.title}
</Modal.Title>
</Modal.Header>
<Modal.Body>
{__(
'Are you sure you would like to delete this video?',
'infinite-uploads'
)}
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={handleClose}>
{__('Cancel', 'infinite-uploads')}
</Button>
<Button variant="danger" onClick={deleteVideo}>
{__('Delete', 'infinite-uploads')}
</Button>
</Modal.Footer>
</Modal>
</>
);
}

View File

@ -0,0 +1,97 @@
import Button from 'react-bootstrap/Button';
import {__, _x, _n, _nx} from '@wordpress/i18n';
import {useState} from '@wordpress/element';
import Form from 'react-bootstrap/Form';
import InputGroup from 'react-bootstrap/InputGroup';
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';
import {VideoSize} from "./VideoAttributes";
import UploadModal from "./UploadModal";
function Header({orderBy, setOrderBy, search, setSearch, selectVideo, getVideos}) {
const sizeOf = function (bytes) {
if (bytes === 0) {
return '0 B';
}
var e = Math.floor(Math.log(bytes) / Math.log(1024));
return (
(bytes / Math.pow(1024, e)).toFixed(1) +
' ' +
' KMGTP'.charAt(e) +
'B'
);
};
return (
<Row className="align-items-center">
<Col sm={8} md={3} className="mb-3 mb-lg-0">
<InputGroup>
<InputGroup.Text>
<span className="dashicons dashicons-search"></span>
</InputGroup.Text>
<Form.Control
placeholder={__('Search', 'infinite-uploads')}
aria-label={__('Search', 'infinite-uploads')}
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</InputGroup>
</Col>
<Col sm={4} md={2} className="mb-3 mb-lg-0">
<InputGroup>
<InputGroup.Text>
{__('Sort', 'infinite-uploads')}
</InputGroup.Text>
<Form.Select
aria-label={__(
'Sort by select',
'infinite-uploads'
)}
value={orderBy}
onChange={(e) => setOrderBy(e.target.value)}
>
>
<option value="title">
{__('Title', 'infinite-uploads')}
</option>
<option value="date">
{__('Date', 'infinite-uploads')}
</option>
</Form.Select>
</InputGroup>
</Col>
<Col className="mb-3 mb-lg-0">
<Row className="justify-content-center flex-nowrap">
<Col className="col-auto">
<p className="mb-0">{__("Video Count", 'infinite-uploads')}</p>
<span className="h4 text-nowrap">{IUP_VIDEO.settings.VideoCount}</span>
</Col>
<Col className="col-auto">
<p className="mb-0">{__("Library Storage", 'infinite-uploads')}</p>
<span className="h4 text-nowrap">{sizeOf(IUP_VIDEO.settings.StorageUsage)}</span>
</Col>
<Col className="col-auto">
<p className="mb-0">{__("Video Bandwidth", 'infinite-uploads')}</p>
<span className="h4 text-nowrap">{sizeOf(IUP_VIDEO.settings.TrafficUsage)}</span>
</Col>
</Row>
</Col>
<Col className="d-flex justify-content-end mb-3 mb-lg-0">
<Button
variant="outline-secondary"
className="rounded-pill text-nowrap"
href={IUP_VIDEO.settingsUrl}
>
<span className="dashicons dashicons-admin-generic"></span>
{__('Settings', 'infinite-uploads')}
</Button>
{!selectVideo && (
<UploadModal getVideos={getVideos}/>
)}
</Col>
</Row>
);
}
export default Header;

View File

@ -0,0 +1,158 @@
import {useEffect, useState} from '@wordpress/element';
import {__, _x, _n, _nx} from '@wordpress/i18n';
import VideoCard from './VideoCard';
import Container from 'react-bootstrap/Container';
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';
import Header from './Header';
import Paginator from './Paginator';
import Spinner from 'react-bootstrap/Spinner';
export default function Library({selectVideo}) {
const [videos, setVideos] = useState([]);
const [loading, setLoading] = useState(true);
const [orderBy, setOrderBy] = useState('date');
const [search, setSearch] = useState('');
const [page, setPage] = useState(1);
const [totalItems, setTotalItems] = useState(0);
const [itemsPerPage, setItemsPerPage] = useState(40);
const [refreshInterval, setRefreshInterval] = useState(60000);
//get videos on render
useEffect(() => {
if (!loading) {
setLoading(true);
getVideos();
}
}, [orderBy, page]);
useEffect(() => {
if (search.length > 2 || search.length === 0) {
setPage(1);
setLoading(true);
getVideos();
}
}, [search]);
useEffect(() => {
//check the videos array if any of the video objects are currently processing or transcoding
const processing = videos.find((video) => (video.status === 2 || video.status === 3));
if (processing) {
setRefreshInterval(10000);
} else {
setRefreshInterval(60000);
}
}, [videos]);
//fetch videos on a 30s interval
useEffect(() => {
const interval = setInterval(() => {
getVideos();
}, refreshInterval);
return () => clearInterval(interval);
}, [orderBy, page, search, refreshInterval]);
function getVideos() {
const options = {
method: 'GET',
headers: {
Accept: 'application/json',
AccessKey: IUP_VIDEO.apiKey,
},
};
fetch(
`https://video.bunnycdn.com/library/${IUP_VIDEO.libraryId}/videos?page=${page}&itemsPerPage=${itemsPerPage}&orderBy=${orderBy}&search=${search}`,
options
)
.then((response) => response.json())
.then((data) => {
console.log('Videos:', data);
setVideos(data.items);
setTotalItems(data.totalItems);
setItemsPerPage(data.itemsPerPage);
setLoading(false);
})
.catch((error) => {
console.error(error);
setLoading(false);
});
}
return (
<>
{!selectVideo && (
<h1 className="text-muted mb-3">
<img
src={IUP_VIDEO.assetBase + '/img/iu-logo-gray.svg'}
alt="Infinite Uploads Logo"
height="32"
width="32"
className="me-2"
/>
{__('Cloud Video Library', 'infinite-uploads')}
</h1>
)}
<Container fluid>
<Header
{...{
orderBy,
setOrderBy,
search,
setSearch,
selectVideo,
getVideos
}}
/>
{!loading ? (
<Container fluid>
<Row
xs={1}
sm={1}
md={2}
lg={3}
xl={4}
xxl={5}
>
{videos.length > 0 ? (
videos.map((video, index) => {
return (
<Col key={index + video.guid}>
<VideoCard
{...{
video,
setVideos,
selectVideo,
}}
/>
</Col>
);
})
) : (
<Container className="my-5 justify-content-center align-items-center">
<p className="text-muted text-center h5">
{__('No videos found.', 'infinite-uploads')}
</p>
</Container>
)}
</Row>
<Paginator
{...{page, setPage, totalItems, itemsPerPage}}
/>
</Container>
) : (
<Container className="d-flex justify-content-center align-middle my-5">
<Spinner
animation="grow"
role="status"
className="my-5"
>
<span className="visually-hidden">Loading...</span>
</Spinner>
</Container>
)}
</Container>
</>
);
}

View File

@ -0,0 +1,39 @@
import Pagination from 'react-bootstrap/Pagination';
import Container from 'react-bootstrap/Container';
function Paginator({page, setPage, totalItems, itemsPerPage}) {
if (totalItems <= itemsPerPage) {
return null;
}
let active = page;
const lastPage = Math.ceil(totalItems / itemsPerPage);
let items = [];
for (let number = 1; number <= lastPage; number++) {
items.push(
<Pagination.Item
key={number}
active={number === active}
onClick={() => setPage(number)}
>
{number}
</Pagination.Item>
);
}
return (
<Pagination className="justify-content-center mt-4">
<Pagination.First
onClick={() => setPage(1)}
disabled={page === 1}
/>
{items}
<Pagination.Last
onClick={() => setPage(lastPage)}
disabled={page === lastPage}
/>
</Pagination>
);
}
export default Paginator;

View File

@ -0,0 +1,27 @@
import {__, _x, _n, _nx} from '@wordpress/i18n';
import Form from 'react-bootstrap/Form';
import Col from 'react-bootstrap/Col';
export default function PlayerCheckbox({control, icon, label, settings, setSettings}) {
return (
<Col>
<Form.Check
type="checkbox"
id={control}
inline
label={
<span className="text-nowrap"><span className={"dashicons dashicons-" + icon}></span> {label}</span>
}
checked={settings.Controls.includes(control)}
onChange={e => setSettings(settings => {
if (e.target.checked) {
return {...settings, Controls: [...settings.Controls, control]};
} else {
return {...settings, Controls: settings.Controls.filter(ctrl => ctrl !== control)};
}
})}
/>
</Col>
);
}

View File

@ -0,0 +1,44 @@
import {__, _x, _n, _nx} from '@wordpress/i18n';
import Form from 'react-bootstrap/Form';
import Row from "react-bootstrap/Row";
import Col from "react-bootstrap/Col";
import {Badge} from "react-bootstrap";
export default function ResCheckbox({px, bitrate, settings, setSettings}) {
const resolution = px + "p";
//calculate width if 16:9 based on height
const width = Math.round(px * 16 / 9);
const label = "(" + width + "x" + px + ")";
const bitrateLabel = bitrate ? bitrate + " kbps" : "";
const hdLabel = px >= 2160 ? "4K UHD" : px >= 1440 ? "2K QHD" : px >= 1080 ? "Full HD" : px >= 720 ? "HD" : "";
const disabled = px >= 1440;
return (
<Badge bg="light" text="dark" className="mb-2 rounded-pill px-3 w-100">
<Row className="d-flex justify-content-between align-items-center">
<Col className="">
<Form.Check
className="d-flex align-items-center"
type="checkbox"
id={resolution}
label={
<span className="text-nowrap"><strong>{resolution}</strong> {label} {hdLabel}</span>
}
checked={settings.EnabledResolutions.includes(resolution)}
onChange={e => setSettings(settings => {
if (e.target.checked) {
return {...settings, EnabledResolutions: [...settings.EnabledResolutions, resolution]};
} else {
return {...settings, EnabledResolutions: settings.EnabledResolutions.filter(res => res !== resolution)};
}
})}
disabled={disabled}
/>
</Col>
<Col className="col-auto">
{bitrateLabel}
</Col>
</Row>
</Badge>
);
}

View File

@ -0,0 +1,188 @@
import {useEffect, useState} from '@wordpress/element';
import {__, _x, _n, _nx} from '@wordpress/i18n';
import Container from 'react-bootstrap/Container';
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';
import Card from "react-bootstrap/Card";
import Spinner from 'react-bootstrap/Spinner';
import Form from 'react-bootstrap/Form';
import PlayerCheckbox from "./PlayerCheckbox";
import ResCheckbox from "./ResCheckbox";
import ColorPick from "./ColorPick";
import Tabs from "react-bootstrap/Tabs";
import Tab from "react-bootstrap/Tab";
import Button from "react-bootstrap/Button";
export default function Settings() {
const [loading, setLoading] = useState(false);
const [settings, setSettings] = useState(IUP_VIDEO.settings);
function updateSettings() {
setLoading(true);
const formData = new FormData();
formData.append('settings', JSON.stringify(settings));
formData.append('nonce', IUP_VIDEO.nonce);
const options = {
method: 'POST',
headers: {
Accept: 'application/json',
},
body: formData,
};
fetch(`${ajaxurl}?action=infinite-uploads-video-settings`, options)
.then((response) => response.json())
.then((data) => {
if (data.success) {
setSettings(data.data);
} else {
console.error(data.data);
}
setLoading(false);
})
.catch((error) => {
console.log('Error:', error);
setLoading(false);
});
}
if (!settings) {
return (
<h2>{__('Video library not yet connected.', 'infinite-uploads')}</h2>
)
}
return (
<Container fluid>
<Row className="justify-content-between align-items-center">
<Col>
<h1 className="text-muted mb-3">
<img
src={IUP_VIDEO.assetBase + '/img/iu-logo-gray.svg'}
alt="Infinite Uploads Logo"
height="32"
width="32"
className="me-2"
/>
{__('Infinite Uploads Video Settings', 'infinite-uploads')}
</h1>
</Col>
<Col>
<Button variant="primary" className="float-end" href={IUP_VIDEO.libraryUrl}>
{__('Video Library', 'infinite-uploads')}
</Button>
</Col>
</Row>
<Card>
<Card.Body>
<Tabs
defaultActiveKey="player"
id="video-settings-tabs"
className="mb-3"
>
<Tab eventKey="player" title={__('Player', 'infinite-uploads')} className="mt-4">
<Row className="justify-content-center mb-5" xs={1} md={2}>
<Col>
<h5>{__('Main Player Color', 'infinite-uploads')}</h5>
<p className="lead">{__('Select the primary color that will be displayed for the controls in the video player.', 'infinite-uploads')}</p>
</Col>
<Col>
<ColorPick {...{settings, setSettings}} />
</Col>
</Row>
<Row className="justify-content-center mb-5" xs={1} md={2}>
<Col>
<h5>{__('Player Language', 'infinite-uploads')}</h5>
<p className="lead">{__('Select the default language that will be displayed in the video player.', 'infinite-uploads')}</p>
</Col>
<Col>
<Form.Select size="lg" value={settings.UILanguage} onChange={(e) => setSettings({...settings, UILanguage: e.target.value})}>
<option value="en" label="English"></option>
<option value="ar" label="Arabic"></option>
<option value="bu" label="Bulgarian"></option>
<option value="cn" label="Chinese"></option>
<option value="cz" label="Czech"></option>
<option value="dk" label="Danish"></option>
<option value="nl" label="Dutch"></option>
<option value="fi" label="Finnish"></option>
<option value="fr" label="French"></option>
<option value="de" label="German"></option>
<option value="gr" label="Greek"></option>
<option value="hu" label="Hungarian"></option>
<option value="id" label="Indonesian"></option>
<option value="it" label="Italian"></option>
<option value="jp" label="Japanese"></option>
<option value="kr" label="Korean"></option>
<option value="no" label="Norwegian"></option>
<option value="pl" label="Polish"></option>
<option value="pt" label="Portuguese"></option>
<option value="ro" label="Romanian"></option>
<option value="rs" label="Serbian"></option>
<option value="sk" label="Slovakian"></option>
<option value="si" label="Slovenian"></option>
<option value="es" label="Spanish"></option>
<option value="se" label="Swedish"></option>
<option value="ru" label="Russian"></option>
<option value="th" label="Thai"></option>
<option value="tr" label="Turkish"></option>
<option value="ua" label="Ukrainian"></option>
<option value="vn" label="Vietnamese"></option>
</Form.Select>
</Col>
</Row>
<Row className="justify-content-center mb-5" xs={1} md={2}>
<Col>
<h5>{__('Player Controls', 'infinite-uploads')}</h5>
<p className="lead">{__('Select the UI controls that will be displayed on the player.', 'infinite-uploads')}</p>
</Col>
<Col className="d-flex flex-wrap justify-content-between">
<Row>
<PlayerCheckbox control="play" icon="controls-play" label={__('Play / Pause', 'infinite-uploads')} {...{settings, setSettings}} />
<PlayerCheckbox control="play-large" icon="video-alt3" label={__('Center Play Button', 'infinite-uploads')} {...{settings, setSettings}} />
<PlayerCheckbox control="volume" icon="controls-volumeon" label={__('Volume', 'infinite-uploads')} {...{settings, setSettings}} />
<PlayerCheckbox control="mute" icon="controls-volumeoff" label={__('Mute', 'infinite-uploads')} {...{settings, setSettings}} />
<PlayerCheckbox control="pip" icon="external" label={__('Picture-in-Picture', 'infinite-uploads')} {...{settings, setSettings}} />
<PlayerCheckbox control="settings" icon="admin-generic" label={__('Settings', 'infinite-uploads')} {...{settings, setSettings}} />
<PlayerCheckbox control="captions" icon="format-status" label={__('Captions', 'infinite-uploads')} {...{settings, setSettings}} />
<PlayerCheckbox control="current-time" icon="clock" label={__('Current Time', 'infinite-uploads')} {...{settings, setSettings}} />
<PlayerCheckbox control="duration" icon="editor-video" label={__('Duration', 'infinite-uploads')} {...{settings, setSettings}} />
<PlayerCheckbox control="rewind" icon="controls-skipback" label={__('10s Backward', 'infinite-uploads')} {...{settings, setSettings}} />
<PlayerCheckbox control="fast-forward" icon="controls-skipforward" label={__('10s Forward', 'infinite-uploads')} {...{settings, setSettings}} />
<PlayerCheckbox control="progress" icon="leftright" label={__('Progress Bar', 'infinite-uploads')} {...{settings, setSettings}} />
<PlayerCheckbox control="fullscreen" icon="fullscreen-alt" label={__('Full Screen', 'infinite-uploads')} {...{settings, setSettings}} />
</Row>
</Col>
</Row>
</Tab>
<Tab eventKey="encoding" title={__('Encoding', 'infinite-uploads')} className="mt-4">
<Row className="justify-content-center mb-5" xs={1} md={2}>
<Col>
<h5>{__('Enabled Resolutions', 'infinite-uploads')}</h5>
<p className="lead">{__('Select the enabled resolutions that will be encoded on upload. More resolutions provide a more efficient streaming service to users, but require more storage space. Resolutions larger than the original video will be skipped.', 'infinite-uploads')}</p>
</Col>
<Col>
<ResCheckbox {...{settings, setSettings}} px={240} bitrate={600}/>
<ResCheckbox {...{settings, setSettings}} px={360} bitrate={800}/>
<ResCheckbox {...{settings, setSettings}} px={480} bitrate={1400}/>
<ResCheckbox {...{settings, setSettings}} px={720} bitrate={2800}/>
<ResCheckbox {...{settings, setSettings}} px={1080} bitrate={5000}/>
<ResCheckbox {...{settings, setSettings}} px={1440} bitrate={8000}/>
<ResCheckbox {...{settings, setSettings}} px={2160} bitrate={25000}/>
</Col>
</Row>
</Tab>
</Tabs>
<Row className="justify-content-center mb-3">
<Col className="text-center">
<Button variant="info" className="text-nowrap text-white px-4" onClick={updateSettings} disabled={loading}>{__('Save Settings', 'infinite-uploads')}</Button>
</Col>
</Row>
</Card.Body>
</Card>
</Container>
);
}

View File

@ -0,0 +1,139 @@
import {__, _x, _n, _nx} from '@wordpress/i18n';
import {useState, useEffect, useRef} from '@wordpress/element';
import Container from 'react-bootstrap/Container';
import Modal from 'react-bootstrap/Modal';
import Button from 'react-bootstrap/Button';
import Uppy from '@uppy/core';
import Tus from '@uppy/tus';
import {DragDrop, StatusBar, useUppy} from '@uppy/react';
import UppyCreateVid from '../../block/edit-uppy-plugin';
import '@uppy/core/dist/style.css';
import '@uppy/drag-drop/dist/style.css';
import '@uppy/status-bar/dist/style.css';
export default function UploadModal({getVideos}) {
const [show, setShow] = useState(false);
const uploadAuth = useRef(null);
const uploaded = useRef({});
const handleShow = () => {
setShow(true);
};
const handleClose = () => {
setShow(false);
};
const uppy = useUppy(() => {
return new Uppy({
debug: true,
restrictions: {
maxNumberOfFiles: null,
allowedFileTypes: ['video/*'],
},
autoProceed: true,
allowMultipleUploadBatches: true,
onBeforeUpload: (files) => {
//TODO trigger error if video_id is null
},
})
.use(Tus, {
endpoint: 'https://video.bunnycdn.com/tusupload',
retryDelays: [0, 1000, 3000, 5000, 10000],
onBeforeRequest: (req, file) => {
//console.log('Video Auth:', uploadAuth.current[file.id]);
if (!uploadAuth.current[file.id]) {
throw new Error('Error fetching auth.');
return false;
}
req.setHeader(
'AuthorizationSignature',
uploadAuth.current[file.id].AuthorizationSignature
);
req.setHeader(
'AuthorizationExpire',
uploadAuth.current[file.id].AuthorizationExpire
);
req.setHeader('VideoId', uploadAuth.current[file.id].VideoId);
req.setHeader('LibraryId', IUP_VIDEO.libraryId);
},
})
.use(UppyCreateVid, {uploadAuth}); //our custom plugin
});
uppy.on('error', (error) => {
console.error(error.stack);
});
uppy.on('upload-error', (file, error, response) => {
console.log('error with file:', file.id);
console.log('error message:', error);
});
uppy.on('upload-success', (file, response) => {
if (!uploaded.current[file.id]) { //make sure it only triggers once
getVideos();
uploaded.current = {...uploaded.current, [file.id]: true};
}
});
return (
<>
<Button
variant="primary"
className="text-nowrap text-white ms-4"
onClick={handleShow}
>
<span className="dashicons dashicons-video-alt3"></span>
{__('Upload Videos', 'infinite-uploads')}
</Button>
<Modal
show={show}
onHide={handleClose}
size="lg"
aria-labelledby="contained-modal-title-vcenter"
centered
>
<Modal.Header closeButton>
<Modal.Title id="contained-modal-title-vcenter">
{__('Upload Videos', 'infinite-uploads')}
</Modal.Title>
</Modal.Header>
<Modal.Body>
<Container fluid className="p-3">
<div className="uppy-wrapper">
<DragDrop
width="100%"
height="100%"
// assuming `props.uppy` contains an Uppy instance:
uppy={uppy}
locale={{
strings: {
// Text to show on the droppable area.
// `%{browse}` is replaced with a link that opens the system file selection dialog.
dropHereOr: __(
'Drop videos here or %{browse}.',
'infinite-uploads'
),
// Used as the label for the link that opens the system file selection dialog.
browse: __(
'browse files',
'infinite-uploads'
),
},
}}
/>
<StatusBar
// assuming `props.uppy` contains an Uppy instance:
uppy={uppy}
hideUploadButton={true}
hideAfterFinish={true}
showProgressDetails
/>
</div>
</Container>
</Modal.Body>
</Modal>
</>
);
}

View File

@ -0,0 +1,80 @@
import {__, _x, _n, _nx} from '@wordpress/i18n';
export function VideoSize({video}) {
const sizeOf = function (bytes) {
if (bytes === 0) {
return '0 B';
}
var e = Math.floor(Math.log(bytes) / Math.log(1024));
return (
(bytes / Math.pow(1024, e)).toFixed(1) +
' ' +
' KMGTP'.charAt(e) +
'B'
);
};
return (
<span
className="d-inline-flex text-nowrap"
title={__('Storage Size', 'infinite-uploads')}
>
<span className="dashicons dashicons-media-video me-1"></span>
{sizeOf(video.storageSize)}
</span>
);
}
export function VideoLength({video}) {
//function to turn seconds into a human readable time
const secondsToTime = function (seconds) {
var h = Math.floor(seconds / 3600);
var m = Math.floor((seconds % 3600) / 60);
var s = Math.floor((seconds % 3600) % 60);
return (
(h > 0 ? h + ':' : '') +
(m > 0 ? (h > 0 && m < 10 ? '0' : '') + m + ':' : '0:') +
(s < 10 ? '0' : '') +
s
);
};
return (
<span
className="d-inline-flex text-nowrap"
title={__('Video Length', 'infinite-uploads')}
>
<span className="dashicons dashicons-clock me-1"></span>
{secondsToTime(video.length)}
</span>
);
}
export function VideoViews({video}) {
return (
<span
className="d-inline-flex text-nowrap"
title={__('View Count', 'infinite-uploads')}
>
<span className="dashicons dashicons-welcome-view-site me-1"></span>
{video.views}
</span>
);
}
export function VideoDate({video}) {
//change datetime to human-readable in localstring
const dateTime = function (date) {
return new Date(date).toLocaleString();
};
return (
<small
className="d-inline-flex text-nowrap"
title={__('Upload Date', 'infinite-uploads')}
>
<span className="dashicons dashicons-calendar me-1"></span>
{dateTime(video.dateUploaded)}
</small>
);
}

View File

@ -0,0 +1,145 @@
import {__, _x, _n, _nx} from '@wordpress/i18n';
import Card from 'react-bootstrap/Card';
import {useState} from '@wordpress/element';
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';
import {VideoLength, VideoViews, VideoSize} from './VideoAttributes';
import VideoModal from './VideoModal';
import ProgressBar from 'react-bootstrap/ProgressBar';
import DeleteModal from './DeleteModal';
function VideoCard({video, videos, setVideos, selectVideo}) {
const getThumbnail = (file) => {
return IUP_VIDEO.cdnUrl + '/' + video.guid + '/' + file;
};
const [src, setSrc] = useState(getThumbnail(video.thumbnailFileName));
const statusLabels = {
0: __('Awaiting Upload', 'infinite-uploads'),
1: __('Uploaded', 'infinite-uploads'),
2: __('Processing', 'infinite-uploads'),
3: __('Transcoding', 'infinite-uploads'),
4: __('Finished', 'infinite-uploads'),
5: __('Error', 'infinite-uploads'),
6: __('Upload Failed', 'infinite-uploads'),
};
const status = statusLabels[video.status];
if ([0, 1, 5, 6].includes(video.status)) {
return (
<span className="m-3 w-100 p-0">
<Card className="m-0 shadow-sm">
<div className="ratio ratio-16x9 overflow-hidden bg-black text-white rounded-top">
<div>
<div className="d-flex justify-content-center align-items-center h-100 text-secondary font-weight-bold">
{status}
</div>
</div>
</div>
<Card.Body className={'p-2'}>
<Card.Title className="h6 card-title text-truncate">
{video.title}
</Card.Title>
<Row className="justify-content-end text-muted align-items-center">
<Col className="justify-content-end d-flex">
{!selectVideo && (
<DeleteModal
video={video}
setVideos={setVideos}
/>
)}
</Col>
</Row>
</Card.Body>
</Card>
</span>
);
} else if ([2].includes(video.status)) {
//processing
return (
<span className="m-3 w-100 p-0">
<Card className="m-0 shadow-sm">
<div className="ratio ratio-16x9 overflow-hidden bg-black text-white rounded-top">
<div>
<div className="d-flex justify-content-center align-items-center h-100 text-secondary font-weight-bold">
{status}
</div>
</div>
</div>
<Card.Body className={'p-2'}>
<Card.Title className="h6 card-title text-truncate">
{video.title}
</Card.Title>
<small className="row justify-content-between text-muted align-items-center">
<Col className="col-auto">
{__('Processing', 'infinite-uploads')}:
</Col>
<Col>
<ProgressBar
animated
now={video.encodeProgress}
label={`${video.encodeProgress}%`}
className="w-100"
/>
</Col>
</small>
</Card.Body>
</Card>
</span>
);
} else {
return (
<VideoModal {...{video, setVideos, selectVideo}}>
<Card className="m-0 shadow-sm">
<div className="ratio ratio-16x9 overflow-hidden bg-black rounded-top">
<div
className="iup-video-thumb"
style={{backgroundImage: `url("${src}")`}}
onMouseOver={() =>
setSrc(getThumbnail('preview.webp'))
}
onMouseOut={() =>
setSrc(
getThumbnail(video.thumbnailFileName)
)
}
></div>
</div>
<Card.Body className={'p-2'}>
<Card.Title className="h6 card-title text-truncate">
{video.title}
</Card.Title>
{video.status === 3 ? (
<small className="row justify-content-between text-muted align-items-center">
<Col className="col-auto">
{__('Transcoding', 'infinite-uploads')}:
</Col>
<Col>
<ProgressBar
animated
now={video.encodeProgress}
label={`${video.encodeProgress}%`}
className="w-100"
/>
</Col>
</small>
) : (
<small className="row justify-content-between text-muted align-items-center">
<Col>
<VideoLength video={video}/>
</Col>
<Col></Col>
<Col>
<VideoSize video={video}/>
</Col>
</small>
)}
</Card.Body>
</Card>
</VideoModal>
);
}
}
export default VideoCard;

View File

@ -0,0 +1,604 @@
import {__, _x, _n, _nx} from '@wordpress/i18n';
import Card from 'react-bootstrap/Card';
import {useState, useEffect} from '@wordpress/element';
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';
import Container from 'react-bootstrap/Container';
import Modal from 'react-bootstrap/Modal';
import {
VideoLength,
VideoSize,
VideoViews,
VideoDate,
} from './VideoAttributes';
import Form from 'react-bootstrap/Form';
import InputGroup from 'react-bootstrap/InputGroup';
import Button from 'react-bootstrap/Button';
import Tab from 'react-bootstrap/Tab';
import Tabs from 'react-bootstrap/Tabs';
import DeleteModal from './DeleteModal';
import Spinner from 'react-bootstrap/Spinner';
export default function VideoModal({
video,
setVideos,
selectVideo,
children,
}) {
const [show, setShow] = useState(false);
const [title, setTitle] = useState(video.title);
const [autoPlay, setAutoPlay] = useState(false);
const [loop, setLoop] = useState(false);
const [muted, setMuted] = useState(false);
const [preload, setPreload] = useState(true);
const [embedParams, setEmbedParams] = useState('');
const [uploading, setUploading] = useState(false);
const [loading, setLoading] = useState(false);
const [iframe, setIframe] = useState(null);
useEffect(() => {
let params = [];
if (autoPlay) {
params.push('autoplay="true"');
}
if (loop) {
params.push('loop="true"');
}
if (muted) {
params.push('muted="true"');
}
if (preload) {
params.push('preload="true"');
}
setEmbedParams(params.join(' '));
}, [autoPlay, loop, muted, preload]);
useEffect(() => {
const component = (
<iframe
src={`https://iframe.mediadelivery.net/embed/${
video.videoLibraryId
}/${video.guid}?autoplay=false&v=${Math.random()}`}
loading="lazy"
allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;"
allowFullScreen={true}
></iframe>
);
setIframe(component);
}, [video]);
const getThumbnail = (file) => {
return IUP_VIDEO.cdnUrl + '/' + video.guid + '/' + file;
};
const handleShow = () => {
setShow(true);
};
const handleClose = () => {
setShow(false);
};
function updateVideo() {
setLoading(true);
const formData = new FormData();
formData.append('title', title);
formData.append('video_id', video.guid);
formData.append('nonce', IUP_VIDEO.nonce);
const options = {
method: 'POST',
headers: {
Accept: 'application/json',
},
body: formData,
};
fetch(`${ajaxurl}?action=infinite-uploads-video-update`, options)
.then((response) => response.json())
.then((data) => {
if (data.success) {
setVideos((videos) =>
videos.map((v) =>
v.guid === video.guid ? {...v, title} : v
)
);
} else {
console.error(data.data);
}
setLoading(false);
})
.catch((error) => {
console.log('Error:', error);
setLoading(false);
});
}
function setThumbnail(thumbnailFileName) {
setLoading(true);
const formData = new FormData();
formData.append('thumbnail', thumbnailFileName);
formData.append('video_id', video.guid);
formData.append('nonce', IUP_VIDEO.nonce);
const options = {
method: 'POST',
headers: {
Accept: 'application/json',
},
body: formData,
};
fetch(`${ajaxurl}?action=infinite-uploads-video-update`, options)
.then((response) => response.json())
.then((data) => {
if (data.success) {
setVideos((videos) =>
videos.map((v) =>
v.guid === video.guid
? {...v, thumbnailFileName}
: v
)
);
} else {
console.error(data.data);
}
setLoading(false);
})
.catch((error) => {
console.log('Error:', error);
setLoading(false);
});
}
function uploadThumbnail(file) {
setUploading(true);
const formData = new FormData();
formData.append('thumbnailFile', file);
formData.append('video_id', video.guid);
formData.append('nonce', IUP_VIDEO.nonce);
const options = {
method: 'POST',
headers: {
Accept: 'application/json',
},
body: formData,
};
fetch(`${ajaxurl}?action=infinite-uploads-video-update`, options)
.then((response) => response.json())
.then((data) => {
if (data.success) {
getVideo(); // refresh video data
setUploading(false);
} else {
console.error(data.data);
setUploading(false);
}
})
.catch((error) => {
console.log('Error:', error);
setUploading(false);
});
}
function getVideo() {
const options = {
method: 'GET',
headers: {
Accept: 'application/json',
AccessKey: IUP_VIDEO.apiKey,
},
};
fetch(
`https://video.bunnycdn.com/library/${IUP_VIDEO.libraryId}/videos/${video.guid}`,
options
)
.then((response) => response.json())
.then((data) => {
//replace video in videos array
setVideos((videos) =>
videos.map((v) =>
v.guid === video.guid ? {...v, ...data} : v
)
);
})
.catch((error) => {
console.error(error);
});
}
let thumbnails = [];
for (let i = 1; i <= 5; i++) {
thumbnails.push(
<Col key={i} className="mb-2">
<Card
className="bg-dark text-white h-100 p-0"
role="button"
disabled={loading || uploading}
onClick={() => setThumbnail('thumbnail_' + i + '.jpg')}
>
<div className="ratio ratio-16x9 overflow-hidden bg-black rounded">
<div
className="iup-video-thumb rounded border-0"
style={{
backgroundImage: `url("${getThumbnail(
'thumbnail_' + i + '.jpg'
)}")`,
}}
></div>
</div>
<div className="card-img-overlay rounded border-0">
<div className="card-title align-middle text-center text-white">
{__('Set', 'infinite-uploads')}
</div>
</div>
</Card>
</Col>
);
}
thumbnails.push(
<Col key="fileupload" className="mb-2">
<Card
className="h-100 p-0 border-4 border-secondary"
style={{borderStyle: 'dashed'}}
disabled={loading || uploading}
role="button"
onClick={() =>
document.getElementById('upload-thumbnail').click()
}
>
<div className="ratio ratio-16x9 overflow-hidden bg-light border-0 rounded">
<div>
{uploading ? (
<div className="h-100 w-100 d-flex align-items-center justify-content-center">
<Spinner
animation="border"
role="status"
className="text-muted"
/>
</div>
) : (
<span className="dashicons dashicons-upload h-100 w-100 d-flex align-items-center justify-content-center text-muted h3"></span>
)}
</div>
</div>
<Form.Control
type="file"
id="upload-thumbnail"
className="d-none"
accept="image/png, image/jpeg"
disabled={loading || uploading}
onChange={() =>
uploadThumbnail(
document.getElementById('upload-thumbnail')
.files[0]
)
}
/>
</Card>
</Col>
);
return (
<>
<a
className="m-3 w-100 p-0 text-decoration-none"
role="button"
aria-label={__('Open video modal', 'infinite-uploads')}
onClick={() => {
if (selectVideo) {
selectVideo(video);
} else {
handleShow();
}
}}
>
{children}
</a>
<Modal
show={show}
onHide={handleClose}
size="xl"
aria-labelledby="contained-modal-title-vcenter"
centered
>
<Modal.Header closeButton>
<Modal.Title id="contained-modal-title-vcenter">
{__('Edit Video:', 'infinite-uploads')}{' '}
{video.title}
</Modal.Title>
</Modal.Header>
<Modal.Body>
<Container fluid className="pb-3">
<Row
className="justify-content-center mb-4 mt-3"
xs={1}
lg={2}
>
<Col>
<Row className="mb-2">
<Col>
<div className="ratio ratio-16x9">
{iframe}
</div>
</Col>
</Row>
<Row className="justify-content-between text-muted text-center">
<Col>
<VideoDate video={video}/>
</Col>
<Col>
<VideoLength video={video}/>
</Col>
<Col>
<VideoViews video={video}/>
</Col>
<Col>
<VideoSize video={video}/>
</Col>
</Row>
</Col>
<Col>
<Row className="mb-4">
<Col>
<label htmlFor="video-title">
{__(
'Video Title',
'infinite-uploads'
)}
</label>
<InputGroup>
<Form.Control
id="video-title"
placeholder={__(
'Title',
'infinite-uploads'
)}
aria-label={__(
'Title',
'infinite-uploads'
)}
value={title}
onChange={(e) =>
setTitle(e.target.value)
}
disabled={
loading || uploading
}
/>
<Button
variant="primary"
className="text-white"
disabled={
loading || uploading
}
onClick={updateVideo}
>
{__(
'Update',
'infinite-uploads'
)}
</Button>
</InputGroup>
</Col>
</Row>
<Row className="mb-4">
<Col className="col-4">
<h6>
{__(
'Current Thumbnail',
'infinite-uploads'
)}
</h6>
<Card className="bg-dark text-white w-100 p-0 mb-2">
<div className="ratio ratio-16x9 overflow-hidden bg-black rounded border-0">
<div
className="iup-video-thumb rounded border-0"
style={{
backgroundImage: `url("${getThumbnail(
video.thumbnailFileName
)}")`,
}}
></div>
</div>
</Card>
</Col>
<Col className="col-8">
<p>
{__(
'Choose a new thumbnail to be displayed in the video player:',
'infinite-uploads'
)}
</p>
<Row className="justify-content-start d-flex row-cols-2 row-cols-md-3">
{thumbnails}
</Row>
</Col>
</Row>
<Row className="justify-content-end mb-3">
<Col className="justify-content-end d-flex">
<DeleteModal
video={video}
setVideos={setVideos}
/>
</Col>
</Row>
</Col>
</Row>
<Tabs defaultActiveKey="shortcode" className="mb-4">
<Tab
eventKey="shortcode"
title={
<div className="d-inline-flex align-start">
<span className="dashicons dashicons-shortcode me-1"></span>
{__(
'Embed Code',
'infinite-uploads'
)}
</div>
}
>
<Row className="justify-content-center mt-2">
<Col>
<Row>
<Col>
<p>
{__(
'Copy and paste this code into your post, page, or widget to embed the video. If using Gutenberg editor use our block.',
'infinite-uploads'
)}
</p>
</Col>
</Row>
<Row className="mb-1">
<Col>
<Form>
<Form.Check
inline
label={__(
'Autoplay',
'infinite-uploads'
)}
type="checkbox"
checked={autoPlay}
onChange={(e) =>
setAutoPlay(
e.target.checked
)
}
/>
<Form.Check
inline
label={__(
'Loop',
'infinite-uploads'
)}
type="checkbox"
checked={loop}
onChange={(e) =>
setLoop(
e.target.checked
)
}
/>
<Form.Check
inline
label={__(
'Muted',
'infinite-uploads'
)}
type="checkbox"
checked={muted}
onChange={(e) =>
setMuted(
e.target.checked
)
}
/>
<Form.Check
inline
label={__(
'Preload',
'infinite-uploads'
)}
type="checkbox"
checked={preload}
onChange={(e) =>
setPreload(
e.target.checked
)
}
style={{ display: 'none' }} // Hides the preload checkbox
/>
</Form>
</Col>
</Row>
<Row>
<Col>
<Form.Control
type="text"
aria-label="Embed Code"
readOnly
value={`[infinite-uploads-vid id="${video.guid}" ${embedParams}]`}
onClick={(e) => {
e.target.select();
document.execCommand(
'copy'
);
}}
/>
</Col>
</Row>
</Col>
</Row>
</Tab>
<Tab
eventKey="stats"
disabled
title={
<>
<span className="dashicons dashicons-chart-area me-1"></span>
{__('Stats', 'infinite-uploads')}
</>
}
>
<Row className="justify-content-center">
<Col>
<Row>
<Col>
<h5>
{__(
'Statistics',
'infinite-uploads'
)}
</h5>
<p>
{__(
'View the statistics for this video.',
'infinite-uploads'
)}
</p>
</Col>
</Row>
<Row>
<Col>Chart here</Col>
</Row>
</Col>
</Row>
</Tab>
<Tab
eventKey="captions"
disabled
title={
<>
<span className="dashicons dashicons-format-status me-1"></span>
{__('Captions', 'infinite-uploads')}
</>
}
></Tab>
<Tab
eventKey="chapters"
disabled
title={
<>
<span className="dashicons dashicons-text me-1"></span>
{__('Chapters', 'infinite-uploads')}
</>
}
></Tab>
</Tabs>
</Container>
</Modal.Body>
</Modal>
</>
);
}