your personal website on atproto - mirror blento.app

Initial template

Harry L e13bca21 7f766672

+183
+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 { releaseMbId, alt }: { releaseMbId?: 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="https://coverartarchive.org/release/{releaseMbId}/front-250" 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}
+111
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 + artistName: string; 11 + } 12 + 13 + interface PlayValue { 14 + releaseMbId?: string; 15 + trackName: string; 16 + playedTime?: string; 17 + artists?: Artist[]; 18 + originUrl?: string; 19 + } 20 + 21 + interface Play { 22 + uri: string; 23 + value: PlayValue; 24 + } 25 + 26 + let { item }: { item: Item } = $props(); 27 + 28 + const data = getAdditionalUserData(); 29 + // svelte-ignore state_referenced_locally 30 + let feed = $state(data[item.cardType] as Play[] | undefined); 31 + 32 + let did = getDidContext(); 33 + let handle = getHandleContext(); 34 + 35 + onMount(async () => { 36 + if (feed) return; 37 + 38 + feed = (await CardDefinitionsByType[item.cardType]?.loadData?.([], { 39 + did, 40 + handle 41 + })) as Play[] | undefined; 42 + 43 + data[item.cardType] = feed; 44 + }); 45 + 46 + function isNumeric(str: string) { 47 + if (typeof str != 'string') return false; 48 + return !isNaN(Number(str)) && !isNaN(parseFloat(str)); 49 + } 50 + </script> 51 + 52 + {#snippet musicItem(play: Play)} 53 + <div class="flex w-full items-center gap-3"> 54 + <div class="size-10 shrink-0"> 55 + <AlbumArt releaseMbId={play.value.releaseMbId} alt="" /> 56 + </div> 57 + <div class="min-w-0 flex-1"> 58 + <div class="inline-flex w-full max-w-full justify-between gap-2"> 59 + <div 60 + class="text-accent-500 accent:text-accent-950 min-w-0 flex-1 shrink truncate font-semibold" 61 + > 62 + {play.value.trackName} 63 + </div> 64 + 65 + {#if play.value.playedTime} 66 + <div class="shrink-0 text-xs"> 67 + <RelativeTime 68 + date={new Date( 69 + isNumeric(play.value.playedTime) 70 + ? parseInt(play.value.playedTime) * 1000 71 + : play.value.playedTime 72 + )} 73 + locale="en-US" 74 + /> ago 75 + </div> 76 + {:else} 77 + <div></div> 78 + {/if} 79 + </div> 80 + <div class="my-1 min-w-0 gap-2 truncate text-xs whitespace-nowrap"> 81 + {(play?.value?.artists ?? []).map((a) => a.artistName).join(', ')} 82 + </div> 83 + </div> 84 + </div> 85 + {/snippet} 86 + 87 + <div class="z-10 flex h-full w-full flex-col gap-4 overflow-y-scroll p-4"> 88 + {#if feed && feed.length > 0} 89 + {#each feed as play (play.uri)} 90 + {#if play.value.originUrl} 91 + <a href={play.value.originUrl} target="_blank" rel="noopener noreferrer" class="w-full"> 92 + {@render musicItem(play)} 93 + </a> 94 + {:else} 95 + {@render musicItem(play)} 96 + {/if} 97 + {/each} 98 + {:else if feed} 99 + <div 100 + class="text-base-500 dark:text-base-400 accent:text-white/60 flex h-full items-center justify-center text-center text-sm" 101 + > 102 + No recent plays found. 103 + </div> 104 + {:else} 105 + <div 106 + class="text-base-500 dark:text-base-400 accent:text-white/60 flex h-full items-center justify-center text-center text-sm" 107 + > 108 + Loading plays... 109 + </div> 110 + {/if} 111 + </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' };