appview-less bluesky client
24
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 199 lines 6.5 kB view raw
1<script context="module" lang="ts"> 2 export interface GalleryItem { 3 src: string; 4 thumbnail?: { 5 src: string; 6 width?: number; 7 height?: number; 8 }; 9 width?: number; 10 height?: number; 11 alt?: string; 12 } 13 export type GalleryData = Array<GalleryItem>; 14</script> 15 16<script lang="ts"> 17 import 'photoswipe/photoswipe.css'; 18 import PhotoSwipeLightbox from 'photoswipe/lightbox'; 19 import PhotoSwipe, { type ElementProvider, type PreparedPhotoSwipeOptions } from 'photoswipe'; 20 import { onMount } from 'svelte'; 21 import { writable } from 'svelte/store'; 22 23 export let images: GalleryData; 24 let element: HTMLDivElement; 25 let imageElements: { [key: number]: HTMLImageElement } = {}; 26 27 const options = writable<Partial<PreparedPhotoSwipeOptions> | undefined>(undefined); 28 $: { 29 if (!element) break $; 30 const opts: Partial<PreparedPhotoSwipeOptions> = { 31 pswpModule: PhotoSwipe, 32 children: element.childNodes as ElementProvider, 33 gallery: element, 34 hideAnimationDuration: 0, 35 showAnimationDuration: 0, 36 zoomAnimationDuration: 200, 37 zoomSVG: 38 '<svg class="gallery--icon" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 16 16"><path fill="currentColor" d="M6.25 8.75v-1h-1a.75.75 0 0 1 0-1.5h1v-1a.75.75 0 0 1 1.5 0v1h1a.75.75 0 0 1 0 1.5h-1v1a.75.75 0 0 1-1.5 0"/><path fill="currentColor" fill-rule="evenodd" d="M7 12c1.11 0 2.136-.362 2.965-.974l2.755 2.754a.75.75 0 1 0 1.06-1.06l-2.754-2.755A5 5 0 1 0 7 12m0-1.5a3.5 3.5 0 1 0 0-7a3.5 3.5 0 0 0 0 7" clip-rule="evenodd"/></svg>', 39 closeSVG: 40 '<svg class="gallery--icon" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 16 16"><path fill="currentColor" d="M5.28 4.22a.75.75 0 0 0-1.06 1.06L6.94 8l-2.72 2.72a.75.75 0 1 0 1.06 1.06L8 9.06l2.72 2.72a.75.75 0 1 0 1.06-1.06L9.06 8l2.72-2.72a.75.75 0 0 0-1.06-1.06L8 6.94z"/></svg>', 41 arrowPrevSVG: 42 '<svg class="gallery--icon" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M9.78 4.22a.75.75 0 0 1 0 1.06L7.06 8l2.72 2.72a.75.75 0 1 1-1.06 1.06L5.47 8.53a.75.75 0 0 1 0-1.06l3.25-3.25a.75.75 0 0 1 1.06 0" clip-rule="evenodd"/></svg>', 43 arrowNextSVG: 44 '<svg class="gallery--icon" xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M6.22 4.22a.75.75 0 0 1 1.06 0l3.25 3.25a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06L8.94 8L6.22 5.28a.75.75 0 0 1 0-1.06" clip-rule="evenodd"/></svg>' 45 }; 46 $options = opts; 47 } 48 49 onMount(() => { 50 let lightbox: PhotoSwipeLightbox | undefined; 51 const unsub = options.subscribe((opts) => { 52 lightbox?.destroy?.(); 53 if (opts === undefined) return; 54 lightbox = new PhotoSwipeLightbox(opts); 55 lightbox.init(); 56 }); 57 return () => { 58 unsub(); 59 lightbox?.destroy?.(); 60 }; 61 }); 62</script> 63 64<div class="gallery styling-twitter" data-total={images.length} bind:this={element}> 65 {#each images as img, i (`${img.src}#${i}`)} 66 {@const thumb = img.thumbnail ?? img} 67 {@const isHidden = i > 3} 68 {@const isOverlay = i === 3 && images.length > 4} 69 70 <!-- eslint-disable svelte/no-navigation-without-resolve --> 71 <a 72 href={img.src} 73 data-pswp-width={img.width ?? imageElements[i]?.width} 74 data-pswp-height={img.height ?? imageElements[i]?.height} 75 target="_blank" 76 class:hidden-in-grid={isHidden} 77 class:overlay-container={isOverlay} 78 > 79 <img bind:this={imageElements[i]} src={thumb.src} title={img.alt ?? ''} alt={img.alt ?? ''} /> 80 81 {#if isOverlay} 82 <div class="more-overlay"> 83 +{images.length - 4} 84 </div> 85 {/if} 86 </a> 87 {/each} 88</div> 89 90<style> 91 :global(.gallery--icon) { 92 --drop-color: color-mix(in srgb, var(--color-gray-900) 70%, transparent); 93 color: var(--nucleus-fg); 94 filter: drop-shadow(2px 2px 1px var(--drop-color)) drop-shadow(-2px -2px 1px var(--drop-color)) 95 drop-shadow(-2px 2px 1px var(--drop-color)) drop-shadow(2px -2px 1px var(--drop-color)); 96 } 97 98 /* --- Default Grid (for 2+ images) --- */ 99 .gallery.styling-twitter { 100 display: grid; 101 gap: 2px; 102 border-radius: 4px; 103 overflow: hidden; 104 } 105 106 .gallery.styling-twitter > a { 107 width: 100%; 108 height: 100%; 109 display: block; 110 position: relative; 111 overflow: hidden; 112 } 113 114 .gallery.styling-twitter > a > img { 115 @apply transition-opacity duration-200 hover:opacity-80; 116 width: 100%; 117 height: 100%; 118 object-fit: cover; /* Standard tile crop */ 119 } 120 121 /* --- SINGLE IMAGE OVERRIDES --- */ 122 /* This configuration allows the image to determine the width/height 123 naturally based on aspect ratio, up to a max-height limit. 124 */ 125 .gallery.styling-twitter[data-total='1'] { 126 display: block; /* Remove grid constraints */ 127 height: auto; 128 aspect-ratio: auto; /* Remove 16:9 ratio */ 129 border-radius: 0; 130 } 131 132 .gallery.styling-twitter[data-total='1'] > a { 133 /* fit-content is key: the container shrinks to fit the image width */ 134 width: fit-content; 135 height: auto; 136 display: block; 137 border-radius: 4px; 138 overflow: hidden; 139 max-width: 100%; /* Prevent overflowing the parent */ 140 } 141 142 .gallery.styling-twitter[data-total='1'] > a > img { 143 /* Let dimensions flow naturally */ 144 width: auto; 145 height: auto; 146 147 /* Constraints: */ 148 max-width: 100%; /* Never wider than container */ 149 max-height: 60vh; /* Never taller than 60% of viewport (adjust if needed) */ 150 151 object-fit: contain; /* Never crop the single image */ 152 } 153 154 /* --- Grid Layouts (2+ Images) --- */ 155 /* These retain the standard grid look */ 156 157 /* 2 Images: Split vertically */ 158 .gallery.styling-twitter[data-total='2'] { 159 grid-template-columns: 1fr 1fr; 160 grid-template-rows: 1fr; 161 aspect-ratio: 16/9; 162 } 163 164 /* 3 Images: 1 Big (left), 2 Small (stacked right) */ 165 .gallery.styling-twitter[data-total='3'] { 166 grid-template-columns: 1fr 1fr; 167 grid-template-rows: 1fr 1fr; 168 aspect-ratio: 16/9; 169 } 170 .gallery.styling-twitter[data-total='3'] > a:first-child { 171 grid-row: span 2; 172 } 173 174 /* 4+ Images: 2x2 Grid */ 175 .gallery.styling-twitter[data-total='4'], 176 .gallery.styling-twitter[data-total^='5'], 177 .gallery.styling-twitter:not([data-total='1']):not([data-total='2']):not([data-total='3']) { 178 grid-template-columns: 1fr 1fr; 179 grid-template-rows: 1fr 1fr; 180 aspect-ratio: 16/9; 181 } 182 183 .gallery.styling-twitter .hidden-in-grid { 184 display: none; 185 } 186 187 .more-overlay { 188 position: absolute; 189 inset: 0; 190 background-color: rgba(0, 0, 0, 0.5); 191 color: white; 192 display: flex; 193 align-items: center; 194 justify-content: center; 195 font-size: 2rem; 196 font-weight: bold; 197 pointer-events: none; 198 } 199</style>