Compare commits
	
		
			6 Commits
		
	
	
		
			a267f9e0df
			...
			fbe9153bfc
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 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