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