Personal Site

Re-add all the old SSR NowPlaying content (with slight tweaks as required)

TODO:
- reimplement new /now-playing-sse
- reimplement update functionality

vielle.dev 24a10bee 862b22fa

verified
+343 -1
+343 -1
src/components/home/playing/NowPlaying.astro
··· 1 1 --- 2 - import "./spotify" 2 + import { sdk } from "./spotify"; 3 3 4 + import mp3Base from "/assets/mp3/base.png"; 5 + import mp3AlbumArtMask from "/assets/mp3/album-art-mask.png"; 6 + import mp3RecordCircle from "/assets/mp3/record-circle.png"; 7 + import mp3HeadCircle from "/assets/mp3/head-circle.png"; 8 + import mp3PlaybackHead from "/assets/mp3/playback-head.png"; 9 + import boxTlbr from "/assets/box-tlbr.png"; 10 + import popoutSpeech from "/assets/popout-speech.png"; 11 + import smallBoxMask from "/assets/small-box-mask.png"; 12 + import type { PlaybackState, Track } from "@spotify/web-api-ts-sdk"; 13 + 14 + const track = await sdk.player.getCurrentlyPlayingTrack(); 15 + 16 + const isSong = ( 17 + item: PlaybackState | null, 18 + ): item is PlaybackState & { item: Track } => !!item && "album" in item.item; 4 19 --- 20 + 21 + <section 22 + class="playing" 23 + id="now-playing" 24 + style={` 25 + --mp3-base-png: url(${mp3Base.src}); 26 + --mp3-album-art-mask-png: url(${mp3AlbumArtMask.src}); 27 + --mp3-record-circle-png: url(${mp3RecordCircle.src}); 28 + --mp3-head-circle-png: url(${mp3HeadCircle.src}); 29 + --mp3-playback-head-png: url(${mp3PlaybackHead.src}); 30 + --box-tlbr-png: url("${boxTlbr.src}"); 31 + --popout-speech-png: url("${popoutSpeech.src}"); 32 + --small-box-mask-png: url("${smallBoxMask.src}"); 33 + `} 34 + > 35 + <div 36 + class="player" 37 + tabindex="0" 38 + aria-label="Record player" 39 + data-playing={isSong(track) ? "true" : "false"} 40 + > 41 + <div class="spinner"></div> 42 + 43 + <div class="record"> 44 + <img 45 + src={!isSong(track) 46 + ? "https://undefined/" 47 + : track.item.album.images[0].url} 48 + alt="" 49 + class="art" 50 + /> 51 + </div> 52 + 53 + <div class="spinner hidden"> 54 + <div class="head"></div> 55 + </div> 56 + 57 + <!-- aria-live=off means aria updates are only announced when focused. 58 + this makes sense as what im listening too is irrelevant if --> 59 + <now-playing data-render={isSong(track)} aria-live="off"> 60 + <a 61 + slot="title" 62 + href={isSong(track) ? track.item.external_urls.spotify : "#"} 63 + > 64 + {isSong(track) ? track.item.name : null} 65 + </a> 66 + <span slot="album">{isSong(track) ? track.item.album.name : null}</span> 67 + <span slot="artists"> 68 + { 69 + isSong(track) ? ( 70 + track.item.artists 71 + .map((artist) => ( 72 + <a href={artist.external_urls.spotify}>{artist.name}</a> 73 + )) 74 + // inject a comma before each entry in the list except the first one 75 + // flatmap flattens the returned array into the new map 76 + .flatMap((x, i) => (i === 0 ? x : [", ", x])) 77 + ) : ( 78 + // artist defined by default because 79 + // i cant be bothered to do client error handling 80 + // and this is easier 81 + <a href="#">Artist Name</a> 82 + ) 83 + } 84 + </span> 85 + <img 86 + slot="art" 87 + src={!isSong(track) 88 + ? "https://undefined/" 89 + : track.item.album.images[0].url} 90 + alt="" 91 + /> 92 + 93 + <template shadowrootmode="open" shadowrootdelegatesfocus> 94 + <div class="layout"> 95 + <span class="name"> 96 + <slot is:inline name="title" /> 97 + (<slot is:inline name="album" />) 98 + </span> 99 + <span class="artists"> 100 + <slot is:inline name="artists" /> 101 + </span> 102 + <div class="art"> 103 + <slot is:inline name="art" /> 104 + </div> 105 + </div> 106 + 107 + <style> 108 + /* dont show element if it errored or nothing is playing */ 109 + :host([data-render="false"]) { 110 + display: none !important; 111 + } 112 + 113 + :host { 114 + contain: layout; 115 + position: absolute; 116 + bottom: 105cqh; 117 + 118 + width: 100cqw; 119 + height: auto; 120 + 121 + box-sizing: border-box; 122 + /* gets overridden by the fukcin * selector. for some reason */ 123 + padding: calc((25 / 3) * 1cqw) !important; 124 + 125 + border-image: var(--box-tlbr-png) 10 fill / calc((20 / 3) * 1cqw) 126 + round; 127 + 128 + &::after { 129 + content: ""; 130 + 131 + position: absolute; 132 + bottom: calc((-20 / 3) * 1cqw); 133 + left: 0; 134 + 135 + /* width 100% catches :hover to stop it disapearing. also makes centering less magic numbery */ 136 + width: 100cqw; 137 + height: calc((40 / 3) * 1cqw); 138 + 139 + background-image: var(--popout-speech-png); 140 + background-size: contain; 141 + background-position: center; 142 + background-repeat: no-repeat; 143 + } 144 + } 145 + 146 + .layout { 147 + display: grid; 148 + grid-template: 149 + "name art" auto 150 + "artists art" auto 151 + / 1fr 80px; 152 + align-items: center; 153 + justify-content: center; 154 + 155 + .name { 156 + grid-area: name; 157 + align-self: end; 158 + } 159 + 160 + .artists { 161 + grid-area: artists; 162 + align-self: start; 163 + } 164 + 165 + .art { 166 + grid-area: art; 167 + width: 80px; 168 + height: 80ps; 169 + } 170 + } 171 + </style> 172 + </template> 173 + </now-playing> 174 + </div> 175 + </section> 176 + 177 + <style> 178 + @keyframes spin { 179 + from { 180 + rotate: 0deg; 181 + } 182 + 183 + to { 184 + rotate: 360deg; 185 + } 186 + } 187 + 188 + @keyframes head-move { 189 + from, 190 + 80%, 191 + to { 192 + rotate: 0deg; 193 + } 194 + 195 + 2.5% { 196 + rotate: 25deg; 197 + } 198 + 199 + 70%, 200 + 75% { 201 + rotate: 45deg; 202 + } 203 + } 204 + 205 + .player { 206 + /* internal and external nodes dont affect each other 207 + children dont affect this size 208 + properties dont affect external nodes */ 209 + contain: layout size style; 210 + container: player / size; 211 + width: 100%; 212 + /* design size is 300px by 244px 213 + treat 1cqw = 3px. 214 + */ 215 + aspect-ratio: 300/244; 216 + 217 + image-rendering: pixelated; 218 + background-image: var(--mp3-base-png); 219 + background-size: contain; 220 + 221 + * { 222 + background-size: contain; 223 + } 224 + 225 + position: relative; 226 + 227 + & .record { 228 + position: absolute; 229 + top: calc((20 / 3) * 1cqw); 230 + left: calc((40 / 3) * 1cqw); 231 + 232 + width: calc((200 / 3) * 1cqw); 233 + height: calc((200 / 3) * 1cqw); 234 + background-image: var(--mp3-record-circle-png); 235 + 236 + animation: 30s linear forwards infinite spin; 237 + 238 + [data-playing="false"] & { 239 + animation-play-state: paused; 240 + } 241 + 242 + & .art { 243 + position: absolute; 244 + top: calc((50 / 3) * 1cqw); 245 + left: calc((50 / 3) * 1cqw); 246 + width: calc((100 / 3) * 1cqw); 247 + height: calc((100 / 3) * 1cqw); 248 + max-width: none; 249 + background-color: #008282; 250 + 251 + mask-image: var(--mp3-album-art-mask-png); 252 + mask-size: calc((100 / 3) * 1cqw) calc((100 / 3) * 1cqw); 253 + } 254 + } 255 + 256 + & .spinner { 257 + position: absolute; 258 + top: calc((30 / 3) * 1cqw); 259 + left: calc((214 / 3) * 1cqw); 260 + 261 + background-image: var(--mp3-head-circle-png); 262 + width: calc((60 / 3) * 1cqw); 263 + height: calc((60 / 3) * 1cqw); 264 + 265 + animation: 60s linear 2.5s infinite forwards head-move; 266 + 267 + [data-playing="false"] & { 268 + animation-play-state: paused; 269 + } 270 + 271 + &.hidden { 272 + background: none; 273 + } 274 + 275 + & .head { 276 + position: absolute; 277 + top: calc((-10 / 3) * 1cqw); 278 + left: calc((28 / 3) * 1cqw); 279 + width: calc((30 / 3) * 1cqw); 280 + height: calc((200 / 3) * 1cqw); 281 + background-image: var(--mp3-playback-head-png); 282 + } 283 + } 284 + } 285 + 286 + now-playing { 287 + display: none; 288 + } 289 + 290 + /* setup the ::before to be usable for the outline 291 + filter cannot be applied straight to .player 292 + as the now-playing is a child and also gets the outline 293 + which we dont want */ 294 + .player::before { 295 + content: ""; 296 + 297 + width: 100cqw; 298 + height: 100cqh; 299 + 300 + position: absolute; 301 + top: 0; 302 + left: 0; 303 + 304 + background-image: var(--mp3-base-png); 305 + background-size: contain; 306 + } 307 + 308 + .player:focus, 309 + .player:focus-within { 310 + outline: none; 311 + --outline-colour: #c274d1; 312 + --outline-size: 4px; 313 + 314 + &:focus::before { 315 + /* filter is used instead of a standard property 316 + as it means we can match it to the shape of the custom outline */ 317 + filter: drop-shadow(var(--outline-colour) 0 var(--outline-size)) 318 + drop-shadow(var(--outline-colour) var(--outline-size) 0) 319 + drop-shadow(var(--outline-colour) 0 calc(-1 * var(--outline-size))) 320 + drop-shadow(var(--outline-colour) calc(-1 * var(--outline-size)) 0); 321 + } 322 + 323 + & now-playing { 324 + display: block; 325 + } 326 + } 327 + 328 + now-playing { 329 + & img { 330 + mask-image: var(--small-box-mask-png); 331 + mask-size: contain; 332 + } 333 + & a { 334 + color: black; 335 + 336 + &:focus, 337 + &:hover { 338 + text-decoration-style: dashed; 339 + } 340 + 341 + &:active { 342 + text-decoration: none; 343 + } 344 + } 345 + } 346 + </style>