Merge branch 'develop' into fix/ImplementPasswordRules
@@ -1,3 +1,4 @@
|
||||
# Ignore artifacts:
|
||||
node_modules
|
||||
out
|
||||
styles/variables.css
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
import { ColorRow } from './Color';
|
||||
|
||||
<Meta title="owncast/Styles/Colors" />
|
||||
<Meta title="owncast/Styles/Colors" parameters={{ chromatic: { disableSnapshot: true } }} />
|
||||
|
||||
# Default theme colors
|
||||
|
||||
These colors are assigned in our [color token](https://github.com/owncast/owncast/tree/webv2/web/style-definitions/tokens/color) files
|
||||
These colors are assigned in our [color token](https://github.com/owncast/owncast/tree/develop/web/style-definitions/tokens/color) files
|
||||
and get reflected here as they change. run `npm run build-styles` to regenerate.
|
||||
|
||||
<Story
|
||||
name="Default Theme"
|
||||
>
|
||||
<Story name="Default Theme">
|
||||
|
||||
## Default Theme
|
||||
|
||||
These color names are assigned to specific component variables. They can be overwritten via CSS.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
|
||||
<Meta title="owncast/Documentation/Design" />
|
||||
<Meta title="owncast/Documentation/Design" parameters={{chromatic: { disableSnapshot: true }}}/>
|
||||
|
||||
# Owncast Design Guidelines & Resources
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
|
||||
<Meta title="owncast/Documentation/Get Started with Owncast Development" />
|
||||
<Meta title="owncast/Documentation/Get Started with Owncast Development" parameters={{chromatic: { disableSnapshot: true }}}/>
|
||||
|
||||
---
|
||||
title: "How to work on Owncast"
|
||||
@@ -35,8 +35,9 @@ The web frontend of Owncast is written in React with TypeScript built using [Nex
|
||||
|
||||
You can browse the React components in the project using our [Storybook](https://owncast.online/components) page to get an idea of how the frontend is structured.
|
||||
|
||||
1. Clone the Owncast repository with `git clone https://github.com/owncast/owncast`.
|
||||
1. Change to the `webv2` branch with `git checkout webv2`.
|
||||
1. Fork the Owncast repository on Github located at https://github.com/owncast/owncast.
|
||||
1. Check out your fork locally with `git clone https://github.com/yourusername/owncast`.
|
||||
1. Create a new branch for your new changes with `git checkout -b my-new-feature`.
|
||||
|
||||
### Run the web project
|
||||
|
||||
@@ -48,7 +49,7 @@ You must have an instance of Owncast running locally to connect to. You can run
|
||||
|
||||
### Learn about how to write React Components with Owncast
|
||||
|
||||
We have a [short document](https://github.com/owncast/owncast/blob/webv2/web/components/_COMPONENT_HOW_TO.md) outlining the specifics of the hows and whys of our specific component approach.
|
||||
We have a [short document](https://github.com/owncast/owncast/blob/web/components/_COMPONENT_HOW_TO.md) outlining the specifics of the hows and whys of our specific component approach.
|
||||
|
||||
### Use Storybook to update and create components
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
import { Image, ImageRow } from './ImageAsset';
|
||||
|
||||
<Meta title="owncast/Frontend Assets/Emoji" />
|
||||
<Meta title="owncast/Frontend Assets/Emoji" parameters={{chromatic: { disableSnapshot: true }}} />
|
||||
|
||||
# Built-in Custom Emoji
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
import { Image, ImageRow } from './ImageAsset';
|
||||
|
||||
<Meta title="owncast/Frontend Assets/Images" />
|
||||
<Meta title="owncast/Frontend Assets/Images" parameters={{chromatic: { disableSnapshot: true }}} />
|
||||
|
||||
# Images
|
||||
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import { Mermaid } from 'mdx-mermaid/Mermaid';
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
|
||||
<Meta
|
||||
title="owncast/Documentation/Usage Examples"
|
||||
parameters={{ chromatic: { disableSnapshot: true } }}
|
||||
/>
|
||||
|
||||
<Story name="Basic">
|
||||
|
||||
This is the most basic Owncast setup and is what you get when you run it without any additional services.
|
||||
|
||||
<Mermaid
|
||||
chart={`graph TD
|
||||
Owncast{fa:fa-server Owncast}
|
||||
|
||||
A[fa:fa-window-maximize Web App] --Video-->Owncast
|
||||
A[fa:fa-window-maximize Web App] --Web-->Owncast
|
||||
A[fa:fa-window-maximize Web App] <--Chat-->Owncast
|
||||
B[fa:fa-tv Smart TV] --Video--> Owncast
|
||||
I[fa:fa-window-restore Chat Embeds] --Web-->Owncast
|
||||
I[fa:fa-window-restore Chat Embeds] <--Chat-->Owncast
|
||||
Owncast --Video-->F[fa:fa-shapes VLC]
|
||||
Owncast --Video-->G[fa:fa-window-restore Video Embeds]
|
||||
Owncast --Web-->G[fa:fa-window-restore Video Embeds]
|
||||
Owncast --Video-->H[fa:fa-mobile-screen Mobile Apps]
|
||||
|
||||
`}
|
||||
/>
|
||||
|
||||
</Story>
|
||||
|
||||
<Story name="S3 Object Storage">
|
||||
|
||||
If you would like no video traffic to take place from your Owncast server to your clients, you can use S3 Object Storage to be responsible for this instead.
|
||||
Instead of video players pulling the stream from your Owncast server it would instead make these requests to a S3 provider, and Owncast would upload video to this provider to make it available.
|
||||
|
||||
<Mermaid
|
||||
chart={`graph TD
|
||||
subgraph Chat & Web Assets
|
||||
Owncast{fa:fa-server Owncast}
|
||||
end
|
||||
|
||||
Owncast--Upload\\nVideo-->S3
|
||||
|
||||
subgraph Video Assets
|
||||
S3[fa:fa-hard-drive S3 Object Storage]
|
||||
end
|
||||
|
||||
subgraph Clients
|
||||
WebPlayer[fa:fa-window-maximize Web App]
|
||||
Embeds[fa:fa-window-restore Embeds]
|
||||
MobileApps[fa:fa-mobile-screen Mobile Apps]
|
||||
SmartTV[fa:fa-tv Smart TV]
|
||||
VLC[fa:fa-shapes VLC]
|
||||
end
|
||||
|
||||
Owncast--Web-->WebPlayer
|
||||
Owncast--Web-->Embeds
|
||||
Owncast<--Chat-->WebPlayer
|
||||
S3--Download\\nVideo-->WebPlayer
|
||||
S3--Download\\nVideo-->Embeds
|
||||
S3--Download\\nVideo-->MobileApps
|
||||
S3--Download\\nVideo-->SmartTV
|
||||
S3--Download\\nVideo-->VLC
|
||||
|
||||
`}
|
||||
/>
|
||||
|
||||
</Story>
|
||||
|
||||
<Story name="CDN in front of S3 Object Storage">
|
||||
|
||||
If you're using a S3 provider but would like to increase the speed of your video delivery to your clients around the world, you can use a CDN in front of your S3 provider.
|
||||
This leads to not only your server not serving any video traffic, but the video traffic would be distributed globally so each client player would be pulling video from somewhere geographically closer, reducing the likelihood of buffering and slower network requests.
|
||||
|
||||
<Mermaid
|
||||
chart={`flowchart TD
|
||||
subgraph Video Assets
|
||||
S3[fa:fa-hard-drive S3 Object Storage]
|
||||
CDN[fa:fa-cloud Global CDN]
|
||||
end
|
||||
|
||||
subgraph Clients
|
||||
WebPlayer[fa:fa-window-maximize Web App]
|
||||
Embeds[fa:fa-window-restore Embeds]
|
||||
MobileApps[fa:fa-mobile-screen Mobile Apps]
|
||||
SmartTV[fa:fa-tv Smart TV]
|
||||
VLC[fa:fa-shapes VLC]
|
||||
end
|
||||
|
||||
subgraph Web Assets & Chat Service
|
||||
direction TB
|
||||
Owncast{fa:fa-server Owncast}
|
||||
end
|
||||
|
||||
Owncast--Upload\\nVideo-->S3
|
||||
Owncast--Web-->WebPlayer
|
||||
Owncast<--Chat-->WebPlayer
|
||||
Owncast--Web-->Embeds
|
||||
|
||||
CDN--Download\\nVideo-->WebPlayer
|
||||
CDN--Download\\nVideo-->Embeds
|
||||
CDN--Download\\nVideo-->MobileApps
|
||||
CDN--Download\\nVideo-->SmartTV
|
||||
CDN--Download\\nVideo-->VLC
|
||||
|
||||
S3 --- CDN
|
||||
|
||||
`}
|
||||
/>
|
||||
|
||||
</Story>
|
||||
|
||||
<Story name="CDN in front of Owncast">
|
||||
|
||||
If you're ok with some video requests coming directly to your Owncast server, and want to generally increase the overall speed that your viewers globally access all your Owncast assets, including video and the web interface, you can put a CDN in front of your entire Owncast server.
|
||||
|
||||
<Mermaid
|
||||
chart={`flowchart TD
|
||||
CDN{{fa:fa-cloud Global CDN}}
|
||||
Owncast{fa:fa-server Owncast}
|
||||
|
||||
subgraph Clients
|
||||
WebPlayer[fa:fa-window-maximize Web App]
|
||||
Embeds[fa:fa-window-restore Embeds]
|
||||
MobileApps[fa:fa-mobile-screen Mobile Apps]
|
||||
SmartTV[fa:fa-tv Smart TV]
|
||||
VLC[fa:fa-shapes VLC]
|
||||
end
|
||||
|
||||
CDN--Web-->WebPlayer
|
||||
Owncast<--Chat-->WebPlayer
|
||||
CDN--Web-->Embeds
|
||||
|
||||
CDN--Download\\nVideo-->WebPlayer
|
||||
CDN--Download\\nVideo-->Embeds
|
||||
CDN--Download\\nVideo-->MobileApps
|
||||
CDN--Download\\nVideo-->SmartTV
|
||||
CDN--Download\\nVideo-->VLC
|
||||
|
||||
CDN --- Owncast
|
||||
|
||||
`}
|
||||
/>
|
||||
|
||||
</Story>
|
||||
@@ -1,69 +0,0 @@
|
||||
import { Meta } from '@storybook/addon-docs';
|
||||
import { Typography } from 'antd';
|
||||
|
||||
<Meta title="Owncast/Documentation/Readme" />
|
||||
|
||||
<Typography.Title style={{ color: 'var(--primary-color)' }}>Owncast Web UI v2</Typography.Title>
|
||||
|
||||
Owncast is going through a complete rewrite of the web app frontend.
|
||||
|
||||
Visit the [UIv2 milestone](https://github.com/owncast/owncast/milestone/18) on GitHub to see the individual tasks for this project.
|
||||
|
||||
## Quick Links
|
||||
|
||||
- [Redesign project](https://github.com/owncast/owncast/milestone/18)
|
||||
- [Currently defined colors](/story/owncast-style-guide-colors--page)
|
||||
- [Owncast Frontend Chat](https://owncast.rocket.chat/group/frontend-dev)
|
||||
|
||||
## Why?
|
||||
|
||||
- Moving to a full React Component workflow allows the project to be more productive and build features faster.
|
||||
- Share code between the web frontend UI and the existing admin.
|
||||
- Address feedback from users.
|
||||
- Better accessibility.
|
||||
- Better mobile experience.
|
||||
- Standardize styling across the project by using a single design language and style guide.
|
||||
- Allows more people to contribute to the project if we use popular frameworks.
|
||||
|
||||
## What
|
||||
|
||||
- [Next.js](https://nextjs.org/)
|
||||
- [React](https://reactjs.org/)
|
||||
- [Ant Design](https://ant.design/)
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Find a component that hasn't yet been worked on by looking through the [UIv2 milestone](https://github.com/owncast/owncast/milestone/18)
|
||||
and the sidebar of components to the left.
|
||||
1. See if you can have an example of this functionality in action via the [Owncast Demo Server](https://watch.owncast.online) or [Owncast Nightly Build](https://nightly.owncast.online) so you know how it's supposed to work if it's interactive.
|
||||
1. Visit the `Docs` tab to read any specific documentation that may have been written about how this component works.
|
||||
1. Go to the `Canvas` tab of the component you selected and see if there's a Design attached to it.
|
||||
1. If there is a design, then that's a starting point you can use to start building out the component.
|
||||
1. If there isn't, then visit the [Owncast Demo Server](https://watch.owncast.online), the [Owncast Nightly Build](https://nightly.owncast.online), or the proposed [v2 design](https://www.figma.com/proto/B6ICOn1J3dyYeoZM5kPM2A/Owncast---Review?node-id=643%3A646&scaling=min-zoom&page-id=643%3A17&starting-point-node-id=643%3A44) for some ways to start.
|
||||
1. If no design exists, then you can ask around the Owncast chat for help, for come up with your own ideas!
|
||||
1. No designs are stuck in stone, and we're using this as an opportunity to level up the UI of Owncast, so all ideas are welcome.
|
||||
|
||||
## How?
|
||||
|
||||
This rewrite is a large project, but like anything else, breaking it into pieces and working on one thing at a time will eventually get us to the finish line.
|
||||
And that's what this interface lets us do. On this page we see all the different components still needing to be worked on, and have a place to document the functionality of these pieces.
|
||||
|
||||
## What about the Admin?
|
||||
|
||||
The admin has always been a Next+React+Ant project, so the goal is to touch that as little as possible except where needed to share code and styles.
|
||||
|
||||
## What is this page?
|
||||
|
||||
This is called [_Storybook_](https://storybook.js.org/docs/react/get-started/introduction).
|
||||
Storybook is a tool for UI development. It makes development faster and easier by isolating components.
|
||||
This allows you to work on one component at a time. You can develop entire UIs without needing to start
|
||||
up a complex dev stack, force certain data into your database, or navigate around your application.
|
||||
|
||||
For example a button may have a disabled state that requires a specific scenario to take place in real-world use,
|
||||
but here we you can just toggle the state to verify things are working as expected.
|
||||
|
||||
This means [new components should have a corresponding story added](https://storybook.js.org/docs/react/get-started/whats-a-story) to make it easier to maintain the project.
|
||||
|
||||
For more details about building React components read [this document](https://github.com/owncast/owncast/blob/webv2/web/components/_COMPONENT_HOW_TO.md) with specifics.
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/9563255/187112334-e6c73bad-72df-42fc-9ee2-8d36ba615275.png"/>
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
import { Image, ImageRow } from './ImageAsset';
|
||||
|
||||
<Meta title="owncast/Project Assets/Logos & Graphics" />
|
||||
<Meta title="owncast/Project Assets/Logos & Graphics" parameters={{chromatic: { disableSnapshot: true }}} />
|
||||
|
||||
# Logos & Graphics
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
|
||||
<Meta title="owncast/Documentation/Product Definition" />
|
||||
<Meta title="owncast/Documentation/Product Definition" parameters={{chromatic: { disableSnapshot: true }}}/>
|
||||
|
||||
# Owncast Product Definition
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
import { Image, ImageRow } from './ImageAsset';
|
||||
|
||||
<Meta title="owncast/Frontend Assets/Social Platform Images" />
|
||||
<Meta title="owncast/Frontend Assets/Social Platform Images" parameters={{chromatic: { disableSnapshot: true }}} />
|
||||
|
||||
# Social Platform Images
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
import { Image, ImageRow } from './ImageAsset';
|
||||
|
||||
<Meta title="owncast/Project Assets/T-Shirt" />
|
||||
<Meta title="owncast/Project Assets/T-Shirt" parameters={{chromatic: { disableSnapshot: true }}} />
|
||||
|
||||
# T-shirt
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
|
||||
<Meta title="owncast/Documentation/Building Frontend Components" />
|
||||
<Meta title="owncast/Documentation/Building Frontend Components" parameters={{chromatic: { disableSnapshot: true }}}/>
|
||||
|
||||
# How we develop components
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 3.2 MiB After Width: | Height: | Size: 2.7 MiB |
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 682 KiB After Width: | Height: | Size: 664 KiB |
|
Before Width: | Height: | Size: 605 KiB After Width: | Height: | Size: 598 KiB |
|
Before Width: | Height: | Size: 612 KiB After Width: | Height: | Size: 607 KiB |
|
Before Width: | Height: | Size: 690 KiB After Width: | Height: | Size: 672 KiB |
|
Before Width: | Height: | Size: 690 KiB After Width: | Height: | Size: 683 KiB |
|
Before Width: | Height: | Size: 597 KiB After Width: | Height: | Size: 594 KiB |
|
Before Width: | Height: | Size: 482 KiB After Width: | Height: | Size: 479 KiB |
|
Before Width: | Height: | Size: 725 KiB After Width: | Height: | Size: 705 KiB |
|
Before Width: | Height: | Size: 530 KiB After Width: | Height: | Size: 524 KiB |
|
Before Width: | Height: | Size: 760 KiB After Width: | Height: | Size: 741 KiB |
|
Before Width: | Height: | Size: 533 KiB After Width: | Height: | Size: 529 KiB |
|
Before Width: | Height: | Size: 502 KiB After Width: | Height: | Size: 498 KiB |
|
Before Width: | Height: | Size: 460 KiB After Width: | Height: | Size: 456 KiB |
@@ -1,5 +1,5 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
|
||||
<Meta title="owncast/Documentation/{{title}}" />
|
||||
<Meta title="owncast/Documentation/{{title}}" parameters=\{{chromatic: { disableSnapshot: true }}}/>
|
||||
|
||||
{{content}}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
import { Image, ImageRow } from './ImageAsset';
|
||||
|
||||
<Meta title="owncast/Frontend Assets/Emoji" />
|
||||
<Meta title="owncast/Frontend Assets/Emoji" parameters=\{{chromatic: { disableSnapshot: true }}} />
|
||||
|
||||
# Built-in Custom Emoji
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
import { Image, ImageRow } from './ImageAsset';
|
||||
|
||||
<Meta title="{{category}}" />
|
||||
<Meta title="{{category}}" parameters=\{{chromatic: { disableSnapshot: true }}} />
|
||||
|
||||
# {{capitalize title}}
|
||||
|
||||
|
||||
@@ -1,15 +1,35 @@
|
||||
import ChartJs from 'chart.js/auto';
|
||||
import Chartkick from 'chartkick';
|
||||
import format from 'date-fns/format';
|
||||
import { LineChart } from 'react-chartkick';
|
||||
import { FC } from 'react';
|
||||
|
||||
// from https://github.com/ankane/chartkick.js/blob/master/chart.js/chart.esm.js
|
||||
Chartkick.use(ChartJs);
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
LogarithmicScale,
|
||||
} from 'chart.js';
|
||||
import { Line } from 'react-chartjs-2';
|
||||
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LogarithmicScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
);
|
||||
|
||||
interface TimedValue {
|
||||
time: Date;
|
||||
value: number;
|
||||
pointStyle?: boolean | string;
|
||||
pointRadius?: number;
|
||||
}
|
||||
|
||||
export type ChartProps = {
|
||||
@@ -45,47 +65,46 @@ export const Chart: FC<ChartProps> = ({
|
||||
|
||||
if (data && data.length > 0) {
|
||||
renderData.push({
|
||||
name: title,
|
||||
color,
|
||||
id: title,
|
||||
label: title,
|
||||
backgroundColor: color,
|
||||
borderColor: color,
|
||||
borderWidth: 3,
|
||||
data: createGraphDataset(data),
|
||||
});
|
||||
}
|
||||
|
||||
dataCollections.forEach(collection => {
|
||||
renderData.push({
|
||||
name: collection.name,
|
||||
id: collection.name,
|
||||
label: collection.name,
|
||||
data: createGraphDataset(collection.data),
|
||||
color: collection.color,
|
||||
dataset: collection.options,
|
||||
backgroundColor: collection.color,
|
||||
borderColor: collection.color,
|
||||
borderWidth: 3,
|
||||
pointStyle: collection.pointStyle || 'circle',
|
||||
radius: collection.pointRadius || 1,
|
||||
});
|
||||
});
|
||||
|
||||
// ChartJs.defaults.scales.linear.reverse = true;
|
||||
|
||||
const options = {
|
||||
responsive: true,
|
||||
|
||||
scales: {
|
||||
y: { reverse: false, type: 'linear' },
|
||||
x: {
|
||||
type: 'time',
|
||||
y: {
|
||||
type: yLogarithmic ? ('logarithmic' as const) : ('linear' as const),
|
||||
reverse: yFlipped,
|
||||
title: {
|
||||
display: true,
|
||||
text: unit,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
options.scales.y.reverse = yFlipped;
|
||||
options.scales.y.type = yLogarithmic ? 'logarithmic' : 'linear';
|
||||
|
||||
return (
|
||||
<div className="line-chart-container">
|
||||
<LineChart
|
||||
xtitle="Time"
|
||||
ytitle={title}
|
||||
suffix={unit}
|
||||
legend="bottom"
|
||||
color={color}
|
||||
data={renderData}
|
||||
download={title}
|
||||
library={options}
|
||||
/>
|
||||
<Line data={{ datasets: renderData }} options={options} height="70vh" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -117,7 +117,7 @@ export const Offline: FC<OfflineProps> = ({ logs = [], config }) => {
|
||||
content: (
|
||||
<div>
|
||||
List yourself in the Owncast Directory and show off your stream. Enable it in{' '}
|
||||
<Link href="/config-public-details">settings.</Link>
|
||||
<Link href="/admin/config/general/">settings.</Link>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
@@ -129,7 +129,7 @@ export const Offline: FC<OfflineProps> = ({ logs = [], config }) => {
|
||||
title: 'Add your Owncast instance to the Fediverse',
|
||||
content: (
|
||||
<div>
|
||||
<Link href="/config-federation">Enable Owncast social</Link> features to have your
|
||||
<Link href="/admin/config-federation/">Enable Owncast social</Link> features to have your
|
||||
instance join the Fediverse, allowing people to follow, share and engage with your live
|
||||
stream.
|
||||
</div>
|
||||
|
||||
@@ -33,7 +33,7 @@ export const ConfigNotify = () => {
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<Link passHref href="/config-federation">
|
||||
<Link passHref href="/admin/config-federation/">
|
||||
<Button
|
||||
type="primary"
|
||||
style={{
|
||||
|
||||
@@ -8,7 +8,7 @@ export default {
|
||||
title: 'owncast/Chat/Chat messages container',
|
||||
component: ChatContainer,
|
||||
parameters: {
|
||||
chromatic: { diffThreshold: 0.2 },
|
||||
chromatic: { diffThreshold: 0.8 },
|
||||
docs: {
|
||||
description: {
|
||||
component: `
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
.root {
|
||||
padding: 10px 0px;
|
||||
text-align: center;
|
||||
font-size: 0.8rem;
|
||||
font-style: italic;
|
||||
color: var(--theme-color-components-chat-text);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FC } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { ChatUserBadge } from '../ChatUserBadge/ChatUserBadge';
|
||||
import styles from './ChatJoinMessage.module.scss';
|
||||
import { ModerationBadge } from '../ChatUserBadge/ModerationBadge';
|
||||
|
||||
// Lazy loaded components
|
||||
|
||||
@@ -31,7 +31,7 @@ export const ChatJoinMessage: FC<ChatJoinMessageProps> = ({
|
||||
<span style={{ fontWeight: 'bold' }}>{displayName}</span>
|
||||
{isAuthorModerator && (
|
||||
<span>
|
||||
<ChatUserBadge badge="mod" userColor={userColor} />
|
||||
<ModerationBadge userColor={userColor} />
|
||||
</span>
|
||||
)}
|
||||
</span>{' '}
|
||||
|
||||
@@ -33,7 +33,7 @@ export default {
|
||||
component: ChatTextField,
|
||||
parameters: {
|
||||
fetchMock: mocks,
|
||||
chromatic: { diffThreshold: 0.2 },
|
||||
chromatic: { diffThreshold: 0.8 },
|
||||
|
||||
design: {
|
||||
type: 'image',
|
||||
|
||||
17
web/components/chat/ChatUserBadge/AuthedUserBadge.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import dynamic from 'next/dynamic';
|
||||
import React, { FC } from 'react';
|
||||
import { ChatUserBadge } from './ChatUserBadge';
|
||||
|
||||
// Lazy loaded components
|
||||
|
||||
const SafetyCertificateFilled = dynamic(() => import('@ant-design/icons/SafetyCertificateFilled'), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export type AuthedUserBadgeProps = {
|
||||
userColor: number;
|
||||
};
|
||||
|
||||
export const AuthedUserBadge: FC<AuthedUserBadgeProps> = ({ userColor }) => (
|
||||
<ChatUserBadge badge={<SafetyCertificateFilled />} userColor={userColor} title="Authenticated" />
|
||||
);
|
||||
@@ -1,13 +1,15 @@
|
||||
.badge {
|
||||
font-family: var(--theme-text-display-font-family);
|
||||
font-weight: 500;
|
||||
font-size: 0.5rem;
|
||||
text-transform: uppercase;
|
||||
color: white;
|
||||
background-color: var(--color-owncast-palette-0);
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
border-radius: 2px;
|
||||
text-align: center;
|
||||
padding: 2px;
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
border-radius: 3px;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
margin-left: 3px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
margin-left: 5px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { ChatUserBadge } from './ChatUserBadge';
|
||||
import { ModerationBadge } from './ModerationBadge';
|
||||
import { AuthedUserBadge } from './AuthedUserBadge';
|
||||
|
||||
export default {
|
||||
title: 'owncast/Chat/Messages/User Flag',
|
||||
@@ -14,15 +16,26 @@ export default {
|
||||
} as ComponentMeta<typeof ChatUserBadge>;
|
||||
|
||||
const Template: ComponentStory<typeof ChatUserBadge> = args => <ChatUserBadge {...args} />;
|
||||
const ModerationTemplate: ComponentStory<typeof ModerationBadge> = args => (
|
||||
<ModerationBadge {...args} />
|
||||
);
|
||||
|
||||
export const Moderator = Template.bind({});
|
||||
const AuthedTemplate: ComponentStory<typeof ModerationBadge> = args => (
|
||||
<AuthedUserBadge {...args} />
|
||||
);
|
||||
|
||||
export const Authenticated = AuthedTemplate.bind({});
|
||||
Authenticated.args = {
|
||||
userColor: '3',
|
||||
};
|
||||
|
||||
export const Moderator = ModerationTemplate.bind({});
|
||||
Moderator.args = {
|
||||
badge: 'mod',
|
||||
userColor: '5',
|
||||
};
|
||||
|
||||
export const Authenticated = Template.bind({});
|
||||
Authenticated.args = {
|
||||
badge: 'auth',
|
||||
export const Generic = Template.bind({});
|
||||
Generic.args = {
|
||||
badge: '?',
|
||||
userColor: '6',
|
||||
};
|
||||
|
||||
@@ -4,14 +4,15 @@ import styles from './ChatUserBadge.module.scss';
|
||||
export type ChatUserBadgeProps = {
|
||||
badge: React.ReactNode;
|
||||
userColor: number;
|
||||
title: string;
|
||||
};
|
||||
|
||||
export const ChatUserBadge: FC<ChatUserBadgeProps> = ({ badge, userColor }) => {
|
||||
const color = `var(--theme-user-colors-${userColor})`;
|
||||
const style = { color, borderColor: color };
|
||||
export const ChatUserBadge: FC<ChatUserBadgeProps> = ({ badge, userColor, title }) => {
|
||||
const color = `var(--theme-color-users-${userColor})`;
|
||||
const style = { color };
|
||||
|
||||
return (
|
||||
<span style={style} className={styles.badge}>
|
||||
<span style={style} className={styles.badge} title={title}>
|
||||
{badge}
|
||||
</span>
|
||||
);
|
||||
|
||||
17
web/components/chat/ChatUserBadge/ModerationBadge.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import dynamic from 'next/dynamic';
|
||||
import React, { FC } from 'react';
|
||||
import { ChatUserBadge } from './ChatUserBadge';
|
||||
|
||||
// Lazy loaded components
|
||||
|
||||
const StarFilled = dynamic(() => import('@ant-design/icons/StarFilled'), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export type ModerationBadgeProps = {
|
||||
userColor: number;
|
||||
};
|
||||
|
||||
export const ModerationBadge: FC<ModerationBadgeProps> = ({ userColor }) => (
|
||||
<ChatUserBadge badge={<StarFilled />} userColor={userColor} title="Moderator" />
|
||||
);
|
||||
@@ -25,9 +25,18 @@ $p-size: 8px;
|
||||
position: relative;
|
||||
|
||||
mark {
|
||||
padding-left: 0.35em;
|
||||
padding-right: 0.35em;
|
||||
padding-left: 0.3em;
|
||||
padding-right: 0.3em;
|
||||
color: var(--theme-color-palette-4);
|
||||
border-radius: var(--theme-rounded-corners);
|
||||
background-color: var(--color-owncast-palette-7);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--theme-color-palette-12);
|
||||
&:hover {
|
||||
color: var(--theme-color-palette-4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,21 @@ const standardMessage: ChatMessage = JSON.parse(`{
|
||||
},
|
||||
"body": "Test message from a regular user."}`);
|
||||
|
||||
const messageWithLinkAndCustomEmoji: ChatMessage = JSON.parse(`{
|
||||
"type": "CHAT",
|
||||
"id": "wY-MEXwnR",
|
||||
"timestamp": "2022-04-28T20:30:27.001762726Z",
|
||||
"user": {
|
||||
"id": "h_5GQ6E7R",
|
||||
"displayName": "EliteMooseTaskForce",
|
||||
"displayColor": 3,
|
||||
"createdAt": "2022-03-24T03:52:37.966584694Z",
|
||||
"previousNames": ["gifted-nobel", "EliteMooseTaskForce"],
|
||||
"nameChangedAt": "2022-04-26T23:56:05.531287897Z",
|
||||
"scopes": []
|
||||
},
|
||||
"body": "Test message with a link https://owncast.online and a custom emoji <img src='/img/emoji/blob/ablobattention.gif' width='30px'/> ."}`);
|
||||
|
||||
const moderatorMessage: ChatMessage = JSON.parse(`{
|
||||
"type": "CHAT",
|
||||
"id": "wY-MEXwnR",
|
||||
@@ -80,6 +95,12 @@ WithoutModeratorMenu.args = {
|
||||
showModeratorMenu: false,
|
||||
};
|
||||
|
||||
export const WithLinkAndCustomEmoji = Template.bind({});
|
||||
WithLinkAndCustomEmoji.args = {
|
||||
message: messageWithLinkAndCustomEmoji,
|
||||
showModeratorMenu: false,
|
||||
};
|
||||
|
||||
export const WithModeratorMenu = Template.bind({});
|
||||
WithModeratorMenu.args = {
|
||||
message: standardMessage,
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
/* eslint-disable react/no-danger */
|
||||
import { FC, ReactNode, useEffect, useState } from 'react';
|
||||
import { FC, ReactNode } from 'react';
|
||||
import cn from 'classnames';
|
||||
import { Tooltip } from 'antd';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { decodeHTML } from 'entities';
|
||||
import linkifyHtml from 'linkify-html';
|
||||
import { Interweave } from 'interweave';
|
||||
import { UrlMatcher } from 'interweave-autolink';
|
||||
import { ChatMessageHighlightMatcher } from './customMatcher';
|
||||
import styles from './ChatUserMessage.module.scss';
|
||||
import { formatTimestamp } from './messageFmt';
|
||||
import { ChatMessage } from '../../../interfaces/chat-message.model';
|
||||
import { ChatUserBadge } from '../ChatUserBadge/ChatUserBadge';
|
||||
import { accessTokenAtom } from '../../stores/ClientConfigStore';
|
||||
import { User } from '../../../interfaces/user.model';
|
||||
import { AuthedUserBadge } from '../ChatUserBadge/AuthedUserBadge';
|
||||
import { ModerationBadge } from '../ChatUserBadge/ModerationBadge';
|
||||
|
||||
// Lazy loaded components
|
||||
|
||||
const LinkOutlined = dynamic(() => import('@ant-design/icons/LinkOutlined'), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const ChatModerationActionMenu = dynamic(
|
||||
() =>
|
||||
import('../ChatModerationActionMenu/ChatModerationActionMenu').then(
|
||||
@@ -29,10 +27,6 @@ const ChatModerationActionMenu = dynamic(
|
||||
},
|
||||
);
|
||||
|
||||
const Highlight = dynamic(() => import('react-highlighter-ts').then(mod => mod.Highlight), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export type ChatUserMessageProps = {
|
||||
message: ChatMessage;
|
||||
showModeratorMenu: boolean;
|
||||
@@ -74,26 +68,15 @@ export const ChatUserMessage: FC<ChatUserMessageProps> = ({
|
||||
|
||||
const color = `var(--theme-color-users-${displayColor})`;
|
||||
const formattedTimestamp = `Sent ${formatTimestamp(timestamp)}`;
|
||||
const [formattedMessage, setFormattedMessage] = useState<string>(body);
|
||||
|
||||
const badgeNodes = [];
|
||||
if (isAuthorModerator) {
|
||||
badgeNodes.push(<ChatUserBadge key="mod" badge="mod" userColor={displayColor} />);
|
||||
badgeNodes.push(<ModerationBadge key="mod" userColor={displayColor} />);
|
||||
}
|
||||
if (isAuthorAuthenticated) {
|
||||
badgeNodes.push(
|
||||
<ChatUserBadge
|
||||
key="auth"
|
||||
badge={<LinkOutlined title="authenticated" />}
|
||||
userColor={displayColor}
|
||||
/>,
|
||||
);
|
||||
badgeNodes.push(<AuthedUserBadge key="auth" userColor={displayColor} />);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setFormattedMessage(decodeHTML(body));
|
||||
}, [message]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -119,12 +102,14 @@ export const ChatUserMessage: FC<ChatUserMessageProps> = ({
|
||||
</UserTooltip>
|
||||
)}
|
||||
<Tooltip title={formattedTimestamp} mouseEnterDelay={1}>
|
||||
<Highlight search={highlightString}>
|
||||
<div
|
||||
className={styles.message}
|
||||
dangerouslySetInnerHTML={{ __html: linkifyHtml(formattedMessage) }}
|
||||
/>
|
||||
</Highlight>
|
||||
<Interweave
|
||||
className={styles.message}
|
||||
content={body}
|
||||
matchers={[
|
||||
new UrlMatcher('url', { validateTLD: false }),
|
||||
new ChatMessageHighlightMatcher('highlight', { highlightString }),
|
||||
]}
|
||||
/>
|
||||
</Tooltip>
|
||||
{showModeratorMenu && (
|
||||
<div className={styles.modMenuWrapper}>
|
||||
|
||||
44
web/components/chat/ChatUserMessage/customMatcher.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/* eslint-disable class-methods-use-this */
|
||||
import { ChildrenNode, Matcher, MatchResponse, Node } from 'interweave';
|
||||
import React from 'react';
|
||||
|
||||
export interface CustomProps {
|
||||
children: React.ReactNode;
|
||||
key: string;
|
||||
}
|
||||
|
||||
interface options {
|
||||
highlightString: string;
|
||||
}
|
||||
|
||||
export class ChatMessageHighlightMatcher extends Matcher {
|
||||
match(str: string): MatchResponse<{}> | null {
|
||||
const { highlightString } = this.options as options;
|
||||
|
||||
if (!highlightString) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = str.match(highlightString);
|
||||
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
index: result.index!,
|
||||
length: result[0].length,
|
||||
match: result[0],
|
||||
valid: true,
|
||||
};
|
||||
}
|
||||
|
||||
replaceWith(children: ChildrenNode, props: CustomProps): Node {
|
||||
const { key } = props;
|
||||
return React.createElement('mark', { key }, children);
|
||||
}
|
||||
|
||||
asTag(): string {
|
||||
return 'mark';
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,8 @@
|
||||
import { convertToText } from '../chat';
|
||||
import { getDiffInDaysFromNow } from '../../../utils/helpers';
|
||||
|
||||
const stripTags = (str: string) => str && str.replace(/<\/?[^>]+(>|$)/g, '');
|
||||
const convertToMarkup = (str = '') => convertToText(str).replace(/\n/g, '<p></p>');
|
||||
|
||||
function getInstagramEmbedFromURL(url: string) {
|
||||
const urlObject = new URL(url.replace(/\/$/, ''));
|
||||
urlObject.pathname += '/embed';
|
||||
return `<iframe class="chat-embed instagram-embed" src="${urlObject.href}" frameborder="0" allowfullscreen></iframe>`;
|
||||
}
|
||||
|
||||
function isMessageJustAnchor(embedText: string, message: string, anchors: HTMLAnchorElement[]) {
|
||||
if (embedText !== '' && anchors.length === 1) return false;
|
||||
return stripTags(message) === stripTags(anchors[0]?.innerHTML);
|
||||
}
|
||||
|
||||
function getMessageWithEmbeds(message: string) {
|
||||
let embedText = '';
|
||||
// Make a temporary element so we can actually parse the html and pull anchor tags from it.
|
||||
// This is a better approach than regex.
|
||||
const container = document.createElement('p');
|
||||
container.innerHTML = message;
|
||||
|
||||
const anchors = Array.from(container.querySelectorAll('a'));
|
||||
anchors.forEach(({ href }) => {
|
||||
if (href.includes('instagram.com/p/')) embedText += getInstagramEmbedFromURL(href);
|
||||
});
|
||||
|
||||
// If this message only consists of a single embeddable link
|
||||
// then only return the embed and strip the link url from the text.
|
||||
if (isMessageJustAnchor(embedText, message, anchors)) return embedText;
|
||||
return message + embedText;
|
||||
}
|
||||
|
||||
export function formatTimestamp(sentAt: Date) {
|
||||
const now = new Date(sentAt);
|
||||
if (Number.isNaN(now)) return '';
|
||||
@@ -56,8 +26,6 @@ export function formatTimestamp(sentAt: Date) {
|
||||
*/
|
||||
|
||||
export function formatMessageText(message: string) {
|
||||
let formattedText = getMessageWithEmbeds(message);
|
||||
formattedText = convertToMarkup(formattedText);
|
||||
const formattedText = convertToMarkup(message);
|
||||
return formattedText;
|
||||
// return await highlightUsername(formattedText, username);
|
||||
}
|
||||
|
||||
@@ -3,17 +3,18 @@ import cn from 'classnames';
|
||||
import styles from './OwncastLogo.module.scss';
|
||||
|
||||
export type LogoProps = {
|
||||
variant: 'simple' | 'contrast';
|
||||
variant?: 'simple' | 'contrast';
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const OwncastLogo: FC<LogoProps> = ({ variant = 'simple' }) => {
|
||||
export const OwncastLogo: FC<LogoProps> = ({ variant = 'simple', className = '' }) => {
|
||||
const rootClassName = cn(styles.root, {
|
||||
[styles.simple]: variant === 'simple',
|
||||
[styles.contrast]: variant === 'contrast',
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={rootClassName}>
|
||||
<div className={`${rootClassName} ${className}`}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 95.68623352050781 104.46271514892578"
|
||||
|
||||
@@ -24,6 +24,7 @@ import { Theme } from '../../theme/Theme';
|
||||
import styles from './Main.module.scss';
|
||||
import { PushNotificationServiceWorker } from '../../workers/PushNotificationServiceWorker/PushNotificationServiceWorker';
|
||||
import { AppStateOptions } from '../../stores/application-state';
|
||||
import { Noscript } from '../../ui/Noscript/Noscript';
|
||||
|
||||
const lockBodyStyle = `
|
||||
body {
|
||||
@@ -152,6 +153,8 @@ export const Main: FC = () => {
|
||||
<FatalErrorStateModal title={fatalError.title} message={fatalError.message} />
|
||||
)}
|
||||
</Layout>
|
||||
|
||||
<Noscript />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ export default {
|
||||
title: 'owncast/Modals/Browser Notifications',
|
||||
component: BrowserNotifyModal,
|
||||
parameters: {
|
||||
chromatic: { diffThreshold: 0.7 },
|
||||
design: {
|
||||
type: 'image',
|
||||
url: BrowserNotifyModalMock,
|
||||
|
||||
@@ -158,7 +158,7 @@ export const ClientConfigStore: FC = () => {
|
||||
const [currentUser, setCurrentUser] = useRecoilState(currentUserAtom);
|
||||
const setChatAuthenticated = useSetRecoilState<boolean>(chatAuthenticatedAtom);
|
||||
const [clientConfig, setClientConfig] = useRecoilState<ClientConfig>(clientConfigStateAtom);
|
||||
const [serverStatus, setServerStatus] = useRecoilState<ServerStatus>(serverStatusState);
|
||||
const [, setServerStatus] = useRecoilState<ServerStatus>(serverStatusState);
|
||||
const setClockSkew = useSetRecoilState<Number>(clockSkewAtom);
|
||||
const [chatMessages, setChatMessages] = useRecoilState<ChatMessage[]>(chatMessagesAtom);
|
||||
const [accessToken, setAccessToken] = useRecoilState<string>(accessTokenAtom);
|
||||
@@ -166,7 +166,6 @@ export const ClientConfigStore: FC = () => {
|
||||
const setGlobalFatalErrorMessage = useSetRecoilState<DisplayableError>(fatalErrorStateAtom);
|
||||
const setWebsocketService = useSetRecoilState<WebsocketService>(websocketServiceAtom);
|
||||
const [hiddenMessageIds, setHiddenMessageIds] = useRecoilState<string[]>(removedMessageIdsAtom);
|
||||
const [, setHasLoadedStatus] = useState(false);
|
||||
const [hasLoadedConfig, setHasLoadedConfig] = useState(false);
|
||||
|
||||
let ws: WebsocketService;
|
||||
@@ -177,21 +176,27 @@ export const ClientConfigStore: FC = () => {
|
||||
message,
|
||||
});
|
||||
};
|
||||
const sendEvent = (event: string) => {
|
||||
const sendEvent = (events: string[]) => {
|
||||
// console.debug('---- sending event:', event);
|
||||
appStateSend({ type: event });
|
||||
appStateSend(events);
|
||||
};
|
||||
|
||||
const handleStatusChange = (status: ServerStatus) => {
|
||||
if (appState.matches('loading')) {
|
||||
sendEvent(AppStateEvent.Loaded);
|
||||
const events = [AppStateEvent.Loaded];
|
||||
if (status.online) {
|
||||
events.push(AppStateEvent.Online);
|
||||
} else {
|
||||
events.push(AppStateEvent.Offline);
|
||||
}
|
||||
sendEvent(events);
|
||||
return;
|
||||
}
|
||||
|
||||
if (status.online && appState.matches('ready')) {
|
||||
sendEvent(AppStateEvent.Online);
|
||||
sendEvent([AppStateEvent.Online]);
|
||||
} else if (!status.online && !appState.matches('ready.offline')) {
|
||||
sendEvent(AppStateEvent.Offline);
|
||||
sendEvent([AppStateEvent.Offline]);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -210,8 +215,9 @@ export const ClientConfigStore: FC = () => {
|
||||
const updateServerStatus = async () => {
|
||||
try {
|
||||
const status = await ServerStatusService.getStatus();
|
||||
handleStatusChange(status);
|
||||
setServerStatus(status);
|
||||
setHasLoadedStatus(true);
|
||||
|
||||
const { serverTime } = status;
|
||||
|
||||
const clockSkew = new Date(serverTime).getTime() - Date.now();
|
||||
@@ -219,7 +225,7 @@ export const ClientConfigStore: FC = () => {
|
||||
|
||||
setGlobalFatalErrorMessage(null);
|
||||
} catch (error) {
|
||||
sendEvent(AppStateEvent.Fail);
|
||||
sendEvent([AppStateEvent.Fail]);
|
||||
setGlobalFatalError('Unable to reach Owncast server', serverConnectivityError);
|
||||
console.error(`serverStatusState -> getStatus() ERROR: \n${error}`);
|
||||
}
|
||||
@@ -233,7 +239,7 @@ export const ClientConfigStore: FC = () => {
|
||||
}
|
||||
|
||||
try {
|
||||
sendEvent(AppStateEvent.NeedsRegister);
|
||||
sendEvent([AppStateEvent.NeedsRegister]);
|
||||
const response = await ChatService.registerUser(optionalDisplayName);
|
||||
const { accessToken: newAccessToken, displayName: newDisplayName, displayColor } = response;
|
||||
if (!newAccessToken) {
|
||||
@@ -248,7 +254,7 @@ export const ClientConfigStore: FC = () => {
|
||||
setAccessToken(newAccessToken);
|
||||
setLocalStorage(ACCESS_TOKEN_KEY, newAccessToken);
|
||||
} catch (e) {
|
||||
sendEvent(AppStateEvent.Fail);
|
||||
sendEvent([AppStateEvent.Fail]);
|
||||
console.error(`ChatService -> registerUser() ERROR: \n${e}`);
|
||||
}
|
||||
};
|
||||
@@ -356,17 +362,13 @@ export const ClientConfigStore: FC = () => {
|
||||
if ((window as any).statusHydration) {
|
||||
const status = JSON.parse((window as any).statusHydration);
|
||||
setServerStatus(status);
|
||||
setHasLoadedStatus(true);
|
||||
handleStatusChange(status);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('error parsing status hydration', e);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
handleStatusChange(serverStatus);
|
||||
}, [serverStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!clientConfig.chatDisabled && accessToken && hasLoadedConfig) {
|
||||
startChat();
|
||||
|
||||
@@ -77,6 +77,7 @@ const OwncastPlayer = dynamic(
|
||||
() => import('../../video/OwncastPlayer/OwncastPlayer').then(mod => mod.OwncastPlayer),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => <Skeleton loading active paragraph={{ rows: 12 }} />,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -103,7 +104,7 @@ const DesktopContent = ({
|
||||
}) => {
|
||||
const aboutTabContent = <CustomPageContent content={extraPageContent} />;
|
||||
const followersTabContent = (
|
||||
<div style={{ minHeight: '16vh' }}>
|
||||
<div>
|
||||
<FollowerCollection name={name} onFollowButtonClick={() => setShowFollowModal(true)} />
|
||||
</div>
|
||||
);
|
||||
@@ -343,16 +344,18 @@ export const Content: FC = () => {
|
||||
/>
|
||||
)}
|
||||
{!online && !appState.appLoading && (
|
||||
<OfflineBanner
|
||||
showsHeader={false}
|
||||
streamName={name}
|
||||
customText={offlineMessage}
|
||||
notificationsEnabled={browserNotificationsEnabled}
|
||||
fediverseAccount={fediverseAccount}
|
||||
lastLive={lastDisconnectTime}
|
||||
onNotifyClick={() => setShowNotifyModal(true)}
|
||||
onFollowClick={() => setShowFollowModal(true)}
|
||||
/>
|
||||
<div id="offline-message">
|
||||
<OfflineBanner
|
||||
showsHeader={false}
|
||||
streamName={name}
|
||||
customText={offlineMessage}
|
||||
notificationsEnabled={browserNotificationsEnabled}
|
||||
fediverseAccount={fediverseAccount}
|
||||
lastLive={lastDisconnectTime}
|
||||
onNotifyClick={() => setShowNotifyModal(true)}
|
||||
onFollowClick={() => setShowFollowModal(true)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isStreamLive && (
|
||||
<Statusbar
|
||||
|
||||
@@ -6,7 +6,9 @@ import { Header } from './Header';
|
||||
export default {
|
||||
title: 'owncast/Layout/Header',
|
||||
component: Header,
|
||||
parameters: {},
|
||||
parameters: {
|
||||
chromatic: { diffThreshold: 0.75 },
|
||||
},
|
||||
} as ComponentMeta<typeof Header>;
|
||||
|
||||
const Template: ComponentStory<typeof Header> = args => (
|
||||
|
||||
@@ -29,10 +29,14 @@ export const Header: FC<HeaderComponentProps> = ({
|
||||
online,
|
||||
}) => (
|
||||
<header className={cn([`${styles.header}`], 'global-header')}>
|
||||
{online && (
|
||||
{online ? (
|
||||
<Link href="#player" className={styles.skipLink}>
|
||||
Skip to player
|
||||
</Link>
|
||||
) : (
|
||||
<Link href="#offline-message" className={styles.skipLink}>
|
||||
Skip to offline message
|
||||
</Link>
|
||||
)}
|
||||
<Link href="#skip-to-content" className={styles.skipLink}>
|
||||
Skip to page content
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
.image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
50
web/components/ui/Noscript/Noscript.module.scss
Normal file
@@ -0,0 +1,50 @@
|
||||
@import '../../../styles/mixins.scss';
|
||||
|
||||
.noscript {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 2em;
|
||||
font-size: large;
|
||||
background-color: var(--theme-color-background-main);
|
||||
z-index: 999;
|
||||
|
||||
h2 {
|
||||
margin-top: 25px;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
font-size: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
// Necessary in case content y-overflows becuase
|
||||
// align-items: center would otherwise hide some
|
||||
// of the content
|
||||
.scrollContainer {
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.content {
|
||||
max-width: 100%;
|
||||
width: 70ch;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 70%;
|
||||
|
||||
// For some weir reason, the logo isn't displayed on screens <= 767px.
|
||||
// This coincides with the tablet breakpoint, but god knows what exactly
|
||||
// the issue is. Since it's just a design element, just hide the logo on
|
||||
// those smaller screens. For more information, see
|
||||
// https://github.com/owncast/owncast/pull/2592
|
||||
@include screen(tablet) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
43
web/components/ui/Noscript/Noscript.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { FC } from 'react';
|
||||
import { OwncastLogo } from '../../common/OwncastLogo/OwncastLogo';
|
||||
import styles from './Noscript.module.scss';
|
||||
|
||||
export const Noscript: FC = () => (
|
||||
<noscript className={styles.noscript}>
|
||||
<div className={styles.scrollContainer}>
|
||||
<div className={styles.content}>
|
||||
<OwncastLogo className={styles.logo} />
|
||||
<br />
|
||||
<p>
|
||||
This website is powered by
|
||||
<a href="https://owncast.online" rel="noopener noreferrer" target="_blank">
|
||||
Owncast
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<p>
|
||||
Owncast uses JavaScript for playing the HTTP Live Streaming (HLS) video, and its chat
|
||||
client. But your web browser does not seem to support JavaScript, or you have it disabled.
|
||||
</p>
|
||||
<p>
|
||||
For the best experience, you should use a different browser with JavaScript support. If
|
||||
you have disabled JavaScript in your browser, you can re-enable it.
|
||||
</p>
|
||||
<h2>How can I watch this stream without JavaScript?</h2>
|
||||
<p>
|
||||
You can open the URL of this website in your media player (such as
|
||||
<a href="https://mpv.io" rel="noopener noreferrer" target="_blank">
|
||||
mpv
|
||||
</a>
|
||||
or
|
||||
<a href="https://www.videolan.org/vlc/" rel="noopener noreferrer" target="_blank">
|
||||
VLC
|
||||
</a>
|
||||
) to watch the stream.
|
||||
</p>
|
||||
<h2>How can I chat with the others without JavaScript?</h2>
|
||||
<p>Currently, there is no option to use the chat without JavaScript.</p>
|
||||
</div>
|
||||
</div>
|
||||
</noscript>
|
||||
);
|
||||
@@ -1,3 +1,5 @@
|
||||
@import '../../../styles/mixins.scss';
|
||||
|
||||
.outerContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -11,9 +13,16 @@
|
||||
background-color: var(--theme-color-background-main);
|
||||
margin: 3rem auto;
|
||||
border-radius: var(--theme-rounded-corners);
|
||||
padding: 2.5em;
|
||||
font-size: 1.2rem;
|
||||
padding: 2.4em;
|
||||
font-size: 1.3rem;
|
||||
border: 1px solid lightgray;
|
||||
font-family: var(--theme-text-display-font-family);
|
||||
|
||||
@include screen(tablet) {
|
||||
font-size: 1.2rem;
|
||||
padding: 1em;
|
||||
margin: 1rem auto;
|
||||
}
|
||||
}
|
||||
|
||||
.bodyText {
|
||||
@@ -29,6 +38,7 @@
|
||||
margin-top: 15px;
|
||||
font-size: 1rem;
|
||||
opacity: 0.5;
|
||||
font-family: var(--theme-text-body-font-family);
|
||||
|
||||
.clockIcon {
|
||||
margin-right: 5px;
|
||||
|
||||
@@ -3,6 +3,7 @@ import intervalToDuration from 'date-fns/intervalToDuration';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import styles from './Statusbar.module.scss';
|
||||
import { pluralize } from '../../../utils/helpers';
|
||||
|
||||
// Lazy loaded components
|
||||
|
||||
@@ -18,15 +19,23 @@ export type StatusbarProps = {
|
||||
};
|
||||
|
||||
function makeDurationString(lastConnectTime: Date): string {
|
||||
const DAY_LABEL = 'day';
|
||||
const HOUR_LABEL = 'hour';
|
||||
const MINUTE_LABEL = 'minute';
|
||||
const SECOND_LABEL = 'second';
|
||||
const diff = intervalToDuration({ start: lastConnectTime, end: new Date() });
|
||||
if (diff.days > 1) {
|
||||
return `${diff.days} days ${diff.hours} hours`;
|
||||
|
||||
if (diff.days >= 1) {
|
||||
return `${diff.days} ${pluralize(DAY_LABEL, diff.days)}
|
||||
${diff.hours} ${pluralize(HOUR_LABEL, diff.hours)}`;
|
||||
}
|
||||
if (diff.hours >= 1) {
|
||||
return `${diff.hours} hours ${diff.minutes} minutes`;
|
||||
return `${diff.hours} ${pluralize(HOUR_LABEL, diff.hours)} ${diff.minutes}
|
||||
${pluralize(MINUTE_LABEL, diff.minutes)}`;
|
||||
}
|
||||
|
||||
return `${diff.minutes} minutes ${diff.seconds} seconds`;
|
||||
return `${diff.minutes} ${pluralize(MINUTE_LABEL, diff.minutes)}
|
||||
${diff.seconds} ${pluralize(SECOND_LABEL, diff.seconds)}`;
|
||||
}
|
||||
|
||||
export const Statusbar: FC<StatusbarProps> = ({
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
.account {
|
||||
color: var(--theme-color-components-text-on-light);
|
||||
word-break: break-all;
|
||||
line-height: 0.9rem;
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
@include screen(mobile) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Avatar, Col, Row } from 'antd';
|
||||
import { Avatar, Col, Row, Typography } from 'antd';
|
||||
import React, { FC } from 'react';
|
||||
import cn from 'classnames';
|
||||
import { Follower } from '../../../../interfaces/follower';
|
||||
@@ -17,9 +17,13 @@ export const SingleFollower: FC<SingleFollowerProps> = ({ follower }) => (
|
||||
<img src="/logo" alt="Logo" className={styles.placeholder} />
|
||||
</Avatar>
|
||||
</Col>
|
||||
<Col>
|
||||
<Row className={styles.name}>{follower.name}</Row>
|
||||
<Row className={styles.account}>{follower.username}</Row>
|
||||
<Col span={18}>
|
||||
<Row className={styles.name}>
|
||||
<Typography.Text ellipsis>{follower.name}</Typography.Text>
|
||||
</Row>
|
||||
<Row className={styles.account}>
|
||||
<Typography.Text ellipsis>{follower.username}</Typography.Text>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
</a>
|
||||
|
||||
@@ -4,9 +4,14 @@
|
||||
display: grid;
|
||||
width: 100%;
|
||||
justify-items: center;
|
||||
max-height: 75vh;
|
||||
height: 75vh;
|
||||
aspect-ratio: 16 / 9;
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
height: unset;
|
||||
max-height: 75vh;
|
||||
}
|
||||
|
||||
.player,
|
||||
.poster {
|
||||
width: 100%;
|
||||
|
||||
@@ -1,23 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 46 42" style="enable-background:new 0 0 46 42;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{clip-path:url(#SVGID_2_);}
|
||||
</style>
|
||||
<g>
|
||||
<defs>
|
||||
<path id="SVGID_1_" d="M22.2,24.2L8.5,39.9c-0.5,0.6-0.1,1.5,0.7,1.5h27.5c0.8,0,1.2-0.9,0.7-1.5L23.8,24.2
|
||||
c-0.2-0.2-0.5-0.4-0.8-0.4C22.7,23.8,22.4,23.9,22.2,24.2 M6.5,0.6c-2.3,0-3.1,0.2-3.9,0.7C1.8,1.7,1.1,2.4,0.7,3.2
|
||||
C0.2,4,0,4.9,0,7.1v17.5c0,2.3,0.2,3.1,0.7,3.9c0.4,0.8,1.1,1.5,1.9,1.9c0.8,0.4,1.7,0.7,3.9,0.7h5.2l2.2-2.6H5.8
|
||||
c-1.1,0-1.6-0.1-2-0.3c-0.4-0.2-0.7-0.5-1-1c-0.2-0.4-0.3-0.8-0.3-2v-19c0-1.1,0.1-1.6,0.3-2c0.2-0.4,0.5-0.7,1-1
|
||||
c0.4-0.2,0.8-0.3,2-0.3h34.3c1.1,0,1.6,0.1,2,0.3c0.4,0.2,0.7,0.5,1,1c0.2,0.4,0.3,0.8,0.3,2v19c0,1.1-0.1,1.6-0.3,2
|
||||
c-0.2,0.4-0.5,0.7-1,1c-0.4,0.2-0.8,0.3-2,0.3H32l2.2,2.6h5.2c2.3,0,3.1-0.2,3.9-0.7c0.8-0.4,1.5-1.1,1.9-1.9
|
||||
c0.4-0.8,0.7-1.7,0.7-3.9V7.1c0-2.3-0.2-3.1-0.7-3.9c-0.4-0.8-1.1-1.5-1.9-1.9c-0.8-0.4-1.7-0.7-3.9-0.7H6.5z"/>
|
||||
</defs>
|
||||
<clipPath id="SVGID_2_">
|
||||
<use xlink:href="#SVGID_1_" style="overflow:visible;"/>
|
||||
</clipPath>
|
||||
<rect x="-6.4" y="-5.8" class="st0" width="58.7" height="53.6"/>
|
||||
</g>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="Layer_1" x="0" y="0" version="1.1" viewBox="0 0 46 42" style="enable-background:new 0 0 46 42" xml:space="preserve"><style type="text/css">.st0{clip-path:url(#SVGID_2_)}</style><g><defs><path id="SVGID_1_" d="M22.2,24.2L8.5,39.9c-0.5,0.6-0.1,1.5,0.7,1.5h27.5c0.8,0,1.2-0.9,0.7-1.5L23.8,24.2 c-0.2-0.2-0.5-0.4-0.8-0.4C22.7,23.8,22.4,23.9,22.2,24.2 M6.5,0.6c-2.3,0-3.1,0.2-3.9,0.7C1.8,1.7,1.1,2.4,0.7,3.2 C0.2,4,0,4.9,0,7.1v17.5c0,2.3,0.2,3.1,0.7,3.9c0.4,0.8,1.1,1.5,1.9,1.9c0.8,0.4,1.7,0.7,3.9,0.7h5.2l2.2-2.6H5.8 c-1.1,0-1.6-0.1-2-0.3c-0.4-0.2-0.7-0.5-1-1c-0.2-0.4-0.3-0.8-0.3-2v-19c0-1.1,0.1-1.6,0.3-2c0.2-0.4,0.5-0.7,1-1 c0.4-0.2,0.8-0.3,2-0.3h34.3c1.1,0,1.6,0.1,2,0.3c0.4,0.2,0.7,0.5,1,1c0.2,0.4,0.3,0.8,0.3,2v19c0,1.1-0.1,1.6-0.3,2 c-0.2,0.4-0.5,0.7-1,1c-0.4,0.2-0.8,0.3-2,0.3H32l2.2,2.6h5.2c2.3,0,3.1-0.2,3.9-0.7c0.8-0.4,1.5-1.1,1.9-1.9 c0.4-0.8,0.7-1.7,0.7-3.9V7.1c0-2.3-0.2-3.1-0.7-3.9c-0.4-0.8-1.1-1.5-1.9-1.9c-0.8-0.4-1.7-0.7-3.9-0.7H6.5z"/></defs><clipPath id="SVGID_2_"><use xlink:href="#SVGID_1_" style="overflow:visible"/></clipPath><rect width="58.7" height="53.6" x="-6.4" y="-5.8" class="st0"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -31,7 +31,7 @@
|
||||
|
||||
- This admin site chooses to have a generally Dark color palette, but with colors that are different from Ant design's _dark_ stylesheet, so that style sheet is not included. This results in a very large `ant-overrides.scss` file to reset colors on frequently used Ant components in the system. If you find yourself a new Ant Component that has not yet been used in this app, feel free to add a reset style for that component to the overrides stylesheet.
|
||||
|
||||
- Take a look at `variables.scss` CSS file if you want to give some elements custom css colors.
|
||||
- Take a look at `variables.css` CSS file if you want to give some elements custom css colors.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
||||
|
||||
module.exports = withBundleAnalyzer(
|
||||
withLess({
|
||||
productionBrowserSourceMaps: true,
|
||||
productionBrowserSourceMaps: process.env.SOURCE_MAPS === 'true',
|
||||
trailingSlash: true,
|
||||
reactStrictMode: true,
|
||||
images: {
|
||||
|
||||
1806
web/package-lock.json
generated
@@ -13,7 +13,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "4.8.0",
|
||||
"@codemirror/lang-css": "6.0.1",
|
||||
"@codemirror/lang-css": "6.0.2",
|
||||
"@codemirror/lang-javascript": "^6.1.2",
|
||||
"@codemirror/lang-markdown": "6.0.5",
|
||||
"@codemirror/language-data": "6.1.0",
|
||||
@@ -23,56 +23,56 @@
|
||||
"@uiw/codemirror-theme-bbedit": "4.19.7",
|
||||
"@uiw/react-codemirror": "4.19.7",
|
||||
"@xstate/react": "3.0.2",
|
||||
"antd": "4.24.3",
|
||||
"antd": "4.24.7",
|
||||
"autoprefixer": "10.4.13",
|
||||
"chart.js": "4.2.0",
|
||||
"chartkick": "5.0.1",
|
||||
"chart.js": "^4.2.0",
|
||||
"classnames": "2.3.2",
|
||||
"date-fns": "2.29.3",
|
||||
"entities": "^4.4.0",
|
||||
"linkify-html": "^4.1.0",
|
||||
"date-fns": "^2.29.3",
|
||||
"interweave": "^13.0.0",
|
||||
"interweave-autolink": "^5.1.0",
|
||||
"linkifyjs": "^4.1.0",
|
||||
"lodash": "4.17.21",
|
||||
"next": "13.1.5",
|
||||
"next": "13.1.6",
|
||||
"next-with-less": "2.0.5",
|
||||
"picmo": "5.7.2",
|
||||
"picmo": "5.7.3",
|
||||
"postcss-flexbugs-fixes": "5.0.2",
|
||||
"prop-types": "15.8.1",
|
||||
"react": "18.2.0",
|
||||
"react-chartkick": "0.5.3",
|
||||
"react-chartjs-2": "^5.2.0",
|
||||
"react-chartkick": "^0.5.3",
|
||||
"react-crossfade-img": "1.0.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-highlighter-ts": "18.0.1",
|
||||
"react-hotkeys-hook": "4.3.2",
|
||||
"react-hotkeys-hook": "4.3.4",
|
||||
"react-linkify": "1.0.0-alpha",
|
||||
"react-markdown": "8.0.5",
|
||||
"react-use": "^17.4.0",
|
||||
"react-virtuoso": "4.0.5",
|
||||
"react-virtuoso": "4.0.7",
|
||||
"recoil": "0.7.6",
|
||||
"sharp": "0.31.3",
|
||||
"slate": "0.88.1",
|
||||
"slate-react": "0.88.2",
|
||||
"ua-parser-js": "1.0.33",
|
||||
"video.js": "7.20.3",
|
||||
"xstate": "4.35.2",
|
||||
"xstate": "4.35.4",
|
||||
"yaml": "2.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.20.12",
|
||||
"@mdx-js/react": "2.2.1",
|
||||
"@storybook/addon-a11y": "6.5.15",
|
||||
"@storybook/addon-actions": "6.5.15",
|
||||
"@storybook/addon-docs": "6.5.15",
|
||||
"@storybook/addon-essentials": "6.5.15",
|
||||
"@storybook/addon-links": "6.5.15",
|
||||
"@storybook/addon-a11y": "6.5.16",
|
||||
"@storybook/addon-actions": "6.5.16",
|
||||
"@storybook/addon-docs": "6.5.16",
|
||||
"@storybook/addon-essentials": "6.5.16",
|
||||
"@storybook/addon-links": "6.5.16",
|
||||
"@storybook/addon-postcss": "2.0.0",
|
||||
"@storybook/addon-viewport": "6.5.15",
|
||||
"@storybook/builder-webpack5": "6.5.15",
|
||||
"@storybook/cli": "6.5.15",
|
||||
"@storybook/manager-webpack5": "6.5.15",
|
||||
"@storybook/addon-viewport": "6.5.16",
|
||||
"@storybook/builder-webpack5": "6.5.16",
|
||||
"@storybook/cli": "6.5.16",
|
||||
"@storybook/manager-webpack5": "6.5.16",
|
||||
"@storybook/mdx2-csf": "0.0.3",
|
||||
"@storybook/preset-scss": "1.0.3",
|
||||
"@storybook/react": "6.5.15",
|
||||
"@storybook/react": "6.5.16",
|
||||
"@storybook/testing-library": "0.0.13",
|
||||
"@svgr/webpack": "6.5.1",
|
||||
"@types/chart.js": "2.9.37",
|
||||
@@ -90,14 +90,14 @@
|
||||
"chromatic": "6.15.0",
|
||||
"css-loader": "6.7.3",
|
||||
"cypress": "^12.0.0",
|
||||
"eslint": "8.32.0",
|
||||
"eslint": "8.33.0",
|
||||
"eslint-config-airbnb": "19.0.4",
|
||||
"eslint-config-next": "13.1.5",
|
||||
"eslint-config-next": "13.1.6",
|
||||
"eslint-config-prettier": "8.6.0",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"eslint-plugin-jsx-a11y": "6.7.1",
|
||||
"eslint-plugin-prettier": "4.2.1",
|
||||
"eslint-plugin-react": "7.32.1",
|
||||
"eslint-plugin-react": "7.32.2",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"eslint-plugin-storybook": "0.6.10",
|
||||
"handlebars": "^4.7.7",
|
||||
@@ -105,16 +105,18 @@
|
||||
"install": "^0.13.0",
|
||||
"less": "4.1.3",
|
||||
"less-loader": "11.1.0",
|
||||
"mdx-mermaid": "^1.3.2",
|
||||
"mermaid": "^9.3.0",
|
||||
"npm": "^9.4.0",
|
||||
"prettier": "2.8.3",
|
||||
"sass": "1.57.1",
|
||||
"sass": "1.58.0",
|
||||
"sass-loader": "13.2.0",
|
||||
"sb": "6.5.15",
|
||||
"sb": "6.5.16",
|
||||
"storybook-addon-designs": "6.3.1",
|
||||
"storybook-addon-fetch-mock": "1.0.1",
|
||||
"storybook-preset-less": "1.1.3",
|
||||
"style-dictionary": "3.7.2",
|
||||
"style-loader": "3.3.1",
|
||||
"typescript": "4.9.4"
|
||||
"typescript": "4.9.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,16 +69,19 @@ export default function HardwareInfo() {
|
||||
name: 'CPU',
|
||||
color: '#B63FFF',
|
||||
data: hardwareStatus.cpu,
|
||||
pointStyle: 'rect',
|
||||
},
|
||||
{
|
||||
name: 'Memory',
|
||||
color: '#2087E2',
|
||||
data: hardwareStatus.memory,
|
||||
pointStyle: 'circle',
|
||||
},
|
||||
{
|
||||
name: 'Disk',
|
||||
color: '#FF7700',
|
||||
data: hardwareStatus.disk,
|
||||
pointStyle: 'rectRounded',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -168,7 +168,7 @@ export default function Help() {
|
||||
<div>
|
||||
Most general questions are answered in our
|
||||
<a
|
||||
href="https://owncast.online/docs/faq/?source=admin"
|
||||
href="https://owncast.online/faq/?source=admin"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
|
||||
@@ -130,13 +130,13 @@ const StreamHealth = () => {
|
||||
{
|
||||
name: 'Errors',
|
||||
color: '#B63FFF',
|
||||
options: { radius: 3 },
|
||||
data: errors,
|
||||
pointStyle: 'crossRot',
|
||||
pointRadius: 7,
|
||||
},
|
||||
{
|
||||
name: 'Quality changes',
|
||||
color: '#2087E2',
|
||||
options: { radius: 2 },
|
||||
data: qualityVariantChanges,
|
||||
},
|
||||
];
|
||||
@@ -145,19 +145,16 @@ const StreamHealth = () => {
|
||||
{
|
||||
name: 'Median stream latency',
|
||||
color: '#00FFFF',
|
||||
options: { radius: 2 },
|
||||
data: medianLatency,
|
||||
},
|
||||
{
|
||||
name: 'Lowest stream latency',
|
||||
color: '#02FD0D',
|
||||
options: { radius: 2 },
|
||||
data: lowestLatency,
|
||||
},
|
||||
{
|
||||
name: 'Highest stream latency',
|
||||
color: '#B63FFF',
|
||||
options: { radius: 2 },
|
||||
data: highestLatency,
|
||||
},
|
||||
];
|
||||
@@ -188,6 +185,7 @@ const StreamHealth = () => {
|
||||
time: item.time,
|
||||
value: segmentLength,
|
||||
})),
|
||||
pointStyle: 'dash' as const,
|
||||
options: { radius: 0 },
|
||||
},
|
||||
];
|
||||
@@ -358,7 +356,7 @@ const StreamHealth = () => {
|
||||
title="Seconds"
|
||||
dataCollections={segmentDownloadDurationChart}
|
||||
color="#FF7700"
|
||||
unit="s"
|
||||
unit="seconds"
|
||||
yLogarithmic
|
||||
/>
|
||||
</Card>
|
||||
@@ -420,7 +418,7 @@ const StreamHealth = () => {
|
||||
title="Viewer Latency"
|
||||
description="An approximate number of seconds that your viewers are behind your live video. The largest cause of latency spikes is buffering. High latency itself is not a problem, and optimizing for low latency can result in buffering, resulting in even higher latency."
|
||||
/>
|
||||
<Chart title="Seconds" dataCollections={latencyChart} color="#FF7700" unit="s" />
|
||||
<Chart title="Seconds" dataCollections={latencyChart} color="#FF7700" unit="seconds" />
|
||||
</Card>
|
||||
</Space>
|
||||
</>
|
||||
|
||||
@@ -151,7 +151,7 @@ export default function ViewersOverTime() {
|
||||
</button>
|
||||
</Dropdown>
|
||||
{viewerInfo.length > 0 && (
|
||||
<Chart title="Viewers" data={viewerInfo} color="#2087E2" unit="" />
|
||||
<Chart title="Viewers" data={viewerInfo} color="#2087E2" unit="viewers" />
|
||||
)}
|
||||
|
||||
<ViewerTable data={viewerDetails} />
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
import React from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { useRouter } from 'next/router';
|
||||
import { Skeleton } from 'antd';
|
||||
import {
|
||||
clientConfigStateAtom,
|
||||
ClientConfigStore,
|
||||
isOnlineSelector,
|
||||
serverStatusState,
|
||||
appStateAtom,
|
||||
} from '../../../components/stores/ClientConfigStore';
|
||||
import { OfflineBanner } from '../../../components/ui/OfflineBanner/OfflineBanner';
|
||||
import { Statusbar } from '../../../components/ui/Statusbar/Statusbar';
|
||||
import { OwncastPlayer } from '../../../components/video/OwncastPlayer/OwncastPlayer';
|
||||
import { ClientConfig } from '../../../interfaces/client-config.model';
|
||||
import { ServerStatus } from '../../../interfaces/server-status.model';
|
||||
import { AppStateOptions } from '../../../components/stores/application-state';
|
||||
import { Theme } from '../../../components/theme/Theme';
|
||||
|
||||
export default function VideoEmbed() {
|
||||
const status = useRecoilValue<ServerStatus>(serverStatusState);
|
||||
const clientConfig = useRecoilValue<ClientConfig>(clientConfigStateAtom);
|
||||
const appState = useRecoilValue<AppStateOptions>(appStateAtom);
|
||||
|
||||
const { name } = clientConfig;
|
||||
|
||||
@@ -39,34 +44,45 @@ export default function VideoEmbed() {
|
||||
|
||||
const initiallyMuted = query.initiallyMuted === 'true';
|
||||
|
||||
const loadingState = <Skeleton active style={{ padding: '10px' }} paragraph={{ rows: 10 }} />;
|
||||
|
||||
const offlineState = (
|
||||
<OfflineBanner streamName={name} customText={offlineMessage} notificationsEnabled={false} />
|
||||
);
|
||||
|
||||
const onlineState = (
|
||||
<>
|
||||
<OwncastPlayer
|
||||
source="/hls/stream.m3u8"
|
||||
online={online}
|
||||
initiallyMuted={initiallyMuted}
|
||||
title={streamTitle || name}
|
||||
/>
|
||||
<Statusbar
|
||||
online={online}
|
||||
lastConnectTime={lastConnectTime}
|
||||
lastDisconnectTime={lastDisconnectTime}
|
||||
viewerCount={viewerCount}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
const getView = () => {
|
||||
if (appState.appLoading) {
|
||||
return loadingState;
|
||||
}
|
||||
if (online) {
|
||||
return onlineState;
|
||||
}
|
||||
|
||||
return offlineState;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ClientConfigStore />
|
||||
<div className="video-embed">
|
||||
{online && (
|
||||
<OwncastPlayer
|
||||
source="/hls/stream.m3u8"
|
||||
online={online}
|
||||
initiallyMuted={initiallyMuted}
|
||||
title={streamTitle || name}
|
||||
/>
|
||||
)}
|
||||
{!online && (
|
||||
<OfflineBanner
|
||||
streamName={name}
|
||||
customText={offlineMessage}
|
||||
notificationsEnabled={false}
|
||||
/>
|
||||
)}
|
||||
{online && (
|
||||
<Statusbar
|
||||
online={online}
|
||||
lastConnectTime={lastConnectTime}
|
||||
lastDisconnectTime={lastDisconnectTime}
|
||||
viewerCount={viewerCount}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Theme />
|
||||
<div className="video-embed">{getView()}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 16 KiB |
@@ -1,14 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 520 520" style="enable-background:new 0 0 520 520;" xml:space="preserve">
|
||||
<path d="M13.7,11.9v496.2h35.7V520H0V0h49.4v11.9H13.7z"/>
|
||||
<path d="M166.3,169.2v25.1h0.7c6.7-9.6,14.8-17,24.2-22.2c9.4-5.3,20.3-7.9,32.5-7.9c11.7,0,22.4,2.3,32.1,6.8
|
||||
c9.7,4.5,17,12.6,22.1,24c5.5-8.1,13-15.3,22.4-21.5c9.4-6.2,20.6-9.3,33.5-9.3c9.8,0,18.9,1.2,27.3,3.6c8.4,2.4,15.5,6.2,21.5,11.5
|
||||
c6,5.3,10.6,12.1,14,20.6c3.3,8.5,5,18.7,5,30.7v124.1h-50.9V249.6c0-6.2-0.2-12.1-0.7-17.6c-0.5-5.5-1.8-10.3-3.9-14.3
|
||||
c-2.2-4.1-5.3-7.3-9.5-9.7c-4.2-2.4-9.9-3.6-17-3.6c-7.2,0-13,1.4-17.4,4.1c-4.4,2.8-7.9,6.3-10.4,10.8c-2.5,4.4-4.2,9.4-5,15.1
|
||||
c-0.8,5.6-1.3,11.3-1.3,17v103.3h-50.9v-104c0-5.5-0.1-10.9-0.4-16.3c-0.2-5.4-1.3-10.3-3.1-14.9c-1.8-4.5-4.8-8.2-9-10.9
|
||||
c-4.2-2.7-10.3-4.1-18.5-4.1c-2.4,0-5.6,0.5-9.5,1.6c-3.9,1.1-7.8,3.1-11.5,6.1c-3.7,3-6.9,7.3-9.5,12.9c-2.6,5.6-3.9,13-3.9,22.1
|
||||
v107.6h-50.9V169.2H166.3z"/>
|
||||
<path d="M506.3,508.1V11.9h-35.7V0H520v520h-49.4v-11.9H506.3z"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="Layer_1" x="0" y="0" version="1.1" viewBox="0 0 520 520" style="enable-background:new 0 0 520 520" xml:space="preserve"><path d="M13.7,11.9v496.2h35.7V520H0V0h49.4v11.9H13.7z"/><path d="M166.3,169.2v25.1h0.7c6.7-9.6,14.8-17,24.2-22.2c9.4-5.3,20.3-7.9,32.5-7.9c11.7,0,22.4,2.3,32.1,6.8 c9.7,4.5,17,12.6,22.1,24c5.5-8.1,13-15.3,22.4-21.5c9.4-6.2,20.6-9.3,33.5-9.3c9.8,0,18.9,1.2,27.3,3.6c8.4,2.4,15.5,6.2,21.5,11.5 c6,5.3,10.6,12.1,14,20.6c3.3,8.5,5,18.7,5,30.7v124.1h-50.9V249.6c0-6.2-0.2-12.1-0.7-17.6c-0.5-5.5-1.8-10.3-3.9-14.3 c-2.2-4.1-5.3-7.3-9.5-9.7c-4.2-2.4-9.9-3.6-17-3.6c-7.2,0-13,1.4-17.4,4.1c-4.4,2.8-7.9,6.3-10.4,10.8c-2.5,4.4-4.2,9.4-5,15.1 c-0.8,5.6-1.3,11.3-1.3,17v103.3h-50.9v-104c0-5.5-0.1-10.9-0.4-16.3c-0.2-5.4-1.3-10.3-3.1-14.9c-1.8-4.5-4.8-8.2-9-10.9 c-4.2-2.7-10.3-4.1-18.5-4.1c-2.4,0-5.6,0.5-9.5,1.6c-3.9,1.1-7.8,3.1-11.5,6.1c-3.7,3-6.9,7.3-9.5,12.9c-2.6,5.6-3.9,13-3.9,22.1 v107.6h-50.9V169.2H166.3z"/><path d="M506.3,508.1V11.9h-35.7V0H520v520h-49.4v-11.9H506.3z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -1,120 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
|
||||
|
||||
<svg:svg
|
||||
version="1.1"
|
||||
xml:space="preserve"
|
||||
viewBox="0 0 200 200"
|
||||
width="200px"
|
||||
height="200px"
|
||||
x="0px"
|
||||
y="0px"
|
||||
enable-background="new 0 0 200 200"
|
||||
class="TridactylThemeDark"
|
||||
id="svg36"
|
||||
sodipodi:docname="xmpp.svg"
|
||||
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:xhtml="http://www.w3.org/1999/xhtml"><svg:defs
|
||||
id="defs40" /><sodipodi:namedview
|
||||
id="namedview38"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#ffffff"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="1"
|
||||
inkscape:deskcolor="#505050"
|
||||
showgrid="false"
|
||||
inkscape:zoom="3.337544"
|
||||
inkscape:cx="134.53006"
|
||||
inkscape:cy="118.05088"
|
||||
inkscape:window-width="3834"
|
||||
inkscape:window-height="1568"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="26"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg36" />
|
||||
|
||||
<svg:linearGradient
|
||||
id="SVGID_right_"
|
||||
y2="1.279e-13"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x2="-1073.2"
|
||||
gradientTransform="matrix(1.1429608,0,0,1.1429608,1352.3375,27.297)"
|
||||
y1="126.85"
|
||||
x1="-1073.2">
|
||||
<svg:stop
|
||||
stop-color="#1b3967"
|
||||
offset=".011"
|
||||
id="stop2" />
|
||||
<svg:stop
|
||||
stop-color="#13b5ea"
|
||||
offset=".467"
|
||||
id="stop4" />
|
||||
<svg:stop
|
||||
stop-color="#002b5c"
|
||||
offset=".9945"
|
||||
id="stop6" />
|
||||
</svg:linearGradient>
|
||||
|
||||
<svg:linearGradient
|
||||
id="SVGID_left_"
|
||||
y2="1.279e-13"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x2="-1073.2"
|
||||
gradientTransform="matrix(-1.1429608,0,0,1.1429608,-1152.3376,27.295856)"
|
||||
y1="126.85"
|
||||
x1="-1073.2">
|
||||
<svg:stop
|
||||
stop-color="#1b3967"
|
||||
offset=".011"
|
||||
id="stop9" />
|
||||
<svg:stop
|
||||
stop-color="#13b5ea"
|
||||
offset=".467"
|
||||
id="stop11" />
|
||||
<svg:stop
|
||||
stop-color="#002b5c"
|
||||
offset=".9945"
|
||||
id="stop13" />
|
||||
</svg:linearGradient>
|
||||
|
||||
<svg:path
|
||||
d="m 158.17335,43.514472 c 0.088,1.500707 -2.04247,1.106386 -2.04247,2.620809 0,44.062284 -53.21398,111.285539 -104.795782,124.274139 v 1.87332 C 119.85902,165.9736 198.27413,94.789982 200,27.298143 l -41.83123,16.217472 z"
|
||||
style="fill:url(#SVGID_right_);stroke-width:1.14296"
|
||||
id="path16" />
|
||||
<svg:path
|
||||
d="m 137.44918,48.935535 c 0.0868,1.500708 0.13715,3.005988 0.13715,4.522696 0,44.062284 -35.08773,103.434549 -86.667269,116.421999 v 1.87332 C 118.40404,168.56469 171.85574,99.719572 171.85574,46.942212 c 0,-2.714532 -0.1463,-5.405062 -0.42403,-8.064732 l -33.98023,10.05577 z"
|
||||
style="fill:#e96d1f;stroke-width:1.14296"
|
||||
id="path18" />
|
||||
<svg:path
|
||||
d="m 171.75858,38.249995 -8.70592,3.111139 c 0.0469,1.099528 0.0755,2.576234 0.0755,3.686048 0,47.111704 -42.59929,112.243348 -99.748468,122.433978 -3.708919,1.24354 -8.61565,2.37393 -12.494848,3.35002 v 1.87217 C 125.46753,166.34848 177.86772,90.563313 171.7643,38.245422 Z"
|
||||
style="fill:#d9541e;stroke-width:1.14296"
|
||||
id="path20" />
|
||||
|
||||
<svg:path
|
||||
d="m 41.826653,43.513329 c -0.088,1.500708 2.042471,1.106387 2.042471,2.620809 0,44.062284 53.21398,111.285542 104.795786,124.274152 v 1.87331 C 80.140986,165.97245 1.7258709,94.78884 0,27.297 l 41.831225,16.217472 z"
|
||||
style="fill:url(#SVGID_left_);stroke-width:1.14296"
|
||||
id="path22" />
|
||||
<svg:path
|
||||
d="m 62.550819,48.934392 c -0.08687,1.500708 -0.137155,3.005988 -0.137155,4.522697 0,44.062284 35.087749,103.434541 86.667286,116.422011 v 1.87331 C 81.595976,168.56354 28.144269,99.718429 28.144269,46.941069 c 0,-2.714533 0.146298,-5.405062 0.424038,-8.064732 l 33.980226,10.05577 z"
|
||||
style="fill:#a0ce67;stroke-width:1.14296"
|
||||
id="path24" />
|
||||
<svg:path
|
||||
d="m 28.241419,38.248851 8.705933,3.111139 c -0.04686,1.099529 -0.07543,2.576234 -0.07543,3.68605 0,47.111704 42.599295,112.24334 99.748468,122.43398 3.7089,1.24353 8.61563,2.37393 12.49485,3.35002 v 1.87216 C 74.532478,166.34735 22.132294,90.56217 28.235705,38.24428 Z"
|
||||
style="fill:#439639;stroke-width:1.14296"
|
||||
id="path26" />
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<script /><xhtml:iframe
|
||||
class="cleanslate hidden"
|
||||
src="xmpp_files/commandline.html"
|
||||
id="cmdline_iframe"
|
||||
loading="lazy"
|
||||
style="height: 0px !important;" /></svg:svg>
|
||||
<svg:svg xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg" xmlns:xhtml="http://www.w3.org/1999/xhtml" id="svg36" width="200" height="200" x="0" y="0" class="TridactylThemeDark" enable-background="new 0 0 200 200" version="1.1" viewBox="0 0 200 200" inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)" sodipodi:docname="xmpp.svg" xml:space="preserve"><svg:defs id="defs40"/><sodipodi:namedview id="namedview38" bordercolor="#ffffff" borderopacity="1" pagecolor="#505050" showgrid="false" inkscape:current-layer="svg36" inkscape:cx="134.53" inkscape:cy="118.051" inkscape:deskcolor="#505050" inkscape:pagecheckerboard="1" inkscape:pageopacity="0" inkscape:showpageshadow="0" inkscape:window-height="1568" inkscape:window-maximized="1" inkscape:window-width="3834" inkscape:window-x="0" inkscape:window-y="26" inkscape:zoom="3.338"/><svg:linearGradient id="SVGID_right_" x1="-1073.2" x2="-1073.2" y1="126.85" y2="0" gradientTransform="matrix(1.1429608,0,0,1.1429608,1352.3375,27.297)" gradientUnits="userSpaceOnUse"><svg:stop id="stop2" offset=".011" stop-color="#1b3967"/><svg:stop id="stop4" offset=".467" stop-color="#13b5ea"/><svg:stop id="stop6" offset=".995" stop-color="#002b5c"/></svg:linearGradient><svg:linearGradient id="SVGID_left_" x1="-1073.2" x2="-1073.2" y1="126.85" y2="0" gradientTransform="matrix(-1.1429608,0,0,1.1429608,-1152.3376,27.295856)" gradientUnits="userSpaceOnUse"><svg:stop id="stop9" offset=".011" stop-color="#1b3967"/><svg:stop id="stop11" offset=".467" stop-color="#13b5ea"/><svg:stop id="stop13" offset=".995" stop-color="#002b5c"/></svg:linearGradient><svg:path d="m 158.17335,43.514472 c 0.088,1.500707 -2.04247,1.106386 -2.04247,2.620809 0,44.062284 -53.21398,111.285539 -104.795782,124.274139 v 1.87332 C 119.85902,165.9736 198.27413,94.789982 200,27.298143 l -41.83123,16.217472 z" style="fill:url(#SVGID_right_);stroke-width:1.14296" id="path16"/><svg:path d="m 137.44918,48.935535 c 0.0868,1.500708 0.13715,3.005988 0.13715,4.522696 0,44.062284 -35.08773,103.434549 -86.667269,116.421999 v 1.87332 C 118.40404,168.56469 171.85574,99.719572 171.85574,46.942212 c 0,-2.714532 -0.1463,-5.405062 -0.42403,-8.064732 l -33.98023,10.05577 z" style="fill:#e96d1f;stroke-width:1.14296" id="path18"/><svg:path d="m 171.75858,38.249995 -8.70592,3.111139 c 0.0469,1.099528 0.0755,2.576234 0.0755,3.686048 0,47.111704 -42.59929,112.243348 -99.748468,122.433978 -3.708919,1.24354 -8.61565,2.37393 -12.494848,3.35002 v 1.87217 C 125.46753,166.34848 177.86772,90.563313 171.7643,38.245422 Z" style="fill:#d9541e;stroke-width:1.14296" id="path20"/><svg:path d="m 41.826653,43.513329 c -0.088,1.500708 2.042471,1.106387 2.042471,2.620809 0,44.062284 53.21398,111.285542 104.795786,124.274152 v 1.87331 C 80.140986,165.97245 1.7258709,94.78884 0,27.297 l 41.831225,16.217472 z" style="fill:url(#SVGID_left_);stroke-width:1.14296" id="path22"/><svg:path d="m 62.550819,48.934392 c -0.08687,1.500708 -0.137155,3.005988 -0.137155,4.522697 0,44.062284 35.087749,103.434541 86.667286,116.422011 v 1.87331 C 81.595976,168.56354 28.144269,99.718429 28.144269,46.941069 c 0,-2.714533 0.146298,-5.405062 0.424038,-8.064732 l 33.980226,10.05577 z" style="fill:#a0ce67;stroke-width:1.14296" id="path24"/><svg:path d="m 28.241419,38.248851 8.705933,3.111139 c -0.04686,1.099529 -0.07543,2.576234 -0.07543,3.68605 0,47.111704 42.599295,112.24334 99.748468,122.43398 3.7089,1.24353 8.61563,2.37393 12.49485,3.35002 v 1.87216 C 74.532478,166.34735 22.132294,90.56217 28.235705,38.24428 Z" style="fill:#439639;stroke-width:1.14296" id="path26"/><script/><xhtml:iframe id="cmdline_iframe" class="cleanslate hidden" loading="lazy" src="xmpp_files/commandline.html" style="height:0!important"/></svg:svg>
|
||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 3.7 KiB |
@@ -21,19 +21,17 @@
|
||||
.chat-messages .bulk-editor {
|
||||
margin: 0.5rem 0;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--textfield-border);
|
||||
border: 1px solid var(--theme-color-palette-8);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.chat-messages .bulk-editor.active .label {
|
||||
color: var(--black-3);
|
||||
}
|
||||
|
||||
.chat-messages .bulk-editor .label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--white-50);
|
||||
color: var(--theme-color-palette-10);
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
.chat-messages .bulk-editor button {
|
||||
@@ -67,7 +65,7 @@
|
||||
opacity: 1;
|
||||
}
|
||||
.toggle-switch .ant-btn-text:hover {
|
||||
background-color: var(--black-35);
|
||||
background-color: var(--theme-color-palette-10);
|
||||
}
|
||||
.blockuser-popover {
|
||||
max-width: 400px;
|
||||
@@ -82,14 +80,14 @@
|
||||
outline: none;
|
||||
}
|
||||
.user-item-container .display-name {
|
||||
color: var(--white);
|
||||
border-bottom: 1px dotted var(--white-50);
|
||||
color: var(--theme-color-palette-4);
|
||||
border-bottom: 1px dotted var(--theme-color-palette-10);
|
||||
}
|
||||
.user-item-container:hover .display-name {
|
||||
border-color: var(--white);
|
||||
border-color: var(--theme-color-palette-4);
|
||||
}
|
||||
.user-details h5 {
|
||||
color: var(--white);
|
||||
color: var(--theme-color-palette-4);
|
||||
}
|
||||
.user-details .created-at {
|
||||
font-size: 0.75em;
|
||||
@@ -110,11 +108,9 @@
|
||||
.user-details .previous-names-list .latest .user-name-item {
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
color: var(--pink);
|
||||
}
|
||||
.user-details .ant-divider {
|
||||
border-color: var(--white-25);
|
||||
color: var(--theme-color-palette-12);
|
||||
}
|
||||
|
||||
.block-user-button {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
@@ -12,13 +12,12 @@
|
||||
margin-left: 0.3rem;
|
||||
padding: 2px;
|
||||
border-radius: 5rem;
|
||||
color: var(--black);
|
||||
border: 1px solid var(--black);
|
||||
border: 1px solid black;
|
||||
transition-duration: var(--ant-transition-duration);
|
||||
}
|
||||
.edit-current-strings .ant-tag .ant-tag-close-icon:hover {
|
||||
border-color: var(--owncast-purple);
|
||||
background-color: var(--white);
|
||||
border-color: var(--theme-color-palette-6);
|
||||
background-color: var(--theme-color-palette-4);
|
||||
}
|
||||
.edit-current-strings .ant-tag .ant-tag-close-icon:hover svg {
|
||||
fill: black;
|
||||
|
||||
@@ -41,13 +41,13 @@
|
||||
width: 100%;
|
||||
}
|
||||
.config-public-details-page .social-items-container {
|
||||
background-color: var(--container-bg-color-alt);
|
||||
background-color: var(--theme-color-palette-5);
|
||||
padding: 0 0.75em;
|
||||
margin-left: 1em;
|
||||
max-width: 450px;
|
||||
}
|
||||
.config-public-details-page .social-items-container .form-module {
|
||||
background-color: var(--black);
|
||||
background-color: var(--theme-color-palette-10);
|
||||
}
|
||||
.config-public-details-page .social-items-container .social-handles-container {
|
||||
min-width: 350px;
|
||||
@@ -78,5 +78,5 @@
|
||||
max-width: 256px;
|
||||
margin-right: 1em;
|
||||
display: inline-block;
|
||||
border: 1px solid var(--white-25);
|
||||
border: 1px solid var(--theme-color-palette-5);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
color: var(--white-75);
|
||||
}
|
||||
.social-links-edit-container .social-handles-table .social-handle-cell .option-icon {
|
||||
height: 2em;
|
||||
|
||||
@@ -10,11 +10,11 @@
|
||||
.config-variant-form .passthrough-warning {
|
||||
text-align: center;
|
||||
padding: 1em;
|
||||
color: var(--ant-warning);
|
||||
color: var(--theme-color-palette-error);
|
||||
font-size: 0.88em;
|
||||
font-weight: 500;
|
||||
background-color: var(--black-50);
|
||||
border-radius: var(--container-border-radius);
|
||||
background-color: var(--theme-color-palette-10);
|
||||
border-radius: var(--theme-rounded-corners);
|
||||
}
|
||||
.config-variant-form .cpu-usage-container,
|
||||
.config-variant-form .bitrate-container {
|
||||
@@ -43,9 +43,7 @@
|
||||
.read-more-subtext {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.codec-module .ant-collapse-content-active {
|
||||
background-color: var(--white-15);
|
||||
}
|
||||
|
||||
.video-text-field-container {
|
||||
margin-left: 12px;
|
||||
margin-bottom: 30px;
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
}
|
||||
.field-tip {
|
||||
font-size: 0.8em;
|
||||
color: var(--white-50);
|
||||
color: var(--theme-color-palette-10);
|
||||
}
|
||||
.field-container {
|
||||
padding: 0.85em 0 0.5em;
|
||||
@@ -32,8 +32,8 @@
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
padding: 1em 2em 0.75em;
|
||||
background-color: var(--owncast-purple-25);
|
||||
border-radius: var(--container-border-radius);
|
||||
background-color: var(--color-owncast-palette-7);
|
||||
border-radius: var(--theme-rounded-corners);
|
||||
}
|
||||
.segment-slider-container .status-container {
|
||||
width: 100%;
|
||||
@@ -46,10 +46,10 @@
|
||||
text-align: center;
|
||||
font-size: 0.75em;
|
||||
line-height: normal;
|
||||
color: var(--white);
|
||||
color: var(--theme-color-palette-4);
|
||||
padding: 1em;
|
||||
border-radius: var(--container-border-radius);
|
||||
background-color: var(--black-35);
|
||||
border-radius: var(--theme-rounded-corners);
|
||||
background-color: var(--theme-color-palette-error);
|
||||
}
|
||||
.segment-tip {
|
||||
width: 10em;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.home-container .online-status-section .online-details-card {
|
||||
border-color: var(--online-color);
|
||||
border-color: var(--theme-color-palette-6);
|
||||
}
|
||||
.home-container .stream-details-item-container {
|
||||
margin: 1em 0;
|
||||
@@ -17,8 +17,8 @@
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.offline-content .list-section {
|
||||
background-color: var(--container-bg-color-alt);
|
||||
border-radius: var(--container-border-radius);
|
||||
/* background-color: var(--theme-color-palette-3); */
|
||||
border-radius: var(--theme-rounded-corners);
|
||||
padding: 1em;
|
||||
}
|
||||
.offline-content .list-section > .ant-card {
|
||||
@@ -38,17 +38,18 @@
|
||||
.news-feed {
|
||||
margin-top: 0;
|
||||
padding: 1.5em;
|
||||
border: 1px solid var(--theme-color-palette-15);
|
||||
}
|
||||
.news-feed h2 {
|
||||
font-size: 1.2em;
|
||||
margin-top: 0;
|
||||
color: var(--pink);
|
||||
color: var(--theme-color-palette-11);
|
||||
}
|
||||
.news-feed article {
|
||||
padding: 1em 0.25em;
|
||||
font-size: 0.9rem;
|
||||
color: var(--white-75);
|
||||
border-bottom: 1px solid var(--gray);
|
||||
border-bottom: 1px solid var(--theme-color-palette-10);
|
||||
}
|
||||
.news-feed article h3 {
|
||||
font-size: 1.2em;
|
||||
@@ -60,5 +61,14 @@
|
||||
.news-feed article .timestamp {
|
||||
margin-top: 0;
|
||||
font-size: 0.75em;
|
||||
color: var(--white-50);
|
||||
}
|
||||
|
||||
.ant-collapse > .ant-collapse-item:last-child,
|
||||
.ant-collapse > .ant-collapse-item:last-child > .ant-collapse-header {
|
||||
background-color: var(--theme-color-palette-5);
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
/* background-color: var(--theme-color-palette-5); */
|
||||
border: 1px solid var(--theme-color-palette-15);
|
||||
}
|
||||
|
||||
@@ -5,11 +5,12 @@
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
z-index: 10;
|
||||
background-color: var(--theme-color-background-main);
|
||||
background-color: var(--theme-color-palette-1);
|
||||
}
|
||||
.app-container .menu-container {
|
||||
border-color: transparent;
|
||||
background-color: unset;
|
||||
background-color: unset;
|
||||
color: var(--theme-color-components-text-on-dark);
|
||||
}
|
||||
.app-container h1.owncast-title {
|
||||
padding: 1rem;
|
||||
@@ -20,7 +21,7 @@
|
||||
align-items: center;
|
||||
}
|
||||
.app-container h1.owncast-title .logo-container {
|
||||
background-color: var(--white);
|
||||
background-color: var(--theme-color-palette-4);
|
||||
padding-top: 4px;
|
||||
padding-right: 6px;
|
||||
padding-left: 6px;
|
||||
@@ -29,7 +30,7 @@
|
||||
.app-container h1.owncast-title .title-label {
|
||||
display: inline-block;
|
||||
margin-left: 1rem;
|
||||
color: var(--white);
|
||||
color: var(--theme-color-palette-4);
|
||||
font-size: 1.15rem;
|
||||
font-weight: 200;
|
||||
text-transform: uppercase;
|
||||
@@ -45,9 +46,10 @@
|
||||
justify-content: flex-end;
|
||||
padding-right: 1rem;
|
||||
padding-left: 1rem;
|
||||
background-color: var(--nav-bg-color);
|
||||
background-color: var(--theme-color-background-main);
|
||||
}
|
||||
.app-container .main-content-container {
|
||||
background-color: var(--theme-color-palette-3);
|
||||
padding: 2em 3em 3em;
|
||||
min-width: 50vw;
|
||||
}
|
||||
@@ -64,7 +66,7 @@
|
||||
width: 12.5rem;
|
||||
}
|
||||
.app-container .online-status-indicator .status-label {
|
||||
color: var(--white);
|
||||
color: var(--theme-color-palette-4);
|
||||
text-transform: uppercase;
|
||||
font-size: 0.75rem;
|
||||
display: inline-block;
|
||||
@@ -135,3 +137,9 @@
|
||||
.ant-select:not(.ant-select-customize-input) .ant-select-selector {
|
||||
background-color: var(--theme-color-components-form-field-background);
|
||||
}
|
||||
|
||||
.ant-menu-item a,
|
||||
.ant-menu-submenu-expand-icon,
|
||||
.ant-menu-submenu-arrow {
|
||||
color: unset;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export default {
|
||||
title: 'owncast/Chat/Embeds/Read-write chat',
|
||||
component: ReadWritePage,
|
||||
parameters: {
|
||||
chromatic: { diffThreshold: 0.2 },
|
||||
chromatic: { diffThreshold: 0.8 },
|
||||
},
|
||||
} as ComponentMeta<typeof ReadWritePage>;
|
||||
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
|
||||
const Template = ({
|
||||
origin,
|
||||
query,
|
||||
title,
|
||||
width,
|
||||
height,
|
||||
}: {
|
||||
origin: string;
|
||||
query: string;
|
||||
title: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}) => (
|
||||
<iframe
|
||||
src={`${origin}/embed/video?${query}`}
|
||||
title={title}
|
||||
height={`${height}px`}
|
||||
width={`${width}px`}
|
||||
referrerPolicy="origin"
|
||||
scrolling="no"
|
||||
allowFullScreen
|
||||
/>
|
||||
);
|
||||
|
||||
const origins = {
|
||||
DemoServer: `https://watch.owncast.online`,
|
||||
RetroStrangeTV: `https://live.retrostrange.com`,
|
||||
localhost: `http://localhost:3000`,
|
||||
};
|
||||
|
||||
export default {
|
||||
title: 'owncast/Player/Embeds',
|
||||
component: Template,
|
||||
argTypes: {
|
||||
origin: {
|
||||
options: Object.keys(origins),
|
||||
mapping: origins,
|
||||
control: {
|
||||
type: 'select',
|
||||
},
|
||||
defaultValue: origins.DemoServer,
|
||||
},
|
||||
query: {
|
||||
type: 'string',
|
||||
},
|
||||
title: {
|
||||
defaultValue: 'My Title',
|
||||
type: 'string',
|
||||
},
|
||||
height: {
|
||||
defaultValue: 350,
|
||||
type: 'number',
|
||||
},
|
||||
width: {
|
||||
defaultValue: 550,
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
} satisfies ComponentMeta<typeof Template>;
|
||||
|
||||
export const Default: ComponentStory<typeof Template> = Template.bind({});
|
||||
Default.args = {};
|
||||
|
||||
export const InitiallyMuted: ComponentStory<typeof Template> = Template.bind({});
|
||||
InitiallyMuted.args = {
|
||||
query: 'initiallyMuted=true',
|
||||
};
|
||||
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 46 KiB |