some prettifying

This commit is contained in:
gingervitis
2021-01-31 01:38:20 -08:00
parent d01c2f081f
commit 67c160afdb
24 changed files with 727 additions and 637 deletions

View File

@@ -15,7 +15,7 @@ export const SUCCESS_STATES = {
},
error: {
icon: <ExclamationCircleFilled style={{ color: 'red' }} />,
message: 'An error occurred.',
message: 'An error occurred.',
},
};
@@ -39,14 +39,8 @@ export const API_VIDEO_VARIANTS = '/video/streamoutputvariants';
export const API_WEB_PORT = '/webserverport';
export const API_YP_SWITCH = '/directoryenabled';
export async function postConfigUpdateToAPI(args: ApiPostArgs) {
const {
apiPath,
data,
onSuccess,
onError,
} = args;
const { apiPath, data, onSuccess, onError } = args;
const result = await fetchData(`${SERVER_CONFIG_UPDATE_URL}${apiPath}`, {
data,
method: 'POST',
@@ -59,7 +53,6 @@ export async function postConfigUpdateToAPI(args: ApiPostArgs) {
}
}
// Some default props to help build out a TextField
export const TEXTFIELD_PROPS_USERNAME = {
apiPath: API_USERNAME,
@@ -95,7 +88,8 @@ export const TEXTFIELD_PROPS_LOGO = {
maxLength: 255,
placeholder: '/img/mylogo.png',
label: 'Logo',
tip: 'Path to your logo from website root. We recommend that you use a square image that is at least 256x256. (upload functionality coming soon)',
tip:
'Path to your logo from website root. We recommend that you use a square image that is at least 256x256. (upload functionality coming soon)',
};
export const TEXTFIELD_PROPS_STREAM_KEY = {
apiPath: API_STREAM_KEY,
@@ -163,17 +157,19 @@ export const FIELD_PROPS_NSFW = {
apiPath: API_NSFW_SWITCH,
configPath: 'instanceDetails',
label: 'NSFW?',
tip: "Turn this ON if you plan to steam explicit or adult content. You may want to respectfully set this flag so that unexpecting eyes won't accidentally see it from the Directory.",
tip:
"Turn this ON if you plan to steam explicit or adult content. You may want to respectfully set this flag so that unexpecting eyes won't accidentally see it from the Directory.",
};
export const FIELD_PROPS_YP = {
apiPath: API_YP_SWITCH,
configPath: 'yp',
label: 'Display in the Owncast Directory?',
tip: 'Turn this ON if you want to show up in the Owncast directory at https://directory.owncast.online.',
tip:
'Turn this ON if you want to show up in the Owncast directory at https://directory.owncast.online.',
};
export const DEFAULT_VARIANT_STATE:VideoVariant = {
export const DEFAULT_VARIANT_STATE: VideoVariant = {
framerate: 24,
videoPassthrough: false,
videoBitrate: 800,
@@ -182,7 +178,7 @@ export const DEFAULT_VARIANT_STATE:VideoVariant = {
cpuUsageLevel: 3,
};
export const DEFAULT_SOCIAL_HANDLE:SocialHandle = {
export const DEFAULT_SOCIAL_HANDLE: SocialHandle = {
url: '',
platform: '',
};

View File

@@ -1,5 +1,5 @@
import React, { useContext, useState, useEffect } from 'react';
import { Typography, Slider, } from 'antd';
import { Typography, Slider } from 'antd';
import { ServerStatusContext } from '../../../utils/server-status-context';
const { Title } = Typography;
@@ -12,8 +12,7 @@ const SLIDER_MARKS = {
5: 'highest',
};
export default function CPUUsageSelector({defaultValue, onChange}) {
export default function CPUUsageSelector({ defaultValue, onChange }) {
const [selectedOption, setSelectedOption] = useState(null);
const serverStatusData = useContext(ServerStatusContext);
@@ -27,21 +26,20 @@ export default function CPUUsageSelector({defaultValue, onChange}) {
useEffect(() => {
setSelectedOption(defaultValue);
}, [videoSettings]);
const handleChange = value => {
setSelectedOption(value);
onChange(value);
setSelectedOption(value);
onChange(value);
};
return (
<div className="module-container config-video-segements-conatiner">
<Title level={3}>CPU Usage</Title>
<p>
There are trade-offs when considering CPU usage blah blah more wording here.
</p>
<br /><br />
<p>There are trade-offs when considering CPU usage blah blah more wording here.</p>
<br />
<br />
<div className="segment-slider">
<Slider
<Slider
onChange={handleChange}
min={1}
max={Object.keys(SLIDER_MARKS).length}
@@ -52,4 +50,4 @@ export default function CPUUsageSelector({defaultValue, onChange}) {
</div>
</div>
);
}
}

View File

@@ -19,7 +19,6 @@ export default function EditYPDetails() {
const { nsfw } = instanceDetails;
const { enabled, instanceUrl } = yp;
useEffect(() => {
setFormDataValues({
...yp,
@@ -35,10 +34,21 @@ export default function EditYPDetails() {
return (
<div className="config-directory-details-form">
<Title level={3}>Owncast Directory Settings</Title>
<p>Would you like to appear in the <a href="https://directory.owncast.online" target="_blank" rel="noreferrer"><strong>Owncast Directory</strong></a>?</p>
<p style={{ backgroundColor: 'black', fontSize: '.75rem', padding: '5px' }}><em>NOTE: You will need to have a URL specified in the <code>Instance URL</code> field to be able to use this.</em></p>
<p>
Would you like to appear in the{' '}
<a href="https://directory.owncast.online" target="_blank" rel="noreferrer">
<strong>Owncast Directory</strong>
</a>
?
</p>
<p style={{ backgroundColor: 'black', fontSize: '.75rem', padding: '5px' }}>
<em>
NOTE: You will need to have a URL specified in the <code>Instance URL</code> field to be
able to use this.
</em>
</p>
<div className="config-yp-container">
<ToggleSwitch
@@ -53,9 +63,7 @@ export default function EditYPDetails() {
checked={formDataValues.nsfw}
disabled={!hasInstanceUrl}
/>
</div>
</div>
</div>
);
);
}

View File

@@ -1,8 +1,20 @@
import React, { useState, useContext, useEffect } from 'react';
import TextFieldWithSubmit, { TEXTFIELD_TYPE_TEXTAREA, TEXTFIELD_TYPE_URL } from './form-textfield-with-submit';
import TextFieldWithSubmit, {
TEXTFIELD_TYPE_TEXTAREA,
TEXTFIELD_TYPE_URL,
} from './form-textfield-with-submit';
import { ServerStatusContext } from '../../../utils/server-status-context';
import { postConfigUpdateToAPI, TEXTFIELD_PROPS_USERNAME, TEXTFIELD_PROPS_INSTANCE_URL, TEXTFIELD_PROPS_SERVER_TITLE, TEXTFIELD_PROPS_STREAM_TITLE, TEXTFIELD_PROPS_SERVER_SUMMARY, TEXTFIELD_PROPS_LOGO, API_YP_SWITCH } from './constants';
import {
postConfigUpdateToAPI,
TEXTFIELD_PROPS_USERNAME,
TEXTFIELD_PROPS_INSTANCE_URL,
TEXTFIELD_PROPS_SERVER_TITLE,
TEXTFIELD_PROPS_STREAM_TITLE,
TEXTFIELD_PROPS_SERVER_SUMMARY,
TEXTFIELD_PROPS_LOGO,
API_YP_SWITCH,
} from './constants';
import configStyles from '../../../styles/config-pages.module.scss';
import { UpdateArgs } from '../../../types/config-section';
@@ -35,16 +47,16 @@ export default function EditInstanceDetails() {
});
}
}
}
};
const handleFieldChange = ({ fieldName, value }: UpdateArgs) => {
setFormDataValues({
...formDataValues,
[fieldName]: value,
});
}
};
return (
return (
<div className={configStyles.publicDetailsContainer}>
<div className={configStyles.textFieldsSection}>
<TextFieldWithSubmit
@@ -56,7 +68,7 @@ export default function EditInstanceDetails() {
onChange={handleFieldChange}
onSubmit={handleSubmitInstanceUrl}
/>
<TextFieldWithSubmit
fieldName="title"
{...TEXTFIELD_PROPS_SERVER_TITLE}
@@ -94,8 +106,6 @@ export default function EditInstanceDetails() {
onChange={handleFieldChange}
/>
</div>
</div>
);
</div>
);
}

View File

@@ -6,7 +6,12 @@ import { TEXTFIELD_TYPE_NUMBER, TEXTFIELD_TYPE_PASSWORD } from './form-textfield
import TextFieldWithSubmit from './form-textfield-with-submit';
import { ServerStatusContext } from '../../../utils/server-status-context';
import { TEXTFIELD_PROPS_FFMPEG, TEXTFIELD_PROPS_RTMP_PORT, TEXTFIELD_PROPS_STREAM_KEY, TEXTFIELD_PROPS_WEB_PORT, } from './constants';
import {
TEXTFIELD_PROPS_FFMPEG,
TEXTFIELD_PROPS_RTMP_PORT,
TEXTFIELD_PROPS_STREAM_KEY,
TEXTFIELD_PROPS_WEB_PORT,
} from './constants';
import configStyles from '../../../styles/config-pages.module.scss';
import { UpdateArgs } from '../../../types/config-section';
@@ -18,13 +23,16 @@ export default function EditInstanceDetails() {
const { streamKey, ffmpegPath, rtmpServerPort, webServerPort } = serverConfig;
const [copyIsVisible, setCopyVisible] = useState(false);
const [copyIsVisible, setCopyVisible] = useState(false);
const COPY_TOOLTIP_TIMEOUT = 3000;
useEffect(() => {
setFormDataValues({
streamKey, ffmpegPath, rtmpServerPort, webServerPort
streamKey,
ffmpegPath,
rtmpServerPort,
webServerPort,
});
}, [serverConfig]);
@@ -37,26 +45,25 @@ export default function EditInstanceDetails() {
...formDataValues,
[fieldName]: value,
});
}
};
function generateStreamKey () {
function generateStreamKey() {
let key = '';
for (let i = 0; i < 3; i+=1) {
for (let i = 0; i < 3; i += 1) {
key += Math.random().toString(36).substring(2);
}
handleFieldChange({ fieldName: 'streamKey', value: key });
}
function copyStreamKey () {
navigator.clipboard.writeText(formDataValues.streamKey)
.then(() => {
setCopyVisible(true);
setTimeout(() => setCopyVisible(false), COPY_TOOLTIP_TIMEOUT);
});
function copyStreamKey() {
navigator.clipboard.writeText(formDataValues.streamKey).then(() => {
setCopyVisible(true);
setTimeout(() => setCopyVisible(false), COPY_TOOLTIP_TIMEOUT);
});
}
return (
return (
<div className={configStyles.publicDetailsContainer}>
<div className={configStyles.textFieldsSection}>
<TextFieldWithSubmit
@@ -68,25 +75,14 @@ export default function EditInstanceDetails() {
onChange={handleFieldChange}
/>
<div>
<span style={{fontSize: '0.75em', color: '#ff7777', marginRight: '0.5em'}}>
Save this key somewhere safe,
you will need it to stream or login to the admin dashboard!
<span style={{ fontSize: '0.75em', color: '#ff7777', marginRight: '0.5em' }}>
Save this key somewhere safe, you will need it to stream or login to the admin
dashboard!
</span>
<Tooltip className="copy-tooltip"
title="Copied!"
trigger=""
visible={copyIsVisible}>
<Button type="primary"
icon={<CopyOutlined />}
size="small"
onClick={copyStreamKey}
/>
<Tooltip className="copy-tooltip" title="Copied!" trigger="" visible={copyIsVisible}>
<Button type="primary" icon={<CopyOutlined />} size="small" onClick={copyStreamKey} />
</Tooltip>
<Button type="primary"
icon={<RedoOutlined />}
size="small"
onClick={generateStreamKey}
/>
<Button type="primary" icon={<RedoOutlined />} size="small" onClick={generateStreamKey} />
</div>
<TextFieldWithSubmit
fieldName="ffmpegPath"
@@ -112,8 +108,6 @@ export default function EditInstanceDetails() {
onChange={handleFieldChange}
/>
</div>
</div>
);
</div>
);
}

View File

@@ -5,7 +5,14 @@ import { DeleteOutlined } from '@ant-design/icons';
import SocialDropdown from './social-icons-dropdown';
import { fetchData, NEXT_PUBLIC_API_HOST, SOCIAL_PLATFORMS_LIST } from '../../../utils/apis';
import { ServerStatusContext } from '../../../utils/server-status-context';
import { API_SOCIAL_HANDLES, postConfigUpdateToAPI, RESET_TIMEOUT, SUCCESS_STATES, DEFAULT_SOCIAL_HANDLE, OTHER_SOCIAL_HANDLE_OPTION } from './constants';
import {
API_SOCIAL_HANDLES,
postConfigUpdateToAPI,
RESET_TIMEOUT,
SUCCESS_STATES,
DEFAULT_SOCIAL_HANDLE,
OTHER_SOCIAL_HANDLE_OPTION,
} from './constants';
import { SocialHandle } from '../../../types/config-section';
import { isValidUrl } from '../../../utils/urls';
@@ -21,7 +28,7 @@ export default function EditSocialLinks() {
const [displayOther, setDisplayOther] = useState(false);
const [modalProcessing, setModalProcessing] = useState(false);
const [editId, setEditId] = useState(-1);
// current data inside modal
const [modalDataState, setModalDataState] = useState(DEFAULT_SOCIAL_HANDLE);
@@ -44,15 +51,15 @@ export default function EditSocialLinks() {
...result[item],
}));
setAvailableIconsList(list);
} catch (error) {
console.log(error)
console.log(error);
// do nothing
}
};
const selectedOther = modalDataState.platform !== '' && !availableIconsList.find(item => item.key === modalDataState.platform);
const selectedOther =
modalDataState.platform !== '' &&
!availableIconsList.find(item => item.key === modalDataState.platform);
useEffect(() => {
getAvailableIcons();
@@ -64,7 +71,6 @@ export default function EditSocialLinks() {
}
}, [instanceDetails]);
const resetStates = () => {
setSubmitStatus(null);
setSubmitStatusMessage('');
@@ -76,7 +82,7 @@ export default function EditSocialLinks() {
setEditId(-1);
setDisplayOther(false);
setModalProcessing(false);
setModalDataState({...DEFAULT_SOCIAL_HANDLE});
setModalDataState({ ...DEFAULT_SOCIAL_HANDLE });
};
const handleModalCancel = () => {
@@ -106,7 +112,6 @@ export default function EditSocialLinks() {
const { value } = event.target;
updateModalState('url', value);
};
// posts all the variants at once as an array obj
const postUpdateToAPI = async (postValue: any) => {
@@ -114,7 +119,11 @@ export default function EditSocialLinks() {
apiPath: API_SOCIAL_HANDLES,
data: { value: postValue },
onSuccess: () => {
setFieldInConfigState({ fieldName: 'socialHandles', value: postValue, path: 'instanceDetails' });
setFieldInConfigState({
fieldName: 'socialHandles',
value: postValue,
path: 'instanceDetails',
});
// close modal
setModalProcessing(false);
@@ -132,15 +141,12 @@ export default function EditSocialLinks() {
});
};
// on Ok, send all of dataState to api
// show loading
// close modal when api is done
const handleModalOk = () => {
setModalProcessing(true);
const postData = currentSocialHandles.length ? [
...currentSocialHandles,
]: [];
setModalProcessing(true);
const postData = currentSocialHandles.length ? [...currentSocialHandles] : [];
if (editId === -1) {
postData.push(modalDataState);
} else {
@@ -150,23 +156,21 @@ export default function EditSocialLinks() {
};
const handleDeleteItem = index => {
const postData = [
...currentSocialHandles,
];
const postData = [...currentSocialHandles];
postData.splice(index, 1);
postUpdateToAPI(postData);
};
const socialHandlesColumns: ColumnsType<SocialHandle> = [
const socialHandlesColumns: ColumnsType<SocialHandle> = [
{
title: "#",
dataIndex: "key",
key: "key"
title: '#',
dataIndex: 'key',
key: 'key',
},
{
title: "Platform",
dataIndex: "platform",
key: "platform",
title: 'Platform',
dataIndex: 'platform',
key: 'platform',
render: (platform: string) => {
const platformInfo = availableIconsList.find(item => item.key === platform);
if (!platformInfo) {
@@ -185,9 +189,9 @@ export default function EditSocialLinks() {
},
{
title: "Url Link",
dataIndex: "url",
key: "url",
title: 'Url Link',
dataIndex: 'url',
key: 'url',
},
{
title: '',
@@ -196,28 +200,31 @@ export default function EditSocialLinks() {
render: (data, record, index) => {
return (
<span className="actions">
<Button type="primary" size="small" onClick={() => {
setEditId(index);
setModalDataState({...currentSocialHandles[index]});
setDisplayModal(true);
}}>
<Button
type="primary"
size="small"
onClick={() => {
setEditId(index);
setModalDataState({ ...currentSocialHandles[index] });
setDisplayModal(true);
}}
>
Edit
</Button>
<Button
className="delete-button"
icon={<DeleteOutlined />}
size="small"
onClick={() => handleDeleteItem(index)}
/>
onClick={() => handleDeleteItem(index)}
/>
</span>
)},
);
},
];
const {
icon: newStatusIcon = null,
message: newStatusMessage = '',
} = SUCCESS_STATES[submitStatus] || {};
},
];
const { icon: newStatusIcon = null, message: newStatusMessage = '' } =
SUCCESS_STATES[submitStatus] || {};
const statusMessage = (
<div className={`status-message ${submitStatus || ''}`}>
{newStatusIcon} {newStatusMessage} {submitStatusMessage}
@@ -225,9 +232,8 @@ export default function EditSocialLinks() {
);
const okButtonProps = {
disabled: !isValidUrl(modalDataState.url)
};
disabled: !isValidUrl(modalDataState.url),
};
return (
<div className={configStyles.socialLinksEditor}>
@@ -258,21 +264,17 @@ export default function EditSocialLinks() {
selectedOption={selectedOther ? OTHER_SOCIAL_HANDLE_OPTION : modalDataState.platform}
onSelected={handleDropdownSelect}
/>
{
displayOther
? (
<>
<Input
placeholder="Other"
defaultValue={modalDataState.platform}
onChange={handleOtherNameChange}
/>
<br/>
</>
) : null
}
<br/>
{displayOther ? (
<>
<Input
placeholder="Other"
defaultValue={modalDataState.platform}
onChange={handleOtherNameChange}
/>
<br />
</>
) : null}
<br />
URL
<Input
placeholder="Url to page"
@@ -280,17 +282,18 @@ export default function EditSocialLinks() {
value={modalDataState.url}
onChange={handleUrlChange}
/>
{statusMessage}
</Modal>
<br />
<Button type="primary" onClick={() => {
<Button
type="primary"
onClick={() => {
resetModal();
setDisplayModal(true);
}}>
}}
>
Add a new social link
</Button>
</div>
);
);
}

View File

@@ -6,7 +6,14 @@ import { ServerStatusContext } from '../../../utils/server-status-context';
import { FIELD_PROPS_TAGS, RESET_TIMEOUT, postConfigUpdateToAPI } from './constants';
import TextField from './form-textfield';
import { UpdateArgs } from '../../../types/config-section';
import { createInputStatus, StatusState, STATUS_ERROR, STATUS_PROCESSING, STATUS_SUCCESS, STATUS_WARNING } from '../../../utils/input-statuses';
import {
createInputStatus,
StatusState,
STATUS_ERROR,
STATUS_PROCESSING,
STATUS_SUCCESS,
STATUS_WARNING,
} from '../../../utils/input-statuses';
const { Title } = Typography;
@@ -21,19 +28,14 @@ export default function EditInstanceTags() {
const { instanceDetails } = serverConfig;
const { tags = [] } = instanceDetails;
const {
apiPath,
maxLength,
placeholder,
configPath,
} = FIELD_PROPS_TAGS;
const { apiPath, maxLength, placeholder, configPath } = FIELD_PROPS_TAGS;
let resetTimer = null;
useEffect(() => {
return () => {
clearTimeout(resetTimer);
}
};
}, []);
const resetStates = () => {
@@ -42,7 +44,7 @@ export default function EditInstanceTags() {
setFieldStatus(null);
resetTimer = null;
clearTimeout(resetTimer);
}
};
// posts all the tags at once as an array obj
const postUpdateToAPI = async (postValue: any) => {
@@ -89,7 +91,7 @@ export default function EditInstanceTags() {
// setSubmitStatusMessage('Please enter a tag');
return;
}
}
if (tags.some(tag => tag.toLowerCase() === newTag.toLowerCase())) {
setFieldStatus(createInputStatus(STATUS_WARNING, 'This tag is already used!'));
@@ -106,7 +108,7 @@ export default function EditInstanceTags() {
const updatedTags = [...tags];
updatedTags.splice(index, 1);
postUpdateToAPI(updatedTags);
}
};
// const {
// icon: newStatusIcon = null,
@@ -115,7 +117,6 @@ export default function EditInstanceTags() {
return (
<div className="tag-editor-container">
<Title level={3}>Add Tags</Title>
<p>This is a great way to categorize your Owncast server on the Directory!</p>
@@ -125,7 +126,9 @@ export default function EditInstanceTags() {
handleDeleteTag(index);
};
return (
<Tag closable onClose={handleClose} key={`tag-${tag}-${index}`}>{tag}</Tag>
<Tag closable onClose={handleClose} key={`tag-${tag}-${index}`}>
{tag}
</Tag>
);
})}
</div>

View File

@@ -5,7 +5,13 @@ import { RESET_TIMEOUT, postConfigUpdateToAPI } from './constants';
import { ServerStatusContext } from '../../../utils/server-status-context';
import TextField, { TextFieldProps } from './form-textfield';
import { createInputStatus, StatusState, STATUS_ERROR, STATUS_PROCESSING, STATUS_SUCCESS } from '../../../utils/input-statuses';
import {
createInputStatus,
StatusState,
STATUS_ERROR,
STATUS_PROCESSING,
STATUS_SUCCESS,
} from '../../../utils/input-statuses';
import { UpdateArgs } from '../../../types/config-section';
export const TEXTFIELD_TYPE_TEXT = 'default';
@@ -114,11 +120,11 @@ export default function TextFieldWithSubmit(props: TextFieldWithSubmitProps) {
onSubmit();
}
}
}
};
return (
<div className="textfield-with-submit-container">
<TextField
<TextField
{...textFieldProps}
status={status || fieldStatus}
onSubmit={null}
@@ -126,9 +132,13 @@ export default function TextFieldWithSubmit(props: TextFieldWithSubmitProps) {
onChange={handleChange}
/>
{ hasChanged ? <Button type="primary" size="small" className="submit-button" onClick={handleSubmit}>Update</Button> : null }
{hasChanged ? (
<Button type="primary" size="small" className="submit-button" onClick={handleSubmit}>
Update
</Button>
) : null}
</div>
);
);
}
TextFieldWithSubmit.defaultProps = {

View File

@@ -12,7 +12,7 @@ export const TEXTFIELD_TYPE_URL = 'url';
export interface TextFieldProps {
fieldName: string;
onSubmit?: () => void;
onPressEnter?: () => void;
@@ -30,7 +30,6 @@ export interface TextFieldProps {
onChange?: FieldUpdaterFunc;
}
export default function TextField(props: TextFieldProps) {
const {
className,
@@ -70,11 +69,14 @@ export default function TextField(props: TextFieldProps) {
if (onPressEnter) {
onPressEnter();
}
}
};
// display the appropriate Ant text field
let Field = Input as typeof Input | typeof InputNumber | typeof Input.TextArea | typeof Input.Password;
let Field = Input as
| typeof Input
| typeof InputNumber
| typeof Input.TextArea
| typeof Input.Password;
let fieldProps = {};
if (type === TEXTFIELD_TYPE_TEXTAREA) {
Field = Input.TextArea;
@@ -91,12 +93,11 @@ export default function TextField(props: TextFieldProps) {
fieldProps = {
type: 'number',
min: 1,
max: (10**maxLength) - 1,
max: 10 ** maxLength - 1,
onKeyDown: (e: React.KeyboardEvent) => {
if (e.target.value.length > maxLength - 1 )
e.preventDefault();
if (e.target.value.length > maxLength - 1) e.preventDefault();
return false;
}
},
};
} else if (type === TEXTFIELD_TYPE_URL) {
fieldProps = {
@@ -110,8 +111,10 @@ export default function TextField(props: TextFieldProps) {
return (
<div className={`textfield-container type-${type}`}>
{ required ? <span className="required-label">*</span> : null }
<label htmlFor={fieldId} className="textfield-label">{label}</label>
{required ? <span className="required-label">*</span> : null}
<label htmlFor={fieldId} className="textfield-label">
{label}
</label>
<div className="textfield">
<Field
id={fieldId}
@@ -128,10 +131,10 @@ export default function TextField(props: TextFieldProps) {
/>
</div>
<InfoTip tip={tip} />
{ status ? statusMessage : null }
{ status ? statusIcon : null }
{status ? statusMessage : null}
{status ? statusIcon : null}
</div>
);
);
}
TextField.defaultProps = {

View File

@@ -26,22 +26,14 @@ export default function ToggleSwitch(props: ToggleSwitchProps) {
const serverStatusData = useContext(ServerStatusContext);
const { setFieldInConfigState } = serverStatusData || {};
const {
apiPath,
checked,
configPath = '',
disabled = false,
fieldName,
label,
tip,
} = props;
const { apiPath, checked, configPath = '', disabled = false, fieldName, label, tip } = props;
const resetStates = () => {
setSubmitStatus('');
clearTimeout(resetTimer);
resetTimer = null;
}
};
const handleChange = async (isChecked: boolean) => {
setSubmitStatus('validating');
@@ -58,12 +50,10 @@ export default function ToggleSwitch(props: ToggleSwitchProps) {
},
});
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
}
};
const {
icon: newStatusIcon = null,
message: newStatusMessage = '',
} = SUCCESS_STATES[submitStatus] || {};
const { icon: newStatusIcon = null, message: newStatusMessage = '' } =
SUCCESS_STATES[submitStatus] || {};
return (
<div className="toggleswitch-container">
@@ -74,18 +64,20 @@ export default function ToggleSwitch(props: ToggleSwitchProps) {
onChange={handleChange}
defaultChecked={checked}
checked={checked}
checkedChildren="ON"
checkedChildren="ON"
unCheckedChildren="OFF"
disabled={disabled}
/>
<span className="label">{label} <InfoTip tip={tip} /></span>
<span className="label">
{label} <InfoTip tip={tip} />
</span>
{submitStatus}
</div>
<div className={`status-message ${submitStatus || ''}`}>
{newStatusIcon} {newStatusMessage} {submitStatusMessage}
</div>
</div>
);
);
}
ToggleSwitch.defaultProps = {

View File

@@ -1,10 +1,9 @@
import React from 'react';
import { Select } from "antd";
import { SocialHandleDropdownItem } from "../../../types/config-section";
import { Select } from 'antd';
import { SocialHandleDropdownItem } from '../../../types/config-section';
import { NEXT_PUBLIC_API_HOST } from '../../../utils/apis';
import { OTHER_SOCIAL_HANDLE_OPTION } from './constants';
interface DropdownProps {
iconList: SocialHandleDropdownItem[];
selectedOption: string;
@@ -12,7 +11,6 @@ interface DropdownProps {
}
export default function SocialDropdown({ iconList, selectedOption, onSelected }: DropdownProps) {
const handleSelected = value => {
if (onSelected) {
onSelected(value);
@@ -21,9 +19,16 @@ export default function SocialDropdown({ iconList, selectedOption, onSelected }:
const inititalSelected = selectedOption === '' ? null : selectedOption;
return (
<div className="social-dropdown-container">
<p className="">If you are looking for a platform name not on this list, please select Other and type in your own name. A logo will not be provided.</p>
<p className="">If you DO have a logo, drop it in to the <code>/webroot/img/platformicons</code> directory and update the <code>/socialHandle.go</code> list. Then restart the server and it will show up in the list.</p>
<p className="">
If you are looking for a platform name not on this list, please select Other and type in
your own name. A logo will not be provided.
</p>
<p className="">
If you DO have a logo, drop it in to the <code>/webroot/img/platformicons</code> directory
and update the <code>/socialHandle.go</code> list. Then restart the server and it will show
up in the list.
</p>
<Select
style={{ width: 240 }}
className="social-dropdown"
@@ -33,7 +38,7 @@ export default function SocialDropdown({ iconList, selectedOption, onSelected }:
onSelect={handleSelected}
>
{iconList.map(item => {
const { platform, icon, key } = item;
const { platform, icon, key } = item;
return (
<Select.Option className="social-option" key={`platform-${key}`} value={key}>
<span className="option-icon">
@@ -42,9 +47,12 @@ export default function SocialDropdown({ iconList, selectedOption, onSelected }:
<span className="option-label">{platform}</span>
</Select.Option>
);
})
}
<Select.Option className="social-option" key={`platform-${OTHER_SOCIAL_HANDLE_OPTION}`} value={OTHER_SOCIAL_HANDLE_OPTION}>
})}
<Select.Option
className="social-option"
key={`platform-${OTHER_SOCIAL_HANDLE_OPTION}`}
value={OTHER_SOCIAL_HANDLE_OPTION}
>
Other...
</Select.Option>
</Select>

View File

@@ -1,7 +1,12 @@
import React, { useContext, useState, useEffect } from 'react';
import { Typography, Slider, } from 'antd';
import { Typography, Slider } from 'antd';
import { ServerStatusContext } from '../../../utils/server-status-context';
import { API_VIDEO_SEGMENTS, SUCCESS_STATES, RESET_TIMEOUT,postConfigUpdateToAPI } from './constants';
import {
API_VIDEO_SEGMENTS,
SUCCESS_STATES,
RESET_TIMEOUT,
postConfigUpdateToAPI,
} from './constants';
const { Title } = Typography;
@@ -28,9 +33,7 @@ interface SegmentToolTipProps {
}
function SegmentToolTip({ value }: SegmentToolTipProps) {
return (
<span className="segment-tip">{value}</span>
);
return <span className="segment-tip">{value}</span>;
}
export default function VideoLatency() {
@@ -57,8 +60,8 @@ export default function VideoLatency() {
setSubmitStatusMessage('');
resetTimer = null;
clearTimeout(resetTimer);
}
};
// posts all the variants at once as an array obj
const postUpdateToAPI = async (postValue: any) => {
await postConfigUpdateToAPI({
@@ -66,9 +69,9 @@ export default function VideoLatency() {
data: { value: postValue },
onSuccess: () => {
setFieldInConfigState({
fieldName: 'latencyLevel',
value: postValue,
path: 'videoSettings'
fieldName: 'latencyLevel',
value: postValue,
path: 'videoSettings',
});
setSubmitStatus('success');
@@ -83,17 +86,15 @@ export default function VideoLatency() {
});
};
const {
icon: newStatusIcon = null,
message: newStatusMessage = '',
} = SUCCESS_STATES[submitStatus] || {};
const { icon: newStatusIcon = null, message: newStatusMessage = '' } =
SUCCESS_STATES[submitStatus] || {};
const statusMessage = (
<div className={`status-message ${submitStatus || ''}`}>
{newStatusIcon} {newStatusMessage} {submitStatusMessage}
</div>
);
const handleChange = value => {
postUpdateToAPI(value);
};
@@ -102,11 +103,13 @@ export default function VideoLatency() {
<div className="module-container config-video-segements-conatiner">
<Title level={3}>Latency Buffer</Title>
<p>
There are trade-offs when cosidering video latency and reliability. Blah blah .. better wording here needed.
There are trade-offs when cosidering video latency and reliability. Blah blah .. better
wording here needed.
</p>
<br /><br />
<br />
<br />
<div className="segment-slider">
<Slider
<Slider
tooltipVisible
tipFormatter={value => <SegmentToolTip value={SLIDER_COMMENTS[value]} />}
onChange={handleChange}
@@ -120,4 +123,4 @@ export default function VideoLatency() {
{statusMessage}
</div>
);
}
}

View File

@@ -32,13 +32,13 @@ const VIDEO_VARIANT_DEFAULTS = {
defaultValue: 800,
unit: 'kbps',
incrementBy: 100,
tip: 'nothing to see here'
tip: 'nothing to see here',
},
videoPassthrough: {
tip: 'If No is selected, then you should set your desired Video Bitrate.'
tip: 'If No is selected, then you should set your desired Video Bitrate.',
},
audioPassthrough: {
tip: 'If No is selected, then you should set your desired Audio Bitrate.'
tip: 'If No is selected, then you should set your desired Audio Bitrate.',
},
};
@@ -47,8 +47,10 @@ interface VideoVariantFormProps {
onUpdateField: FieldUpdaterFunc;
}
export default function VideoVariantForm({ dataState = DEFAULT_VARIANT_STATE, onUpdateField }: VideoVariantFormProps) {
export default function VideoVariantForm({
dataState = DEFAULT_VARIANT_STATE,
onUpdateField,
}: VideoVariantFormProps) {
const handleFramerateChange = (value: number) => {
onUpdateField({ fieldName: 'framerate', value });
};
@@ -65,8 +67,8 @@ export default function VideoVariantForm({ dataState = DEFAULT_VARIANT_STATE, on
onUpdateField({ fieldName: 'videoPassthrough', value });
};
const handleVideoCpuUsageLevelChange = (value: number) => {
onUpdateField({ fieldName: 'cpuUsageLevel', value })
}
onUpdateField({ fieldName: 'cpuUsageLevel', value });
};
const framerateDefaults = VIDEO_VARIANT_DEFAULTS.framerate;
const framerateMin = framerateDefaults.min;
@@ -91,24 +93,27 @@ export default function VideoVariantForm({ dataState = DEFAULT_VARIANT_STATE, on
return (
<div className="variant-form">
<div className="section-intro">
Say a thing here about how this all works.
Read more <a href="https://owncast.online/docs/configuration/">here</a>.
<br /><br />
Say a thing here about how this all works. Read more{' '}
<a href="https://owncast.online/docs/configuration/">here</a>.
<br />
<br />
</div>
{/* ENCODER PRESET FIELD */}
<div className="field">
<div className="form-component">
<CPUUsageSelector defaultValue={dataState.cpuUsageLevel} onChange={handleVideoCpuUsageLevelChange} />
{selectedPresetNote ? <span className="selected-value-note">{selectedPresetNote}</span> : null }
<CPUUsageSelector
defaultValue={dataState.cpuUsageLevel}
onChange={handleVideoCpuUsageLevelChange}
/>
{selectedPresetNote ? (
<span className="selected-value-note">{selectedPresetNote}</span>
) : null}
</div>
</div>
{/* VIDEO PASSTHROUGH FIELD */}
<div style={{ display: 'none'}}>
<div style={{ display: 'none' }}>
<div className="field">
<p className="label">
<InfoTip tip={VIDEO_VARIANT_DEFAULTS.videoPassthrough.tip} />
@@ -147,19 +152,20 @@ export default function VideoVariantForm({ dataState = DEFAULT_VARIANT_STATE, on
[videoBRMax]: `${videoBRMax} ${videoBRUnit}`,
}}
/>
{selectedVideoBRnote ? <span className="selected-value-note">{selectedVideoBRnote}</span> : null }
{selectedVideoBRnote ? (
<span className="selected-value-note">{selectedVideoBRnote}</span>
) : null}
</div>
</div>
<br />
<br />
<br />
<br />
<br /><br /><br /><br />
<Collapse>
<Panel header="Advanced Settings" key="1">
<div className="section-intro">
Touch if you dare.
</div>
<div className="section-intro">Touch if you dare.</div>
{/* FRAME RATE FIELD */}
<div className="field">
@@ -182,8 +188,9 @@ export default function VideoVariantForm({ dataState = DEFAULT_VARIANT_STATE, on
[framerateMax]: `${framerateMax} ${framerateUnit}`,
}}
/>
{selectedFramerateNote ? <span className="selected-value-note">{selectedFramerateNote}</span> : null }
{selectedFramerateNote ? (
<span className="selected-value-note">{selectedFramerateNote}</span>
) : null}
</div>
</div>
@@ -203,7 +210,7 @@ export default function VideoVariantForm({ dataState = DEFAULT_VARIANT_STATE, on
checkedChildren="Yes"
unCheckedChildren="No"
/>
{dataState.audioPassthrough ? <span className="note">Same as source</span>: null}
{dataState.audioPassthrough ? <span className="note">Same as source</span> : null}
</div>
</div>
@@ -230,13 +237,13 @@ export default function VideoVariantForm({ dataState = DEFAULT_VARIANT_STATE, on
}}
/>
{selectedAudioBRnote ? <span className="selected-value-note">{selectedAudioBRnote}</span> : null }
{selectedAudioBRnote ? (
<span className="selected-value-note">{selectedAudioBRnote}</span>
) : null}
</div>
</div>
</Panel>
</Collapse>
</div>
);
}
}

View File

@@ -8,7 +8,13 @@ import { DeleteOutlined } from '@ant-design/icons';
import { ServerStatusContext } from '../../../utils/server-status-context';
import { UpdateArgs, VideoVariant } from '../../../types/config-section';
import VideoVariantForm from './video-variant-form';
import { API_VIDEO_VARIANTS, DEFAULT_VARIANT_STATE, SUCCESS_STATES, RESET_TIMEOUT, postConfigUpdateToAPI } from './constants';
import {
API_VIDEO_VARIANTS,
DEFAULT_VARIANT_STATE,
SUCCESS_STATES,
RESET_TIMEOUT,
postConfigUpdateToAPI,
} from './constants';
const { Title } = Typography;
@@ -19,7 +25,7 @@ export default function CurrentVariantsTable() {
// current data inside modal
const [modalDataState, setModalDataState] = useState(DEFAULT_VARIANT_STATE);
const [submitStatus, setSubmitStatus] = useState(null);
const [submitStatusMessage, setSubmitStatusMessage] = useState('');
@@ -39,13 +45,13 @@ export default function CurrentVariantsTable() {
setSubmitStatusMessage('');
resetTimer = null;
clearTimeout(resetTimer);
}
};
const handleModalCancel = () => {
setDisplayModal(false);
setEditId(-1);
setModalDataState(DEFAULT_VARIANT_STATE);
}
};
// posts all the variants at once as an array obj
const postUpdateToAPI = async (postValue: any) => {
@@ -53,7 +59,11 @@ export default function CurrentVariantsTable() {
apiPath: API_VIDEO_VARIANTS,
data: { value: postValue },
onSuccess: () => {
setFieldInConfigState({ fieldName: 'videoQualityVariants', value: postValue, path: 'videoSettings' });
setFieldInConfigState({
fieldName: 'videoQualityVariants',
value: postValue,
path: 'videoSettings',
});
// close modal
setModalProcessing(false);
@@ -76,10 +86,8 @@ export default function CurrentVariantsTable() {
// close modal when api is done
const handleModalOk = () => {
setModalProcessing(true);
const postData = [
...videoQualityVariants,
];
const postData = [...videoQualityVariants];
if (editId === -1) {
postData.push(modalDataState);
} else {
@@ -89,11 +97,9 @@ export default function CurrentVariantsTable() {
};
const handleDeleteVariant = index => {
const postData = [
...videoQualityVariants,
];
const postData = [...videoQualityVariants];
postData.splice(index, 1);
postUpdateToAPI(postData)
postUpdateToAPI(postData);
};
const handleUpdateField = ({ fieldName, value }: UpdateArgs) => {
@@ -101,41 +107,37 @@ export default function CurrentVariantsTable() {
...modalDataState,
[fieldName]: value,
});
}
};
const { icon: newStatusIcon = null, message: newStatusMessage = '' } =
SUCCESS_STATES[submitStatus] || {};
const {
icon: newStatusIcon = null,
message: newStatusMessage = '',
} = SUCCESS_STATES[submitStatus] || {};
const cpuUsageLevelLabelMap = {
1: 'lowest',
2: 'low',
3: 'medium',
4: 'high',
5: 'highest'
5: 'highest',
};
const videoQualityColumns: ColumnsType<VideoVariant> = [
const videoQualityColumns: ColumnsType<VideoVariant> = [
{
title: "#",
dataIndex: "key",
key: "key"
title: '#',
dataIndex: 'key',
key: 'key',
},
{
title: "Video bitrate",
dataIndex: "videoBitrate",
key: "videoBitrate",
render: (bitrate: number) =>
!bitrate ? "Same as source" : `${bitrate} kbps`,
title: 'Video bitrate',
dataIndex: 'videoBitrate',
key: 'videoBitrate',
render: (bitrate: number) => (!bitrate ? 'Same as source' : `${bitrate} kbps`),
},
{
title: "CPU Usage",
dataIndex: "cpuUsageLevel",
key: "cpuUsageLevel",
render: (level: string) =>
!level ? "n/a" : cpuUsageLevelLabelMap[level],
title: 'CPU Usage',
dataIndex: 'cpuUsageLevel',
key: 'cpuUsageLevel',
render: (level: string) => (!level ? 'n/a' : cpuUsageLevelLabelMap[level]),
},
{
title: '',
@@ -145,11 +147,15 @@ export default function CurrentVariantsTable() {
const index = data.key - 1;
return (
<span className="actions">
<Button type="primary" size="small" onClick={() => {
setEditId(index);
setModalDataState(videoQualityVariants[index]);
setDisplayModal(true);
}}>
<Button
type="primary"
size="small"
onClick={() => {
setEditId(index);
setModalDataState(videoQualityVariants[index]);
setDisplayModal(true);
}}
>
Edit
</Button>
<Button
@@ -160,11 +166,12 @@ export default function CurrentVariantsTable() {
onClick={() => {
handleDeleteVariant(index);
}}
/>
/>
</span>
)},
);
},
];
},
];
const statusMessage = (
<div className={`status-message ${submitStatus || ''}`}>
@@ -172,12 +179,15 @@ export default function CurrentVariantsTable() {
</div>
);
const videoQualityVariantData = videoQualityVariants.map((variant, index) => ({ key: index + 1, ...variant }));
const videoQualityVariantData = videoQualityVariants.map((variant, index) => ({
key: index + 1,
...variant,
}));
return (
<>
<Title level={3}>Current Variants</Title>
{statusMessage}
<Table
@@ -195,22 +205,21 @@ export default function CurrentVariantsTable() {
onCancel={handleModalCancel}
confirmLoading={modalProcessing}
>
<VideoVariantForm
dataState={{...modalDataState}}
onUpdateField={handleUpdateField}
/>
<VideoVariantForm dataState={{ ...modalDataState }} onUpdateField={handleUpdateField} />
{statusMessage}
</Modal>
<br />
<Button type="primary" onClick={() => {
<Button
type="primary"
onClick={() => {
setEditId(-1);
setModalDataState(DEFAULT_VARIANT_STATE);
setDisplayModal(true);
}}>
}}
>
Add a new variant
</Button>
</>
);
}
}