Merge branch 'webv2' into fix/ImplementPasswordRules
@@ -18,7 +18,6 @@ module.exports = {
|
||||
'@storybook/addon-postcss',
|
||||
'@storybook/addon-a11y',
|
||||
'storybook-addon-designs',
|
||||
'storybook-dark-mode',
|
||||
'storybook-addon-fetch-mock',
|
||||
],
|
||||
webpackFinal: async (config, { configType }) => {
|
||||
@@ -45,7 +44,9 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return config;
|
||||
},
|
||||
framework: '@storybook/react',
|
||||
staticDirs: ['../public', '../../static', './story-assets'],
|
||||
};
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
import { ColorRow } from './Color';
|
||||
|
||||
<Meta title="owncast/Style Guide/Default Theme" />
|
||||
<Meta title="owncast/Styles/Colors" />
|
||||
|
||||
# Default theme colors
|
||||
|
||||
These colors are assigned in our [color token](https://github.com/owncast/owncast/tree/webv2/web/style-definitions/tokens/color) files
|
||||
and get reflected here as they change. run `npm run build-styles` to regenerate.
|
||||
|
||||
<Story
|
||||
name="Default Theme"
|
||||
>
|
||||
## Default Theme
|
||||
|
||||
These color names are assigned to specific component variables. They can be overwritten via CSS.
|
||||
@@ -39,7 +42,11 @@ These color names are assigned to specific component variables. They can be over
|
||||
'theme-color-action-disabled',
|
||||
]}
|
||||
/>
|
||||
</Story>
|
||||
|
||||
<Story
|
||||
name="Frontend Components"
|
||||
>
|
||||
## Component Colors
|
||||
|
||||
<ColorRow
|
||||
@@ -72,9 +79,15 @@ These color names are assigned to specific component variables. They can be over
|
||||
'theme-color-components-form-field-placeholder',
|
||||
'theme-color-components-form-field-text',
|
||||
'theme-color-components-form-field-border',
|
||||
'theme-color-components-video-status-bar-background',
|
||||
'theme-color-components-video-status-bar-foreground',
|
||||
]}
|
||||
/>
|
||||
</Story>
|
||||
|
||||
<Story
|
||||
name="Owncast Color Palette"
|
||||
>
|
||||
## Default Palette
|
||||
|
||||
These are the core colors for the default, out of the box, Owncast web application theme.
|
||||
@@ -98,7 +111,11 @@ They should not be overwritten, instead the theme variables should be overwritte
|
||||
'color-owncast-palette-15',
|
||||
]}
|
||||
/>
|
||||
</Story>
|
||||
|
||||
<Story
|
||||
name="User Chat Colors"
|
||||
>
|
||||
## User Colors
|
||||
|
||||
<ColorRow
|
||||
@@ -113,3 +130,4 @@ They should not be overwritten, instead the theme variables should be overwritte
|
||||
'theme-color-users-7',
|
||||
]}
|
||||
/>
|
||||
</Story>
|
||||
|
||||
64
web/.storybook/stories-category-doc-pages/Design.stories.mdx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
|
||||
<Meta title="owncast/Documentation/Design" />
|
||||
|
||||
# Owncast Design Guidelines & Resources
|
||||
|
||||
A collection of design contribution guidelines and resources for the Owncast interface.
|
||||
|
||||
> **All participating designers are highly encouraged to shape and evolve these guidelines!**
|
||||
> It is a work in progress and as we have design contributors we can work to solidify the process, tools and resources.
|
||||
|
||||
## 👋 Welcome
|
||||
|
||||
Owncast is a is a live streaming and chat server targeted to anybody who has live streaming needs. This means anybody from corporate events, government meetings, game streamers, musicians, churches, TV stations, and more.
|
||||
|
||||
Read the detailed [product definition](https://github.com/owncast/owncast/blob/develop/docs/product-definition.md) to learn more.
|
||||
|
||||
## 🚢 How to contribute to product design
|
||||
|
||||
1. Check out open [issues](https://github.com/owncast/owncast/issues) here on GitHub (we label them with `needs design`)
|
||||
2. Feel free to open an issue on your own if you find something you would like to contribute to the project.
|
||||
3. Add your contributions to an issue and we promise we will review your contribution carefully and foster discussions
|
||||
|
||||
**We encourage you to:**
|
||||
|
||||
- Get in touch with the team by joining our [Community Chat](https://owncast.rocket.chat).
|
||||
- Check out our [Contributor Guide](https://owncast.online/help) and
|
||||
[Code of Conduct](https://github.com/owncast/owncast/blob/develop/CODE_OF_CONDUCT.md)
|
||||
|
||||
## 🎭 Target audience
|
||||
|
||||
Owncast is a is a live streaming and chat server targeted to anybody who has live streaming needs. This means anything from corporate events, government meetings, game streams, concerts, TV stations, and more.
|
||||
|
||||
## 💅 Design relevant materials
|
||||
|
||||
Here is a list of design relevant information and materials:
|
||||
|
||||
### Fonts
|
||||
|
||||
http://owncast.online/components/?path=/story/owncast-style-guide-typography--page
|
||||
|
||||
Body text: OpenSans
|
||||
|
||||
Display/Header text: Poppins
|
||||
|
||||
### Colors
|
||||
|
||||
http://owncast.online/components/?path=/story/owncast-style-guide-default-theme--page
|
||||
|
||||
### Design Files, Screenshots, etc
|
||||
|
||||
We do not currently have any design files that fully represent the state of
|
||||
the Owncast interface. However going forward it would be nice to resolve this
|
||||
and collaborate on designs.
|
||||
|
||||
We do have a [PenPot organization](https://design.penpot.app/#/dashboard/team/8373f780-f255-11ec-b774-f940e3befd53/projects). Please ask for access.
|
||||
|
||||
## 🎓 License
|
||||
|
||||
All design work is licensed under the
|
||||
[MIT](https://mit-license.org/)
|
||||
|
||||
[(Back to top)](#-table-of-contents)
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
|
||||
<Meta title="owncast/Documentation/Get Started with Owncast Development" />
|
||||
|
||||
---
|
||||
title: "How to work on Owncast"
|
||||
description: The technical details for those wishing to take part in Owncast development.
|
||||
tags:
|
||||
[
|
||||
development,
|
||||
contribute,
|
||||
open-source,
|
||||
github,
|
||||
git,
|
||||
go,
|
||||
react,
|
||||
typescript,
|
||||
contributing,
|
||||
]
|
||||
aliases: [/docs/building]
|
||||
type: toc
|
||||
toc: true
|
||||
---
|
||||
|
||||
Owncast is a straightforward web application and compared to many projects is very easy to get running locally and contributing to.
|
||||
|
||||
- The backend is written in [Go](https://go.dev/).
|
||||
- The frontend is written in [React](https://reactjs.org/).
|
||||
|
||||
If you're interested in contributing to Owncast, here's how you can get started.
|
||||
|
||||
## How to start with Frontend development
|
||||
|
||||
The web frontend of Owncast is written in React with TypeScript built using [Next.js](https://nextjs.org/).
|
||||
|
||||
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`.
|
||||
|
||||
### Run the web project
|
||||
|
||||
1. Change to the `web` directory and install dependencies with `npm install`.
|
||||
1. Start the development server with `npm run dev`.
|
||||
1. Open `http://localhost:3000` in your browser.
|
||||
|
||||
You must have an instance of Owncast running locally to connect to. You can run one with `go run main.go` from the root of the repository. Read more details about running development Owncast under the backend section.
|
||||
|
||||
### 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.
|
||||
|
||||
### Use Storybook to update and create components
|
||||
|
||||
Storybook is a tool that allows you to create and test components in isolation. It's a great way to develop new components and test them out without running a copy of the Owncast server.
|
||||
|
||||
1. Run `npm run storybook` to start the Storybook server.
|
||||
1. Open `http://localhost:6006` in your browser.
|
||||
1. Navigate the Storybook interface to browse and test components.
|
||||
|
||||
## How to start with Backend development
|
||||
|
||||
The backend of Owncast is written in Go. It operates as a web and API server, inbound RTMP ingestion server, outbound HLS distribution server, and chat server.
|
||||
|
||||
1. Ensure you have the [Go programming language](https://go.dev/dl/) tools installed for your system.
|
||||
1. Clone the Owncast repository with `git clone https://github.com/owncast/owncast`.
|
||||
1. A c compiler and tooling must be available on your system. Generally this means installing `gcc` and its development libraries.
|
||||
1. Run `go run main.go` from the root of the repository.
|
||||
|
||||
### Go Linting
|
||||
|
||||
We use golangci-lint to lint our Go code. While optional, it is a useful tool to assist you in writing better Go code. You can install it from the [golangci-lint](https://golangci-lint.run/usage/install/#local-installation) website.
|
||||
|
||||
## Run a development stream
|
||||
|
||||
Many features are only enabled when a stream is live. You can run a local stream using any video file you have around by running:
|
||||
|
||||
```bash
|
||||
./test/ocTestStream.sh somevideofile.mp4
|
||||
```
|
||||
|
||||
## If you haven't yet, find an issue to work on
|
||||
|
||||
Visit our [Good First Issues](https://github.com/owncast/owncast/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) list to find something that might be a good fit for you to start on. Otherwise, feel free to drop into our [community chat](https://owncast.rocket.chat) and say hi and we can get to know you and see where you'd like to take part.
|
||||
|
||||
148
web/.storybook/stories-category-doc-pages/Emoji.stories.mdx
Normal file
@@ -0,0 +1,148 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
import { Image, ImageRow } from './ImageAsset';
|
||||
|
||||
<Meta title="owncast/Frontend Assets/Emoji" />
|
||||
|
||||
# Built-in Custom Emoji
|
||||
|
||||
|
||||
## Blob
|
||||
|
||||
<Story
|
||||
name="Blob"
|
||||
>
|
||||
<a href="img/emoji/blob/LICENSE.md" target="_blank">
|
||||
LICENSE
|
||||
</a>
|
||||
<ImageRow images={[
|
||||
{src: "img/emoji/blob/ablobattention.gif", name: "ablobattention.gif"},
|
||||
{src: "img/emoji/blob/ablobaww.gif", name: "ablobaww.gif"},
|
||||
{src: "img/emoji/blob/ablobblewobble.gif", name: "ablobblewobble.gif"},
|
||||
{src: "img/emoji/blob/ablobcheer.gif", name: "ablobcheer.gif"},
|
||||
{src: "img/emoji/blob/ablobcry.gif", name: "ablobcry.gif"},
|
||||
{src: "img/emoji/blob/ablobdancer.gif", name: "ablobdancer.gif"},
|
||||
{src: "img/emoji/blob/ablobgift.gif", name: "ablobgift.gif"},
|
||||
{src: "img/emoji/blob/ablobgiggle.gif", name: "ablobgiggle.gif"},
|
||||
{src: "img/emoji/blob/ablobparty.gif", name: "ablobparty.gif"},
|
||||
{src: "img/emoji/blob/ablobsleep.gif", name: "ablobsleep.gif"},
|
||||
{src: "img/emoji/blob/ablobthinking.gif", name: "ablobthinking.gif"},
|
||||
{src: "img/emoji/blob/ablobwave.gif", name: "ablobwave.gif"},
|
||||
{src: "img/emoji/blob/blobangry.png", name: "blobangry.png"},
|
||||
{src: "img/emoji/blob/blobaww.png", name: "blobaww.png"},
|
||||
{src: "img/emoji/blob/blobdancer.png", name: "blobdancer.png"},
|
||||
{src: "img/emoji/blob/blobjam.png", name: "blobjam.png"},
|
||||
{src: "img/emoji/blob/blobscream.png", name: "blobscream.png"},
|
||||
{src: "img/emoji/blob/blobthanks.png", name: "blobthanks.png"},
|
||||
{src: "img/emoji/blob/blobthinking.png", name: "blobthinking.png"},
|
||||
{src: "img/emoji/blob/blobwave.png", name: "blobwave.png"},
|
||||
{src: "img/emoji/blob/blobyes.png", name: "blobyes.png"},
|
||||
{src: "img/emoji/blob/blobyum.png", name: "blobyum.png"},
|
||||
]}/>
|
||||
</Story>
|
||||
|
||||
|
||||
## Conigliolo96
|
||||
|
||||
<Story
|
||||
name="Conigliolo96"
|
||||
>
|
||||
<a href="img/emoji/conigliolo96/LICENSE.md" target="_blank">
|
||||
LICENSE
|
||||
</a>
|
||||
<ImageRow images={[
|
||||
{src: "img/emoji/conigliolo96/conigliolo1.gif", name: "conigliolo1.gif"},
|
||||
{src: "img/emoji/conigliolo96/conigliolo15.gif", name: "conigliolo15.gif"},
|
||||
{src: "img/emoji/conigliolo96/conigliolo17.gif", name: "conigliolo17.gif"},
|
||||
{src: "img/emoji/conigliolo96/conigliolo21.gif", name: "conigliolo21.gif"},
|
||||
{src: "img/emoji/conigliolo96/conigliolo25.gif", name: "conigliolo25.gif"},
|
||||
{src: "img/emoji/conigliolo96/conigliolo28.gif", name: "conigliolo28.gif"},
|
||||
]}/>
|
||||
</Story>
|
||||
|
||||
|
||||
## Dog
|
||||
|
||||
<Story
|
||||
name="Dog"
|
||||
>
|
||||
<a href="img/emoji/dog/LICENSE.md" target="_blank">
|
||||
LICENSE
|
||||
</a>
|
||||
<ImageRow images={[
|
||||
{src: "img/emoji/dog/img001.svg", name: "img001.svg"},
|
||||
{src: "img/emoji/dog/img091.svg", name: "img091.svg"},
|
||||
{src: "img/emoji/dog/img093.svg", name: "img093.svg"},
|
||||
{src: "img/emoji/dog/img203.svg", name: "img203.svg"},
|
||||
{src: "img/emoji/dog/img288.svg", name: "img288.svg"},
|
||||
{src: "img/emoji/dog/img327.svg", name: "img327.svg"},
|
||||
{src: "img/emoji/dog/img346.svg", name: "img346.svg"},
|
||||
{src: "img/emoji/dog/img347.svg", name: "img347.svg"},
|
||||
{src: "img/emoji/dog/img352.svg", name: "img352.svg"},
|
||||
]}/>
|
||||
</Story>
|
||||
|
||||
|
||||
## Mutant
|
||||
|
||||
<Story
|
||||
name="Mutant"
|
||||
>
|
||||
<a href="img/emoji/mutant/LICENSE.md" target="_blank">
|
||||
LICENSE
|
||||
</a>
|
||||
<ImageRow images={[
|
||||
{src: "img/emoji/mutant/8_ball.svg", name: "8_ball.svg"},
|
||||
{src: "img/emoji/mutant/alien.svg", name: "alien.svg"},
|
||||
{src: "img/emoji/mutant/american_football.svg", name: "american_football.svg"},
|
||||
{src: "img/emoji/mutant/arms_in_the_air.svg", name: "arms_in_the_air.svg"},
|
||||
{src: "img/emoji/mutant/artist.svg", name: "artist.svg"},
|
||||
{src: "img/emoji/mutant/astronaut.svg", name: "astronaut.svg"},
|
||||
{src: "img/emoji/mutant/back_of_hand_clw.svg", name: "back_of_hand_clw.svg"},
|
||||
{src: "img/emoji/mutant/back_of_hand_hoof.svg", name: "back_of_hand_hoof.svg"},
|
||||
{src: "img/emoji/mutant/back_of_hand_paw.svg", name: "back_of_hand_paw.svg"},
|
||||
{src: "img/emoji/mutant/baseball.svg", name: "baseball.svg"},
|
||||
{src: "img/emoji/mutant/basketball.svg", name: "basketball.svg"},
|
||||
{src: "img/emoji/mutant/blep.svg", name: "blep.svg"},
|
||||
{src: "img/emoji/mutant/bow_b3.svg", name: "bow_b3.svg"},
|
||||
{src: "img/emoji/mutant/cat_crying.svg", name: "cat_crying.svg"},
|
||||
{src: "img/emoji/mutant/cat_devious.svg", name: "cat_devious.svg"},
|
||||
{src: "img/emoji/mutant/cat_grin.svg", name: "cat_grin.svg"},
|
||||
{src: "img/emoji/mutant/cat_heart_eyes.svg", name: "cat_heart_eyes.svg"},
|
||||
{src: "img/emoji/mutant/cat_joy.svg", name: "cat_joy.svg"},
|
||||
{src: "img/emoji/mutant/cat_kiss.svg", name: "cat_kiss.svg"},
|
||||
{src: "img/emoji/mutant/cat_pouting.svg", name: "cat_pouting.svg"},
|
||||
{src: "img/emoji/mutant/cat_scream.svg", name: "cat_scream.svg"},
|
||||
{src: "img/emoji/mutant/cat_smile.svg", name: "cat_smile.svg"},
|
||||
{src: "img/emoji/mutant/chef.svg", name: "chef.svg"},
|
||||
{src: "img/emoji/mutant/detective.svg", name: "detective.svg"},
|
||||
{src: "img/emoji/mutant/ear.svg", name: "ear.svg"},
|
||||
{src: "img/emoji/mutant/eye.svg", name: "eye.svg"},
|
||||
{src: "img/emoji/mutant/eyes.svg", name: "eyes.svg"},
|
||||
{src: "img/emoji/mutant/facepalm.svg", name: "facepalm.svg"},
|
||||
{src: "img/emoji/mutant/football.svg", name: "football.svg"},
|
||||
{src: "img/emoji/mutant/ghost.svg", name: "ghost.svg"},
|
||||
{src: "img/emoji/mutant/grumpy_block.svg", name: "grumpy_block.svg"},
|
||||
{src: "img/emoji/mutant/hot_shit.svg", name: "hot_shit.svg"},
|
||||
{src: "img/emoji/mutant/jack_o_lantern.svg", name: "jack_o_lantern.svg"},
|
||||
{src: "img/emoji/mutant/long_pointed_ear.svg", name: "long_pointed_ear.svg"},
|
||||
{src: "img/emoji/mutant/mechanical_arm.svg", name: "mechanical_arm.svg"},
|
||||
{src: "img/emoji/mutant/no_good.svg", name: "no_good.svg"},
|
||||
{src: "img/emoji/mutant/office_worker.svg", name: "office_worker.svg"},
|
||||
{src: "img/emoji/mutant/ok_gesture.svg", name: "ok_gesture.svg"},
|
||||
{src: "img/emoji/mutant/person_frowning.svg", name: "person_frowning.svg"},
|
||||
{src: "img/emoji/mutant/raising_hand.svg", name: "raising_hand.svg"},
|
||||
{src: "img/emoji/mutant/robot.svg", name: "robot.svg"},
|
||||
{src: "img/emoji/mutant/shrug.svg", name: "shrug.svg"},
|
||||
{src: "img/emoji/mutant/singer.svg", name: "singer.svg"},
|
||||
{src: "img/emoji/mutant/skull.svg", name: "skull.svg"},
|
||||
{src: "img/emoji/mutant/skull_and_crossbones.svg", name: "skull_and_crossbones.svg"},
|
||||
{src: "img/emoji/mutant/softball.svg", name: "softball.svg"},
|
||||
{src: "img/emoji/mutant/student.svg", name: "student.svg"},
|
||||
{src: "img/emoji/mutant/studio_microphone.svg", name: "studio_microphone.svg"},
|
||||
{src: "img/emoji/mutant/technologist.svg", name: "technologist.svg"},
|
||||
{src: "img/emoji/mutant/tennis.svg", name: "tennis.svg"},
|
||||
{src: "img/emoji/mutant/volleyball.svg", name: "volleyball.svg"},
|
||||
]}/>
|
||||
</Story>
|
||||
|
||||
|
||||
@@ -1,51 +1,16 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
import { Image, ImageRow } from './ImageAsset';
|
||||
|
||||
import Logo from '../../assets/images/logo.svg';
|
||||
import FediverseColor from '../../public/img/fediverse-color.png';
|
||||
import FediverseBlack from '../../public/img/fediverse-black.png';
|
||||
import Moderator from '../../assets/images/moderator.svg';
|
||||
import IndieAuth from '../../public/img/indieauth.png';
|
||||
import IsBot from '../../assets/images/bot.svg';
|
||||
|
||||
<Meta title="owncast/Style Guide/Images+Icons" />
|
||||
|
||||
export const images = [
|
||||
{
|
||||
src: Logo,
|
||||
name: 'Logo',
|
||||
},
|
||||
];
|
||||
<Meta title="owncast/Frontend Assets/Images" />
|
||||
|
||||
# Images
|
||||
|
||||
## TODO: Determine the icon style/images for v2 of the web UI.
|
||||
<ImageRow images={[
|
||||
{src: "img/fediverse-black.png", name: "fediverse-black.png"},
|
||||
{src: "img/fediverse-color.png", name: "fediverse-color.png"},
|
||||
{src: "img/follow.svg", name: "follow.svg"},
|
||||
{src: "img/indieauth.png", name: "indieauth.png"},
|
||||
{src: "img/like.svg", name: "like.svg"},
|
||||
{src: "img/repost.svg", name: "repost.svg"},
|
||||
]}/>
|
||||
|
||||
<ImageRow images={images} />
|
||||
|
||||
## App Icons
|
||||
|
||||
export const icons = [
|
||||
{
|
||||
src: FediverseColor,
|
||||
name: 'Fediverse Color',
|
||||
},
|
||||
{
|
||||
src: FediverseBlack,
|
||||
name: 'Fediverse Black',
|
||||
},
|
||||
{
|
||||
src: Moderator,
|
||||
name: 'Moderator',
|
||||
},
|
||||
{
|
||||
src: IndieAuth,
|
||||
name: 'IndieAuth',
|
||||
},
|
||||
{
|
||||
src: IsBot,
|
||||
name: 'Bot Flag',
|
||||
},
|
||||
];
|
||||
|
||||
<ImageRow images={icons} />
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
import { Image, ImageRow } from './ImageAsset';
|
||||
|
||||
<Meta title="owncast/Project Assets/Logos & Graphics" />
|
||||
|
||||
# Logos & Graphics
|
||||
|
||||
<ImageRow images={[
|
||||
{src: "project/header.png", name: "header.png"},
|
||||
{src: "project/kiss-cut-stickers-5.5x5.5-default-60874a6c11849.png", name: "kiss-cut-stickers-5.5x5.5-default-60874a6c11849.png"},
|
||||
{src: "project/logo-glare-outlined.png", name: "logo-glare-outlined.png"},
|
||||
{src: "project/logo-glare-vector.svg", name: "logo-glare-vector.svg"},
|
||||
{src: "project/logo-noglare-vector.svg", name: "logo-noglare-vector.svg"},
|
||||
{src: "project/logo-translucent-grey.svg", name: "logo-translucent-grey.svg"},
|
||||
{src: "project/logo-white.svg", name: "logo-white.svg"},
|
||||
{src: "project/owncast-background.png", name: "owncast-background.png"},
|
||||
{src: "project/owncast-browser-mobile.png", name: "owncast-browser-mobile.png"},
|
||||
{src: "project/sticker-bigtech-alt.png", name: "sticker-bigtech-alt.png"},
|
||||
]}/>
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
|
||||
<Meta title="owncast/Documentation/Product Definition" />
|
||||
|
||||
# Owncast Product Definition
|
||||
|
||||
## Why
|
||||
|
||||
By defining the goals and target user bases we have something stable to guide decisions, features, conversations and keep clarity around what is being built.
|
||||
|
||||
While these definitions and lists should not be seen as exhaustive, in theory, once this is seen as "complete" there should be few, if any changes, as that would note a large change in direction and goals.
|
||||
|
||||
[TOC]
|
||||
|
||||
## Vision
|
||||
|
||||
> The out-of-the-box personal broadcast platform for DIY streamers and integrators.
|
||||
|
||||
## Primary Goals
|
||||
|
||||
- Useful out of the box.
|
||||
- Fast to get running.
|
||||
- Self-contained.
|
||||
- An alternative, not a competitor.
|
||||
- For individuals, not service providers.
|
||||
- Easy to integrate into other projects/products.
|
||||
- Low barrier to entry.
|
||||
- Empowering.
|
||||
- Customizable and hackable.
|
||||
|
||||
## Primary Users
|
||||
|
||||
### The DIY Streamer
|
||||
|
||||
An individual who is streaming as a hobby, a project, or is moving their audience from an existing streaming platform.
|
||||
|
||||
**Needs**:
|
||||
|
||||
- Security/ownership of their own stream.
|
||||
- Building an independent space.
|
||||
- Personalization.
|
||||
- Tools to manage a relationship with their audience.
|
||||
|
||||
**Pains**:
|
||||
|
||||
- Kicked off other streaming services.
|
||||
- Feeling of inequality or bias.
|
||||
- Their content has low visibility.
|
||||
- Platform rules do not align with them.
|
||||
- Do not agree with the forced ads, tracking and analytics.
|
||||
|
||||
### The Integrator
|
||||
|
||||
An individual or organization that has existing content, products or platforms that they want to add live streaming functionality to.
|
||||
|
||||
**Needs**:
|
||||
|
||||
- Broadcasting without censorship.
|
||||
- Full ownership of their brand.
|
||||
- Embedding and 3rd party playback.
|
||||
- Support private or invite-only streams.
|
||||
- Independence.
|
||||
|
||||
**Pains**:
|
||||
|
||||
- Censorship.
|
||||
- Rules.
|
||||
- Ads.
|
||||
- Risk of losing viewers from competitors and distractions.
|
||||
|
||||
**Desires**:
|
||||
|
||||
- Hosting events.
|
||||
- Running their own broadcasting service.
|
||||
|
||||
## Secondary Users
|
||||
|
||||
### The Viewer
|
||||
|
||||
An audience member that is often, but not always, taking part in chat.
|
||||
|
||||
**Needs**:
|
||||
|
||||
- To watch high quality video.
|
||||
- Ways to interact with the streamer. Chat, memes, emoji.
|
||||
- Calls to actions, links, next steps.
|
||||
|
||||
**Pains**:
|
||||
|
||||
- Understanding the interface and knowing they're in the correct place.
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
import { Image, ImageRow } from './ImageAsset';
|
||||
|
||||
<Meta title="owncast/Frontend Assets/Social Platform Images" />
|
||||
|
||||
# Social Platform Images
|
||||
|
||||
<ImageRow images={[
|
||||
{src: "img/platformlogos/bandcamp.svg", name: "bandcamp.svg"},
|
||||
{src: "img/platformlogos/default.svg", name: "default.svg"},
|
||||
{src: "img/platformlogos/discord.svg", name: "discord.svg"},
|
||||
{src: "img/platformlogos/donate.svg", name: "donate.svg"},
|
||||
{src: "img/platformlogos/facebook.svg", name: "facebook.svg"},
|
||||
{src: "img/platformlogos/fediverse.svg", name: "fediverse.svg"},
|
||||
{src: "img/platformlogos/follow.svg", name: "follow.svg"},
|
||||
{src: "img/platformlogos/github.svg", name: "github.svg"},
|
||||
{src: "img/platformlogos/gitlab.svg", name: "gitlab.svg"},
|
||||
{src: "img/platformlogos/google.svg", name: "google.svg"},
|
||||
{src: "img/platformlogos/instagram.svg", name: "instagram.svg"},
|
||||
{src: "img/platformlogos/keyoxide.png", name: "keyoxide.png"},
|
||||
{src: "img/platformlogos/ko-fi.svg", name: "ko-fi.svg"},
|
||||
{src: "img/platformlogos/lbry.svg", name: "lbry.svg"},
|
||||
{src: "img/platformlogos/liberapay.svg", name: "liberapay.svg"},
|
||||
{src: "img/platformlogos/link.svg", name: "link.svg"},
|
||||
{src: "img/platformlogos/linkedin.svg", name: "linkedin.svg"},
|
||||
{src: "img/platformlogos/mastodon.svg", name: "mastodon.svg"},
|
||||
{src: "img/platformlogos/matrix.svg", name: "matrix.svg"},
|
||||
{src: "img/platformlogos/odysee.svg", name: "odysee.svg"},
|
||||
{src: "img/platformlogos/patreon.svg", name: "patreon.svg"},
|
||||
{src: "img/platformlogos/paypal.svg", name: "paypal.svg"},
|
||||
{src: "img/platformlogos/snapchat.svg", name: "snapchat.svg"},
|
||||
{src: "img/platformlogos/soundcloud.svg", name: "soundcloud.svg"},
|
||||
{src: "img/platformlogos/spotify.svg", name: "spotify.svg"},
|
||||
{src: "img/platformlogos/steam.svg", name: "steam.svg"},
|
||||
{src: "img/platformlogos/tiktok.svg", name: "tiktok.svg"},
|
||||
{src: "img/platformlogos/twitch.svg", name: "twitch.svg"},
|
||||
{src: "img/platformlogos/twitter.svg", name: "twitter.svg"},
|
||||
{src: "img/platformlogos/xmpp.svg", name: "xmpp.svg"},
|
||||
{src: "img/platformlogos/youtube.svg", name: "youtube.svg"},
|
||||
]}/>
|
||||
|
||||
23
web/.storybook/stories-category-doc-pages/Tshirt.stories.mdx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
import { Image, ImageRow } from './ImageAsset';
|
||||
|
||||
<Meta title="owncast/Project Assets/T-Shirt" />
|
||||
|
||||
# T-shirt
|
||||
|
||||
<ImageRow images={[
|
||||
{src: "tshirt/all-over-print-mens-crew-neck-t-shirt-white-back-60873dde52297.png", name: "all-over-print-mens-crew-neck-t-shirt-white-back-60873dde52297.png"},
|
||||
{src: "tshirt/all-over-print-mens-crew-neck-t-shirt-white-back-60873dde523ae.png", name: "all-over-print-mens-crew-neck-t-shirt-white-back-60873dde523ae.png"},
|
||||
{src: "tshirt/all-over-print-mens-crew-neck-t-shirt-white-back-60873dde524ca.png", name: "all-over-print-mens-crew-neck-t-shirt-white-back-60873dde524ca.png"},
|
||||
{src: "tshirt/all-over-print-mens-crew-neck-t-shirt-white-front-60873dde51eb3.png", name: "all-over-print-mens-crew-neck-t-shirt-white-front-60873dde51eb3.png"},
|
||||
{src: "tshirt/all-over-print-mens-crew-neck-t-shirt-white-front-60873dde52064.png", name: "all-over-print-mens-crew-neck-t-shirt-white-front-60873dde52064.png"},
|
||||
{src: "tshirt/all-over-print-mens-crew-neck-t-shirt-white-left-60873dde525e2.png", name: "all-over-print-mens-crew-neck-t-shirt-white-left-60873dde525e2.png"},
|
||||
{src: "tshirt/all-over-print-mens-crew-neck-t-shirt-white-right-60873dde52184.png", name: "all-over-print-mens-crew-neck-t-shirt-white-right-60873dde52184.png"},
|
||||
{src: "tshirt/all-over-print-womens-crew-neck-t-shirt-white-back-6087418b62999.png", name: "all-over-print-womens-crew-neck-t-shirt-white-back-6087418b62999.png"},
|
||||
{src: "tshirt/all-over-print-womens-crew-neck-t-shirt-white-back-6087418b62aa4.png", name: "all-over-print-womens-crew-neck-t-shirt-white-back-6087418b62aa4.png"},
|
||||
{src: "tshirt/all-over-print-womens-crew-neck-t-shirt-white-front-6087418b626d5.png", name: "all-over-print-womens-crew-neck-t-shirt-white-front-6087418b626d5.png"},
|
||||
{src: "tshirt/all-over-print-womens-crew-neck-t-shirt-white-front-6087418b62878.png", name: "all-over-print-womens-crew-neck-t-shirt-white-front-6087418b62878.png"},
|
||||
{src: "tshirt/all-over-print-womens-crew-neck-t-shirt-white-left-6087418b62b91.png", name: "all-over-print-womens-crew-neck-t-shirt-white-left-6087418b62b91.png"},
|
||||
{src: "tshirt/all-over-print-womens-crew-neck-t-shirt-white-right-6087418b62c88.png", name: "all-over-print-womens-crew-neck-t-shirt-white-right-6087418b62c88.png"},
|
||||
]}/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
|
||||
<Meta title="owncast/Style Guide/Typography" />
|
||||
<Meta title="owncast/Styles/Typography" />
|
||||
|
||||
## Body
|
||||
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
|
||||
<Meta title="owncast/Documentation/Building Frontend Components" />
|
||||
|
||||
# How we develop components
|
||||
|
||||
This document outlines how we develop the components for the Owncast Web UI.
|
||||
|
||||
You should use this document as a guide when making changes to existing components, and adding new ones.
|
||||
Working with the same development process help keep the project maintainable.
|
||||
|
||||
## What are components
|
||||
|
||||
A component in React is a custom HTML element. They're included in the DOM just like regular elements `<ChatBox /`>.
|
||||
|
||||
## Functional Components
|
||||
|
||||
In react, there's two ways to write a component: there's Class-based Components, and Functional Components.
|
||||
|
||||
Class-based is older and has fallen out of favor.
|
||||
Functional Components are the new standard and you'll find them in most React projects written today.
|
||||
|
||||
See the [React Functional Component docs](https://reactjs.org/docs/components-and-props.html) for more info.
|
||||
|
||||
### How we write Functional Components
|
||||
|
||||
We've defined a pattern for how we write Functional Components in the Owncast Web UI.
|
||||
There's a few ways to to write Functional Components that are common, so defining a standard helps keep this project readable and consistent.
|
||||
|
||||
The pattern we've settled on is:
|
||||
|
||||
**For stateless components:**
|
||||
|
||||
```tsx
|
||||
export type MyNewButtonProps = {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
export const MyNewButton: FC<MyNewButtonProps> = ({ label, onClick }) => (
|
||||
<button onClick={onCLick}>{label}</button>
|
||||
);
|
||||
```
|
||||
|
||||
**For stateful components:**
|
||||
|
||||
```tsx
|
||||
export type MyNewButtonProps = {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
export const MyNewButton: FC<MyNewButtonProps> = ({ label, onClick }) => {
|
||||
// do something, then call the onClick fn. e.g.:
|
||||
const handleClick = useCallback(() => {
|
||||
alert(label);
|
||||
onClick && onClick();
|
||||
}, [label, onClick]);
|
||||
|
||||
return <button onClick={onCLick}>{label}</button>;
|
||||
};
|
||||
```
|
||||
|
||||
### Rationale
|
||||
|
||||
Since there's a lot of ways to create components, settling on one pattern helps maintain readability.
|
||||
But why _this_ style?
|
||||
|
||||
See the discussion on the PR that introduced this pattern: [#2082](https://github.com/owncast/owncast/pull/2082).
|
||||
|
||||
## Storybook
|
||||
|
||||
We use [Storybook](https://storybook.js.org/) to create a component library where we can see and interact with each component.
|
||||
|
||||
Make sure to include a `.stories.tsx` file with each (exported) component you create, and to update the stories file when making changes to existing components.
|
||||
|
||||
You can run the Storybook server locally with `npm run storybook`.
|
||||
|
||||
BIN
web/.storybook/story-assets/project/header.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 127 KiB |
BIN
web/.storybook/story-assets/project/logo-glare-outlined.png
Normal file
|
After Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
@@ -0,0 +1,80 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 96 105"
|
||||
version="1.1"
|
||||
xml:space="preserve"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"
|
||||
class="svg-logo-solid"
|
||||
id="svg125"
|
||||
sodipodi:docname="logo-translucent-grey.svg"
|
||||
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs129" /><sodipodi:namedview
|
||||
id="namedview127"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="7.4571429"
|
||||
inkscape:cx="19.444444"
|
||||
inkscape:cy="68.591954"
|
||||
inkscape:window-width="1916"
|
||||
inkscape:window-height="1055"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="21"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg125" />
|
||||
<g
|
||||
transform="matrix(1.04457,0,0,1.04457,-0.742448,-0.0626735)"
|
||||
id="g123"
|
||||
style="fill:#4699ff;opacity:0.04;fill-opacity:1">
|
||||
<g
|
||||
id="g105"
|
||||
style="fill:#4699ff;fill-opacity:1">
|
||||
<path
|
||||
d="M91.5,75.35C92.533,72.55 92.583,70 91.65,67.7C90.783,65.567 89.117,63.767 86.65,62.3C84.35,60.967 81.567,60 78.3,59.4C75.333,58.867 72.1,58.633 68.6,58.7C65.233,58.8 61.967,59.167 58.8,59.8C55.767,60.433 53.1,61.233 50.8,62.2C48.533,63.167 46.767,64.217 45.5,65.35C44.233,66.55 43.567,67.783 43.5,69.05C43.4,70.55 44.167,72.167 45.8,73.9C47.3,75.5 49.4,77.067 52.1,78.6C54.8,80.133 57.783,81.45 61.05,82.55C64.55,83.717 68,84.467 71.4,84.8C73.6,85 75.65,85.033 77.55,84.9C79.617,84.7 81.533,84.267 83.3,83.6C85.2,82.867 86.817,81.85 88.15,80.55C89.65,79.117 90.767,77.383 91.5,75.35M70.6,67.5C71.733,68.1 72.567,68.833 73.1,69.7C73.633,70.667 73.75,71.767 73.45,73C73.217,73.867 72.833,74.617 72.3,75.25C71.8,75.817 71.133,76.267 70.3,76.6C69.6,76.9 68.75,77.117 67.75,77.25C66.783,77.35 65.817,77.367 64.85,77.3C63.15,77.2 61.283,76.867 59.25,76.3C57.483,75.767 55.783,75.1 54.15,74.3C52.65,73.567 51.417,72.8 50.45,72C49.517,71.167 49.067,70.433 49.1,69.8C49.167,69.267 49.55,68.75 50.25,68.25C50.95,67.783 51.917,67.367 53.15,67C54.383,66.6 55.75,66.3 57.25,66.1C58.95,65.9 60.567,65.8 62.1,65.8C63.8,65.833 65.333,65.967 66.7,66.2C68.167,66.5 69.467,66.933 70.6,67.5Z"
|
||||
style="fill:#4699ff;fill-rule:nonzero;fill-opacity:1"
|
||||
id="path103" />
|
||||
</g>
|
||||
<g
|
||||
id="g109"
|
||||
style="fill:#4699ff;fill-opacity:1">
|
||||
<path
|
||||
d="M66.6,15.05C66.467,11.45 65.567,8.45 63.9,6.05C62.133,3.417 59.533,1.617 56.1,0.65C55.333,0.417 54.517,0.25 53.65,0.15C52.883,0.05 52.1,0.033 51.3,0.1C50.567,0.1 49.833,0.183 49.1,0.35C48.467,0.483 47.767,0.7 47,1C44.533,1.967 42.3,3.667 40.3,6.1C38.433,8.3 36.833,11.033 35.5,14.3C34.333,17.067 33.4,20.1 32.7,23.4C32.033,26.5 31.583,29.65 31.35,32.85C31.15,35.75 31.133,38.533 31.3,41.2C31.5,43.833 31.867,46.217 32.4,48.35C33.467,52.717 35.1,55.4 37.3,56.4C37.5,56.5 37.7,56.583 37.9,56.65L39.2,56.85C39.367,56.85 39.617,56.833 39.95,56.8C41.35,56.667 42.933,56.083 44.7,55.05C46.4,54.017 48.183,52.6 50.05,50.8C52.05,48.867 53.983,46.617 55.85,44.05C57.817,41.383 59.567,38.567 61.1,35.6C62.9,32.1 64.283,28.667 65.25,25.3C66.25,21.6 66.7,18.183 66.6,15.05M47.55,23.15C47.883,23.217 48.167,23.3 48.4,23.4C51.1,24.333 52.483,26.483 52.55,29.85C52.583,32.617 51.733,35.8 50,39.4C48.567,42.4 46.85,45.033 44.85,47.3C42.983,49.433 41.417,50.567 40.15,50.7L39.9,50.75L39.45,50.7L39.2,50.6C38.267,50.167 37.617,48.75 37.25,46.35C36.883,43.917 36.9,41.133 37.3,38C37.733,34.5 38.55,31.433 39.75,28.8C41.183,25.667 42.95,23.817 45.05,23.25C45.417,23.15 45.683,23.1 45.85,23.1C46.117,23.067 46.383,23.05 46.65,23.05C46.917,23.05 47.217,23.083 47.55,23.15Z"
|
||||
style="fill:#4699ff;fill-rule:nonzero;fill-opacity:1"
|
||||
id="path107" />
|
||||
</g>
|
||||
<g
|
||||
id="g113"
|
||||
style="fill:#4699ff;fill-opacity:1">
|
||||
<path
|
||||
d="M2.7,33.6C2.3,34.133 1.967,34.717 1.7,35.35C1.4,36.117 1.183,36.9 1.05,37.7C0.35,40.967 0.733,44.133 2.2,47.2C3.4,49.733 5.333,52.117 8,54.35C10.367,56.317 13.033,57.917 16,59.15C19,60.383 21.617,60.95 23.85,60.85C24.283,60.85 24.75,60.8 25.25,60.7C25.75,60.6 26.167,60.467 26.5,60.3C26.833,60.133 27.15,59.917 27.45,59.65C27.75,59.383 27.983,59.083 28.15,58.75C28.95,57.217 28.733,54.85 27.5,51.65C26.233,48.55 24.317,45.367 21.75,42.1C19.083,38.7 16.3,36.017 13.4,34.05C10.267,31.95 7.617,31.167 5.45,31.7C4.917,31.833 4.417,32.067 3.95,32.4C3.483,32.7 3.067,33.1 2.7,33.6M10.1,43.55C10.267,43.25 10.433,43.017 10.6,42.85C10.767,42.683 10.967,42.533 11.2,42.4C11.467,42.3 11.7,42.233 11.9,42.2C12.967,42 14.317,42.467 15.95,43.6C17.417,44.567 18.883,45.933 20.35,47.7C21.683,49.3 22.75,50.867 23.55,52.4C24.317,53.967 24.55,55.067 24.25,55.7C24.183,55.833 24.1,55.933 24,56C23.9,56.133 23.783,56.217 23.65,56.25C23.583,56.317 23.45,56.367 23.25,56.4L22.7,56.5C21.633,56.567 20.25,56.267 18.55,55.6C16.883,54.933 15.317,54.05 13.85,52.95C12.283,51.783 11.117,50.517 10.35,49.15C9.483,47.583 9.283,46.017 9.75,44.45C9.85,44.117 9.967,43.817 10.1,43.55Z"
|
||||
style="fill:#4699ff;fill-rule:nonzero;fill-opacity:1"
|
||||
id="path111" />
|
||||
</g>
|
||||
<g
|
||||
id="g117"
|
||||
style="fill:#4699ff;fill-opacity:1">
|
||||
<path
|
||||
d="M34.95,74.2L34.75,74.2C33.717,74.167 32.767,74.517 31.9,75.25C31.1,75.95 30.417,76.95 29.85,78.25C29.35,79.417 29,80.733 28.8,82.2C28.6,83.667 28.567,85.15 28.7,86.65C28.967,89.817 29.9,92.5 31.5,94.7C33.367,97.233 35.967,98.9 39.3,99.7L39.4,99.7L39.7,99.8L39.85,99.8C43.483,100.5 45.917,99.817 47.15,97.75C47.717,96.783 48,95.55 48,94.05C47.967,92.617 47.7,91.05 47.2,89.35C46.7,87.617 46,85.883 45.1,84.15C44.2,82.383 43.183,80.783 42.05,79.35C40.85,77.85 39.65,76.65 38.45,75.75C37.183,74.817 36.017,74.3 34.95,74.2M33.55,80.4C34.083,78.933 34.767,78.233 35.6,78.3L35.65,78.3C36.483,78.4 37.467,79.267 38.6,80.9C39.733,82.533 40.583,84.25 41.15,86.05C41.783,88.017 41.917,89.583 41.55,90.75C41.117,91.983 40.05,92.483 38.35,92.25L38.3,92.25L38.25,92.2L38.1,92.2C36.433,91.867 35.15,91 34.25,89.6C33.483,88.333 33.05,86.8 32.95,85C32.85,83.233 33.05,81.7 33.55,80.4Z"
|
||||
style="fill:#4699ff;fill-rule:nonzero;fill-opacity:1"
|
||||
id="path115" />
|
||||
</g>
|
||||
<g
|
||||
id="g121"
|
||||
style="fill:#4699ff;fill-opacity:1">
|
||||
<path
|
||||
d="M22.7,69.65C22.4,69.417 22.033,69.217 21.6,69.05C21.167,68.883 20.717,68.767 20.25,68.7C19.817,68.6 19.35,68.533 18.85,68.5C17.417,68.467 16.017,68.683 14.65,69.15C13.317,69.583 12.233,70.233 11.4,71.1C10.567,72.033 10.167,73.067 10.2,74.2C10.233,75.433 10.817,76.767 11.95,78.2C12.25,78.567 12.617,78.967 13.05,79.4C13.383,79.733 13.767,80.033 14.2,80.3C14.533,80.5 14.9,80.683 15.3,80.85C15.767,81.017 16.133,81.1 16.4,81.1C17.6,81.267 18.767,81.017 19.9,80.35C21,79.717 21.95,78.817 22.75,77.65C23.583,76.45 24.1,75.217 24.3,73.95C24.5,72.55 24.25,71.4 23.55,70.5C23.283,70.167 23,69.883 22.7,69.65M21.7,71.7C22,72.1 22.067,72.633 21.9,73.3C21.767,73.933 21.467,74.583 21,75.25C20.533,75.883 20,76.383 19.4,76.75C18.767,77.15 18.15,77.317 17.55,77.25L17,77.15C16.8,77.083 16.617,76.983 16.45,76.85C16.317,76.783 16.133,76.65 15.9,76.45C15.767,76.317 15.6,76.133 15.4,75.9C14.8,75.133 14.567,74.433 14.7,73.8C14.767,73.233 15.117,72.733 15.75,72.3C16.317,71.9 17,71.6 17.8,71.4C18.6,71.2 19.367,71.117 20.1,71.15L20.65,71.2L21.1,71.3C21.233,71.367 21.35,71.433 21.45,71.5L21.7,71.7Z"
|
||||
style="fill:#4699ff;fill-rule:nonzero;fill-opacity:1"
|
||||
id="path119" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.5 KiB |
19
web/.storybook/story-assets/project/logo-white.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<svg width="100%" height="100%" viewBox="0 0 96 105" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;" class="svg-logo-solid">
|
||||
<g transform="matrix(1.04457,0,0,1.04457,-0.742448,-0.0626735)">
|
||||
<g>
|
||||
<path d="M91.5,75.35C92.533,72.55 92.583,70 91.65,67.7C90.783,65.567 89.117,63.767 86.65,62.3C84.35,60.967 81.567,60 78.3,59.4C75.333,58.867 72.1,58.633 68.6,58.7C65.233,58.8 61.967,59.167 58.8,59.8C55.767,60.433 53.1,61.233 50.8,62.2C48.533,63.167 46.767,64.217 45.5,65.35C44.233,66.55 43.567,67.783 43.5,69.05C43.4,70.55 44.167,72.167 45.8,73.9C47.3,75.5 49.4,77.067 52.1,78.6C54.8,80.133 57.783,81.45 61.05,82.55C64.55,83.717 68,84.467 71.4,84.8C73.6,85 75.65,85.033 77.55,84.9C79.617,84.7 81.533,84.267 83.3,83.6C85.2,82.867 86.817,81.85 88.15,80.55C89.65,79.117 90.767,77.383 91.5,75.35M70.6,67.5C71.733,68.1 72.567,68.833 73.1,69.7C73.633,70.667 73.75,71.767 73.45,73C73.217,73.867 72.833,74.617 72.3,75.25C71.8,75.817 71.133,76.267 70.3,76.6C69.6,76.9 68.75,77.117 67.75,77.25C66.783,77.35 65.817,77.367 64.85,77.3C63.15,77.2 61.283,76.867 59.25,76.3C57.483,75.767 55.783,75.1 54.15,74.3C52.65,73.567 51.417,72.8 50.45,72C49.517,71.167 49.067,70.433 49.1,69.8C49.167,69.267 49.55,68.75 50.25,68.25C50.95,67.783 51.917,67.367 53.15,67C54.383,66.6 55.75,66.3 57.25,66.1C58.95,65.9 60.567,65.8 62.1,65.8C63.8,65.833 65.333,65.967 66.7,66.2C68.167,66.5 69.467,66.933 70.6,67.5Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M66.6,15.05C66.467,11.45 65.567,8.45 63.9,6.05C62.133,3.417 59.533,1.617 56.1,0.65C55.333,0.417 54.517,0.25 53.65,0.15C52.883,0.05 52.1,0.033 51.3,0.1C50.567,0.1 49.833,0.183 49.1,0.35C48.467,0.483 47.767,0.7 47,1C44.533,1.967 42.3,3.667 40.3,6.1C38.433,8.3 36.833,11.033 35.5,14.3C34.333,17.067 33.4,20.1 32.7,23.4C32.033,26.5 31.583,29.65 31.35,32.85C31.15,35.75 31.133,38.533 31.3,41.2C31.5,43.833 31.867,46.217 32.4,48.35C33.467,52.717 35.1,55.4 37.3,56.4C37.5,56.5 37.7,56.583 37.9,56.65L39.2,56.85C39.367,56.85 39.617,56.833 39.95,56.8C41.35,56.667 42.933,56.083 44.7,55.05C46.4,54.017 48.183,52.6 50.05,50.8C52.05,48.867 53.983,46.617 55.85,44.05C57.817,41.383 59.567,38.567 61.1,35.6C62.9,32.1 64.283,28.667 65.25,25.3C66.25,21.6 66.7,18.183 66.6,15.05M47.55,23.15C47.883,23.217 48.167,23.3 48.4,23.4C51.1,24.333 52.483,26.483 52.55,29.85C52.583,32.617 51.733,35.8 50,39.4C48.567,42.4 46.85,45.033 44.85,47.3C42.983,49.433 41.417,50.567 40.15,50.7L39.9,50.75L39.45,50.7L39.2,50.6C38.267,50.167 37.617,48.75 37.25,46.35C36.883,43.917 36.9,41.133 37.3,38C37.733,34.5 38.55,31.433 39.75,28.8C41.183,25.667 42.95,23.817 45.05,23.25C45.417,23.15 45.683,23.1 45.85,23.1C46.117,23.067 46.383,23.05 46.65,23.05C46.917,23.05 47.217,23.083 47.55,23.15Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M2.7,33.6C2.3,34.133 1.967,34.717 1.7,35.35C1.4,36.117 1.183,36.9 1.05,37.7C0.35,40.967 0.733,44.133 2.2,47.2C3.4,49.733 5.333,52.117 8,54.35C10.367,56.317 13.033,57.917 16,59.15C19,60.383 21.617,60.95 23.85,60.85C24.283,60.85 24.75,60.8 25.25,60.7C25.75,60.6 26.167,60.467 26.5,60.3C26.833,60.133 27.15,59.917 27.45,59.65C27.75,59.383 27.983,59.083 28.15,58.75C28.95,57.217 28.733,54.85 27.5,51.65C26.233,48.55 24.317,45.367 21.75,42.1C19.083,38.7 16.3,36.017 13.4,34.05C10.267,31.95 7.617,31.167 5.45,31.7C4.917,31.833 4.417,32.067 3.95,32.4C3.483,32.7 3.067,33.1 2.7,33.6M10.1,43.55C10.267,43.25 10.433,43.017 10.6,42.85C10.767,42.683 10.967,42.533 11.2,42.4C11.467,42.3 11.7,42.233 11.9,42.2C12.967,42 14.317,42.467 15.95,43.6C17.417,44.567 18.883,45.933 20.35,47.7C21.683,49.3 22.75,50.867 23.55,52.4C24.317,53.967 24.55,55.067 24.25,55.7C24.183,55.833 24.1,55.933 24,56C23.9,56.133 23.783,56.217 23.65,56.25C23.583,56.317 23.45,56.367 23.25,56.4L22.7,56.5C21.633,56.567 20.25,56.267 18.55,55.6C16.883,54.933 15.317,54.05 13.85,52.95C12.283,51.783 11.117,50.517 10.35,49.15C9.483,47.583 9.283,46.017 9.75,44.45C9.85,44.117 9.967,43.817 10.1,43.55Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M34.95,74.2L34.75,74.2C33.717,74.167 32.767,74.517 31.9,75.25C31.1,75.95 30.417,76.95 29.85,78.25C29.35,79.417 29,80.733 28.8,82.2C28.6,83.667 28.567,85.15 28.7,86.65C28.967,89.817 29.9,92.5 31.5,94.7C33.367,97.233 35.967,98.9 39.3,99.7L39.4,99.7L39.7,99.8L39.85,99.8C43.483,100.5 45.917,99.817 47.15,97.75C47.717,96.783 48,95.55 48,94.05C47.967,92.617 47.7,91.05 47.2,89.35C46.7,87.617 46,85.883 45.1,84.15C44.2,82.383 43.183,80.783 42.05,79.35C40.85,77.85 39.65,76.65 38.45,75.75C37.183,74.817 36.017,74.3 34.95,74.2M33.55,80.4C34.083,78.933 34.767,78.233 35.6,78.3L35.65,78.3C36.483,78.4 37.467,79.267 38.6,80.9C39.733,82.533 40.583,84.25 41.15,86.05C41.783,88.017 41.917,89.583 41.55,90.75C41.117,91.983 40.05,92.483 38.35,92.25L38.3,92.25L38.25,92.2L38.1,92.2C36.433,91.867 35.15,91 34.25,89.6C33.483,88.333 33.05,86.8 32.95,85C32.85,83.233 33.05,81.7 33.55,80.4Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M22.7,69.65C22.4,69.417 22.033,69.217 21.6,69.05C21.167,68.883 20.717,68.767 20.25,68.7C19.817,68.6 19.35,68.533 18.85,68.5C17.417,68.467 16.017,68.683 14.65,69.15C13.317,69.583 12.233,70.233 11.4,71.1C10.567,72.033 10.167,73.067 10.2,74.2C10.233,75.433 10.817,76.767 11.95,78.2C12.25,78.567 12.617,78.967 13.05,79.4C13.383,79.733 13.767,80.033 14.2,80.3C14.533,80.5 14.9,80.683 15.3,80.85C15.767,81.017 16.133,81.1 16.4,81.1C17.6,81.267 18.767,81.017 19.9,80.35C21,79.717 21.95,78.817 22.75,77.65C23.583,76.45 24.1,75.217 24.3,73.95C24.5,72.55 24.25,71.4 23.55,70.5C23.283,70.167 23,69.883 22.7,69.65M21.7,71.7C22,72.1 22.067,72.633 21.9,73.3C21.767,73.933 21.467,74.583 21,75.25C20.533,75.883 20,76.383 19.4,76.75C18.767,77.15 18.15,77.317 17.55,77.25L17,77.15C16.8,77.083 16.617,76.983 16.45,76.85C16.317,76.783 16.133,76.65 15.9,76.45C15.767,76.317 15.6,76.133 15.4,75.9C14.8,75.133 14.567,74.433 14.7,73.8C14.767,73.233 15.117,72.733 15.75,72.3C16.317,71.9 17,71.6 17.8,71.4C18.6,71.2 19.367,71.117 20.1,71.15L20.65,71.2L21.1,71.3C21.233,71.367 21.35,71.433 21.45,71.5L21.7,71.7Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.2 KiB |
BIN
web/.storybook/story-assets/project/owncast-background.png
Normal file
|
After Width: | Height: | Size: 3.2 MiB |
BIN
web/.storybook/story-assets/project/owncast-browser-mobile.png
Normal file
|
After Width: | Height: | Size: 572 KiB |
BIN
web/.storybook/story-assets/project/sticker-bigtech-alt.png
Normal file
|
After Width: | Height: | Size: 162 KiB |
|
After Width: | Height: | Size: 682 KiB |
|
After Width: | Height: | Size: 605 KiB |
|
After Width: | Height: | Size: 612 KiB |
|
After Width: | Height: | Size: 690 KiB |
|
After Width: | Height: | Size: 690 KiB |
|
After Width: | Height: | Size: 597 KiB |
|
After Width: | Height: | Size: 482 KiB |
|
After Width: | Height: | Size: 725 KiB |
|
After Width: | Height: | Size: 530 KiB |
|
After Width: | Height: | Size: 760 KiB |
|
After Width: | Height: | Size: 533 KiB |
|
After Width: | Height: | Size: 502 KiB |
|
After Width: | Height: | Size: 460 KiB |
@@ -1,11 +1,8 @@
|
||||
import React from 'react';
|
||||
import { DocsContainer as BaseContainer } from '@storybook/addon-docs';
|
||||
import { useDarkMode } from 'storybook-dark-mode';
|
||||
import { themes } from '@storybook/theming';
|
||||
|
||||
export const DocsContainer = ({ children, context }) => {
|
||||
const dark = useDarkMode();
|
||||
|
||||
return (
|
||||
<BaseContainer
|
||||
context={{
|
||||
@@ -18,7 +15,7 @@ export const DocsContainer = ({ children, context }) => {
|
||||
...storyContext?.parameters,
|
||||
docs: {
|
||||
...storyContext?.parameters?.docs,
|
||||
theme: dark ? themes.dark : themes.light,
|
||||
theme: themes.light,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
5
web/.storybook/tools/Document.stories.mdx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
|
||||
<Meta title="owncast/Documentation/{{title}}" />
|
||||
|
||||
{{content}}
|
||||
25
web/.storybook/tools/Emoji.stories.mdx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
import { Image, ImageRow } from './ImageAsset';
|
||||
|
||||
<Meta title="owncast/Frontend Assets/Emoji" />
|
||||
|
||||
# Built-in Custom Emoji
|
||||
|
||||
{{#each emojiCollections}}
|
||||
|
||||
## {{capitalize this.name}}
|
||||
|
||||
<Story
|
||||
name="{{capitalize this.name}}"
|
||||
>
|
||||
<a href="img/emoji/{{this.name}}/LICENSE.md" target="_blank">
|
||||
LICENSE
|
||||
</a>
|
||||
<ImageRow images={[
|
||||
{{#each this.images}}
|
||||
{src: "{{this.src}}", name: "{{this.name}}"},
|
||||
{{/each}}
|
||||
]}/>
|
||||
</Story>
|
||||
|
||||
{{/each}}
|
||||
12
web/.storybook/tools/Images.stories.mdx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Canvas, Meta, Story } from '@storybook/addon-docs';
|
||||
import { Image, ImageRow } from './ImageAsset';
|
||||
|
||||
<Meta title="{{category}}" />
|
||||
|
||||
# {{capitalize title}}
|
||||
|
||||
<ImageRow images={[
|
||||
{{#each images}}
|
||||
{src: "{{this.src}}", name: "{{this.name}}"},
|
||||
{{/each}}
|
||||
]}/>
|
||||
34
web/.storybook/tools/generate-document-stories.mjs
Normal file
@@ -0,0 +1,34 @@
|
||||
import fs from 'fs';
|
||||
import handlebars from 'handlebars';
|
||||
|
||||
const template = fs.readFileSync('./Document.stories.mdx', 'utf8');
|
||||
let t = handlebars.compile(template, { noEscape: true });
|
||||
|
||||
const documents = [
|
||||
{
|
||||
title: 'Product Definition',
|
||||
name: 'ProductDefinition',
|
||||
path: '../../../docs/product-definition.md',
|
||||
},
|
||||
{ title: 'Design', name: 'Design', path: '../../../.design/DESIGN.md' },
|
||||
{
|
||||
title: 'Building Frontend Components',
|
||||
name: 'WebComponents',
|
||||
path: '../../../web/components/_COMPONENT_HOW_TO.md',
|
||||
},
|
||||
{
|
||||
title: 'Get Started with Owncast Development',
|
||||
name: 'Development',
|
||||
path: '/tmp/development.md',
|
||||
},
|
||||
];
|
||||
|
||||
documents.forEach(doc => {
|
||||
if (!fs.existsSync(doc.path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const document = fs.readFileSync(doc.path, 'utf8');
|
||||
const output = t({ name: doc.name, title: doc.title, content: document });
|
||||
fs.writeFileSync(`../stories-category-doc-pages/${doc.name}.stories.mdx`, output);
|
||||
});
|
||||
37
web/.storybook/tools/generate-emoji-story.mjs
Normal file
@@ -0,0 +1,37 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { readdirSync } from 'fs';
|
||||
import handlebars from 'handlebars';
|
||||
|
||||
handlebars.registerHelper('capitalize', function (str) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
});
|
||||
|
||||
function getDirectories(path) {
|
||||
return fs.readdirSync(path).filter(function (file) {
|
||||
return fs.statSync(path + '/' + file).isDirectory();
|
||||
});
|
||||
}
|
||||
|
||||
const emojiDir = path.resolve('../../../static/img/emoji');
|
||||
|
||||
const emojiCollectionDirs = getDirectories(emojiDir).map(dir => {
|
||||
return dir;
|
||||
});
|
||||
|
||||
let emojiCollections = {};
|
||||
|
||||
emojiCollectionDirs.forEach(collection => {
|
||||
const emojiCollection = readdirSync(path.resolve(emojiDir, collection))
|
||||
.filter(f => f.toLowerCase() !== 'license.md')
|
||||
.map(emoji => {
|
||||
return { name: emoji, src: `img/emoji/${collection}/${emoji}` };
|
||||
});
|
||||
emojiCollections[collection] = { name: collection, images: emojiCollection };
|
||||
});
|
||||
|
||||
const template = fs.readFileSync('./Emoji.stories.mdx', 'utf8');
|
||||
let t = handlebars.compile(template);
|
||||
let output = t({ emojiCollections });
|
||||
console.log(output);
|
||||
36
web/.storybook/tools/generate-image-story.mjs
Normal file
@@ -0,0 +1,36 @@
|
||||
import fs from 'fs';
|
||||
import path, { resolve } from 'path';
|
||||
|
||||
import { readdirSync, lstatSync } from 'fs';
|
||||
import handlebars from 'handlebars';
|
||||
|
||||
handlebars.registerHelper('capitalize', function (str) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
});
|
||||
|
||||
const args = process.argv;
|
||||
const dir = args[2];
|
||||
const title = args[3];
|
||||
const category = args[4];
|
||||
const publicPath = args[5];
|
||||
|
||||
if (args.length < 6) {
|
||||
console.error('Usage: generate-image-story.mjs <dir> <title> <category> <webpublicpath>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const images = readdirSync(dir)
|
||||
.map(img => {
|
||||
const resolvedPath = path.resolve(dir, img);
|
||||
if (lstatSync(resolvedPath).isDirectory()) {
|
||||
return;
|
||||
}
|
||||
|
||||
return { name: img, src: `${publicPath}/${img}` };
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
const template = fs.readFileSync('./Images.stories.mdx', 'utf8');
|
||||
let t = handlebars.compile(template);
|
||||
let output = t({ images, title, category });
|
||||
console.log(output);
|
||||
17
web/.storybook/tools/generate-stories.sh
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Generate the custom Emoji story
|
||||
node generate-emoji-story.mjs >../stories-category-doc-pages/Emoji.stories.mdx
|
||||
|
||||
# Generate stories out of documentation
|
||||
|
||||
# Pull down the doc about development
|
||||
curl -s https://raw.githubusercontent.com/owncast/owncast.github.io/master/content/development.md >/tmp/development.md
|
||||
node generate-document-stories.mjs
|
||||
|
||||
# Project image assets
|
||||
|
||||
node generate-image-story.mjs ../../public/img/ Images "owncast/Frontend Assets/Images" "img" >../stories-category-doc-pages/Images.stories.mdx
|
||||
node generate-image-story.mjs ../../public/img/platformlogos/ "Social Platform Images" "owncast/Frontend Assets/Social Platform Images" "img/platformlogos" >../stories-category-doc-pages/SocialPlatformImages.stories.mdx
|
||||
node generate-image-story.mjs ../story-assets/project/ "Logos & Graphics" "owncast/Project Assets/Logos & Graphics" "project" >../stories-category-doc-pages/LogosAndGraphics.stories.mdx
|
||||
node generate-image-story.mjs ../story-assets/tshirt/ "T-shirt" "owncast/Project Assets/T-Shirt" "tshirt" >../stories-category-doc-pages/Tshirt.stories.mdx
|
||||
118
web/components/admin/EditCustomJavascript.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import React, { useState, useEffect, useContext, FC } from 'react';
|
||||
import { Typography, Button } from 'antd';
|
||||
import CodeMirror from '@uiw/react-codemirror';
|
||||
import { bbedit } from '@uiw/codemirror-theme-bbedit';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
|
||||
import { ServerStatusContext } from '../../utils/server-status-context';
|
||||
import {
|
||||
postConfigUpdateToAPI,
|
||||
RESET_TIMEOUT,
|
||||
API_CUSTOM_JAVASCRIPT,
|
||||
} from '../../utils/config-constants';
|
||||
import {
|
||||
createInputStatus,
|
||||
StatusState,
|
||||
STATUS_ERROR,
|
||||
STATUS_PROCESSING,
|
||||
STATUS_SUCCESS,
|
||||
} from '../../utils/input-statuses';
|
||||
import { FormStatusIndicator } from './FormStatusIndicator';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const EditCustomJavascript: FC = () => {
|
||||
const [content, setContent] = useState('/* Enter custom Javascript here */');
|
||||
const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
|
||||
const [hasChanged, setHasChanged] = useState(false);
|
||||
|
||||
const serverStatusData = useContext(ServerStatusContext);
|
||||
const { serverConfig, setFieldInConfigState } = serverStatusData || {};
|
||||
|
||||
const { instanceDetails } = serverConfig;
|
||||
const { customJavascript: initialContent } = instanceDetails;
|
||||
|
||||
let resetTimer = null;
|
||||
|
||||
// Clear out any validation states and messaging
|
||||
const resetStates = () => {
|
||||
setSubmitStatus(null);
|
||||
setHasChanged(false);
|
||||
clearTimeout(resetTimer);
|
||||
resetTimer = null;
|
||||
};
|
||||
|
||||
// posts all the tags at once as an array obj
|
||||
async function handleSave() {
|
||||
setSubmitStatus(createInputStatus(STATUS_PROCESSING));
|
||||
await postConfigUpdateToAPI({
|
||||
apiPath: API_CUSTOM_JAVASCRIPT,
|
||||
data: { value: content },
|
||||
onSuccess: (message: string) => {
|
||||
setFieldInConfigState({
|
||||
fieldName: 'customJavascript',
|
||||
value: content,
|
||||
path: 'instanceDetails',
|
||||
});
|
||||
setSubmitStatus(createInputStatus(STATUS_SUCCESS, message));
|
||||
},
|
||||
onError: (message: string) => {
|
||||
setSubmitStatus(createInputStatus(STATUS_ERROR, message));
|
||||
},
|
||||
});
|
||||
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setContent(initialContent);
|
||||
}, [instanceDetails]);
|
||||
|
||||
const onCSSValueChange = React.useCallback(value => {
|
||||
setContent(value);
|
||||
if (value !== initialContent && !hasChanged) {
|
||||
setHasChanged(true);
|
||||
} else if (value === initialContent && hasChanged) {
|
||||
setHasChanged(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="edit-custom-css">
|
||||
<Title level={3} className="section-title">
|
||||
Customize your page styling with CSS
|
||||
</Title>
|
||||
|
||||
<p className="description">
|
||||
Customize the look and feel of your Owncast instance by overriding the CSS styles of various
|
||||
components on the page. Refer to the{' '}
|
||||
<a href="https://owncast.online/docs/website/" rel="noopener noreferrer" target="_blank">
|
||||
CSS & Components guide
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<p className="description">
|
||||
Please input plain CSS text, as this will be directly injected onto your page during load.
|
||||
</p>
|
||||
|
||||
<CodeMirror
|
||||
value={content}
|
||||
placeholder="/* Enter custom Javascript here */"
|
||||
theme={bbedit}
|
||||
height="200px"
|
||||
extensions={[javascript()]}
|
||||
onChange={onCSSValueChange}
|
||||
/>
|
||||
|
||||
<br />
|
||||
<div className="page-content-actions">
|
||||
{hasChanged && (
|
||||
<Button type="primary" onClick={handleSave}>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
<FormStatusIndicator status={submitStatus} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -261,7 +261,7 @@ export const MainLayout: FC<MainLayoutProps> = ({ children }) => {
|
||||
},
|
||||
upgradeVersion && {
|
||||
key: 'upgrade',
|
||||
label: <Link href="/upgrade">{upgradeMessage}</Link>,
|
||||
label: <Link href="/admin/upgrade">{upgradeMessage}</Link>,
|
||||
},
|
||||
{
|
||||
key: 'help',
|
||||
|
||||
@@ -125,7 +125,7 @@ export const Offline: FC<OfflineProps> = ({ logs = [], config }) => {
|
||||
|
||||
if (!config?.federation?.enabled) {
|
||||
data.push({
|
||||
icon: <img alt="fediverse" width="20px" src="fediverse-white.png" />,
|
||||
icon: <img alt="fediverse" width="20px" src="/img/fediverse-color.png" />,
|
||||
title: 'Add your Owncast instance to the Fediverse',
|
||||
content: (
|
||||
<div>
|
||||
|
||||
@@ -284,6 +284,8 @@ export const VideoVariantForm: FC<VideoVariantFormProps> = ({
|
||||
onConfirm={handleVideoPassConfirm}
|
||||
okText="Yes"
|
||||
cancelText="No"
|
||||
getPopupContainer={triggerNode => triggerNode}
|
||||
placement="topLeft"
|
||||
>
|
||||
{/* adding an <a> tag to force Popcofirm to register click on toggle */}
|
||||
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import React, { FC, useContext, useEffect, useState } from 'react';
|
||||
|
||||
import { Button, Col, Collapse, Row, Slider, Space } from 'antd';
|
||||
import Paragraph from 'antd/lib/typography/Paragraph';
|
||||
@@ -35,49 +35,39 @@ const chatColorVariables = [
|
||||
{ name: 'theme-color-users-7', description: '' },
|
||||
];
|
||||
|
||||
const paletteVariables = [
|
||||
{ name: 'theme-color-palette-0', description: '' },
|
||||
{ name: 'theme-color-palette-1', description: '' },
|
||||
{ name: 'theme-color-palette-2', description: '' },
|
||||
{ name: 'theme-color-palette-3', description: '' },
|
||||
{ name: 'theme-color-palette-4', description: '' },
|
||||
{ name: 'theme-color-palette-5', description: '' },
|
||||
{ name: 'theme-color-palette-6', description: '' },
|
||||
{ name: 'theme-color-palette-7', description: '' },
|
||||
{ name: 'theme-color-palette-8', description: '' },
|
||||
{ name: 'theme-color-palette-9', description: '' },
|
||||
{ name: 'theme-color-palette-10', description: '' },
|
||||
{ name: 'theme-color-palette-11', description: '' },
|
||||
{ name: 'theme-color-palette-12', description: '' },
|
||||
];
|
||||
|
||||
const componentColorVariables = [
|
||||
{ name: 'theme-color-background-main', description: 'Background' },
|
||||
{ name: 'theme-color-action', description: 'Action' },
|
||||
{ name: 'theme-color-action-hover', description: 'Action Hover' },
|
||||
{ name: 'theme-color-components-primary-button-border', description: 'Primary Button Border' },
|
||||
{ name: 'theme-color-components-primary-button-text', description: 'Primary Button Text' },
|
||||
{ name: 'theme-color-components-chat-background', description: 'Chat Background' },
|
||||
{ name: 'theme-color-components-chat-text', description: 'Text: Chat' },
|
||||
{ name: 'theme-color-components-text-on-dark', description: 'Text: Light' },
|
||||
{ name: 'theme-color-components-text-on-light', description: 'Text: Dark' },
|
||||
{ name: 'theme-color-background-header', description: 'Header/Footer' },
|
||||
{ name: 'theme-color-components-content-background', description: 'Page Content' },
|
||||
{ name: 'theme-color-components-scrollbar-background', description: 'Scrollbar Background' },
|
||||
{ name: 'theme-color-components-scrollbar-thumb', description: 'Scrollbar Thumb' },
|
||||
{
|
||||
name: 'theme-color-components-video-status-bar-background',
|
||||
description: 'Video Status Bar Background',
|
||||
},
|
||||
{
|
||||
name: 'theme-color-components-video-status-bar-foreground',
|
||||
description: 'Video Status Bar Foreground',
|
||||
},
|
||||
];
|
||||
|
||||
const others = [{ name: 'theme-rounded-corners', description: 'Corner radius' }];
|
||||
|
||||
// Create an object so these vars can be indexed by name.
|
||||
const allAvailableValues = [
|
||||
...paletteVariables,
|
||||
...componentColorVariables,
|
||||
...chatColorVariables,
|
||||
...others,
|
||||
].reduce((obj, val) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
obj[val.name] = { name: val.name, description: val.description };
|
||||
return obj;
|
||||
}, {});
|
||||
const allAvailableValues = [...componentColorVariables, ...chatColorVariables, ...others].reduce(
|
||||
(obj, val) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
obj[val.name] = { name: val.name, description: val.description };
|
||||
return obj;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
function ColorPicker({
|
||||
@@ -106,6 +96,7 @@ function ColorPicker({
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
export default function Appearance() {
|
||||
const serverStatusData = useContext(ServerStatusContext);
|
||||
@@ -113,7 +104,9 @@ export default function Appearance() {
|
||||
const { instanceDetails } = serverConfig;
|
||||
const { appearanceVariables } = instanceDetails;
|
||||
|
||||
const [colors, setColors] = useState<Record<string, AppearanceVariable>>();
|
||||
const [defaultValues, setDefaultValues] = useState<Record<string, AppearanceVariable>>();
|
||||
const [customValues, setCustomValues] = useState<Record<string, AppearanceVariable>>();
|
||||
|
||||
const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
|
||||
|
||||
let resetTimer = null;
|
||||
@@ -123,39 +116,37 @@ export default function Appearance() {
|
||||
clearTimeout(resetTimer);
|
||||
};
|
||||
|
||||
const setColorDefaults = () => {
|
||||
const setDefaults = () => {
|
||||
const c = {};
|
||||
[...paletteVariables, ...componentColorVariables, ...chatColorVariables, ...others].forEach(
|
||||
color => {
|
||||
const resolvedColor = getComputedStyle(document.documentElement).getPropertyValue(
|
||||
`--${color.name}`,
|
||||
);
|
||||
c[color.name] = { value: resolvedColor.trim(), description: color.description };
|
||||
},
|
||||
);
|
||||
setColors(c);
|
||||
[...componentColorVariables, ...chatColorVariables, ...others].forEach(color => {
|
||||
const resolvedColor = getComputedStyle(document.documentElement).getPropertyValue(
|
||||
`--${color.name}`,
|
||||
);
|
||||
c[color.name] = { value: resolvedColor.trim(), description: color.description };
|
||||
});
|
||||
setDefaultValues(c);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setColorDefaults();
|
||||
setDefaults();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (Object.keys(appearanceVariables).length === 0) return;
|
||||
|
||||
const c = colors || {};
|
||||
const c = {};
|
||||
Object.keys(appearanceVariables).forEach(key => {
|
||||
c[key] = {
|
||||
value: appearanceVariables[key],
|
||||
description: allAvailableValues[key]?.description || '',
|
||||
};
|
||||
});
|
||||
setColors(c);
|
||||
setCustomValues(c);
|
||||
}, [appearanceVariables]);
|
||||
|
||||
const updateColor = (variable: string, color: string, description: string) => {
|
||||
setColors({
|
||||
...colors,
|
||||
setCustomValues({
|
||||
...customValues,
|
||||
[variable]: { value: color, description },
|
||||
});
|
||||
};
|
||||
@@ -167,7 +158,7 @@ export default function Appearance() {
|
||||
onSuccess: () => {
|
||||
setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Updated.'));
|
||||
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
|
||||
setColorDefaults();
|
||||
setCustomValues(null);
|
||||
},
|
||||
onError: (message: string) => {
|
||||
setSubmitStatus(createInputStatus(STATUS_ERROR, message));
|
||||
@@ -178,8 +169,8 @@ export default function Appearance() {
|
||||
|
||||
const save = async () => {
|
||||
const c = {};
|
||||
Object.keys(colors).forEach(color => {
|
||||
c[color] = colors[color].value;
|
||||
Object.keys(customValues).forEach(color => {
|
||||
c[color] = customValues[color].value;
|
||||
});
|
||||
|
||||
await postConfigUpdateToAPI({
|
||||
@@ -202,7 +193,31 @@ export default function Appearance() {
|
||||
updateColor(variableName, `${value.toString()}px`, '');
|
||||
};
|
||||
|
||||
if (!colors) {
|
||||
type ColorCollectionProps = {
|
||||
variables: { name; description }[];
|
||||
};
|
||||
// eslint-disable-next-line react/no-unstable-nested-components
|
||||
const ColorCollection: FC<ColorCollectionProps> = ({ variables }) => {
|
||||
const cc = variables.map(colorVar => {
|
||||
const source = customValues?.[colorVar.name] ? customValues : defaultValues;
|
||||
const { name, description } = colorVar;
|
||||
const { value } = source[name];
|
||||
|
||||
return (
|
||||
<ColorPicker
|
||||
key={name}
|
||||
value={value}
|
||||
name={name}
|
||||
description={description}
|
||||
onChange={updateColor}
|
||||
/>
|
||||
);
|
||||
});
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <>{cc}</>;
|
||||
};
|
||||
|
||||
if (!defaultValues) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
@@ -217,56 +232,15 @@ export default function Appearance() {
|
||||
Certain sections of the interface can be customized by selecting new colors for them.
|
||||
</p>
|
||||
<Row gutter={[16, 16]}>
|
||||
{componentColorVariables.map(colorVar => {
|
||||
const { name } = colorVar;
|
||||
const c = colors[name];
|
||||
|
||||
return (
|
||||
<ColorPicker
|
||||
key={name}
|
||||
value={c.value}
|
||||
name={name}
|
||||
description={c.description}
|
||||
onChange={updateColor}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<ColorCollection variables={componentColorVariables} />
|
||||
</Row>
|
||||
</Panel>
|
||||
<Panel header={<Title level={3}>Chat User Colors</Title>} key="2">
|
||||
<Row gutter={[16, 16]}>
|
||||
{chatColorVariables.map(colorVar => {
|
||||
const { name } = colorVar;
|
||||
const c = colors[name];
|
||||
return (
|
||||
<ColorPicker
|
||||
key={name}
|
||||
value={c.value}
|
||||
name={name}
|
||||
description={c.description}
|
||||
onChange={updateColor}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
</Panel>
|
||||
<Panel header={<Title level={3}>Theme Colors</Title>} key="3">
|
||||
<Row gutter={[16, 16]}>
|
||||
{paletteVariables.map(colorVar => {
|
||||
const { name } = colorVar;
|
||||
const c = colors[name];
|
||||
return (
|
||||
<ColorPicker
|
||||
key={name}
|
||||
value={c.value}
|
||||
name={name}
|
||||
description={c.description}
|
||||
onChange={updateColor}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<ColorCollection variables={chatColorVariables} />
|
||||
</Row>
|
||||
</Panel>
|
||||
|
||||
<Panel header={<Title level={3}>Other Settings</Title>} key="4">
|
||||
How rounded should corners be?
|
||||
<Row gutter={[16, 16]}>
|
||||
@@ -278,7 +252,9 @@ export default function Appearance() {
|
||||
onChange={v => {
|
||||
onBorderRadiusChange(v);
|
||||
}}
|
||||
value={Number(colors['theme-rounded-corners']?.value?.replace('px', '') || 0)}
|
||||
value={Number(
|
||||
defaultValues['theme-rounded-corners']?.value?.replace('px', '') || 0,
|
||||
)}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
@@ -286,7 +262,7 @@ export default function Appearance() {
|
||||
style={{
|
||||
width: '100px',
|
||||
height: '30px',
|
||||
borderRadius: `${colors['theme-rounded-corners']?.value}`,
|
||||
borderRadius: `${defaultValues['theme-rounded-corners']?.value}`,
|
||||
backgroundColor: 'var(--theme-color-palette-7)',
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
import { Button, Typography } from 'antd';
|
||||
import React, { useState, useContext, useEffect } from 'react';
|
||||
import { ServerStatusContext } from '../../../utils/server-status-context';
|
||||
import { TextField, TEXTFIELD_TYPE_PASSWORD } from '../TextField';
|
||||
import { FormStatusIndicator } from '../FormStatusIndicator';
|
||||
import {
|
||||
postConfigUpdateToAPI,
|
||||
RESET_TIMEOUT,
|
||||
TWITTER_CONFIG_FIELDS,
|
||||
} from '../../../utils/config-constants';
|
||||
import { ToggleSwitch } from '../ToggleSwitch';
|
||||
import {
|
||||
createInputStatus,
|
||||
StatusState,
|
||||
STATUS_ERROR,
|
||||
STATUS_SUCCESS,
|
||||
} from '../../../utils/input-statuses';
|
||||
import { UpdateArgs } from '../../../types/config-section';
|
||||
import { TEXTFIELD_TYPE_TEXT } from '../TextFieldWithSubmit';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
export const ConfigNotify = () => {
|
||||
const serverStatusData = useContext(ServerStatusContext);
|
||||
const { serverConfig, setFieldInConfigState } = serverStatusData || {};
|
||||
const { notifications } = serverConfig || {};
|
||||
const { twitter } = notifications || {};
|
||||
|
||||
const [formDataValues, setFormDataValues] = useState<any>({});
|
||||
const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
|
||||
|
||||
const [enableSaveButton, setEnableSaveButton] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
const {
|
||||
enabled,
|
||||
apiKey,
|
||||
apiSecret,
|
||||
accessToken,
|
||||
accessTokenSecret,
|
||||
bearerToken,
|
||||
goLiveMessage,
|
||||
} = twitter || {};
|
||||
setFormDataValues({
|
||||
enabled,
|
||||
apiKey,
|
||||
apiSecret,
|
||||
accessToken,
|
||||
accessTokenSecret,
|
||||
bearerToken,
|
||||
goLiveMessage,
|
||||
});
|
||||
}, [twitter]);
|
||||
|
||||
const canSave = (): boolean => {
|
||||
const { apiKey, apiSecret, accessToken, accessTokenSecret, bearerToken, goLiveMessage } =
|
||||
formDataValues;
|
||||
|
||||
return (
|
||||
!!apiKey &&
|
||||
!!apiSecret &&
|
||||
!!accessToken &&
|
||||
!!accessTokenSecret &&
|
||||
!!bearerToken &&
|
||||
!!goLiveMessage
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setEnableSaveButton(canSave());
|
||||
}, [formDataValues]);
|
||||
|
||||
// update individual values in state
|
||||
const handleFieldChange = ({ fieldName, value }: UpdateArgs) => {
|
||||
setFormDataValues({
|
||||
...formDataValues,
|
||||
[fieldName]: value,
|
||||
});
|
||||
};
|
||||
|
||||
// toggle switch.
|
||||
const handleSwitchChange = (switchEnabled: boolean) => {
|
||||
const previouslySaved = formDataValues.enabled;
|
||||
|
||||
handleFieldChange({ fieldName: 'enabled', value: switchEnabled });
|
||||
|
||||
return switchEnabled !== previouslySaved;
|
||||
};
|
||||
|
||||
let resetTimer = null;
|
||||
const resetStates = () => {
|
||||
setSubmitStatus(null);
|
||||
resetTimer = null;
|
||||
clearTimeout(resetTimer);
|
||||
setEnableSaveButton(false);
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
const postValue = formDataValues;
|
||||
|
||||
await postConfigUpdateToAPI({
|
||||
apiPath: '/notifications/twitter',
|
||||
data: { value: postValue },
|
||||
onSuccess: () => {
|
||||
setFieldInConfigState({
|
||||
fieldName: 'twitter',
|
||||
value: postValue,
|
||||
path: 'notifications',
|
||||
});
|
||||
setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Updated.'));
|
||||
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
|
||||
},
|
||||
onError: (message: string) => {
|
||||
setSubmitStatus(createInputStatus(STATUS_ERROR, message));
|
||||
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title>Twitter</Title>
|
||||
<p className="description reduced-margins">
|
||||
Let your Twitter followers know each time you go live.
|
||||
</p>
|
||||
<div style={{ display: formDataValues.enabled ? 'block' : 'none' }}>
|
||||
<p className="description reduced-margins">
|
||||
<a href="https://owncast.online/docs/notifications" target="_blank" rel="noreferrer">
|
||||
Read how to configure your Twitter account
|
||||
</a>{' '}
|
||||
to support posting from Owncast.
|
||||
</p>
|
||||
<p className="description reduced-margins">
|
||||
<a
|
||||
href="https://developer.twitter.com/en/portal/dashboard"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
And then get your Twitter developer credentials
|
||||
</a>{' '}
|
||||
to fill in below.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ToggleSwitch
|
||||
apiPath=""
|
||||
fieldName="enabled"
|
||||
label="Enable Twitter"
|
||||
onChange={handleSwitchChange}
|
||||
checked={formDataValues.enabled}
|
||||
/>
|
||||
<div style={{ display: formDataValues.enabled ? 'block' : 'none' }}>
|
||||
<TextField
|
||||
{...TWITTER_CONFIG_FIELDS.apiKey}
|
||||
required
|
||||
value={formDataValues.apiKey}
|
||||
onChange={handleFieldChange}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: formDataValues.enabled ? 'block' : 'none' }}>
|
||||
<TextField
|
||||
{...TWITTER_CONFIG_FIELDS.apiSecret}
|
||||
type={TEXTFIELD_TYPE_PASSWORD}
|
||||
required
|
||||
value={formDataValues.apiSecret}
|
||||
onChange={handleFieldChange}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: formDataValues.enabled ? 'block' : 'none' }}>
|
||||
<TextField
|
||||
{...TWITTER_CONFIG_FIELDS.accessToken}
|
||||
required
|
||||
value={formDataValues.accessToken}
|
||||
onChange={handleFieldChange}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: formDataValues.enabled ? 'block' : 'none' }}>
|
||||
<TextField
|
||||
{...TWITTER_CONFIG_FIELDS.accessTokenSecret}
|
||||
type={TEXTFIELD_TYPE_PASSWORD}
|
||||
required
|
||||
value={formDataValues.accessTokenSecret}
|
||||
onChange={handleFieldChange}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: formDataValues.enabled ? 'block' : 'none' }}>
|
||||
<TextField
|
||||
{...TWITTER_CONFIG_FIELDS.bearerToken}
|
||||
required
|
||||
value={formDataValues.bearerToken}
|
||||
onChange={handleFieldChange}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: formDataValues.enabled ? 'block' : 'none' }}>
|
||||
<TextField
|
||||
{...TWITTER_CONFIG_FIELDS.goLiveMessage}
|
||||
type={TEXTFIELD_TYPE_TEXT}
|
||||
required
|
||||
value={formDataValues.goLiveMessage}
|
||||
onChange={handleFieldChange}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={save}
|
||||
style={{
|
||||
display: enableSaveButton ? 'inline-block' : 'none',
|
||||
position: 'relative',
|
||||
marginLeft: 'auto',
|
||||
right: '0',
|
||||
marginTop: '20px',
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<FormStatusIndicator status={submitStatus} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default ConfigNotify;
|
||||
@@ -45,13 +45,11 @@
|
||||
}
|
||||
|
||||
.virtuoso::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
height: auto;
|
||||
background-color: var(--theme-color-components-chat-background);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.virtuoso::-webkit-scrollbar-thumb {
|
||||
background: var(--theme-color-components-scrollbar-thumb);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.chatTextField {
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
|
||||
.chatModerationNotification {
|
||||
background-color: var(--theme-background-primary);
|
||||
color: var(--theme-color-components-chat-text);
|
||||
margin: 5px;
|
||||
border-radius: 15px;
|
||||
border-color: rgba(0, 0, 0, 0.3);
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
padding: 10px 10px;
|
||||
@include flexCenter;
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
rgb(83, 67, 130) 80%
|
||||
);
|
||||
margin: 5px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 5px;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
@@ -34,4 +35,13 @@
|
||||
background-color: var(--theme-color-palette-12);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--theme-color-palette-4);
|
||||
:hover {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
text-decoration-color: var(--theme-color-palette-15);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
|
||||
padding: 4px 0.1vw;
|
||||
padding: 0.6em;
|
||||
overflow-x: hidden;
|
||||
background-color: var(--theme-color-palette-3);
|
||||
background-color: var(--theme-color-components-chat-background);
|
||||
|
||||
.inputWrap {
|
||||
position: relative;
|
||||
@@ -23,7 +23,6 @@
|
||||
transition: box-shadow 90ms ease-in-out;
|
||||
&:focus-within {
|
||||
background-color: var(--theme-color-components-form-field-background);
|
||||
// outline: 1px solid var(--theme-color-components-form-field-border);
|
||||
box-shadow: inset 0px 0px 2px 2px var(--theme-color-palette-3);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ $p-size: 8px;
|
||||
overflow: hidden;
|
||||
overflow-wrap: anywhere;
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
|
||||
mark {
|
||||
padding-left: 0.35em;
|
||||
@@ -52,10 +53,16 @@ $p-size: 8px;
|
||||
display: none;
|
||||
top: 0;
|
||||
right: 10px;
|
||||
color: var(--theme-color-components-text-on-light);
|
||||
& button:focus,
|
||||
& button:active {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: var(--theme-rounded-corners);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .modMenuWrapper {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Tooltip } from 'antd';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { decodeHTML } from 'entities';
|
||||
import linkifyHtml from 'linkify-html';
|
||||
import styles from './ChatUserMessage.module.scss';
|
||||
import { formatTimestamp } from './messageFmt';
|
||||
import { ChatMessage } from '../../../interfaces/chat-message.model';
|
||||
@@ -107,6 +108,8 @@ export const ChatUserMessage: FC<ChatUserMessageProps> = ({
|
||||
})}
|
||||
style={{ borderColor: color }}
|
||||
>
|
||||
<div className={styles.background} style={{ color }} />
|
||||
|
||||
{!sameUserAsLast && (
|
||||
<UserTooltip user={user}>
|
||||
<div className={styles.user} style={{ color }}>
|
||||
@@ -119,11 +122,10 @@ export const ChatUserMessage: FC<ChatUserMessageProps> = ({
|
||||
<Highlight search={highlightString}>
|
||||
<div
|
||||
className={styles.message}
|
||||
dangerouslySetInnerHTML={{ __html: formattedMessage }}
|
||||
dangerouslySetInnerHTML={{ __html: linkifyHtml(formattedMessage) }}
|
||||
/>
|
||||
</Highlight>
|
||||
</Tooltip>
|
||||
|
||||
{showModeratorMenu && (
|
||||
<div className={styles.modMenuWrapper}>
|
||||
<ChatModerationActionMenu
|
||||
@@ -134,7 +136,6 @@ export const ChatUserMessage: FC<ChatUserMessageProps> = ({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.background} style={{ color }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -29,10 +29,10 @@ export const ContentHeader: FC<ContentHeaderProps> = ({
|
||||
<Logo src={logo} />
|
||||
</div>
|
||||
<div className={styles.titleSection}>
|
||||
<div className={cn(styles.title, styles.row, 'header-title')}>{name}</div>
|
||||
<div className={cn(styles.subtitle, styles.row, 'header-subtitle')}>
|
||||
<h2 className={cn(styles.title, styles.row, 'header-title')}>{name}</h2>
|
||||
<h3 className={cn(styles.subtitle, styles.row, 'header-subtitle')}>
|
||||
<Linkify>{title || summary}</Linkify>
|
||||
</div>
|
||||
</h3>
|
||||
<div className={cn(styles.tagList, styles.row)}>
|
||||
{tags.length > 0 && tags.map(tag => <span key={tag}>#{tag} </span>)}
|
||||
</div>
|
||||
|
||||
@@ -96,7 +96,12 @@ export const UserDropdown: FC<UserDropdownProps> = ({ username: defaultUsername
|
||||
Authenticate
|
||||
</Menu.Item>
|
||||
{appState.chatAvailable && (
|
||||
<Menu.Item key="3" icon={<MessageOutlined />} onClick={() => toggleChatVisibility()}>
|
||||
<Menu.Item
|
||||
key="3"
|
||||
icon={<MessageOutlined />}
|
||||
onClick={() => toggleChatVisibility()}
|
||||
aria-expanded={chatToggleVisible}
|
||||
>
|
||||
{chatToggleVisible ? 'Hide Chat' : 'Show Chat'}
|
||||
</Menu.Item>
|
||||
)}
|
||||
|
||||
@@ -5,11 +5,13 @@ import Head from 'next/head';
|
||||
import { FC, useEffect, useRef } from 'react';
|
||||
import { Layout } from 'antd';
|
||||
import dynamic from 'next/dynamic';
|
||||
import Script from 'next/script';
|
||||
import {
|
||||
ClientConfigStore,
|
||||
isChatAvailableSelector,
|
||||
clientConfigStateAtom,
|
||||
fatalErrorStateAtom,
|
||||
appStateAtom,
|
||||
} from '../../stores/ClientConfigStore';
|
||||
import { Content } from '../../ui/Content/Content';
|
||||
import { Header } from '../../ui/Header/Header';
|
||||
@@ -21,6 +23,7 @@ import { ServerRenderedHydration } from '../../ServerRendered/ServerRenderedHydr
|
||||
import { Theme } from '../../theme/Theme';
|
||||
import styles from './Main.module.scss';
|
||||
import { PushNotificationServiceWorker } from '../../workers/PushNotificationServiceWorker/PushNotificationServiceWorker';
|
||||
import { AppStateOptions } from '../../stores/application-state';
|
||||
|
||||
const lockBodyStyle = `
|
||||
body {
|
||||
@@ -45,9 +48,11 @@ export const Main: FC = () => {
|
||||
const { name, title, customStyles } = clientConfig;
|
||||
const isChatAvailable = useRecoilValue<boolean>(isChatAvailableSelector);
|
||||
const fatalError = useRecoilValue<DisplayableError>(fatalErrorStateAtom);
|
||||
const appState = useRecoilValue<AppStateOptions>(appStateAtom);
|
||||
|
||||
const layoutRef = useRef<HTMLDivElement>(null);
|
||||
const { chatDisabled } = clientConfig;
|
||||
const { videoAvailable } = appState;
|
||||
|
||||
useEffect(() => {
|
||||
setupNoLinkReferrer(layoutRef.current);
|
||||
@@ -133,8 +138,15 @@ export const Main: FC = () => {
|
||||
<PushNotificationServiceWorker />
|
||||
<TitleNotifier name={name} />
|
||||
<Theme />
|
||||
<Script strategy="afterInteractive" src="/customjavascript" />
|
||||
|
||||
<Layout ref={layoutRef} className={styles.layout}>
|
||||
<Header name={title || name} chatAvailable={isChatAvailable} chatDisabled={chatDisabled} />
|
||||
<Header
|
||||
name={title || name}
|
||||
chatAvailable={isChatAvailable}
|
||||
chatDisabled={chatDisabled}
|
||||
online={videoAvailable}
|
||||
/>
|
||||
<Content />
|
||||
{fatalError && (
|
||||
<FatalErrorStateModal title={fatalError.title} message={fatalError.message} />
|
||||
|
||||
@@ -77,6 +77,7 @@ export const NameChangeModal: FC = () => {
|
||||
value={newName}
|
||||
onChange={e => setNewName(e.target.value)}
|
||||
placeholder="Your chat display name"
|
||||
aria-label="Your chat display name"
|
||||
maxLength={30}
|
||||
showCount
|
||||
defaultValue={displayName}
|
||||
@@ -90,7 +91,7 @@ export const NameChangeModal: FC = () => {
|
||||
>
|
||||
{colorOptions.map(e => (
|
||||
<Option key={e.toString()} title={e}>
|
||||
<UserColor color={e} />
|
||||
<UserColor color={e} aria-label={e.toString()} />
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
@@ -35,6 +35,8 @@ const ACCESS_TOKEN_KEY = 'accessToken';
|
||||
let serverStatusRefreshPoll: ReturnType<typeof setInterval>;
|
||||
let hasBeenModeratorNotified = false;
|
||||
|
||||
const serverConnectivityError = `Cannot connect to the Owncast service. Please check your internet connection or if needed, double check this Owncast server is running.`;
|
||||
|
||||
// Server status is what gets updated such as viewer count, durations,
|
||||
// stream title, online/offline state, etc.
|
||||
export const serverStatusState = atom<ServerStatus>({
|
||||
@@ -200,10 +202,7 @@ export const ClientConfigStore: FC = () => {
|
||||
setGlobalFatalErrorMessage(null);
|
||||
setHasLoadedConfig(true);
|
||||
} catch (error) {
|
||||
setGlobalFatalError(
|
||||
'Unable to reach Owncast server',
|
||||
`Owncast cannot launch. Please make sure the Owncast server is running.`,
|
||||
);
|
||||
setGlobalFatalError('Unable to reach Owncast server', serverConnectivityError);
|
||||
console.error(`ClientConfigService -> getConfig() ERROR: \n${error}`);
|
||||
}
|
||||
};
|
||||
@@ -221,10 +220,7 @@ export const ClientConfigStore: FC = () => {
|
||||
setGlobalFatalErrorMessage(null);
|
||||
} catch (error) {
|
||||
sendEvent(AppStateEvent.Fail);
|
||||
setGlobalFatalError(
|
||||
'Unable to reach Owncast server',
|
||||
`Owncast cannot launch. Please make sure the Owncast server is running.`,
|
||||
);
|
||||
setGlobalFatalError('Unable to reach Owncast server', serverConnectivityError);
|
||||
console.error(`serverStatusState -> getStatus() ERROR: \n${error}`);
|
||||
}
|
||||
};
|
||||
@@ -325,7 +321,13 @@ export const ClientConfigStore: FC = () => {
|
||||
const startChat = async () => {
|
||||
try {
|
||||
const { socketHostOverride } = clientConfig;
|
||||
const host = socketHostOverride || window.location.toString();
|
||||
|
||||
// Get a copy of the browser location without #fragments.
|
||||
const l = window.location;
|
||||
l.hash = '';
|
||||
const location = l.toString().replaceAll('#', '');
|
||||
const host = socketHostOverride || location;
|
||||
|
||||
ws = new WebsocketService(accessToken, '/ws', host);
|
||||
ws.handleMessage = handleMessage;
|
||||
setWebsocketService(ws);
|
||||
|
||||
@@ -20,14 +20,11 @@
|
||||
}
|
||||
|
||||
.mainSection::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
height: auto;
|
||||
background-color: var(--theme-color-components-scrollbar-background);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mainSection::-webkit-scrollbar-thumb {
|
||||
background: var(--theme-color-components-scrollbar-thumb);
|
||||
border-radius: 1px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.topSection {
|
||||
|
||||
@@ -115,7 +115,7 @@ const DesktopContent = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.lowerHalf}>
|
||||
<div className={styles.lowerHalf} id="skip-to-content">
|
||||
<ContentHeader
|
||||
name={name}
|
||||
title={streamTitle}
|
||||
@@ -233,7 +233,7 @@ export const Content: FC = () => {
|
||||
const isChatVisible = useRecoilValue<boolean>(isChatVisibleSelector);
|
||||
const isChatAvailable = useRecoilValue<boolean>(isChatAvailableSelector);
|
||||
const currentUser = useRecoilValue(currentUserAtom);
|
||||
|
||||
const serverStatus = useRecoilValue<ServerStatus>(serverStatusState);
|
||||
const [isMobile, setIsMobile] = useRecoilState<boolean | undefined>(isMobileAtom);
|
||||
const messages = useRecoilValue<ChatMessage[]>(chatMessagesAtom);
|
||||
const online = useRecoilValue<boolean>(isOnlineSelector);
|
||||
@@ -259,6 +259,7 @@ export const Content: FC = () => {
|
||||
const { account: fediverseAccount, enabled: fediverseEnabled } = federation;
|
||||
const { browser: browserNotifications } = notifications;
|
||||
const { enabled: browserNotificationsEnabled } = browserNotifications;
|
||||
const { online: isStreamLive } = serverStatus;
|
||||
const [externalActionToDisplay, setExternalActionToDisplay] = useState<ExternalAction>(null);
|
||||
|
||||
const [supportsBrowserNotifications, setSupportsBrowserNotifications] = useState(false);
|
||||
@@ -334,9 +335,16 @@ export const Content: FC = () => {
|
||||
<div className={styles.mainSection}>
|
||||
<div className={styles.topSection}>
|
||||
{appState.appLoading && <Skeleton loading active paragraph={{ rows: 7 }} />}
|
||||
{online && <OwncastPlayer source="/hls/stream.m3u8" online={online} />}
|
||||
{online && (
|
||||
<OwncastPlayer
|
||||
source="/hls/stream.m3u8"
|
||||
online={online}
|
||||
title={streamTitle || name}
|
||||
/>
|
||||
)}
|
||||
{!online && !appState.appLoading && (
|
||||
<OfflineBanner
|
||||
showsHeader={false}
|
||||
streamName={name}
|
||||
customText={offlineMessage}
|
||||
notificationsEnabled={browserNotificationsEnabled}
|
||||
@@ -346,7 +354,7 @@ export const Content: FC = () => {
|
||||
onFollowClick={() => setShowFollowModal(true)}
|
||||
/>
|
||||
)}
|
||||
{online && (
|
||||
{isStreamLive && (
|
||||
<Statusbar
|
||||
online={online}
|
||||
lastConnectTime={lastConnectTime}
|
||||
|
||||
@@ -8,12 +8,10 @@
|
||||
justify-content: space-between;
|
||||
flex-direction: row;
|
||||
background-color: var(--theme-color-background-header);
|
||||
width: 100%;
|
||||
min-height: 30px;
|
||||
color: var(--theme-color-components-text-on-dark);
|
||||
font-family: var(--theme-text-body-font-family);
|
||||
|
||||
padding: 0 0.6rem;
|
||||
padding: 0.6rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
border-top: 1px solid rgba(214, 211, 211, 0.5);
|
||||
|
||||
@@ -6,7 +6,7 @@ export type FooterProps = {
|
||||
};
|
||||
|
||||
export const Footer: FC<FooterProps> = ({ version }) => (
|
||||
<footer className={styles.footer}>
|
||||
<footer className={styles.footer} id="footer">
|
||||
<span>
|
||||
Powered by <a href="https://owncast.online">{version}</a>
|
||||
</span>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
z-index: 20;
|
||||
padding: 1rem 0.7rem;
|
||||
padding: 0.7rem;
|
||||
box-shadow: 0px 1px 3px 1px rgb(0 0 0 / 10%);
|
||||
background-color: var(--theme-color-background-header);
|
||||
|
||||
@@ -42,3 +42,18 @@
|
||||
overflow: hidden;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.skipLink {
|
||||
position: absolute;
|
||||
left: -10000px;
|
||||
top: auto;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.skipLink:focus {
|
||||
position: static;
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Tag, Tooltip } from 'antd';
|
||||
import { FC } from 'react';
|
||||
import cn from 'classnames';
|
||||
import dynamic from 'next/dynamic';
|
||||
import Link from 'next/link';
|
||||
import { OwncastLogo } from '../../common/OwncastLogo/OwncastLogo';
|
||||
import styles from './Header.module.scss';
|
||||
|
||||
@@ -18,19 +19,32 @@ export type HeaderComponentProps = {
|
||||
name: string;
|
||||
chatAvailable: boolean;
|
||||
chatDisabled: boolean;
|
||||
online: boolean;
|
||||
};
|
||||
|
||||
export const Header: FC<HeaderComponentProps> = ({
|
||||
name = 'Your stream title',
|
||||
chatAvailable,
|
||||
chatDisabled,
|
||||
online,
|
||||
}) => (
|
||||
<header className={cn([`${styles.header}`], 'global-header')}>
|
||||
{online && (
|
||||
<Link href="#player" className={styles.skipLink}>
|
||||
Skip to player
|
||||
</Link>
|
||||
)}
|
||||
<Link href="#skip-to-content" className={styles.skipLink}>
|
||||
Skip to page content
|
||||
</Link>
|
||||
<Link href="#footer" className={styles.skipLink}>
|
||||
Skip to footer
|
||||
</Link>
|
||||
<div className={styles.logo}>
|
||||
<div id="header-logo" className={styles.logoImage}>
|
||||
<OwncastLogo variant="contrast" />
|
||||
</div>
|
||||
<h1 className={styles.title} id="global-header-text" title={name}>
|
||||
<h1 className={styles.title} id="global-header-text">
|
||||
{name}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -9,15 +9,15 @@
|
||||
flex-direction: column;
|
||||
color: var(--theme-color-components-text-on-light);
|
||||
background-color: var(--theme-color-background-main);
|
||||
margin: 1rem auto;
|
||||
margin: 3rem auto;
|
||||
border-radius: var(--theme-rounded-corners);
|
||||
padding: 1rem;
|
||||
padding: 2.5em;
|
||||
font-size: 1.2rem;
|
||||
border: 1px solid lightgray;
|
||||
}
|
||||
|
||||
.bodyText {
|
||||
line-height: 1.5rem;
|
||||
line-height: 2rem;
|
||||
}
|
||||
|
||||
.separator {
|
||||
|
||||
@@ -17,6 +17,7 @@ export type OfflineBannerProps = {
|
||||
lastLive?: Date;
|
||||
notificationsEnabled: boolean;
|
||||
fediverseAccount?: string;
|
||||
showsHeader?: boolean;
|
||||
onNotifyClick?: () => void;
|
||||
onFollowClick?: () => void;
|
||||
};
|
||||
@@ -27,6 +28,7 @@ export const OfflineBanner: FC<OfflineBannerProps> = ({
|
||||
lastLive,
|
||||
notificationsEnabled,
|
||||
fediverseAccount,
|
||||
showsHeader = true,
|
||||
onNotifyClick,
|
||||
onFollowClick,
|
||||
}) => {
|
||||
@@ -74,8 +76,12 @@ export const OfflineBanner: FC<OfflineBannerProps> = ({
|
||||
return (
|
||||
<div id="offline-banner" className={styles.outerContainer}>
|
||||
<div className={styles.innerContainer}>
|
||||
<div className={styles.header}>{streamName}</div>
|
||||
<Divider className={styles.separator} />
|
||||
{showsHeader && (
|
||||
<>
|
||||
<div className={styles.header}>{streamName}</div>
|
||||
<Divider className={styles.separator} />
|
||||
</>
|
||||
)}
|
||||
<div className={styles.bodyText}>{text}</div>
|
||||
{lastLive && (
|
||||
<div className={styles.lastLiveDate}>
|
||||
|
||||
@@ -7,7 +7,6 @@ export type SocialLinksProps = {
|
||||
links: SocialLink[];
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export const SocialLinks: FC<SocialLinksProps> = ({ links }) => (
|
||||
<div className={styles.links}>
|
||||
{links.map(link => (
|
||||
@@ -22,7 +21,6 @@ export const SocialLinks: FC<SocialLinksProps> = ({ links }) => (
|
||||
<Image
|
||||
src={link.icon || '/img/platformlogos/default.svg'}
|
||||
alt={link.platform}
|
||||
title={link.platform}
|
||||
className={styles.link}
|
||||
width="30"
|
||||
height="30"
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
height: 2rem;
|
||||
width: 100%;
|
||||
padding: var(--content-padding);
|
||||
color: var(--theme-color-components-text-on-light);
|
||||
background-color: var(--component-background);
|
||||
color: var(--theme-color-components-video-status-bar-foreground);
|
||||
background-color: var(--theme-color-components-video-status-bar-background);
|
||||
font-family: var(--theme-text-display-font-family);
|
||||
font-weight: 600;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ export const Statusbar: FC<StatusbarProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.statusbar}>
|
||||
<div className={styles.statusbar} role="status">
|
||||
<div>{onlineMessage}</div>
|
||||
<div>{rightSideMessage}</div>
|
||||
</div>
|
||||
|
||||
@@ -9,13 +9,33 @@
|
||||
height: 75px;
|
||||
width: 250px;
|
||||
font-size: 0.8rem;
|
||||
overflow: hidden;
|
||||
@include screen(mobile){
|
||||
|
||||
color: var(--theme-color-components-text-on-light);
|
||||
|
||||
.name {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
color: var(--theme-color-components-text-on-light);
|
||||
display: inline-block;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
width: calc(85%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.account {
|
||||
color: var(--theme-color-components-text-on-light);
|
||||
word-break: break-all;
|
||||
line-height: 0.9rem;
|
||||
}
|
||||
|
||||
@include screen(mobile) {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--theme-text-link);
|
||||
border-color: var(--theme-color-action);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
@@ -26,11 +46,6 @@
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.account {
|
||||
color: var(--theme-text-secondary);
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@@ -18,7 +18,7 @@ export const SingleFollower: FC<SingleFollowerProps> = ({ follower }) => (
|
||||
</Avatar>
|
||||
</Col>
|
||||
<Col>
|
||||
<Row>{follower.name}</Row>
|
||||
<Row className={styles.name}>{follower.name}</Row>
|
||||
<Row className={styles.account}>{follower.username}</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
@import '../../../styles/mixins.scss';
|
||||
|
||||
.container {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
justify-items: center;
|
||||
max-height: 75vh;
|
||||
aspect-ratio: 16 / 9;
|
||||
|
||||
.player,
|
||||
.poster {
|
||||
// position: static;
|
||||
// height: auto !important;
|
||||
width: 100%;
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
aspect-ratio: 16 / 9;
|
||||
max-height: 75vh;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,4 +34,5 @@ export const LiveDemo = Template.bind({});
|
||||
LiveDemo.args = {
|
||||
online: true,
|
||||
source: 'https://watch.owncast.online/hls/stream.m3u8',
|
||||
title: 'Stream title',
|
||||
};
|
||||
|
||||
@@ -10,7 +10,6 @@ import { isVideoPlayingAtom, clockSkewAtom } from '../../stores/ClientConfigStor
|
||||
import PlaybackMetrics from '../metrics/playback';
|
||||
import createVideoSettingsMenuButton from '../settings-menu';
|
||||
import LatencyCompensator from '../latencyCompensator';
|
||||
|
||||
import styles from './OwncastPlayer.module.scss';
|
||||
|
||||
const VIDEO_CONFIG_URL = '/api/video/variants';
|
||||
@@ -26,6 +25,7 @@ export type OwncastPlayerProps = {
|
||||
source: string;
|
||||
online: boolean;
|
||||
initiallyMuted?: boolean;
|
||||
title: string;
|
||||
};
|
||||
|
||||
async function getVideoSettings() {
|
||||
@@ -44,6 +44,7 @@ export const OwncastPlayer: FC<OwncastPlayerProps> = ({
|
||||
source,
|
||||
online,
|
||||
initiallyMuted = false,
|
||||
title,
|
||||
}) => {
|
||||
const playerRef = React.useRef(null);
|
||||
const [videoPlaying, setVideoPlaying] = useRecoilState<boolean>(isVideoPlayingAtom);
|
||||
@@ -85,13 +86,13 @@ export const OwncastPlayer: FC<OwncastPlayerProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const setLatencyCompensatorItemTitle = title => {
|
||||
const setLatencyCompensatorItemTitle = t => {
|
||||
const item = document.querySelector('.latency-toggle-item > .vjs-menu-item-text');
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
item.innerHTML = title;
|
||||
item.innerHTML = t;
|
||||
};
|
||||
|
||||
const startLatencyCompensator = () => {
|
||||
@@ -218,6 +219,7 @@ export const OwncastPlayer: FC<OwncastPlayerProps> = ({
|
||||
controls: true,
|
||||
responsive: true,
|
||||
fluid: false,
|
||||
fill: true,
|
||||
playsinline: true,
|
||||
liveui: true,
|
||||
preload: 'auto',
|
||||
@@ -306,10 +308,10 @@ export const OwncastPlayer: FC<OwncastPlayerProps> = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.container} id="player">
|
||||
{online && (
|
||||
<div className={styles.player}>
|
||||
<VideoJS options={videoJsOptions} onReady={handlePlayerReady} />
|
||||
<VideoJS options={videoJsOptions} onReady={handlePlayerReady} aria-label={title} />
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.poster}>
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
@import '../../../styles/mixins.scss';
|
||||
|
||||
.player {
|
||||
height: auto !important;
|
||||
width: 100%;
|
||||
video {
|
||||
position: static !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,9 +33,9 @@ export const VideoPoster: FC<VideoPosterProps> = ({ online, initialSrc, src: bas
|
||||
<CrossfadeImage
|
||||
src={src}
|
||||
duration={duration}
|
||||
objectFit="cover"
|
||||
objectFit="contain"
|
||||
height="auto"
|
||||
width="100%"
|
||||
height="100%"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -93,6 +93,7 @@ export function createVideoSettingsMenuButton(player, videojs, qualities, latenc
|
||||
}
|
||||
|
||||
const menuButton = new MenuButton();
|
||||
menuButton.el().setAttribute('aria-label', 'Settings');
|
||||
|
||||
// If none of the settings in this menu are applicable then don't show it.
|
||||
const tech = player.tech({ IWillNotUseThisInPlugins: true });
|
||||
|
||||
@@ -43,6 +43,10 @@ module.exports = withBundleAnalyzer(
|
||||
source: '/thumbnail.jpg',
|
||||
destination: 'http://localhost:8080/thumbnail.jpg', // Proxy to Backend to work around CORS.
|
||||
},
|
||||
{
|
||||
source: '/customjavascript',
|
||||
destination: 'http://localhost:8080/customjavascript', // Proxy to Backend to work around CORS.
|
||||
},
|
||||
];
|
||||
},
|
||||
pageExtensions: ['tsx'],
|
||||
|
||||
3860
web/package-lock.json
generated
@@ -14,23 +14,26 @@
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "4.8.0",
|
||||
"@codemirror/lang-css": "6.0.1",
|
||||
"@codemirror/lang-javascript": "^6.1.2",
|
||||
"@codemirror/lang-markdown": "6.0.5",
|
||||
"@codemirror/language-data": "6.1.0",
|
||||
"@fontsource/open-sans": "4.5.13",
|
||||
"@fontsource/open-sans": "4.5.14",
|
||||
"@fontsource/poppins": "4.5.10",
|
||||
"@uiw/codemirror-theme-bbedit": "4.19.6",
|
||||
"@uiw/react-codemirror": "4.19.6",
|
||||
"@xstate/react": "3.0.1",
|
||||
"@next/bundle-analyzer": "^13.1.1",
|
||||
"@uiw/codemirror-theme-bbedit": "4.19.7",
|
||||
"@uiw/react-codemirror": "4.19.7",
|
||||
"@xstate/react": "3.0.2",
|
||||
"antd": "4.24.3",
|
||||
"autoprefixer": "10.4.13",
|
||||
"chart.js": "4.1.2",
|
||||
"chartkick": "4.2.0",
|
||||
"chart.js": "4.2.0",
|
||||
"chartkick": "5.0.1",
|
||||
"classnames": "2.3.2",
|
||||
"date-fns": "2.29.3",
|
||||
"entities": "^4.4.0",
|
||||
"install": "^0.13.0",
|
||||
"linkify-html": "^4.1.0",
|
||||
"linkifyjs": "^4.1.0",
|
||||
"lodash": "4.17.21",
|
||||
"next": "13.1.2",
|
||||
"next": "13.1.5",
|
||||
"next-with-less": "2.0.5",
|
||||
"picmo": "5.7.2",
|
||||
"postcss-flexbugs-fixes": "5.0.2",
|
||||
@@ -42,21 +45,17 @@
|
||||
"react-highlighter-ts": "18.0.1",
|
||||
"react-hotkeys-hook": "4.3.2",
|
||||
"react-linkify": "1.0.0-alpha",
|
||||
"react-markdown": "8.0.4",
|
||||
"react-markdown": "8.0.5",
|
||||
"react-use": "^17.4.0",
|
||||
"react-virtuoso": "4.0.3",
|
||||
"react-virtuoso": "4.0.5",
|
||||
"recoil": "0.7.6",
|
||||
"sharp": "0.31.3",
|
||||
"slate": "0.88.1",
|
||||
"slate-react": "0.88.0",
|
||||
"storybook-addon-designs": "6.3.1",
|
||||
"storybook-addon-fetch-mock": "1.0.1",
|
||||
"style-dictionary": "3.7.2",
|
||||
"ua-parser-js": "1.0.32",
|
||||
"slate-react": "0.88.2",
|
||||
"ua-parser-js": "1.0.33",
|
||||
"video.js": "7.20.3",
|
||||
"xstate": "4.35.2",
|
||||
"yaml": "2.2.1",
|
||||
"@next/bundle-analyzer": "^13.1.1"
|
||||
"yaml": "2.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.20.12",
|
||||
@@ -81,35 +80,40 @@
|
||||
"@types/markdown-it": "12.2.3",
|
||||
"@types/node": "18.11.18",
|
||||
"@types/prop-types": "15.7.5",
|
||||
"@types/react": "18.0.26",
|
||||
"@types/react": "18.0.27",
|
||||
"@types/react-linkify": "1.0.1",
|
||||
"@types/ua-parser-js": "0.7.36",
|
||||
"@types/video.js": "^7.3.50",
|
||||
"@typescript-eslint/eslint-plugin": "5.48.1",
|
||||
"@typescript-eslint/parser": "5.48.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.49.0",
|
||||
"@typescript-eslint/parser": "5.49.0",
|
||||
"babel-loader": "9.1.2",
|
||||
"chromatic": "6.14.0",
|
||||
"chromatic": "6.15.0",
|
||||
"css-loader": "6.7.3",
|
||||
"cypress": "^12.0.0",
|
||||
"eslint": "8.31.0",
|
||||
"eslint": "8.32.0",
|
||||
"eslint-config-airbnb": "19.0.4",
|
||||
"eslint-config-next": "13.1.2",
|
||||
"eslint-config-next": "13.1.5",
|
||||
"eslint-config-prettier": "8.6.0",
|
||||
"eslint-plugin-import": "2.27.4",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"eslint-plugin-jsx-a11y": "6.7.1",
|
||||
"eslint-plugin-prettier": "4.2.1",
|
||||
"eslint-plugin-react": "7.32.0",
|
||||
"eslint-plugin-react": "7.32.1",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"eslint-plugin-storybook": "0.6.10",
|
||||
"handlebars": "^4.7.7",
|
||||
"html-webpack-plugin": "5.5.0",
|
||||
"install": "^0.13.0",
|
||||
"less": "4.1.3",
|
||||
"less-loader": "11.1.0",
|
||||
"npm": "^9.4.0",
|
||||
"prettier": "2.8.3",
|
||||
"sass": "1.57.1",
|
||||
"sass-loader": "13.2.0",
|
||||
"sb": "6.5.15",
|
||||
"storybook-dark-mode": "2.0.5",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import Link from 'next/link';
|
||||
|
||||
import Discord from '../../components/admin/notification/discord';
|
||||
import Browser from '../../components/admin/notification/browser';
|
||||
import Twitter from '../../components/admin/notification/twitter';
|
||||
import Federation from '../../components/admin/notification/federation';
|
||||
import {
|
||||
TextFieldWithSubmit,
|
||||
@@ -99,13 +98,6 @@ export default function ConfigNotify() {
|
||||
>
|
||||
<Browser />
|
||||
</Col>
|
||||
<Col
|
||||
span={10}
|
||||
className={`form-module ${enabled ? '' : 'disabled'}`}
|
||||
style={{ margin: '5px', display: 'flex', flexDirection: 'column' }}
|
||||
>
|
||||
<Twitter />
|
||||
</Col>
|
||||
|
||||
<Col
|
||||
span={10}
|
||||
|
||||
@@ -5,6 +5,7 @@ import GeneralConfig from '../../../../components/admin/config/general/GeneralCo
|
||||
import AppearanceConfig from '../../../../components/admin/config/general/AppearanceConfig';
|
||||
|
||||
import { AdminLayout } from '../../../../components/layouts/AdminLayout';
|
||||
import { EditCustomJavascript } from '../../../../components/admin/EditCustomJavascript';
|
||||
|
||||
export default function PublicFacingDetails() {
|
||||
return (
|
||||
@@ -23,6 +24,11 @@ export default function PublicFacingDetails() {
|
||||
key: '2',
|
||||
children: <AppearanceConfig />,
|
||||
},
|
||||
{
|
||||
label: `Custom Scripting`,
|
||||
key: '3',
|
||||
children: <EditCustomJavascript />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -6,16 +6,20 @@ import {
|
||||
currentUserAtom,
|
||||
visibleChatMessagesSelector,
|
||||
clientConfigStateAtom,
|
||||
appStateAtom,
|
||||
} from '../../../../components/stores/ClientConfigStore';
|
||||
import Header from '../../../../components/ui/Header/Header';
|
||||
import { ClientConfig } from '../../../../interfaces/client-config.model';
|
||||
import { AppStateOptions } from '../../../../components/stores/application-state';
|
||||
|
||||
export default function ReadWriteChatEmbed() {
|
||||
const currentUser = useRecoilValue(currentUserAtom);
|
||||
const messages = useRecoilValue<ChatMessage[]>(visibleChatMessagesSelector);
|
||||
const clientConfig = useRecoilValue<ClientConfig>(clientConfigStateAtom);
|
||||
const appState = useRecoilValue<AppStateOptions>(appStateAtom);
|
||||
|
||||
const { name, chatDisabled } = clientConfig;
|
||||
const { videoAvailable } = appState;
|
||||
|
||||
if (!currentUser) {
|
||||
return null;
|
||||
@@ -26,7 +30,7 @@ export default function ReadWriteChatEmbed() {
|
||||
return (
|
||||
<div>
|
||||
<ClientConfigStore />
|
||||
<Header name={name} chatAvailable chatDisabled={chatDisabled} />
|
||||
<Header name={name} chatAvailable chatDisabled={chatDisabled} online={videoAvailable} />
|
||||
<ChatContainer
|
||||
messages={messages}
|
||||
usernameToHighlight={displayName}
|
||||
|
||||
@@ -20,7 +20,7 @@ export default function VideoEmbed() {
|
||||
const { name } = clientConfig;
|
||||
|
||||
const { offlineMessage } = clientConfig;
|
||||
const { viewerCount, lastConnectTime, lastDisconnectTime } = status;
|
||||
const { viewerCount, lastConnectTime, lastDisconnectTime, streamTitle } = status;
|
||||
const online = useRecoilValue<boolean>(isOnlineSelector);
|
||||
|
||||
const router = useRouter();
|
||||
@@ -48,6 +48,7 @@ export default function VideoEmbed() {
|
||||
source="/hls/stream.m3u8"
|
||||
online={online}
|
||||
initiallyMuted={initiallyMuted}
|
||||
title={streamTitle || name}
|
||||
/>
|
||||
)}
|
||||
{!online && (
|
||||
|
||||
@@ -147,7 +147,7 @@ theme:
|
||||
value: 'var(--theme-color-palette-10)'
|
||||
comment: '{theme.color.palette.10.comment}'
|
||||
border:
|
||||
value: 'var(--theme-color-palette-4)'
|
||||
value: 'var(--theme-color-action)'
|
||||
comment: '{theme.color.palette.4.comment}'
|
||||
border-disabled:
|
||||
value: 'var(--theme-color-action-disabled)'
|
||||
@@ -174,25 +174,17 @@ theme:
|
||||
|
||||
chat:
|
||||
background:
|
||||
value: 'var(--theme-color-palette-1)'
|
||||
comment: '{theme.color.palette.0.comment}'
|
||||
text:
|
||||
value: 'var(--theme-color-palette-15)'
|
||||
comment: '{theme.color.palette.15.comment}'
|
||||
text:
|
||||
value: 'var(--theme-color-palette-2)'
|
||||
comment: '{theme.color.palette.2.comment}'
|
||||
|
||||
content:
|
||||
background:
|
||||
value: 'var(--theme-color-palette-15)'
|
||||
comment: '{theme.color.palette.15.comment}'
|
||||
|
||||
scrollbar:
|
||||
background:
|
||||
value: 'var(--theme-color-palette-15)'
|
||||
comment: '{theme.color.palette.15.comment}'
|
||||
thumb:
|
||||
value: 'var(--theme-color-palette-6)'
|
||||
comment: '{theme.color.palette.6.comment}'
|
||||
|
||||
modal:
|
||||
header:
|
||||
background:
|
||||
@@ -245,3 +237,10 @@ theme:
|
||||
live-indicator:
|
||||
value: 'var(--theme-color-palette-7)'
|
||||
comment: 'The Live dot indicator in the control bar of the video player'
|
||||
status-bar:
|
||||
background:
|
||||
value: 'var(--theme-color-palette-2)'
|
||||
comment: 'The background color of the video status bar'
|
||||
foreground:
|
||||
value: 'var(--theme-color-palette-4)'
|
||||
comment: 'The foreground color of the video status bar'
|
||||
|
||||
@@ -12,23 +12,24 @@ color:
|
||||
# If you add more colors here make sure to add them to
|
||||
# GenerateRandomDisplayColor in the Go codebase so it knows the max
|
||||
# number of colors to use.
|
||||
|
||||
user:
|
||||
0:
|
||||
value: 'rgb(244, 11, 11)'
|
||||
value: '#ff717b'
|
||||
1:
|
||||
value: 'rgb(244, 128, 11)'
|
||||
value: '#F4E413'
|
||||
2:
|
||||
value: 'rgb(162, 162, 1)'
|
||||
value: '#b99c45'
|
||||
3:
|
||||
value: 'rgb(88, 244, 11)'
|
||||
value: '#58f40b'
|
||||
4:
|
||||
value: 'rgb(11, 244, 244)'
|
||||
value: '#0bf4f4'
|
||||
5:
|
||||
value: 'rgb(11, 166, 244)'
|
||||
value: '#0ba6f4'
|
||||
6:
|
||||
value: 'rgb(102, 102, 255)'
|
||||
value: '#9a92ff'
|
||||
7:
|
||||
value: 'rgb(244, 11, 244)'
|
||||
value: '#ff53ff'
|
||||
|
||||
palette:
|
||||
0:
|
||||
@@ -62,7 +63,7 @@ color:
|
||||
value: '#39373d'
|
||||
comment: 'Neutral dark'
|
||||
10:
|
||||
value: '#707283'
|
||||
value: '#5d5f72'
|
||||
comment: 'Neutral gray light'
|
||||
11:
|
||||
value: '#2386e2'
|
||||
|
||||
@@ -25,7 +25,7 @@ BUTTONS
|
||||
.ant-btn-default {
|
||||
color: currentColor;
|
||||
border-width: 2px;
|
||||
border-color: transparent;
|
||||
border-color: var(--theme-color-components-primary-button-border);
|
||||
border-radius: var(--theme-rounded-corners);
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
&:hover,
|
||||
@@ -43,6 +43,7 @@ BUTTONS
|
||||
border-width: 2px;
|
||||
border-radius: var(--theme-rounded-corners);
|
||||
color: var(--theme-color-components-primary-button-text);
|
||||
border-color: var(--theme-color-components-primary-button-border);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
@@ -59,7 +60,6 @@ BUTTONS
|
||||
}
|
||||
background-color: var(--theme-color-components-primary-button-background);
|
||||
color: var(--theme-color-components-primary-button-text);
|
||||
border-color: var(--theme-color-components-primary-button-background);
|
||||
&:hover {
|
||||
background-color: var(--theme-color-action-hover);
|
||||
color: var(--theme-color-components-primary-button-text);
|
||||
@@ -158,7 +158,7 @@ DROPDOWN
|
||||
padding: var(--content-padding);
|
||||
background-color: transparent;
|
||||
border-radius: var(--theme-rounded-corners) var(--theme-rounded-corners) 0 0;
|
||||
font-weight: bold;
|
||||
font-weight: 600;
|
||||
& + .ant-tabs-tab {
|
||||
margin-left: var(--module-spacing);
|
||||
}
|
||||
@@ -213,3 +213,7 @@ th {
|
||||
font-family: var(--theme-text-display-font-family);
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
|
||||
.ant-popover {
|
||||
z-index: 800; // Lower the z-index so it renders under modals.
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
:root {
|
||||
--content-padding: 12px;
|
||||
--module-spacing: 12px; // margin size between lines of stuff, if needed
|
||||
--header-height: 5.375rem; // needed for making main content scrollable;
|
||||
--header-height: 4.3rem; // needed for making main content scrollable;
|
||||
--footer-height: 2.5rem; // needed for making main content scrollable;
|
||||
--content-height: calc(100vh - var(--header-height));
|
||||
--replacement-bar-height: 46px; // needed for making main content scrollable on mobile;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
// Do not edit directly
|
||||
// Generated on Wed, 21 Dec 2022 07:38:01 GMT
|
||||
// Generated on Sun, 29 Jan 2023 01:28:51 GMT
|
||||
//
|
||||
// How to edit these values:
|
||||
// Edit the corresponding token file under the style-definitions directory
|
||||
@@ -75,7 +75,7 @@
|
||||
@theme-color-components-primary-button-background-disabled: var(--theme-color-action-disabled); // Disabled background
|
||||
@theme-color-components-primary-button-text: var(--theme-color-palette-4); // Light secondary
|
||||
@theme-color-components-primary-button-text-disabled: var(--theme-color-palette-10); // Neutral gray light
|
||||
@theme-color-components-primary-button-border: var(--theme-color-palette-4); // Light secondary
|
||||
@theme-color-components-primary-button-border: var(--theme-color-action); // Light secondary
|
||||
@theme-color-components-primary-button-border-disabled: var(--theme-color-action-disabled); // Disabled background
|
||||
@theme-color-components-secondary-button-background: var(--theme-color-palette-4); // Light secondary
|
||||
@theme-color-components-secondary-button-background-disabled: transparent;
|
||||
@@ -83,11 +83,9 @@
|
||||
@theme-color-components-secondary-button-text-disabled: var(--theme-color-action-disabled); // Disabled background
|
||||
@theme-color-components-secondary-button-border: var(--theme-color-action); // Text link/secondary light text
|
||||
@theme-color-components-secondary-button-border-disabled: var(--theme-color-action-disabled); // Disabled background
|
||||
@theme-color-components-chat-background: var(--theme-color-palette-15); // Lighter background
|
||||
@theme-color-components-chat-text: var(--theme-color-palette-2); // Dark alternate
|
||||
@theme-color-components-chat-background: var(--theme-color-palette-1); // Dark primary
|
||||
@theme-color-components-chat-text: var(--theme-color-palette-15); // Lighter background
|
||||
@theme-color-components-content-background: var(--theme-color-palette-15); // Lighter background
|
||||
@theme-color-components-scrollbar-background: var(--theme-color-palette-15); // Lighter background
|
||||
@theme-color-components-scrollbar-thumb: var(--theme-color-palette-6); // Text link/secondary light text
|
||||
@theme-color-components-modal-header-background: var(--theme-color-palette-1); // Dark secondary
|
||||
@theme-color-components-modal-header-text: var(--theme-color-palette-3); // Light primary
|
||||
@theme-color-components-modal-content-background: var(--theme-color-palette-3); // Light primary
|
||||
@@ -103,17 +101,19 @@
|
||||
@theme-color-components-form-field-border: var(--theme-color-palette-0); // Dark primary
|
||||
@theme-color-components-video-background: var(--theme-color-palette-2); // Dark alternate
|
||||
@theme-color-components-video-live-indicator: var(--theme-color-palette-7); // The Live dot indicator in the control bar of the video player
|
||||
@theme-color-components-video-status-bar-background: var(--theme-color-palette-2); // The background color of the video status bar
|
||||
@theme-color-components-video-status-bar-foreground: var(--theme-color-palette-4); // The foreground color of the video status bar
|
||||
@owncast-purple-25: rgba(120, 113, 255, 0.25);
|
||||
@color-unknown: #7a5cf3;
|
||||
@color-unknown-2: #fffffe;
|
||||
@color-owncast-user-0: #f40b0b;
|
||||
@color-owncast-user-1: #f4800b;
|
||||
@color-owncast-user-2: #a2a201;
|
||||
@color-owncast-user-0: #ff717b;
|
||||
@color-owncast-user-1: #f4e413;
|
||||
@color-owncast-user-2: #b99c45;
|
||||
@color-owncast-user-3: #58f40b;
|
||||
@color-owncast-user-4: #0bf4f4;
|
||||
@color-owncast-user-5: #0ba6f4;
|
||||
@color-owncast-user-6: #6666ff;
|
||||
@color-owncast-user-7: #f40bf4;
|
||||
@color-owncast-user-6: #9a92ff;
|
||||
@color-owncast-user-7: #ff53ff;
|
||||
@color-owncast-palette-0: #12161d; // Dark primary
|
||||
@color-owncast-palette-1: #2d3748; // Dark secondary
|
||||
@color-owncast-palette-2: #000000; // Dark alternate
|
||||
@@ -124,7 +124,7 @@
|
||||
@color-owncast-palette-7: #5d38f3; // Text link hover
|
||||
@color-owncast-palette-8: #b6b3c6; // Disabled background
|
||||
@color-owncast-palette-9: #39373d; // Neutral dark
|
||||
@color-owncast-palette-10: #707283; // Neutral gray light
|
||||
@color-owncast-palette-10: #5d5f72; // Neutral gray light
|
||||
@color-owncast-palette-11: #2386e2; // Fun color 1
|
||||
@color-owncast-palette-12: #da9eff; // Fun color 2
|
||||
@color-owncast-palette-13: #42bea6; // Fun color 3
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Do not edit directly
|
||||
* Generated on Wed, 21 Dec 2022 07:38:01 GMT
|
||||
* Generated on Sun, 29 Jan 2023 01:28:51 GMT
|
||||
*
|
||||
* How to edit these values:
|
||||
* Edit the corresponding token file under the style-definitions directory
|
||||
@@ -35,12 +35,8 @@
|
||||
--theme-rounded-corners: 9px; /* How much corners are rounded in places in the UI. */
|
||||
--theme-unknown-1: green; /* This should never be used and it means something is wrong. */
|
||||
--theme-unknown-2: red; /* This should never be used and it means something is wrong. */
|
||||
--theme-text-body-font-family: var(
|
||||
--font-owncast-body
|
||||
); /* The font family used for the body text. */
|
||||
--theme-text-display-font-family: var(
|
||||
--font-owncast-display
|
||||
); /* The font family used for the display/header text. */
|
||||
--theme-text-body-font-family: var(--font-owncast-body); /* The font family used for the body text. */
|
||||
--theme-text-display-font-family: var(--font-owncast-display); /* The font family used for the display/header text. */
|
||||
--theme-color-users-0: var(--color-owncast-user-0);
|
||||
--theme-color-users-1: var(--color-owncast-user-1);
|
||||
--theme-color-users-2: var(--color-owncast-user-2);
|
||||
@@ -77,85 +73,49 @@
|
||||
--theme-color-warning: var(--theme-color-palette-warning); /* Warning */
|
||||
--theme-color-components-text-on-light: var(--theme-color-palette-0); /* Dark primary */
|
||||
--theme-color-components-text-on-dark: var(--theme-color-palette-3); /* Light primary */
|
||||
--theme-color-components-primary-button-background: var(
|
||||
--theme-color-action
|
||||
); /* Text link/secondary light text */
|
||||
--theme-color-components-primary-button-background-disabled: var(
|
||||
--theme-color-action-disabled
|
||||
); /* Disabled background */
|
||||
--theme-color-components-primary-button-background: var(--theme-color-action); /* Text link/secondary light text */
|
||||
--theme-color-components-primary-button-background-disabled: var(--theme-color-action-disabled); /* Disabled background */
|
||||
--theme-color-components-primary-button-text: var(--theme-color-palette-4); /* Light secondary */
|
||||
--theme-color-components-primary-button-text-disabled: var(
|
||||
--theme-color-palette-10
|
||||
); /* Neutral gray light */
|
||||
--theme-color-components-primary-button-border: var(
|
||||
--theme-color-palette-4
|
||||
); /* Light secondary */
|
||||
--theme-color-components-primary-button-border-disabled: var(
|
||||
--theme-color-action-disabled
|
||||
); /* Disabled background */
|
||||
--theme-color-components-secondary-button-background: var(
|
||||
--theme-color-palette-4
|
||||
); /* Light secondary */
|
||||
--theme-color-components-primary-button-text-disabled: var(--theme-color-palette-10); /* Neutral gray light */
|
||||
--theme-color-components-primary-button-border: var(--theme-color-action); /* Light secondary */
|
||||
--theme-color-components-primary-button-border-disabled: var(--theme-color-action-disabled); /* Disabled background */
|
||||
--theme-color-components-secondary-button-background: var(--theme-color-palette-4); /* Light secondary */
|
||||
--theme-color-components-secondary-button-background-disabled: transparent;
|
||||
--theme-color-components-secondary-button-text: var(
|
||||
--theme-color-action-disabled
|
||||
); /* Disabled background */
|
||||
--theme-color-components-secondary-button-text-disabled: var(
|
||||
--theme-color-action-disabled
|
||||
); /* Disabled background */
|
||||
--theme-color-components-secondary-button-border: var(
|
||||
--theme-color-action
|
||||
); /* Text link/secondary light text */
|
||||
--theme-color-components-secondary-button-border-disabled: var(
|
||||
--theme-color-action-disabled
|
||||
); /* Disabled background */
|
||||
--theme-color-components-chat-background: var(--theme-color-palette-15); /* Lighter background */
|
||||
--theme-color-components-chat-text: var(--theme-color-palette-2); /* Dark alternate */
|
||||
--theme-color-components-content-background: var(
|
||||
--theme-color-palette-15
|
||||
); /* Lighter background */
|
||||
--theme-color-components-scrollbar-background: var(
|
||||
--theme-color-palette-15
|
||||
); /* Lighter background */
|
||||
--theme-color-components-scrollbar-thumb: var(
|
||||
--theme-color-palette-6
|
||||
); /* Text link/secondary light text */
|
||||
--theme-color-components-modal-header-background: var(
|
||||
--theme-color-palette-1
|
||||
); /* Dark secondary */
|
||||
--theme-color-components-secondary-button-text: var(--theme-color-action-disabled); /* Disabled background */
|
||||
--theme-color-components-secondary-button-text-disabled: var(--theme-color-action-disabled); /* Disabled background */
|
||||
--theme-color-components-secondary-button-border: var(--theme-color-action); /* Text link/secondary light text */
|
||||
--theme-color-components-secondary-button-border-disabled: var(--theme-color-action-disabled); /* Disabled background */
|
||||
--theme-color-components-chat-background: var(--theme-color-palette-1); /* Dark primary */
|
||||
--theme-color-components-chat-text: var(--theme-color-palette-15); /* Lighter background */
|
||||
--theme-color-components-content-background: var(--theme-color-palette-15); /* Lighter background */
|
||||
--theme-color-components-modal-header-background: var(--theme-color-palette-1); /* Dark secondary */
|
||||
--theme-color-components-modal-header-text: var(--theme-color-palette-3); /* Light primary */
|
||||
--theme-color-components-modal-content-background: var(
|
||||
--theme-color-palette-3
|
||||
); /* Light primary */
|
||||
--theme-color-components-modal-content-background: var(--theme-color-palette-3); /* Light primary */
|
||||
--theme-color-components-modal-content-text: var(--theme-color-palette-0); /* Dark primary */
|
||||
--theme-color-components-menu-background: var(--theme-color-palette-3); /* Light primary */
|
||||
--theme-color-components-menu-item-text: var(--theme-color-palette-0); /* Dark primary */
|
||||
--theme-color-components-menu-item-bg: transparent;
|
||||
--theme-color-components-menu-item-hover-bg: rgba(0, 0, 0, 0.05);
|
||||
--theme-color-components-menu-item-focus-bg: rgba(0, 0, 0, 0.1);
|
||||
--theme-color-components-form-field-background: var(
|
||||
--theme-color-palette-4
|
||||
); /* Light secondary */
|
||||
--theme-color-components-form-field-placeholder: var(
|
||||
--theme-color-action-disabled
|
||||
); /* Disabled background */
|
||||
--theme-color-components-form-field-background: var(--theme-color-palette-4); /* Light secondary */
|
||||
--theme-color-components-form-field-placeholder: var(--theme-color-action-disabled); /* Disabled background */
|
||||
--theme-color-components-form-field-text: var(--theme-color-palette-0); /* Dark primary */
|
||||
--theme-color-components-form-field-border: var(--theme-color-palette-0); /* Dark primary */
|
||||
--theme-color-components-video-background: var(--theme-color-palette-2); /* Dark alternate */
|
||||
--theme-color-components-video-live-indicator: var(
|
||||
--theme-color-palette-7
|
||||
); /* The Live dot indicator in the control bar of the video player */
|
||||
--theme-color-components-video-live-indicator: var(--theme-color-palette-7); /* The Live dot indicator in the control bar of the video player */
|
||||
--theme-color-components-video-status-bar-background: var(--theme-color-palette-2); /* The background color of the video status bar */
|
||||
--theme-color-components-video-status-bar-foreground: var(--theme-color-palette-4); /* The foreground color of the video status bar */
|
||||
--owncast-purple-25: rgba(120, 113, 255, 0.25);
|
||||
--color-unknown: #7a5cf3;
|
||||
--color-unknown-2: #fffffe;
|
||||
--color-owncast-user-0: #f40b0b;
|
||||
--color-owncast-user-1: #f4800b;
|
||||
--color-owncast-user-2: #a2a201;
|
||||
--color-owncast-user-0: #ff717b;
|
||||
--color-owncast-user-1: #f4e413;
|
||||
--color-owncast-user-2: #b99c45;
|
||||
--color-owncast-user-3: #58f40b;
|
||||
--color-owncast-user-4: #0bf4f4;
|
||||
--color-owncast-user-5: #0ba6f4;
|
||||
--color-owncast-user-6: #6666ff;
|
||||
--color-owncast-user-7: #f40bf4;
|
||||
--color-owncast-user-6: #9a92ff;
|
||||
--color-owncast-user-7: #ff53ff;
|
||||
--color-owncast-palette-0: #12161d; /* Dark primary */
|
||||
--color-owncast-palette-1: #2d3748; /* Dark secondary */
|
||||
--color-owncast-palette-2: #000000; /* Dark alternate */
|
||||
@@ -166,7 +126,7 @@
|
||||
--color-owncast-palette-7: #5d38f3; /* Text link hover */
|
||||
--color-owncast-palette-8: #b6b3c6; /* Disabled background */
|
||||
--color-owncast-palette-9: #39373d; /* Neutral dark */
|
||||
--color-owncast-palette-10: #707283; /* Neutral gray light */
|
||||
--color-owncast-palette-10: #5d5f72; /* Neutral gray light */
|
||||
--color-owncast-palette-11: #2386e2; /* Fun color 1 */
|
||||
--color-owncast-palette-12: #da9eff; /* Fun color 2 */
|
||||
--color-owncast-palette-13: #42bea6; /* Fun color 3 */
|
||||
@@ -174,10 +134,6 @@
|
||||
--color-owncast-palette-15: #eff1f4; /* Lighter background */
|
||||
--color-owncast-palette-error: #ff4b39; /* Error */
|
||||
--color-owncast-palette-warning: #ffc655; /* Warning */
|
||||
--font-owncast-body: 'Open Sans', 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-owncast-display: 'Poppins', 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-owncast-body: 'Open Sans', 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-owncast-display: 'Poppins', 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';
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ export interface ConfigDirectoryFields {
|
||||
|
||||
export interface ConfigInstanceDetailsFields {
|
||||
customStyles: string;
|
||||
customJavascript: string;
|
||||
extraPageContent: string;
|
||||
logo: string;
|
||||
name: string;
|
||||
@@ -115,20 +116,9 @@ export interface DiscordNotification {
|
||||
goLiveMessage: string;
|
||||
}
|
||||
|
||||
export interface TwitterNotification {
|
||||
enabled: boolean;
|
||||
apiKey: string;
|
||||
apiSecret: string;
|
||||
accessToken: string;
|
||||
accessTokenSecret: string;
|
||||
bearerToken: string;
|
||||
goLiveMessage: string;
|
||||
}
|
||||
|
||||
export interface NotificationsConfig {
|
||||
browser: BrowserNotification;
|
||||
discord: DiscordNotification;
|
||||
twitter: TwitterNotification;
|
||||
}
|
||||
|
||||
export interface Health {
|
||||
|
||||
@@ -11,6 +11,7 @@ export const RESET_TIMEOUT = 3000;
|
||||
// CONFIG API ENDPOINTS
|
||||
export const API_CUSTOM_CONTENT = '/pagecontent';
|
||||
export const API_CUSTOM_CSS_STYLES = '/customstyles';
|
||||
export const API_CUSTOM_JAVASCRIPT = '/customjavascript';
|
||||
export const API_FFMPEG = '/ffmpegpath';
|
||||
export const API_INSTANCE_URL = '/serverurl';
|
||||
export const API_LOGO = '/logo';
|
||||
@@ -557,48 +558,3 @@ export const BROWSER_PUSH_CONFIG_FIELDS = {
|
||||
placeholder: `I've gone live! Come watch!`,
|
||||
},
|
||||
};
|
||||
|
||||
export const TWITTER_CONFIG_FIELDS = {
|
||||
apiKey: {
|
||||
fieldName: 'apiKey',
|
||||
label: 'API Key',
|
||||
maxLength: 200,
|
||||
tip: '',
|
||||
placeholder: `gaUQhRC2lqfrEFfElBXJgOctU`,
|
||||
},
|
||||
apiSecret: {
|
||||
fieldName: 'apiSecret',
|
||||
label: 'API Secret',
|
||||
maxLength: 200,
|
||||
tip: '',
|
||||
placeholder: `IIz4jFZMWbUKdFOEGUprFjRwIslG56d1SPQlolJYjXwJ2y2qKS`,
|
||||
},
|
||||
accessToken: {
|
||||
fieldName: 'accessToken',
|
||||
label: 'Access Token',
|
||||
maxLength: 200,
|
||||
tip: '',
|
||||
placeholder: `952540400-EEiwe9fkuSvWjnNC82YFa9kgpqbyAP3J7FjE2dkka`,
|
||||
},
|
||||
accessTokenSecret: {
|
||||
fieldName: 'accessTokenSecret',
|
||||
label: 'Access Token Secret',
|
||||
maxLength: 200,
|
||||
tip: '',
|
||||
placeholder: `xO0AZWNGfZxpNsYPg3zNEKhAsPPGvNZFlzQArA2khI9Kg`,
|
||||
},
|
||||
bearerToken: {
|
||||
fieldName: 'bearerToken',
|
||||
label: 'Bearer Token',
|
||||
maxLength: 200,
|
||||
tip: '',
|
||||
placeholder: `AAAAAAAAAAAAAAFqpXwEAAnnepHkjA8XD5ftx5jUadYIRtPtaq7AAAAwpXPpDWKDcdhiWr0tVDjsgW%2B4awGOM9VQ%3XPoMFuWcHsE42TK`,
|
||||
},
|
||||
goLiveMessage: {
|
||||
fieldName: 'goLiveMessage',
|
||||
label: 'Go Live Text',
|
||||
maxLength: 200,
|
||||
tip: 'The text to send when you go live.',
|
||||
placeholder: `I've gone live! Come watch!`,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ export const initialServerConfigState: ConfigDetails = {
|
||||
adminPassword: '',
|
||||
instanceDetails: {
|
||||
customStyles: '',
|
||||
customJavascript: '',
|
||||
extraPageContent: '',
|
||||
logo: '',
|
||||
name: '',
|
||||
@@ -60,15 +61,6 @@ export const initialServerConfigState: ConfigDetails = {
|
||||
notifications: {
|
||||
browser: { enabled: false, goLiveMessage: '' },
|
||||
discord: { enabled: false, webhook: '', goLiveMessage: '' },
|
||||
twitter: {
|
||||
enabled: false,
|
||||
goLiveMessage: '',
|
||||
apiKey: '',
|
||||
apiSecret: '',
|
||||
accessToken: '',
|
||||
accessTokenSecret: '',
|
||||
bearerToken: '',
|
||||
},
|
||||
},
|
||||
externalActions: [],
|
||||
supportedCodecs: [],
|
||||
|
||||