10 changed files with 315 additions and 4 deletions
			
			
		@ -0,0 +1,93 @@ | 
				
			|||||||
 | 
					import { useRecoilValue } from 'recoil'; | 
				
			||||||
 | 
					import { Layout, Row, Col } from 'antd'; | 
				
			||||||
 | 
					import { useState } from 'react'; | 
				
			||||||
 | 
					import { ServerStatus } from '../../models/ServerStatus'; | 
				
			||||||
 | 
					import { ServerStatusStore, serverStatusState } from '../stores/ServerStatusStore'; | 
				
			||||||
 | 
					import { ClientConfigStore, clientConfigState } from '../stores/ClientConfigStore'; | 
				
			||||||
 | 
					import { ClientConfig } from '../../models/ClientConfig'; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { Header, Content, Footer, Sider } = Layout; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function Main() { | 
				
			||||||
 | 
					  const serverStatus = useRecoilValue<ServerStatus>(serverStatusState); | 
				
			||||||
 | 
					  const clientConfig = useRecoilValue<ClientConfig>(clientConfigState); | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { name, version, extraPageContent } = clientConfig; | 
				
			||||||
 | 
					  const [chatCollapsed, setChatCollapsed] = useState(false); | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const toggleChatCollapsed = () => { | 
				
			||||||
 | 
					    setChatCollapsed(!chatCollapsed); | 
				
			||||||
 | 
					  }; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return ( | 
				
			||||||
 | 
					    <> | 
				
			||||||
 | 
					      <ServerStatusStore /> | 
				
			||||||
 | 
					      <ClientConfigStore /> | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <Layout> | 
				
			||||||
 | 
					        <Sider | 
				
			||||||
 | 
					          collapsed={chatCollapsed} | 
				
			||||||
 | 
					          width={300} | 
				
			||||||
 | 
					          style={{ | 
				
			||||||
 | 
					            position: 'fixed', | 
				
			||||||
 | 
					            right: 0, | 
				
			||||||
 | 
					            top: 0, | 
				
			||||||
 | 
					            bottom: 0, | 
				
			||||||
 | 
					          }} | 
				
			||||||
 | 
					        /> | 
				
			||||||
 | 
					        <Layout className="site-layout" style={{ marginRight: 200 }}> | 
				
			||||||
 | 
					          <Header | 
				
			||||||
 | 
					            className="site-layout-background" | 
				
			||||||
 | 
					            style={{ position: 'fixed', zIndex: 1, width: '100%' }} | 
				
			||||||
 | 
					          > | 
				
			||||||
 | 
					            {name} | 
				
			||||||
 | 
					            <button onClick={toggleChatCollapsed}>Toggle Chat</button> | 
				
			||||||
 | 
					          </Header> | 
				
			||||||
 | 
					          <Content style={{ margin: '80px 16px 0', overflow: 'initial' }}> | 
				
			||||||
 | 
					            <div> | 
				
			||||||
 | 
					              <Row> | 
				
			||||||
 | 
					                <Col span={24}>Video player goes here</Col> | 
				
			||||||
 | 
					              </Row> | 
				
			||||||
 | 
					              <Row> | 
				
			||||||
 | 
					                <Col span={24}> | 
				
			||||||
 | 
					                  <Content dangerouslySetInnerHTML={{ __html: extraPageContent }} /> | 
				
			||||||
 | 
					                </Col> | 
				
			||||||
 | 
					              </Row> | 
				
			||||||
 | 
					            </div> | 
				
			||||||
 | 
					          </Content> | 
				
			||||||
 | 
					          <Footer style={{ textAlign: 'center' }}>Footer: Owncast {version}</Footer> | 
				
			||||||
 | 
					        </Layout> | 
				
			||||||
 | 
					      </Layout> | 
				
			||||||
 | 
					    </> | 
				
			||||||
 | 
					  ); | 
				
			||||||
 | 
					  // return (
 | 
				
			||||||
 | 
					  //   <div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  //     <Layout>
 | 
				
			||||||
 | 
					  //       <Header className="header">
 | 
				
			||||||
 | 
					  //         {name}
 | 
				
			||||||
 | 
					  //         <button onClick={toggleChatCollapsed}>Toggle Chat</button>
 | 
				
			||||||
 | 
					  //       </Header>
 | 
				
			||||||
 | 
					  //       <Content>
 | 
				
			||||||
 | 
					  //         <Layout>
 | 
				
			||||||
 | 
					  //           <Row>
 | 
				
			||||||
 | 
					  //             <Col span={24}>Video player goes here</Col>
 | 
				
			||||||
 | 
					  //           </Row>
 | 
				
			||||||
 | 
					  //           <Row>
 | 
				
			||||||
 | 
					  //             <Col span={24}>
 | 
				
			||||||
 | 
					  //               <Content dangerouslySetInnerHTML={{ __html: extraPageContent }} />
 | 
				
			||||||
 | 
					  //             </Col>
 | 
				
			||||||
 | 
					  //           </Row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  //           <Sider collapsed={chatCollapsed} width={300}>
 | 
				
			||||||
 | 
					  //             chat
 | 
				
			||||||
 | 
					  //           </Sider>
 | 
				
			||||||
 | 
					  //         </Layout>
 | 
				
			||||||
 | 
					  //       </Content>
 | 
				
			||||||
 | 
					  //       <Footer>Footer: Owncast {version}</Footer>
 | 
				
			||||||
 | 
					  //     </Layout>
 | 
				
			||||||
 | 
					  //   </div>
 | 
				
			||||||
 | 
					  // );
 | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Main; | 
				
			||||||
@ -0,0 +1,30 @@ | 
				
			|||||||
 | 
					import { useEffect } from 'react'; | 
				
			||||||
 | 
					import { ReactElement } from 'react-markdown/lib/react-markdown'; | 
				
			||||||
 | 
					import { atom, useRecoilState } from 'recoil'; | 
				
			||||||
 | 
					import { makeEmptyClientConfig, ClientConfig } from '../../models/ClientConfig'; | 
				
			||||||
 | 
					import ClientConfigService from '../../services/ClientConfigService'; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const clientConfigState = atom({ | 
				
			||||||
 | 
					  key: 'clientConfigState', | 
				
			||||||
 | 
					  default: makeEmptyClientConfig(), | 
				
			||||||
 | 
					}); | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function ClientConfigStore(): ReactElement { | 
				
			||||||
 | 
					  const [, setClientConfig] = useRecoilState<ClientConfig>(clientConfigState); | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const updateClientConfig = async () => { | 
				
			||||||
 | 
					    try { | 
				
			||||||
 | 
					      const config = await ClientConfigService.getConfig(); | 
				
			||||||
 | 
					      console.log(`ClientConfig: ${JSON.stringify(config)}`); | 
				
			||||||
 | 
					      setClientConfig(config); | 
				
			||||||
 | 
					    } catch (error) { | 
				
			||||||
 | 
					      console.error(`ClientConfigService -> getConfig() ERROR: \n${error}`); | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  }; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => { | 
				
			||||||
 | 
					    updateClientConfig(); | 
				
			||||||
 | 
					  }, []); | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return null; | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,34 @@ | 
				
			|||||||
 | 
					import { useEffect } from 'react'; | 
				
			||||||
 | 
					import { ReactElement } from 'react-markdown/lib/react-markdown'; | 
				
			||||||
 | 
					import { atom, useRecoilState } from 'recoil'; | 
				
			||||||
 | 
					import { ServerStatus, makeEmptyServerStatus } from '../../models/ServerStatus'; | 
				
			||||||
 | 
					import ServerStatusService from '../../services/StatusService'; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const serverStatusState = atom({ | 
				
			||||||
 | 
					  key: 'serverStatusState', | 
				
			||||||
 | 
					  default: makeEmptyServerStatus(), | 
				
			||||||
 | 
					}); | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function ServerStatusStore(): ReactElement { | 
				
			||||||
 | 
					  const [, setServerStatus] = useRecoilState<ServerStatus>(serverStatusState); | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const updateServerStatus = async () => { | 
				
			||||||
 | 
					    try { | 
				
			||||||
 | 
					      const status = await ServerStatusService.getStatus(); | 
				
			||||||
 | 
					      setServerStatus(status); | 
				
			||||||
 | 
					      return status; | 
				
			||||||
 | 
					    } catch (error) { | 
				
			||||||
 | 
					      console.error(`serverStatusState -> getStatus() ERROR: \n${error}`); | 
				
			||||||
 | 
					      return null; | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					  }; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => { | 
				
			||||||
 | 
					    setInterval(() => { | 
				
			||||||
 | 
					      updateServerStatus(); | 
				
			||||||
 | 
					    }, 5000); | 
				
			||||||
 | 
					    updateServerStatus(); | 
				
			||||||
 | 
					  }, []); | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return null; | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,73 @@ | 
				
			|||||||
 | 
					export interface ClientConfig { | 
				
			||||||
 | 
					  name: string; | 
				
			||||||
 | 
					  summary: string; | 
				
			||||||
 | 
					  logo: string; | 
				
			||||||
 | 
					  tags: string[]; | 
				
			||||||
 | 
					  version: string; | 
				
			||||||
 | 
					  nsfw: boolean; | 
				
			||||||
 | 
					  extraPageContent: string; | 
				
			||||||
 | 
					  socialHandles: SocialHandle[]; | 
				
			||||||
 | 
					  chatDisabled: boolean; | 
				
			||||||
 | 
					  externalActions: any[]; | 
				
			||||||
 | 
					  customStyles: string; | 
				
			||||||
 | 
					  maxSocketPayloadSize: number; | 
				
			||||||
 | 
					  federation: Federation; | 
				
			||||||
 | 
					  notifications: Notifications; | 
				
			||||||
 | 
					  authentication: Authentication; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Authentication { | 
				
			||||||
 | 
					  indieAuthEnabled: boolean; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Federation { | 
				
			||||||
 | 
					  enabled: boolean; | 
				
			||||||
 | 
					  account: string; | 
				
			||||||
 | 
					  followerCount: number; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Notifications { | 
				
			||||||
 | 
					  browser: Browser; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Browser { | 
				
			||||||
 | 
					  enabled: boolean; | 
				
			||||||
 | 
					  publicKey: string; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface SocialHandle { | 
				
			||||||
 | 
					  platform: string; | 
				
			||||||
 | 
					  url: string; | 
				
			||||||
 | 
					  icon: string; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function makeEmptyClientConfig(): ClientConfig { | 
				
			||||||
 | 
					  return { | 
				
			||||||
 | 
					    name: '', | 
				
			||||||
 | 
					    summary: '', | 
				
			||||||
 | 
					    logo: '', | 
				
			||||||
 | 
					    tags: [], | 
				
			||||||
 | 
					    version: '', | 
				
			||||||
 | 
					    nsfw: false, | 
				
			||||||
 | 
					    extraPageContent: '', | 
				
			||||||
 | 
					    socialHandles: [], | 
				
			||||||
 | 
					    chatDisabled: false, | 
				
			||||||
 | 
					    externalActions: [], | 
				
			||||||
 | 
					    customStyles: '', | 
				
			||||||
 | 
					    maxSocketPayloadSize: 0, | 
				
			||||||
 | 
					    federation: { | 
				
			||||||
 | 
					      enabled: false, | 
				
			||||||
 | 
					      account: '', | 
				
			||||||
 | 
					      followerCount: 0, | 
				
			||||||
 | 
					    }, | 
				
			||||||
 | 
					    notifications: { | 
				
			||||||
 | 
					      browser: { | 
				
			||||||
 | 
					        enabled: false, | 
				
			||||||
 | 
					        publicKey: '', | 
				
			||||||
 | 
					      }, | 
				
			||||||
 | 
					    }, | 
				
			||||||
 | 
					    authentication: { | 
				
			||||||
 | 
					      indieAuthEnabled: false, | 
				
			||||||
 | 
					    }, | 
				
			||||||
 | 
					  }; | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,15 @@ | 
				
			|||||||
 | 
					export interface ServerStatus { | 
				
			||||||
 | 
					  online: boolean; | 
				
			||||||
 | 
					  viewerCount: number; | 
				
			||||||
 | 
					  lastConnectTime?: Date; | 
				
			||||||
 | 
					  lastDisconnectTime?: Date; | 
				
			||||||
 | 
					  versionNumber?: string; | 
				
			||||||
 | 
					  streamTitle?: string; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function makeEmptyServerStatus(): ServerStatus { | 
				
			||||||
 | 
					  return { | 
				
			||||||
 | 
					    online: false, | 
				
			||||||
 | 
					    viewerCount: 0, | 
				
			||||||
 | 
					  }; | 
				
			||||||
 | 
					} | 
				
			||||||
@ -1,8 +1,10 @@ | 
				
			|||||||
 | 
					import { RecoilRoot } from 'recoil'; | 
				
			||||||
 | 
					import Main from '../components/layouts/main'; | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function Home() { | 
					export default function Home() { | 
				
			||||||
  return ( | 
					  return ( | 
				
			||||||
    <div> | 
					    <RecoilRoot> | 
				
			||||||
      This is where v2 of the Owncast web UI will be built. Begin with the layout component | 
					      <Main /> | 
				
			||||||
      https://ant.design/components/layout/ and edit pages/index.tsx.
 | 
					    </RecoilRoot> | 
				
			||||||
    </div> | 
					 | 
				
			||||||
  ); | 
					  ); | 
				
			||||||
} | 
					} | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,12 @@ | 
				
			|||||||
 | 
					import { ClientConfig } from '../models/ClientConfig'; | 
				
			||||||
 | 
					const ENDPOINT = `http://localhost:8080/api/config`; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ClientConfigService { | 
				
			||||||
 | 
					  public static async getConfig(): Promise<ClientConfig> { | 
				
			||||||
 | 
					    const response = await fetch(ENDPOINT); | 
				
			||||||
 | 
					    const status = await response.json(); | 
				
			||||||
 | 
					    return status; | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default ClientConfigService; | 
				
			||||||
@ -0,0 +1,13 @@ | 
				
			|||||||
 | 
					import ServerStatus from '../models/ServerStatus'; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ENDPOINT = `http://localhost:8080/api/status`; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ServerStatusService { | 
				
			||||||
 | 
					  public static async getStatus(): Promise<ServerStatus> { | 
				
			||||||
 | 
					    const response = await fetch(ENDPOINT); | 
				
			||||||
 | 
					    const status = await response.json(); | 
				
			||||||
 | 
					    return status; | 
				
			||||||
 | 
					  } | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default ServerStatusService; | 
				
			||||||
					Loading…
					
					
				
		Reference in new issue