add websockets to get downloads list

This commit is contained in:
2026-02-20 22:54:26 +03:00
parent e87d02b1cb
commit de495c0f78
8 changed files with 121 additions and 43 deletions

View File

@@ -12,7 +12,8 @@
"dependencies": {
"@tanstack/react-query": "^5.90.21",
"react": "^19.2.0",
"react-dom": "^19.2.0"
"react-dom": "^19.2.0",
"react-hot-toast": "^2.6.0"
},
"devDependencies": {
"@eslint/js": "^9.39.1",

26
web/pnpm-lock.yaml generated
View File

@@ -20,6 +20,9 @@ importers:
react-dom:
specifier: ^19.2.0
version: 19.2.4(react@19.2.4)
react-hot-toast:
specifier: ^2.6.0
version: 2.6.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
devDependencies:
'@eslint/js':
specifier: ^9.39.1
@@ -742,6 +745,11 @@ packages:
resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==}
engines: {node: '>=18'}
goober@2.1.18:
resolution: {integrity: sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==}
peerDependencies:
csstype: ^3.0.10
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
@@ -975,6 +983,13 @@ packages:
peerDependencies:
react: ^19.2.4
react-hot-toast@2.6.0:
resolution: {integrity: sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==}
engines: {node: '>=10'}
peerDependencies:
react: '>=16'
react-dom: '>=16'
react-refresh@0.18.0:
resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==}
engines: {node: '>=0.10.0'}
@@ -1836,6 +1851,10 @@ snapshots:
globals@16.5.0: {}
goober@2.1.18(csstype@3.2.3):
dependencies:
csstype: 3.2.3
graceful-fs@4.2.11: {}
has-flag@4.0.0: {}
@@ -2015,6 +2034,13 @@ snapshots:
react: 19.2.4
scheduler: 0.27.0
react-hot-toast@2.6.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
csstype: 3.2.3
goober: 2.1.18(csstype@3.2.3)
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
react-refresh@0.18.0: {}
react@19.2.4: {}

View File

@@ -1,27 +1,62 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { useSearchQuery, type Album } from "./api/useSearchQuery";
import { useDebouncedValue } from "./hooks/useDebouncedValue";
import { getContrastYIQ } from "./getContrastYIQ";
import { useDownloadAlbumMutation } from "./api/useDownloadAlbumMutation";
import { useDownloadsQuery } from "./api/useDownloadsQuery";
import toast from "react-hot-toast";
type DownloadItem = {
id: number;
status: "ADDED" | "IN_PROGRESS" | "COMPLETED";
album: Album;
installedFiles: number;
totalFiles: number;
};
export default function App() {
const [search, setSearch] = useState("");
const debouncedSearch = useDebouncedValue(search);
const [downloads, setDownloads] = useState<DownloadItem[]>([]);
const { data } = useSearchQuery({
query: debouncedSearch,
});
const { data: downloads } = useDownloadsQuery();
const downloadAlbumMutation = useDownloadAlbumMutation();
useEffect(() => {
const ws = new WebSocket("/ws/downloads");
ws.onopen = () => {
console.log("ws connection opened");
};
ws.onclose = () => {
console.log("ws connection closed");
};
ws.onmessage = (e) => {
const data = JSON.parse(e.data) as { downloads: DownloadItem[] };
const items = [...data.downloads].sort((a, b) => {
if (a.status === "COMPLETED" && b.status !== "COMPLETED") {
return -1;
}
if (a.status !== "COMPLETED" && b.status === "COMPLETED") {
return 1;
}
return a.album.title.localeCompare(b.album.title);
});
setDownloads(items);
};
}, []);
const handleDownloadAlbum = (album: Album) => {
downloadAlbumMutation.mutate(
{ albumId: album.id },
{
onError(error) {
console.log({ error });
toast.error(error.message);
},
},
);
@@ -42,6 +77,7 @@ export default function App() {
/>
<div>
<p>{item.album.title}</p>
<p>Status: {formatDownloadStatus(item.status)}</p>
<p>
Downloaded {item.installedFiles}/{item.totalFiles}
</p>
@@ -102,3 +138,14 @@ export default function App() {
</div>
);
}
const formatDownloadStatus = (status: DownloadItem["status"]): string => {
switch (status) {
case "ADDED":
return "added";
case "IN_PROGRESS":
return "in progress";
case "COMPLETED":
return "completed";
}
};

View File

@@ -1,19 +0,0 @@
import { useQuery } from "@tanstack/react-query";
import type { Album } from "./useSearchQuery";
export type DownloadItem = {
id: number;
album: Album;
installedFiles: number;
totalFiles: number;
};
export const useDownloadsQuery = () => {
return useQuery({
queryKey: ["downloads"],
queryFn: async () => {
const resp = await fetch("/downloads");
return (await resp.json()) as DownloadItem[];
},
});
};

View File

@@ -3,6 +3,7 @@ import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App.tsx";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Toaster } from "react-hot-toast";
const queryClient = new QueryClient({
defaultOptions: {
@@ -15,6 +16,7 @@ const queryClient = new QueryClient({
createRoot(document.getElementById("root")!).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<Toaster />
<App />
</QueryClientProvider>
</StrictMode>,