From 22caf8899b4ba44d1f45ac5bc57ed3304de9f6a1 Mon Sep 17 00:00:00 2001 From: marcobaobao Date: Thu, 28 Aug 2025 14:40:04 +0200 Subject: [PATCH] added twitch frontend components --- frontend/package.json | 2 +- frontend/src/Layout.tsx | 14 ++ frontend/src/components/HomeSpeedDial.tsx | 1 - frontend/src/components/TwitchIcon.tsx | 22 +++ .../src/components/twitch/TwitchDialog.tsx | 143 ++++++++++++++++++ frontend/src/router.tsx | 9 ++ frontend/src/views/Twitch.tsx | 77 ++++++++++ server/server.go | 5 +- server/twitch/monitor.go | 7 + server/twitch/rest.go | 27 +++- 10 files changed, 302 insertions(+), 5 deletions(-) create mode 100644 frontend/src/components/TwitchIcon.tsx create mode 100644 frontend/src/components/twitch/TwitchDialog.tsx create mode 100644 frontend/src/views/Twitch.tsx diff --git a/frontend/package.json b/frontend/package.json index 77c17b3..614b5b2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,11 +18,11 @@ "@mui/icons-material": "^6.2.0", "@mui/material": "^6.2.0", "fp-ts": "^2.16.5", + "jotai": "^2.10.3", "react": "^19.0.0", "react-dom": "^19.0.0", "react-router-dom": "^6.23.1", "react-virtuoso": "^4.7.11", - "jotai": "^2.10.3", "rxjs": "^7.8.1" }, "devDependencies": { diff --git a/frontend/src/Layout.tsx b/frontend/src/Layout.tsx index 0d136bc..61055e0 100644 --- a/frontend/src/Layout.tsx +++ b/frontend/src/Layout.tsx @@ -28,6 +28,7 @@ import Footer from './components/Footer' import Logout from './components/Logout' import SocketSubscriber from './components/SocketSubscriber' import ThemeToggler from './components/ThemeToggler' +import TwitchIcon from './components/TwitchIcon' import { useI18n } from './hooks/useI18n' import Toaster from './providers/ToasterProvider' import { getAccentValue } from './utils' @@ -154,6 +155,19 @@ export default function Layout() { + + + + + + + + = ({ onDownloadOpen, onEditorOpen }) => { ariaLabel="Home speed dial" sx={{ position: 'absolute', bottom: 64, right: 24 }} icon={} - onClick={onDownloadOpen} > : } diff --git a/frontend/src/components/TwitchIcon.tsx b/frontend/src/components/TwitchIcon.tsx new file mode 100644 index 0000000..7c7d107 --- /dev/null +++ b/frontend/src/components/TwitchIcon.tsx @@ -0,0 +1,22 @@ +import { useAtomValue } from 'jotai' +import { settingsState } from '../atoms/settings' + +const TwitchIcon: React.FC = () => { + const { theme } = useAtomValue(settingsState) + + return ( + + Twitch + + + ) +} + +export default TwitchIcon \ No newline at end of file diff --git a/frontend/src/components/twitch/TwitchDialog.tsx b/frontend/src/components/twitch/TwitchDialog.tsx new file mode 100644 index 0000000..95d5399 --- /dev/null +++ b/frontend/src/components/twitch/TwitchDialog.tsx @@ -0,0 +1,143 @@ +import CloseIcon from '@mui/icons-material/Close' +import { + Alert, + AppBar, + Box, + Button, + Container, + Dialog, + Grid, + IconButton, + Paper, + Slide, + TextField, + Toolbar, + Typography +} from '@mui/material' +import { TransitionProps } from '@mui/material/transitions' +import { matchW } from 'fp-ts/lib/Either' +import { pipe } from 'fp-ts/lib/function' +import { useAtomValue } from 'jotai' +import { forwardRef, startTransition, useState } from 'react' +import { serverURL } from '../../atoms/settings' +import { useToast } from '../../hooks/toast' +import { useI18n } from '../../hooks/useI18n' +import { ffetch } from '../../lib/httpClient' + +type Props = { + open: boolean + onClose: () => void +} + +const Transition = forwardRef(function Transition( + props: TransitionProps & { + children: React.ReactElement + }, + ref: React.Ref, +) { + return +}) + +const TwitchDialog: React.FC = ({ open, onClose }) => { + const [channelURL, setChannelURL] = useState('') + + const { i18n } = useI18n() + const { pushMessage } = useToast() + + const baseURL = useAtomValue(serverURL) + + const submit = async (channelURL: string) => { + const task = ffetch(`${baseURL}/twitch/user`, { + method: 'POST', + body: JSON.stringify({ + user: channelURL.split('/').at(-1) + }) + }) + const either = await task() + + pipe( + either, + matchW( + (l) => pushMessage(l, 'error'), + (_) => onClose() + ) + ) + } + + return ( + + + + + + + + {i18n.t('subscriptionsButtonLabel')} + + + + theme.palette.background.default, + minHeight: (theme) => `calc(99vh - ${theme.mixins.toolbar.minHeight}px)` + }}> + + + + + + + + {i18n.t('subscriptionsInfo')} + + + {i18n.t('livestreamExperimentalWarning')} + + + + setChannelURL(e.target.value)} + /> + + + + + + + + + + + + ) +} + +export default TwitchDialog \ No newline at end of file diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index 8fbfaa4..d88be34 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -6,6 +6,7 @@ import Terminal from './views/Terminal' const Home = lazy(() => import('./views/Home')) const Login = lazy(() => import('./views/Login')) +const Twitch = lazy(() => import('./views/Twitch')) const Archive = lazy(() => import('./views/Archive')) const Settings = lazy(() => import('./views/Settings')) const LiveStream = lazy(() => import('./views/Livestream')) @@ -111,6 +112,14 @@ export const router = createHashRouter([ ) }, + { + path: '/twitch', + element: ( + }> + + + ) + }, ] }, ]) \ No newline at end of file diff --git a/frontend/src/views/Twitch.tsx b/frontend/src/views/Twitch.tsx new file mode 100644 index 0000000..214c7c5 --- /dev/null +++ b/frontend/src/views/Twitch.tsx @@ -0,0 +1,77 @@ +import { + Chip, + Container, + Paper +} from '@mui/material' +import { matchW } from 'fp-ts/lib/Either' +import { pipe } from 'fp-ts/lib/function' +import { useAtomValue } from 'jotai' +import { useState, useTransition } from 'react' +import { serverURL } from '../atoms/settings' +import LoadingBackdrop from '../components/LoadingBackdrop' +import NoSubscriptions from '../components/subscriptions/NoSubscriptions' +import SubscriptionsSpeedDial from '../components/subscriptions/SubscriptionsSpeedDial' +import TwitchDialog from '../components/twitch/TwitchDialog' +import { useToast } from '../hooks/toast' +import useFetch from '../hooks/useFetch' +import { ffetch } from '../lib/httpClient' + +const TwitchView: React.FC = () => { + const { pushMessage } = useToast() + + const baseURL = useAtomValue(serverURL) + + const [openDialog, setOpenDialog] = useState(false) + + const { data: users, fetcher: refetch } = useFetch>('/twitch/users') + + const [isPending, startTransition] = useTransition() + + const deleteUser = async (user: string) => { + const task = ffetch(`${baseURL}/twitch/user/${user}`, { + method: 'DELETE', + }) + const either = await task() + + pipe( + either, + matchW( + (l) => pushMessage(l, 'error'), + () => refetch() + ) + ) + } + + return ( + <> + + + setOpenDialog(s => !s)} /> + + { + setOpenDialog(s => !s) + refetch() + }} /> + + { + !users || users.length === 0 ? + : + + + {users.map(user => ( + startTransition(async () => await deleteUser(user))} + /> + ))} + + + } + + ) +} + +export default TwitchView \ No newline at end of file diff --git a/server/server.go b/server/server.go index 3a26e00..75bcaa9 100644 --- a/server/server.go +++ b/server/server.go @@ -251,8 +251,9 @@ func newServer(c serverConfig) *http.Server { // Twitch r.Route("/twitch", func(r chi.Router) { r.Use(middlewares.ApplyAuthenticationByConfig) - r.Get("/all", twitch.GetMonitoredUsers(c.tm)) - r.Post("/add", twitch.MonitorUserHandler(c.tm)) + r.Get("/users", twitch.GetMonitoredUsers(c.tm)) + r.Post("/user", twitch.MonitorUserHandler(c.tm)) + r.Delete("/user/{user}", twitch.DeleteUser(c.tm)) }) return &http.Server{Handler: r} diff --git a/server/twitch/monitor.go b/server/twitch/monitor.go index c07b984..d803d6d 100644 --- a/server/twitch/monitor.go +++ b/server/twitch/monitor.go @@ -83,6 +83,13 @@ func (m *Monitor) GetMonitoredUsers() iter.Seq[string] { return maps.Keys(m.monitored) } +func (m *Monitor) DeleteUser(user string) { + m.mu.Lock() + defer m.mu.Unlock() + delete(m.monitored, user) + delete(m.lastState, user) +} + func DEFAULT_DOWNLOAD_HANDLER(db *internal.MemoryDB, mq *internal.MessageQueue) func(url string) error { return func(url string) error { p := &internal.Process{ diff --git a/server/twitch/rest.go b/server/twitch/rest.go index 7b8238b..bb43063 100644 --- a/server/twitch/rest.go +++ b/server/twitch/rest.go @@ -4,6 +4,8 @@ import ( "encoding/json" "net/http" "slices" + + "github.com/go-chi/chi/v5" ) type addUserReq struct { @@ -32,7 +34,30 @@ func GetMonitoredUsers(m *Monitor) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { it := m.GetMonitoredUsers() - if err := json.NewEncoder(w).Encode(slices.Collect(it)); err != nil { + users := slices.Collect(it) + if users == nil { + users = make([]string, 0) + } + + if err := json.NewEncoder(w).Encode(users); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } +} + +func DeleteUser(m *Monitor) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + user := chi.URLParam(r, "user") + + if user == "" { + http.Error(w, "empty user", http.StatusBadRequest) + return + } + + m.DeleteUser(user) + + if err := json.NewEncoder(w).Encode("ok"); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return }