diff --git a/controllers/admin/appearance.go b/controllers/admin/appearance.go
new file mode 100644
index 000000000..4904ec325
--- /dev/null
+++ b/controllers/admin/appearance.go
@@ -0,0 +1,35 @@
+package admin
+
+import (
+ "encoding/json"
+ "net/http"
+
+ "github.com/owncast/owncast/controllers"
+ "github.com/owncast/owncast/core/data"
+)
+
+// SetCustomColorVariableValues sets the custom color variables.
+func SetCustomColorVariableValues(w http.ResponseWriter, r *http.Request) {
+ if !requirePOST(w, r) {
+ return
+ }
+
+ type request struct {
+ Value map[string]string `json:"value"`
+ }
+
+ decoder := json.NewDecoder(r.Body)
+ var values request
+
+ if err := decoder.Decode(&values); err != nil {
+ controllers.WriteSimpleResponse(w, false, "unable to update appearance variable values")
+ return
+ }
+
+ if err := data.SetCustomColorVariableValues(values.Value); err != nil {
+ controllers.WriteSimpleResponse(w, false, err.Error())
+ return
+ }
+
+ controllers.WriteSimpleResponse(w, true, "custom appearance variables updated")
+}
diff --git a/controllers/admin/serverConfig.go b/controllers/admin/serverConfig.go
index 7acc25fee..d0129fbfd 100644
--- a/controllers/admin/serverConfig.go
+++ b/controllers/admin/serverConfig.go
@@ -35,17 +35,18 @@ func GetServerConfig(w http.ResponseWriter, r *http.Request) {
}
response := serverConfigAdminResponse{
InstanceDetails: webConfigResponse{
- Name: data.GetServerName(),
- Summary: data.GetServerSummary(),
- Tags: data.GetServerMetadataTags(),
- ExtraPageContent: data.GetExtraPageBodyContent(),
- StreamTitle: data.GetStreamTitle(),
- WelcomeMessage: data.GetServerWelcomeMessage(),
- OfflineMessage: data.GetCustomOfflineMessage(),
- Logo: data.GetLogoPath(),
- SocialHandles: data.GetSocialHandles(),
- NSFW: data.GetNSFW(),
- CustomStyles: data.GetCustomStyles(),
+ Name: data.GetServerName(),
+ Summary: data.GetServerSummary(),
+ Tags: data.GetServerMetadataTags(),
+ ExtraPageContent: data.GetExtraPageBodyContent(),
+ StreamTitle: data.GetStreamTitle(),
+ WelcomeMessage: data.GetServerWelcomeMessage(),
+ OfflineMessage: data.GetCustomOfflineMessage(),
+ Logo: data.GetLogoPath(),
+ SocialHandles: data.GetSocialHandles(),
+ NSFW: data.GetNSFW(),
+ CustomStyles: data.GetCustomStyles(),
+ AppearanceVariables: data.GetCustomColorVariableValues(),
},
FFmpegPath: ffmpeg,
StreamKey: data.GetStreamKey(),
@@ -124,18 +125,19 @@ type videoSettings struct {
}
type webConfigResponse struct {
- Name string `json:"name"`
- Summary string `json:"summary"`
- WelcomeMessage string `json:"welcomeMessage"`
- OfflineMessage string `json:"offlineMessage"`
- Logo string `json:"logo"`
- Tags []string `json:"tags"`
- Version string `json:"version"`
- NSFW bool `json:"nsfw"`
- ExtraPageContent string `json:"extraPageContent"`
- StreamTitle string `json:"streamTitle"` // What's going on with the current stream
- SocialHandles []models.SocialHandle `json:"socialHandles"`
- CustomStyles string `json:"customStyles"`
+ Name string `json:"name"`
+ Summary string `json:"summary"`
+ WelcomeMessage string `json:"welcomeMessage"`
+ OfflineMessage string `json:"offlineMessage"`
+ Logo string `json:"logo"`
+ Tags []string `json:"tags"`
+ Version string `json:"version"`
+ NSFW bool `json:"nsfw"`
+ ExtraPageContent string `json:"extraPageContent"`
+ StreamTitle string `json:"streamTitle"` // What's going on with the current stream
+ SocialHandles []models.SocialHandle `json:"socialHandles"`
+ CustomStyles string `json:"customStyles"`
+ AppearanceVariables map[string]string `json:"appearanceVariables"`
}
type yp struct {
diff --git a/controllers/config.go b/controllers/config.go
index b2a9997f1..212582e02 100644
--- a/controllers/config.go
+++ b/controllers/config.go
@@ -30,6 +30,7 @@ type webConfigResponse struct {
ChatDisabled bool `json:"chatDisabled"`
ExternalActions []models.ExternalAction `json:"externalActions"`
CustomStyles string `json:"customStyles"`
+ AppearanceVariables map[string]string `json:"appearanceVariables"`
MaxSocketPayloadSize int `json:"maxSocketPayloadSize"`
Federation federationConfigResponse `json:"federation"`
Notifications notificationsConfigResponse `json:"notifications"`
@@ -133,6 +134,7 @@ func getConfigResponse() webConfigResponse {
Federation: federationResponse,
Notifications: notificationsResponse,
Authentication: authenticationResponse,
+ AppearanceVariables: data.GetCustomColorVariableValues(),
}
}
diff --git a/core/data/config.go b/core/data/config.go
index 09f1676b6..f3f4866e7 100644
--- a/core/data/config.go
+++ b/core/data/config.go
@@ -67,6 +67,7 @@ const (
hasConfiguredInitialNotificationsKey = "has_configured_initial_notifications"
hideViewerCountKey = "hide_viewer_count"
customOfflineMessageKey = "custom_offline_message"
+ customColorVariableValuesKey = "custom_color_variable_values"
)
// GetExtraPageBodyContent will return the user-supplied body content.
@@ -932,3 +933,14 @@ func GetCustomOfflineMessage() string {
func SetCustomOfflineMessage(message string) error {
return _datastore.SetString(customOfflineMessageKey, message)
}
+
+// SetCustomColorVariableValues sets CSS variable names and values.
+func SetCustomColorVariableValues(variables map[string]string) error {
+ return _datastore.SetStringMap(customColorVariableValuesKey, variables)
+}
+
+// GetCustomColorVariableValues gets CSS variable names and values.
+func GetCustomColorVariableValues() map[string]string {
+ values, _ := _datastore.GetStringMap(customColorVariableValuesKey)
+ return values
+}
diff --git a/core/data/configEntry.go b/core/data/configEntry.go
index c33f0310a..938924368 100644
--- a/core/data/configEntry.go
+++ b/core/data/configEntry.go
@@ -19,6 +19,13 @@ func (c *ConfigEntry) getStringSlice() ([]string, error) {
return result, err
}
+func (c *ConfigEntry) getStringMap() (map[string]string, error) {
+ decoder := c.getDecoder()
+ var result map[string]string
+ err := decoder.Decode(&result)
+ return result, err
+}
+
func (c *ConfigEntry) getString() (string, error) {
decoder := c.getDecoder()
var result string
diff --git a/core/data/data_test.go b/core/data/data_test.go
index 2a6dd14db..e1a0a86b7 100644
--- a/core/data/data_test.go
+++ b/core/data/data_test.go
@@ -110,6 +110,40 @@ func TestCustomType(t *testing.T) {
}
}
+func TestStringMap(t *testing.T) {
+ const testKey = "test string map key"
+
+ testMap := map[string]string{
+ "test string 1": "test string 2",
+ "test string 3": "test string 4",
+ }
+
+ // Save config entry to the database
+ if err := _datastore.Save(ConfigEntry{testKey, &testMap}); err != nil {
+ t.Error(err)
+ }
+
+ // Get the config entry from the database
+ entryResult, err := _datastore.Get(testKey)
+ if err != nil {
+ t.Error(err)
+ }
+
+ testResult, err := entryResult.getStringMap()
+ if err != nil {
+ t.Error(err)
+ }
+
+ fmt.Printf("%+v", testResult)
+
+ if testResult["test string 1"] != testMap["test string 1"] {
+ t.Error("expected", testMap["test string 1"], "but test returned", testResult["test string 1"])
+ }
+ if testResult["test string 3"] != testMap["test string 3"] {
+ t.Error("expected", testMap["test string 3"], "but test returned", testResult["test string 3"])
+ }
+}
+
// Custom type for testing
type TestStruct struct {
Test string
diff --git a/core/data/types.go b/core/data/types.go
index 0da6c26c6..5405e6c12 100644
--- a/core/data/types.go
+++ b/core/data/types.go
@@ -59,3 +59,18 @@ func (ds *Datastore) SetBool(key string, value bool) error {
configEntry := ConfigEntry{key, value}
return ds.Save(configEntry)
}
+
+// GetStringMap will return the string map value for a key.
+func (ds *Datastore) GetStringMap(key string) (map[string]string, error) {
+ configEntry, err := ds.Get(key)
+ if err != nil {
+ return map[string]string{}, err
+ }
+ return configEntry.getStringMap()
+}
+
+// SetStringMap will set the string map value for a key.
+func (ds *Datastore) SetStringMap(key string, value map[string]string) error {
+ configEntry := ConfigEntry{key, value}
+ return ds.Save(configEntry)
+}
diff --git a/router/router.go b/router/router.go
index 7da2af2b7..e1fadfc84 100644
--- a/router/router.go
+++ b/router/router.go
@@ -197,6 +197,9 @@ func Start() error {
// Set video codec
http.HandleFunc("/api/admin/config/video/codec", middleware.RequireAdminAuth(admin.SetVideoCodec))
+ // Set style/color/css values
+ http.HandleFunc("/api/admin/config/appearance", middleware.RequireAdminAuth(admin.SetCustomColorVariableValues))
+
// Return all webhooks
http.HandleFunc("/api/admin/webhooks", middleware.RequireAdminAuth(admin.GetWebhooks))
diff --git a/test/automated/api/configmanagement.test.js b/test/automated/api/configmanagement.test.js
index 7e605b13e..1ba17f07c 100644
--- a/test/automated/api/configmanagement.test.js
+++ b/test/automated/api/configmanagement.test.js
@@ -8,6 +8,11 @@ const offlineMessage = randomString();
const pageContent = `
${randomString()}
`;
const tags = [randomString(), randomString(), randomString()];
const latencyLevel = Math.floor(Math.random() * 4);
+const appearanceValues = {
+ variable1: randomString(),
+ variable2: randomString(),
+ variable3: randomString(),
+};
const streamOutputVariants = {
videoBitrate: randomNumber() * 100,
@@ -103,6 +108,11 @@ test('set offline message', async (done) => {
done();
});
+test('set custom style values', async (done) => {
+ const res = await sendConfigChangeRequest('appearance', appearanceValues);
+ done();
+});
+
test('verify updated config values', async (done) => {
const res = await request.get('/api/config');
expect(res.body.name).toBe(serverName);
diff --git a/web/.storybook/stories-category-doc-pages/Colors.stories.mdx b/web/.storybook/stories-category-doc-pages/Colors.stories.mdx
index f0ddd52b4..d76ac31ab 100644
--- a/web/.storybook/stories-category-doc-pages/Colors.stories.mdx
+++ b/web/.storybook/stories-category-doc-pages/Colors.stories.mdx
@@ -28,6 +28,7 @@ These color names are assigned to specific component variables. They can be over
'theme-color-palette-11',
'theme-color-palette-12',
'theme-color-palette-13',
+ 'theme-color-palette-15',
'theme-color-palette-error',
'theme-color-palette-warning',
'theme-color-background-main',
@@ -60,6 +61,7 @@ These color names are assigned to specific component variables. They can be over
'theme-color-components-modal-header-background',
'theme-color-components-modal-header-text',
'theme-color-components-modal-content-background',
+ 'theme-color-components-content-background',
'theme-color-components-modal-content-text',
'theme-color-components-menu-background',
'theme-color-components-menu-item-text',
@@ -93,6 +95,7 @@ They should not be overwritten, instead the theme variables should be overwritte
'color-owncast-palette-11',
'color-owncast-palette-12',
'color-owncast-palette-13',
+ 'color-owncast-palette-15',
]}
/>
diff --git a/web/components/MainLayout.tsx b/web/components/MainLayout.tsx
index 2d8c90f17..f3a5c9aaa 100644
--- a/web/components/MainLayout.tsx
+++ b/web/components/MainLayout.tsx
@@ -201,7 +201,9 @@ export const MainLayout: FC = ({ children }) => {
Notifications
-
+
+ Appearance
+
S3 Storage
diff --git a/web/components/chat/ChatActionMessage/ChatActionMessage.module.scss b/web/components/chat/ChatActionMessage/ChatActionMessage.module.scss
index f7971ebd0..90351a6b8 100644
--- a/web/components/chat/ChatActionMessage/ChatActionMessage.module.scss
+++ b/web/components/chat/ChatActionMessage/ChatActionMessage.module.scss
@@ -1,4 +1,5 @@
.chatAction {
padding: 5px;
text-align: center;
+ color: var(--theme-color-components-chat-text);
}
diff --git a/web/components/chat/ChatContainer/ChatContainer.module.scss b/web/components/chat/ChatContainer/ChatContainer.module.scss
index 01cce6488..f717d4dda 100644
--- a/web/components/chat/ChatContainer/ChatContainer.module.scss
+++ b/web/components/chat/ChatContainer/ChatContainer.module.scss
@@ -42,7 +42,7 @@
.chatContainer {
display: flex;
flex-direction: column;
- background-color: var(--theme-color-background-chat);
+ background-color: var(--theme-color-components-chat-background);
height: 100%;
}
.virtuoso {
diff --git a/web/components/chat/ChatJoinMessage/ChatJoinMessage.module.scss b/web/components/chat/ChatJoinMessage/ChatJoinMessage.module.scss
index 3943742a2..627681b05 100644
--- a/web/components/chat/ChatJoinMessage/ChatJoinMessage.module.scss
+++ b/web/components/chat/ChatJoinMessage/ChatJoinMessage.module.scss
@@ -1,6 +1,7 @@
.root {
padding: 10px 0px;
text-align: center;
- font-size: .8rem;
+ font-size: 0.8rem;
font-style: italic;
+ color: var(--theme-color-components-chat-text);
}
diff --git a/web/components/chat/ChatUserMessage/ChatUserMessage.module.scss b/web/components/chat/ChatUserMessage/ChatUserMessage.module.scss
index 7aca8c195..8e842e1eb 100644
--- a/web/components/chat/ChatUserMessage/ChatUserMessage.module.scss
+++ b/web/components/chat/ChatUserMessage/ChatUserMessage.module.scss
@@ -9,6 +9,8 @@ $p-size: 8px;
position: relative;
font-size: 0.9rem;
padding: 0px $p-size $p-size $p-size;
+ color: var(--theme-color-components-chat-text);
+
.user {
display: flex;
align-items: center;
@@ -63,7 +65,7 @@ $p-size: 8px;
.messagePadding {
padding: 0px 0px;
- padding-top: .4rem;
+ padding-top: 0.4rem;
}
.messagePaddingCollapsed {
diff --git a/web/components/layouts/Main.tsx b/web/components/layouts/Main.tsx
index 073c43aad..9161b1849 100644
--- a/web/components/layouts/Main.tsx
+++ b/web/components/layouts/Main.tsx
@@ -21,6 +21,7 @@ import { TitleNotifier } from '../TitleNotifier/TitleNotifier';
import { ServerRenderedHydration } from '../ServerRendered/ServerRenderedHydration';
import Footer from '../ui/Footer/Footer';
+import { Theme } from '../theme/Theme';
export const Main: FC = () => {
const [isMobile] = useRecoilState(isMobileAtom);
@@ -111,6 +112,7 @@ export const Main: FC = () => {
+
diff --git a/web/components/theme/Theme.tsx b/web/components/theme/Theme.tsx
new file mode 100644
index 000000000..ec9356737
--- /dev/null
+++ b/web/components/theme/Theme.tsx
@@ -0,0 +1,27 @@
+/* eslint-disable react/no-danger */
+import { FC } from 'react';
+import { useRecoilValue } from 'recoil';
+import { ClientConfig } from '../../interfaces/client-config.model';
+import { clientConfigStateAtom } from '../stores/ClientConfigStore';
+
+export const Theme: FC = () => {
+ const clientConfig = useRecoilValue(clientConfigStateAtom);
+ const { appearanceVariables, customStyles } = clientConfig;
+
+ const appearanceVars = Object.keys(appearanceVariables)
+ .filter(variable => !!appearanceVariables[variable])
+ .map(variable => `--${variable}: ${appearanceVariables[variable]}`);
+
+ return (
+
+ );
+};
diff --git a/web/components/ui/CustomPageContent/CustomPageContent.module.scss b/web/components/ui/CustomPageContent/CustomPageContent.module.scss
index 2a783600d..dc942e877 100644
--- a/web/components/ui/CustomPageContent/CustomPageContent.module.scss
+++ b/web/components/ui/CustomPageContent/CustomPageContent.module.scss
@@ -7,16 +7,16 @@
.customPageContent {
font-size: 1rem;
line-height: 1.6em;
- color: var(--theme-color-palette-0);
+ color: var(--theme-color-components-text-on-light);
padding: calc(2 * var(--content-padding));
border-radius: var(--theme-rounded-corners);
width: 100%;
- background-color: var(--theme-color-background-light);
+ background-color: var(--theme-color-components-content-background);
hr {
margin: 1.35em 0;
border: 0;
- border-top: solid 1px var(--theme-text-secondary);
+ border-top: solid 1px var(--theme-color-components-content-background);
}
div.summary {
diff --git a/web/components/ui/Footer/Footer.module.scss b/web/components/ui/Footer/Footer.module.scss
index c93cd3945..c02470141 100644
--- a/web/components/ui/Footer/Footer.module.scss
+++ b/web/components/ui/Footer/Footer.module.scss
@@ -9,16 +9,12 @@
width: 100%;
color: var(--theme-color-components-text-on-dark);
font-family: var(--theme-text-body-font-family);
-
- padding: 0 .6rem;
+
+ padding: 0 0.6rem;
font-size: 0.8rem;
font-weight: 600;
border-top: 1px solid rgba(214, 211, 211, 0.5);
- a {
- color: var(--theme-text-secondary);
- }
-
.links {
column-gap: 2rem;
width: auto;
diff --git a/web/components/ui/Sidebar/Sidebar.module.scss b/web/components/ui/Sidebar/Sidebar.module.scss
index eb7e1a8f1..1ddd13844 100644
--- a/web/components/ui/Sidebar/Sidebar.module.scss
+++ b/web/components/ui/Sidebar/Sidebar.module.scss
@@ -1,11 +1,11 @@
@import '../../../styles/mixins.scss';
.root {
- background-color: var(--theme-color-background-chat);
+ background-color: var(--theme-color-components-chat-background);
display: none;
@include screen(desktop) {
- position: sticky;
- display: block;
+ position: sticky;
+ display: block;
}
}
diff --git a/web/components/ui/followers/FollowerCollection/FollowerCollection.module.scss b/web/components/ui/followers/FollowerCollection/FollowerCollection.module.scss
index bc60baa46..573be0ebd 100644
--- a/web/components/ui/followers/FollowerCollection/FollowerCollection.module.scss
+++ b/web/components/ui/followers/FollowerCollection/FollowerCollection.module.scss
@@ -1,6 +1,6 @@
.followers {
width: 100%;
- background-color: var(--theme-color-background-light);
+ background-color: var(--theme-color-components-content-background);
padding: 5px;
}
@@ -8,5 +8,5 @@
padding: calc(2 * var(--content-padding));
border-radius: var(--theme-rounded-corners);
width: 100%;
- background-color: var(--theme-color-background-light);
+ background-color: var(--theme-color-components-content-background);
}
diff --git a/web/interfaces/client-config.model.ts b/web/interfaces/client-config.model.ts
index e1971257c..9466f1b0c 100644
--- a/web/interfaces/client-config.model.ts
+++ b/web/interfaces/client-config.model.ts
@@ -12,6 +12,7 @@ export interface ClientConfig {
chatDisabled: boolean;
externalActions: any[];
customStyles: string;
+ appearanceVariables: Map;
maxSocketPayloadSize: number;
federation: Federation;
notifications: Notifications;
@@ -58,6 +59,7 @@ export function makeEmptyClientConfig(): ClientConfig {
chatDisabled: false,
externalActions: [],
customStyles: '',
+ appearanceVariables: new Map(),
maxSocketPayloadSize: 0,
federation: {
enabled: false,
diff --git a/web/pages/admin/config-public-details.tsx b/web/pages/admin/config-public-details.tsx
index 492b58aba..24293d657 100644
--- a/web/pages/admin/config-public-details.tsx
+++ b/web/pages/admin/config-public-details.tsx
@@ -5,7 +5,6 @@ import { EditInstanceDetails } from '../../components/config/EditInstanceDetails
import { EditInstanceTags } from '../../components/config/EditInstanceTags';
import { EditSocialLinks } from '../../components/config/EditSocialLinks';
import { EditPageContent } from '../../components/config/EditPageContent';
-import { EditCustomStyles } from '../../components/config/EditCustomStyles';
const { Title } = Typography;
@@ -42,9 +41,6 @@ export default function PublicFacingDetails() {
-
-
-
);
}
diff --git a/web/pages/admin/config/appearance/appearance.module.scss b/web/pages/admin/config/appearance/appearance.module.scss
new file mode 100644
index 000000000..411bd1d80
--- /dev/null
+++ b/web/pages/admin/config/appearance/appearance.module.scss
@@ -0,0 +1,4 @@
+.colorPicker {
+ width: 100%;
+ height: 50px;
+}
diff --git a/web/pages/admin/config/appearance/index.tsx b/web/pages/admin/config/appearance/index.tsx
new file mode 100644
index 000000000..345ce6232
--- /dev/null
+++ b/web/pages/admin/config/appearance/index.tsx
@@ -0,0 +1,256 @@
+import React, { useContext, useEffect, useState } from 'react';
+
+import { Button, Col, Collapse, Row, Space } from 'antd';
+import Paragraph from 'antd/lib/typography/Paragraph';
+import Title from 'antd/lib/typography/Title';
+import { EditCustomStyles } from '../../../../components/config/EditCustomStyles';
+import s from './appearance.module.scss';
+import { postConfigUpdateToAPI, RESET_TIMEOUT } from '../../../../utils/config-constants';
+import {
+ createInputStatus,
+ StatusState,
+ STATUS_ERROR,
+ STATUS_SUCCESS,
+} from '../../../../utils/input-statuses';
+import { ServerStatusContext } from '../../../../utils/server-status-context';
+import { FormStatusIndicator } from '../../../../components/config/FormStatusIndicator';
+
+const { Panel } = Collapse;
+
+const ENDPOINT = '/appearance';
+
+interface AppearanceVariable {
+ value: string;
+ description: string;
+}
+
+function ColorPicker({
+ value,
+ name,
+ description,
+ onChange,
+}: {
+ value: string;
+ name: string;
+ description: string;
+ onChange: (name: string, value: string, description: string) => void;
+}) {
+ return (
+
+ onChange(name, e.target.value, description)}
+ />
+ {description}
+
+ );
+}
+export default function Appearance() {
+ const serverStatusData = useContext(ServerStatusContext);
+ const { serverConfig } = serverStatusData || {};
+ const { instanceDetails } = serverConfig;
+ const { appearanceVariables } = instanceDetails;
+
+ const chatColorVariables = [
+ { name: 'theme-color-users-0', description: '' },
+ { name: 'theme-color-users-1', description: '' },
+ { name: 'theme-color-users-2', description: '' },
+ { name: 'theme-color-users-3', description: '' },
+ { name: 'theme-color-users-4', description: '' },
+ { name: 'theme-color-users-5', description: '' },
+ { name: 'theme-color-users-6', description: '' },
+ { 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-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' },
+ ];
+
+ const [colors, setColors] = useState>();
+ const [submitStatus, setSubmitStatus] = useState(null);
+
+ let resetTimer = null;
+ const resetStates = () => {
+ setSubmitStatus(null);
+ resetTimer = null;
+ clearTimeout(resetTimer);
+ };
+
+ useEffect(() => {
+ const c = {};
+ [...paletteVariables, ...componentColorVariables, ...chatColorVariables].forEach(color => {
+ const resolvedColor = getComputedStyle(document.documentElement).getPropertyValue(
+ `--${color.name}`,
+ );
+ c[color.name] = { value: resolvedColor.trim(), description: color.description };
+ });
+ setColors(c);
+ }, []);
+
+ useEffect(() => {
+ if (!appearanceVariables || !colors) return;
+
+ const c = colors;
+ Object.keys(appearanceVariables).forEach(key => {
+ c[key] = { value: appearanceVariables[key], description: colors[key]?.description || '' };
+ });
+ setColors(c);
+ }, [appearanceVariables]);
+
+ const updateColor = (variable: string, color: string, description: string) => {
+ setColors({
+ ...colors,
+ [variable]: { value: color, description },
+ });
+ };
+
+ const reset = async () => {
+ setColors({});
+ await postConfigUpdateToAPI({
+ apiPath: ENDPOINT,
+ data: { value: {} },
+ onSuccess: () => {
+ setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Updated.'));
+ resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
+ },
+ onError: (message: string) => {
+ setSubmitStatus(createInputStatus(STATUS_ERROR, message));
+ resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
+ },
+ });
+ };
+
+ const save = async () => {
+ const c = {};
+ Object.keys(colors).forEach(color => {
+ c[color] = colors[color].value;
+ });
+
+ await postConfigUpdateToAPI({
+ apiPath: ENDPOINT,
+ data: { value: c },
+ onSuccess: () => {
+ setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Updated.'));
+ resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
+ },
+ onError: (message: string) => {
+ setSubmitStatus(createInputStatus(STATUS_ERROR, message));
+ resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
+ },
+ });
+ };
+
+ if (!colors) {
+ return Loading...
;
+ }
+
+ return (
+
+ Customize Appearance
+
+ The following colors are used across the user interface. You can change them.
+
+
+
+ Section Colors} key="1">
+
+ Certain specific sections of the interface changed by selecting new colors for them
+ here.
+
+
+ {componentColorVariables.map(colorVar => {
+ const { name } = colorVar;
+ const c = colors[name];
+ return (
+
+ );
+ })}
+
+
+ Chat User Colors} key="2">
+
+ {chatColorVariables.map(colorVar => {
+ const { name } = colorVar;
+ const c = colors[name];
+ return (
+
+ );
+ })}
+
+
+ Theme Colors} key="3">
+
+ {paletteVariables.map(colorVar => {
+ const { name } = colorVar;
+ const c = colors[name];
+ return (
+
+ );
+ })}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/web/stories/ReadwriteChat.stories.tsx b/web/stories/ReadwriteChat.stories.tsx
index 7e618f764..adc0b6d23 100644
--- a/web/stories/ReadwriteChat.stories.tsx
+++ b/web/stories/ReadwriteChat.stories.tsx
@@ -43,6 +43,7 @@ const Page = () => {
federation: undefined,
notifications: undefined,
authentication: undefined,
+ appearanceVariables: undefined,
};
useEffect(() => {
diff --git a/web/style-definitions/build.sh b/web/style-definitions/build.sh
index 1ea03fe91..f7c873ab5 100755
--- a/web/style-definitions/build.sh
+++ b/web/style-definitions/build.sh
@@ -1,4 +1,4 @@
#!/bin/sh
mv build/variables.css ../styles/variables.css
-mv build/variables.less ../styles/theme.less
\ No newline at end of file
+mv build/variables.less ../styles/theme.less
diff --git a/web/style-definitions/tokens/color/default-theme.yaml b/web/style-definitions/tokens/color/default-theme.yaml
index bc217c893..044c367a8 100644
--- a/web/style-definitions/tokens/color/default-theme.yaml
+++ b/web/style-definitions/tokens/color/default-theme.yaml
@@ -87,6 +87,9 @@ theme:
14:
value: 'var(--color-owncast-palette-14)'
comment: '{color.owncast.palette.14.comment}'
+ 15:
+ value: 'var(--color-owncast-palette-15)'
+ comment: '{color.owncast.palette.15.comment}'
error:
value: 'var(--color-owncast-palette-error)'
comment: '{color.owncast.palette.error.comment}'
@@ -99,14 +102,12 @@ theme:
value: 'var(--theme-color-palette-3)'
comment: '{theme.color.palette.3.comment}'
light:
- value: 'var(--theme-color-palette-14)'
- comment: '{theme.color.palette.14.comment}'
+ value: 'var(--theme-color-palette-3)'
+ comment: '{theme.color.palette.3.comment}'
header:
value: 'var(--theme-color-palette-0)'
comment: '{theme.color.palette.0.comment}'
- chat:
- value: 'var(--theme-color-palette-14)'
- comment: '{theme.color.palette.14.comment}'
+
action:
value: 'var(--theme-color-palette-6)'
comment: '{theme.color.palette.6.comment}'
@@ -173,11 +174,16 @@ theme:
chat:
background:
- value: 'var(--theme-color-palette-1)'
- comment: '{theme.color.palette.1.comment}'
+ value: 'var(--theme-color-palette-4)'
+ comment: '{theme.color.palette.4.comment}'
text:
- value: 'var(--theme-color-palette-3)'
- comment: '{theme.color.palette.3.comment}'
+ 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}'
modal:
header:
diff --git a/web/style-definitions/tokens/color/owncast-colors.yaml b/web/style-definitions/tokens/color/owncast-colors.yaml
index 5c6e8cce2..f70ade39a 100644
--- a/web/style-definitions/tokens/color/owncast-colors.yaml
+++ b/web/style-definitions/tokens/color/owncast-colors.yaml
@@ -76,6 +76,9 @@ color:
14:
value: '#f0f3f8'
comment: 'Light background'
+ 15:
+ value: '#eff1f4'
+ comment: 'Lighter background'
error:
value: '#ff4b39'
comment: 'Error'
diff --git a/web/styles/theme.less b/web/styles/theme.less
index fa01d0319..d5d3895f4 100644
--- a/web/styles/theme.less
+++ b/web/styles/theme.less
@@ -1,6 +1,6 @@
// Do not edit directly
-// Generated on Mon, 10 Oct 2022 23:54:14 GMT
+// Generated on Sun, 13 Nov 2022 04:09:55 GMT
//
// How to edit these values:
// Edit the corresponding token file under the style-definitions directory
@@ -58,12 +58,12 @@
@theme-color-palette-12: var(--color-owncast-palette-12); // Fun color 2
@theme-color-palette-13: var(--color-owncast-palette-13); // Fun color 3
@theme-color-palette-14: var(--color-owncast-palette-14); // Light background
+@theme-color-palette-15: var(--color-owncast-palette-15); // Lighter background
@theme-color-palette-error: var(--color-owncast-palette-error); // Error
@theme-color-palette-warning: var(--color-owncast-palette-warning); // Warning
@theme-color-background-main: var(--theme-color-palette-3); // Light primary
-@theme-color-background-light: var(--theme-color-palette-14); // Light background
+@theme-color-background-light: var(--theme-color-palette-3); // Light primary
@theme-color-background-header: var(--theme-color-palette-0); // Dark primary
-@theme-color-background-chat: var(--theme-color-palette-14); // Light background
@theme-color-action: var(--theme-color-palette-6); // Text link/secondary light text
@theme-color-action-hover: var(--theme-color-palette-7); // Text link hover
@theme-color-action-disabled: var(--theme-color-palette-8); // Disabled background
@@ -83,8 +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-1); // Dark secondary
-@theme-color-components-chat-text: var(--theme-color-palette-3); // Light primary
+@theme-color-components-chat-background: var(--theme-color-palette-4); // Light secondary
+@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-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
@@ -126,6 +127,7 @@
@color-owncast-palette-12: #da9eff; // Fun color 2
@color-owncast-palette-13: #42bea6; // Fun color 3
@color-owncast-palette-14: #f0f3f8; // Light background
+@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';
diff --git a/web/styles/variables.css b/web/styles/variables.css
index 1183dc33b..b1ba7f449 100644
--- a/web/styles/variables.css
+++ b/web/styles/variables.css
@@ -1,6 +1,6 @@
/**
* Do not edit directly
- * Generated on Mon, 10 Oct 2022 23:54:14 GMT
+ * Generated on Sun, 13 Nov 2022 04:09:55 GMT
*
* How to edit these values:
* Edit the corresponding token file under the style-definitions directory
@@ -64,12 +64,12 @@
--theme-color-palette-12: var(--color-owncast-palette-12); /* Fun color 2 */
--theme-color-palette-13: var(--color-owncast-palette-13); /* Fun color 3 */
--theme-color-palette-14: var(--color-owncast-palette-14); /* Light background */
+ --theme-color-palette-15: var(--color-owncast-palette-15); /* Lighter background */
--theme-color-palette-error: var(--color-owncast-palette-error); /* Error */
--theme-color-palette-warning: var(--color-owncast-palette-warning); /* Warning */
--theme-color-background-main: var(--theme-color-palette-3); /* Light primary */
- --theme-color-background-light: var(--theme-color-palette-14); /* Light background */
+ --theme-color-background-light: var(--theme-color-palette-3); /* Light primary */
--theme-color-background-header: var(--theme-color-palette-0); /* Dark primary */
- --theme-color-background-chat: var(--theme-color-palette-14); /* Light background */
--theme-color-action: var(--theme-color-palette-6); /* Text link/secondary light text */
--theme-color-action-hover: var(--theme-color-palette-7); /* Text link hover */
--theme-color-action-disabled: var(--theme-color-palette-8); /* Disabled background */
@@ -109,8 +109,11 @@
--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 secondary */
- --theme-color-components-chat-text: var(--theme-color-palette-3); /* Light primary */
+ --theme-color-components-chat-background: var(--theme-color-palette-4); /* Light secondary */
+ --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-modal-header-background: var(
--theme-color-palette-1
); /* Dark secondary */
@@ -162,6 +165,7 @@
--color-owncast-palette-12: #da9eff; /* Fun color 2 */
--color-owncast-palette-13: #42bea6; /* Fun color 3 */
--color-owncast-palette-14: #f0f3f8; /* Light background */
+ --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,
diff --git a/web/types/config-section.ts b/web/types/config-section.ts
index 2935374a7..5d95ac4d2 100644
--- a/web/types/config-section.ts
+++ b/web/types/config-section.ts
@@ -40,6 +40,7 @@ export interface ConfigInstanceDetailsFields {
tags: string[];
title: string;
welcomeMessage: string;
+ appearanceVariables: AppearanceVariables;
}
export type CpuUsageLevel = 1 | 2 | 3 | 4 | 5;
@@ -83,6 +84,10 @@ export interface S3Field {
forcePathStyle: boolean;
}
+type AppearanceVariables = {
+ [key: string]: string;
+};
+
export interface ExternalAction {
title: string;
description: string;
diff --git a/web/utils/server-status-context.tsx b/web/utils/server-status-context.tsx
index 529ed72f1..73f5474cc 100644
--- a/web/utils/server-status-context.tsx
+++ b/web/utils/server-status-context.tsx
@@ -22,6 +22,7 @@ export const initialServerConfigState: ConfigDetails = {
title: '',
welcomeMessage: '',
offlineMessage: '',
+ appearanceVariables: {},
},
ffmpegPath: '',
rtmpServerPort: '',