mirror of
				https://github.com/owncast/owncast.git
				synced 2025-11-04 05:17:27 +08:00 
			
		
		
		
	Add action buttons and status bar
This commit is contained in:
		@ -2,7 +2,7 @@ import { Menu, Dropdown } from 'antd';
 | 
				
			|||||||
import { DownOutlined } from '@ant-design/icons';
 | 
					import { DownOutlined } from '@ant-design/icons';
 | 
				
			||||||
import { useRecoilState } from 'recoil';
 | 
					import { useRecoilState } from 'recoil';
 | 
				
			||||||
import { ChatVisibilityState, ChatState } from '../interfaces/application-state';
 | 
					import { ChatVisibilityState, ChatState } from '../interfaces/application-state';
 | 
				
			||||||
import { chatVisibilityAtom as chatVisibilityAtom } from './stores/ClientConfigStore';
 | 
					import { chatVisibilityAtom } from './stores/ClientConfigStore';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface Props {
 | 
					interface Props {
 | 
				
			||||||
  username: string;
 | 
					  username: string;
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										7
									
								
								web/components/action-buttons/ActionButton.module.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								web/components/action-buttons/ActionButton.module.scss
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					.button {
 | 
				
			||||||
 | 
					  margin: 3px;
 | 
				
			||||||
 | 
					  .icon {
 | 
				
			||||||
 | 
					    width: 20px;
 | 
				
			||||||
 | 
					    margin-right: 3px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,17 +1,18 @@
 | 
				
			|||||||
import { Button } from 'antd';
 | 
					import { Button } from 'antd';
 | 
				
			||||||
import { ExternalAction } from '../interfaces/external-action.interface';
 | 
					import { ExternalAction } from '../interfaces/external-action.interface';
 | 
				
			||||||
 | 
					import s from './ActionButton.module.scss';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface Props {
 | 
					interface Props {
 | 
				
			||||||
  action: ExternalAction;
 | 
					  action: ExternalAction;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function ExternalActionButton(props: Props) {
 | 
					export default function ActionButton(props: Props) {
 | 
				
			||||||
  const { action } = props;
 | 
					  const { action } = props;
 | 
				
			||||||
  const { url, title, description, icon, color, openExternally } = action;
 | 
					  const { url, title, description, icon, color, openExternally } = action;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Button type="primary" style={{ backgroundColor: color }}>
 | 
					    <Button type="primary" className={`${s.button}`} style={{ backgroundColor: color }}>
 | 
				
			||||||
      <img src={icon} width="30px" alt={description} />
 | 
					      <img src={icon} className={`${s.icon}`} alt={description} />
 | 
				
			||||||
      {title}
 | 
					      {title}
 | 
				
			||||||
    </Button>
 | 
					    </Button>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
							
								
								
									
										12
									
								
								web/components/action-buttons/ActionButtonRow.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								web/components/action-buttons/ActionButtonRow.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					import React from 'react';
 | 
				
			||||||
 | 
					import s from './ActionButtons.module.scss';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Props {
 | 
				
			||||||
 | 
					  children: React.ReactNode[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function ActionButtonRow(props: Props) {
 | 
				
			||||||
 | 
					  const { children } = props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return <div className={`${s.row}`}>{children}</div>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										10
									
								
								web/components/action-buttons/ActionButtons.module.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								web/components/action-buttons/ActionButtons.module.scss
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					.row {
 | 
				
			||||||
 | 
					  margin: 5px;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  justify-content: flex-end;
 | 
				
			||||||
 | 
					  button {
 | 
				
			||||||
 | 
					    margin-left: 5px;
 | 
				
			||||||
 | 
					    margin-right: 5px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,19 +0,0 @@
 | 
				
			|||||||
import { ExternalAction } from '../interfaces/external-action.interface';
 | 
					 | 
				
			||||||
import ExternalActionButton from './ExternalActionButton';
 | 
					 | 
				
			||||||
import s from './ExternalActionButtons.module.scss';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface Props {
 | 
					 | 
				
			||||||
  actions: ExternalAction[];
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default function ExternalActionButtonRow(props: Props) {
 | 
					 | 
				
			||||||
  const { actions } = props;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <div className={`${s.row}`}>
 | 
					 | 
				
			||||||
      {actions.map(action => (
 | 
					 | 
				
			||||||
        <ExternalActionButton key={action.id} action={action} />
 | 
					 | 
				
			||||||
      ))}
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
import { useRecoilValue } from 'recoil';
 | 
					import { useRecoilValue } from 'recoil';
 | 
				
			||||||
import { Layout, Row, Col, Tabs } from 'antd';
 | 
					import { Layout, Button, Col, Tabs } from 'antd';
 | 
				
			||||||
import Grid from 'antd/lib/card/Grid';
 | 
					import Grid from 'antd/lib/card/Grid';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  chatVisibilityAtom,
 | 
					  chatVisibilityAtom,
 | 
				
			||||||
@ -7,6 +7,7 @@ import {
 | 
				
			|||||||
  chatMessagesAtom,
 | 
					  chatMessagesAtom,
 | 
				
			||||||
  chatStateAtom,
 | 
					  chatStateAtom,
 | 
				
			||||||
} from '../../stores/ClientConfigStore';
 | 
					} from '../../stores/ClientConfigStore';
 | 
				
			||||||
 | 
					import { serverStatusState } from '../../stores/ServerStatusStore';
 | 
				
			||||||
import { ClientConfig } from '../../../interfaces/client-config.model';
 | 
					import { ClientConfig } from '../../../interfaces/client-config.model';
 | 
				
			||||||
import CustomPageContent from '../../CustomPageContent';
 | 
					import CustomPageContent from '../../CustomPageContent';
 | 
				
			||||||
import OwncastPlayer from '../../video/OwncastPlayer';
 | 
					import OwncastPlayer from '../../video/OwncastPlayer';
 | 
				
			||||||
@ -18,30 +19,30 @@ import ChatContainer from '../../chat/ChatContainer';
 | 
				
			|||||||
import { ChatMessage } from '../../../interfaces/chat-message.model';
 | 
					import { ChatMessage } from '../../../interfaces/chat-message.model';
 | 
				
			||||||
import { ChatState, ChatVisibilityState } from '../../../interfaces/application-state';
 | 
					import { ChatState, ChatVisibilityState } from '../../../interfaces/application-state';
 | 
				
			||||||
import ChatTextField from '../../chat/ChatTextField/ChatTextField';
 | 
					import ChatTextField from '../../chat/ChatTextField/ChatTextField';
 | 
				
			||||||
import ExternalActionButtonRow from '../../action-buttons/ExternalActionButtonRow';
 | 
					import ActionButtonRow from '../../action-buttons/ActionButtonRow';
 | 
				
			||||||
 | 
					import ActionButton from '../../action-buttons/ActionButton';
 | 
				
			||||||
 | 
					import Statusbar from '../Statusbar/Statusbar';
 | 
				
			||||||
 | 
					import { ServerStatus } from '../../../interfaces/server-status.model';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const { TabPane } = Tabs;
 | 
					const { TabPane } = Tabs;
 | 
				
			||||||
 | 
					 | 
				
			||||||
const { Content } = Layout;
 | 
					const { Content } = Layout;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function FooterComponent() {
 | 
					export default function FooterComponent() {
 | 
				
			||||||
 | 
					  const status = useRecoilValue<ServerStatus>(serverStatusState);
 | 
				
			||||||
  const clientConfig = useRecoilValue<ClientConfig>(clientConfigStateAtom);
 | 
					  const clientConfig = useRecoilValue<ClientConfig>(clientConfigStateAtom);
 | 
				
			||||||
  const chatOpen = useRecoilValue<ChatVisibilityState>(chatVisibilityAtom);
 | 
					  const chatOpen = useRecoilValue<ChatVisibilityState>(chatVisibilityAtom);
 | 
				
			||||||
  const messages = useRecoilValue<ChatMessage[]>(chatMessagesAtom);
 | 
					  const messages = useRecoilValue<ChatMessage[]>(chatMessagesAtom);
 | 
				
			||||||
  const chatState = useRecoilValue<ChatState>(chatStateAtom);
 | 
					  const chatState = useRecoilValue<ChatState>(chatStateAtom);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const { extraPageContent } = clientConfig;
 | 
					  const { extraPageContent } = clientConfig;
 | 
				
			||||||
 | 
					  const { online, viewerCount, lastConnectTime, lastDisconnectTime, streamTitle } = status;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const followers: Follower[] = [];
 | 
					  const followers: Follower[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const total = 0;
 | 
					  const total = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  // This is example content. It should be removed.
 | 
				
			||||||
    <Content className={`${s.root}`} data-columns={chatOpen ? 2 : 1}>
 | 
					  const externalActions = [
 | 
				
			||||||
      <div className={`${s.leftCol}`}>
 | 
					 | 
				
			||||||
        <OwncastPlayer source="https://watch.owncast.online" />
 | 
					 | 
				
			||||||
        <ExternalActionButtonRow
 | 
					 | 
				
			||||||
          actions={[
 | 
					 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
      url: 'https://owncast.online/docs',
 | 
					      url: 'https://owncast.online/docs',
 | 
				
			||||||
      title: 'Example button',
 | 
					      title: 'Example button',
 | 
				
			||||||
@ -50,8 +51,27 @@ export default function FooterComponent() {
 | 
				
			|||||||
      color: '#5232c8',
 | 
					      color: '#5232c8',
 | 
				
			||||||
      openExternally: true,
 | 
					      openExternally: true,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
          ]}
 | 
					  ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const externalActionButtons = externalActions.map(action => (
 | 
				
			||||||
 | 
					    <ActionButton key={action.url} action={action} />
 | 
				
			||||||
 | 
					  ));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Content className={`${s.root}`} data-columns={chatOpen ? 2 : 1}>
 | 
				
			||||||
 | 
					      <div className={`${s.leftCol}`}>
 | 
				
			||||||
 | 
					        <OwncastPlayer source="https://watch.owncast.online" />
 | 
				
			||||||
 | 
					        <Statusbar
 | 
				
			||||||
 | 
					          online={online}
 | 
				
			||||||
 | 
					          lastConnectTime={lastConnectTime}
 | 
				
			||||||
 | 
					          lastDisconnectTime={lastDisconnectTime}
 | 
				
			||||||
 | 
					          viewerCount={viewerCount}
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
 | 
					        <ActionButtonRow>
 | 
				
			||||||
 | 
					          {externalActionButtons}
 | 
				
			||||||
 | 
					          <Button>Follow</Button>
 | 
				
			||||||
 | 
					          <Button>Notify</Button>
 | 
				
			||||||
 | 
					        </ActionButtonRow>
 | 
				
			||||||
        <div className={`${s.lowerRow}`}>
 | 
					        <div className={`${s.lowerRow}`}>
 | 
				
			||||||
          <Tabs defaultActiveKey="1" type="card">
 | 
					          <Tabs defaultActiveKey="1" type="card">
 | 
				
			||||||
            <TabPane tab="About" key="1" className={`${s.pageContentSection}`}>
 | 
					            <TabPane tab="About" key="1" className={`${s.pageContentSection}`}>
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										10
									
								
								web/components/ui/Statusbar/Statusbar.module.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								web/components/ui/Statusbar/Statusbar.module.scss
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					.statusbar {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  padding-left: 10px;
 | 
				
			||||||
 | 
					  padding-right: 10px;
 | 
				
			||||||
 | 
					  justify-content: space-between;
 | 
				
			||||||
 | 
					  height: 30px;
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  background-color: black;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										50
									
								
								web/components/ui/Statusbar/Statusbar.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								web/components/ui/Statusbar/Statusbar.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,50 @@
 | 
				
			|||||||
 | 
					import formatDistanceToNow from 'date-fns/formatDistanceToNow';
 | 
				
			||||||
 | 
					import formatDistanceToNowStrict from 'date-fns/formatDistanceToNowStrict';
 | 
				
			||||||
 | 
					import { useEffect, useState } from 'react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import s from './Statusbar.module.scss';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Props {
 | 
				
			||||||
 | 
					  online: Boolean;
 | 
				
			||||||
 | 
					  lastConnectTime?: Date;
 | 
				
			||||||
 | 
					  lastDisconnectTime?: Date;
 | 
				
			||||||
 | 
					  viewerCount: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export default function Statusbar(props: Props) {
 | 
				
			||||||
 | 
					  const [now, setNow] = useState(new Date());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    const interval = setInterval(() => setNow(new Date()), 1000);
 | 
				
			||||||
 | 
					    return () => {
 | 
				
			||||||
 | 
					      clearInterval(interval);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { online, lastConnectTime, lastDisconnectTime, viewerCount } = props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let onlineMessage = '';
 | 
				
			||||||
 | 
					  let rightSideMessage = '';
 | 
				
			||||||
 | 
					  if (online && lastConnectTime) {
 | 
				
			||||||
 | 
					    const diff = formatDistanceToNowStrict(new Date(lastConnectTime));
 | 
				
			||||||
 | 
					    onlineMessage = online ? `Streaming for  ${diff}` : 'Offline';
 | 
				
			||||||
 | 
					    rightSideMessage = `${viewerCount} viewers`;
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    onlineMessage = 'Offline';
 | 
				
			||||||
 | 
					    if (lastDisconnectTime) {
 | 
				
			||||||
 | 
					      rightSideMessage = `Last live ${formatDistanceToNow(lastDisconnectTime)} ago.`;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className={s.statusbar}>
 | 
				
			||||||
 | 
					      {/* <div>{streamStartedAt}</div> */}
 | 
				
			||||||
 | 
					      <div>{onlineMessage}</div>
 | 
				
			||||||
 | 
					      <div>{rightSideMessage}</div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Statusbar.defaultProps = {
 | 
				
			||||||
 | 
					  lastConnectTime: null,
 | 
				
			||||||
 | 
					  lastDisconectTime: null,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
.player {
 | 
					.player {
 | 
				
			||||||
  height: 90vh;
 | 
					  height: 80vh;
 | 
				
			||||||
  width: 100%;
 | 
					  width: 100%;
 | 
				
			||||||
  background-color: green;
 | 
					  background-color: green;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -1,9 +0,0 @@
 | 
				
			|||||||
interface Props {
 | 
					 | 
				
			||||||
  online: boolean;
 | 
					 | 
				
			||||||
  viewers: number;
 | 
					 | 
				
			||||||
  timer: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default function StatusBar(props: Props) {
 | 
					 | 
				
			||||||
  return <div>Stream status bar goes here</div>;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,17 +1,15 @@
 | 
				
			|||||||
import React from 'react';
 | 
					import React from 'react';
 | 
				
			||||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
 | 
					import { ComponentStory, ComponentMeta } from '@storybook/react';
 | 
				
			||||||
import ExternalActionButton from '../components/action-buttons/ExternalActionButton';
 | 
					import ActionButton from '../components/action-buttons/ActionButton';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  title: 'owncast/External action button',
 | 
					  title: 'owncast/External action button',
 | 
				
			||||||
  component: ExternalActionButton,
 | 
					  component: ActionButton,
 | 
				
			||||||
  parameters: {},
 | 
					  parameters: {},
 | 
				
			||||||
} as ComponentMeta<typeof ExternalActionButton>;
 | 
					} as ComponentMeta<typeof ActionButton>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
					// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
				
			||||||
const Template: ComponentStory<typeof ExternalActionButton> = args => (
 | 
					const Template: ComponentStory<typeof ActionButton> = args => <ActionButton {...args} />;
 | 
				
			||||||
  <ExternalActionButton {...args} />
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
					// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
				
			||||||
export const Example1 = Template.bind({});
 | 
					export const Example1 = Template.bind({});
 | 
				
			||||||
							
								
								
									
										41
									
								
								web/stories/ActionButtonRow.stories.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								web/stories/ActionButtonRow.stories.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,41 @@
 | 
				
			|||||||
 | 
					import React from 'react';
 | 
				
			||||||
 | 
					import { ComponentStory, ComponentMeta } from '@storybook/react';
 | 
				
			||||||
 | 
					import ActionButtonRow from '../components/action-buttons/ActionButtonRow';
 | 
				
			||||||
 | 
					import ActionButton from '../components/action-buttons/ActionButton';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  title: 'owncast/External action button row',
 | 
				
			||||||
 | 
					  component: ActionButtonRow,
 | 
				
			||||||
 | 
					  parameters: {},
 | 
				
			||||||
 | 
					} as ComponentMeta<typeof ActionButtonRow>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
				
			||||||
 | 
					const Template: ComponentStory<typeof ActionButtonRow> = args => (
 | 
				
			||||||
 | 
					  <ActionButtonRow>{args.buttons}</ActionButtonRow>
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
				
			||||||
 | 
					const actions = [
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    url: 'https://owncast.online/docs',
 | 
				
			||||||
 | 
					    title: 'Documentation',
 | 
				
			||||||
 | 
					    description: 'Owncast Documentation',
 | 
				
			||||||
 | 
					    icon: 'https://owncast.online/images/logo.svg',
 | 
				
			||||||
 | 
					    color: '#5232c8',
 | 
				
			||||||
 | 
					    openExternally: false,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    url: 'https://opencollective.com/embed/owncast/donate',
 | 
				
			||||||
 | 
					    title: 'Support Owncast',
 | 
				
			||||||
 | 
					    description: 'Contribute to Owncast',
 | 
				
			||||||
 | 
					    icon: 'https://opencollective.com/static/images/opencollective-icon.svg',
 | 
				
			||||||
 | 
					    color: '#2b4863',
 | 
				
			||||||
 | 
					    openExternally: false,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const buttons = actions.map(action => <ActionButton action={action} />);
 | 
				
			||||||
 | 
					export const Example1 = Template.bind({});
 | 
				
			||||||
 | 
					Example1.args = {
 | 
				
			||||||
 | 
					  buttons,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@ -24,7 +24,7 @@ import {Color, ColorRow} from './Color';
 | 
				
			|||||||
<ColorRow colors={['theme-text-color', 'theme-text-color-secondary', 'theme-link-color']} />
 | 
					<ColorRow colors={['theme-text-color', 'theme-text-color-secondary', 'theme-link-color']} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Backgrounds
 | 
					## Backgrounds
 | 
				
			||||||
<ColorRow colors={['theme-background', 'popover-background']} />
 | 
					<ColorRow colors={['theme-background', 'theme-background-secondary', 'popover-background']} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Status
 | 
					## Status
 | 
				
			||||||
<ColorRow colors={['theme-success-color', 'theme-info-color', 'theme-warning-color', 'theme-error-color']} />
 | 
					<ColorRow colors={['theme-success-color', 'theme-info-color', 'theme-warning-color', 'theme-error-color']} />
 | 
				
			||||||
 | 
				
			|||||||
@ -1,37 +0,0 @@
 | 
				
			|||||||
import React from 'react';
 | 
					 | 
				
			||||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
 | 
					 | 
				
			||||||
import ExternalActionButtonRow from '../components/action-buttons/ExternalActionButtonRow';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default {
 | 
					 | 
				
			||||||
  title: 'owncast/External action button row',
 | 
					 | 
				
			||||||
  component: ExternalActionButtonRow,
 | 
					 | 
				
			||||||
  parameters: {},
 | 
					 | 
				
			||||||
} as ComponentMeta<typeof ExternalActionButtonRow>;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
					 | 
				
			||||||
const Template: ComponentStory<typeof ExternalActionButtonRow> = args => (
 | 
					 | 
				
			||||||
  <ExternalActionButtonRow {...args} />
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
					 | 
				
			||||||
export const Example1 = Template.bind({});
 | 
					 | 
				
			||||||
Example1.args = {
 | 
					 | 
				
			||||||
  actions: [
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
      url: 'https://owncast.online/docs',
 | 
					 | 
				
			||||||
      title: 'Documentation',
 | 
					 | 
				
			||||||
      description: 'Owncast Documentation',
 | 
					 | 
				
			||||||
      icon: 'https://owncast.online/images/logo.svg',
 | 
					 | 
				
			||||||
      color: '#5232c8',
 | 
					 | 
				
			||||||
      openExternally: false,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
      url: 'https://opencollective.com/embed/owncast/donate',
 | 
					 | 
				
			||||||
      title: 'Support Owncast',
 | 
					 | 
				
			||||||
      description: 'Contribute to Owncast',
 | 
					 | 
				
			||||||
      icon: 'https://opencollective.com/static/images/opencollective-icon.svg',
 | 
					 | 
				
			||||||
      color: '#2b4863',
 | 
					 | 
				
			||||||
      openExternally: false,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  ],
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
@ -1,25 +1,25 @@
 | 
				
			|||||||
import React from 'react';
 | 
					import React from 'react';
 | 
				
			||||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
 | 
					import { ComponentStory, ComponentMeta } from '@storybook/react';
 | 
				
			||||||
import StatusBar from '../components/video/StatusBar';
 | 
					import { subHours } from 'date-fns';
 | 
				
			||||||
 | 
					import Statusbar from '../components/ui/Statusbar/Statusbar';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  title: 'owncast/Status bar',
 | 
					  title: 'owncast/Status bar',
 | 
				
			||||||
  component: StatusBar,
 | 
					  component: Statusbar,
 | 
				
			||||||
  parameters: {},
 | 
					  parameters: {},
 | 
				
			||||||
} as ComponentMeta<typeof StatusBar>;
 | 
					} as ComponentMeta<typeof Statusbar>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
					const Template: ComponentStory<typeof Statusbar> = args => <Statusbar {...args} />;
 | 
				
			||||||
const Template: ComponentStory<typeof StatusBar> = args => <StatusBar {...args} />;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
					 | 
				
			||||||
export const Online = Template.bind({});
 | 
					export const Online = Template.bind({});
 | 
				
			||||||
Online.args = {
 | 
					Online.args = {
 | 
				
			||||||
  online: true,
 | 
					  online: true,
 | 
				
			||||||
  viewers: 42,
 | 
					  viewerCount: 42,
 | 
				
			||||||
  timer: '10:42',
 | 
					  lastConnectTime: subHours(new Date(), 3),
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const Offline = Template.bind({});
 | 
					export const Offline = Template.bind({});
 | 
				
			||||||
Offline.args = {
 | 
					Offline.args = {
 | 
				
			||||||
  online: false,
 | 
					  online: false,
 | 
				
			||||||
 | 
					  lastDisconnectTime: subHours(new Date(), 3),
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
# These are the variables used in the user interface.
 | 
					# These are the variables used in the user interface.
 | 
				
			||||||
# They can be overwritten to customize the look and feel.
 | 
					# They can be overwritten to customize the look and feel.
 | 
				
			||||||
# We should write some documentation on how to do that when the time comes.
 | 
					# We should write some documentation on how to do that when the time comes.
 | 
				
			||||||
# The fewer there are the better as it'll be easier to customize and document.
 | 
					# The fewer there are the easier it'll be easier to customize and document.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
theme:
 | 
					theme:
 | 
				
			||||||
  primary-color:
 | 
					  primary-color:
 | 
				
			||||||
@ -24,7 +24,7 @@ theme:
 | 
				
			|||||||
    comment: "A secondary background color used in sections and controls."
 | 
					    comment: "A secondary background color used in sections and controls."
 | 
				
			||||||
  rounded-corners:
 | 
					  rounded-corners:
 | 
				
			||||||
    value: "5px"
 | 
					    value: "5px"
 | 
				
			||||||
    comment: "The radius of rounded corners."
 | 
					    comment: "The radius of rounded corners used in places."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  success-color:
 | 
					  success-color:
 | 
				
			||||||
    value: "{color.owncast.green.500.value}"
 | 
					    value: "{color.owncast.green.500.value}"
 | 
				
			||||||
 | 
				
			|||||||
@ -22,7 +22,9 @@
 | 
				
			|||||||
  --theme-text-color: #d0d5dd; /* The color of the text in the application. */
 | 
					  --theme-text-color: #d0d5dd; /* The color of the text in the application. */
 | 
				
			||||||
  --theme-text-color-secondary: #667085;
 | 
					  --theme-text-color-secondary: #667085;
 | 
				
			||||||
  --theme-link-color: #9e77ed;
 | 
					  --theme-link-color: #9e77ed;
 | 
				
			||||||
  --theme-font-family: 'Inter', 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';
 | 
					  --theme-font-family: 'Inter', 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';
 | 
				
			||||||
  --theme-background: #1b1a26; /* The main background color of the page. */
 | 
					  --theme-background: #1b1a26; /* The main background color of the page. */
 | 
				
			||||||
  --theme-background-secondary: #16151f; /* A secondary background color used in sections and controls. */
 | 
					  --theme-background-secondary: #16151f; /* A secondary background color used in sections and controls. */
 | 
				
			||||||
  --theme-rounded-corners: 5px; /* The radius of rounded corners. */
 | 
					  --theme-rounded-corners: 5px; /* The radius of rounded corners. */
 | 
				
			||||||
@ -60,7 +62,9 @@
 | 
				
			|||||||
  --color-owncast-logo-blue: #2086e1;
 | 
					  --color-owncast-logo-blue: #2086e1;
 | 
				
			||||||
  --color-owncast-background: #1b1a26;
 | 
					  --color-owncast-background: #1b1a26;
 | 
				
			||||||
  --color-owncast-background-secondary: #16151f;
 | 
					  --color-owncast-background-secondary: #16151f;
 | 
				
			||||||
  --font-owncast-family: 'Inter', 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-family: 'Inter', 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';
 | 
				
			||||||
  --owncast-purple: #7871ff;
 | 
					  --owncast-purple: #7871ff;
 | 
				
			||||||
  --owncast-purple-25: rgba(120, 113, 255, 0.25);
 | 
					  --owncast-purple-25: rgba(120, 113, 255, 0.25);
 | 
				
			||||||
  --owncast-purple-50: rgba(120, 113, 255, 0.5);
 | 
					  --owncast-purple-50: rgba(120, 113, 255, 0.5);
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user