WIP. A little custom music server
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: migrate audio player

+260 -1
+5
bun.lock
··· 94 94 "dependencies": { 95 95 "@elysiajs/eden": "^1.4.4", 96 96 "@modelcontextprotocol/sdk": "^1.17.0", 97 + "@radix-ui/react-slider": "^1.3.6", 98 + "@radix-ui/react-slot": "^1.2.4", 97 99 "@tailwindcss/vite": "^4.0.6", 98 100 "@tanstack/react-devtools": "^0.7.0", 99 101 "@tanstack/react-query": "^5.66.5", ··· 105 107 "@tanstack/router-plugin": "^1.132.0", 106 108 "class-variance-authority": "^0.7.1", 107 109 "clsx": "^2.1.1", 110 + "date-fns": "^4.1.0", 108 111 "effect": "^3.19.0", 109 112 "lucide-react": "^0.544.0", 110 113 "react": "^19.2.0", ··· 1945 1948 "waku/@vitejs/plugin-react": ["@vitejs/plugin-react@5.0.2", "", { "dependencies": { "@babel/core": "^7.28.3", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.34", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-tmyFgixPZCx2+e6VO9TNITWcCQl8+Nl/E8YbAyPVv85QCc7/A3JrdfG2A8gIzvVhWuzMOVrFW1aReaNxrI6tbw=="], 1946 1949 1947 1950 "waku/vite": ["vite@7.1.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ=="], 1951 + 1952 + "web-tanstack/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], 1948 1953 1949 1954 "web-tanstack/@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], 1950 1955
+3
web-tanstack/package.json
··· 14 14 "dependencies": { 15 15 "@elysiajs/eden": "^1.4.4", 16 16 "@modelcontextprotocol/sdk": "^1.17.0", 17 + "@radix-ui/react-slider": "^1.3.6", 18 + "@radix-ui/react-slot": "^1.2.4", 17 19 "@tailwindcss/vite": "^4.0.6", 18 20 "@tanstack/react-devtools": "^0.7.0", 19 21 "@tanstack/react-query": "^5.66.5", ··· 25 27 "@tanstack/router-plugin": "^1.132.0", 26 28 "class-variance-authority": "^0.7.1", 27 29 "clsx": "^2.1.1", 30 + "date-fns": "^4.1.0", 28 31 "effect": "^3.19.0", 29 32 "lucide-react": "^0.544.0", 30 33 "react": "^19.2.0",
+64
web-tanstack/src/components/player/audio-player.tsx
··· 1 + "use client"; 2 + 3 + import { useEffect, useRef } from "react"; 4 + import { usePlayerStore } from "@/store/player"; 5 + import { Controls } from "./controls"; 6 + 7 + function usePlayer() { 8 + const audioRef = useRef<HTMLAudioElement | null>(null); 9 + 10 + const currentTrack = usePlayerStore((state) => state.currentTrack); 11 + const isPlaying = usePlayerStore((state) => state.isPlaying); 12 + const volume = usePlayerStore((state) => state.volume); 13 + const play = usePlayerStore((state) => state.play); 14 + const pause = usePlayerStore((state) => state.pause); 15 + 16 + const link = currentTrack ? `http://localhost:3003/file/${currentTrack}` : ""; 17 + const player = audioRef.current; 18 + 19 + useEffect(() => { 20 + if (!player) { 21 + return; 22 + } 23 + 24 + player.volume = volume; 25 + 26 + if (isPlaying) { 27 + player.play(); 28 + } else { 29 + player.pause(); 30 + } 31 + }, [audioRef, currentTrack, isPlaying]); 32 + 33 + useEffect(() => { 34 + if (!player) { 35 + return; 36 + } 37 + player.volume = volume; 38 + }, [audioRef, volume]); 39 + 40 + const onPause = () => { 41 + pause(); 42 + }; 43 + const onPlay = () => { 44 + play(); 45 + }; 46 + 47 + return { 48 + ref: audioRef, 49 + src: link, 50 + onPause, 51 + onPlay, 52 + }; 53 + } 54 + 55 + export function AudioPlayer() { 56 + const { ref, src, onPause, onPlay } = usePlayer(); 57 + 58 + return ( 59 + <> 60 + <Controls /> 61 + <audio ref={ref} src={src} onPause={onPause} onPlay={onPlay}></audio> 62 + </> 63 + ); 64 + }
+65
web-tanstack/src/components/player/controls.tsx
··· 1 + import { Pause, Play } from "lucide-react"; 2 + import { Slider } from "@/components/ui/slider"; 3 + import { Button } from "@/components/ui/button"; 4 + import { formatDate } from "date-fns"; 5 + import { usePlayerStore, getVolumePercent } from "@/store/player"; 6 + 7 + export function Controls() { 8 + const currentTrack = usePlayerStore((state) => state.currentTrack); 9 + const isPlaying = usePlayerStore((state) => state.isPlaying); 10 + const volume = usePlayerStore((state) => state.volume); 11 + const togglePlayPause = usePlayerStore((state) => state.togglePlayPause); 12 + const setVolumePercent = usePlayerStore((state) => state.setVolumePercent); 13 + 14 + const volumePercent = getVolumePercent(volume); 15 + 16 + // Hardcoded duration values (unchanged from original) 17 + const maxDurationMs = (12 * 60 + 47) * 1000; 18 + const curDurationMs = (0 * 60 + 0) * 1000; 19 + 20 + if (!currentTrack) { 21 + return null; 22 + } 23 + 24 + return ( 25 + <div className="fixed h-fit py-6 bg-white border border-black bottom-0 left-0 right-0 w-full grid place-items-center gap-4 z-10"> 26 + <div className="grid place-items-center grid-rows-1 grid-cols-[1fr_auto_1fr] gap-x-10 w-full"> 27 + <div></div> 28 + <div> 29 + <Button 30 + onClick={togglePlayPause} 31 + size="icon" 32 + variant="outline" 33 + className="aspect-square border-2 border-primary text-primary hover:text-primary p-3" 34 + > 35 + {isPlaying ? <Pause /> : <Play />} 36 + </Button> 37 + </div> 38 + 39 + {/* right */} 40 + <div className="w-full flex flex-row items-center gap-5"> 41 + <Slider 42 + defaultValue={[volumePercent]} 43 + min={0} 44 + max={100} 45 + step={0.01} 46 + className="max-w-[200px]" 47 + onValueChange={([value]) => value && setVolumePercent(value)} 48 + /> 49 + {/* 50 + *<pre>{JSON.stringify({ volumePercent, volumeReal }, null, 2)}</pre> 51 + */} 52 + </div> 53 + </div> 54 + <div className="w-full max-w-sm flex flex-row items-center gap-4 text-xs text-black/70"> 55 + <div>{formatTime(curDurationMs)}</div> 56 + <Slider /> 57 + <div>{formatTime(maxDurationMs)}</div> 58 + </div> 59 + </div> 60 + ); 61 + } 62 + 63 + function formatTime(ms: number): string { 64 + return formatDate(new Date(ms), "m:ss"); 65 + }
+60
web-tanstack/src/components/ui/button.tsx
··· 1 + import * as React from "react" 2 + import { Slot } from "@radix-ui/react-slot" 3 + import { cva, type VariantProps } from "class-variance-authority" 4 + 5 + import { cn } from "@/lib/utils" 6 + 7 + const buttonVariants = cva( 8 + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 9 + { 10 + variants: { 11 + variant: { 12 + default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 + destructive: 14 + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 15 + outline: 16 + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", 17 + secondary: 18 + "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 + ghost: 20 + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", 21 + link: "text-primary underline-offset-4 hover:underline", 22 + }, 23 + size: { 24 + default: "h-9 px-4 py-2 has-[>svg]:px-3", 25 + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", 26 + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 27 + icon: "size-9", 28 + "icon-sm": "size-8", 29 + "icon-lg": "size-10", 30 + }, 31 + }, 32 + defaultVariants: { 33 + variant: "default", 34 + size: "default", 35 + }, 36 + } 37 + ) 38 + 39 + function Button({ 40 + className, 41 + variant, 42 + size, 43 + asChild = false, 44 + ...props 45 + }: React.ComponentProps<"button"> & 46 + VariantProps<typeof buttonVariants> & { 47 + asChild?: boolean 48 + }) { 49 + const Comp = asChild ? Slot : "button" 50 + 51 + return ( 52 + <Comp 53 + data-slot="button" 54 + className={cn(buttonVariants({ variant, size, className }))} 55 + {...props} 56 + /> 57 + ) 58 + } 59 + 60 + export { Button, buttonVariants }
+61
web-tanstack/src/components/ui/slider.tsx
··· 1 + import * as React from "react" 2 + import * as SliderPrimitive from "@radix-ui/react-slider" 3 + 4 + import { cn } from "@/lib/utils" 5 + 6 + function Slider({ 7 + className, 8 + defaultValue, 9 + value, 10 + min = 0, 11 + max = 100, 12 + ...props 13 + }: React.ComponentProps<typeof SliderPrimitive.Root>) { 14 + const _values = React.useMemo( 15 + () => 16 + Array.isArray(value) 17 + ? value 18 + : Array.isArray(defaultValue) 19 + ? defaultValue 20 + : [min, max], 21 + [value, defaultValue, min, max] 22 + ) 23 + 24 + return ( 25 + <SliderPrimitive.Root 26 + data-slot="slider" 27 + defaultValue={defaultValue} 28 + value={value} 29 + min={min} 30 + max={max} 31 + className={cn( 32 + "relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col", 33 + className 34 + )} 35 + {...props} 36 + > 37 + <SliderPrimitive.Track 38 + data-slot="slider-track" 39 + className={cn( 40 + "bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5" 41 + )} 42 + > 43 + <SliderPrimitive.Range 44 + data-slot="slider-range" 45 + className={cn( 46 + "bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full" 47 + )} 48 + /> 49 + </SliderPrimitive.Track> 50 + {Array.from({ length: _values.length }, (_, index) => ( 51 + <SliderPrimitive.Thumb 52 + data-slot="slider-thumb" 53 + key={index} 54 + className="border-primary ring-ring/50 block size-4 shrink-0 rounded-full border bg-white shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50" 55 + /> 56 + ))} 57 + </SliderPrimitive.Root> 58 + ) 59 + } 60 + 61 + export { Slider }
+2 -1
web-tanstack/src/routes/__root.tsx
··· 9 9 import appCss from "../styles.css?url"; 10 10 11 11 import type { QueryClient } from "@tanstack/react-query"; 12 + import { AudioPlayer } from "@/components/player/audio-player"; 12 13 13 14 interface MyRouterContext { 14 15 queryClient: QueryClient; ··· 46 47 <HeadContent /> 47 48 </head> 48 49 <body> 49 - <Header /> 50 + <AudioPlayer /> 50 51 {children} 51 52 <TanStackDevtools 52 53 config={{