diff --git a/config.ts b/config.ts index a507eb3..b8284a6 100644 --- a/config.ts +++ b/config.ts @@ -6,7 +6,7 @@ export class Config { * The base URL of the PDS (Personal Data Server) * @default "https://pds.witchcraft.systems" */ - static readonly PDS_URL: string = "https://ap.brid.gy"; + static readonly PDS_URL: string = "https://pds.witchcraft.systems"; /** * The base URL of the frontend service for linking to replies @@ -18,5 +18,11 @@ export class Config { * Maximum number of posts to fetch from the PDS per user * @default 10 */ - static readonly MAX_POSTS_PER_USER: number = 1; + static readonly MAX_POSTS_PER_USER: number = 22; + + /** + * Footer text for the dashboard + * @default "Astrally projected from witchcraft.systems" + */ + static readonly FOOTER_TEXT: string = "Astrally projected from <a href='https://witchcraft.systems' target='_blank'>witchcraft.systems</a>"; } \ No newline at end of file diff --git a/src/App.svelte b/src/App.svelte index c5a024c..fbf59f1 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -2,6 +2,7 @@ import PostComponent from "./lib/PostComponent.svelte"; import AccountComponent from "./lib/AccountComponent.svelte"; import { fetchAllPosts, Post, getAllMetadataFromPds } from "./lib/pdsfetch"; + import { Config } from "../config"; const postsPromise = fetchAllPosts(); const accountsPromise = getAllMetadataFromPds(); </script> @@ -19,6 +20,7 @@ <AccountComponent account={accountObject} /> {/each} </div> + <p>{@html Config.FOOTER_TEXT}</p> </div> {:catch error} <p>Error: {error.message}</p> @@ -74,6 +76,7 @@ background-color: #0d0620; height: 80vh; padding: 20px; + margin-left: 20px; } #accountsList { display: flex; diff --git a/src/app.css b/src/app.css index 988c405..05a51fd 100644 --- a/src/app.css +++ b/src/app.css @@ -40,6 +40,7 @@ a { } a:hover { color: #535bf2; + text-decoration: underline; } body { diff --git a/src/lib/PostComponent.svelte b/src/lib/PostComponent.svelte index b240c95..fc24700 100644 --- a/src/lib/PostComponent.svelte +++ b/src/lib/PostComponent.svelte @@ -1,7 +1,58 @@ <script lang="ts"> import { Post } from "./pdsfetch"; import { Config } from "../../config"; + import { onMount } from "svelte"; + let { post }: { post: Post } = $props(); + + // State for image carousel + let currentImageIndex = $state(0); + + // Functions to navigate carousel + function nextImage() { + if (post.imagesCid && currentImageIndex < post.imagesCid.length - 1) { + currentImageIndex++; + } + } + + function prevImage() { + if (currentImageIndex > 0) { + currentImageIndex--; + } + } + + // Function to preload an image + function preloadImage(index: number): void { + if (!post.imagesCid || index < 0 || index >= post.imagesCid.length) return; + + const img = new Image(); + img.src = `${Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did=${post.authorDid}&cid=${post.imagesCid[index]}`; + } + + // Preload adjacent images when current index changes + $effect(() => { + if (post.imagesCid && post.imagesCid.length > 1) { + // Preload next image if available + if (currentImageIndex < post.imagesCid.length - 1) { + preloadImage(currentImageIndex + 1); + } + + // Preload previous image if available + if (currentImageIndex > 0) { + preloadImage(currentImageIndex - 1); + } + } + }); + + // Initial preload of images + onMount(() => { + if (post.imagesCid && post.imagesCid.length > 1) { + // Preload the next image if it exists + if (post.imagesCid.length > 1) { + preloadImage(1); + } + } + }); </script> <div id="postContainer"> @@ -14,24 +65,30 @@ /> {/if} <div id="headerText"> - <a href="{Config.FRONTEND_URL}/profile/{post.authorDid}" - >{post.displayName} ( {post.authorHandle} )</a - > - | - <a href="{Config.FRONTEND_URL}/profile/{post.authorDid}/post/{post.cid}" - >{post.timenotstamp}</a + <a id="displayName" href="{Config.FRONTEND_URL}/profile/{post.authorDid}" + >{post.displayName}</a > + <p id="handle"> + <a href="{Config.FRONTEND_URL}/profile/{post.authorHandle}" + >{post.authorHandle}</a + > + + <a + id="postLink" + href="{Config.FRONTEND_URL}/profile/{post.authorDid}/post/{post.recordName}" + >{post.timenotstamp}</a + > + </p> </div> </div> <div id="postContent"> {#if post.replyingUri} - <a - id="replyingText" - href="{Config.FRONTEND_URL}/profile/{post.replyingUri.repo}/post/{post - .replyingUri.rkey}">replying to {post.replyingUri.repo}</a - > + <a + id="replyingText" + href="{Config.FRONTEND_URL}/profile/{post.replyingUri.repo}/post/{post + .replyingUri.rkey}">replying to {post.replyingUri.repo}</a + > {/if} - <div id="postText">{post.text}</div> {#if post.quotingUri} <a id="quotingText" @@ -39,27 +96,54 @@ .quotingUri.rkey}">quoting {post.quotingUri.repo}</a > {/if} - {#if post.imagesCid} - <div id="imagesContainer"> - {#each post.imagesCid as imageLink} - <img - id="embedImages" - alt="Post Image" - src="{Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did={post.authorDid}&cid={imageLink}" - /> - {/each} + <div id="postText">{post.text}</div> + {#if post.imagesCid && post.imagesCid.length > 0} + <div id="carouselContainer"> + <img + id="embedImages" + alt="Post Image {currentImageIndex + 1} of {post.imagesCid.length}" + src="{Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did={post.authorDid}&cid={post + .imagesCid[currentImageIndex]}" + /> + + {#if post.imagesCid.length > 1} + <div id="carouselControls"> + <button + id="prevBtn" + on:click={prevImage} + disabled={currentImageIndex === 0}>←</button + > + <div id="carouselIndicators"> + {#each post.imagesCid as _, i} + <div + class="indicator {i === currentImageIndex ? 'active' : ''}" + ></div> + {/each} + </div> + <button + id="nextBtn" + on:click={nextImage} + disabled={currentImageIndex === post.imagesCid.length - 1} + >→</button + > + </div> + {/if} </div> {/if} {#if post.videosLinkCid} <video id="embedVideo" src="{Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did={post.authorDid}&cid={post.videosLinkCid}" - /> + controls + ></video> {/if} </div> </div> <style> + a:hover { + text-decoration: underline; + } #postContainer { display: flex; flex-direction: column; @@ -79,6 +163,26 @@ border-bottom: 1px solid #8054f0; font-weight: bold; overflow-wrap: break-word; + height: 60px; + } + #displayName { + color: white; + font-size: 1.2em; + padding: 0; + margin: 0; + } + #handle { + color: #8054f0; + font-size: 0.8em; + padding: 0; + margin: 0; + } + + #postLink { + color: #8054f0; + font-size: 0.8em; + padding: 0; + margin: 0; } #postContent { display: flex; @@ -95,9 +199,14 @@ padding: 0; padding-bottom: 5px; } + #quotingText { + font-size: 0.7em; + margin: 0; + padding: 0; + padding-bottom: 5px; + } #postText { margin: 0; - margin-bottom: 5px; padding: 0; } #headerText { @@ -108,20 +217,68 @@ overflow: hidden; } #avatar { - width: 50px; - height: 50px; + height: 100%; margin: 0px; margin-left: 0px; border-right: #8054f0 1px solid; } #embedImages { - width: 50%; - height: 50%; - margin-top: 0px; - margin-bottom: -5px; + min-width: 500px; + max-width: 500px; + max-height: 500px; + object-fit: contain; + + margin: 0; + } + #carouselContainer { + position: relative; + width: 100%; + margin-top: 10px; + display: flex; + flex-direction: column; + align-items: center; + } + #carouselControls { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + max-width: 500px; + margin-top: 5px; + } + #carouselIndicators { + display: flex; + gap: 5px; + } + .indicator { + width: 8px; + height: 8px; + background-color: #4a4a4a; + } + .indicator.active { + background-color: #8054f0; + } + #prevBtn, + #nextBtn { + background-color: rgba(31, 17, 69, 0.7); + color: white; + border: 1px solid #8054f0; + width: 30px; + height: 30px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + } + #prevBtn:disabled, + #nextBtn:disabled { + opacity: 0.5; + cursor: not-allowed; } #embedVideo { - width: 50%; - height: 50%; + width: 100%; + max-width: 500px; + margin-top: 10px; + align-self: center; } </style> diff --git a/src/lib/pdsfetch.ts b/src/lib/pdsfetch.ts index 61be9bf..d48e1b9 100644 --- a/src/lib/pdsfetch.ts +++ b/src/lib/pdsfetch.ts @@ -32,6 +32,7 @@ class Post { authorDid: string; authorAvatarCid: string | null; postCid: string; + recordName: string; authorHandle: string; displayName: string; text: string; @@ -47,6 +48,7 @@ class Post { account: AccountMetadata, ) { this.postCid = record.cid; + this.recordName = record.uri.split("/").slice(-1)[0]; this.authorDid = account.did; this.authorAvatarCid = account.avatarCid; this.authorHandle = account.handle;