Merge branch '0.0.6' of github.com:owncast/owncast-admin into 0.0.6

This commit is contained in:
gingervitis 2021-02-12 23:57:01 -08:00
commit 5b88b068ed
40 changed files with 1150 additions and 552 deletions

View file

@ -20,4 +20,3 @@ jobs:
config-path: '.eslintrc.js'
ignore-path: '.eslintignore'
extensions: 'ts,tsx,js,jsx'
extra-args: '--max-warnings=0'

View file

@ -19,8 +19,11 @@ const TOOLTIPS = {
4: 'high',
5: 'highest',
};
export default function CPUUsageSelector({ defaultValue, onChange }) {
interface Props {
defaultValue: number;
onChange: (arg: number) => void;
}
export default function CPUUsageSelector({ defaultValue, onChange }: Props) {
const [selectedOption, setSelectedOption] = useState(null);
const serverStatusData = useContext(ServerStatusContext);
@ -42,10 +45,14 @@ export default function CPUUsageSelector({ defaultValue, onChange }) {
return (
<div className="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 />
<Title level={3} className="section-title">
CPU Usage
</Title>
<p className="description">
There are trade-offs when considering CPU usage blah blah more wording here.
</p>
<br />
<div className="segment-slider-container">
<Slider
tipFormatter={value => TOOLTIPS[value]}

View file

@ -33,9 +33,11 @@ export default function EditYPDetails() {
}
return (
<div className="config-directory-details-form">
<Title level={3}>Owncast Directory Settings</Title>
<Title level={3} className="section-title">
Owncast Directory Settings
</Title>
<p>
<p className="description">
Would you like to appear in the{' '}
<a href="https://directory.owncast.online" target="_blank" rel="noreferrer">
<strong>Owncast Directory</strong>

View file

@ -1,4 +1,6 @@
import React, { useState, useContext, useEffect } from 'react';
import { Typography } from 'antd';
import TextFieldWithSubmit, {
TEXTFIELD_TYPE_TEXTAREA,
TEXTFIELD_TYPE_URL,
@ -12,9 +14,14 @@ import {
TEXTFIELD_PROPS_SERVER_SUMMARY,
TEXTFIELD_PROPS_LOGO,
API_YP_SWITCH,
FIELD_PROPS_YP,
FIELD_PROPS_NSFW,
} from '../../utils/config-constants';
import { UpdateArgs } from '../../types/config-section';
import ToggleSwitch from './form-toggleswitch-with-submit';
const { Title } = Typography;
export default function EditInstanceDetails() {
const [formDataValues, setFormDataValues] = useState(null);
@ -22,6 +29,7 @@ export default function EditInstanceDetails() {
const { serverConfig } = serverStatusData || {};
const { instanceDetails, yp } = serverConfig;
const { instanceUrl } = yp;
useEffect(() => {
setFormDataValues({
@ -53,40 +61,74 @@ export default function EditInstanceDetails() {
});
};
return (
<div className={`publicDetailsContainer`}>
<div className={`textFieldsSection`}>
<TextFieldWithSubmit
fieldName="instanceUrl"
{...TEXTFIELD_PROPS_INSTANCE_URL}
value={formDataValues.instanceUrl}
initialValue={yp.instanceUrl}
type={TEXTFIELD_TYPE_URL}
onChange={handleFieldChange}
onSubmit={handleSubmitInstanceUrl}
/>
const hasInstanceUrl = instanceUrl !== '';
<TextFieldWithSubmit
fieldName="name"
{...TEXTFIELD_PROPS_SERVER_NAME}
value={formDataValues.name}
initialValue={instanceDetails.name}
onChange={handleFieldChange}
return (
<div className="edit-general-settings">
<Title level={3} className="section-title">
Configure Instance Details
</Title>
<br />
<TextFieldWithSubmit
fieldName="instanceUrl"
{...TEXTFIELD_PROPS_INSTANCE_URL}
value={formDataValues.instanceUrl}
initialValue={yp.instanceUrl}
type={TEXTFIELD_TYPE_URL}
onChange={handleFieldChange}
onSubmit={handleSubmitInstanceUrl}
/>
<TextFieldWithSubmit
fieldName="name"
{...TEXTFIELD_PROPS_SERVER_NAME}
value={formDataValues.name}
initialValue={instanceDetails.name}
onChange={handleFieldChange}
/>
<TextFieldWithSubmit
fieldName="summary"
{...TEXTFIELD_PROPS_SERVER_SUMMARY}
type={TEXTFIELD_TYPE_TEXTAREA}
value={formDataValues.summary}
initialValue={instanceDetails.summary}
onChange={handleFieldChange}
/>
<TextFieldWithSubmit
fieldName="logo"
{...TEXTFIELD_PROPS_LOGO}
value={formDataValues.logo}
initialValue={instanceDetails.logo}
onChange={handleFieldChange}
/>
<br />
<Title level={3} className="section-title">
Owncast Directory Settings
</Title>
<p className="description">
Would you like to appear in the{' '}
<a href="https://directory.owncast.online" target="_blank" rel="noreferrer">
<strong>Owncast Directory</strong>
</a>
?
</p>
<div className="config-yp-container">
<ToggleSwitch
fieldName="enabled"
useSubmit
{...FIELD_PROPS_YP}
checked={formDataValues.enabled}
disabled={!hasInstanceUrl}
/>
<TextFieldWithSubmit
fieldName="summary"
{...TEXTFIELD_PROPS_SERVER_SUMMARY}
type={TEXTFIELD_TYPE_TEXTAREA}
value={formDataValues.summary}
initialValue={instanceDetails.summary}
onChange={handleFieldChange}
/>
<TextFieldWithSubmit
fieldName="logo"
{...TEXTFIELD_PROPS_LOGO}
value={formDataValues.logo}
initialValue={instanceDetails.logo}
onChange={handleFieldChange}
<ToggleSwitch
fieldName="nsfw"
useSubmit
{...FIELD_PROPS_NSFW}
checked={formDataValues.nsfw}
disabled={!hasInstanceUrl}
/>
</div>
</div>

View file

@ -1,33 +1,34 @@
// EDIT CUSTOM DETAILS ON YOUR PAGE
import React, { useState, useEffect, useContext } from 'react';
import { Typography, Button } from 'antd';
import dynamic from 'next/dynamic';
import MarkdownIt from 'markdown-it';
import { ServerStatusContext } from '../utils/server-status-context';
import { ServerStatusContext } from '../../utils/server-status-context';
import {
postConfigUpdateToAPI,
RESET_TIMEOUT,
API_CUSTOM_CONTENT,
} from '../utils/config-constants';
} from '../../utils/config-constants';
import {
createInputStatus,
StatusState,
STATUS_ERROR,
STATUS_PROCESSING,
STATUS_SUCCESS,
} from '../utils/input-statuses';
import 'react-markdown-editor-lite/lib/index.css';
import FormStatusIndicator from '../components/config/form-status-indicator';
} from '../../utils/input-statuses';
import FormStatusIndicator from './form-status-indicator';
const { Title } = Typography;
import 'react-markdown-editor-lite/lib/index.css';
const mdParser = new MarkdownIt(/* Markdown-it options */);
const MdEditor = dynamic(() => import('react-markdown-editor-lite'), {
ssr: false,
});
export default function PageContentEditor() {
const { Title } = Typography;
export default function EditPageContent() {
const [content, setContent] = useState('');
const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
const [hasChanged, setHasChanged] = useState(false);
@ -83,10 +84,12 @@ export default function PageContentEditor() {
}, [instanceDetails]);
return (
<div className="config-page-content-form">
<Title level={2}>Page Content</Title>
<div className="edit-page-content">
<Title level={3} className="section-title">
Custom Page Content
</Title>
<p>
<p className="description">
Edit the content of your page by using simple{' '}
<a href="https://www.markdownguide.org/basic-syntax/">Markdown syntax</a>.
</p>

View file

@ -1,7 +1,6 @@
import React, { useState, useContext, useEffect } from 'react';
import { Button, Tooltip, Collapse, Popconfirm } from 'antd';
import { Button, Tooltip, Collapse } from 'antd';
import { CopyOutlined, RedoOutlined } from '@ant-design/icons';
const { Panel } = Collapse;
import { TEXTFIELD_TYPE_NUMBER, TEXTFIELD_TYPE_PASSWORD } from './form-textfield';
import TextFieldWithSubmit from './form-textfield-with-submit';
@ -15,9 +14,11 @@ import {
TEXTFIELD_PROPS_STREAM_KEY,
TEXTFIELD_PROPS_WEB_PORT,
} from '../../utils/config-constants';
import { fetchData, API_YP_RESET } from '../../utils/apis';
import { UpdateArgs } from '../../types/config-section';
import ResetYP from './reset-yp';
const { Panel } = Collapse;
export default function EditInstanceDetails() {
const [formDataValues, setFormDataValues] = useState(null);
@ -68,41 +69,6 @@ export default function EditInstanceDetails() {
}
};
const resetDirectoryRegistration = async () => {
try {
await fetchData(API_YP_RESET);
setMessage('');
} catch (error) {
alert(error);
}
};
function ResetYP() {
// TODO: Uncomment this after it's styled.
// if (yp.enabled) {
return (
<div className="field-container">
Reset Directory:
<Popconfirm
placement="topLeft"
title={'Are you sure you want to reset your connection to the Owncast directory?'}
onConfirm={resetDirectoryRegistration}
okText="Yes"
cancelText="No"
>
<Button>Reset Directory Connection</Button>
</Popconfirm>
<p>
If you are experiencing issues with your listing on the Owncast Directory and were asked
to "reset" your connection to the service, you can do that here. The next time you go live
it will try and re-register your server with the directory from scratch.
</p>
</div>
);
// }
// return null;
}
function generateStreamKey() {
let key = '';
for (let i = 0; i < 3; i += 1) {
@ -172,13 +138,14 @@ export default function EditInstanceDetails() {
onChange={handleFieldChange}
onSubmit={showConfigurationRestartMessage}
/>
<Collapse>
<Panel header="Advanced Settings" key="1">
<div className="form-fields">
{yp.enabled && (
<Collapse className="advanced-settings">
<Panel header="Advanced Settings" key="1">
<ResetYP />
</div>
</Panel>
</Collapse>
</Panel>
</Collapse>
)}
</div>
);
}

View file

@ -165,43 +165,36 @@ export default function EditSocialLinks() {
const socialHandlesColumns: ColumnsType<SocialHandle> = [
{
title: '#',
dataIndex: 'key',
key: 'key',
},
{
title: 'Platform',
dataIndex: 'platform',
key: 'platform',
render: (platform: string) => {
title: 'Social Link',
dataIndex: '',
key: 'combo',
render: (data, record) => {
const { platform, url } = record;
const platformInfo = availableIconsList.find(item => item.key === platform);
if (!platformInfo) {
return platform;
}
const { icon, platform: platformName } = platformInfo;
return (
<>
<div className="social-handle-cell">
<span className="option-icon">
<img src={`${NEXT_PUBLIC_API_HOST}${icon}`} alt="" className="option-icon" />
</span>
<span className="option-label">{platformName}</span>
</>
<p className="option-label">
<strong>{platformName}</strong>
<span>{url}</span>
</p>
</div>
);
},
},
{
title: 'Url Link',
dataIndex: 'url',
key: 'url',
},
{
title: '',
dataIndex: '',
key: 'edit',
render: (data, record, index) => {
return (
<span className="actions">
<div className="actions">
<Button
type="primary"
size="small"
@ -219,7 +212,7 @@ export default function EditSocialLinks() {
size="small"
onClick={() => handleDeleteItem(index)}
/>
</span>
</div>
);
},
},
@ -231,12 +224,17 @@ export default function EditSocialLinks() {
return (
<div className="social-links-edit-container">
<p>Add all your social media handles and links to your other profiles here.</p>
<Title level={3} className="section-title">
Your Social Handles
</Title>
<p className="description">
Add all your social media handles and links to your other profiles here.
</p>
<FormStatusIndicator status={submitStatus} />
<Table
className="dataTable"
className="social-handles-table"
pagination={false}
size="small"
rowKey={record => record.url}

View file

@ -21,6 +21,7 @@ import {
import TextField from './form-textfield';
import FormStatusIndicator from './form-status-indicator';
import { isValidUrl } from '../../utils/urls';
// import ToggleSwitch from './form-toggleswitch-with-submit';
const { Panel } = Collapse;
@ -135,6 +136,7 @@ export default function EditStorage() {
const containerClass = classNames({
'edit-storage-container': true,
'form-module': true,
enabled: shouldDisplayForm,
});
@ -143,6 +145,12 @@ export default function EditStorage() {
return (
<div className={containerClass}>
<div className="enable-switch">
{/* <ToggleSwitch
fieldName="enabled"
label="Storage Enabled"
checked={formDataValues.enabled}
onChange={handleSwitchChange}
/> */}
<Switch
checked={formDataValues.enabled}
defaultChecked={formDataValues.enabled}

View file

@ -21,6 +21,8 @@ import {
const { Title } = Typography;
const TAG_COLOR = '#5a67d8';
export default function EditInstanceTags() {
const [newTagInput, setNewTagInput] = useState<string>('');
const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
@ -100,8 +102,12 @@ 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>
<Title level={3} className="section-title">
Add Tags
</Title>
<p className="description">
This is a great way to categorize your Owncast server on the Directory!
</p>
<div className="tag-current-tags">
{tags.map((tag, index) => {
@ -109,7 +115,7 @@ export default function EditInstanceTags() {
handleDeleteTag(index);
};
return (
<Tag closable onClose={handleClose} key={`tag-${tag}-${index}`}>
<Tag closable onClose={handleClose} color={TAG_COLOR} key={`tag-${tag}-${index}`}>
{tag}
</Tag>
);

View file

@ -120,7 +120,7 @@ export default function TextFieldWithSubmit(props: TextFieldWithSubmitProps) {
onChange={handleChange}
/>
</div>
<div className="textfield-container lower-container">
<div className="formfield-container lower-container">
<p className="label-spacer" />
<div className="lower-content">
<div className="field-tip">{tip}</div>

View file

@ -108,6 +108,7 @@ export default function TextField(props: TextFieldProps) {
const { type: statusType } = status || {};
const containerClass = classNames({
'formfield-container': true,
'textfield-container': true,
[`type-${type}`]: true,
required,
@ -117,7 +118,7 @@ export default function TextField(props: TextFieldProps) {
<div className={containerClass}>
{label ? (
<div className="label-side">
<label htmlFor={fieldId} className="textfield-label">
<label htmlFor={fieldId} className="formfield-label">
{label}
</label>
</div>
@ -140,10 +141,7 @@ export default function TextField(props: TextFieldProps) {
/>
</div>
<FormStatusIndicator status={status} />
<p className="field-tip">
{tip}
{/* <InfoTip tip={tip} /> */}
</p>
<p className="field-tip">{tip}</p>
</div>
</div>
);
@ -151,9 +149,7 @@ export default function TextField(props: TextFieldProps) {
TextField.defaultProps = {
className: '',
// configPath: '',
disabled: false,
// initialValue: '',
label: '',
maxLength: 255,

View file

@ -1,3 +1,6 @@
// This is a wrapper for the Ant Switch component.
// onChange of the switch, it will automatically post a change to the config api.
import React, { useState, useContext } from 'react';
import { Switch } from 'antd';
import {
@ -12,7 +15,6 @@ import FormStatusIndicator from './form-status-indicator';
import { RESET_TIMEOUT, postConfigUpdateToAPI } from '../../utils/config-constants';
import { ServerStatusContext } from '../../utils/server-status-context';
import InfoTip from '../info-tip';
interface ToggleSwitchProps {
apiPath: string;
@ -23,8 +25,9 @@ interface ToggleSwitchProps {
disabled?: boolean;
label?: string;
tip?: string;
useSubmit?: boolean;
onChange?: (arg: boolean) => void;
}
export default function ToggleSwitch(props: ToggleSwitchProps) {
const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
@ -33,7 +36,17 @@ 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,
useSubmit,
onChange,
} = props;
const resetStates = () => {
setSubmitStatus(null);
@ -42,41 +55,52 @@ export default function ToggleSwitch(props: ToggleSwitchProps) {
};
const handleChange = async (isChecked: boolean) => {
setSubmitStatus(createInputStatus(STATUS_PROCESSING));
if (useSubmit) {
setSubmitStatus(createInputStatus(STATUS_PROCESSING));
await postConfigUpdateToAPI({
apiPath,
data: { value: isChecked },
onSuccess: () => {
setFieldInConfigState({ fieldName, value: isChecked, path: configPath });
setSubmitStatus(createInputStatus(STATUS_SUCCESS));
},
onError: (message: string) => {
setSubmitStatus(createInputStatus(STATUS_ERROR, `There was an error: ${message}`));
},
});
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
await postConfigUpdateToAPI({
apiPath,
data: { value: isChecked },
onSuccess: () => {
setFieldInConfigState({ fieldName, value: isChecked, path: configPath });
setSubmitStatus(createInputStatus(STATUS_SUCCESS));
},
onError: (message: string) => {
setSubmitStatus(createInputStatus(STATUS_ERROR, `There was an error: ${message}`));
},
});
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
}
if (onChange) {
onChange(isChecked);
}
};
const loading = submitStatus !== null && submitStatus.type === STATUS_PROCESSING;
return (
<div className="toggleswitch-container">
<div className="toggleswitch">
<Switch
className={`switch field-${fieldName}`}
loading={loading}
onChange={handleChange}
defaultChecked={checked}
checked={checked}
checkedChildren="ON"
unCheckedChildren="OFF"
disabled={disabled}
/>
<span className="label">
{label} <InfoTip tip={tip} />
</span>
<div className="formfield-container toggleswitch-container">
{label && (
<div className="label-side">
<span className="formfield-label">{label}</span>
</div>
)}
<div className="input-side">
<div className="input-group">
<Switch
className={`switch field-${fieldName}`}
loading={loading}
onChange={handleChange}
defaultChecked={checked}
checked={checked}
checkedChildren="ON"
unCheckedChildren="OFF"
disabled={disabled}
/>
<FormStatusIndicator status={submitStatus} />
</div>
<p className="field-tip">{tip}</p>
</div>
<FormStatusIndicator status={submitStatus} />
</div>
);
}
@ -87,4 +111,6 @@ ToggleSwitch.defaultProps = {
disabled: false,
label: '',
tip: '',
useSubmit: false,
onChange: null,
};

View file

@ -0,0 +1,42 @@
import { Popconfirm, Button, Typography } from 'antd';
import { useContext } from 'react';
import { AlertMessageContext } from '../../utils/alert-message-context';
import { API_YP_RESET, fetchData } from '../../utils/apis';
export default function ResetYP() {
const { setMessage } = useContext(AlertMessageContext);
const { Title } = Typography;
const resetDirectoryRegistration = async () => {
try {
await fetchData(API_YP_RESET);
setMessage('');
} catch (error) {
alert(error);
}
};
return (
<>
<Title level={3} className="section-title">
Reset Directory
</Title>
<p className="description">
If you are experiencing issues with your listing on the Owncast Directory and were asked to
&quot;reset&quot; your connection to the service, you can do that here. The next time you go
live it will try and re-register your server with the directory from scratch.
</p>
<Popconfirm
placement="topLeft"
title="Are you sure you want to reset your connection to the Owncast directory?"
onConfirm={resetDirectoryRegistration}
okText="Yes"
cancelText="No"
>
<Button type="primary">Reset Directory Connection</Button>
</Popconfirm>
</>
);
}

View file

@ -19,11 +19,11 @@ export default function SocialDropdown({ iconList, selectedOption, onSelected }:
const inititalSelected = selectedOption === '' ? null : selectedOption;
return (
<div className="social-dropdown-container">
<p className="">
<p className="description">
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="">
<p className="description">
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.

View file

@ -46,9 +46,6 @@ function SegmentToolTip({ value }: SegmentToolTipProps) {
export default function VideoLatency() {
const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
// const [submitStatus, setSubmitStatus] = useState(null);
// const [submitStatusMessage, setSubmitStatusMessage] = useState('');
const [selectedOption, setSelectedOption] = useState(null);
const serverStatusData = useContext(ServerStatusContext);
@ -68,7 +65,6 @@ export default function VideoLatency() {
const resetStates = () => {
setSubmitStatus(null);
// setSubmitStatusMessage('');
resetTimer = null;
clearTimeout(resetTimer);
};
@ -88,8 +84,6 @@ export default function VideoLatency() {
});
setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Latency buffer level updated.'));
// setSubmitStatus('success');
// setSubmitStatusMessage('Variants updated.');
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
if (serverStatusData.online) {
setMessage(
@ -100,8 +94,6 @@ export default function VideoLatency() {
onError: (message: string) => {
setSubmitStatus(createInputStatus(STATUS_ERROR, message));
// setSubmitStatus('error');
// setSubmitStatusMessage(message);
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
},
});
@ -113,15 +105,19 @@ export default function VideoLatency() {
return (
<div className="config-video-segements-conatiner">
<Title level={3}>Latency Buffer</Title>
<p>
While it's natural to want to keep your latency as low as possible, you may experience
<Title level={3} className="section-title">
Latency Buffer
</Title>
<p className="description">
While it&apos;s natural to want to keep your latency as low as possible, you may experience
reduced error tolerance and stability in some environments the lower you go.
</p>
For interactive live streams you may want to experiment with a lower latency, for
non-interactive broadcasts you may want to increase it.{' '}
<a href="https://owncast.online/docs/encoding#latency-buffer">Read to learn more.</a>
<p></p>
<p className="description">
For interactive live streams you may want to experiment with a lower latency, for
non-interactive broadcasts you may want to increase it.{' '}
<a href="https://owncast.online/docs/encoding#latency-buffer">Read to learn more.</a>
</p>
<div className="segment-slider-container">
<Slider
tipFormatter={value => <SegmentToolTip value={SLIDER_COMMENTS[value]} />}
@ -132,8 +128,8 @@ export default function VideoLatency() {
defaultValue={selectedOption}
value={selectedOption}
/>
<FormStatusIndicator status={submitStatus} />
</div>
<FormStatusIndicator status={submitStatus} />
</div>
);
}

View file

@ -1,11 +1,12 @@
// This content populates the video variant modal, which is spawned from the variants table.
import React from 'react';
import { Slider, Switch, Collapse } from 'antd';
import { Slider, Switch, Collapse, Typography } from 'antd';
import { FieldUpdaterFunc, VideoVariant, UpdateArgs } from '../../types/config-section';
import TextField from './form-textfield';
import { DEFAULT_VARIANT_STATE } from '../../utils/config-constants';
import InfoTip from '../info-tip';
import CPUUsageSelector from './cpu-usage';
// import ToggleSwitch from './form-toggleswitch-with-submit';
const { Panel } = Collapse;
@ -55,7 +56,6 @@ const VIDEO_VARIANT_DEFAULTS = {
tip: "Optionally resize this content's height.",
},
};
interface VideoVariantFormProps {
dataState: VideoVariant;
onUpdateField: FieldUpdaterFunc;
@ -79,6 +79,7 @@ export default function VideoVariantForm({
};
const handleScaledWidthChanged = (args: UpdateArgs) => {
const value = Number(args.value);
// eslint-disable-next-line no-restricted-globals
if (isNaN(value)) {
return;
}
@ -86,6 +87,7 @@ export default function VideoVariantForm({
};
const handleScaledHeightChanged = (args: UpdateArgs) => {
const value = Number(args.value);
// eslint-disable-next-line no-restricted-globals
if (isNaN(value)) {
return;
}
@ -108,124 +110,123 @@ export default function VideoVariantForm({
return (
<div className="config-variant-form">
<div className="section-intro">
<p className="description">
Say a thing here about how this all works. Read more{' '}
<a href="https://owncast.online/docs/configuration/">here</a>.
<br />
<br />
</div>
</p>
{/* 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}
</div>
</div>
{/* VIDEO PASSTHROUGH FIELD */}
<div style={{ display: 'none' }}>
<div className="field">
<p className="label">
<InfoTip tip={VIDEO_VARIANT_DEFAULTS.videoPassthrough.tip} />
Use Video Passthrough?
</p>
<div className="form-component">
<Switch
defaultChecked={dataState.videoPassthrough}
checked={dataState.videoPassthrough}
onChange={handleVideoPassChange}
checkedChildren="Yes"
unCheckedChildren="No"
/>
</div>
</div>
</div>
{/* VIDEO BITRATE FIELD */}
<div className={`field ${dataState.videoPassthrough ? 'disabled' : ''}`}>
<p className="label">
<InfoTip tip={VIDEO_VARIANT_DEFAULTS.videoBitrate.tip} />
Video Bitrate:
</p>
<div className="form-component">
<Slider
tipFormatter={value => `${value} ${videoBRUnit}`}
disabled={dataState.videoPassthrough === true}
defaultValue={dataState.videoBitrate}
value={dataState.videoBitrate}
onChange={handleVideoBitrateChange}
step={videoBitrateDefaults.incrementBy}
min={videoBRMin}
max={videoBRMax}
marks={{
[videoBRMin]: `${videoBRMin} ${videoBRUnit}`,
[videoBRMax]: `${videoBRMax} ${videoBRUnit}`,
}}
/>
{selectedVideoBRnote ? (
<span className="selected-value-note">{selectedVideoBRnote}</span>
) : null}
</div>
</div>
<Collapse>
<Panel header="Advanced Settings" key="1">
<div className="section-intro">
Resizing your content will take additional resources on your server. If you wish to
optionally resize your output for this stream variant then you should either set the
width <strong>or</strong> the height to keep your aspect ratio.
</div>
<div className="field">
<TextField
type="number"
{...VIDEO_VARIANT_DEFAULTS.scaledWidth}
value={dataState.scaledWidth}
onChange={handleScaledWidthChanged}
/>
</div>
<div className="field">
<TextField
type="number"
{...VIDEO_VARIANT_DEFAULTS.scaledHeight}
value={dataState.scaledHeight}
onChange={handleScaledHeightChanged}
<div className="row">
<div>
{/* ENCODER PRESET FIELD */}
<div className="form-module cpu-usage-container">
<CPUUsageSelector
defaultValue={dataState.cpuUsageLevel}
onChange={handleVideoCpuUsageLevelChange}
/>
{selectedPresetNote && (
<span className="selected-value-note">{selectedPresetNote}</span>
)}
</div>
{/* FRAME RATE FIELD */}
<div className="field">
{/* VIDEO PASSTHROUGH FIELD */}
<div style={{ display: 'none' }} className="form-module">
<p className="label">
<InfoTip tip={VIDEO_VARIANT_DEFAULTS.framerate.tip} />
Frame rate:
<InfoTip tip={VIDEO_VARIANT_DEFAULTS.videoPassthrough.tip} />
Use Video Passthrough?
</p>
<div className="form-component">
<Slider
// tooltipVisible
tipFormatter={value => `${value} ${framerateUnit}`}
defaultValue={dataState.framerate}
value={dataState.framerate}
onChange={handleFramerateChange}
step={framerateDefaults.incrementBy}
min={framerateMin}
max={framerateMax}
marks={{
[framerateMin]: `${framerateMin} ${framerateUnit}`,
[framerateMax]: `${framerateMax} ${framerateUnit}`,
}}
{/* todo: change to ToggleSwitch for layout */}
<Switch
defaultChecked={dataState.videoPassthrough}
checked={dataState.videoPassthrough}
onChange={handleVideoPassChange}
// label="Use Video Passthrough"
checkedChildren="Yes"
unCheckedChildren="No"
/>
{selectedFramerateNote ? (
<span className="selected-value-note">{selectedFramerateNote}</span>
) : null}
</div>
</div>
</Panel>
</Collapse>
{/* VIDEO BITRATE FIELD */}
<div className={`form-module ${dataState.videoPassthrough ? 'disabled' : ''}`}>
<Typography.Title level={3} className="section-title">
Video Bitrate
</Typography.Title>
<p className="description">{VIDEO_VARIANT_DEFAULTS.videoBitrate.tip}</p>
<div className="segment-slider-container">
<Slider
tipFormatter={value => `${value} ${videoBRUnit}`}
disabled={dataState.videoPassthrough === true}
defaultValue={dataState.videoBitrate}
value={dataState.videoBitrate}
onChange={handleVideoBitrateChange}
step={videoBitrateDefaults.incrementBy}
min={videoBRMin}
max={videoBRMax}
marks={{
[videoBRMin]: `${videoBRMin} ${videoBRUnit}`,
[videoBRMax]: `${videoBRMax} ${videoBRUnit}`,
}}
/>
{selectedVideoBRnote && (
<span className="selected-value-note">{selectedVideoBRnote}</span>
)}
</div>
</div>
</div>
<Collapse className="advanced-settings">
<Panel header="Advanced Settings" key="1">
<div className="section-intro">
Resizing your content will take additional resources on your server. If you wish to
optionally resize your output for this stream variant then you should either set the
width <strong>or</strong> the height to keep your aspect ratio.
</div>
<div className="field">
<TextField
type="number"
{...VIDEO_VARIANT_DEFAULTS.scaledWidth}
value={dataState.scaledWidth}
onChange={handleScaledWidthChanged}
/>
</div>
<div className="field">
<TextField
type="number"
{...VIDEO_VARIANT_DEFAULTS.scaledHeight}
value={dataState.scaledHeight}
onChange={handleScaledHeightChanged}
/>
</div>
{/* FRAME RATE FIELD */}
<div className="field">
<p className="label">
<InfoTip tip={VIDEO_VARIANT_DEFAULTS.framerate.tip} />
Frame rate:
</p>
<div className="segment-slider-container form-component">
<Slider
// tooltipVisible
tipFormatter={value => `${value} ${framerateUnit}`}
defaultValue={dataState.framerate}
value={dataState.framerate}
onChange={handleFramerateChange}
step={framerateDefaults.incrementBy}
min={framerateMin}
max={framerateMax}
marks={{
[framerateMin]: `${framerateMin} ${framerateUnit}`,
[framerateMax]: `${framerateMax} ${framerateUnit}`,
}}
/>
{selectedFramerateNote ? (
<span className="selected-value-note">{selectedFramerateNote}</span>
) : null}
</div>
</div>
</Panel>
</Collapse>
</div>
</div>
);
}

View file

@ -12,10 +12,17 @@ import VideoVariantForm from './video-variant-form';
import {
API_VIDEO_VARIANTS,
DEFAULT_VARIANT_STATE,
SUCCESS_STATES,
RESET_TIMEOUT,
postConfigUpdateToAPI,
} from '../../utils/config-constants';
import {
createInputStatus,
StatusState,
STATUS_ERROR,
STATUS_PROCESSING,
STATUS_SUCCESS,
} from '../../utils/input-statuses';
import FormStatusIndicator from './form-status-indicator';
const { Title } = Typography;
@ -36,8 +43,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('');
const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
const serverStatusData = useContext(ServerStatusContext);
const { serverConfig, setFieldInConfigState } = serverStatusData || {};
@ -52,7 +58,6 @@ export default function CurrentVariantsTable() {
const resetStates = () => {
setSubmitStatus(null);
setSubmitStatusMessage('');
resetTimer = null;
clearTimeout(resetTimer);
};
@ -65,6 +70,8 @@ export default function CurrentVariantsTable() {
// posts all the variants at once as an array obj
const postUpdateToAPI = async (postValue: any) => {
setSubmitStatus(createInputStatus(STATUS_PROCESSING));
await postConfigUpdateToAPI({
apiPath: API_VIDEO_VARIANTS,
data: { value: postValue },
@ -79,8 +86,7 @@ export default function CurrentVariantsTable() {
setModalProcessing(false);
handleModalCancel();
setSubmitStatus('success');
setSubmitStatusMessage('Variants updated.');
setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Variants updated'));
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
if (serverStatusData.online) {
@ -90,8 +96,7 @@ export default function CurrentVariantsTable() {
}
},
onError: (message: string) => {
setSubmitStatus('error');
setSubmitStatusMessage(message);
setSubmitStatus(createInputStatus(STATUS_ERROR, message));
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
},
});
@ -112,7 +117,7 @@ export default function CurrentVariantsTable() {
postUpdateToAPI(postData);
};
const handleDeleteVariant = index => {
const handleDeleteVariant = (index: number) => {
const postData = [...videoQualityVariants];
postData.splice(index, 1);
postUpdateToAPI(postData);
@ -125,9 +130,6 @@ export default function CurrentVariantsTable() {
});
};
const { icon: newStatusIcon = null, message: newStatusMessage = '' } =
SUCCESS_STATES[submitStatus] || {};
const videoQualityColumns: ColumnsType<VideoVariant> = [
{
title: 'Video bitrate',
@ -176,12 +178,6 @@ export default function CurrentVariantsTable() {
},
];
const statusMessage = (
<div className={`status-message ${submitStatus || ''}`}>
{newStatusIcon} {newStatusMessage} {submitStatusMessage}
</div>
);
const videoQualityVariantData = videoQualityVariants.map((variant, index) => ({
key: index + 1,
...variant,
@ -189,9 +185,11 @@ export default function CurrentVariantsTable() {
return (
<>
<Title level={3}>Stream output</Title>
<Title level={3} className="section-title">
Stream output
</Title>
{statusMessage}
<FormStatusIndicator status={submitStatus} />
<Table
className="variants-table"
@ -207,10 +205,11 @@ export default function CurrentVariantsTable() {
onOk={handleModalOk}
onCancel={handleModalCancel}
confirmLoading={modalProcessing}
width={900}
>
<VideoVariantForm dataState={{ ...modalDataState }} onUpdateField={handleUpdateField} />
{statusMessage}
<FormStatusIndicator status={submitStatus} />
</Modal>
<br />
<Button

View file

@ -49,7 +49,7 @@ export default function MainLayout(props) {
const { Header, Footer, Content, Sider } = Layout;
const { SubMenu } = Menu;
const [upgradeVersion, setUpgradeVersion] = useState(null);
const [upgradeVersion, setUpgradeVersion] = useState('');
const checkForUpgrade = async () => {
try {
const result = await upgradeVersionAvailable(versionNumber);
@ -80,7 +80,8 @@ export default function MainLayout(props) {
});
const upgradeMenuItemStyle = upgradeVersion ? 'block' : 'none';
const upgradeVersionString = upgradeVersion || '';
const upgradeVersionString = `${upgradeVersion}` || '';
const upgradeMessage = `Upgrade to v${upgradeVersionString}`;
const clearAlertMessage = () => {
alertMessage.setMessage(null);
@ -123,10 +124,10 @@ export default function MainLayout(props) {
<Sider width={240} className="side-nav">
<Menu
theme="dark"
defaultSelectedKeys={[route.substring(1) || 'home']}
defaultOpenKeys={['current-stream-menu', 'utilities-menu', 'configuration']}
mode="inline"
className="menu-container"
>
<h1 className="owncast-title">
<span className="logo-container">
@ -150,13 +151,6 @@ export default function MainLayout(props) {
<Menu.Item key="config-public-details">
<Link href="/config-public-details">General</Link>
</Menu.Item>
<Menu.Item key="config-social-items">
<Link href="/config-social-items">Social Links</Link>
</Menu.Item>
<Menu.Item key="config-page-content">
<Link href="/config-page-content">Page Content</Link>
</Menu.Item>
<Menu.Item key="config-server-details">
<Link href="/config-server-details">Server Setup</Link>
@ -177,9 +171,7 @@ export default function MainLayout(props) {
<Link href="/logs">Logs</Link>
</Menu.Item>
<Menu.Item key="upgrade" style={{ display: upgradeMenuItemStyle }}>
<Link href="/upgrade">
<a>Upgrade to v{upgradeVersionString}</a>
</Link>
<Link href="/upgrade">{upgradeMessage}</Link>
</Menu.Item>
</SubMenu>
<SubMenu key="integrations-menu" icon={<ExperimentOutlined />} title="Integrations">

29
web/package-lock.json generated
View file

@ -1587,6 +1587,12 @@
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==",
"dev": true
},
"@types/highlight.js": {
"version": "9.12.4",
"resolved": "https://registry.npmjs.org/@types/highlight.js/-/highlight.js-9.12.4.tgz",
"integrity": "sha512-t2szdkwmg2JJyuCM20e8kR2X59WCE5Zkl4bzm1u1Oukjm79zpbiAv+QjnwLnuuV0WHEcX2NgUItu0pAMKuOPww==",
"dev": true
},
"@types/json-schema": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz",
@ -1598,6 +1604,23 @@
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=",
"dev": true
},
"@types/linkify-it": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.0.tgz",
"integrity": "sha512-x9OaQQTb1N2hPZ/LWJsqushexDvz7NgzuZxiRmZio44WPuolTZNHDBCrOxCzRVOMwamJRO2dWax5NbygOf1OTQ==",
"dev": true
},
"@types/markdown-it": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.0.1.tgz",
"integrity": "sha512-mHfT8j/XkPb1uLEfs0/C3se6nd+webC2kcqcy8tgcVr0GDEONv/xaQzAN+aQvkxQXk/jC0Q6mPS+0xhFwRF35g==",
"dev": true,
"requires": {
"@types/highlight.js": "^9.7.0",
"@types/linkify-it": "*",
"@types/mdurl": "*"
}
},
"@types/mdast": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.3.tgz",
@ -1606,6 +1629,12 @@
"@types/unist": "*"
}
},
"@types/mdurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz",
"integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==",
"dev": true
},
"@types/node": {
"version": "14.11.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.2.tgz",

View file

@ -27,6 +27,7 @@
"devDependencies": {
"@types/chart.js": "^2.9.28",
"@types/classnames": "^2.2.11",
"@types/markdown-it": "^12.0.1",
"@types/node": "^14.11.2",
"@types/prop-types": "^15.7.3",
"@types/react": "^16.9.49",

View file

@ -1,19 +1,19 @@
// order matters!
import 'antd/dist/antd.css';
import '../styles/colors.scss';
import '../styles/globals.scss';
import '../styles/ant-overrides.scss';
import '../styles/markdown-editor.scss';
import '../styles/globals.scss';
import '../styles/main-layout.scss';
import '../styles/form-textfields.scss';
import '../styles/form-toggleswitch.scss';
import '../styles/form-misc-elements.scss';
import '../styles/config-socialhandles.scss';
import '../styles/config-storage.scss';
import '../styles/config-tags.scss';
import '../styles/config-video-variants.scss';
import '../styles/config-public-details.scss';
import '../styles/home.scss';
import '../styles/chat.scss';

View file

@ -1,26 +1,42 @@
import React from 'react';
import { Typography } from 'antd';
import Link from 'next/link';
import EditInstanceDetails from '../components/config/edit-instance-details';
import EditDirectoryDetails from '../components/config/edit-directory';
import EditInstanceTags from '../components/config/edit-tags';
import EditSocialLinks from '../components/config/edit-social-links';
import EditPageContent from '../components/config/edit-page-content';
const { Title } = Typography;
export default function PublicFacingDetails() {
return (
<>
<Title level={2}>General Settings</Title>
<p>
<div className="config-public-details-page">
<Title level={2} className="page-title">
General Settings
</Title>
<p className="description">
The following are displayed on your site to describe your stream and its content.{' '}
<a href="https://owncast.online/docs/website/">Learn more.</a>
</p>
<div className="edit-public-details-container">
<EditInstanceDetails />
<EditInstanceTags />
<EditDirectoryDetails />
<div className="top-container">
<div className="form-module instance-details-container">
<EditInstanceDetails />
</div>
<div className="form-module social-items-container ">
<div className="form-module tags-module">
<EditInstanceTags />
</div>
<div className="form-module social-handles-container">
<EditSocialLinks />
</div>
</div>
</div>
</>
<div className="form-module page-content-module">
<EditPageContent />
</div>
</div>
);
}

View file

@ -7,12 +7,14 @@ const { Title } = Typography;
export default function ConfigServerDetails() {
return (
<div className="config-server-details-form">
<Title level={2}>Server Settings</Title>
<p>
You should change your stream key from the default and keep it safe. For most people it's
likely the other settings will not need to be changed.
<Title level={2} className="page-title">
Server Settings
</Title>
<p className="description">
You should change your stream key from the default and keep it safe. For most people
it&apos;s likely the other settings will not need to be changed.
</p>
<div className="config-server-details-container">
<div className="form-module config-server-details-container">
<EditServerDetails />
</div>
</div>

View file

@ -7,13 +7,15 @@ const { Title } = Typography;
export default function ConfigStorageInfo() {
return (
<>
<Title level={2}>Storage</Title>
<p>
<Title level={2} className="page-title">
Storage
</Title>
<p className="description">
Owncast supports optionally using external storage providers to distribute your video. Learn
more about this by visiting our{' '}
<a href="https://owncast.online/docs/storage/">Storage Documentation</a>.
</p>
<p>
<p className="description">
Configuring this incorrectly will likely cause your video to be unplayable. Double check the
documentation for your storage provider on how to configure the bucket you created for
Owncast.

View file

@ -9,22 +9,24 @@ const { Title } = Typography;
export default function ConfigVideoSettings() {
return (
<div className="config-video-variants">
<Title level={2}>Video configuration</Title>
<p>
<Title level={2} className="page-title">
Video configuration
</Title>
<p className="description">
Before changing your video configuration{' '}
<a href="https://owncast.online/docs/encoding">visit the video documentation</a> to learn
how it impacts your stream performance.
</p>
<p>
<VideoVariantsTable />
</p>
<br />
<hr />
<br />
<p>
<VideoLatency />
</p>
<div className="row">
<div className="form-module variants-table-module">
<VideoVariantsTable />
</div>
<div className="form-module latency-module">
<VideoLatency />
</div>
</div>
</div>
);
}

View file

@ -1,52 +1,231 @@
// GENERAL ANT OVERRIDES
// RESET BG, TEXT COLORS
.ant-layout,
.ant-layout-footer,
.ant-menu,
.ant-menu.ant-menu-dark {
background-color: transparent;
}
.owncast-layout .ant-menu-dark.ant-menu-dark:not(.ant-menu-horizontal) .ant-menu-item-selected {
background-color: var(--owncast-purple);
}
// LAYOUT
.ant-layout-header,
.ant-layout-sider {
background-color: #07050d;
.ant-layout-sider,
.ant-layout-footer,
.ant-card,
.ant-collapse,
.ant-collapse-content,
.ant-table,
.ant-table-thead > tr > th,
.ant-table-small .ant-table-thead > tr > th,
th.ant-table-column-sort,
td.ant-table-column-sort,
.ant-table-tbody > tr.ant-table-row:hover > td,
.ant-table-thead th.ant-table-column-sort,
.ant-menu,
.ant-menu-submenu > .ant-menu,
.ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected {
background-color: transparent;
color: var(--default-text-color)
}
// MENU
.ant-menu-dark .ant-menu-inline.ant-menu-sub {
// background-color: rgba(255,255,255,.05);
background-color: #140028;
h1.ant-typography,
h2.ant-typography,
h3.ant-typography,
h4.ant-typography,
h5.ant-typography,
.ant-typography,
.ant-typography h1,
.ant-typography h2,
.ant-typography h3,
.ant-typography h4,
.ant-typography h5 {
color: var(--default-text-color);
font-weight: 500;
}
// CARD
.ant-typography.ant-typography-secondary {
color: rgba(255,255,255,.85);
font-weight: 400;
}
.ant-progress-text,
.ant-progress-circle .ant-progress-text {
color: var(--default-text-color);
}
// ANT MENU
// menu base
.ant-menu-item {
transition-duration: var(--ant-transition-duration);
.anticon {
transition-duration: var(--ant-transition-duration);
color: var(--nav-text);
}
a {
transition-duration: var(--ant-transition-duration);
color: var(--nav-text);
&:hover {
color: white;
}
}
&:hover {
background-color: rgba(0,0,0,.15);
.anticon {
color: white;
}
}
}
// menu item selected
.ant-menu-item-selected,
.ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected {
background-color: black;
a {
color: var(--nav-selected-text);
}
.anticon {
color: var(--nav-selected-text);
}
// the right border color
&:after {
border-color: var(--nav-selected-text);
transition-duration: var(--ant-transition-duration);
}
}
// submenu items
.ant-menu-submenu {
&> .ant-menu {
border-left: 1px solid rgba(255,255,255,.4);
background-color: rgba(0,0,0,.15);
}
.ant-menu-submenu-title {
transition-duration: var(--ant-transition-duration);
color: var(--nav-text);
.anticon {
color: var(--nav-text);
}
.ant-menu-submenu-arrow {
&:before,
&:after {
transition-duration: var(--ant-transition-duration);
background-image: linear-gradient(to right, var(--nav-text), var(--nav-text));
}
}
}
&:hover {
.ant-menu-submenu-title {
color: white;
.anticon {
color: white;
}
.ant-menu-submenu-arrow {
&:before,
&:after {
background-image: linear-gradient(to right, white, white);
}
}
}
}
}
// ANT RESULT
.ant-result-title {
color: var(--default-text-color);
}
.ant-result-subtitle {
color: var(--default-text-color);
}
// ANT CARD
.ant-card {
border-radius: .5em;
border-radius: var(--container-border-radius);
background-color: var(--container-bg-color);
color: var(--default-text-color);
}
.ant-card-bordered {
border-color: rgba(255,255,255,.25);
}
.ant-card-meta-title,
.ant-card-meta-description {
color: white;
}
// INPUT
.ant-input-affix-wrapper {
// border-radius: 5px;
// background-color: rgba(255,255,255,.1);
// ANT INPUT
.ant-input-affix-wrapper,
.ant-input-number {
background-color: var(--textfield-bg);
border-color: var(--textfield-border);
input,
textarea {
// border-radius: 5px;
background-color: transparent;
color: rgba(255,255,255,.85);
border-color: rgba(0,0,0,1);
&::placeholder {
color: var(--textfield-border);
}
&:-webkit-autofill {
background-color: transparent;
}
}
input {
// background-color: transparent;
}
.ant-input-number:hover,
.ant-input-affix-wrapper:hover {
border-color: var(--owncast-purple-highlight);
input,
textarea {
border-color: var(--owncast-purple-highlight);
}
}
.ant-input-number:focus,
.ant-input-affix-wrapper:focus,
.ant-input-affix-wrapper-focused {
border-color: var(--owncast-purple);
input,
textarea {
color: white;
border-color: var(--owncast-purple);
}
}
.ant-input-textarea-clear-icon,
.ant-input-clear-icon {
color: rgba(255,255,255,.5);
}
textarea.ant-input {
padding-right: 25px;
}
// BUTTON
.ant-btn-primary:hover, .ant-btn-primary:focus {
background-color: white;
color: #40a9ff;
// ANT BUTTON
.ant-btn-primary {
background-color: var(--owncast-purple);
border-color: var(--owncast-purple);
}
.ant-btn.ant-btn-primary:focus {
.ant-btn-primary:hover,
.ant-btn-primary:focus {
background-color: var(--form-focused);
border-color: var(--form-focused);
}
.ant-btn.ant-btn-primary:hover {
border-color: white;
}
.ant-btn-primary[disabled] {
background-color: rgba(255,255,255,.2);
border-color: rgba(255,255,255,.2);
color: white;
&:hover {
background-color: rgba(255,255,255,.2);
border-color: rgba(255,255,255,.2);
}
}
.ant-input-affix-wrapper,
.ant-btn {
@ -54,29 +233,72 @@
transition-duration: 0.15s;
}
// TABLE
// ANT TABLE
.ant-table-thead > tr > th,
.ant-table-small .ant-table-thead > tr > th {
background-color: #000;
transition-duration: var(--ant-transition-duration);
background-color: #112;
font-weight: 500;
color: var(--owncast-purple);
}
.ant-table-tbody > tr > td,
.ant-table-thead > tr > th,
.ant-table-small .ant-table-thead > tr > th {
border-color: var(--textfield-border);
}
.ant-table-tbody > tr > td {
transition-duration: var(--ant-transition-duration);
background-color: var(--textfield-bg);
}
.ant-table-tbody > tr:nth-child(odd) > td {
background-color: var(--textfield-bg);
}
.ant-empty {
color: white;
opacity: .75;
}
.ant-table-empty .ant-table-tbody > tr.ant-table-placeholder {
&:hover > td {
background-color: transparent;
}
}
.ant-table-thead th.ant-table-column-has-sorters:hover {
background-color: var(--textfield-border);
.ant-table-filter-trigger-container {
background-color: var(--textfield-border);
}
}
// MODAL
.ant-modal-content {
border-radius: 6px;
border-radius: var(--container-border-radius);
border: 1px solid var(--owncast-purple-highlight);
}
.ant-modal-header {
background-color: #1c173d;
border-radius: 6px 6px 0 0;
border-radius: var(--container-border-radius) var(--container-border-radius) 0 0;
}
.ant-modal-close-x {
color: white;
}
.ant-modal-title {
font-weight: bold;
font-size: 1.5em;
font-weight: 500;
font-size: 1.25em;
color: var(--nav-selected-text);
}
.ant-modal-body {
background-color: #33333c;
background-color: var(--nav-bg-color);
color: var(--default-text-color);
}
.ant-modal-header,
.ant-modal-footer {
background-color: #222229;
background-color: black;
}
.ant-modal-content,
.ant-modal-header,
.ant-modal-footer {
border-color: #333;
}
// SELECT
@ -86,10 +308,59 @@
// SLIDER
.ant-slider-with-marks {
margin-right: 2em;
}
// .ant-slider-with-marks {
// margin-right: 2em;
// }
.ant-slider-mark-text {
font-size: .85em;
white-space: nowrap;
}
}
// ANT SWITCH
.ant-switch {
background-color: #666;
}
.ant-switch-checked {
background-color: var(--ant-success);
.ant-switch-inner {
color: white;
}
}
// ANT COLLAPSE
.ant-collapse {
border-color: transparent;
&> .ant-collapse-item,
.ant-collapse-content {
border-color: transparent;
&> .ant-collapse-header {
border-color: transparent;
background-color: var(--textfield-bg);
color: var(--nav-text);
font-weight: 500;
}
}
}
.ant-collapse-content {
background-color: #181231;
}
// ANT POPOVER
.ant-popover {
}
.ant-popover-inner {
background-color: black;
}
.ant-popover-message,
.ant-popover-inner-content {
color: var(--default-text-color);
}
.ant-popover-placement-topLeft > .ant-popover-content > .ant-popover-arrow {
border-color: black;
}

View file

@ -1,14 +1,35 @@
// rename to variables.scss
:root {
--owncast-purple: rgba(90,103,216,1);
--default-text-color: #fff;
--owncast-purple: rgba(90,103,216,1); //5a67d8
--owncast-purple-highlight: #ccd;
--online-color: #73dd3f;
--owncast-dark1: #1f1f21;
--ant-error: #ff4d4f;
--ant-success: #52c41a;
--ant-warning: #faad14;
--ant-transition-duration: .15s;
--container-bg-color: #1A1C24;
--container-bg-color-alt: #251c49;
--container-border-radius: 2px;
--code-purple: #82aaff;
--nav-bg-color: #1A1C24;
--nav-text: #6a76ba;
--nav-selected-text: #c48dff;
--form-focused: #8d71ff;
--textfield-border: #373640;
--textfield-bg: #100f0f;
}

View file

@ -0,0 +1,65 @@
// dealiing with some general layout on the public details page
.config-public-details-page {
width: 100%;
.top-container {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
@media (max-width: 1200px) {
flex-wrap: wrap;
.social-items-container {
display: flex;
flex-direction: row;
justify-content: space-around;
flex-wrap: nowrap;
margin: 1em 0;
width: 100%;
max-width: none;
.tags-module {
margin-right: 1em;
}
.form-module {
max-width: none;
}
@media (max-width: 980px) {
flex-direction: column;
.form-module {
width: 100%;
}
.tags-module {
margin-bottom: 0;
}
}
}
}
}
.instance-details-container {
width: 100%;
}
.social-items-container {
background-color: var(--container-bg-color-alt);
padding: 0 .75em;
margin-left: 1em;
max-width: 450px;
.form-module {
background-color: #000;
}
.social-handles-container {
min-width: 350px;
}
}
.instance-details-container,
.page-content-module {
margin: 1em 0;
}
.field-summary {
textarea {
height: 6em !important;
}
}
}

View file

@ -23,3 +23,35 @@
}
}
}
.social-links-edit-container {
.social-handles-table {
.social-handle-cell {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
color: rgba(255,255,255,.85);
.option-icon {
height: 2em;
width: 2em;
line-height: normal;
}
.option-label {
display: flex;
flex-direction: column;
margin: 0 0 0 1em;
line-height: 2;
font-size: .85em;
}
}
.actions {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-evenly;
width: 6em;
}
}
}

View file

@ -2,6 +2,7 @@
.edit-storage-container {
padding: 1em;
.form-fields {
display: none;
margin-bottom: 1em;
@ -13,7 +14,7 @@
}
.button-container {
margin: 1em 0;
margin: 2em 0 1em 0;
}
.advanced-section {
margin: 1em 0;

View file

@ -6,19 +6,24 @@
font-size: .85rem;
border-radius: 10em;
padding: .25em 1em;
background-color: rgba(255,255,255,.5);
&:hover {
opacity: 1;
}
.ant-tag-close-icon {
transform: translateY(-1px);
margin-left: .3rem;
padding: 2px;
border-radius: 5rem;
border: 1px solid #eee;
color: black;
border: 1px solid #000;
transition-duration: var(--ant-transition-duration);
&:hover {
border-color: #e03;
border-color: #5a67d8;
background-color: white;
svg {
fill: black;
transition: fill .3s;
transition: fill var(--ant-transition-duration);
}
}
}
@ -30,8 +35,5 @@
flex-direction: row;
justify-content: flex-start;
align-items: center;
.new-tag-input {
width: 16em;
}
margin-top: 2em;
}

View file

@ -1,7 +1,30 @@
// styles for Video variant editor (table + modal)
.config-video-variants {
.variants-table {
margin-top: 2em;
}
.variants-table-module {
min-width: 48%;
max-width: 600px;
margin-right: 1em
}
}
// modal content
.config-variant-form {
.description {
margin-top: 0;
}
.advanced-settings {
width: 48%;
margin-left: 2em;
}
.blurb {
margin: 1em;
opacity: .75;
@ -13,58 +36,61 @@
opacity: .5;
font-style: italic;
}
.section-intro {
margin-bottom: 2em;
}
.field {
margin-bottom: 2em;
display: flex;
flex-direction: row;
justify-content: center;
align-items: flex-start;
transform: opacity .15s;
&.disabled {
opacity: .25;
}
.label {
width: 40%;
text-align: right;
padding-right: 2em;
font-weight: bold;
color: var(--owncast-purple);
}
.info-tip {
margin-right: 1em;
}
.form-component {
width: 60%;
// .field {
// margin-bottom: 2em;
// display: flex;
// flex-direction: row;
// justify-content: center;
// align-items: flex-start;
// transform: opacity .15s;
// &.disabled {
// opacity: .25;
// }
// .label {
// width: 40%;
// text-align: right;
// padding-right: 2em;
// font-weight: bold;
// color: var(--owncast-purple);
// }
// .info-tip {
// margin-right: 1em;
// }
// .form-component {
// width: 60%;
.selected-value-note {
font-size: .85em;
display: inline-block;
text-align: center;
}
}
}
.ant-collapse {
border: none;
border-radius: 6px;
}
.ant-collapse > .ant-collapse-item:last-child,
.ant-collapse > .ant-collapse-item:last-child > .ant-collapse-header {
border: none;
background-color: rgba(0,0,0,.25);
border-radius: 6px;
}
.ant-collapse-content {
background-color: rgba(0,0,0,.1);
}
// .selected-value-note {
// font-size: .85em;
// display: inline-block;
// text-align: center;
// }
// }
// }
// .ant-collapse {
// border: none;
// border-radius: 6px;
// }
// .ant-collapse > .ant-collapse-item:last-child,
// .ant-collapse > .ant-collapse-item:last-child > .ant-collapse-header {
// border: none;
// background-color: rgba(0,0,0,.25);
// border-radius: 6px;
// }
// .ant-collapse-content {
// background-color: rgba(0,0,0,.1);
// }
}
.config-video-segements-conatiner {
// display: flex;
// flex-direction: row;
// justify-content: center;
// align-items: flex-start;
.status-message {
text-align: center;
@ -82,4 +108,8 @@
margin-left: .5em;
opacity: .8;
}
}
.advanced-settings {
margin-top: 2em;
}

View file

@ -1,9 +1,10 @@
// todo: put these somewhere else
.config-page-content-form {
.edit-page-content {
.page-content-actions {
margin-top: 1em;
display: flex;
@ -26,25 +27,3 @@
margin: auto;
display: inline-block;
}
// .social-option {
// .ant-select-item-option-content {
// display: flex;
// flex-direction: row;
// justify-content: flex-start;
// align-items: center;
// padding: .25em;
// .option-icon {
// height: 1.75em;
// width: 1.75em;
// }
// .option-label {
// display: inline-block;
// margin-left: 1em;
// }
// }
// }

View file

@ -29,7 +29,7 @@
/* TIP CONTAINER BASE */
.field-tip {
font-size: .7em;
font-size: .8em;
color: rgba(255,255,255,.5)
}
@ -39,9 +39,9 @@ Ideal for wrapping each Textfield on a page with many text fields in a row. This
*/
.field-container {
padding: .85em 0 .5em;
&:nth-child(even) {
background-color: rgba(0,0,0,.25);
}
// &:nth-child(even) {
// background-color: rgba(0,0,0,.25);
// }
}
@ -50,8 +50,27 @@ Ideal for wrapping each Textfield on a page with many text fields in a row. This
width: 90%;
margin: auto;
padding: 1em 2em .75em;
background-color: black;
background-color: var(--textfield-border);
border-radius: 1em;
.ant-slider-rail {
background-color: black;
}
.ant-slider-track {
background-color: var(--nav-text);
}
.ant-slider-mark-text,
.ant-slider-mark-text-active {
color: white;
opacity: .5;
}
.ant-slider-mark-text-active {
opacity: 1;
}
.status-container {
width: 100%;
margin: .5em auto;
text-align: center;
}
}

View file

@ -1,8 +1,14 @@
// Base styles for form-textfield, form-textfield-with-submit
/*
Base styles for
- form-textfield,
- form-textfield-with-submit
- form-toggleswitch
Both text and toggle use this class for base layout.
*/
.formfield-container {
--form-label-container-width: 15em;
/* TEXTFIELD-CONTAINER BASE */
.textfield-container {
display: flex;
flex-direction: row;
align-items: flex-start;
@ -10,14 +16,14 @@
width: 100%;
max-width: 600px;
.label-side {
padding-right: .75em;
padding-right: 1.25em;
text-align: right;
width: 12em;
width: var(--form-label-container-width);
margin: .2em 0;
}
.textfield-label {
font-weight: 400;
font-size: .85em;
.formfield-label {
font-weight: 500;
font-size: 1em;
color: var(--owncast-purple);
&::after {
@ -25,7 +31,7 @@
}
}
&.required {
.textfield-label {
.formfield-label {
&::before {
content: '*';
display: inline-block;
@ -97,7 +103,7 @@
justify-content: flex-start;
.label-spacer {
width: 12em;
width: var(--form-label-container-width);
}
.lower-content {
display: flex;
@ -138,3 +144,26 @@
}
}
}
/* TOGGLE SWITCH CONTAINER BASE */
.toggleswitch-container {
margin: 2em 0 1em;
.label-side {
margin-top: 0;
}
.input-group {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
.status-container {
width: auto;
margin: 0 0 0 1em;
display: inline-block;
}
}
}

View file

@ -1,29 +0,0 @@
/* TOGGLE SWITCH-WITH-SUBMIT-CONTAINER BASE */
.toggleswitch-container {
.status-container {
margin-top: .25rem;
}
.toggleswitch {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
.label {
font-weight: bold;
color: var(--owncast-purple);
}
.info-tip {
margin-left: .5rem;
svg {
fill: white;
}
}
.ant-form-item {
margin: 0 .75rem 0 0;
}
}
}

View file

@ -1,4 +1,4 @@
@import "~antd/dist/antd.dark";
// @import "~antd/dist/antd.dark";
html,
body {
@ -6,15 +6,20 @@ body {
margin: 0;
font-family: system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
font-size: 16px;
font-size: 14px;
background-color: #1f1f21;
background-color: #000;
color: var(--default-text-color);;
}
a {
color: inherit;
text-decoration: none;
color: rgba(90,103,216,1);
color: var(--owncast-purple);
&:hover {
color: var(--default-text-color);
}
}
* {
@ -40,13 +45,50 @@ code {
height: 2rem;
width: 2rem;
}
.ant-btn {
transition-duration: .15s;
transition-delay: 0s;
}
p.page-description {
p.description {
margin: 1em 0;
color: #ccc;
width: 80%;
}
.line-chart-container {
margin: 2em auto;
}
h2.ant-typography.page-title,
h3.ant-typography.page-title
{
font-weight: 400;
font-size: 1.5em;
color: var(--nav-selected-text);
}
h2.section-title,
h3.section-title {
font-weight: 400;
font-size: 1.25em;
}
.form-module {
// width: 100%;
// max-width: 500px;
// min-width: 300px;
margin: 1em 0;
background-color: var(--container-bg-color);
padding: 2em;
border-radius: var(--container-border-radius);
}
.row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-start;
@media (max-width: 980px) {
flex-direction: column;
.form-module {
width: 100%;
}
}
}

View file

@ -5,6 +5,11 @@
height: 100vh;
overflow: auto;
z-index: 10;
background-color: var(--nav-bg-color);
}
.menu-container {
border-color: transparent;
}
h1.owncast-title {
@ -40,6 +45,7 @@
flex-direction: row;
justify-content: flex-end;
padding-right: 1rem;
background-color: var(--nav-bg-color);
}

View file

@ -5,17 +5,24 @@
display: block !important;
}
.rc-md-editor {
border-color: black !important;
border: 1px solid black;
background-color: black !important;
.rc-md-navigation {
background-color: black;
border-color: black;
}
// Set the background color of the preview container
.editor-container {
background-color: #E2E8F0;
color: rgba(45,55,72,1);
background-color: rgba(226,232,240, 1) !important;
}
// Custom CSS for formatting the preview text
.markdown-editor-preview-pane {
// color:lightgrey;
a {
color: var(--owncast-purple);;
color: var(--owncast-purple);
}
h1 {
font-size: 2em;
@ -24,20 +31,20 @@
// Custom CSS class used to format the text of the editor
.markdown-editor-pane {
color: white !important;
color: rgba(255,255,255,.85) !important;
border-color: black !important;
background-color: black;
font-family: monospace;
}
// Set the background color of the editor text input
textarea {
background-color: rgb(44,44,44) !important;
color:lightgrey !important;
background-color: #223 !important;
color: rgba(255,255,255,.5) !important;
}
.ant-btn {
transition-duration: .15s;
transition-delay: 0s;
}
// Hide extra toolbar buttons.
.button-type-undo, .button-type-redo, .button-type-clear, .button-type-image, .button-type-wrap, .button-type-quote, .button-type-strikethrough, .button-type-code-inline, .button-type-code-block {

View file

@ -1,6 +1,4 @@
// DEFAULT VALUES
import React from 'react';
import { CheckCircleFilled, ExclamationCircleFilled } from '@ant-design/icons';
import { fetchData, SERVER_CONFIG_UPDATE_URL } from './apis';
import { ApiPostArgs, VideoVariant, SocialHandle } from '../types/config-section';
@ -8,17 +6,6 @@ export const TEXT_MAXLENGTH = 255;
export const RESET_TIMEOUT = 3000;
export const SUCCESS_STATES = {
success: {
icon: <CheckCircleFilled style={{ color: 'green' }} />,
message: 'Success!',
},
error: {
icon: <ExclamationCircleFilled style={{ color: 'red' }} />,
message: 'An error occurred.',
},
};
// CONFIG API ENDPOINTS
export const API_CUSTOM_CONTENT = '/pagecontent';
export const API_FFMPEG = '/ffmpegpath';