diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts new file mode 100644 index 0000000..cf3722d --- /dev/null +++ b/frontend/src/api/client.ts @@ -0,0 +1,59 @@ +/** Cienka warstwa nad REST API bota - jedno miejsce, w którym znane są adresy. */ + +import type { BotStatus, UploadedFile } from '@/types/status' + +export class ApiError extends Error {} + +async function request(path: string, init?: RequestInit): Promise { + const response = await fetch(path, init) + const payload = await response.json().catch(() => null) + + if (!response.ok) { + const detail = payload && typeof payload.detail === 'string' ? payload.detail : response.statusText + throw new ApiError(detail || 'Żądanie nie powiodło się') + } + return payload as T +} + +function post(path: string, body?: unknown): Promise { + return request(path, { + method: 'POST', + headers: body === undefined ? undefined : { 'Content-Type': 'application/json' }, + body: body === undefined ? undefined : JSON.stringify(body), + }) +} + +export const api = { + status: () => request('/api/status'), + + connect: (channelId: string) => post('/api/connect', { channel_id: channelId }), + disconnect: () => post('/api/disconnect'), + + play: (query: string) => post('/api/play', { query }), + playFile: (filename: string) => post('/api/play_file', { filename }), + pause: () => post('/api/pause'), + resume: () => post('/api/resume'), + skip: () => post('/api/skip'), + stop: () => post('/api/stop'), + setVolume: (volume: number) => post('/api/volume', { volume }), + cycleLoop: () => post<{ mode: 'off' | 'playlist' | 'single' }>('/api/loop_cycle'), + + jump: (index: number) => post('/api/jump', { index }), + remove: (id: string) => post('/api/remove', { id }), + reorder: (queueIds: string[]) => post('/api/reorder', { queue_ids: queueIds }), + setTrackPlays: (id: string, plays: number) => post('/api/queue/plays', { id, plays }), + + uploads: () => request<{ files: UploadedFile[] }>('/api/uploads'), + deleteUpload: (filename: string) => + request(`/api/uploads/${encodeURIComponent(filename)}`, { method: 'DELETE' }), + upload: (file: File) => { + const form = new FormData() + form.append('file', file) + return request<{ filename: string }>('/api/upload', { method: 'POST', body: form }) + }, + + setDefaultPlays: (defaultPlays: number) => + post('/api/settings/default_plays', { default_plays: defaultPlays }), + setLogChannel: (guildId: string, channelId: string) => + post('/api/settings/text_channel', { guild_id: guildId, channel_id: channelId }), +} diff --git a/frontend/src/types/status.ts b/frontend/src/types/status.ts new file mode 100644 index 0000000..6972543 --- /dev/null +++ b/frontend/src/types/status.ts @@ -0,0 +1,78 @@ +/** Kształt danych zwracanych przez /api/status oraz rozgłaszanych po WebSockecie. */ + +export interface VoiceChannel { + guild_id: string + guild_name: string + channel_id: string + channel_name: string +} + +export interface TextChannel { + channel_id: string + channel_name: string +} + +export interface GuildSettings { + guild_id: string + guild_name: string + text_channels: TextChannel[] + active_notification_channel_id: string | null +} + +export interface CurrentTrack { + id: string + title: string + url: string | null + duration: number | null + elapsed: number + plays_left: number +} + +export interface QueueTrack { + id: string + title: string + url: string | null + plays_left: number +} + +export interface HistoryTrack { + id: string + title: string + url: string | null + duration: number | null + is_local: boolean + filepath: string | null +} + +export interface UploadedFile { + filename: string + size_mb: number + mtime: number +} + +export interface BotStatus { + playing: boolean + paused: boolean + loop_single: boolean + loop_playlist: boolean + volume: number + current: CurrentTrack | null + queue: QueueTrack[] + queue_index: number + history: HistoryTrack[] + voice_channels: VoiceChannel[] + active_voice_channel: VoiceChannel | null + text_channels: VoiceChannel[] + active_text_channel_id: string | null + guilds: GuildSettings[] + default_plays: number +} + +export type LoopMode = 'off' | 'playlist' | 'single' + +export function loopModeOf(status: BotStatus | null): LoopMode { + if (!status) return 'off' + if (status.loop_single) return 'single' + if (status.loop_playlist) return 'playlist' + return 'off' +}