+8
-2
config.ts
+8
-2
config.ts
···
6
6
* The base URL of the PDS (Personal Data Server)
7
7
* @default "https://pds.witchcraft.systems"
8
8
*/
9
-
static readonly PDS_URL: string = "https://ap.brid.gy";
9
+
static readonly PDS_URL: string = "https://pds.witchcraft.systems";
10
10
11
11
/**
12
12
* The base URL of the frontend service for linking to replies
···
18
18
* Maximum number of posts to fetch from the PDS per user
19
19
* @default 10
20
20
*/
21
-
static readonly MAX_POSTS_PER_USER: number = 1;
21
+
static readonly MAX_POSTS_PER_USER: number = 22;
22
+
23
+
/**
24
+
* Footer text for the dashboard
25
+
* @default "Astrally projected from witchcraft.systems"
26
+
*/
27
+
static readonly FOOTER_TEXT: string = "Astrally projected from <a href='https://witchcraft.systems' target='_blank'>witchcraft.systems</a>";
22
28
}
+3
src/App.svelte
+3
src/App.svelte
···
2
2
import PostComponent from "./lib/PostComponent.svelte";
3
3
import AccountComponent from "./lib/AccountComponent.svelte";
4
4
import { fetchAllPosts, Post, getAllMetadataFromPds } from "./lib/pdsfetch";
5
+
import { Config } from "../config";
5
6
const postsPromise = fetchAllPosts();
6
7
const accountsPromise = getAllMetadataFromPds();
7
8
</script>
···
19
20
<AccountComponent account={accountObject} />
20
21
{/each}
21
22
</div>
23
+
<p>{@html Config.FOOTER_TEXT}</p>
22
24
</div>
23
25
{:catch error}
24
26
<p>Error: {error.message}</p>
···
74
76
background-color: #0d0620;
75
77
height: 80vh;
76
78
padding: 20px;
79
+
margin-left: 20px;
77
80
}
78
81
#accountsList {
79
82
display: flex;
+1
src/app.css
+1
src/app.css
+188
-31
src/lib/PostComponent.svelte
+188
-31
src/lib/PostComponent.svelte
···
1
1
<script lang="ts">
2
2
import { Post } from "./pdsfetch";
3
3
import { Config } from "../../config";
4
+
import { onMount } from "svelte";
5
+
4
6
let { post }: { post: Post } = $props();
7
+
8
+
// State for image carousel
9
+
let currentImageIndex = $state(0);
10
+
11
+
// Functions to navigate carousel
12
+
function nextImage() {
13
+
if (post.imagesCid && currentImageIndex < post.imagesCid.length - 1) {
14
+
currentImageIndex++;
15
+
}
16
+
}
17
+
18
+
function prevImage() {
19
+
if (currentImageIndex > 0) {
20
+
currentImageIndex--;
21
+
}
22
+
}
23
+
24
+
// Function to preload an image
25
+
function preloadImage(index: number): void {
26
+
if (!post.imagesCid || index < 0 || index >= post.imagesCid.length) return;
27
+
28
+
const img = new Image();
29
+
img.src = `${Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did=${post.authorDid}&cid=${post.imagesCid[index]}`;
30
+
}
31
+
32
+
// Preload adjacent images when current index changes
33
+
$effect(() => {
34
+
if (post.imagesCid && post.imagesCid.length > 1) {
35
+
// Preload next image if available
36
+
if (currentImageIndex < post.imagesCid.length - 1) {
37
+
preloadImage(currentImageIndex + 1);
38
+
}
39
+
40
+
// Preload previous image if available
41
+
if (currentImageIndex > 0) {
42
+
preloadImage(currentImageIndex - 1);
43
+
}
44
+
}
45
+
});
46
+
47
+
// Initial preload of images
48
+
onMount(() => {
49
+
if (post.imagesCid && post.imagesCid.length > 1) {
50
+
// Preload the next image if it exists
51
+
if (post.imagesCid.length > 1) {
52
+
preloadImage(1);
53
+
}
54
+
}
55
+
});
5
56
</script>
6
57
7
58
<div id="postContainer">
···
14
65
/>
15
66
{/if}
16
67
<div id="headerText">
17
-
<a href="{Config.FRONTEND_URL}/profile/{post.authorDid}"
18
-
>{post.displayName} ( {post.authorHandle} )</a
19
-
>
20
-
|
21
-
<a href="{Config.FRONTEND_URL}/profile/{post.authorDid}/post/{post.cid}"
22
-
>{post.timenotstamp}</a
68
+
<a id="displayName" href="{Config.FRONTEND_URL}/profile/{post.authorDid}"
69
+
>{post.displayName}</a
23
70
>
71
+
<p id="handle">
72
+
<a href="{Config.FRONTEND_URL}/profile/{post.authorHandle}"
73
+
>{post.authorHandle}</a
74
+
>
75
+
76
+
<a
77
+
id="postLink"
78
+
href="{Config.FRONTEND_URL}/profile/{post.authorDid}/post/{post.recordName}"
79
+
>{post.timenotstamp}</a
80
+
>
81
+
</p>
24
82
</div>
25
83
</div>
26
84
<div id="postContent">
27
85
{#if post.replyingUri}
28
-
<a
29
-
id="replyingText"
30
-
href="{Config.FRONTEND_URL}/profile/{post.replyingUri.repo}/post/{post
31
-
.replyingUri.rkey}">replying to {post.replyingUri.repo}</a
32
-
>
86
+
<a
87
+
id="replyingText"
88
+
href="{Config.FRONTEND_URL}/profile/{post.replyingUri.repo}/post/{post
89
+
.replyingUri.rkey}">replying to {post.replyingUri.repo}</a
90
+
>
33
91
{/if}
34
-
<div id="postText">{post.text}</div>
35
92
{#if post.quotingUri}
36
93
<a
37
94
id="quotingText"
···
39
96
.quotingUri.rkey}">quoting {post.quotingUri.repo}</a
40
97
>
41
98
{/if}
42
-
{#if post.imagesCid}
43
-
<div id="imagesContainer">
44
-
{#each post.imagesCid as imageLink}
45
-
<img
46
-
id="embedImages"
47
-
alt="Post Image"
48
-
src="{Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did={post.authorDid}&cid={imageLink}"
49
-
/>
50
-
{/each}
99
+
<div id="postText">{post.text}</div>
100
+
{#if post.imagesCid && post.imagesCid.length > 0}
101
+
<div id="carouselContainer">
102
+
<img
103
+
id="embedImages"
104
+
alt="Post Image {currentImageIndex + 1} of {post.imagesCid.length}"
105
+
src="{Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did={post.authorDid}&cid={post
106
+
.imagesCid[currentImageIndex]}"
107
+
/>
108
+
109
+
{#if post.imagesCid.length > 1}
110
+
<div id="carouselControls">
111
+
<button
112
+
id="prevBtn"
113
+
on:click={prevImage}
114
+
disabled={currentImageIndex === 0}>←</button
115
+
>
116
+
<div id="carouselIndicators">
117
+
{#each post.imagesCid as _, i}
118
+
<div
119
+
class="indicator {i === currentImageIndex ? 'active' : ''}"
120
+
></div>
121
+
{/each}
122
+
</div>
123
+
<button
124
+
id="nextBtn"
125
+
on:click={nextImage}
126
+
disabled={currentImageIndex === post.imagesCid.length - 1}
127
+
>→</button
128
+
>
129
+
</div>
130
+
{/if}
51
131
</div>
52
132
{/if}
53
133
{#if post.videosLinkCid}
54
134
<video
55
135
id="embedVideo"
56
136
src="{Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did={post.authorDid}&cid={post.videosLinkCid}"
57
-
/>
137
+
controls
138
+
></video>
58
139
{/if}
59
140
</div>
60
141
</div>
61
142
62
143
<style>
144
+
a:hover {
145
+
text-decoration: underline;
146
+
}
63
147
#postContainer {
64
148
display: flex;
65
149
flex-direction: column;
···
79
163
border-bottom: 1px solid #8054f0;
80
164
font-weight: bold;
81
165
overflow-wrap: break-word;
166
+
height: 60px;
167
+
}
168
+
#displayName {
169
+
color: white;
170
+
font-size: 1.2em;
171
+
padding: 0;
172
+
margin: 0;
173
+
}
174
+
#handle {
175
+
color: #8054f0;
176
+
font-size: 0.8em;
177
+
padding: 0;
178
+
margin: 0;
179
+
}
180
+
181
+
#postLink {
182
+
color: #8054f0;
183
+
font-size: 0.8em;
184
+
padding: 0;
185
+
margin: 0;
82
186
}
83
187
#postContent {
84
188
display: flex;
···
95
199
padding: 0;
96
200
padding-bottom: 5px;
97
201
}
202
+
#quotingText {
203
+
font-size: 0.7em;
204
+
margin: 0;
205
+
padding: 0;
206
+
padding-bottom: 5px;
207
+
}
98
208
#postText {
99
209
margin: 0;
100
-
margin-bottom: 5px;
101
210
padding: 0;
102
211
}
103
212
#headerText {
···
108
217
overflow: hidden;
109
218
}
110
219
#avatar {
111
-
width: 50px;
112
-
height: 50px;
220
+
height: 100%;
113
221
margin: 0px;
114
222
margin-left: 0px;
115
223
border-right: #8054f0 1px solid;
116
224
}
117
225
#embedImages {
118
-
width: 50%;
119
-
height: 50%;
120
-
margin-top: 0px;
121
-
margin-bottom: -5px;
226
+
min-width: 500px;
227
+
max-width: 500px;
228
+
max-height: 500px;
229
+
object-fit: contain;
230
+
231
+
margin: 0;
232
+
}
233
+
#carouselContainer {
234
+
position: relative;
235
+
width: 100%;
236
+
margin-top: 10px;
237
+
display: flex;
238
+
flex-direction: column;
239
+
align-items: center;
240
+
}
241
+
#carouselControls {
242
+
display: flex;
243
+
justify-content: space-between;
244
+
align-items: center;
245
+
width: 100%;
246
+
max-width: 500px;
247
+
margin-top: 5px;
248
+
}
249
+
#carouselIndicators {
250
+
display: flex;
251
+
gap: 5px;
252
+
}
253
+
.indicator {
254
+
width: 8px;
255
+
height: 8px;
256
+
background-color: #4a4a4a;
257
+
}
258
+
.indicator.active {
259
+
background-color: #8054f0;
260
+
}
261
+
#prevBtn,
262
+
#nextBtn {
263
+
background-color: rgba(31, 17, 69, 0.7);
264
+
color: white;
265
+
border: 1px solid #8054f0;
266
+
width: 30px;
267
+
height: 30px;
268
+
cursor: pointer;
269
+
display: flex;
270
+
align-items: center;
271
+
justify-content: center;
272
+
}
273
+
#prevBtn:disabled,
274
+
#nextBtn:disabled {
275
+
opacity: 0.5;
276
+
cursor: not-allowed;
122
277
}
123
278
#embedVideo {
124
-
width: 50%;
125
-
height: 50%;
279
+
width: 100%;
280
+
max-width: 500px;
281
+
margin-top: 10px;
282
+
align-self: center;
126
283
}
127
284
</style>
+2
src/lib/pdsfetch.ts
+2
src/lib/pdsfetch.ts
···
32
32
authorDid: string;
33
33
authorAvatarCid: string | null;
34
34
postCid: string;
35
+
recordName: string;
35
36
authorHandle: string;
36
37
displayName: string;
37
38
text: string;
···
47
48
account: AccountMetadata,
48
49
) {
49
50
this.postCid = record.cid;
51
+
this.recordName = record.uri.split("/").slice(-1)[0];
50
52
this.authorDid = account.did;
51
53
this.authorAvatarCid = account.avatarCid;
52
54
this.authorHandle = account.handle;