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