First pass on bot management page

This commit is contained in:
Benjamin Sherriff
2023-10-08 00:18:39 -04:00
parent 0ec6264bfa
commit 1035d3bc21
8 changed files with 233 additions and 4 deletions

View File

@@ -166,6 +166,56 @@ async fn play(path: web::Path<(String, String)>, play_request: web::Json<PlayReq
}
}
#[post("/{guild_id}/voice/stop")]
async fn stop(path: web::Path<String>, data: web::Data<Arc<AppState>>) -> HttpResponse {
let guild_id = path.into_inner();
let guild_id = match guild_id.parse::<u64>() {
Ok(id) => id,
Err(err) => {
warn!("Could not parse guild id: {:?}", err);
return ResponseError::error_response(&ServiceError {
status: 422,
message: err.to_string()
})
}
};
if let Some(handler_lock) = data.songbird.get(guild_id) {
let handler = handler_lock.lock().await;
handler.queue().stop();
}
HttpResponse::Ok().finish()
}
#[post("/{guild_id}/voice/resume")]
async fn resume(path: web::Path<String>, data: web::Data<Arc<AppState>>) -> HttpResponse {
let guild_id = path.into_inner();
let guild_id = match guild_id.parse::<u64>() {
Ok(id) => id,
Err(err) => {
warn!("Could not parse guild id: {:?}", err);
return ResponseError::error_response(&ServiceError {
status: 422,
message: err.to_string()
})
}
};
if let Some(handler_lock) = data.songbird.get(guild_id) {
let handler = handler_lock.lock().await;
if let Err(err) = handler.queue().resume() {
warn!("Could not resume track: {:?}", err);
return ResponseError::error_response(&ServiceError {
status: 422,
message: err.to_string()
})
}
}
HttpResponse::Ok().finish()
}
#[post("/{guild_id}/voice/pause")]
async fn pause(path: web::Path<String>, data: web::Data<Arc<AppState>>) -> HttpResponse {
let guild_id = path.into_inner();
@@ -201,7 +251,9 @@ pub fn init_routes(config: &mut web::ServiceConfig) {
.service(get_text_channels)
.service(get_voice_channels)
.service(send_message)
.service(pause)
.service(play)
.service(stop)
.service(resume)
.service(pause)
);
}

24
ui/package-lock.json generated
View File

@@ -9,6 +9,7 @@
"version": "0.1.0",
"dependencies": {
"@mantine/core": "^7.1.2",
"@mantine/form": "^7.1.2",
"@mantine/hooks": "^7.1.2",
"@mantine/modals": "^7.1.2",
"@mantine/notifications": "^7.1.2",
@@ -225,6 +226,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@mantine/form": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/@mantine/form/-/form-7.1.2.tgz",
"integrity": "sha512-FnUu5XNmRM265G0wy19qSRiItG/2eQ0GQCctnokw6ws9ZnCU1NqvsmpuDE/UiV4YCAOhAVHfqnjG/8tsrlw7ug==",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"klona": "^2.0.5"
},
"peerDependencies": {
"react": "^18.2.0"
}
},
"node_modules/@mantine/hooks": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.1.2.tgz",
@@ -2305,8 +2318,7 @@
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"node_modules/fast-diff": {
"version": "1.3.0",
@@ -3353,6 +3365,14 @@
"json-buffer": "3.0.1"
}
},
"node_modules/klona": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz",
"integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==",
"engines": {
"node": ">= 8"
}
},
"node_modules/language-subtag-registry": {
"version": "0.3.22",
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz",

View File

@@ -10,6 +10,7 @@
},
"dependencies": {
"@mantine/core": "^7.1.2",
"@mantine/form": "^7.1.2",
"@mantine/hooks": "^7.1.2",
"@mantine/modals": "^7.1.2",
"@mantine/notifications": "^7.1.2",

33
ui/src/api/guilds.ts Normal file
View File

@@ -0,0 +1,33 @@
import { getRequest, postRequest } from '.';
import { GuildChannel, GuildInfo } from './guilds.types';
export async function getGuilds(): Promise<GuildInfo[]> {
const response = await getRequest('guilds', {});
return response?.data || { data: [] };
}
export async function getTextChannels(guildId: number): Promise<GuildChannel[]> {
const response = await getRequest(`guilds/${guildId}/text`, {});
return response?.data || { data: [] };
}
export async function getVoiceChannels(guildId: number): Promise<GuildChannel[]> {
const response = await getRequest(`guilds/${guildId}/voice`, {});
return response?.data || { data: [] };
}
export async function playTrack(guildId: number, channelId: number, track: string): Promise<void> {
await postRequest(`guilds/${guildId}/voice/${channelId}/play`, { track_url: track });
}
export async function stopTrack(guildId: number, channelId: number): Promise<void> {
await postRequest(`guilds/${guildId}/voice/${channelId}/stop`, {});
}
export async function pauseTrack(guildId: number, channelId: number): Promise<void> {
await postRequest(`guilds/${guildId}/voice/${channelId}/pause`, {});
}
export async function resumeTrack(guildId: number, channelId: number): Promise<void> {
await postRequest(`guilds/${guildId}/voice/${channelId}/resume`, {});
}

View File

@@ -0,0 +1,13 @@
export interface GuildInfo {
id: number;
icon?: string;
name: string;
owner: boolean;
}
export interface GuildChannel {
id: number;
name: string;
type: string;
guild_id: number;
}

View File

@@ -12,7 +12,7 @@ export async function getRequest(endpoint: string, params: any): Promise<AxiosRe
export async function postRequest(endpoint: string, body: any): Promise<AxiosResponse<any, any> | undefined> {
const response = await axios
.post(`${serviceHost}:${servicePort}/${endpoint}`, { body })
.post(`${serviceHost}:${servicePort}/${endpoint}`, body || {})
.catch((error) => console.error(error));
return response || undefined;
}

View File

@@ -0,0 +1,106 @@
'use client';
import {
getGuilds,
getTextChannels,
getVoiceChannels,
pauseTrack,
playTrack,
resumeTrack,
stopTrack
} from '@/api/guilds';
import { GuildChannel, GuildInfo } from '@/api/guilds.types';
import { Button, Slider, Tabs, TextInput } from '@mantine/core';
import { useForm } from '@mantine/form';
import React, { useEffect, useState } from 'react';
export default function Page() {
const [guilds, setGuilds] = useState<GuildInfo[]>([]);
const [activeGuild, setActiveGuild] = useState<GuildInfo | null>(null);
const [textChannels, setTextChannels] = useState<GuildChannel[]>([]);
const [voiceChannels, setVoiceChannels] = useState<GuildChannel[]>([]);
useEffect(() => {
getGuilds().then((g) => {
setGuilds(g);
if (g.length > 0) {
setActiveGuild(g[0]);
}
});
}, []);
useEffect(() => {
if (activeGuild) {
getTextChannels(activeGuild.id).then((c) => setTextChannels(c));
getVoiceChannels(activeGuild.id).then((c) => setVoiceChannels(c));
}
}, [activeGuild]);
const playForm = useForm({
initialValues: {
trackUrl: ''
}
});
return (
<Tabs orientation='vertical' defaultValue={activeGuild?.name}>
<Tabs.List>
{guilds.map((guild) => (
<Tabs.Tab key={guild.id} value={guild.name} onClick={() => setActiveGuild(guild)}>
{guild.name}
</Tabs.Tab>
))}
</Tabs.List>
{guilds.map((guild) => (
<Tabs.Panel key={guild.id} value={guild.name}>
<h1>{guild.name}</h1>
<h2>Text Channels</h2>
<Tabs orientation='horizontal' defaultValue={textChannels[0]?.name}>
<Tabs.List>
{textChannels.map((channel) => (
<Tabs.Tab key={channel.id} value={channel.name}>
{channel.name}
</Tabs.Tab>
))}
</Tabs.List>
{textChannels.map((channel) => (
<Tabs.Panel key={channel.id} value={channel.name}>
{channel.name}
</Tabs.Panel>
))}
</Tabs>
<h2>Voice Channels</h2>
<Tabs orientation='horizontal' defaultValue={voiceChannels[0]?.name}>
<Tabs.List>
{voiceChannels.map((channel) => (
<Tabs.Tab key={channel.id} value={channel.name}>
{channel.name}
</Tabs.Tab>
))}
</Tabs.List>
{voiceChannels.map((channel) => (
<Tabs.Panel key={channel.id} value={channel.name}>
{channel.name}
<form
style={{ display: 'flex' }}
onSubmit={playForm.onSubmit((values) => {
playTrack(activeGuild!.id, channel.id, values.trackUrl);
})}
>
<TextInput placeholder='Youtube URL...' {...playForm.getInputProps('trackUrl')} />
<Button type='submit'>Play Track</Button>
</form>
<Button onClick={() => stopTrack(activeGuild!.id, channel.id)}>Stop</Button>
<Button onClick={() => pauseTrack(activeGuild!.id, channel.id)}>Pause</Button>
<Button onClick={() => resumeTrack(activeGuild!.id, channel.id)}>Resume</Button>
<div>
<Slider label='Volume' style={{ width: '20%' }} defaultValue={50} onChange={(v) => {}} />
</div>
</Tabs.Panel>
))}
</Tabs>
</Tabs.Panel>
))}
</Tabs>
);
}

View File

@@ -32,6 +32,10 @@ const headerItems = [
{
name: 'Spells',
link: '/spells'
},
{
name: 'Management',
link: '/management'
}
];