import { denormalizePiece, pieceCache } from "@/cache"; import { client } from "@/client"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { useCache } from "@/hooks/useCache"; import { useLoading } from "@/hooks/useLoading"; import { Updater } from "@/hooks/useStore"; import { authors, created, DEBOUNCE, modified, SAVE_DELAY } from "@/snippets"; import clsx from "clsx"; import { PieceId } from "common"; import * as Body from "common/Body"; import { getMediaTypeForFilename } from "common/MediaType"; import { Array, Cause, Effect, Fiber, Iterable, Match, Option, Order, pipe, Predicate, Scope, SortedMap } from "effect"; import { Import, Loader2, Plus } from "lucide-react"; import { DragEventHandler, FormEventHandler, useId, useRef, useState } from "react"; import { Link, useNavigate } from "react-router-dom"; export function Pieces() { const [name, setName] = useState(""); const [author, setAuthor] = useState(""); const [importDialogOpen, setImportDialogOpen] = useState(false); const debounce = useRef(Effect.void); const { isLoading, error, data: pieceIds } = useLoading(Effect.gen(function* () { yield* debounce.current; const data = yield* client.queryPieces({ name: name !== "" ? Option.some(name) : Option.none(), author: author !== "" ? Option.some(author) : Option.none(), offset: 0, limit: 100, }); return data; }), [name, author]); return (
{ setName(e.target.value); debounce.current = DEBOUNCE; }} /> { setAuthor(e.target.value); debounce.current = DEBOUNCE; }} />
Tytuł Twórcy Dodano Zmodyfikowano {isLoading ? (
Ładowanie…
) : error !== null ? ( {Cause.isUnknownException(error) ? "Wystąpił nieznany błąd" : `Wystąpił błąd: ${JSON.stringify(error)}`} ) : ( pieceIds.map((pieceId) => ) )}
); } namespace PieceRow { export interface Props { readonly pieceId: PieceId; } } function PieceRow(props: PieceRow.Props) { const { isLoading, error, data: piece } = useCache(pieceCache, props.pieceId); if (isLoading) { return ( Ładowanie… ); } if (error !== null) { return ( Wystąpił błąd: {Match.value(error).pipe( Match.tag("FetchError", () => "Nie można połączyć się z serwerem"), Match.tag("NotFound", () => "Utwór nie istnieje"), Match.tag("Unauthenticated", () => "Zaloguj się, aby kontynuować"), Match.tag("Unauthorized", () => "Nie posiadasz uprawnień"), Match.exhaustive, )} ); } return ( {piece.name} {authors(piece)} {created(piece)} {modified(piece)} ); } function AddPieceDialogContent() { const navigate = useNavigate(); const [name, setName] = useState(""); const [composer, setComposer] = useState(""); const [lyricist, setLyricist] = useState(""); const [arranger, setArranger] = useState(""); const nameId = useId(); const composerId = useId(); const lyricistId = useId(); const arrangerId = useId(); const [isLoading, setIsLoading] = useState(false); const onSubmit: FormEventHandler = (e) => Effect.gen(function* () { e.preventDefault(); yield* Effect.scopedWith((scope) => Effect.gen(function* () { yield* Scope.addFinalizer(scope, Effect.sync(() => setIsLoading(false))); setIsLoading(true); const { pieceId } = yield* pipe( client.createPiece({ name, composer: composer.length > 0 ? Option.some(composer) : Option.none(), lyricist: lyricist.length > 0 ? Option.some(lyricist) : Option.none(), arranger: arranger.length > 0 ? Option.some(arranger) : Option.none(), }), Effect.flatMap(denormalizePiece), Effect.tap((piece) => Effect.sync(() => pieceCache.setFulfilledSucceed(piece.pieceId, piece))), ); navigate(pieceId); })); }).pipe(Effect.runPromise); return (
Dodaj utwór
setName(e.target.value)} /> setComposer(e.target.value)} /> setLyricist(e.target.value)} /> setArranger(e.target.value)} />
); } namespace ImportPiecesDialogContent { export interface Props { readonly setDialogOpen: Updater; } } function ImportPiecesDialogContent(props: ImportPiecesDialogContent.Props) { const [isLoading, setIsLoading] = useState(false); const [files, setFiles] = useState>(() => SortedMap.empty(Order.string)); const onDragOver: DragEventHandler = (e) => { e.preventDefault(); e.dataTransfer.dropEffect = "copy"; }; const onDrop: DragEventHandler = (e) => Effect.gen(function* () { e.preventDefault(); setFiles((value) => { for (const file of e.dataTransfer.files) { const mediaType = getMediaTypeForFilename(file.name); if (mediaType === undefined) { continue; } const name = file.name.substring(0, file.name.lastIndexOf(".")); const entry = pipe( value, SortedMap.get(name), Option.getOrElse(Array.empty), Array.append(file), ); value = SortedMap.set(value, name, entry); } return value; }); }).pipe(Effect.runPromise); const onSubmit: FormEventHandler = (e) => Effect.gen(function* () { e.preventDefault(); if (isLoading) { return; } const delay = yield* Effect.fork(SAVE_DELAY); yield* Effect.scopedWith((scope) => Effect.gen(function* () { yield* Scope.addFinalizer(scope, Effect.gen(function* () { yield* Fiber.join(delay); setIsLoading(false); })); setIsLoading(true); yield* pipe( files, Iterable.map(([name, files]) => Effect.gen(function* () { const piece = yield* pipe( client.createPiece({ name, composer: Option.none(), arranger: Option.none(), lyricist: Option.none(), }), ); const attachments = yield* pipe( files, Array.map((file) => Effect.gen(function* () { const mediaType = getMediaTypeForFilename(file.name); if (mediaType === undefined) { return; } // NOTE Apparently, file.bytes is not a thing in this context const data = new Uint8Array(yield* Body.arrayBuffer(file)); const attachment = yield* client.createAttachment({ pieceId: piece.pieceId, data, filename: file.name, mediaType, }); return attachment; })), Effect.allWith({ concurrency: "unbounded" }), Effect.map(Array.filter(Predicate.isNotUndefined)), ); yield* pipe( { ...piece, attachments }, denormalizePiece, Effect.tap((piece) => Effect.sync(() => pieceCache.setFulfilledSucceed(piece.pieceId, piece))), ); })), Effect.allWith({ concurrency: "unbounded" }), ); props.setDialogOpen(false); })); }).pipe(Effect.runPromise); return (
Importuj utwory
{SortedMap.size(files) > 0 && (
Tytuł Liczba plików {pipe( files, SortedMap.entries, Iterable.map(([name, files]) => ( {name} {files.length} )), )}
)}
Przeciągnij i upuść tutaj załączniki
); }