restreamer-ui-v2/src/views/Playersite.js
2022-07-13 20:21:02 +02:00

842 lines
24 KiB
JavaScript

import React from 'react';
import { useNavigate } from 'react-router-dom';
import { useLingui } from '@lingui/react';
import { Trans, t } from '@lingui/macro';
import makeStyles from '@mui/styles/makeStyles';
import Backdrop from '@mui/material/Backdrop';
import Button from '@mui/material/Button';
import CircularProgress from '@mui/material/CircularProgress';
import Divider from '@mui/material/Divider';
import Grid from '@mui/material/Grid';
import MenuItem from '@mui/material/MenuItem';
import Tab from '@mui/material/Tab';
import Tabs from '@mui/material/Tabs';
import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
import * as M from '../utils/metadata';
import playerSiteThumb from '../assets/images/playersite.png';
import Checkbox from '../misc/Checkbox';
import ColorPicker from '../misc/ColorPicker';
import Dialog from '../misc/modals/Dialog';
import FormInlineButton from '../misc/FormInlineButton';
import H from '../utils/help';
import NotifyContext from '../contexts/Notify';
import Paper from '../misc/Paper';
import PaperHeader from '../misc/PaperHeader';
import PaperFooter from '../misc/PaperFooter';
import PaperThumb from '../misc/PaperThumb';
import Select from '../misc/Select';
import TabPanel from '../misc/TabPanel';
import TabsVerticalGrid from '../misc/TabsVerticalGrid';
const useStyles = makeStyles((theme) => ({
buttonOpen: {
float: 'right',
marginLeft: '.5em',
},
}));
const imageTypes = [
{ mimetype: 'image/gif', extension: 'gif', maxSize: 1 * 1024 * 1024 },
{ mimetype: 'image/png', extension: 'png', maxSize: 1 * 1024 * 1024 },
{ mimetype: 'image/jpeg', extension: 'jpg', maxSize: 1 * 1024 * 1024 },
{ mimetype: 'image/svg+xml', extension: 'svg', maxSize: 1 * 1024 * 1024 },
];
const imageAcceptString = imageTypes.map((t) => t.mimetype).join(',');
const templateTypes = [{ mimetype: 'text/html', extension: 'html', maxSize: 500 * 1024 }];
const templateAcceptString = templateTypes.map((t) => t.mimetype).join(',');
export default function Playersite(props) {
const classes = useStyles();
const navigate = useNavigate();
const { i18n } = useLingui();
const address = props.restreamer.Address() + '/';
const playersiteUrl = props.restreamer.GetPlayersiteUrl();
const notify = React.useContext(NotifyContext);
const [$ready, setReady] = React.useState(false);
const [$ingest, setIngest] = React.useState(false);
const [$available, setAvailable] = React.useState(false);
const [$data, setData] = React.useState(M.getDefaultMetadata());
const [$settings, setSettings] = React.useState({});
const [$channels, setChannels] = React.useState([]);
const [$templates, setTemplates] = React.useState([]);
const [$tab, setTab] = React.useState('general');
const [$saving, setSaving] = React.useState(false);
const [$error, setError] = React.useState({
open: false,
title: '',
message: '',
});
React.useEffect(() => {
(async () => {
await mount();
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const mount = async () => {
const data = await props.restreamer.GetMetadata();
setData(data);
setSettings(props.restreamer.InitPlayersiteSettings(data.playersite));
setChannels(props.restreamer.ListChannels());
setIngest(props.restreamer.HasIngest());
setTemplates(await props.restreamer.ListPlayersiteTemplates());
setAvailable(await props.restreamer.HasPlayersite());
setReady(true);
};
const handleChange = (what) => (event) => {
const value = event.target.value;
const settings = $settings;
if (['playersite', 'header', 'share', 'support', 'chromecast', 'airplay'].includes(what)) {
settings[what] = !settings[what];
} else {
settings[what] = value;
}
setSettings({
...$settings,
...settings,
});
};
const handleBackgroundImageUpload = (event) => {
const handler = (event) => {
const files = event.target.files;
setSaving(true);
if (files.length === 0) {
// no files selected
setSaving(false);
showUploadError(<Trans>Please select a file to upload.</Trans>);
return;
}
const file = files[0];
let type = null;
for (let t of imageTypes) {
if (t.mimetype === file.type) {
type = t;
break;
}
}
if (type === null) {
// not one of the allowed mimetypes
setSaving(false);
const types = imageAcceptString;
showUploadError(
<Trans>
The selected file type ({file.type}) is not allowed. Allowed file types are {types}
</Trans>
);
return;
}
if (file.size > type.maxSize) {
// the file is too big
setSaving(false);
showUploadError(
<Trans>
The selected file is too big ({file.size} bytes). Only {type.maxSize} bytes are allowed.
</Trans>
);
return;
}
let reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onloadend = async () => {
if (reader.result === null) {
// reading the file failed
setSaving(false);
showUploadError(<Trans>There was an error during upload: {reader.error.message}</Trans>);
return;
}
const path = await props.restreamer.UploadPlayersiteBackgroundImage(reader.result, type.extension);
handleChange('bgimage_url')({
target: {
value: path,
},
});
setSaving(false);
};
};
handler(event);
// reset the value such that the onChange event will be triggered again
// if the same file gets selected again
event.target.value = null;
};
const handleTemplateUpload = (event) => {
const handler = (event) => {
const files = event.target.files;
setSaving(true);
if (files.length === 0) {
// no files selected
setSaving(false);
showUploadError(<Trans>Please select a file to upload.</Trans>);
return;
}
const file = files[0];
let type = null;
for (let t of templateTypes) {
if (t.mimetype === file.type) {
type = t;
break;
}
}
if (type === null) {
// not one of the allowed mimetypes
setSaving(false);
const types = templateAcceptString;
showUploadError(
<Trans>
The selected file type ({file.type}) is not allowed. Allowed file types are {types}
</Trans>
);
return;
}
if (file.size > type.maxSize) {
// the file is too big
setSaving(false);
showUploadError(
<Trans>
The selected file is too big ({file.size} bytes). Only {type.maxSize} bytes are allowed.
</Trans>
);
return;
}
let reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onloadend = async () => {
if (reader.result === null) {
// reading the file failed
setSaving(false);
showUploadError(<Trans>There was an error during upload: {reader.error.message}</Trans>);
return;
}
const name = await props.restreamer.UploadPlayersiteTemplate(reader.result, $settings.templatename);
setTemplates(await props.restreamer.ListPlayersiteTemplates());
setSettings({
...$settings,
template: name,
templatename: '',
});
setSaving(false);
};
};
handler(event);
// reset the value such that the onChange event will be triggered again
// if the same file gets selected again
event.target.value = null;
};
const handleTemplateDelete = async () => {
setSaving(true);
await props.restreamer.DeletePlayersiteTemplate($settings.template);
setSettings({
...$settings,
template: '!default',
});
setTemplates(await props.restreamer.ListPlayersiteTemplates());
setSaving(false);
};
const showUploadError = (message) => {
setError({
...$error,
open: true,
title: <Trans>Uploading the file failed</Trans>,
message: message,
});
};
const hideUploadError = () => {
setError({
...$error,
open: false,
});
};
const handleDone = async () => {
setSaving(true);
const data = {
...$data,
playersite: $settings,
};
let res = await props.restreamer.SetMetadata(data);
if (res === false) {
notify.Dispatch('error', 'save:playersite', i18n._(t`Failed to store player size setting.`));
setSaving(false);
return;
}
res = await props.restreamer.UpdatePlayersite();
if (res === false) {
notify.Dispatch('error', 'save:playersite', i18n._(t`Failed to create publication website files.`));
setSaving(false);
return;
}
setAvailable(await props.restreamer.HasPlayersite());
setSaving(false);
notify.Dispatch('success', 'save:playersite', i18n._(t`Publication website settings saved`));
};
const handleChangeTab = (event, value) => {
setTab(value);
};
const handleAbort = () => {
navigate('/');
};
const handleHelp = () => {
H('playersite-' + $tab);
};
if ($ready === false) {
return null;
}
if ($ingest === false) {
navigate('/');
return null;
}
return (
<React.Fragment>
<Paper xs={12} md={10}>
<PaperHeader title={<Trans>EDIT: Publication Website</Trans>} onAbort={handleAbort} onHelp={handleHelp} />
<Grid container spacing={2}>
<TabsVerticalGrid>
<Tabs orientation="vertical" variant="scrollable" value={$tab} onChange={handleChangeTab} className="tabs">
<Tab className="tab" label={<Trans>General</Trans>} value="general" />
<Tab className="tab" label={<Trans>Template</Trans>} value="template" disabled={!$settings.playersite} />
<Tab className="tab" label={<Trans>Design</Trans>} value="design" disabled={!$settings.playersite} />
<Tab className="tab" label={<Trans>Notes</Trans>} value="notes" disabled={!$settings.playersite} />
<Tab className="tab" label={<Trans>Code injection</Trans>} value="code_injection" disabled={!$settings.playersite} />
</Tabs>
<TabPanel value={$tab} index="general">
<Grid container spacing={2}>
<Grid item xs={12}>
<PaperThumb image={playerSiteThumb} title="Playersite" />
</Grid>
<Grid item xs={12}>
<Typography variant="h1">
<Trans>General</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Typography variant="body1">
<Trans>
In addition to the player, the Restreamer offers a complete landingpage, which you can use to present your live
stream easily and quickly.
</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Checkbox label={<Trans>Playersite</Trans>} checked={$settings.playersite} onChange={handleChange('playersite')} />
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
fullWidth
label={<Trans>Sitename</Trans>}
disabled={!$settings.playersite}
value={$settings.title}
onChange={handleChange('title')}
/>
</Grid>
<Grid item xs={12}>
<Select
label={<Trans>Main channel</Trans>}
value={$settings.channelid}
disabled={!$settings.playersite}
onChange={handleChange('channelid')}
>
<MenuItem value="current">
<Trans>Selected channel</Trans>
</MenuItem>
{$channels
.sort((a, b) => {
const aname = a.name.toUpperCase();
const bname = b.name.toUpperCase();
return aname < bname ? -1 : aname > bname ? 1 : 0;
})
.map((c) => {
return (
<MenuItem key={c.channelid} value={c.channelid}>
{c.name}
</MenuItem>
);
})}
</Select>
<Typography variant="caption">
<Trans>Main page channel (index.html).</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Checkbox
label={<Trans>Share button</Trans>}
checked={$settings.share}
disabled={!$settings.playersite}
onChange={handleChange('share')}
/>
</Grid>
<Grid item xs={12}>
<Checkbox
label={<Trans>Chromecast</Trans>}
checked={$settings.chromecast}
disabled={!$settings.playersite}
onChange={handleChange('chromecast')}
/>
</Grid>
<Grid item xs={12}>
<Checkbox
label={<Trans>AirPlay</Trans>}
checked={$settings.airplay}
disabled={!$settings.playersite}
onChange={handleChange('airplay')}
/>
</Grid>
<Grid item xs={12}>
<Checkbox
label={<Trans>Support datarhei Restreamer</Trans>}
checked={$settings.support}
disabled={!$settings.playersite}
onChange={handleChange('support')}
/>
<Typography variant="caption">
<Trans>Shows a reference to the project.</Trans>
</Typography>
</Grid>
</Grid>
</TabPanel>
<TabPanel value={$tab} index="template">
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography variant="h1">
<Trans>Template</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Typography variant="body1">
<Trans>Available</Trans>
</Typography>
</Grid>
<Grid item xs={12} md={9}>
<Select label={<Trans>Selection</Trans>} value={$settings.template} onChange={handleChange('template')}>
<MenuItem value="!default">
<Trans>Default</Trans>
</MenuItem>
{$templates.sort().map((a) => {
return (
<MenuItem key={a} value={a}>
{a}
</MenuItem>
);
})}
</Select>
<Typography variant="caption">
<Trans>
Template to be used for creating the publication website. The delete button removes the selection from the system.
</Trans>
</Typography>
</Grid>
<Grid item xs={12} md={3}>
<FormInlineButton
variant="outlined"
color="secondary"
disabled={$settings.template === '!default'}
onClick={handleTemplateDelete}
>
<Trans>Delete</Trans>
</FormInlineButton>
</Grid>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={12}>
<Typography variant="body1">
<Trans>Upload</Trans>
</Typography>
</Grid>
<Grid item xs={12} md={9}>
<TextField
variant="outlined"
fullWidth
label={<Trans>Name</Trans>}
value={$settings.templatename}
onChange={handleChange('templatename')}
/>
<Typography variant="caption">
<Trans>Name for the template. If the name already exists, it will be overwritten.</Trans>
</Typography>
</Grid>
<Grid item xs={12} md={3}>
<FormInlineButton variant="outlined" color="primary" component="label" disabled={$settings.templatename.length === 0}>
<Trans>Upload</Trans>
<input accept={templateAcceptString} type="file" hidden onChange={handleTemplateUpload} />
</FormInlineButton>
</Grid>
</Grid>
</TabPanel>
<TabPanel value={$tab} index="design">
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography variant="h1">
<Trans>Design</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Typography variant="body1">
<Trans>Adjust publication site colors and background as you like.</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Typography variant="h3">
<Trans>Text colors</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<ColorPicker
variant="outlined"
fullWidth
label={<Trans>Default</Trans>}
value={$settings.textcolor_default}
onChange={handleChange('textcolor_default')}
/>
</Grid>
<Grid item xs={12}>
<ColorPicker
variant="outlined"
fullWidth
label={<Trans>Headline</Trans>}
value={$settings.textcolor_title}
onChange={handleChange('textcolor_title')}
/>
</Grid>
<Grid item xs={6}>
<ColorPicker
variant="outlined"
fullWidth
label={<Trans>Link</Trans>}
value={$settings.textcolor_link}
onChange={handleChange('textcolor_link')}
/>
</Grid>
<Grid item xs={6}>
<ColorPicker
variant="outlined"
fullWidth
label={<Trans>Link, mouseover</Trans>}
value={$settings.textcolor_link_hover}
onChange={handleChange('textcolor_link_hover')}
/>
</Grid>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={12}>
<Typography variant="h3">
<Trans>Background colors</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<ColorPicker
variant="outlined"
fullWidth
label={<Trans>Default</Trans>}
value={$settings.bgcolor_default}
onChange={handleChange('bgcolor_default')}
/>
</Grid>
<Grid item xs={12}>
<ColorPicker
variant="outlined"
fullWidth
label={<Trans>Header</Trans>}
value={$settings.bgcolor_header}
onChange={handleChange('bgcolor_header')}
/>
</Grid>
<Grid item xs={12}>
<ColorPicker
variant="outlined"
fullWidth
label={<Trans>Selected</Trans>}
value={$settings.bgcolor_selected}
onChange={handleChange('bgcolor_selected')}
/>
</Grid>
<Grid item xs={12}>
<ColorPicker
variant="outlined"
fullWidth
label={<Trans>Unselected</Trans>}
value={$settings.bgcolor_unselected}
onChange={handleChange('bgcolor_unselected')}
/>
</Grid>
<Grid item xs={12}>
<ColorPicker
variant="outlined"
fullWidth
label={<Trans>Linecolor</Trans>}
value={$settings.hrcolor}
onChange={handleChange('hrcolor')}
/>
</Grid>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={12}>
<Typography variant="h3">
<Trans>Background image</Trans>
</Typography>
</Grid>
<Grid item xs={12} md={8}>
<TextField
variant="outlined"
fullWidth
label={<Trans>Image URL</Trans>}
value={$settings.bgimage_url}
onChange={handleChange('bgimage_url')}
/>
<Typography variant="caption">
<Trans>Address for the background image.</Trans>
</Typography>
</Grid>
<Grid item xs={12} md={4}>
<FormInlineButton component="label">
<Trans>Upload</Trans>
<input accept={imageAcceptString} type="file" hidden onChange={handleBackgroundImageUpload} />
</FormInlineButton>
</Grid>
</Grid>
</TabPanel>
<TabPanel value={$tab} index="notes">
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography variant="h2">
<Trans>Notes</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Typography variant="h3">
<Trans>Imprint</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<TextField
className={classes.root}
variant="outlined"
fullWidth
multiline
rows={10}
label={<Trans>Content</Trans>}
value={$settings.imprint}
onChange={handleChange('imprint')}
/>
</Grid>
<Grid item xs={12}>
<Typography variant="h3">
<Trans>Terms</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<TextField
className={classes.root}
variant="outlined"
fullWidth
multiline
rows={10}
label={<Trans>Content</Trans>}
value={$settings.terms}
onChange={handleChange('terms')}
/>
</Grid>
</Grid>
</TabPanel>
<TabPanel value={$tab} index="code_injection">
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography variant="h2">
<Trans>Custom code injection</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Typography variant="body1">
<Trans>Add external widgets and styles to the publication site. You can find some examples on the help page.</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Typography variant="h3">
<Trans>Extend header</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<TextField
className={classes.root}
variant="outlined"
fullWidth
multiline
rows={10}
label={<Trans>Inject 1</Trans>}
value={$settings.inject1}
onChange={handleChange('inject1')}
/>
<Typography variant="caption">
<Trans>For Stylesheets.</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Typography variant="h3">
<Trans>Extend channel list</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<TextField
className={classes.root}
variant="outlined"
fullWidth
multiline
rows={10}
label={<Trans>Inject 2</Trans>}
value={$settings.inject2}
onChange={handleChange('inject2')}
/>
<Typography variant="caption">
<Trans>Expands the area above the channel list (live chat).</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Typography variant="h3">
<Trans>Extend content</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<TextField
className={classes.root}
variant="outlined"
fullWidth
multiline
rows={10}
label={<Trans>Inject 3</Trans>}
value={$settings.inject3}
onChange={handleChange('inject3')}
/>
<Typography variant="caption">
<Trans>Expands the area under the channel description (comment boxes).</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<Typography variant="h3">
<Trans>Extend footer</Trans>
</Typography>
</Grid>
<Grid item xs={12}>
<TextField
className={classes.root}
variant="outlined"
fullWidth
multiline
rows={10}
label={<Trans>Inject 4</Trans>}
value={$settings.inject4}
onChange={handleChange('inject4')}
/>
<Typography variant="caption">
<Trans>For Javascripts.</Trans>
</Typography>
</Grid>
</Grid>
</TabPanel>
</TabsVerticalGrid>
</Grid>
<PaperFooter
buttonsLeft={
<Button variant="outlined" color="default" onClick={handleAbort}>
<Trans>Close</Trans>
</Button>
}
buttonsRight={
<React.Fragment>
<Button variant="outlined" color="primary" onClick={handleDone}>
<Trans>Save</Trans>
</Button>
<Button
variant="outlined"
color="primary"
className={classes.buttonOpen}
href={address + playersiteUrl}
target="blank"
disabled={!$available}
>
<Trans>Open</Trans>
</Button>
</React.Fragment>
}
/>
</Paper>
<Backdrop open={$saving}>
<CircularProgress color="inherit" />
</Backdrop>
<Dialog
open={$error.open}
title={$error.title}
onClose={hideUploadError}
buttonsRight={
<Button variant="outlined" color="primary" onClick={hideUploadError}>
<Trans>OK</Trans>
</Button>
}
>
<Typography variant="body1">{$error.message}</Typography>
</Dialog>
</React.Fragment>
);
}
Playersite.defaultProps = {
restreamer: null,
};