Compare commits
	
		
			7 Commits
		
	
	
		
			a267f9e0df
			...
			717790cb97
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 717790cb97 | |||
| fbe9153bfc | |||
| 2a5f0ea601 | |||
| f0dd632922 | |||
| 031505600c | |||
| 03a5fb5497 | |||
| c193e5da2d | 
| @@ -60,7 +60,6 @@ aside { | |||||||
|  |  | ||||||
| .menu-list { | .menu-list { | ||||||
|     list-style: none; |     list-style: none; | ||||||
|     background-color: brown; |  | ||||||
|     margin-top: 0px; |     margin-top: 0px; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ | |||||||
| <html lang="en"> | <html lang="en"> | ||||||
|   <head> |   <head> | ||||||
|     <meta charset="utf-8"/> |     <meta charset="utf-8"/> | ||||||
|     <title>stream.t-juice.club</title> |     <title></title> | ||||||
|     <script src="./js/app.tsx" type="module"></script> |     <script src="./js/app.tsx" type="module"></script> | ||||||
|     <link rel="stylesheet" href="./css/style.css" /> |     <link rel="stylesheet" href="./css/style.css" /> | ||||||
|     <link rel="stylesheet" href="./css/fonts.css" /> |     <link rel="stylesheet" href="./css/fonts.css" /> | ||||||
|   | |||||||
| @@ -1,4 +1,8 @@ | |||||||
|  |  | ||||||
|  | export interface SiteInfo { | ||||||
|  |     siteName: string | ||||||
|  | } | ||||||
|  |  | ||||||
| export class MinistreamApiClient { | export class MinistreamApiClient { | ||||||
|     ENV: string |     ENV: string | ||||||
|  |  | ||||||
| @@ -21,14 +25,22 @@ export class MinistreamApiClient { | |||||||
|             const resp = await fetch( |             const resp = await fetch( | ||||||
|                 url, |                 url, | ||||||
|             ); |             ); | ||||||
|             const res = resp.json() as unknown as string[]; |             data = await resp.json() as unknown as string[]; | ||||||
|             data = res; |  | ||||||
|         } |         } | ||||||
|         catch (e) { |         catch (e) { | ||||||
|             throw e; |             throw e; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         return data; |         const sortedStreams = data.sort((a, b) => { | ||||||
|  |             if (a > b) { | ||||||
|  |                 return -1 | ||||||
|  |             } else if (a < b) { | ||||||
|  |                 return 1 | ||||||
|  |             } | ||||||
|  |             return 0 | ||||||
|  |         }) | ||||||
|  |  | ||||||
|  |         return sortedStreams; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async postOffer(streamKey: string, offer_sdp: RTCSessionDescription): Promise<RTCSessionDescription> { |     async postOffer(streamKey: string, offer_sdp: RTCSessionDescription): Promise<RTCSessionDescription> { | ||||||
| @@ -43,8 +55,23 @@ export class MinistreamApiClient { | |||||||
|                 body: offer_sdp.sdp |                 body: offer_sdp.sdp | ||||||
|             }) |             }) | ||||||
|         const body = await resp.text() |         const body = await resp.text() | ||||||
|         const answer = new RTCSessionDescription({type: "answer", sdp: body}) |         const answer = new RTCSessionDescription({ type: "answer", sdp: body }) | ||||||
|  |  | ||||||
|         return answer |         return answer | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     async siteInfo(): Promise<SiteInfo> { | ||||||
|  |         var url = "/api/siteinfo" | ||||||
|  |         if (this.ENV !== "production") { | ||||||
|  |             url = "http://localhost:8080/api/siteinfo" | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const resp = await fetch(url, | ||||||
|  |             { | ||||||
|  |                 method: "GET", | ||||||
|  |             }) | ||||||
|  |         const data = resp.json() | ||||||
|  |  | ||||||
|  |         return data as unknown as SiteInfo | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										141
									
								
								src/js/app.tsx
									
									
									
									
									
								
							
							
						
						
									
										141
									
								
								src/js/app.tsx
									
									
									
									
									
								
							| @@ -1,92 +1,91 @@ | |||||||
| import { createRoot } from "react-dom/client"; | import { createRoot } from "react-dom/client"; | ||||||
| import React from "react"; |  | ||||||
| import { Menu } from "./menu"; | import { Menu } from "./menu"; | ||||||
| import { createContext, useState } from "react"; | import { createContext, useEffect, useState } from "react"; | ||||||
| import { MediaContainer } from "./media"; | import { MediaContainer } from "./media"; | ||||||
| import { MinistreamApiClient } from "./api"; | import { MinistreamApiClient } from "./api"; | ||||||
|  | import React from "react"; | ||||||
|  |  | ||||||
| type ThemeContextType = "light" | "dark"; | interface AppProps { | ||||||
|  |     api: MinistreamApiClient | ||||||
| const ThemeContext = createContext<ThemeContextType>("light"); |  | ||||||
|  |  | ||||||
| type AppState = { |  | ||||||
|     streamList: string[] |  | ||||||
|     selectedStream: string | undefined |  | ||||||
| } | } | ||||||
| type AppProps = {} |  | ||||||
|  |  | ||||||
| export class App extends React.Component<AppProps, AppState> { | const titleKey = "ministream.title" | ||||||
|     apiClient: MinistreamApiClient |  | ||||||
|     constructor(props: AppProps) { | function setTitleFromLocalstorage() { | ||||||
|         super(props) |     var title = localStorage.getItem(titleKey) | ||||||
|         this.state = { |     if (title) { | ||||||
|             streamList: [], |         setTitle(title) | ||||||
|             selectedStream: undefined |     } | ||||||
|         } | } | ||||||
|         this.apiClient = new MinistreamApiClient() |  | ||||||
|         this.updateSelect = this.updateSelect.bind(this) | function setTitle(title: string) { | ||||||
|  |     const el = document.querySelector('title') | ||||||
|  |     if (el) { | ||||||
|  |         el.textContent = title | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function App({ api }: AppProps) { | ||||||
|  |     const [streamList, setStreamList] = useState<string[]>([]) | ||||||
|  |     const [selectedStream, setSelectedStream] = useState<string | null>(null) | ||||||
|  |  | ||||||
|  |     const updateSelect = (item: string | null) => { | ||||||
|  |         setSelectedStream(item) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async componentDidMount(): Promise<void> { |     const updateStreamList = () => { | ||||||
|         await this.updateStreamList() |         api.listStreams().then((list) => { | ||||||
|         setInterval(() => { |             setStreamList(list) | ||||||
|             this.updateStreamList() |             if (list.length != streamList.length) { | ||||||
|         }, 10000) |                 setStreamList(list) | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async updateStreamList() { |  | ||||||
|         const streams = await this.apiClient.listStreams() |  | ||||||
|         const sortedStreams = streams.sort((a, b) => { |  | ||||||
|             if (a > b) { |  | ||||||
|                 return -1 |  | ||||||
|             } else if (a < b) { |  | ||||||
|                 return 1 |  | ||||||
|             } |             } | ||||||
|             return 0 |  | ||||||
|         }) |  | ||||||
|  |  | ||||||
|         if (sortedStreams.length != this.state.streamList.length) { |             if (!list.every((_, idx) => { | ||||||
|             this.setStreamList(streams) |                 return list[idx] === streamList[idx] | ||||||
|         } |             })) { | ||||||
|  |                 setStreamList(list) | ||||||
|         if (!sortedStreams.every((_, idx) => { |             } | ||||||
|             return sortedStreams[idx] === this.state.streamList[idx] |  | ||||||
|         })) { |  | ||||||
|             this.setStreamList(streams) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     setStreamList(streamList: string[]) { |  | ||||||
|         console.log("stream list updated") |  | ||||||
|         this.setState((state) => { |  | ||||||
|             return { selectedStream: state.selectedStream, streamList: streamList } |  | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     updateSelect(selected: string): void { |     const updateTitle = () => { | ||||||
|         if (selected !== this.state.selectedStream) { |             api.siteInfo().then((info) => { | ||||||
|             this.setState((state) => { |             if (info.siteName != document.title) { | ||||||
|                 return { streamList: state.streamList, selectedStream: selected } |                 setTitle(info.siteName) | ||||||
|             }) |                 localStorage.setItem(titleKey, info.siteName) | ||||||
|         } |             } | ||||||
|  |         }) | ||||||
|  |         return | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     render() { |     setInterval(() => { | ||||||
|         return ( |         updateStreamList() | ||||||
|             <> |     }, 10000) | ||||||
|                 <Menu |  | ||||||
|                     items={this.state.streamList} |     useEffect(() => { | ||||||
|                     selectedItem={this.state.selectedStream} |         updateStreamList() | ||||||
|                     selectCallback={this.updateSelect} |     }, []) | ||||||
|                 /> |  | ||||||
|                 <MediaContainer selectedStream={this.state.selectedStream} api={this.apiClient} /> |     useEffect(() => { | ||||||
|             </> |         updateTitle() | ||||||
|         ) |     }, []) | ||||||
|     } |  | ||||||
|  |     return ( | ||||||
|  |         <> | ||||||
|  |             <Menu | ||||||
|  |                 items={streamList} | ||||||
|  |                 selectedItem={selectedStream} | ||||||
|  |                 selectCallback={updateSelect} | ||||||
|  |             /> | ||||||
|  |             <MediaContainer selectedStream={selectedStream} api={api} /> | ||||||
|  |         </> | ||||||
|  |     ) | ||||||
| } | } | ||||||
|  |  | ||||||
| const rootElement = document.getElementById("app"); | const rootElement = document.getElementById("app"); | ||||||
|  | setTitleFromLocalstorage() | ||||||
| if (rootElement) { | if (rootElement) { | ||||||
|     const root = createRoot(rootElement); |     const root = createRoot(rootElement) | ||||||
|     root.render(<App />) |     const api = new MinistreamApiClient() | ||||||
|  |     root.render(<App api={api} />) | ||||||
| } | } | ||||||
| @@ -1,9 +1,10 @@ | |||||||
| import React, { ComponentProps, useRef, useEffect, useState } from "react" | import { ComponentProps, useRef, useEffect, useState } from "react" | ||||||
| import { MinistreamApiClient } from "./api" | import { MinistreamApiClient } from "./api" | ||||||
| import { resolve } from "path" | import { resolve } from "path" | ||||||
|  | import React from "react" | ||||||
|  |  | ||||||
| type MediaContainerProps = { | type MediaContainerProps = { | ||||||
|     selectedStream: string | undefined |     selectedStream: string | null | ||||||
|     api: MinistreamApiClient |     api: MinistreamApiClient | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,45 +1,42 @@ | |||||||
| import { createRoot } from "react-dom/client"; | import { createRoot } from "react-dom/client"; | ||||||
| import React from "react"; | import React from "react"; | ||||||
|  |  | ||||||
| type StreamSelectCallback = (selected: string) => void | type StreamSelectCallback = (selected: string | null) => void | ||||||
|  |  | ||||||
| type MenuProps = { | interface MenuProps { | ||||||
|     items: string[] |     items: string[] | ||||||
|     selectedItem: string | undefined |     selectedItem: string | null | ||||||
|     selectCallback: StreamSelectCallback |     selectCallback: StreamSelectCallback | ||||||
| } | } | ||||||
|  |  | ||||||
| export class Menu extends React.Component<MenuProps> { | export function Menu({ items, selectedItem, selectCallback }: MenuProps) { | ||||||
|     constructor(props: MenuProps) { |     const title = document.title | ||||||
|         super(props) |     const menuitems = () => { | ||||||
|     } |  | ||||||
|  |  | ||||||
|     render() { |  | ||||||
|         return ( |  | ||||||
|             <div id="menu"> |  | ||||||
|                 <aside> |  | ||||||
|                     <a className="menu-heading" href="#">stream.t-juice.club</a> |  | ||||||
|                     <ul className="menu-list"> |  | ||||||
|                         {this.menuitems()} |  | ||||||
|                     </ul> |  | ||||||
|                 </aside> |  | ||||||
|             </div> |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private menuitems() { |  | ||||||
|         return ( |         return ( | ||||||
|             <> |             <> | ||||||
|                 {this.props.items.map((value, idx) => { |                 {items.map((value, idx) => { | ||||||
|                     if (this.props.selectedItem == value) { |                     if (selectedItem == value) { | ||||||
|                         return <li key={idx} className="menu-item menu-selected"><a href={"#" + value} className="menu-link">{value}</a></li> |                         return <li key={idx} className="menu-item menu-selected"><a href={"#" + value} className="menu-link">{value}</a></li> | ||||||
|                     } else { |                     } else { | ||||||
|                         return <li key={idx} onClick={() => { |                         return <li key={idx} onClick={() => { | ||||||
|                             this.props.selectCallback(value) |                             selectCallback(value) | ||||||
|                         }} className="menu-item"><a href={"#" + value} className="menu-link">{value}</a></li> |                         }} className="menu-item"><a href={"#" + value} className="menu-link">{value}</a></li> | ||||||
|                     } |                     } | ||||||
|                 })} |                 })} | ||||||
|             </> |             </> | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
|  |     return ( | ||||||
|  |         <div id="menu"> | ||||||
|  |             <aside> | ||||||
|  |                 <a className="menu-heading" onClick={() => { | ||||||
|  |                     selectCallback(null) | ||||||
|  |                 }}href="#">{title}</a> | ||||||
|  |                 <ul className="menu-list"> | ||||||
|  |                     {menuitems()} | ||||||
|  |                 </ul> | ||||||
|  |             </aside> | ||||||
|  |         </div> | ||||||
|  |     ) | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user