your personal website on atproto - mirror blento.app

Merge pull request #230 from LooneyH/Rocksky

Rocksky

authored by

Florian and committed by
GitHub
ef479ff6 027245f0

+186
+2
src/lib/cards/index.ts
··· 23 23 import { GifCardDefinition } from './media/GIFCard'; 24 24 import { PopfeedReviewsCardDefinition } from './media/PopfeedReviews'; 25 25 import { TealFMPlaysCardDefinition } from './media/TealFMPlaysCard'; 26 + import { RockskyPlaysCardDefinition } from './media/RockskyPlaysCard'; 26 27 import { PhotoGalleryCardDefinition } from './media/PhotoGalleryCard'; 27 28 import { StandardSiteDocumentListCardDefinition } from './content/StandardSiteDocumentListCard'; 28 29 import { StatusphereCardDefinition } from './media/StatusphereCard'; ··· 81 82 GifCardDefinition, 82 83 PopfeedReviewsCardDefinition, 83 84 TealFMPlaysCardDefinition, 85 + RockskyPlaysCardDefinition, 84 86 PhotoGalleryCardDefinition, 85 87 StandardSiteDocumentListCardDefinition, 86 88 StatusphereCardDefinition,
+39
src/lib/cards/media/RockskyPlaysCard/AlbumArt.svelte
··· 1 + <script lang="ts"> 2 + let { albumArtUrl, alt }: { albumArtUrl?: string; alt: string } = $props(); 3 + 4 + let isLoading = $state(true); 5 + let hasError = $state(false); 6 + </script> 7 + 8 + {#if isLoading} 9 + <div class="bg-base-200 dark:bg-base-800 h-10 w-10 animate-pulse rounded-lg"></div> 10 + {/if} 11 + 12 + {#if hasError} 13 + <div 14 + class="bg-base-300 dark:bg-base-800 accent:bg-accent-700/50 flex h-10 w-10 items-center justify-center rounded-lg" 15 + > 16 + <svg 17 + class="text-base-400 dark:text-base-600 accent:text-accent-900 h-5 w-5" 18 + fill="currentColor" 19 + viewBox="0 0 20 20" 20 + > 21 + <path 22 + d="M18 3a1 1 0 00-1.196-.98l-10 2A1 1 0 006 5v9.114A4.369 4.369 0 005 14c-1.657 0-3 .895-3 2s1.343 2 3 2 3-.895 3-2V7.82l8-1.6v5.894A4.37 4.37 0 0015 12c-1.657 0-3 .895-3 2s1.343 2 3 2 3-.895 3-2V3z" 23 + /> 24 + </svg> 25 + </div> 26 + {:else} 27 + <img 28 + src="{albumArtUrl}" 29 + {alt} 30 + class="h-10 w-10 rounded-lg object-cover {isLoading && 'hidden'}" 31 + onload={() => { 32 + isLoading = false; 33 + }} 34 + onerror={() => { 35 + isLoading = false; 36 + hasError = true; 37 + }} 38 + /> 39 + {/if}
+114
src/lib/cards/media/RockskyPlaysCard/RockskyPlaysCard.svelte
··· 1 + <script lang="ts"> 2 + import type { Item } from '$lib/types'; 3 + import { onMount } from 'svelte'; 4 + import { getAdditionalUserData, getDidContext, getHandleContext } from '$lib/website/context'; 5 + import { CardDefinitionsByType } from '../..'; 6 + import AlbumArt from './AlbumArt.svelte'; 7 + import { RelativeTime } from '@foxui/time'; 8 + 9 + interface Artist { 10 + artist: string; 11 + } 12 + 13 + interface PlayValue { 14 + mbid?: string; 15 + title: string; 16 + createdAt?: string; 17 + artists?: Artist[]; 18 + albumArtUrl?: string; 19 + spotifyLink?: string; 20 + } 21 + 22 + interface Play { 23 + uri: string; 24 + value: PlayValue; 25 + } 26 + 27 + let { item }: { item: Item } = $props(); 28 + 29 + const data = getAdditionalUserData(); 30 + // svelte-ignore state_referenced_locally 31 + let feed = $state(data[item.cardType] as Play[] | undefined); 32 + 33 + let did = getDidContext(); 34 + let handle = getHandleContext(); 35 + 36 + onMount(async () => { 37 + if (feed) return; 38 + 39 + feed = (await CardDefinitionsByType[item.cardType]?.loadData?.([], { 40 + did, 41 + handle 42 + })) as Play[] | undefined; 43 + 44 + data[item.cardType] = feed; 45 + }); 46 + 47 + function isNumeric(str: string) { 48 + if (typeof str != 'string') return false; 49 + return !isNaN(Number(str)) && !isNaN(parseFloat(str)); 50 + } 51 + </script> 52 + 53 + {#snippet musicItem(play: Play)} 54 + <div class="flex w-full items-center gap-3"> 55 + <div class="size-10 shrink-0"> 56 + <AlbumArt albumArtUrl={play.value.albumArtUrl} alt="" /> 57 + </div> 58 + <div class="min-w-0 flex-1"> 59 + <div class="inline-flex w-full max-w-full justify-between gap-2"> 60 + <div 61 + class="text-accent-500 accent:text-accent-950 min-w-0 flex-1 shrink truncate font-semibold" 62 + > 63 + {play.value.title} 64 + </div> 65 + 66 + {#if play.value.createdAt} 67 + <div class="shrink-0 text-xs"> 68 + <RelativeTime 69 + date={new Date( 70 + isNumeric(play.value.createdAt) 71 + ? parseInt(play.value.createdAt) * 1000 72 + : play.value.createdAt 73 + )} 74 + locale="en-US" 75 + /> ago 76 + </div> 77 + {:else} 78 + <div></div> 79 + {/if} 80 + </div> 81 + <div class="my-1 min-w-0 gap-2 truncate text-xs whitespace-nowrap"> 82 + {(play?.value?.artists ?? []).map((a) => a.name).join(', ')} 83 + </div> 84 + </div> 85 + </div> 86 + {/snippet} 87 + 88 + <div class="z-10 flex h-full w-full flex-col gap-4 overflow-y-scroll p-4"> 89 + {#if feed && feed.length > 0} 90 + {#each feed as play (play.uri)} 91 + {#if play.uri} 92 + <a href="https://rocksky.app/{did}/scrobble/{play.uri.split('/').at(-1)}" target="_blank" rel="noopener noreferrer" class="w-full"> 93 + <!-- {#if play.value.spotifyLink} 94 + <a href={play.value.spotifyLink} target="_blank" rel="noopener noreferrer" class="w-full"> --> 95 + {@render musicItem(play)} 96 + </a> 97 + {:else} 98 + {@render musicItem(play)} 99 + {/if} 100 + {/each} 101 + {:else if feed} 102 + <div 103 + class="text-base-500 dark:text-base-400 accent:text-white/60 flex h-full items-center justify-center text-center text-sm" 104 + > 105 + No recent plays found. 106 + </div> 107 + {:else} 108 + <div 109 + class="text-base-500 dark:text-base-400 accent:text-white/60 flex h-full items-center justify-center text-center text-sm" 110 + > 111 + Loading plays... 112 + </div> 113 + {/if} 114 + </div>
+31
src/lib/cards/media/RockskyPlaysCard/index.ts
··· 1 + import type { CardDefinition } from '../../types'; 2 + import { listRecords } from '$lib/atproto'; 3 + import RockskyPlaysCard from './RockskyPlaysCard.svelte'; 4 + 5 + export const RockskyPlaysCardDefinition = { 6 + type: 'recentRockskyPlays', 7 + contentComponent: RockskyPlaysCard, 8 + createNew: (card) => { 9 + card.w = 4; 10 + card.mobileW = 8; 11 + card.h = 3; 12 + card.mobileH = 6; 13 + }, 14 + loadData: async (items, { did }) => { 15 + const data = await listRecords({ 16 + did, 17 + collection: 'app.rocksky.scrobble', 18 + limit: 99 19 + }); 20 + 21 + return data; 22 + }, 23 + minW: 4, 24 + canHaveLabel: true, 25 + 26 + keywords: ['music', 'scrobble', 'listening', 'songs'], 27 + name: 'Rocksky Plays', 28 + 29 + groups: ['Media'], 30 + icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-4"><path stroke-linecap="round" stroke-linejoin="round" d="m9 9 10.5-3m0 6.553v3.75a2.25 2.25 0 0 1-1.632 2.163l-1.32.377a1.803 1.803 0 1 1-.99-3.467l2.31-.66a2.25 2.25 0 0 0 1.632-2.163Zm0 0V2.25L9 5.25v10.303m0 0v3.75a2.25 2.25 0 0 1-1.632 2.163l-1.32.377a1.803 1.803 0 0 1-.99-3.467l2.31-.66A2.25 2.25 0 0 0 9 15.553Z" /></svg>` 31 + } as CardDefinition & { type: 'recentRockskyPlays' };