your personal website on atproto - mirror blento.app

Merge pull request #125 from flo-bit/apple-music-playlist

Apple music playlist

authored by Florian and committed by GitHub 9059b09f c14c3c15

+153 -5
+22
src/lib/cards/AppleMusicCard/AppleMusicCard.svelte
··· 1 + <script lang="ts"> 2 + import type { ContentComponentProps } from '../types'; 3 + 4 + let { item, isEditing }: ContentComponentProps = $props(); 5 + </script> 6 + 7 + {#if item.cardData?.appleMusicType && item.cardData?.appleMusicId && item.cardData?.appleMusicStorefront} 8 + <div class="absolute inset-0 p-2"> 9 + <iframe 10 + class={['h-full w-full rounded-2xl', isEditing && 'pointer-events-none']} 11 + src="https://embed.music.apple.com/{item.cardData.appleMusicStorefront}/{item.cardData 12 + .appleMusicType}/{item.cardData.appleMusicId}" 13 + sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" 14 + loading="lazy" 15 + title="Apple Music {item.cardData.appleMusicType}" 16 + ></iframe> 17 + </div> 18 + {:else} 19 + <div class="flex h-full items-center justify-center p-4 text-center opacity-50"> 20 + Missing Apple Music data 21 + </div> 22 + {/if}
+52
src/lib/cards/AppleMusicCard/CreateAppleMusicCardModal.svelte
··· 1 + <script lang="ts"> 2 + import { Alert, Button, Input, Modal, Subheading } from '@foxui/core'; 3 + import type { CreationModalComponentProps } from '../types'; 4 + 5 + let { item = $bindable(), oncreate, oncancel }: CreationModalComponentProps = $props(); 6 + 7 + let errorMessage = $state(''); 8 + 9 + function checkUrl() { 10 + errorMessage = ''; 11 + 12 + const pattern = /music\.apple\.com\/([a-z]{2})\/(album|playlist)\/[^/]+\/([a-zA-Z0-9.]+)/; 13 + const match = item.cardData.href?.match(pattern); 14 + 15 + if (!match) { 16 + errorMessage = 'Please enter a valid Apple Music album or playlist URL'; 17 + return false; 18 + } 19 + 20 + item.cardData.appleMusicStorefront = match[1]; 21 + item.cardData.appleMusicType = match[2]; 22 + item.cardData.appleMusicId = match[3]; 23 + 24 + return true; 25 + } 26 + </script> 27 + 28 + <Modal open={true} closeButton={false}> 29 + <Subheading>Enter an Apple Music album or playlist URL</Subheading> 30 + <Input 31 + bind:value={item.cardData.href} 32 + placeholder="https://music.apple.com/us/album/..." 33 + onkeydown={(e) => { 34 + if (e.key === 'Enter' && checkUrl()) oncreate(); 35 + }} 36 + /> 37 + 38 + {#if errorMessage} 39 + <Alert type="error" title="Invalid URL"><span>{errorMessage}</span></Alert> 40 + {/if} 41 + 42 + <div class="mt-4 flex justify-end gap-2"> 43 + <Button onclick={oncancel} variant="ghost">Cancel</Button> 44 + <Button 45 + onclick={() => { 46 + if (checkUrl()) oncreate(); 47 + }} 48 + > 49 + Create 50 + </Button> 51 + </div> 52 + </Modal>
+70
src/lib/cards/AppleMusicCard/index.ts
··· 1 + import type { CardDefinition } from '../types'; 2 + import CreateAppleMusicCardModal from './CreateAppleMusicCardModal.svelte'; 3 + import AppleMusicCard from './AppleMusicCard.svelte'; 4 + 5 + const cardType = 'apple-music-embed'; 6 + 7 + export const AppleMusicCardDefinition = { 8 + type: cardType, 9 + contentComponent: AppleMusicCard, 10 + creationModalComponent: CreateAppleMusicCardModal, 11 + createNew: (item) => { 12 + item.cardType = cardType; 13 + item.cardData = {}; 14 + item.w = 4; 15 + item.mobileW = 8; 16 + item.h = 5; 17 + item.mobileH = 10; 18 + }, 19 + 20 + onUrlHandler: (url, item) => { 21 + const match = matchAppleMusicUrl(url); 22 + if (!match) return null; 23 + 24 + item.cardData.appleMusicType = match.type; 25 + item.cardData.appleMusicId = match.id; 26 + item.cardData.appleMusicStorefront = match.storefront; 27 + item.cardData.href = url; 28 + 29 + item.w = 4; 30 + item.mobileW = 8; 31 + item.h = 5; 32 + item.mobileH = 10; 33 + 34 + return item; 35 + }, 36 + 37 + urlHandlerPriority: 2, 38 + 39 + name: 'Apple Music Embed', 40 + canResize: true, 41 + minW: 4, 42 + minH: 5, 43 + 44 + keywords: ['music', 'apple', 'playlist', 'album'], 45 + groups: ['Media'], 46 + icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-4"><path d="M23.994 6.124a9.23 9.23 0 00-.24-2.19c-.317-1.31-1.062-2.31-2.18-3.043a5.022 5.022 0 00-1.877-.726 10.496 10.496 0 00-1.564-.15c-.04-.003-.083-.01-.124-.013H5.986c-.152.01-.303.017-.455.026-.747.043-1.49.123-2.193.4-1.336.53-2.3 1.452-2.865 2.78-.192.448-.292.925-.363 1.408-.056.392-.088.785-.1 1.18 0 .032-.007.062-.01.093v12.223c.01.14.017.283.027.424.05.815.154 1.624.497 2.373.65 1.42 1.738 2.353 3.234 2.802.42.127.856.187 1.293.228.555.053 1.11.06 1.667.06h11.03a12.5 12.5 0 001.57-.1c.822-.106 1.596-.35 2.295-.81a5.046 5.046 0 001.88-2.207c.186-.42.293-.87.37-1.324.113-.675.138-1.358.137-2.04-.002-3.8 0-7.595-.003-11.393zm-6.423 3.99v5.712c0 .417-.058.827-.244 1.206-.29.59-.76.962-1.388 1.14-.35.1-.706.157-1.07.173-.95.042-1.8-.6-1.965-1.483-.18-.965.46-1.97 1.553-2.142.238-.037.48-.065.72-.082.39-.03.78-.056 1.168-.1.207-.02.357-.127.404-.334a1.14 1.14 0 00.025-.26V9.97a.48.48 0 00-.357-.47c-.107-.033-.218-.06-.33-.073-.565-.065-1.13-.118-1.696-.18l-3.535-.38c-.043-.004-.088-.005-.13 0a.334.334 0 00-.32.334c-.003.06 0 .12 0 .18v7.63c0 .4-.046.793-.216 1.16-.293.635-.792 1.03-1.466 1.205-.32.082-.647.136-.978.152-.93.043-1.764-.585-1.95-1.443-.2-.924.39-1.893 1.397-2.1.36-.073.724-.118 1.088-.158.274-.03.55-.06.82-.105.164-.027.3-.1.367-.27a.77.77 0 00.048-.268V7.762c0-.282.07-.53.275-.735a1.09 1.09 0 01.49-.282c.333-.093.674-.143 1.012-.18l3.384-.38c.56-.063 1.12-.123 1.68-.187.321-.037.642-.063.96-.04.37.03.658.2.86.518.088.138.135.292.148.453.016.224.02.448.02.672v2.533z"/></svg>` 47 + } as CardDefinition & { type: typeof cardType }; 48 + 49 + // Match Apple Music album and playlist URLs 50 + // Examples: 51 + // https://music.apple.com/us/album/midnights/1649434004 52 + // https://music.apple.com/us/playlist/todays-hits/pl.f4d106fed2bd41149aaacabb233eb5eb 53 + function matchAppleMusicUrl( 54 + url: string | undefined 55 + ): { type: 'album' | 'playlist'; id: string; storefront: string } | null { 56 + if (!url) return null; 57 + 58 + const pattern = /music\.apple\.com\/([a-z]{2})\/(album|playlist)\/[^/]+\/([a-zA-Z0-9.]+)/; 59 + const match = url.match(pattern); 60 + 61 + if (match) { 62 + return { 63 + storefront: match[1], 64 + type: match[2] as 'album' | 'playlist', 65 + id: match[3] 66 + }; 67 + } 68 + 69 + return null; 70 + }
-1
src/lib/cards/SpecialCards/UpdatedBlentos/UpdatedBlentosCard.svelte
··· 20 20 </script> 21 21 22 22 <div class="flex h-full flex-col"> 23 - <div class="px-4 py-2 text-2xl font-bold">Recently updated blentos</div> 24 23 <div class="flex max-w-full grow items-center gap-4 overflow-x-scroll overflow-y-hidden px-4"> 25 24 {#each profiles as profile (profile.did)} 26 25 <a
+3
src/lib/cards/SpecialCards/UpdatedBlentos/index.ts
··· 57 57 return []; 58 58 } 59 59 } 60 + // name: 'Updated Blentos', 61 + // groups: ['Social'], 62 + // 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="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0ZM12 6c-1.602 0-3.155.474-4.434 1.357L18 16.791A8.959 8.959 0 0 0 21 12h-4.5Z" /></svg>` 60 63 } as CardDefinition & { type: 'updatedBlentos' };
+2
src/lib/cards/index.ts
··· 32 32 import { ClockCardDefinition } from './ClockCard'; 33 33 import { CountdownCardDefinition } from './CountdownCard'; 34 34 import { SpotifyCardDefinition } from './SpotifyCard'; 35 + import { AppleMusicCardDefinition } from './AppleMusicCard'; 35 36 import { ButtonCardDefinition } from './ButtonCard'; 36 37 import { GuestbookCardDefinition } from './GuestbookCard'; 37 38 import { FriendsCardDefinition } from './FriendsCard'; ··· 73 74 ClockCardDefinition, 74 75 CountdownCardDefinition, 75 76 SpotifyCardDefinition, 77 + AppleMusicCardDefinition, 76 78 // Model3DCardDefinition 77 79 FriendsCardDefinition 78 80 ] as const;
+3 -1
src/lib/website/Context.svelte
··· 19 19 // svelte-ignore state_referenced_locally 20 20 setAdditionalUserData(data.additionalData); 21 21 22 - setCanEdit(() => dev || (user.isLoggedIn && user.profile?.did === data.did && isEditing === true)); 22 + setCanEdit( 23 + () => dev || (user.isLoggedIn && user.profile?.did === data.did && isEditing === true) 24 + ); 23 25 24 26 // svelte-ignore state_referenced_locally 25 27 setDidContext(data.did as Did);
+1 -3
src/lib/website/EditableWebsite.svelte
··· 201 201 document.body.style.removeProperty('padding-right'); 202 202 document.body.style.removeProperty('margin-right'); 203 203 // Remove any orphaned dialog overlay/content elements left by the portal 204 - for (const el of document.querySelectorAll( 205 - '[data-dialog-overlay], [data-dialog-content]' 206 - )) { 204 + for (const el of document.querySelectorAll('[data-dialog-overlay], [data-dialog-content]')) { 207 205 el.remove(); 208 206 } 209 207 };