This is my personal website
1
fork

Configure Feed

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

add nowplaying progressbar

+180 -55
+1
deno.json
··· 23 23 "imports": { 24 24 "$fresh/": "https://deno.land/x/fresh@1.7.3/", 25 25 "dayjs": "npm:dayjs@^1.11.13", 26 + "lodash": "npm:lodash@^4.17.21", 26 27 "preact": "https://esm.sh/preact@10.22.0", 27 28 "preact/": "https://esm.sh/preact@10.22.0/", 28 29 "@preact/signals": "https://esm.sh/*@preact/signals@1.2.2",
+2
fresh.gen.ts
··· 8 8 import * as $greet_name_ from "./routes/greet/[name].tsx"; 9 9 import * as $index from "./routes/index.tsx"; 10 10 import * as $NowPlaying from "./islands/NowPlaying.tsx"; 11 + import * as $Progressbar from "./islands/Progressbar.tsx"; 11 12 import type { Manifest } from "$fresh/server.ts"; 12 13 13 14 const manifest = { ··· 20 21 }, 21 22 islands: { 22 23 "./islands/NowPlaying.tsx": $NowPlaying, 24 + "./islands/Progressbar.tsx": $Progressbar, 23 25 }, 24 26 baseUrl: import.meta.url, 25 27 } satisfies Manifest;
+15
hooks/useFormat.tsx
··· 1 + export const useTimeFormat = () => { 2 + const formatTime = (millis: number) => { 3 + let minutes = Math.floor(millis / 60000); 4 + const seconds = ((millis % 60000) / 1000).toFixed(0); 5 + const secondsDisplay = seconds.length === 1 ? `0${seconds}` : seconds; 6 + 7 + if (seconds === "60") { 8 + minutes += 1; 9 + return `${minutes < 10 ? `0${minutes}` : minutes}:00`; 10 + } else { 11 + return `${minutes < 10 ? `0${minutes}` : minutes}:${secondsDisplay}`; 12 + } 13 + }; 14 + return { formatTime }; 15 + };
+144 -55
islands/NowPlaying.tsx
··· 1 - import { useEffect, useState } from "preact/hooks"; 1 + import _ from "lodash"; 2 + import { useCallback, useEffect, useRef, useState } from "preact/hooks"; 3 + import { useTimeFormat } from "../hooks/useFormat.tsx"; 4 + import Progressbar from "./Progressbar.tsx"; 2 5 3 6 const did = "did:plc:7vdlgi2bflelz7mmuxoqjfcr"; 4 7 5 8 function NowPlaying() { 9 + const progressInterval = useRef<number | null>(null); 10 + const [progress, setProgress] = useState(0); 11 + const { formatTime } = useTimeFormat(); 6 12 const [label, setLabel] = useState("Now Playing"); 13 + const [isPlaying, setIsPlaying] = useState(false); 7 14 const [song, setSong] = useState< 8 15 { 9 16 title: string; ··· 12 19 albumUri?: string | null; 13 20 songUri?: string | null; 14 21 artistUri?: string | null; 22 + duration?: number; 23 + progress?: number; 24 + isPlaying?: boolean; 15 25 } | null 16 26 >(null); 27 + const songRef = useRef(song); 17 28 18 29 const fetchNowPlaying = async () => { 19 30 const response = await fetch( ··· 33 44 const data = await response.json(); 34 45 35 46 if (Object.keys(data).length === 0) { 47 + setIsPlaying(false); 36 48 await fetchLastPlayedSong(); 37 49 return; 38 50 } 51 + 52 + setIsPlaying(true); 39 53 40 54 setSong({ 41 55 title: data.item.name, 42 56 artist: data.item.artists.map((artist: { name: string }) => artist.name) 43 57 .join(", "), 44 - albumArt: data.item.album.images[0].url, 58 + duration: data.item.duration_ms, 59 + progress: data.progress_ms, 60 + albumArt: _.get(data, "item.album.images.0.url"), 45 61 albumUri: data.albumUri 46 62 ? `https://rocksky.app/${data.albumUri.split("at://")[1]}` 47 63 : null, ··· 51 67 artistUri: data.artistUri 52 68 ? `https://rocksky.app/${data.artistUri.split("at://")[1]}` 53 69 : null, 70 + isPlaying: data.is_playing, 54 71 }); 55 72 }; 56 73 ··· 78 95 }); 79 96 }; 80 97 98 + const startProgressTracking = useCallback(() => { 99 + if (progressInterval.current) { 100 + clearInterval(progressInterval.current); 101 + } 102 + 103 + progressInterval.current = setInterval(() => { 104 + if ( 105 + songRef.current && songRef.current.progress && songRef.current.duration 106 + ) { 107 + setProgress(Math.floor( 108 + (songRef.current.progress / songRef.current.duration) * 100, 109 + )); 110 + setSong((prevSong) => { 111 + if ( 112 + prevSong && 113 + _.get(prevSong, "progress", 0) >= _.get(prevSong, "duration", 0) 114 + ) { 115 + return { 116 + ...prevSong, 117 + progress: 0, 118 + }; 119 + } 120 + 121 + if (prevSong?.isPlaying) { 122 + return { 123 + ...prevSong!, 124 + progress: (songRef.current?.progress || 0) + 100, 125 + }; 126 + } 127 + return prevSong; 128 + }); 129 + } 130 + }, 100); 131 + 132 + return () => { 133 + if (progressInterval.current) { 134 + clearInterval(progressInterval.current); 135 + } 136 + }; 137 + }, []); 138 + 81 139 useEffect(() => { 82 140 const interval = setInterval(() => { 83 141 fetchNowPlaying(); ··· 86 144 return () => clearInterval(interval); 87 145 }, []); 88 146 147 + useEffect(() => { 148 + startProgressTracking(); 149 + 150 + return () => { 151 + if (progressInterval.current) { 152 + clearInterval(progressInterval.current); 153 + } 154 + }; 155 + // eslint-disable-next-line react-hooks/exhaustive-deps 156 + }, []); 157 + 158 + useEffect(() => { 159 + songRef.current = song; 160 + }, [song]); 161 + 89 162 return ( 90 163 <div> 91 164 {song && ( 92 - <div class="flex flex-row"> 93 - {song?.albumUri && ( 94 - <a href={song.albumUri} target="_blank" class="mr-[20px]"> 95 - <div class="max-w-[96px] max-h-[96px]"> 165 + <> 166 + <div class="flex flex-row"> 167 + {song?.albumUri && ( 168 + <a href={song.albumUri} target="_blank" class="mr-[20px]"> 169 + <div class="max-w-[96px] max-h-[96px]"> 170 + <img 171 + class="w-[96px] h-[96px] rounded-[10px]" 172 + src={song?.albumArt!} 173 + /> 174 + </div> 175 + </a> 176 + )} 177 + {!song?.albumUri && ( 178 + <div class="max-w-[96px] max-h-[96px] mr-[20px]"> 96 179 <img 97 - class="w-[96px] h-[96px] rounded-[10px]" 180 + class="w-[96px] h-[96px] rounded-[10px] mr-[20px]" 98 181 src={song?.albumArt!} 99 182 /> 100 183 </div> 101 - </a> 102 - )} 103 - {!song?.albumUri && ( 104 - <div class="max-w-[96px] max-h-[96px] mr-[20px]"> 105 - <img 106 - class="w-[96px] h-[96px] rounded-[10px] mr-[20px]" 107 - src={song?.albumArt!} 108 - /> 109 - </div> 110 - )} 111 - <div> 112 - <p class="text-[16px] text-[rgb(109,109,156)]"> 113 - {label} 114 - {" on "} 115 - <a 116 - href={`https://rocksky.app/profile/tsiry-sandratraina.com`} 117 - target="_blank" 118 - > 119 - <b>Rocksky</b> 120 - </a> 121 - </p> 184 + )} 122 185 <div> 123 - {song?.songUri && ( 186 + <p class="text-[16px] text-[rgb(109,109,156)]"> 187 + {label} 188 + {" on "} 124 189 <a 125 - href={song?.songUri} 126 - class="text-[20px] line-clamp-1 overflow-hidden text-ellipsis max-w-[240px] md:max-w-[630px]" 190 + href={`https://rocksky.app/profile/tsiry-sandratraina.com`} 127 191 target="_blank" 128 192 > 129 - {song?.title} 193 + <b>Rocksky</b> 130 194 </a> 131 - )} 132 - {!song?.songUri && ( 133 - <p class="text-[20px] line-clamp-1 overflow-hidden text-ellipsis max-w-[240px] md:max-w-[630px]"> 134 - {song?.title} 135 - </p> 136 - )} 195 + </p> 196 + <div> 197 + {song?.songUri && ( 198 + <a 199 + href={song?.songUri} 200 + class="text-[20px] line-clamp-1 overflow-hidden text-ellipsis max-w-[240px] md:max-w-[630px]" 201 + target="_blank" 202 + > 203 + {song?.title} 204 + </a> 205 + )} 206 + {!song?.songUri && ( 207 + <p class="text-[20px] line-clamp-1 overflow-hidden text-ellipsis max-w-[240px] md:max-w-[630px]"> 208 + {song?.title} 209 + </p> 210 + )} 211 + </div> 212 + <div> 213 + {song?.artistUri && ( 214 + <a 215 + href={song?.artistUri} 216 + class="line-clamp-1 overflow-hidden text-ellipsis max-w-[240px] md:max-w-[630px]" 217 + target="_blank" 218 + > 219 + {song?.artist} 220 + </a> 221 + )} 222 + {!song?.artistUri && ( 223 + <p class="line-clamp-1 overflow-hidden text-ellipsis max-w-[240px] md:max-w-[630px]"> 224 + {song?.artist} 225 + </p> 226 + )} 227 + </div> 137 228 </div> 138 - <div> 139 - {song?.artistUri && ( 140 - <a 141 - href={song?.artistUri} 142 - class="line-clamp-1 overflow-hidden text-ellipsis max-w-[240px] md:max-w-[630px]" 143 - target="_blank" 144 - > 145 - {song?.artist} 146 - </a> 147 - )} 148 - {!song?.artistUri && ( 149 - <p class="line-clamp-1 overflow-hidden text-ellipsis max-w-[240px] md:max-w-[630px]"> 150 - {song?.artist} 151 - </p> 152 - )} 229 + </div> 230 + {isPlaying && ( 231 + <div class="flex flex-row justify-between mt-[15px]"> 232 + <span class="text-[rgb(109,109,156)] text-[16px]"> 233 + {formatTime(song?.progress || 0)} 234 + </span> 235 + <Progressbar 236 + progress={progress} 237 + class="mt-[10px] ml-[10px] mr-[10px]" 238 + /> 239 + <span class="text-[rgb(109,109,156)] text-[16px]"> 240 + {formatTime(song?.duration || 0)} 241 + </span> 153 242 </div> 154 - </div> 155 - </div> 243 + )} 244 + </> 156 245 )} 157 246 </div> 158 247 );
+18
islands/Progressbar.tsx
··· 1 + function Progressbar(props: { 2 + progress: number; 3 + class?: string; 4 + }) { 5 + return ( 6 + <div 7 + class={`w-full h-[4px] bg-[rgba(109,109,156,0.3)] rounded-[10px] ${props.class}`} 8 + > 9 + <div 10 + class="h-full bg-[#ff5dae] rounded-[10px] transition-all duration-300 ease-in-out" 11 + style={{ width: `${props.progress}%` }} 12 + > 13 + </div> 14 + </div> 15 + ); 16 + } 17 + 18 + export default Progressbar;