Support color customization from the admin (#2338)
* Add user-customizable theming. Closes #1915 * Prettified Code! * Add user-customizable theming. Closes #1915 * Add explicit color for page content background * Prettified Code! Co-authored-by: gabek <gabek@users.noreply.github.com>
This commit is contained in:
35
controllers/admin/appearance.go
Normal file
35
controllers/admin/appearance.go
Normal file
@@ -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")
|
||||||
|
}
|
||||||
@@ -35,17 +35,18 @@ func GetServerConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
response := serverConfigAdminResponse{
|
response := serverConfigAdminResponse{
|
||||||
InstanceDetails: webConfigResponse{
|
InstanceDetails: webConfigResponse{
|
||||||
Name: data.GetServerName(),
|
Name: data.GetServerName(),
|
||||||
Summary: data.GetServerSummary(),
|
Summary: data.GetServerSummary(),
|
||||||
Tags: data.GetServerMetadataTags(),
|
Tags: data.GetServerMetadataTags(),
|
||||||
ExtraPageContent: data.GetExtraPageBodyContent(),
|
ExtraPageContent: data.GetExtraPageBodyContent(),
|
||||||
StreamTitle: data.GetStreamTitle(),
|
StreamTitle: data.GetStreamTitle(),
|
||||||
WelcomeMessage: data.GetServerWelcomeMessage(),
|
WelcomeMessage: data.GetServerWelcomeMessage(),
|
||||||
OfflineMessage: data.GetCustomOfflineMessage(),
|
OfflineMessage: data.GetCustomOfflineMessage(),
|
||||||
Logo: data.GetLogoPath(),
|
Logo: data.GetLogoPath(),
|
||||||
SocialHandles: data.GetSocialHandles(),
|
SocialHandles: data.GetSocialHandles(),
|
||||||
NSFW: data.GetNSFW(),
|
NSFW: data.GetNSFW(),
|
||||||
CustomStyles: data.GetCustomStyles(),
|
CustomStyles: data.GetCustomStyles(),
|
||||||
|
AppearanceVariables: data.GetCustomColorVariableValues(),
|
||||||
},
|
},
|
||||||
FFmpegPath: ffmpeg,
|
FFmpegPath: ffmpeg,
|
||||||
StreamKey: data.GetStreamKey(),
|
StreamKey: data.GetStreamKey(),
|
||||||
@@ -124,18 +125,19 @@ type videoSettings struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type webConfigResponse struct {
|
type webConfigResponse struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Summary string `json:"summary"`
|
Summary string `json:"summary"`
|
||||||
WelcomeMessage string `json:"welcomeMessage"`
|
WelcomeMessage string `json:"welcomeMessage"`
|
||||||
OfflineMessage string `json:"offlineMessage"`
|
OfflineMessage string `json:"offlineMessage"`
|
||||||
Logo string `json:"logo"`
|
Logo string `json:"logo"`
|
||||||
Tags []string `json:"tags"`
|
Tags []string `json:"tags"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
NSFW bool `json:"nsfw"`
|
NSFW bool `json:"nsfw"`
|
||||||
ExtraPageContent string `json:"extraPageContent"`
|
ExtraPageContent string `json:"extraPageContent"`
|
||||||
StreamTitle string `json:"streamTitle"` // What's going on with the current stream
|
StreamTitle string `json:"streamTitle"` // What's going on with the current stream
|
||||||
SocialHandles []models.SocialHandle `json:"socialHandles"`
|
SocialHandles []models.SocialHandle `json:"socialHandles"`
|
||||||
CustomStyles string `json:"customStyles"`
|
CustomStyles string `json:"customStyles"`
|
||||||
|
AppearanceVariables map[string]string `json:"appearanceVariables"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type yp struct {
|
type yp struct {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ type webConfigResponse struct {
|
|||||||
ChatDisabled bool `json:"chatDisabled"`
|
ChatDisabled bool `json:"chatDisabled"`
|
||||||
ExternalActions []models.ExternalAction `json:"externalActions"`
|
ExternalActions []models.ExternalAction `json:"externalActions"`
|
||||||
CustomStyles string `json:"customStyles"`
|
CustomStyles string `json:"customStyles"`
|
||||||
|
AppearanceVariables map[string]string `json:"appearanceVariables"`
|
||||||
MaxSocketPayloadSize int `json:"maxSocketPayloadSize"`
|
MaxSocketPayloadSize int `json:"maxSocketPayloadSize"`
|
||||||
Federation federationConfigResponse `json:"federation"`
|
Federation federationConfigResponse `json:"federation"`
|
||||||
Notifications notificationsConfigResponse `json:"notifications"`
|
Notifications notificationsConfigResponse `json:"notifications"`
|
||||||
@@ -133,6 +134,7 @@ func getConfigResponse() webConfigResponse {
|
|||||||
Federation: federationResponse,
|
Federation: federationResponse,
|
||||||
Notifications: notificationsResponse,
|
Notifications: notificationsResponse,
|
||||||
Authentication: authenticationResponse,
|
Authentication: authenticationResponse,
|
||||||
|
AppearanceVariables: data.GetCustomColorVariableValues(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ const (
|
|||||||
hasConfiguredInitialNotificationsKey = "has_configured_initial_notifications"
|
hasConfiguredInitialNotificationsKey = "has_configured_initial_notifications"
|
||||||
hideViewerCountKey = "hide_viewer_count"
|
hideViewerCountKey = "hide_viewer_count"
|
||||||
customOfflineMessageKey = "custom_offline_message"
|
customOfflineMessageKey = "custom_offline_message"
|
||||||
|
customColorVariableValuesKey = "custom_color_variable_values"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetExtraPageBodyContent will return the user-supplied body content.
|
// GetExtraPageBodyContent will return the user-supplied body content.
|
||||||
@@ -932,3 +933,14 @@ func GetCustomOfflineMessage() string {
|
|||||||
func SetCustomOfflineMessage(message string) error {
|
func SetCustomOfflineMessage(message string) error {
|
||||||
return _datastore.SetString(customOfflineMessageKey, message)
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,13 @@ func (c *ConfigEntry) getStringSlice() ([]string, error) {
|
|||||||
return result, err
|
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) {
|
func (c *ConfigEntry) getString() (string, error) {
|
||||||
decoder := c.getDecoder()
|
decoder := c.getDecoder()
|
||||||
var result string
|
var result string
|
||||||
|
|||||||
@@ -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
|
// Custom type for testing
|
||||||
type TestStruct struct {
|
type TestStruct struct {
|
||||||
Test string
|
Test string
|
||||||
|
|||||||
@@ -59,3 +59,18 @@ func (ds *Datastore) SetBool(key string, value bool) error {
|
|||||||
configEntry := ConfigEntry{key, value}
|
configEntry := ConfigEntry{key, value}
|
||||||
return ds.Save(configEntry)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -197,6 +197,9 @@ func Start() error {
|
|||||||
// Set video codec
|
// Set video codec
|
||||||
http.HandleFunc("/api/admin/config/video/codec", middleware.RequireAdminAuth(admin.SetVideoCodec))
|
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
|
// Return all webhooks
|
||||||
http.HandleFunc("/api/admin/webhooks", middleware.RequireAdminAuth(admin.GetWebhooks))
|
http.HandleFunc("/api/admin/webhooks", middleware.RequireAdminAuth(admin.GetWebhooks))
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ const offlineMessage = randomString();
|
|||||||
const pageContent = `<p>${randomString()}</p>`;
|
const pageContent = `<p>${randomString()}</p>`;
|
||||||
const tags = [randomString(), randomString(), randomString()];
|
const tags = [randomString(), randomString(), randomString()];
|
||||||
const latencyLevel = Math.floor(Math.random() * 4);
|
const latencyLevel = Math.floor(Math.random() * 4);
|
||||||
|
const appearanceValues = {
|
||||||
|
variable1: randomString(),
|
||||||
|
variable2: randomString(),
|
||||||
|
variable3: randomString(),
|
||||||
|
};
|
||||||
|
|
||||||
const streamOutputVariants = {
|
const streamOutputVariants = {
|
||||||
videoBitrate: randomNumber() * 100,
|
videoBitrate: randomNumber() * 100,
|
||||||
@@ -103,6 +108,11 @@ test('set offline message', async (done) => {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('set custom style values', async (done) => {
|
||||||
|
const res = await sendConfigChangeRequest('appearance', appearanceValues);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
test('verify updated config values', async (done) => {
|
test('verify updated config values', async (done) => {
|
||||||
const res = await request.get('/api/config');
|
const res = await request.get('/api/config');
|
||||||
expect(res.body.name).toBe(serverName);
|
expect(res.body.name).toBe(serverName);
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ These color names are assigned to specific component variables. They can be over
|
|||||||
'theme-color-palette-11',
|
'theme-color-palette-11',
|
||||||
'theme-color-palette-12',
|
'theme-color-palette-12',
|
||||||
'theme-color-palette-13',
|
'theme-color-palette-13',
|
||||||
|
'theme-color-palette-15',
|
||||||
'theme-color-palette-error',
|
'theme-color-palette-error',
|
||||||
'theme-color-palette-warning',
|
'theme-color-palette-warning',
|
||||||
'theme-color-background-main',
|
'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-background',
|
||||||
'theme-color-components-modal-header-text',
|
'theme-color-components-modal-header-text',
|
||||||
'theme-color-components-modal-content-background',
|
'theme-color-components-modal-content-background',
|
||||||
|
'theme-color-components-content-background',
|
||||||
'theme-color-components-modal-content-text',
|
'theme-color-components-modal-content-text',
|
||||||
'theme-color-components-menu-background',
|
'theme-color-components-menu-background',
|
||||||
'theme-color-components-menu-item-text',
|
'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-11',
|
||||||
'color-owncast-palette-12',
|
'color-owncast-palette-12',
|
||||||
'color-owncast-palette-13',
|
'color-owncast-palette-13',
|
||||||
|
'color-owncast-palette-15',
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -201,7 +201,9 @@ export const MainLayout: FC<MainLayoutProps> = ({ children }) => {
|
|||||||
<Menu.Item key="config-notify">
|
<Menu.Item key="config-notify">
|
||||||
<Link href="/admin/config-notify">Notifications</Link>
|
<Link href="/admin/config-notify">Notifications</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
<Menu.Item key="config-appearance">
|
||||||
|
<Link href="/admin/config/appearance">Appearance</Link>
|
||||||
|
</Menu.Item>
|
||||||
<Menu.Item key="config-storage">
|
<Menu.Item key="config-storage">
|
||||||
<Link href="/admin/config-storage">S3 Storage</Link>
|
<Link href="/admin/config-storage">S3 Storage</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
.chatAction {
|
.chatAction {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
color: var(--theme-color-components-chat-text);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
.chatContainer {
|
.chatContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background-color: var(--theme-color-background-chat);
|
background-color: var(--theme-color-components-chat-background);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
.virtuoso {
|
.virtuoso {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
.root {
|
.root {
|
||||||
padding: 10px 0px;
|
padding: 10px 0px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: .8rem;
|
font-size: 0.8rem;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
|
color: var(--theme-color-components-chat-text);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ $p-size: 8px;
|
|||||||
position: relative;
|
position: relative;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
padding: 0px $p-size $p-size $p-size;
|
padding: 0px $p-size $p-size $p-size;
|
||||||
|
color: var(--theme-color-components-chat-text);
|
||||||
|
|
||||||
.user {
|
.user {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -63,7 +65,7 @@ $p-size: 8px;
|
|||||||
|
|
||||||
.messagePadding {
|
.messagePadding {
|
||||||
padding: 0px 0px;
|
padding: 0px 0px;
|
||||||
padding-top: .4rem;
|
padding-top: 0.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.messagePaddingCollapsed {
|
.messagePaddingCollapsed {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { TitleNotifier } from '../TitleNotifier/TitleNotifier';
|
|||||||
import { ServerRenderedHydration } from '../ServerRendered/ServerRenderedHydration';
|
import { ServerRenderedHydration } from '../ServerRendered/ServerRenderedHydration';
|
||||||
|
|
||||||
import Footer from '../ui/Footer/Footer';
|
import Footer from '../ui/Footer/Footer';
|
||||||
|
import { Theme } from '../theme/Theme';
|
||||||
|
|
||||||
export const Main: FC = () => {
|
export const Main: FC = () => {
|
||||||
const [isMobile] = useRecoilState<boolean | undefined>(isMobileAtom);
|
const [isMobile] = useRecoilState<boolean | undefined>(isMobileAtom);
|
||||||
@@ -111,6 +112,7 @@ export const Main: FC = () => {
|
|||||||
|
|
||||||
<ClientConfigStore />
|
<ClientConfigStore />
|
||||||
<TitleNotifier />
|
<TitleNotifier />
|
||||||
|
<Theme />
|
||||||
<Layout ref={layoutRef} style={{ minHeight: '100vh' }}>
|
<Layout ref={layoutRef} style={{ minHeight: '100vh' }}>
|
||||||
<Header name={title || name} chatAvailable={isChatAvailable} chatDisabled={chatDisabled} />
|
<Header name={title || name} chatAvailable={isChatAvailable} chatDisabled={chatDisabled} />
|
||||||
<Content />
|
<Content />
|
||||||
|
|||||||
27
web/components/theme/Theme.tsx
Normal file
27
web/components/theme/Theme.tsx
Normal file
@@ -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<ClientConfig>(clientConfigStateAtom);
|
||||||
|
const { appearanceVariables, customStyles } = clientConfig;
|
||||||
|
|
||||||
|
const appearanceVars = Object.keys(appearanceVariables)
|
||||||
|
.filter(variable => !!appearanceVariables[variable])
|
||||||
|
.map(variable => `--${variable}: ${appearanceVariables[variable]}`);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<style
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: `
|
||||||
|
:root {
|
||||||
|
${appearanceVars.join(';\n')}
|
||||||
|
}
|
||||||
|
${customStyles}
|
||||||
|
`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -7,16 +7,16 @@
|
|||||||
.customPageContent {
|
.customPageContent {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
line-height: 1.6em;
|
line-height: 1.6em;
|
||||||
color: var(--theme-color-palette-0);
|
color: var(--theme-color-components-text-on-light);
|
||||||
padding: calc(2 * var(--content-padding));
|
padding: calc(2 * var(--content-padding));
|
||||||
border-radius: var(--theme-rounded-corners);
|
border-radius: var(--theme-rounded-corners);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: var(--theme-color-background-light);
|
background-color: var(--theme-color-components-content-background);
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
margin: 1.35em 0;
|
margin: 1.35em 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-top: solid 1px var(--theme-text-secondary);
|
border-top: solid 1px var(--theme-color-components-content-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
div.summary {
|
div.summary {
|
||||||
|
|||||||
@@ -9,16 +9,12 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
color: var(--theme-color-components-text-on-dark);
|
color: var(--theme-color-components-text-on-dark);
|
||||||
font-family: var(--theme-text-body-font-family);
|
font-family: var(--theme-text-body-font-family);
|
||||||
|
|
||||||
padding: 0 .6rem;
|
padding: 0 0.6rem;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
border-top: 1px solid rgba(214, 211, 211, 0.5);
|
border-top: 1px solid rgba(214, 211, 211, 0.5);
|
||||||
|
|
||||||
a {
|
|
||||||
color: var(--theme-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.links {
|
.links {
|
||||||
column-gap: 2rem;
|
column-gap: 2rem;
|
||||||
width: auto;
|
width: auto;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
@import '../../../styles/mixins.scss';
|
@import '../../../styles/mixins.scss';
|
||||||
|
|
||||||
.root {
|
.root {
|
||||||
background-color: var(--theme-color-background-chat);
|
background-color: var(--theme-color-components-chat-background);
|
||||||
display: none;
|
display: none;
|
||||||
@include screen(desktop) {
|
@include screen(desktop) {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
.followers {
|
.followers {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: var(--theme-color-background-light);
|
background-color: var(--theme-color-components-content-background);
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8,5 +8,5 @@
|
|||||||
padding: calc(2 * var(--content-padding));
|
padding: calc(2 * var(--content-padding));
|
||||||
border-radius: var(--theme-rounded-corners);
|
border-radius: var(--theme-rounded-corners);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: var(--theme-color-background-light);
|
background-color: var(--theme-color-components-content-background);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export interface ClientConfig {
|
|||||||
chatDisabled: boolean;
|
chatDisabled: boolean;
|
||||||
externalActions: any[];
|
externalActions: any[];
|
||||||
customStyles: string;
|
customStyles: string;
|
||||||
|
appearanceVariables: Map<string, string>;
|
||||||
maxSocketPayloadSize: number;
|
maxSocketPayloadSize: number;
|
||||||
federation: Federation;
|
federation: Federation;
|
||||||
notifications: Notifications;
|
notifications: Notifications;
|
||||||
@@ -58,6 +59,7 @@ export function makeEmptyClientConfig(): ClientConfig {
|
|||||||
chatDisabled: false,
|
chatDisabled: false,
|
||||||
externalActions: [],
|
externalActions: [],
|
||||||
customStyles: '',
|
customStyles: '',
|
||||||
|
appearanceVariables: new Map(),
|
||||||
maxSocketPayloadSize: 0,
|
maxSocketPayloadSize: 0,
|
||||||
federation: {
|
federation: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { EditInstanceDetails } from '../../components/config/EditInstanceDetails
|
|||||||
import { EditInstanceTags } from '../../components/config/EditInstanceTags';
|
import { EditInstanceTags } from '../../components/config/EditInstanceTags';
|
||||||
import { EditSocialLinks } from '../../components/config/EditSocialLinks';
|
import { EditSocialLinks } from '../../components/config/EditSocialLinks';
|
||||||
import { EditPageContent } from '../../components/config/EditPageContent';
|
import { EditPageContent } from '../../components/config/EditPageContent';
|
||||||
import { EditCustomStyles } from '../../components/config/EditCustomStyles';
|
|
||||||
|
|
||||||
const { Title } = Typography;
|
const { Title } = Typography;
|
||||||
|
|
||||||
@@ -42,9 +41,6 @@ export default function PublicFacingDetails() {
|
|||||||
<div className="form-module page-content-module">
|
<div className="form-module page-content-module">
|
||||||
<EditPageContent />
|
<EditPageContent />
|
||||||
</div>
|
</div>
|
||||||
<div className="form-module page-content-module">
|
|
||||||
<EditCustomStyles />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
4
web/pages/admin/config/appearance/appearance.module.scss
Normal file
4
web/pages/admin/config/appearance/appearance.module.scss
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.colorPicker {
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
256
web/pages/admin/config/appearance/index.tsx
Normal file
256
web/pages/admin/config/appearance/index.tsx
Normal file
@@ -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 (
|
||||||
|
<Col span={3} key={name}>
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
id={name}
|
||||||
|
name={description}
|
||||||
|
title={description}
|
||||||
|
value={value}
|
||||||
|
className={s.colorPicker}
|
||||||
|
onChange={e => onChange(name, e.target.value, description)}
|
||||||
|
/>
|
||||||
|
<div style={{ padding: '2px' }}>{description}</div>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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<Record<string, AppearanceVariable>>();
|
||||||
|
const [submitStatus, setSubmitStatus] = useState<StatusState>(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 <div>Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Space direction="vertical">
|
||||||
|
<Title>Customize Appearance</Title>
|
||||||
|
<Paragraph>
|
||||||
|
The following colors are used across the user interface. You can change them.
|
||||||
|
</Paragraph>
|
||||||
|
<div>
|
||||||
|
<Collapse defaultActiveKey={['1']}>
|
||||||
|
<Panel header={<Title level={3}>Section Colors</Title>} key="1">
|
||||||
|
<p>
|
||||||
|
Certain specific sections of the interface changed by selecting new colors for them
|
||||||
|
here.
|
||||||
|
</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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Row>
|
||||||
|
</Panel>
|
||||||
|
</Collapse>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Space direction="horizontal">
|
||||||
|
<Button type="primary" onClick={save}>
|
||||||
|
Save Colors
|
||||||
|
</Button>
|
||||||
|
<Button type="ghost" onClick={reset}>
|
||||||
|
Reset to Defaults
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
<FormStatusIndicator status={submitStatus} />
|
||||||
|
<div className="form-module page-content-module">
|
||||||
|
<EditCustomStyles />
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -43,6 +43,7 @@ const Page = () => {
|
|||||||
federation: undefined,
|
federation: undefined,
|
||||||
notifications: undefined,
|
notifications: undefined,
|
||||||
authentication: undefined,
|
authentication: undefined,
|
||||||
|
appearanceVariables: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
mv build/variables.css ../styles/variables.css
|
mv build/variables.css ../styles/variables.css
|
||||||
mv build/variables.less ../styles/theme.less
|
mv build/variables.less ../styles/theme.less
|
||||||
|
|||||||
@@ -87,6 +87,9 @@ theme:
|
|||||||
14:
|
14:
|
||||||
value: 'var(--color-owncast-palette-14)'
|
value: 'var(--color-owncast-palette-14)'
|
||||||
comment: '{color.owncast.palette.14.comment}'
|
comment: '{color.owncast.palette.14.comment}'
|
||||||
|
15:
|
||||||
|
value: 'var(--color-owncast-palette-15)'
|
||||||
|
comment: '{color.owncast.palette.15.comment}'
|
||||||
error:
|
error:
|
||||||
value: 'var(--color-owncast-palette-error)'
|
value: 'var(--color-owncast-palette-error)'
|
||||||
comment: '{color.owncast.palette.error.comment}'
|
comment: '{color.owncast.palette.error.comment}'
|
||||||
@@ -99,14 +102,12 @@ theme:
|
|||||||
value: 'var(--theme-color-palette-3)'
|
value: 'var(--theme-color-palette-3)'
|
||||||
comment: '{theme.color.palette.3.comment}'
|
comment: '{theme.color.palette.3.comment}'
|
||||||
light:
|
light:
|
||||||
value: 'var(--theme-color-palette-14)'
|
value: 'var(--theme-color-palette-3)'
|
||||||
comment: '{theme.color.palette.14.comment}'
|
comment: '{theme.color.palette.3.comment}'
|
||||||
header:
|
header:
|
||||||
value: 'var(--theme-color-palette-0)'
|
value: 'var(--theme-color-palette-0)'
|
||||||
comment: '{theme.color.palette.0.comment}'
|
comment: '{theme.color.palette.0.comment}'
|
||||||
chat:
|
|
||||||
value: 'var(--theme-color-palette-14)'
|
|
||||||
comment: '{theme.color.palette.14.comment}'
|
|
||||||
action:
|
action:
|
||||||
value: 'var(--theme-color-palette-6)'
|
value: 'var(--theme-color-palette-6)'
|
||||||
comment: '{theme.color.palette.6.comment}'
|
comment: '{theme.color.palette.6.comment}'
|
||||||
@@ -173,11 +174,16 @@ theme:
|
|||||||
|
|
||||||
chat:
|
chat:
|
||||||
background:
|
background:
|
||||||
value: 'var(--theme-color-palette-1)'
|
value: 'var(--theme-color-palette-4)'
|
||||||
comment: '{theme.color.palette.1.comment}'
|
comment: '{theme.color.palette.4.comment}'
|
||||||
text:
|
text:
|
||||||
value: 'var(--theme-color-palette-3)'
|
value: 'var(--theme-color-palette-2)'
|
||||||
comment: '{theme.color.palette.3.comment}'
|
comment: '{theme.color.palette.2.comment}'
|
||||||
|
|
||||||
|
content:
|
||||||
|
background:
|
||||||
|
value: 'var(--theme-color-palette-15)'
|
||||||
|
comment: '{theme.color.palette.15.comment}'
|
||||||
|
|
||||||
modal:
|
modal:
|
||||||
header:
|
header:
|
||||||
|
|||||||
@@ -76,6 +76,9 @@ color:
|
|||||||
14:
|
14:
|
||||||
value: '#f0f3f8'
|
value: '#f0f3f8'
|
||||||
comment: 'Light background'
|
comment: 'Light background'
|
||||||
|
15:
|
||||||
|
value: '#eff1f4'
|
||||||
|
comment: 'Lighter background'
|
||||||
error:
|
error:
|
||||||
value: '#ff4b39'
|
value: '#ff4b39'
|
||||||
comment: 'Error'
|
comment: 'Error'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
// Do not edit directly
|
// 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:
|
// How to edit these values:
|
||||||
// Edit the corresponding token file under the style-definitions directory
|
// 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-12: var(--color-owncast-palette-12); // Fun color 2
|
||||||
@theme-color-palette-13: var(--color-owncast-palette-13); // Fun color 3
|
@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-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-error: var(--color-owncast-palette-error); // Error
|
||||||
@theme-color-palette-warning: var(--color-owncast-palette-warning); // Warning
|
@theme-color-palette-warning: var(--color-owncast-palette-warning); // Warning
|
||||||
@theme-color-background-main: var(--theme-color-palette-3); // Light primary
|
@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-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: 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-hover: var(--theme-color-palette-7); // Text link hover
|
||||||
@theme-color-action-disabled: var(--theme-color-palette-8); // Disabled background
|
@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-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: 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-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-background: var(--theme-color-palette-4); // Light secondary
|
||||||
@theme-color-components-chat-text: var(--theme-color-palette-3); // Light primary
|
@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-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-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
|
||||||
@@ -126,6 +127,7 @@
|
|||||||
@color-owncast-palette-12: #da9eff; // Fun color 2
|
@color-owncast-palette-12: #da9eff; // Fun color 2
|
||||||
@color-owncast-palette-13: #42bea6; // Fun color 3
|
@color-owncast-palette-13: #42bea6; // Fun color 3
|
||||||
@color-owncast-palette-14: #f0f3f8; // Light background
|
@color-owncast-palette-14: #f0f3f8; // Light background
|
||||||
|
@color-owncast-palette-15: #eff1f4; // Lighter background
|
||||||
@color-owncast-palette-error: #ff4b39; // Error
|
@color-owncast-palette-error: #ff4b39; // Error
|
||||||
@color-owncast-palette-warning: #ffc655; // Warning
|
@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-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';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Do not edit directly
|
* 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:
|
* How to edit these values:
|
||||||
* Edit the corresponding token file under the style-definitions directory
|
* 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-12: var(--color-owncast-palette-12); /* Fun color 2 */
|
||||||
--theme-color-palette-13: var(--color-owncast-palette-13); /* Fun color 3 */
|
--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-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-error: var(--color-owncast-palette-error); /* Error */
|
||||||
--theme-color-palette-warning: var(--color-owncast-palette-warning); /* Warning */
|
--theme-color-palette-warning: var(--color-owncast-palette-warning); /* Warning */
|
||||||
--theme-color-background-main: var(--theme-color-palette-3); /* Light primary */
|
--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-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: 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-hover: var(--theme-color-palette-7); /* Text link hover */
|
||||||
--theme-color-action-disabled: var(--theme-color-palette-8); /* Disabled background */
|
--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-components-secondary-button-border-disabled: var(
|
||||||
--theme-color-action-disabled
|
--theme-color-action-disabled
|
||||||
); /* Disabled background */
|
); /* Disabled background */
|
||||||
--theme-color-components-chat-background: var(--theme-color-palette-1); /* Dark secondary */
|
--theme-color-components-chat-background: var(--theme-color-palette-4); /* Light secondary */
|
||||||
--theme-color-components-chat-text: var(--theme-color-palette-3); /* Light primary */
|
--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-components-modal-header-background: var(
|
||||||
--theme-color-palette-1
|
--theme-color-palette-1
|
||||||
); /* Dark secondary */
|
); /* Dark secondary */
|
||||||
@@ -162,6 +165,7 @@
|
|||||||
--color-owncast-palette-12: #da9eff; /* Fun color 2 */
|
--color-owncast-palette-12: #da9eff; /* Fun color 2 */
|
||||||
--color-owncast-palette-13: #42bea6; /* Fun color 3 */
|
--color-owncast-palette-13: #42bea6; /* Fun color 3 */
|
||||||
--color-owncast-palette-14: #f0f3f8; /* Light background */
|
--color-owncast-palette-14: #f0f3f8; /* Light background */
|
||||||
|
--color-owncast-palette-15: #eff1f4; /* Lighter background */
|
||||||
--color-owncast-palette-error: #ff4b39; /* Error */
|
--color-owncast-palette-error: #ff4b39; /* Error */
|
||||||
--color-owncast-palette-warning: #ffc655; /* Warning */
|
--color-owncast-palette-warning: #ffc655; /* Warning */
|
||||||
--font-owncast-body: 'Open Sans', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
--font-owncast-body: 'Open Sans', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export interface ConfigInstanceDetailsFields {
|
|||||||
tags: string[];
|
tags: string[];
|
||||||
title: string;
|
title: string;
|
||||||
welcomeMessage: string;
|
welcomeMessage: string;
|
||||||
|
appearanceVariables: AppearanceVariables;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CpuUsageLevel = 1 | 2 | 3 | 4 | 5;
|
export type CpuUsageLevel = 1 | 2 | 3 | 4 | 5;
|
||||||
@@ -83,6 +84,10 @@ export interface S3Field {
|
|||||||
forcePathStyle: boolean;
|
forcePathStyle: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AppearanceVariables = {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
|
||||||
export interface ExternalAction {
|
export interface ExternalAction {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export const initialServerConfigState: ConfigDetails = {
|
|||||||
title: '',
|
title: '',
|
||||||
welcomeMessage: '',
|
welcomeMessage: '',
|
||||||
offlineMessage: '',
|
offlineMessage: '',
|
||||||
|
appearanceVariables: {},
|
||||||
},
|
},
|
||||||
ffmpegPath: '',
|
ffmpegPath: '',
|
||||||
rtmpServerPort: '',
|
rtmpServerPort: '',
|
||||||
|
|||||||
Reference in New Issue
Block a user