your personal website on atproto - mirror blento.app

add first version of bluesky media card

Florian af81a55f 55aa33c4

+239 -15
-6
src/lib/EditableWebsite.svelte
··· 393 ></div> 394 {/if} 395 396 - {#if dev} 397 - <div 398 - class="absolute size-4 rounded-full bg-red-500" 399 - style={`translate: ${debugPoint.x}px ${debugPoint.y}px;`} 400 - ></div> 401 - {/if} 402 <div style="height: {((maxHeight + 2) / 8) * 100}cqw;"></div> 403 </div> 404 </div>
··· 393 ></div> 394 {/if} 395 396 <div style="height: {((maxHeight + 2) / 8) * 100}cqw;"></div> 397 </div> 398 </div>
+1 -1
src/lib/cards/BaseCard/BaseCard.svelte
··· 57 --columns: ${COLUMNS}`} 58 {...rest} 59 > 60 - <div class="relative h-full w-full overflow-hidden rounded-[15px]"> 61 {@render children?.()} 62 </div> 63 {@render controls?.()}
··· 57 --columns: ${COLUMNS}`} 58 {...rest} 59 > 60 + <div class="relative h-full w-full overflow-hidden rounded-[15px] isolate"> 61 {@render children?.()} 62 </div> 63 {@render controls?.()}
+65
src/lib/cards/BlueskyMediaCard/BlueskyMediaCard.svelte
···
··· 1 + <script lang="ts"> 2 + import { getDidContext } from '$lib/website/context'; 3 + import { getImageBlobUrl } from '$lib/website/utils'; 4 + import type { ContentComponentProps } from '../types'; 5 + import Video from './Video.svelte'; 6 + 7 + let { item = $bindable(), ...rest }: ContentComponentProps = $props(); 8 + 9 + const did = getDidContext(); 10 + 11 + function getSrc() { 12 + if (item.cardData.objectUrl) return item.cardData.objectUrl; 13 + 14 + if (item.cardData.image && typeof item.cardData.image === 'object') { 15 + return getImageBlobUrl({ did, link: item.cardData.image?.ref?.$link }); 16 + } 17 + return item.cardData.image; 18 + } 19 + 20 + $inspect(item.cardData); 21 + </script> 22 + 23 + {#if item.cardData.image} 24 + <img 25 + class={[ 26 + 'absolute inset-0 h-full w-full object-cover opacity-100 transition-transform duration-300 ease-in-out', 27 + item.cardData.href ? 'group-hover:scale-102' : '' 28 + ]} 29 + src={item.cardData.image.fullsize} 30 + alt="" 31 + /> 32 + {:else if item.cardData.video} 33 + <Video video={item.cardData.video} /> 34 + {/if} 35 + {#if item.cardData.href} 36 + <a 37 + href={item.cardData.href} 38 + class="absolute inset-0 z-10 h-full w-full" 39 + target="_blank" 40 + rel="noopener noreferrer" 41 + > 42 + <span class="sr-only"> 43 + {item.cardData.hrefText ?? 'Learn more'} 44 + </span> 45 + 46 + <div 47 + class="bg-base-800/30 border-base-900/30 absolute top-2 right-2 rounded-full border p-1 text-white opacity-50 backdrop-blur-lg group-focus-within:opacity-100 group-hover:opacity-100" 48 + > 49 + <svg 50 + xmlns="http://www.w3.org/2000/svg" 51 + fill="none" 52 + viewBox="0 0 24 24" 53 + stroke-width="2.5" 54 + stroke="currentColor" 55 + class="size-4" 56 + > 57 + <path 58 + stroke-linecap="round" 59 + stroke-linejoin="round" 60 + d="m4.5 19.5 15-15m0 0H8.25m11.25 0v11.25" 61 + /> 62 + </svg> 63 + </div> 64 + </a> 65 + {/if}
+119
src/lib/cards/BlueskyMediaCard/CreateBlueskyMediaCardModal.svelte
···
··· 1 + <script lang="ts"> 2 + import { Button, Input, Label, Modal, Subheading } from '@foxui/core'; 3 + import type { CreationModalComponentProps } from '../types'; 4 + import { onMount } from 'svelte'; 5 + import { AtpBaseClient } from '@atproto/api'; 6 + import { getDidContext } from '$lib/website/context'; 7 + import { getImageBlobUrl } from '$lib/website/utils'; 8 + 9 + let { item = $bindable(), oncreate, oncancel }: CreationModalComponentProps = $props(); 10 + 11 + let did = getDidContext(); 12 + 13 + let mediaList: { fullsize: string; isVideo?: boolean; playlist?: string }[] = $state([]); 14 + 15 + onMount(async () => { 16 + const agent = new AtpBaseClient({ service: 'https://api.bsky.app' }); 17 + const authorFeed = await agent.app.bsky.feed.getAuthorFeed({ 18 + actor: did, 19 + filter: 'posts_with_media', 20 + limit: 100 21 + }); 22 + 23 + console.log(authorFeed); 24 + 25 + for (let post of authorFeed.data.feed) { 26 + for (let image of post.post.embed?.images ?? []) { 27 + mediaList.push(image); 28 + } 29 + 30 + if (post.post.embed.thumbnail && post.post.embed.playlist) { 31 + mediaList.push({ 32 + ...post.post.embed, 33 + isVideo: true 34 + }); 35 + } 36 + } 37 + }); 38 + 39 + let selected = $state(); 40 + </script> 41 + 42 + <Modal 43 + bind:open={ 44 + () => true, 45 + (change) => { 46 + if (!change) oncancel(); 47 + } 48 + } 49 + closeButton={false} 50 + class="flex max-h-screen flex-col" 51 + > 52 + <Subheading>Select an image or video</Subheading> 53 + 54 + <div 55 + class="bg-base-100 dark:bg-base-950 grid h-[50dvh] grow grid-cols-3 gap-4 overflow-y-scroll rounded-2xl p-4" 56 + > 57 + {#each mediaList as media} 58 + <button 59 + onclick={() => { 60 + console.log(media); 61 + selected = media; 62 + if (media.isVideo) { 63 + item.cardData = { 64 + video: media 65 + }; 66 + } else item.cardData.image = media; 67 + }} 68 + class="relative cursor-pointer" 69 + > 70 + <img 71 + src={media.fullsize ?? media.thumbnail} 72 + alt="" 73 + class={[ 74 + 'h-24 w-full rounded-xl object-cover', 75 + selected === media 76 + ? 'outline-accent-500 opacity-100 outline-2 -outline-offset-2' 77 + : 'opacity-80' 78 + ]} 79 + /> 80 + {#if media.isVideo} 81 + <div class="absolute inset-0 inline-flex items-center justify-center"> 82 + <svg 83 + xmlns="http://www.w3.org/2000/svg" 84 + viewBox="0 0 24 24" 85 + fill="currentColor" 86 + class="text-accent-500 size-6" 87 + > 88 + <path 89 + fill-rule="evenodd" 90 + d="M4.5 5.653c0-1.427 1.529-2.33 2.779-1.643l11.54 6.347c1.295.712 1.295 2.573 0 3.286L7.28 19.99c-1.25.687-2.779-.217-2.779-1.643V5.653Z" 91 + clip-rule="evenodd" 92 + /> 93 + </svg> 94 + </div> 95 + {/if} 96 + </button> 97 + {/each} 98 + {#if mediaList.length === 0} 99 + <span class="col-span-3 p-4 text-lg italic">Loading your media...</span>{/if} 100 + </div> 101 + 102 + <Label class="mt-4">Link (optional):</Label> 103 + <Input bind:value={item.cardData.href} /> 104 + 105 + <div class="mt-4 flex justify-end gap-2"> 106 + <Button 107 + onclick={() => { 108 + oncancel(); 109 + }} 110 + variant="ghost">Cancel</Button 111 + > 112 + <Button 113 + disabled={!selected} 114 + onclick={async () => { 115 + oncreate(); 116 + }}>Create</Button 117 + > 118 + </div> 119 + </Modal>
+37
src/lib/cards/BlueskyMediaCard/Video.svelte
···
··· 1 + <script lang="ts"> 2 + import { onMount } from 'svelte'; 3 + import Hls from 'hls.js'; 4 + 5 + const { 6 + video 7 + }: { 8 + video: { 9 + playlist: string; 10 + thumbnail: string; 11 + alt: string; 12 + }; 13 + } = $props(); 14 + 15 + onMount(async () => { 16 + if (Hls.isSupported()) { 17 + var hls = new Hls(); 18 + hls.loadSource(video.playlist); 19 + hls.attachMedia(element); 20 + } 21 + 22 + element.play(); 23 + }); 24 + 25 + let element: HTMLMediaElement; 26 + </script> 27 + 28 + <img src={video.thumbnail} class="absolute inset-0 -z-10 h-full w-full object-cover" alt="" /> 29 + <!-- svelte-ignore a11y_media_has_caption --> 30 + <video 31 + bind:this={element} 32 + muted 33 + loop 34 + autoplay 35 + class="absolute inset-0 h-full w-full object-cover" 36 + aria-label={video.alt} 37 + ></video>
+11
src/lib/cards/BlueskyMediaCard/index.ts
···
··· 1 + import type { CardDefinition } from '../types'; 2 + import BlueskyMediaCard from './BlueskyMediaCard.svelte'; 3 + import CreateBlueskyMediaCardModal from './CreateBlueskyMediaCardModal.svelte'; 4 + 5 + export const BlueskyMediaCardDefinition = { 6 + type: 'blueskyMedia', 7 + contentComponent: BlueskyMediaCard, 8 + createNew: (card) => {}, 9 + creationModalComponent: CreateBlueskyMediaCardModal, 10 + sidebarButtonText: 'Bluesky Media' 11 + } as CardDefinition & { type: 'blueskyMedia' };
-4
src/lib/cards/MapCard/Map.svelte
··· 16 17 onMount(async () => { 18 try { 19 - console.log(`Loading map...`); 20 - 21 // @ts-ignore 22 leaflet = await import('leaflet'); 23 ··· 42 !item.color || item.color === 'transparent' || item.color === 'base' 43 ? 'accent' 44 : item.color; 45 - 46 - console.log(color); 47 48 const computedColor = getCSSVar(`--color-${color}-500`); 49
··· 16 17 onMount(async () => { 18 try { 19 // @ts-ignore 20 leaflet = await import('leaflet'); 21 ··· 40 !item.color || item.color === 'transparent' || item.color === 'base' 41 ? 'accent' 42 : item.color; 43 44 const computedColor = getCSSVar(`--color-${color}-500`); 45
+1 -1
src/lib/cards/SpecialCards/UpdatedBlentos/UpdatedBlentosCard.svelte
··· 10 11 const data = getAdditionalUserData(); 12 // svelte-ignore state_referenced_locally 13 - const profiles = (data[item.cardType] as ProfileViewDetailed[]); 14 </script> 15 16 <div class="pointer-events-none">
··· 10 11 const data = getAdditionalUserData(); 12 // svelte-ignore state_referenced_locally 13 + const profiles = (data[item.cardType] as ProfileViewDetailed[]).splice(20); 14 </script> 15 16 <div class="pointer-events-none">
-1
src/lib/cards/SpecialCards/UpdatedBlentos/index.ts
··· 33 } 34 35 const result = [...(await Promise.all(profiles)), ...existingUsersArray]; 36 - result.splice(20); 37 38 if (platform?.env?.USER_DATA_CACHE) { 39 await platform?.env?.USER_DATA_CACHE.put('updatedBlentos', JSON.stringify(result));
··· 33 } 34 35 const result = [...(await Promise.all(profiles)), ...existingUsersArray]; 36 37 if (platform?.env?.USER_DATA_CACHE) { 38 await platform?.env?.USER_DATA_CACHE.put('updatedBlentos', JSON.stringify(result));
+3 -1
src/lib/cards/index.ts
··· 1 import type { Item } from '$lib/types'; 2 import { ATProtoCollectionsCardDefinition } from './ATProtoCollectionsCard'; 3 import { BlueskyPostCardDefinition } from './BlueskyPostCard'; 4 import { EmbedCardDefinition } from './EmbedCard'; 5 import { ImageCardDefinition } from './ImageCard'; ··· 24 EmbedCardDefinition, 25 MapCardDefinition, 26 ATProtoCollectionsCardDefinition, 27 - SectionCardDefinition 28 ] as const; 29 30 export const CardDefinitionsByType = AllCardDefinitions.reduce(
··· 1 import type { Item } from '$lib/types'; 2 import { ATProtoCollectionsCardDefinition } from './ATProtoCollectionsCard'; 3 + import { BlueskyMediaCardDefinition } from './BlueskyMediaCard'; 4 import { BlueskyPostCardDefinition } from './BlueskyPostCard'; 5 import { EmbedCardDefinition } from './EmbedCard'; 6 import { ImageCardDefinition } from './ImageCard'; ··· 25 EmbedCardDefinition, 26 MapCardDefinition, 27 ATProtoCollectionsCardDefinition, 28 + SectionCardDefinition, 29 + BlueskyMediaCardDefinition 30 ] as const; 31 32 export const CardDefinitionsByType = AllCardDefinitions.reduce(
+2 -1
todo.md
··· 1 # todo 2 3 - - video card 4 - edit already created cards (e.g. change link) 5 - link card: save favicon and og image to pds 6 - [x] more cards list
··· 1 # todo 2 3 + - video card or image from bluesky 4 + - general video card 5 - edit already created cards (e.g. change link) 6 - link card: save favicon and og image to pds 7 - [x] more cards list