+69
.forgejo/workflows/deploy.yaml
+69
.forgejo/workflows/deploy.yaml
···
1
+
name: Deploy
2
+
3
+
on:
4
+
push:
5
+
branches:
6
+
- main
7
+
- astra/ci
8
+
9
+
10
+
jobs:
11
+
deploy:
12
+
name: Deploy
13
+
runs-on: ubuntu-latest
14
+
15
+
steps:
16
+
- name: Checkout main repo
17
+
uses: actions/checkout@v4
18
+
19
+
- name: Checkout overrides repo
20
+
uses: actions/checkout@v4
21
+
with:
22
+
repository: scientific-witchery/pds-dash-overrides
23
+
token: ${{ secrets.OVERRIDES_TOKEN}}
24
+
path: overrides
25
+
26
+
- name: Copy config file to root
27
+
run: cp overrides/config.ts ./config.ts
28
+
29
+
- name: Setup Node.js
30
+
uses: actions/setup-node@v3
31
+
with:
32
+
node-version: '20'
33
+
34
+
- name: Setup Deno
35
+
uses: https://github.com/denoland/setup-deno@v2
36
+
37
+
- name: Install dependencies
38
+
run: deno install
39
+
40
+
- name: Build project
41
+
run: deno task build
42
+
43
+
- name: Setup SSH
44
+
run: |
45
+
mkdir -p ~/.ssh
46
+
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
47
+
chmod 600 ~/.ssh/id_ed25519
48
+
cat > ~/.ssh/config << EOF
49
+
Host deploy
50
+
HostName ${{ vars.SERVER_HOST }}
51
+
User ${{ vars.SERVER_USER }}
52
+
IdentityFile ~/.ssh/id_ed25519
53
+
StrictHostKeyChecking accept-new
54
+
BatchMode yes
55
+
PasswordAuthentication no
56
+
PubkeyAuthentication yes
57
+
EOF
58
+
chmod 600 ~/.ssh/config
59
+
ssh-keyscan -H ${{ vars.SERVER_HOST }} >> ~/.ssh/known_hosts
60
+
echo "Deploying to ${{ vars.SERVER_HOST }} as ${{ vars.SERVER_USER }} to /var/www/pds/${{ github.ref_name }}"
61
+
62
+
- name: Debug SSH Connection
63
+
run: ssh -v deploy echo "SSH Connection Successful"
64
+
65
+
- name: Create folder if not exists
66
+
run: ssh deploy "mkdir -p /var/www/pds/${{ github.ref_name }}"
67
+
68
+
- name: Deploy via SCP
69
+
run: scp -r ./dist/* deploy:/var/www/pds/${{ github.ref_name }}
+21
-2
.gitignore
+21
-2
.gitignore
···
4
4
npm-debug.log*
5
5
yarn-debug.log*
6
6
yarn-error.log*
7
+
pnpm-debug.log*
7
8
lerna-debug.log*
8
-
.pnpm-debug.log*
9
+
10
+
node_modules
11
+
dist
12
+
dist-ssr
13
+
*.local
14
+
15
+
# Editor directories and files
16
+
.vscode/*
17
+
!.vscode/extensions.json
18
+
.idea
19
+
.DS_Store
20
+
*.suo
21
+
*.ntvs*
22
+
*.njsproj
23
+
*.sln
24
+
*.sw?
9
25
10
26
# Diagnostic reports (https://nodejs.org/api/report.html)
11
27
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
···
133
149
.yarn/unplugged
134
150
.yarn/build-state.yml
135
151
.yarn/install-state.gz
136
-
.pnp.*
152
+
.pnp.*
153
+
154
+
# Config files
155
+
config.ts
+21
LICENSE
+21
LICENSE
···
1
+
# MIT License
2
+
3
+
Copyright (c) 2025 Witchcraft Systems
4
+
5
+
Permission is hereby granted, free of charge, to any person obtaining a copy
6
+
of this software and associated documentation files (the "Software"), to deal
7
+
in the Software without restriction, including without limitation the rights
8
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+
copies of the Software, and to permit persons to whom the Software is
10
+
furnished to do so, subject to the following conditions:
11
+
12
+
The above copyright notice and this permission notice shall be included in all
13
+
copies or substantial portions of the Software.
14
+
15
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+
SOFTWARE.
+62
-1
README.md
+62
-1
README.md
···
1
1
# pds-dash
2
2
3
-
Frontend with stats for your ATProto PDS
3
+
a frontend dashboard with stats for your ATProto PDS.
4
+
5
+
## setup
6
+
7
+
### prerequisites
8
+
9
+
- [deno](https://deno.com/manual/getting_started/installation)
10
+
11
+
### installing
12
+
13
+
clone the repo, copy `config.ts.example` to `config.ts` and edit it to your liking.
14
+
15
+
then, install dependencies using deno:
16
+
17
+
```sh
18
+
deno install
19
+
```
20
+
21
+
### development server
22
+
23
+
local develompent server with hot reloading:
24
+
25
+
```sh
26
+
deno task dev
27
+
```
28
+
29
+
### building
30
+
31
+
to build the optimized bundle run:
32
+
33
+
```sh
34
+
deno task build
35
+
```
36
+
37
+
the output will be in the `dist/` directory.
38
+
39
+
## deploying
40
+
41
+
we use our own CI/CD workflow at [`.forgejo/workflows/deploy.yaml`](.forgejo/workflows/deploy.yaml), but it boils down to building the project bundle and deploying it to a web server. it'll probably make more sense to host it on the same domain as your PDS, but it doesn't affect anything if you host it somewhere else.
42
+
43
+
## configuring
44
+
45
+
[`config.ts`](config.ts) is the main configuration file, you can find more information in the file itself.
46
+
47
+
## theming
48
+
49
+
the colors are designated in [`src/app.css`](src/app.css) as variables, go crazy with them
50
+
51
+
the rest is done by editing the css files and style tags directly, good luck
52
+
53
+
relevant files:
54
+
55
+
- [`src/App.svelte`](src/App.svelte)
56
+
- [`src/app.css`](src/app.css)
57
+
- [`src/lib/AccountComponent.svelte`](src/lib/AccountComponent.svelte)
58
+
- [`src/lib/PostComponent.svelte`](src/lib/PostComponent.svelte)
59
+
60
+
the favicon is located at [`public/favicon.ico`](public/favicon.ico)
61
+
62
+
## license
63
+
64
+
MIT
+44
config.ts.example
+44
config.ts.example
···
1
+
/**
2
+
* Configuration module for the PDS Dashboard
3
+
*/
4
+
export class Config {
5
+
/**
6
+
* The base URL of the PDS (Personal Data Server).
7
+
* @default none
8
+
*/
9
+
static readonly PDS_URL: string = "";
10
+
11
+
/**
12
+
* Theme to be used
13
+
* @default "default"
14
+
*/
15
+
static readonly THEME: string = "default";
16
+
17
+
/**
18
+
* The base URL of the frontend service for linking to replies/quotes/accounts etc.
19
+
* @default "https://deer.social" // or https://bsky.app if you're boring
20
+
*/
21
+
static readonly FRONTEND_URL: string = "https://deer.social";
22
+
23
+
/**
24
+
* Maximum number of posts to fetch from the PDS per request
25
+
* Should be around 20 for about 10 users on the pds
26
+
* The more users you have, the lower the number should be
27
+
* since sorting is slow and is done on the frontend
28
+
* @default 20
29
+
*/
30
+
static readonly MAX_POSTS: number = 20;
31
+
32
+
/**
33
+
* Footer text for the dashboard, you probably want to change this. Supports HTML.
34
+
* @default "<a href='https://git.witchcraft.systems/scientific-witchery/pds-dash' target='_blank'>Source</a> (<a href='https://github.com/witchcraft-systems/pds-dash/' target='_blank'>github mirror</a>)"
35
+
*/
36
+
static readonly FOOTER_TEXT: string =
37
+
"<a href='https://git.witchcraft.systems/scientific-witchery/pds-dash' target='_blank'>Source</a> (<a href='https://github.com/witchcraft-systems/pds-dash/' target='_blank'>github mirror</a>)";
38
+
39
+
/**
40
+
* Whether to show the posts with timestamps that are in the future.
41
+
* @default false
42
+
*/
43
+
static readonly SHOW_FUTURE_POSTS: boolean = false;
44
+
}
-12
deno.json
-12
deno.json
···
1
-
{
2
-
"tasks": {
3
-
"dev": "deno run --watch main.ts"
4
-
},
5
-
"imports": {
6
-
"@atcute/bluesky": "npm:@atcute/bluesky@^2.0.2",
7
-
"@atcute/client": "npm:@atcute/client@^3.0.1",
8
-
"@skyware/jetstream": "npm:@skyware/jetstream@^0.2.2",
9
-
"@std/assert": "jsr:@std/assert@1",
10
-
"dotenv": "npm:dotenv@^16.5.0"
11
-
}
12
-
}
+575
-37
deno.lock
+575
-37
deno.lock
···
1
1
{
2
-
"version": "4",
2
+
"version": "5",
3
3
"specifiers": {
4
-
"jsr:@std/assert@1": "1.0.12",
5
-
"jsr:@std/internal@^1.0.6": "1.0.6",
6
4
"npm:@atcute/bluesky@^2.0.2": "2.0.2_@atcute+client@3.0.1",
7
5
"npm:@atcute/client@^3.0.1": "3.0.1",
8
-
"npm:@skyware/jetstream@~0.2.2": "0.2.2_@atcute+client@3.0.1",
9
-
"npm:dotenv@^16.5.0": "16.5.0"
10
-
},
11
-
"jsr": {
12
-
"@std/assert@1.0.12": {
13
-
"integrity": "08009f0926dda9cbd8bef3a35d3b6a4b964b0ab5c3e140a4e0351fbf34af5b9a",
14
-
"dependencies": [
15
-
"jsr:@std/internal"
16
-
]
17
-
},
18
-
"@std/internal@1.0.6": {
19
-
"integrity": "9533b128f230f73bd209408bb07a4b12f8d4255ab2a4d22a1fd6d87304aca9a4"
20
-
}
6
+
"npm:@atcute/identity-resolver@~0.1.2": "0.1.2_@atcute+identity@0.1.3",
7
+
"npm:@sveltejs/vite-plugin-svelte@^5.0.3": "5.0.3_svelte@5.28.1__acorn@8.14.1_vite@6.3.2__picomatch@4.0.2",
8
+
"npm:@tsconfig/svelte@^5.0.4": "5.0.4",
9
+
"npm:moment@^2.30.1": "2.30.1",
10
+
"npm:mutex-ts@^1.2.1": "1.2.1",
11
+
"npm:svelte-check@^4.1.5": "4.1.6_svelte@5.28.1__acorn@8.14.1_typescript@5.7.3",
12
+
"npm:svelte-infinite-loading@^1.4.0": "1.4.0",
13
+
"npm:svelte@^5.23.1": "5.28.1_acorn@8.14.1",
14
+
"npm:typescript@~5.7.2": "5.7.3",
15
+
"npm:vite@^6.3.1": "6.3.2_picomatch@4.0.2"
21
16
},
22
17
"npm": {
23
-
"@atcute/bluesky@1.0.15_@atcute+client@3.0.1": {
24
-
"integrity": "sha512-+EFiybmKQ97aBAgtaD+cKRJER5AMn3cZMkEwEg/pDdWyzxYJ9m1UgemmLdTgI8VrxPufKqdXS2nl7uO7TY6BPA==",
18
+
"@ampproject/remapping@2.3.0": {
19
+
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
25
20
"dependencies": [
26
-
"@atcute/client"
21
+
"@jridgewell/gen-mapping",
22
+
"@jridgewell/trace-mapping"
27
23
]
28
24
},
29
25
"@atcute/bluesky@2.0.2_@atcute+client@3.0.1": {
···
35
31
"@atcute/client@3.0.1": {
36
32
"integrity": "sha512-j51SuQYQj5jeKrx1DCXx+Q3fpVvO0JYGnKnJAdDSlesSLjPXjvnx1abC+hkuro58KRHHJvRA6P1MC0pmJsWfcg=="
37
33
},
38
-
"@skyware/jetstream@0.2.2_@atcute+client@3.0.1": {
39
-
"integrity": "sha512-d1MtWPTIFEciSzV8OClXZCJoz0DJ7aupt4EZSwpGAASYG0ZIPmZTt7RVJkoFzQyqRPHAMD7CvEwu0ut3MHX1og==",
34
+
"@atcute/identity-resolver@0.1.2_@atcute+identity@0.1.3": {
35
+
"integrity": "sha512-fP2VbHD04kVcCdNi/Kszo6jFzqM7Pg3p33oGhfp2zVkwFKaVBlwCaFRWEga/Xvu/IDLwNdASGWnLqoA34SFeSg==",
36
+
"dependencies": [
37
+
"@atcute/identity",
38
+
"@atcute/util-fetch",
39
+
"@badrap/valita"
40
+
]
41
+
},
42
+
"@atcute/identity@0.1.3": {
43
+
"integrity": "sha512-ndlD8nypHt8G00wixbozKdSNL0O8HTzBjFGEXeAcBUCXSZPBjRWbqtgyJxhgUWnr7swgxgw1mSbZwRB5b7xCiQ==",
44
+
"dependencies": [
45
+
"@badrap/valita"
46
+
]
47
+
},
48
+
"@atcute/util-fetch@1.0.1": {
49
+
"integrity": "sha512-Clc0E/5ufyGBVfYBUwWNlHONlZCoblSr4Ho50l1LhmRPGB1Wu/AQ9Sz+rsBg7fdaW/auve8ulmwhRhnX2cGRow==",
40
50
"dependencies": [
41
-
"@atcute/bluesky@1.0.15_@atcute+client@3.0.1",
42
-
"partysocket"
51
+
"@badrap/valita"
43
52
]
44
53
},
45
-
"dotenv@16.5.0": {
46
-
"integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg=="
54
+
"@badrap/valita@0.4.4": {
55
+
"integrity": "sha512-GEhUCk9c4XbNxi+0YZHZsV4fYNd6HejfWuN4Ti4c02DauX+LyX5WY1Y3WfyZ8Pxxl0zqhs+MLtW98cMh86vv6g=="
56
+
},
57
+
"@esbuild/aix-ppc64@0.25.2": {
58
+
"integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==",
59
+
"os": ["aix"],
60
+
"cpu": ["ppc64"]
61
+
},
62
+
"@esbuild/android-arm64@0.25.2": {
63
+
"integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==",
64
+
"os": ["android"],
65
+
"cpu": ["arm64"]
66
+
},
67
+
"@esbuild/android-arm@0.25.2": {
68
+
"integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==",
69
+
"os": ["android"],
70
+
"cpu": ["arm"]
71
+
},
72
+
"@esbuild/android-x64@0.25.2": {
73
+
"integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==",
74
+
"os": ["android"],
75
+
"cpu": ["x64"]
47
76
},
48
-
"event-target-polyfill@0.0.4": {
49
-
"integrity": "sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ=="
77
+
"@esbuild/darwin-arm64@0.25.2": {
78
+
"integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==",
79
+
"os": ["darwin"],
80
+
"cpu": ["arm64"]
81
+
},
82
+
"@esbuild/darwin-x64@0.25.2": {
83
+
"integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==",
84
+
"os": ["darwin"],
85
+
"cpu": ["x64"]
50
86
},
51
-
"partysocket@1.1.3": {
52
-
"integrity": "sha512-87Jd/nqPoWnVfzHE6Z12WLWTJ+TAgxs0b7i2S163HfQSrVDUK5tW/FC64T5N8L5ss+gqF+EV0BwjZMWggMY3UA==",
87
+
"@esbuild/freebsd-arm64@0.25.2": {
88
+
"integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==",
89
+
"os": ["freebsd"],
90
+
"cpu": ["arm64"]
91
+
},
92
+
"@esbuild/freebsd-x64@0.25.2": {
93
+
"integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==",
94
+
"os": ["freebsd"],
95
+
"cpu": ["x64"]
96
+
},
97
+
"@esbuild/linux-arm64@0.25.2": {
98
+
"integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==",
99
+
"os": ["linux"],
100
+
"cpu": ["arm64"]
101
+
},
102
+
"@esbuild/linux-arm@0.25.2": {
103
+
"integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==",
104
+
"os": ["linux"],
105
+
"cpu": ["arm"]
106
+
},
107
+
"@esbuild/linux-ia32@0.25.2": {
108
+
"integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==",
109
+
"os": ["linux"],
110
+
"cpu": ["ia32"]
111
+
},
112
+
"@esbuild/linux-loong64@0.25.2": {
113
+
"integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==",
114
+
"os": ["linux"],
115
+
"cpu": ["loong64"]
116
+
},
117
+
"@esbuild/linux-mips64el@0.25.2": {
118
+
"integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==",
119
+
"os": ["linux"],
120
+
"cpu": ["mips64el"]
121
+
},
122
+
"@esbuild/linux-ppc64@0.25.2": {
123
+
"integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==",
124
+
"os": ["linux"],
125
+
"cpu": ["ppc64"]
126
+
},
127
+
"@esbuild/linux-riscv64@0.25.2": {
128
+
"integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==",
129
+
"os": ["linux"],
130
+
"cpu": ["riscv64"]
131
+
},
132
+
"@esbuild/linux-s390x@0.25.2": {
133
+
"integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==",
134
+
"os": ["linux"],
135
+
"cpu": ["s390x"]
136
+
},
137
+
"@esbuild/linux-x64@0.25.2": {
138
+
"integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==",
139
+
"os": ["linux"],
140
+
"cpu": ["x64"]
141
+
},
142
+
"@esbuild/netbsd-arm64@0.25.2": {
143
+
"integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==",
144
+
"os": ["netbsd"],
145
+
"cpu": ["arm64"]
146
+
},
147
+
"@esbuild/netbsd-x64@0.25.2": {
148
+
"integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==",
149
+
"os": ["netbsd"],
150
+
"cpu": ["x64"]
151
+
},
152
+
"@esbuild/openbsd-arm64@0.25.2": {
153
+
"integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==",
154
+
"os": ["openbsd"],
155
+
"cpu": ["arm64"]
156
+
},
157
+
"@esbuild/openbsd-x64@0.25.2": {
158
+
"integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==",
159
+
"os": ["openbsd"],
160
+
"cpu": ["x64"]
161
+
},
162
+
"@esbuild/sunos-x64@0.25.2": {
163
+
"integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==",
164
+
"os": ["sunos"],
165
+
"cpu": ["x64"]
166
+
},
167
+
"@esbuild/win32-arm64@0.25.2": {
168
+
"integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==",
169
+
"os": ["win32"],
170
+
"cpu": ["arm64"]
171
+
},
172
+
"@esbuild/win32-ia32@0.25.2": {
173
+
"integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==",
174
+
"os": ["win32"],
175
+
"cpu": ["ia32"]
176
+
},
177
+
"@esbuild/win32-x64@0.25.2": {
178
+
"integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==",
179
+
"os": ["win32"],
180
+
"cpu": ["x64"]
181
+
},
182
+
"@jridgewell/gen-mapping@0.3.8": {
183
+
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
53
184
"dependencies": [
54
-
"event-target-polyfill"
185
+
"@jridgewell/set-array",
186
+
"@jridgewell/sourcemap-codec",
187
+
"@jridgewell/trace-mapping"
188
+
]
189
+
},
190
+
"@jridgewell/resolve-uri@3.1.2": {
191
+
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="
192
+
},
193
+
"@jridgewell/set-array@1.2.1": {
194
+
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="
195
+
},
196
+
"@jridgewell/sourcemap-codec@1.5.0": {
197
+
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="
198
+
},
199
+
"@jridgewell/trace-mapping@0.3.25": {
200
+
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
201
+
"dependencies": [
202
+
"@jridgewell/resolve-uri",
203
+
"@jridgewell/sourcemap-codec"
55
204
]
205
+
},
206
+
"@rollup/rollup-android-arm-eabi@4.40.0": {
207
+
"integrity": "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==",
208
+
"os": ["android"],
209
+
"cpu": ["arm"]
210
+
},
211
+
"@rollup/rollup-android-arm64@4.40.0": {
212
+
"integrity": "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==",
213
+
"os": ["android"],
214
+
"cpu": ["arm64"]
215
+
},
216
+
"@rollup/rollup-darwin-arm64@4.40.0": {
217
+
"integrity": "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==",
218
+
"os": ["darwin"],
219
+
"cpu": ["arm64"]
220
+
},
221
+
"@rollup/rollup-darwin-x64@4.40.0": {
222
+
"integrity": "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==",
223
+
"os": ["darwin"],
224
+
"cpu": ["x64"]
225
+
},
226
+
"@rollup/rollup-freebsd-arm64@4.40.0": {
227
+
"integrity": "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==",
228
+
"os": ["freebsd"],
229
+
"cpu": ["arm64"]
230
+
},
231
+
"@rollup/rollup-freebsd-x64@4.40.0": {
232
+
"integrity": "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==",
233
+
"os": ["freebsd"],
234
+
"cpu": ["x64"]
235
+
},
236
+
"@rollup/rollup-linux-arm-gnueabihf@4.40.0": {
237
+
"integrity": "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==",
238
+
"os": ["linux"],
239
+
"cpu": ["arm"]
240
+
},
241
+
"@rollup/rollup-linux-arm-musleabihf@4.40.0": {
242
+
"integrity": "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==",
243
+
"os": ["linux"],
244
+
"cpu": ["arm"]
245
+
},
246
+
"@rollup/rollup-linux-arm64-gnu@4.40.0": {
247
+
"integrity": "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==",
248
+
"os": ["linux"],
249
+
"cpu": ["arm64"]
250
+
},
251
+
"@rollup/rollup-linux-arm64-musl@4.40.0": {
252
+
"integrity": "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==",
253
+
"os": ["linux"],
254
+
"cpu": ["arm64"]
255
+
},
256
+
"@rollup/rollup-linux-loongarch64-gnu@4.40.0": {
257
+
"integrity": "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==",
258
+
"os": ["linux"],
259
+
"cpu": ["loong64"]
260
+
},
261
+
"@rollup/rollup-linux-powerpc64le-gnu@4.40.0": {
262
+
"integrity": "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==",
263
+
"os": ["linux"],
264
+
"cpu": ["ppc64"]
265
+
},
266
+
"@rollup/rollup-linux-riscv64-gnu@4.40.0": {
267
+
"integrity": "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==",
268
+
"os": ["linux"],
269
+
"cpu": ["riscv64"]
270
+
},
271
+
"@rollup/rollup-linux-riscv64-musl@4.40.0": {
272
+
"integrity": "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==",
273
+
"os": ["linux"],
274
+
"cpu": ["riscv64"]
275
+
},
276
+
"@rollup/rollup-linux-s390x-gnu@4.40.0": {
277
+
"integrity": "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==",
278
+
"os": ["linux"],
279
+
"cpu": ["s390x"]
280
+
},
281
+
"@rollup/rollup-linux-x64-gnu@4.40.0": {
282
+
"integrity": "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==",
283
+
"os": ["linux"],
284
+
"cpu": ["x64"]
285
+
},
286
+
"@rollup/rollup-linux-x64-musl@4.40.0": {
287
+
"integrity": "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==",
288
+
"os": ["linux"],
289
+
"cpu": ["x64"]
290
+
},
291
+
"@rollup/rollup-win32-arm64-msvc@4.40.0": {
292
+
"integrity": "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==",
293
+
"os": ["win32"],
294
+
"cpu": ["arm64"]
295
+
},
296
+
"@rollup/rollup-win32-ia32-msvc@4.40.0": {
297
+
"integrity": "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==",
298
+
"os": ["win32"],
299
+
"cpu": ["ia32"]
300
+
},
301
+
"@rollup/rollup-win32-x64-msvc@4.40.0": {
302
+
"integrity": "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==",
303
+
"os": ["win32"],
304
+
"cpu": ["x64"]
305
+
},
306
+
"@sveltejs/acorn-typescript@1.0.5_acorn@8.14.1": {
307
+
"integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==",
308
+
"dependencies": [
309
+
"acorn"
310
+
]
311
+
},
312
+
"@sveltejs/vite-plugin-svelte-inspector@4.0.1_@sveltejs+vite-plugin-svelte@5.0.3__svelte@5.28.1___acorn@8.14.1__vite@6.3.2___picomatch@4.0.2_svelte@5.28.1__acorn@8.14.1_vite@6.3.2__picomatch@4.0.2": {
313
+
"integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==",
314
+
"dependencies": [
315
+
"@sveltejs/vite-plugin-svelte",
316
+
"debug",
317
+
"svelte",
318
+
"vite"
319
+
]
320
+
},
321
+
"@sveltejs/vite-plugin-svelte@5.0.3_svelte@5.28.1__acorn@8.14.1_vite@6.3.2__picomatch@4.0.2": {
322
+
"integrity": "sha512-MCFS6CrQDu1yGwspm4qtli0e63vaPCehf6V7pIMP15AsWgMKrqDGCPFF/0kn4SP0ii4aySu4Pa62+fIRGFMjgw==",
323
+
"dependencies": [
324
+
"@sveltejs/vite-plugin-svelte-inspector",
325
+
"debug",
326
+
"deepmerge",
327
+
"kleur",
328
+
"magic-string",
329
+
"svelte",
330
+
"vite",
331
+
"vitefu"
332
+
]
333
+
},
334
+
"@tsconfig/svelte@5.0.4": {
335
+
"integrity": "sha512-BV9NplVgLmSi4mwKzD8BD/NQ8erOY/nUE/GpgWe2ckx+wIQF5RyRirn/QsSSCPeulVpc3RA/iJt6DpfTIZps0Q=="
336
+
},
337
+
"@types/estree@1.0.7": {
338
+
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="
339
+
},
340
+
"acorn@8.14.1": {
341
+
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
342
+
"bin": true
343
+
},
344
+
"aria-query@5.3.2": {
345
+
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="
346
+
},
347
+
"axobject-query@4.1.0": {
348
+
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="
349
+
},
350
+
"chokidar@4.0.3": {
351
+
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
352
+
"dependencies": [
353
+
"readdirp"
354
+
]
355
+
},
356
+
"clsx@2.1.1": {
357
+
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="
358
+
},
359
+
"debug@4.4.0": {
360
+
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
361
+
"dependencies": [
362
+
"ms"
363
+
]
364
+
},
365
+
"deepmerge@4.3.1": {
366
+
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="
367
+
},
368
+
"esbuild@0.25.2": {
369
+
"integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==",
370
+
"optionalDependencies": [
371
+
"@esbuild/aix-ppc64",
372
+
"@esbuild/android-arm",
373
+
"@esbuild/android-arm64",
374
+
"@esbuild/android-x64",
375
+
"@esbuild/darwin-arm64",
376
+
"@esbuild/darwin-x64",
377
+
"@esbuild/freebsd-arm64",
378
+
"@esbuild/freebsd-x64",
379
+
"@esbuild/linux-arm",
380
+
"@esbuild/linux-arm64",
381
+
"@esbuild/linux-ia32",
382
+
"@esbuild/linux-loong64",
383
+
"@esbuild/linux-mips64el",
384
+
"@esbuild/linux-ppc64",
385
+
"@esbuild/linux-riscv64",
386
+
"@esbuild/linux-s390x",
387
+
"@esbuild/linux-x64",
388
+
"@esbuild/netbsd-arm64",
389
+
"@esbuild/netbsd-x64",
390
+
"@esbuild/openbsd-arm64",
391
+
"@esbuild/openbsd-x64",
392
+
"@esbuild/sunos-x64",
393
+
"@esbuild/win32-arm64",
394
+
"@esbuild/win32-ia32",
395
+
"@esbuild/win32-x64"
396
+
],
397
+
"scripts": true,
398
+
"bin": true
399
+
},
400
+
"esm-env@1.2.2": {
401
+
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="
402
+
},
403
+
"esrap@1.4.6": {
404
+
"integrity": "sha512-F/D2mADJ9SHY3IwksD4DAXjTt7qt7GWUf3/8RhCNWmC/67tyb55dpimHmy7EplakFaflV0R/PC+fdSPqrRHAQw==",
405
+
"dependencies": [
406
+
"@jridgewell/sourcemap-codec"
407
+
]
408
+
},
409
+
"fdir@6.4.4_picomatch@4.0.2": {
410
+
"integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
411
+
"dependencies": [
412
+
"picomatch"
413
+
],
414
+
"optionalPeers": [
415
+
"picomatch"
416
+
]
417
+
},
418
+
"fsevents@2.3.3": {
419
+
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
420
+
"os": ["darwin"],
421
+
"scripts": true
422
+
},
423
+
"is-reference@3.0.3": {
424
+
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
425
+
"dependencies": [
426
+
"@types/estree"
427
+
]
428
+
},
429
+
"kleur@4.1.5": {
430
+
"integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="
431
+
},
432
+
"locate-character@3.0.0": {
433
+
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="
434
+
},
435
+
"magic-string@0.30.17": {
436
+
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
437
+
"dependencies": [
438
+
"@jridgewell/sourcemap-codec"
439
+
]
440
+
},
441
+
"moment@2.30.1": {
442
+
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="
443
+
},
444
+
"mri@1.2.0": {
445
+
"integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="
446
+
},
447
+
"ms@2.1.3": {
448
+
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
449
+
},
450
+
"mutex-ts@1.2.1": {
451
+
"integrity": "sha512-OkcXgf0viuCgYdnm48kiNQ9PzC5OzISQ261svHr/Ybc2vBYC/5xfLXn44hQ+dYRX74v7MCSqV/LKPEbpYdDybw=="
452
+
},
453
+
"nanoid@3.3.11": {
454
+
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
455
+
"bin": true
456
+
},
457
+
"picocolors@1.1.1": {
458
+
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
459
+
},
460
+
"picomatch@4.0.2": {
461
+
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="
462
+
},
463
+
"postcss@8.5.3": {
464
+
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
465
+
"dependencies": [
466
+
"nanoid",
467
+
"picocolors",
468
+
"source-map-js"
469
+
]
470
+
},
471
+
"readdirp@4.1.2": {
472
+
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="
473
+
},
474
+
"rollup@4.40.0": {
475
+
"integrity": "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==",
476
+
"dependencies": [
477
+
"@types/estree"
478
+
],
479
+
"optionalDependencies": [
480
+
"@rollup/rollup-android-arm-eabi",
481
+
"@rollup/rollup-android-arm64",
482
+
"@rollup/rollup-darwin-arm64",
483
+
"@rollup/rollup-darwin-x64",
484
+
"@rollup/rollup-freebsd-arm64",
485
+
"@rollup/rollup-freebsd-x64",
486
+
"@rollup/rollup-linux-arm-gnueabihf",
487
+
"@rollup/rollup-linux-arm-musleabihf",
488
+
"@rollup/rollup-linux-arm64-gnu",
489
+
"@rollup/rollup-linux-arm64-musl",
490
+
"@rollup/rollup-linux-loongarch64-gnu",
491
+
"@rollup/rollup-linux-powerpc64le-gnu",
492
+
"@rollup/rollup-linux-riscv64-gnu",
493
+
"@rollup/rollup-linux-riscv64-musl",
494
+
"@rollup/rollup-linux-s390x-gnu",
495
+
"@rollup/rollup-linux-x64-gnu",
496
+
"@rollup/rollup-linux-x64-musl",
497
+
"@rollup/rollup-win32-arm64-msvc",
498
+
"@rollup/rollup-win32-ia32-msvc",
499
+
"@rollup/rollup-win32-x64-msvc",
500
+
"fsevents"
501
+
],
502
+
"bin": true
503
+
},
504
+
"sade@1.8.1": {
505
+
"integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
506
+
"dependencies": [
507
+
"mri"
508
+
]
509
+
},
510
+
"source-map-js@1.2.1": {
511
+
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="
512
+
},
513
+
"svelte-check@4.1.6_svelte@5.28.1__acorn@8.14.1_typescript@5.7.3": {
514
+
"integrity": "sha512-P7w/6tdSfk3zEVvfsgrp3h3DFC75jCdZjTQvgGJtjPORs1n7/v2VMPIoty3PWv7jnfEm3x0G/p9wH4pecTb0Wg==",
515
+
"dependencies": [
516
+
"@jridgewell/trace-mapping",
517
+
"chokidar",
518
+
"fdir",
519
+
"picocolors",
520
+
"sade",
521
+
"svelte",
522
+
"typescript"
523
+
],
524
+
"bin": true
525
+
},
526
+
"svelte-infinite-loading@1.4.0": {
527
+
"integrity": "sha512-Jo+f/yr/HmZQuIiiKKzAHVFXdAUWHW2RBbrcQTil8JVk1sCm/riy7KTJVzjBgQvHasrFQYKF84zvtc9/Y4lFYg=="
528
+
},
529
+
"svelte@5.28.1_acorn@8.14.1": {
530
+
"integrity": "sha512-iOa9WmfNG95lSOSJdMhdjJ4Afok7IRAQYXpbnxhd5EINnXseG0GVa9j6WPght4eX78XfFez45Fi+uRglGKPV/Q==",
531
+
"dependencies": [
532
+
"@ampproject/remapping",
533
+
"@jridgewell/sourcemap-codec",
534
+
"@sveltejs/acorn-typescript",
535
+
"@types/estree",
536
+
"acorn",
537
+
"aria-query",
538
+
"axobject-query",
539
+
"clsx",
540
+
"esm-env",
541
+
"esrap",
542
+
"is-reference",
543
+
"locate-character",
544
+
"magic-string",
545
+
"zimmerframe"
546
+
]
547
+
},
548
+
"tinyglobby@0.2.13_picomatch@4.0.2": {
549
+
"integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
550
+
"dependencies": [
551
+
"fdir",
552
+
"picomatch"
553
+
]
554
+
},
555
+
"typescript@5.7.3": {
556
+
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
557
+
"bin": true
558
+
},
559
+
"vite@6.3.2_picomatch@4.0.2": {
560
+
"integrity": "sha512-ZSvGOXKGceizRQIZSz7TGJ0pS3QLlVY/9hwxVh17W3re67je1RKYzFHivZ/t0tubU78Vkyb9WnHPENSBCzbckg==",
561
+
"dependencies": [
562
+
"esbuild",
563
+
"fdir",
564
+
"picomatch",
565
+
"postcss",
566
+
"rollup",
567
+
"tinyglobby"
568
+
],
569
+
"optionalDependencies": [
570
+
"fsevents"
571
+
],
572
+
"bin": true
573
+
},
574
+
"vitefu@1.0.6_vite@6.3.2__picomatch@4.0.2": {
575
+
"integrity": "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA==",
576
+
"dependencies": [
577
+
"vite"
578
+
],
579
+
"optionalPeers": [
580
+
"vite"
581
+
]
582
+
},
583
+
"zimmerframe@1.1.2": {
584
+
"integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w=="
56
585
}
57
586
},
58
587
"workspace": {
59
-
"dependencies": [
60
-
"jsr:@std/assert@1",
61
-
"npm:@atcute/bluesky@^2.0.2",
62
-
"npm:@atcute/client@^3.0.1",
63
-
"npm:@skyware/jetstream@~0.2.2",
64
-
"npm:dotenv@^16.5.0"
65
-
]
588
+
"packageJson": {
589
+
"dependencies": [
590
+
"npm:@atcute/bluesky@^2.0.2",
591
+
"npm:@atcute/client@^3.0.1",
592
+
"npm:@atcute/identity-resolver@~0.1.2",
593
+
"npm:@sveltejs/vite-plugin-svelte@^5.0.3",
594
+
"npm:@tsconfig/svelte@^5.0.4",
595
+
"npm:moment@^2.30.1",
596
+
"npm:mutex-ts@^1.2.1",
597
+
"npm:svelte-check@^4.1.5",
598
+
"npm:svelte-infinite-loading@^1.4.0",
599
+
"npm:svelte@^5.23.1",
600
+
"npm:typescript@~5.7.2",
601
+
"npm:vite@^6.3.1"
602
+
]
603
+
}
66
604
}
67
605
}
+12
index.html
+12
index.html
···
1
+
<!DOCTYPE html>
2
+
<html lang="en">
3
+
<head>
4
+
<meta charset="UTF-8" />
5
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+
<title>ATProto PDS</title>
7
+
</head>
8
+
<body>
9
+
<div id="app"></div>
10
+
<script type="module" src="/src/main.ts"></script>
11
+
</body>
12
+
</html>
-119
main.ts
-119
main.ts
···
1
-
import { simpleFetchHandler, XRPC } from "@atcute/client";
2
-
import "@atcute/bluesky/lexicons";
3
-
import { ComAtprotoRepoListRecords } from "@atcute/client/lexicons";
4
-
import { AppBskyFeedPost } from "@atcute/client/lexicons";
5
-
import { AppBskyActorDefs } from "@atcute/client/lexicons";
6
-
7
-
interface AccountMetadata {
8
-
did: string;
9
-
displayName: string;
10
-
avatarCid: string | null;
11
-
}
12
-
class Post {
13
-
text: string;
14
-
timestamp: number;
15
-
quotingDid: string | null;
16
-
replyingDid: string | null;
17
-
imagesLinksCid: string[] | null;
18
-
videosLinkCid: string | null;
19
-
constructor(record : ComAtprotoRepoListRecords.Record) {
20
-
const post = record.value as AppBskyFeedPost.Record;
21
-
this.text = post.text;
22
-
this.timestamp = Date.parse(post.createdAt);
23
-
if (post.reply) {
24
-
this.replyingDid = didFromATuri(post.reply.parent.uri).repo;
25
-
} else {
26
-
this.replyingDid = null;
27
-
}
28
-
this.quotingDid = null;
29
-
this.imagesLinksCid = null;
30
-
this.videosLinkCid = null;
31
-
switch (post.embed?.$type) {
32
-
case "app.bsky.embed.images":
33
-
this.imagesLinksCid = post.embed.images.map ((imageRecord) => imageRecord.image.ref.$link);
34
-
break;
35
-
case "app.bsky.embed.video":
36
-
this.videosLinkCid = post.embed.video.ref.$link;
37
-
break;
38
-
case "app.bsky.embed.record":
39
-
this.quotingDid = didFromATuri(post.embed.record.uri).repo;
40
-
break;
41
-
case "app.bsky.embed.recordWithMedia":
42
-
this.quotingDid = didFromATuri(post.embed.record.record.uri).repo;
43
-
switch (post.embed.media.$type) {
44
-
case "app.bsky.embed.images":
45
-
this.imagesLinksCid = post.embed.media.images.map ((imageRecord) => imageRecord.image.ref.$link);
46
-
break;
47
-
case "app.bsky.embed.video":
48
-
this.videosLinkCid = post.embed.media.video.ref.$link;
49
-
break;
50
-
}
51
-
break;
52
-
}
53
-
}
54
-
}
55
-
56
-
const didFromATuri = (aturi : string) => {
57
-
const parts = aturi.split('/');
58
-
return {
59
-
repo: parts[2],
60
-
collection: parts[3],
61
-
rkey: parts[4]
62
-
};
63
-
}
64
-
65
-
const rpc = new XRPC({
66
-
handler: simpleFetchHandler({
67
-
service: Deno.env.get("PDS_URL") || "https://pds.witchcraft.systems",
68
-
}),
69
-
});
70
-
71
-
const getDidsFromPDS = async () => {
72
-
const { data } = await rpc.get("com.atproto.sync.listRepos", {
73
-
params: {},
74
-
});
75
-
return data.repos.map((repo: any) => (repo.did));
76
-
};
77
-
const getAccountMetadata = async (did: `did:${string}:${string}`) => {
78
-
// gonna assume self exists in the app.bsky.actor.profile
79
-
const { data } = await rpc.get("com.atproto.repo.getRecord", {
80
-
params: {
81
-
repo: did,
82
-
collection: "app.bsky.actor.profile",
83
-
rkey: "self",
84
-
},
85
-
});
86
-
const value = data.value as AppBskyActorDefs.ProfileView;
87
-
const account: AccountMetadata = {
88
-
did: did,
89
-
displayName: value.displayName || "",
90
-
avatarCid: null,
91
-
};
92
-
if (value.avatar) {
93
-
account.avatarCid = value.avatar.ref["$link"];
94
-
}
95
-
return account;
96
-
};
97
-
98
-
const getAllMetadataFromPds = async () => {
99
-
const dids = await getDidsFromPDS();
100
-
const metadata = await Promise.all(
101
-
dids.map(async (repo: `did:${string}:${string}`) => {
102
-
return await getAccountMetadata(repo);
103
-
}),
104
-
);
105
-
return metadata;
106
-
};
107
-
108
-
const fetchPosts = async (did: string) => {
109
-
const { data } = await rpc.get("com.atproto.repo.listRecords", {
110
-
params: {
111
-
repo: did,
112
-
collection: "app.bsky.feed.post",
113
-
limit: 5
114
-
}
115
-
});
116
-
return data.records as ComAtprotoRepoListRecords.Record[];
117
-
}
118
-
// console.log((await fetchPosts("did:web:astrra.space")).map((record : any) => new Post(record)))
119
-
console.log(await getAccountMetadata("did:web:astrra.space"));
+28
package.json
+28
package.json
···
1
+
{
2
+
"name": "web",
3
+
"private": true,
4
+
"version": "0.0.0",
5
+
"type": "module",
6
+
"scripts": {
7
+
"dev": "vite",
8
+
"build": "vite build",
9
+
"preview": "vite preview",
10
+
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json"
11
+
},
12
+
"dependencies": {
13
+
"@atcute/bluesky": "^2.0.2",
14
+
"@atcute/client": "^3.0.1",
15
+
"@atcute/identity-resolver": "^0.1.2",
16
+
"moment": "^2.30.1",
17
+
"mutex-ts": "^1.2.1",
18
+
"svelte-infinite-loading": "^1.4.0"
19
+
},
20
+
"devDependencies": {
21
+
"@sveltejs/vite-plugin-svelte": "^5.0.3",
22
+
"@tsconfig/svelte": "^5.0.4",
23
+
"svelte": "^5.23.1",
24
+
"svelte-check": "^4.1.5",
25
+
"typescript": "~5.7.2",
26
+
"vite": "^6.3.1"
27
+
}
28
+
}
public/favicon.ico
public/favicon.ico
This is a binary file and will not be displayed.
+88
src/App.svelte
+88
src/App.svelte
···
1
+
<script lang="ts">
2
+
import PostComponent from "./lib/PostComponent.svelte";
3
+
import AccountComponent from "./lib/AccountComponent.svelte";
4
+
import InfiniteLoading from "svelte-infinite-loading";
5
+
import { getNextPosts, Post, getAllMetadataFromPds } from "./lib/pdsfetch";
6
+
import { Config } from "../config";
7
+
const accountsPromise = getAllMetadataFromPds();
8
+
import { onMount } from "svelte";
9
+
10
+
let posts: Post[] = [];
11
+
12
+
let hue: number = 1;
13
+
const cycleColors = async () => {
14
+
while (true) {
15
+
hue += 1;
16
+
if (hue > 360) {
17
+
hue = 0;
18
+
}
19
+
document.documentElement.style.setProperty("--primary-h", hue.toString());
20
+
await new Promise((resolve) => setTimeout(resolve, 10));
21
+
}
22
+
}
23
+
let clickCounter = 0;
24
+
const carameldansenfusion = async () => {
25
+
clickCounter++;
26
+
if (clickCounter >= 10) {
27
+
clickCounter = 0;
28
+
cycleColors();
29
+
}
30
+
};
31
+
32
+
onMount(() => {
33
+
// Fetch initial posts
34
+
getNextPosts().then((initialPosts) => {
35
+
posts = initialPosts;
36
+
});
37
+
});
38
+
// Infinite loading function
39
+
const onInfinite = ({
40
+
detail: { loaded, complete },
41
+
}: {
42
+
detail: { loaded: () => void; complete: () => void };
43
+
}) => {
44
+
getNextPosts().then((newPosts) => {
45
+
console.log("Loading next posts...");
46
+
if (newPosts.length > 0) {
47
+
posts = [...posts, ...newPosts];
48
+
loaded();
49
+
} else {
50
+
complete();
51
+
}
52
+
});
53
+
};
54
+
</script>
55
+
56
+
<main>
57
+
<div id="Content">
58
+
{#await accountsPromise}
59
+
<p>Loading...</p>
60
+
{:then accountsData}
61
+
<div id="Account">
62
+
<h1 onclick={carameldansenfusion} id="Header">ATProto PDS</h1>
63
+
<p>Home to {accountsData.length} accounts</p>
64
+
<div id="accountsList">
65
+
{#each accountsData as accountObject}
66
+
<AccountComponent account={accountObject} />
67
+
{/each}
68
+
</div>
69
+
<p>{@html Config.FOOTER_TEXT}</p>
70
+
</div>
71
+
{:catch error}
72
+
<p>Error: {error.message}</p>
73
+
{/await}
74
+
75
+
<div id="Feed">
76
+
<div id="spacer"></div>
77
+
{#each posts as postObject}
78
+
<PostComponent post={postObject as Post} />
79
+
{/each}
80
+
<InfiniteLoading on:infinite={onInfinite} distance={3000} />
81
+
<div id="spacer"></div>
82
+
</div>
83
+
</div>
84
+
</main>
85
+
86
+
<style>
87
+
88
+
</style>
+4
src/app.css
+4
src/app.css
+24
src/lib/AccountComponent.svelte
+24
src/lib/AccountComponent.svelte
···
1
+
<script lang="ts">
2
+
import type { AccountMetadata } from "./pdsfetch";
3
+
const { account }: { account: AccountMetadata } = $props();
4
+
import { Config } from "../../config";
5
+
</script>
6
+
7
+
<a id="link" href="{Config.FRONTEND_URL}/profile/{account.did}">
8
+
<div id="accountContainer">
9
+
{#if account.avatarCid}
10
+
<img
11
+
id="avatar"
12
+
alt="avatar of {account.displayName}"
13
+
src="{Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did={account.did}&cid={account.avatarCid}"
14
+
/>
15
+
{/if}
16
+
<div id="accountName">
17
+
{account.displayName || account.handle || account.did}
18
+
</div>
19
+
</div>
20
+
</a>
21
+
22
+
<style>
23
+
24
+
</style>
+156
src/lib/PostComponent.svelte
+156
src/lib/PostComponent.svelte
···
1
+
<script lang="ts">
2
+
import { Post } from "./pdsfetch";
3
+
import { Config } from "../../config";
4
+
import { onMount } from "svelte";
5
+
import moment from "moment";
6
+
7
+
let { post }: { post: Post } = $props();
8
+
9
+
// State for image carousel
10
+
let currentImageIndex = $state(0);
11
+
12
+
// Functions to navigate carousel
13
+
function nextImage() {
14
+
if (post.imagesCid && currentImageIndex < post.imagesCid.length - 1) {
15
+
currentImageIndex++;
16
+
}
17
+
}
18
+
19
+
function prevImage() {
20
+
if (currentImageIndex > 0) {
21
+
currentImageIndex--;
22
+
}
23
+
}
24
+
25
+
// Function to preload an image
26
+
function preloadImage(index: number): void {
27
+
if (!post.imagesCid || index < 0 || index >= post.imagesCid.length) return;
28
+
29
+
const img = new Image();
30
+
img.src = `${Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did=${post.authorDid}&cid=${post.imagesCid[index]}`;
31
+
}
32
+
33
+
// Preload adjacent images when current index changes
34
+
$effect(() => {
35
+
if (post.imagesCid && post.imagesCid.length > 1) {
36
+
// Preload next image if available
37
+
if (currentImageIndex < post.imagesCid.length - 1) {
38
+
preloadImage(currentImageIndex + 1);
39
+
}
40
+
41
+
// Preload previous image if available
42
+
if (currentImageIndex > 0) {
43
+
preloadImage(currentImageIndex - 1);
44
+
}
45
+
}
46
+
});
47
+
48
+
// Initial preload of images
49
+
onMount(() => {
50
+
if (post.imagesCid && post.imagesCid.length > 1) {
51
+
// Preload the next image if it exists
52
+
if (post.imagesCid.length > 1) {
53
+
preloadImage(1);
54
+
}
55
+
}
56
+
});
57
+
</script>
58
+
59
+
<div id="postContainer">
60
+
<div id="postHeader">
61
+
{#if post.authorAvatarCid}
62
+
<img
63
+
id="avatar"
64
+
src="{Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did={post.authorDid}&cid={post.authorAvatarCid}"
65
+
alt="avatar of {post.displayName}"
66
+
/>
67
+
{/if}
68
+
<div id="headerText">
69
+
<a id="displayName" href="{Config.FRONTEND_URL}/profile/{post.authorDid}"
70
+
>{post.displayName}</a
71
+
>
72
+
<p id="handle">
73
+
<a href="{Config.FRONTEND_URL}/profile/{post.authorHandle}"
74
+
>@{post.authorHandle}</a
75
+
>
76
+
77
+
<a
78
+
id="postLink"
79
+
href="{Config.FRONTEND_URL}/profile/{post.authorDid}/post/{post.recordName}"
80
+
>{moment(post.timenotstamp).isBefore(moment().subtract(1, "month"))
81
+
? moment(post.timenotstamp).format("MMM D, YYYY")
82
+
: moment(post.timenotstamp).fromNow()}</a
83
+
>
84
+
</p>
85
+
</div>
86
+
</div>
87
+
<div id="postContent">
88
+
{#if post.replyingUri}
89
+
<a
90
+
id="replyingText"
91
+
href="{Config.FRONTEND_URL}/profile/{post.replyingUri.repo}/post/{post
92
+
.replyingUri.rkey}">replying to {post.replyingUri.repo}</a
93
+
>
94
+
{/if}
95
+
{#if post.quotingUri}
96
+
<a
97
+
id="quotingText"
98
+
href="{Config.FRONTEND_URL}/profile/{post.quotingUri.repo}/post/{post
99
+
.quotingUri.rkey}">quoting {post.quotingUri.repo}</a
100
+
>
101
+
{/if}
102
+
<div id="postText">{post.text}</div>
103
+
{#if post.imagesCid && post.imagesCid.length > 0}
104
+
<div id="carouselContainer">
105
+
<img
106
+
id="embedImages"
107
+
alt="Post Image {currentImageIndex + 1} of {post.imagesCid.length}"
108
+
src="{Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did={post.authorDid}&cid={post
109
+
.imagesCid[currentImageIndex]}"
110
+
/>
111
+
112
+
{#if post.imagesCid.length > 1}
113
+
<div id="carouselControls">
114
+
<button
115
+
id="prevBtn"
116
+
onclick={prevImage}
117
+
disabled={currentImageIndex === 0}>โ</button
118
+
>
119
+
<div id="carouselIndicators">
120
+
{#each post.imagesCid as _, i}
121
+
<div
122
+
class="indicator {i === currentImageIndex ? 'active' : ''}"
123
+
></div>
124
+
{/each}
125
+
</div>
126
+
<button
127
+
id="nextBtn"
128
+
onclick={nextImage}
129
+
disabled={currentImageIndex === post.imagesCid.length - 1}
130
+
>โ</button
131
+
>
132
+
</div>
133
+
{/if}
134
+
</div>
135
+
{/if}
136
+
{#if post.videosLinkCid}
137
+
<!-- svelte-ignore a11y_media_has_caption -->
138
+
<video
139
+
id="embedVideo"
140
+
src="{Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did={post.authorDid}&cid={post.videosLinkCid}"
141
+
controls
142
+
></video>
143
+
{/if}
144
+
{#if post.gifLink}
145
+
<img
146
+
id="embedVideo"
147
+
src="{post.gifLink}"
148
+
alt="Post GIF"
149
+
/>
150
+
{/if}
151
+
</div>
152
+
</div>
153
+
154
+
<style>
155
+
156
+
</style>
+352
src/lib/pdsfetch.ts
+352
src/lib/pdsfetch.ts
···
1
+
import { simpleFetchHandler, XRPC } from "@atcute/client";
2
+
import "@atcute/bluesky/lexicons";
3
+
import type {
4
+
AppBskyActorDefs,
5
+
AppBskyActorProfile,
6
+
AppBskyFeedPost,
7
+
At,
8
+
ComAtprotoRepoListRecords,
9
+
} from "@atcute/client/lexicons";
10
+
import {
11
+
CompositeDidDocumentResolver,
12
+
PlcDidDocumentResolver,
13
+
WebDidDocumentResolver,
14
+
} from "@atcute/identity-resolver";
15
+
import { Config } from "../../config";
16
+
import { Mutex } from "mutex-ts"
17
+
// import { ComAtprotoRepoListRecords.Record } from "@atcute/client/lexicons";
18
+
// import { AppBskyFeedPost } from "@atcute/client/lexicons";
19
+
// import { AppBskyActorDefs } from "@atcute/client/lexicons";
20
+
21
+
interface AccountMetadata {
22
+
did: At.Did;
23
+
displayName: string;
24
+
handle: string;
25
+
avatarCid: string | null;
26
+
currentCursor?: string;
27
+
}
28
+
29
+
let accountsMetadata: AccountMetadata[] = [];
30
+
31
+
interface atUriObject {
32
+
repo: string;
33
+
collection: string;
34
+
rkey: string;
35
+
}
36
+
class Post {
37
+
authorDid: string;
38
+
authorAvatarCid: string | null;
39
+
postCid: string;
40
+
recordName: string;
41
+
authorHandle: string;
42
+
displayName: string;
43
+
text: string;
44
+
timestamp: number;
45
+
timenotstamp: string;
46
+
quotingUri: atUriObject | null;
47
+
replyingUri: atUriObject | null;
48
+
imagesCid: string[] | null;
49
+
videosLinkCid: string | null;
50
+
gifLink: string | null;
51
+
52
+
constructor(
53
+
record: ComAtprotoRepoListRecords.Record,
54
+
account: AccountMetadata,
55
+
) {
56
+
this.postCid = record.cid;
57
+
this.recordName = processAtUri(record.uri).rkey;
58
+
this.authorDid = account.did;
59
+
this.authorAvatarCid = account.avatarCid;
60
+
this.authorHandle = account.handle;
61
+
this.displayName = account.displayName;
62
+
const post = record.value as AppBskyFeedPost.Record;
63
+
this.timenotstamp = post.createdAt;
64
+
this.text = post.text;
65
+
this.timestamp = Date.parse(post.createdAt);
66
+
if (post.reply) {
67
+
this.replyingUri = processAtUri(post.reply.parent.uri);
68
+
} else {
69
+
this.replyingUri = null;
70
+
}
71
+
this.quotingUri = null;
72
+
this.imagesCid = null;
73
+
this.videosLinkCid = null;
74
+
this.gifLink = null;
75
+
switch (post.embed?.$type) {
76
+
case "app.bsky.embed.images":
77
+
this.imagesCid = post.embed.images.map(
78
+
(imageRecord: any) => imageRecord.image.ref.$link,
79
+
);
80
+
break;
81
+
case "app.bsky.embed.video":
82
+
this.videosLinkCid = post.embed.video.ref.$link;
83
+
break;
84
+
case "app.bsky.embed.record":
85
+
this.quotingUri = processAtUri(post.embed.record.uri);
86
+
break;
87
+
case "app.bsky.embed.recordWithMedia":
88
+
this.quotingUri = processAtUri(post.embed.record.record.uri);
89
+
switch (post.embed.media.$type) {
90
+
case "app.bsky.embed.images":
91
+
this.imagesCid = post.embed.media.images.map(
92
+
(imageRecord) => imageRecord.image.ref.$link,
93
+
);
94
+
95
+
break;
96
+
case "app.bsky.embed.video":
97
+
this.videosLinkCid = post.embed.media.video.ref.$link;
98
+
99
+
break;
100
+
}
101
+
break;
102
+
case "app.bsky.embed.external": // assuming that external embeds are gifs for now
103
+
if (post.embed.external.uri.includes(".gif")) {
104
+
this.gifLink = post.embed.external.uri;
105
+
}
106
+
break;
107
+
}
108
+
}
109
+
}
110
+
111
+
const processAtUri = (aturi: string): atUriObject => {
112
+
const parts = aturi.split("/");
113
+
return {
114
+
repo: parts[2],
115
+
collection: parts[3],
116
+
rkey: parts[4],
117
+
};
118
+
};
119
+
120
+
const rpc = new XRPC({
121
+
handler: simpleFetchHandler({
122
+
service: Config.PDS_URL,
123
+
}),
124
+
});
125
+
126
+
const getDidsFromPDS = async (): Promise<At.Did[]> => {
127
+
const { data } = await rpc.get("com.atproto.sync.listRepos", {
128
+
params: {},
129
+
});
130
+
return data.repos.map((repo: any) => repo.did) as At.Did[];
131
+
};
132
+
const getAccountMetadata = async (
133
+
did: `did:${string}:${string}`,
134
+
) => {
135
+
// gonna assume self exists in the app.bsky.actor.profile
136
+
try {
137
+
const { data } = await rpc.get("com.atproto.repo.getRecord", {
138
+
params: {
139
+
repo: did,
140
+
collection: "app.bsky.actor.profile",
141
+
rkey: "self",
142
+
},
143
+
});
144
+
const value = data.value as AppBskyActorProfile.Record;
145
+
const handle = await blueskyHandleFromDid(did);
146
+
const account: AccountMetadata = {
147
+
did: did,
148
+
handle: handle,
149
+
displayName: value.displayName || "",
150
+
avatarCid: null,
151
+
};
152
+
if (value.avatar) {
153
+
account.avatarCid = value.avatar.ref["$link"];
154
+
}
155
+
return account;
156
+
} catch (e) {
157
+
console.error(`Error fetching metadata for ${did}:`, e);
158
+
return null;
159
+
}
160
+
};
161
+
162
+
const getAllMetadataFromPds = async (): Promise<AccountMetadata[]> => {
163
+
const dids = await getDidsFromPDS();
164
+
const metadata = await Promise.all(
165
+
dids.map(async (repo: `did:${string}:${string}`) => {
166
+
return await getAccountMetadata(repo);
167
+
}),
168
+
);
169
+
return metadata.filter((account) => account !== null) as AccountMetadata[];
170
+
};
171
+
172
+
const identityResolve = async (did: At.Did) => {
173
+
const resolver = new CompositeDidDocumentResolver({
174
+
methods: {
175
+
plc: new PlcDidDocumentResolver(),
176
+
web: new WebDidDocumentResolver(),
177
+
},
178
+
});
179
+
180
+
if (did.startsWith("did:plc:") || did.startsWith("did:web:")) {
181
+
const doc = await resolver.resolve(
182
+
did as `did:plc:${string}` | `did:web:${string}`,
183
+
);
184
+
return doc;
185
+
} else {
186
+
throw new Error(`Unsupported DID type: ${did}`);
187
+
}
188
+
};
189
+
190
+
const blueskyHandleFromDid = async (did: At.Did) => {
191
+
const doc = await identityResolve(did);
192
+
if (doc.alsoKnownAs) {
193
+
const handleAtUri = doc.alsoKnownAs.find((url) => url.startsWith("at://"));
194
+
const handle = handleAtUri?.split("/")[2];
195
+
if (!handle) {
196
+
return "Handle not found";
197
+
} else {
198
+
return handle;
199
+
}
200
+
} else {
201
+
return "Handle not found";
202
+
}
203
+
};
204
+
205
+
interface PostsAcc {
206
+
posts: ComAtprotoRepoListRecords.Record[];
207
+
account: AccountMetadata;
208
+
}
209
+
const getCutoffDate = (postAccounts: PostsAcc[]) => {
210
+
const now = Date.now();
211
+
let cutoffDate: Date | null = null;
212
+
postAccounts.forEach((postAcc) => {
213
+
const latestPost = new Date(
214
+
(postAcc.posts[postAcc.posts.length - 1].value as AppBskyFeedPost.Record)
215
+
.createdAt,
216
+
);
217
+
if (!cutoffDate) {
218
+
cutoffDate = latestPost;
219
+
} else {
220
+
if (latestPost > cutoffDate) {
221
+
cutoffDate = latestPost;
222
+
}
223
+
}
224
+
});
225
+
if (cutoffDate) {
226
+
return cutoffDate;
227
+
} else {
228
+
return new Date(now);
229
+
}
230
+
};
231
+
232
+
const filterPostsByDate = (posts: PostsAcc[], cutoffDate: Date) => {
233
+
// filter posts for each account that are older than the cutoff date and save the cursor of the last post included
234
+
const filteredPosts: PostsAcc[] = posts.map((postAcc) => {
235
+
const filtered = postAcc.posts.filter((post) => {
236
+
const postDate = new Date(
237
+
(post.value as AppBskyFeedPost.Record).createdAt,
238
+
);
239
+
return postDate >= cutoffDate;
240
+
});
241
+
if (filtered.length > 0) {
242
+
postAcc.account.currentCursor = processAtUri(filtered[filtered.length - 1].uri).rkey;
243
+
}
244
+
return {
245
+
posts: filtered,
246
+
account: postAcc.account,
247
+
};
248
+
});
249
+
return filteredPosts;
250
+
};
251
+
252
+
const postsMutex = new Mutex();
253
+
// nightmare function. However it works so I am not touching it
254
+
const getNextPosts = async () => {
255
+
const release = await postsMutex.obtain();
256
+
if (!accountsMetadata.length) {
257
+
accountsMetadata = await getAllMetadataFromPds();
258
+
}
259
+
260
+
const postsAcc: PostsAcc[] = await Promise.all(
261
+
accountsMetadata.map(async (account) => {
262
+
const posts = await fetchPostsForUser(
263
+
account.did,
264
+
account.currentCursor || null,
265
+
);
266
+
if (posts) {
267
+
return {
268
+
posts: posts,
269
+
account: account,
270
+
};
271
+
} else {
272
+
return {
273
+
posts: [],
274
+
account: account,
275
+
};
276
+
}
277
+
}),
278
+
);
279
+
const recordsFiltered = postsAcc.filter((postAcc) =>
280
+
postAcc.posts.length > 0
281
+
);
282
+
const cutoffDate = getCutoffDate(recordsFiltered);
283
+
const recordsCutoff = filterPostsByDate(recordsFiltered, cutoffDate);
284
+
// update the accountMetadata with the new cursor
285
+
accountsMetadata = accountsMetadata.map((account) => {
286
+
const postAcc = recordsCutoff.find(
287
+
(postAcc) => postAcc.account.did == account.did,
288
+
);
289
+
if (postAcc) {
290
+
account.currentCursor = postAcc.account.currentCursor;
291
+
}
292
+
return account;
293
+
}
294
+
);
295
+
// throw the records in a big single array
296
+
let records = recordsCutoff.flatMap((postAcc) => postAcc.posts);
297
+
// sort the records by timestamp
298
+
records = records.sort((a, b) => {
299
+
const aDate = new Date(
300
+
(a.value as AppBskyFeedPost.Record).createdAt,
301
+
).getTime();
302
+
const bDate = new Date(
303
+
(b.value as AppBskyFeedPost.Record).createdAt,
304
+
).getTime();
305
+
return bDate - aDate;
306
+
});
307
+
// filter out posts that are in the future
308
+
if (!Config.SHOW_FUTURE_POSTS) {
309
+
const now = Date.now();
310
+
records = records.filter((post) => {
311
+
const postDate = new Date(
312
+
(post.value as AppBskyFeedPost.Record).createdAt,
313
+
).getTime();
314
+
return postDate <= now;
315
+
});
316
+
}
317
+
318
+
const newPosts = records.map((record) => {
319
+
const account = accountsMetadata.find(
320
+
(account) => account.did == processAtUri(record.uri).repo,
321
+
);
322
+
if (!account) {
323
+
throw new Error(
324
+
`Account with DID ${processAtUri(record.uri).repo} not found`,
325
+
);
326
+
}
327
+
return new Post(record, account);
328
+
});
329
+
// release the mutex
330
+
release();
331
+
return newPosts;
332
+
};
333
+
334
+
const fetchPostsForUser = async (did: At.Did, cursor: string | null) => {
335
+
try {
336
+
const { data } = await rpc.get("com.atproto.repo.listRecords", {
337
+
params: {
338
+
repo: did as At.Identifier,
339
+
collection: "app.bsky.feed.post",
340
+
limit: Config.MAX_POSTS,
341
+
cursor: cursor || undefined,
342
+
},
343
+
});
344
+
return data.records as ComAtprotoRepoListRecords.Record[];
345
+
} catch (e) {
346
+
console.error(`Error fetching posts for ${did}:`, e);
347
+
return null;
348
+
}
349
+
};
350
+
351
+
export { getAllMetadataFromPds, getNextPosts, Post };
352
+
export type { AccountMetadata };
+9
src/main.ts
+9
src/main.ts
+2
src/vite-env.d.ts
+2
src/vite-env.d.ts
+7
svelte.config.js
+7
svelte.config.js
+423
themes/default/theme.css
+423
themes/default/theme.css
···
1
+
/* Modern Theme for pds-dash */
2
+
3
+
:root {
4
+
/* Modern color palette */
5
+
--primary-h: 243;
6
+
--link-color: hsl(var(--primary-h), 73%, 59%);
7
+
--link-hover-color: #4338ca;
8
+
--time-color: #8b5cf6;
9
+
--background-color: #f8fafc;
10
+
--header-background-color: #ffffff;
11
+
--content-background-color: #ffffff;
12
+
--text-color: #111827;
13
+
--text-secondary-color: #4b5563;
14
+
--border-color: #e2e8f0;
15
+
--indicator-inactive-color: #cbd5e1;
16
+
--indicator-active-color: #6366f1;
17
+
18
+
/* Modern shadows */
19
+
--button-hover: #f3f4f6;
20
+
}
21
+
22
+
23
+
body {
24
+
margin: 0;
25
+
display: flex;
26
+
place-items: center;
27
+
min-width: 320px;
28
+
min-height: 100vh;
29
+
background-color: var(--background-color);
30
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
31
+
font-size: 18px;
32
+
line-height: 1.5;
33
+
color: var(--text-color);
34
+
border-color: var(--border-color);
35
+
overflow-wrap: break-word;
36
+
word-break: break-word;
37
+
hyphens: none;
38
+
}
39
+
40
+
a {
41
+
font-weight: 500;
42
+
color: var(--link-color);
43
+
text-decoration: none;
44
+
transition: color 0.15s ease;
45
+
}
46
+
a:hover {
47
+
color: var(--link-hover-color);
48
+
}
49
+
50
+
h1 {
51
+
font-size: 2.5em;
52
+
line-height: 1.2;
53
+
font-weight: 700;
54
+
}
55
+
56
+
#app {
57
+
max-width: 1400px;
58
+
width: 100%;
59
+
margin: 0 auto;
60
+
padding: 0;
61
+
text-align: center;
62
+
}
63
+
64
+
/* Post Component */
65
+
#postContainer {
66
+
display: flex;
67
+
flex-direction: column;
68
+
border-radius: 12px;
69
+
border: 1px solid var(--border-color);
70
+
background-color: var(--content-background-color);
71
+
margin-bottom: 20px;
72
+
overflow-wrap: break-word;
73
+
overflow: hidden;
74
+
box-shadow: var(--card-shadow);
75
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
76
+
}
77
+
78
+
#postContainer:hover {
79
+
transform: translateY(-2px);
80
+
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
81
+
}
82
+
83
+
#postHeader {
84
+
display: flex;
85
+
flex-direction: row;
86
+
align-items: center;
87
+
justify-content: start;
88
+
background-color: var(--header-background-color);
89
+
padding: 12px 16px;
90
+
height: 60px;
91
+
border-bottom: 1px solid var(--border-color);
92
+
font-weight: 600;
93
+
overflow-wrap: break-word;
94
+
}
95
+
96
+
#displayName {
97
+
display: block;
98
+
color: var(--text-color);
99
+
font-size: 1.1em;
100
+
padding: 0;
101
+
margin: 0 0 2px 0;
102
+
text-overflow: ellipsis;
103
+
overflow: hidden;
104
+
white-space: nowrap;
105
+
width: 100%;
106
+
letter-spacing: -0.01em;
107
+
}
108
+
109
+
#handle {
110
+
display: flex;
111
+
align-items: center;
112
+
color: #6b7280;
113
+
font-size: 0.85em;
114
+
font-weight: 400;
115
+
padding: 0;
116
+
margin: 0;
117
+
gap: 8px;
118
+
}
119
+
120
+
#postLink {
121
+
color: var(--time-color);
122
+
font-size: 0.85em;
123
+
padding: 0;
124
+
margin: 0;
125
+
opacity: 0.9;
126
+
}
127
+
128
+
#postContent {
129
+
display: flex;
130
+
text-align: start;
131
+
flex-direction: column;
132
+
padding: 16px;
133
+
background-color: var(--content-background-color);
134
+
color: var(--text-color);
135
+
overflow-wrap: break-word;
136
+
white-space: pre-line;
137
+
line-height: 1.6;
138
+
}
139
+
140
+
#replyingText, #quotingText {
141
+
font-size: 0.8em;
142
+
margin: 0;
143
+
padding: 0 0 10px 0;
144
+
color: #6b7280;
145
+
}
146
+
147
+
#postText {
148
+
margin: 0 0 8px 0;
149
+
padding: 0;
150
+
overflow-wrap: break-word;
151
+
word-break: break-word;
152
+
hyphens: none;
153
+
font-size: 1.05em;
154
+
}
155
+
156
+
#headerText {
157
+
margin-left: 12px;
158
+
font-size: 0.9em;
159
+
text-align: start;
160
+
word-break: break-word;
161
+
max-width: 80%;
162
+
max-height: 95%;
163
+
overflow: hidden;
164
+
align-self: flex-start;
165
+
margin-top: auto;
166
+
margin-bottom: auto;
167
+
}
168
+
169
+
#carouselContainer {
170
+
position: relative;
171
+
width: 100%;
172
+
margin-top: 12px;
173
+
display: flex;
174
+
flex-direction: column;
175
+
align-items: center;
176
+
border-radius: 8px;
177
+
overflow: hidden;
178
+
}
179
+
180
+
#carouselControls {
181
+
display: flex;
182
+
justify-content: space-between;
183
+
align-items: center;
184
+
width: 100%;
185
+
max-width: 500px;
186
+
margin-top: 10px;
187
+
}
188
+
189
+
#carouselIndicators {
190
+
display: flex;
191
+
gap: 6px;
192
+
}
193
+
194
+
.indicator {
195
+
width: 6px;
196
+
height: 6px;
197
+
background-color: var(--indicator-inactive-color);
198
+
border-radius: 50%;
199
+
transition: background-color 0.2s ease, transform 0.2s ease;
200
+
}
201
+
202
+
.indicator.active {
203
+
background-color: var(--indicator-active-color);
204
+
transform: scale(1.3);
205
+
}
206
+
207
+
#prevBtn,
208
+
#nextBtn {
209
+
background-color: var(--button-bg);
210
+
color: var(--text-color);
211
+
border: 1px solid var(--border-color);
212
+
width: 32px;
213
+
height: 32px;
214
+
cursor: pointer;
215
+
display: flex;
216
+
align-items: center;
217
+
justify-content: center;
218
+
border-radius: 50%;
219
+
transition: background-color 0.15s ease, transform 0.15s ease;
220
+
font-size: 16px;
221
+
}
222
+
223
+
#prevBtn:hover:not(:disabled),
224
+
#nextBtn:hover:not(:disabled) {
225
+
background-color: var(--button-hover);
226
+
transform: scale(1.05);
227
+
}
228
+
229
+
#prevBtn:disabled,
230
+
#nextBtn:disabled {
231
+
opacity: 0.4;
232
+
cursor: not-allowed;
233
+
}
234
+
235
+
#embedVideo {
236
+
width: 100%;
237
+
max-width: 500px;
238
+
margin-top: 12px;
239
+
align-self: center;
240
+
border-radius: 8px;
241
+
overflow: hidden;
242
+
}
243
+
244
+
#embedImages {
245
+
min-width: min(100%, 500px);
246
+
max-width: min(100%, 500px);
247
+
max-height: 500px;
248
+
object-fit: contain;
249
+
margin: 0;
250
+
border-radius: 8px;
251
+
}
252
+
253
+
/* Account Component */
254
+
#accountContainer {
255
+
display: flex;
256
+
text-align: start;
257
+
align-items: center;
258
+
background-color: var(--content-background-color);
259
+
padding: 12px;
260
+
margin-bottom: 15px;
261
+
border: 1px solid var(--border-color);
262
+
border-radius: 12px;
263
+
transition: background-color 0.15s ease;
264
+
}
265
+
266
+
#accountContainer:hover {
267
+
background-color: var(--hover-bg);
268
+
}
269
+
270
+
#accountName {
271
+
margin-left: 12px;
272
+
font-size: 0.95em;
273
+
max-width: 80%;
274
+
overflow: hidden;
275
+
text-overflow: ellipsis;
276
+
white-space: nowrap;
277
+
font-weight: 500;
278
+
}
279
+
280
+
#avatar {
281
+
width: 48px;
282
+
height: 48px;
283
+
margin: 0;
284
+
object-fit: cover;
285
+
border-radius: 50%;
286
+
border: 2px solid white;
287
+
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
288
+
}
289
+
290
+
/* App.Svelte Layout */
291
+
#Content {
292
+
display: flex;
293
+
width: 100%;
294
+
height: 100%;
295
+
flex-direction: row;
296
+
justify-content: space-between;
297
+
align-items: center;
298
+
background-color: var(--background-color);
299
+
color: var(--text-color);
300
+
gap: 24px;
301
+
}
302
+
303
+
#Feed {
304
+
overflow-y: auto;
305
+
width: 65%;
306
+
height: 100vh;
307
+
padding-right: 16px;
308
+
align-self: flex-start;
309
+
}
310
+
311
+
#spacer {
312
+
padding: 0;
313
+
margin: 0;
314
+
height: 10vh;
315
+
width: 100%;
316
+
}
317
+
318
+
#Account {
319
+
width: 35%;
320
+
display: flex;
321
+
flex-direction: column;
322
+
border: 1px solid var(--border-color);
323
+
background-color: var(--content-background-color);
324
+
max-height: 80vh;
325
+
padding: 24px;
326
+
margin-left: 16px;
327
+
border-radius: 12px;
328
+
box-shadow: var(--card-shadow);
329
+
}
330
+
331
+
#accountsList {
332
+
display: flex;
333
+
flex-direction: column;
334
+
overflow-y: auto;
335
+
height: 100%;
336
+
width: 100%;
337
+
padding: 8px 0;
338
+
margin: 0;
339
+
}
340
+
341
+
#Header {
342
+
text-align: center;
343
+
font-size: 1.8em;
344
+
margin-bottom: 16px;
345
+
font-weight: 700;
346
+
background: linear-gradient(to right, var(--link-color), #8b5cf6);
347
+
-webkit-background-clip: text;
348
+
-webkit-text-fill-color: transparent;
349
+
background-clip: text;
350
+
}
351
+
352
+
/* Mobile Styles */
353
+
@media screen and (max-width: 768px) {
354
+
#Content {
355
+
flex-direction: column;
356
+
width: auto;
357
+
padding: 12px;
358
+
margin-top: 0;
359
+
}
360
+
361
+
#Account {
362
+
width: calc(100% - 32px);
363
+
padding: 16px;
364
+
margin-bottom: 20px;
365
+
margin-left: 0;
366
+
margin-right: 0;
367
+
height: auto;
368
+
order: -1;
369
+
}
370
+
371
+
#Feed {
372
+
width: 100%;
373
+
margin: 0;
374
+
padding: 0;
375
+
overflow-y: visible;
376
+
}
377
+
378
+
#spacer {
379
+
height: 5vh;
380
+
}
381
+
382
+
body {
383
+
font-size: 16px;
384
+
}
385
+
386
+
#postHeader {
387
+
padding: 10px;
388
+
height: auto;
389
+
min-height: 50px;
390
+
}
391
+
}
392
+
393
+
/* Scrollbar Styles */
394
+
::-webkit-scrollbar {
395
+
width: 0px;
396
+
background: transparent;
397
+
padding: 0;
398
+
margin: 0;
399
+
}
400
+
::-webkit-scrollbar-thumb {
401
+
background: transparent;
402
+
border-radius: 0;
403
+
}
404
+
::-webkit-scrollbar-track {
405
+
background: transparent;
406
+
border-radius: 0;
407
+
}
408
+
::-webkit-scrollbar-corner {
409
+
background: transparent;
410
+
border-radius: 0;
411
+
}
412
+
::-webkit-scrollbar-button {
413
+
background: transparent;
414
+
border-radius: 0;
415
+
}
416
+
417
+
* {
418
+
scrollbar-width: none;
419
+
scrollbar-color: transparent transparent;
420
+
-ms-overflow-style: none; /* IE and Edge */
421
+
-webkit-overflow-scrolling: touch;
422
+
-webkit-scrollbar: none; /* Safari */
423
+
}
+370
themes/express/theme.css
+370
themes/express/theme.css
···
1
+
@import url("https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap");
2
+
3
+
:root {
4
+
/* Color overrides, edit to whatever you want */
5
+
--primary-h: 341; /* Hue */
6
+
--background-color: hsl(var(--primary-h), 62%, 30%);
7
+
--text-color: hsl(var(--primary-h), 69%, 18%);
8
+
--link-color: hsl(var(--primary-h), 100%, 20%);
9
+
--link-hover-color: hsl(var(--primary-h), 20%, 20%);
10
+
--border-color: hsl(var(--primary-h), 59%, 52%);
11
+
--content-background-color: hsl(var(--primary-h), 97%, 73%);
12
+
13
+
--header-background-color: hsl(var(--primary-h), 97%, 73%);
14
+
--indicator-inactive-color: #4a4a4a;
15
+
--indicator-active-color: var(--border-color);
16
+
}
17
+
18
+
a {
19
+
font-weight: 500;
20
+
color: var(--link-color);
21
+
text-decoration: inherit;
22
+
}
23
+
a:hover {
24
+
color: var(--link-hover-color);
25
+
text-decoration: underline;
26
+
}
27
+
28
+
body {
29
+
margin: 0;
30
+
display: flex;
31
+
place-items: center;
32
+
min-width: 320px;
33
+
min-height: 100vh;
34
+
background-color: var(--background-color);
35
+
font-family: "Share Tech Mono", monospace;
36
+
font-size: 24px;
37
+
color: var(--text-color);
38
+
border-color: var(--border-color);
39
+
overflow-wrap: break-word;
40
+
word-wrap: normal;
41
+
word-break: break-word;
42
+
hyphens: none;
43
+
}
44
+
45
+
h1 {
46
+
font-size: 3.2em;
47
+
line-height: 1.1;
48
+
}
49
+
50
+
#app {
51
+
max-width: 1400px;
52
+
width: 100%;
53
+
margin: 0;
54
+
padding: 0;
55
+
margin-left: auto;
56
+
margin-right: auto;
57
+
text-align: center;
58
+
}
59
+
60
+
/* Post Component */
61
+
a:hover {
62
+
text-decoration: underline;
63
+
}
64
+
#postContainer {
65
+
display: flex;
66
+
flex-direction: column;
67
+
border: 4px solid var(--border-color);
68
+
background-color: var(--background-color);
69
+
margin-bottom: 15px;
70
+
overflow-wrap: break-word;
71
+
box-shadow: var(--border-color) 10px 10px;
72
+
}
73
+
#postHeader {
74
+
display: flex;
75
+
flex-direction: row;
76
+
align-items: center;
77
+
justify-content: start;
78
+
background-color: var(--header-background-color);
79
+
padding: 0px 0px;
80
+
height: fit-content;
81
+
82
+
font-weight: bold;
83
+
overflow-wrap: break-word;
84
+
height: 64px;
85
+
}
86
+
#displayName {
87
+
display: block;
88
+
color: var(--text-color);
89
+
font-size: 1.2em;
90
+
padding: 0;
91
+
margin: 0;
92
+
overflow-wrap: normal;
93
+
word-wrap: break-word;
94
+
word-break: break-word;
95
+
text-overflow: ellipsis;
96
+
overflow: hidden;
97
+
white-space: nowrap;
98
+
width: 100%;
99
+
}
100
+
#handle {
101
+
display: block;
102
+
color: var(--border-color);
103
+
font-size: 0.8em;
104
+
padding: 0;
105
+
margin: 0;
106
+
}
107
+
108
+
#postLink {
109
+
color: var(--link-hover-color);
110
+
font-size: 0.8em;
111
+
padding: 0;
112
+
margin: 0;
113
+
}
114
+
#postContent {
115
+
display: flex;
116
+
text-align: start;
117
+
flex-direction: column;
118
+
padding: 10px;
119
+
background-color: var(--content-background-color);
120
+
color: var(--text-color);
121
+
overflow-wrap: break-word;
122
+
white-space: pre-line;
123
+
}
124
+
#replyingText {
125
+
font-size: 0.7em;
126
+
margin: 0;
127
+
padding: 0;
128
+
padding-bottom: 5px;
129
+
}
130
+
#quotingText {
131
+
font-size: 0.7em;
132
+
margin: 0;
133
+
padding: 0;
134
+
padding-bottom: 5px;
135
+
}
136
+
#postText {
137
+
margin: 0;
138
+
padding: 0;
139
+
overflow-wrap: break-word;
140
+
word-wrap: normal;
141
+
word-break: break-word;
142
+
hyphens: none;
143
+
}
144
+
#headerText {
145
+
margin-left: 10px;
146
+
font-size: 0.9em;
147
+
text-align: start;
148
+
word-break: break-word;
149
+
max-width: 80%;
150
+
max-height: 95%;
151
+
overflow: hidden;
152
+
align-self: flex-start;
153
+
margin-top: auto;
154
+
margin-bottom: auto;
155
+
}
156
+
#avatar {
157
+
height: 30px;
158
+
width: 30px;
159
+
overflow: hidden;
160
+
object-fit: cover;
161
+
}
162
+
#postContainer #avatar {
163
+
height: 60px;
164
+
width: 60px;
165
+
border-right: var(--border-color) 4px solid;
166
+
border-bottom: var(--border-color) 4px solid;
167
+
}
168
+
#carouselContainer {
169
+
position: relative;
170
+
width: 100%;
171
+
margin-top: 10px;
172
+
display: flex;
173
+
flex-direction: column;
174
+
align-items: center;
175
+
}
176
+
#carouselControls {
177
+
display: flex;
178
+
justify-content: space-between;
179
+
align-items: center;
180
+
width: 100%;
181
+
max-width: 500px;
182
+
margin-top: 5px;
183
+
}
184
+
#carouselIndicators {
185
+
display: flex;
186
+
gap: 5px;
187
+
}
188
+
.indicator {
189
+
width: 8px;
190
+
height: 8px;
191
+
background-color: var(--indicator-inactive-color);
192
+
}
193
+
.indicator.active {
194
+
background-color: var(--indicator-active-color);
195
+
}
196
+
#prevBtn,
197
+
#nextBtn {
198
+
background-color: rgba(31, 17, 69, 0.7);
199
+
color: var(--text-color);
200
+
border: 4px solid var(--border-color);
201
+
width: 30px;
202
+
height: 30px;
203
+
cursor: pointer;
204
+
display: flex;
205
+
align-items: center;
206
+
justify-content: center;
207
+
}
208
+
#prevBtn:disabled,
209
+
#nextBtn:disabled {
210
+
opacity: 0.5;
211
+
cursor: not-allowed;
212
+
}
213
+
#embedVideo {
214
+
width: 100%;
215
+
max-width: 500px;
216
+
margin-top: 10px;
217
+
align-self: center;
218
+
}
219
+
220
+
#embedImages {
221
+
min-width: min(100%, 500px);
222
+
max-width: min(100%, 500px);
223
+
max-height: 500px;
224
+
object-fit: contain;
225
+
226
+
margin: 0;
227
+
}
228
+
229
+
/* Account Component */
230
+
#accountContainer {
231
+
display: flex;
232
+
text-align: start;
233
+
align-items: center;
234
+
background-color: var(--header-background-color);
235
+
padding: 0px;
236
+
margin-bottom: 15px;
237
+
margin-right: 4px;
238
+
border: 4px solid var(--border-color);
239
+
box-shadow: var(--border-color) 10px 10px;
240
+
}
241
+
#accountName {
242
+
margin-left: 10px;
243
+
font-size: 1em;
244
+
max-width: 80%;
245
+
246
+
/* replace overflow with ellipsis */
247
+
overflow: hidden;
248
+
text-overflow: ellipsis;
249
+
white-space: nowrap;
250
+
}
251
+
252
+
/* App.Svelte */
253
+
/* desktop style */
254
+
255
+
#Content {
256
+
display: flex;
257
+
/* split the screen in half, left for accounts, right for posts */
258
+
width: 100%;
259
+
height: 100%;
260
+
flex-direction: row;
261
+
justify-content: space-between;
262
+
align-items: center;
263
+
background-color: var(--background-color);
264
+
color: var(--text-color);
265
+
}
266
+
#Feed {
267
+
overflow-y: scroll;
268
+
width: 65%;
269
+
height: 100vh;
270
+
padding: 20px;
271
+
padding-bottom: 0;
272
+
padding-top: 0;
273
+
margin-top: 0;
274
+
margin-bottom: 0;
275
+
}
276
+
#spacer {
277
+
padding: 0;
278
+
margin: 0;
279
+
height: 10vh;
280
+
width: 100%;
281
+
}
282
+
#Account {
283
+
width: 35%;
284
+
display: flex;
285
+
flex-direction: column;
286
+
border: 4px solid var(--border-color);
287
+
background-color: var(--content-background-color);
288
+
box-shadow: var(--border-color) 10px 10px;
289
+
height: 80vh;
290
+
padding: 20px;
291
+
margin-left: 20px;
292
+
}
293
+
#accountsList {
294
+
display: flex;
295
+
flex-direction: column;
296
+
overflow-y: scroll;
297
+
height: 100%;
298
+
width: 100%;
299
+
padding: 0px;
300
+
margin: 0px;
301
+
}
302
+
303
+
#Header {
304
+
text-align: center;
305
+
font-size: 2em;
306
+
margin-bottom: 20px;
307
+
}
308
+
309
+
/* mobile style */
310
+
@media screen and (max-width: 600px) {
311
+
#Content {
312
+
flex-direction: column;
313
+
width: auto;
314
+
padding-left: 0px;
315
+
padding-right: 0px;
316
+
margin-top: 5%;
317
+
}
318
+
#Account {
319
+
width: 85%;
320
+
padding-left: 5%;
321
+
padding-right: 5%;
322
+
margin-bottom: 20px;
323
+
margin-left: 5%;
324
+
margin-right: 5%;
325
+
height: auto;
326
+
}
327
+
#Feed {
328
+
width: 95%;
329
+
margin: 0px;
330
+
margin-left: 10%;
331
+
margin-right: 10%;
332
+
padding: 0px;
333
+
overflow-y: visible;
334
+
}
335
+
336
+
#spacer {
337
+
height: 0;
338
+
}
339
+
}
340
+
341
+
::-webkit-scrollbar {
342
+
width: 0px;
343
+
background: transparent;
344
+
padding: 0;
345
+
margin: 0;
346
+
}
347
+
::-webkit-scrollbar-thumb {
348
+
background: transparent;
349
+
border-radius: 0;
350
+
}
351
+
::-webkit-scrollbar-track {
352
+
background: transparent;
353
+
border-radius: 0;
354
+
}
355
+
::-webkit-scrollbar-corner {
356
+
background: transparent;
357
+
border-radius: 0;
358
+
}
359
+
::-webkit-scrollbar-button {
360
+
background: transparent;
361
+
border-radius: 0;
362
+
}
363
+
364
+
* {
365
+
scrollbar-width: none;
366
+
scrollbar-color: transparent transparent;
367
+
-ms-overflow-style: none; /* IE and Edge */
368
+
-webkit-overflow-scrolling: touch;
369
+
-webkit-scrollbar: none; /* Safari */
370
+
}
+367
themes/witchcraft/theme.css
+367
themes/witchcraft/theme.css
···
1
+
@font-face {
2
+
font-family: "ProggyClean";
3
+
src: url(https://witchcraft.systems/ProggyCleanNerdFont-Regular.ttf);
4
+
}
5
+
6
+
:root {
7
+
/* Color overrides, edit to whatever you want */
8
+
--primary-h: 260; /* Hue */
9
+
10
+
--link-color: hsl(calc(var(--primary-h) - 30), 75%, 60%);
11
+
--link-hover-color: hsl(calc(var(--primary-h) - 30), 75%, 50%);
12
+
--background-color: hsl(var(--primary-h), 75%, 10%);
13
+
--header-background-color: hsl(var(--primary-h), 75%, 18%);
14
+
--content-background-color: hsl(var(--primary-h), 75%, 8%);
15
+
--text-color: #fff;
16
+
--border-color: hsl(var(--primary-h), 75%, 60%);
17
+
--indicator-inactive-color: #4a4a4a;
18
+
--indicator-active-color: var(--border-color);
19
+
}
20
+
21
+
22
+
a {
23
+
font-weight: 500;
24
+
color: var(--link-color);
25
+
text-decoration: inherit;
26
+
}
27
+
a:hover {
28
+
color: var(--link-hover-color);
29
+
text-decoration: underline;
30
+
}
31
+
32
+
body {
33
+
margin: 0;
34
+
display: flex;
35
+
place-items: center;
36
+
min-width: 320px;
37
+
min-height: 100vh;
38
+
background-color: var(--background-color);
39
+
font-family: "ProggyClean", monospace;
40
+
font-size: 24px;
41
+
color: var(--text-color);
42
+
border-color: var(--border-color);
43
+
overflow-wrap: break-word;
44
+
word-wrap: normal;
45
+
word-break: break-word;
46
+
hyphens: none;
47
+
}
48
+
49
+
h1 {
50
+
font-size: 3.2em;
51
+
line-height: 1.1;
52
+
}
53
+
54
+
#app {
55
+
max-width: 1400px;
56
+
width: 100%;
57
+
margin: 0;
58
+
padding: 0;
59
+
margin-left: auto;
60
+
margin-right: auto;
61
+
text-align: center;
62
+
}
63
+
64
+
/* Post Component */
65
+
a:hover {
66
+
text-decoration: underline;
67
+
}
68
+
#postContainer {
69
+
display: flex;
70
+
flex-direction: column;
71
+
border: 1px solid var(--border-color);
72
+
background-color: var(--background-color);
73
+
margin-bottom: 15px;
74
+
overflow-wrap: break-word;
75
+
}
76
+
#postHeader {
77
+
display: flex;
78
+
flex-direction: row;
79
+
align-items: center;
80
+
justify-content: start;
81
+
background-color: var(--header-background-color);
82
+
padding: 0px 0px;
83
+
height: fit-content;
84
+
border-bottom: 1px solid var(--border-color);
85
+
font-weight: bold;
86
+
overflow-wrap: break-word;
87
+
height: 60px;
88
+
}
89
+
#displayName {
90
+
display: block;
91
+
color: var(--text-color);
92
+
font-size: 1.2em;
93
+
padding: 0;
94
+
margin: 0;
95
+
overflow-wrap: normal;
96
+
word-wrap: break-word;
97
+
word-break: break-word;
98
+
text-overflow: ellipsis;
99
+
overflow: hidden;
100
+
white-space: nowrap;
101
+
width: 100%;
102
+
}
103
+
#handle {
104
+
display: block;
105
+
color: var(--border-color);
106
+
font-size: 0.8em;
107
+
padding: 0;
108
+
margin: 0;
109
+
}
110
+
111
+
#postLink {
112
+
color: var(--border-color);
113
+
font-size: 0.8em;
114
+
padding: 0;
115
+
margin: 0;
116
+
}
117
+
#postContent {
118
+
display: flex;
119
+
text-align: start;
120
+
flex-direction: column;
121
+
padding: 10px;
122
+
background-color: var(--content-background-color);
123
+
color: var(--text-color);
124
+
overflow-wrap: break-word;
125
+
white-space: pre-line;
126
+
}
127
+
#replyingText {
128
+
font-size: 0.7em;
129
+
margin: 0;
130
+
padding: 0;
131
+
padding-bottom: 5px;
132
+
}
133
+
#quotingText {
134
+
font-size: 0.7em;
135
+
margin: 0;
136
+
padding: 0;
137
+
padding-bottom: 5px;
138
+
}
139
+
#postText {
140
+
margin: 0;
141
+
padding: 0;
142
+
overflow-wrap: break-word;
143
+
word-wrap: normal;
144
+
word-break: break-word;
145
+
hyphens: none;
146
+
}
147
+
#headerText {
148
+
margin-left: 10px;
149
+
font-size: 0.9em;
150
+
text-align: start;
151
+
word-break: break-word;
152
+
max-width: 80%;
153
+
max-height: 95%;
154
+
overflow: hidden;
155
+
align-self: flex-start;
156
+
margin-top: auto;
157
+
margin-bottom: auto;
158
+
}
159
+
#avatar {
160
+
height: 60px;
161
+
width: 60px;
162
+
margin: 0px;
163
+
margin-left: 0px;
164
+
overflow: hidden;
165
+
object-fit: cover;
166
+
border-right: var(--border-color) 1px solid;
167
+
}
168
+
#carouselContainer {
169
+
position: relative;
170
+
width: 100%;
171
+
margin-top: 10px;
172
+
display: flex;
173
+
flex-direction: column;
174
+
align-items: center;
175
+
}
176
+
#carouselControls {
177
+
display: flex;
178
+
justify-content: space-between;
179
+
align-items: center;
180
+
width: 100%;
181
+
max-width: 500px;
182
+
margin-top: 5px;
183
+
}
184
+
#carouselIndicators {
185
+
display: flex;
186
+
gap: 5px;
187
+
}
188
+
.indicator {
189
+
width: 8px;
190
+
height: 8px;
191
+
background-color: var(--indicator-inactive-color);
192
+
}
193
+
.indicator.active {
194
+
background-color: var(--indicator-active-color);
195
+
}
196
+
#prevBtn,
197
+
#nextBtn {
198
+
background-color: rgba(31, 17, 69, 0.7);
199
+
color: var(--text-color);
200
+
border: 1px solid var(--border-color);
201
+
width: 30px;
202
+
height: 30px;
203
+
cursor: pointer;
204
+
display: flex;
205
+
align-items: center;
206
+
justify-content: center;
207
+
}
208
+
#prevBtn:disabled,
209
+
#nextBtn:disabled {
210
+
opacity: 0.5;
211
+
cursor: not-allowed;
212
+
}
213
+
#embedVideo {
214
+
width: 100%;
215
+
max-width: 500px;
216
+
margin-top: 10px;
217
+
align-self: center;
218
+
}
219
+
220
+
#embedImages {
221
+
min-width: min(100%, 500px);
222
+
max-width: min(100%, 500px);
223
+
max-height: 500px;
224
+
object-fit: contain;
225
+
226
+
margin: 0;
227
+
}
228
+
229
+
/* Account Component */
230
+
#accountContainer {
231
+
display: flex;
232
+
text-align: start;
233
+
align-items: center;
234
+
background-color: var(--background-color);
235
+
padding: 0px;
236
+
margin-bottom: 15px;
237
+
border: 1px solid var(--border-color);
238
+
}
239
+
#accountName {
240
+
margin-left: 10px;
241
+
font-size: 1em;
242
+
max-width: 80%;
243
+
244
+
/* replace overflow with ellipsis */
245
+
overflow: hidden;
246
+
text-overflow: ellipsis;
247
+
white-space: nowrap;
248
+
}
249
+
250
+
/* App.Svelte */
251
+
/* desktop style */
252
+
253
+
#Content {
254
+
display: flex;
255
+
/* split the screen in half, left for accounts, right for posts */
256
+
width: 100%;
257
+
height: 100%;
258
+
flex-direction: row;
259
+
justify-content: space-between;
260
+
align-items: center;
261
+
background-color: var(--background-color);
262
+
color: var(--text-color);
263
+
}
264
+
#Feed {
265
+
overflow-y: scroll;
266
+
width: 65%;
267
+
height: 100vh;
268
+
padding: 20px;
269
+
padding-bottom: 0;
270
+
padding-top: 0;
271
+
margin-top: 0;
272
+
margin-bottom: 0;
273
+
}
274
+
#spacer {
275
+
padding: 0;
276
+
margin: 0;
277
+
height: 10vh;
278
+
width: 100%;
279
+
}
280
+
#Account {
281
+
width: 35%;
282
+
display: flex;
283
+
flex-direction: column;
284
+
border: 1px solid var(--border-color);
285
+
background-color: var(--content-background-color);
286
+
height: 80vh;
287
+
padding: 20px;
288
+
margin-left: 20px;
289
+
}
290
+
#accountsList {
291
+
display: flex;
292
+
flex-direction: column;
293
+
overflow-y: scroll;
294
+
height: 100%;
295
+
width: 100%;
296
+
padding: 0px;
297
+
margin: 0px;
298
+
}
299
+
300
+
#Header {
301
+
text-align: center;
302
+
font-size: 2em;
303
+
margin-bottom: 20px;
304
+
}
305
+
306
+
/* mobile style */
307
+
@media screen and (max-width: 600px) {
308
+
#Content {
309
+
flex-direction: column;
310
+
width: auto;
311
+
padding-left: 0px;
312
+
padding-right: 0px;
313
+
margin-top: 5%;
314
+
}
315
+
#Account {
316
+
width: 85%;
317
+
padding-left: 5%;
318
+
padding-right: 5%;
319
+
margin-bottom: 20px;
320
+
margin-left: 5%;
321
+
margin-right: 5%;
322
+
height: auto;
323
+
}
324
+
#Feed {
325
+
width: 95%;
326
+
margin: 0px;
327
+
margin-left: 10%;
328
+
margin-right: 10%;
329
+
padding: 0px;
330
+
overflow-y: visible;
331
+
}
332
+
333
+
#spacer {
334
+
height: 0;
335
+
}
336
+
}
337
+
338
+
::-webkit-scrollbar {
339
+
width: 0px;
340
+
background: transparent;
341
+
padding: 0;
342
+
margin: 0;
343
+
}
344
+
::-webkit-scrollbar-thumb {
345
+
background: transparent;
346
+
border-radius: 0;
347
+
}
348
+
::-webkit-scrollbar-track {
349
+
background: transparent;
350
+
border-radius: 0;
351
+
}
352
+
::-webkit-scrollbar-corner {
353
+
background: transparent;
354
+
border-radius: 0;
355
+
}
356
+
::-webkit-scrollbar-button {
357
+
background: transparent;
358
+
border-radius: 0;
359
+
}
360
+
361
+
* {
362
+
scrollbar-width: none;
363
+
scrollbar-color: transparent transparent;
364
+
-ms-overflow-style: none; /* IE and Edge */
365
+
-webkit-overflow-scrolling: touch;
366
+
-webkit-scrollbar: none; /* Safari */
367
+
}
+32
theming.ts
+32
theming.ts
···
1
+
import { Plugin } from 'vite';
2
+
import { Config } from './config';
3
+
4
+
5
+
// Replaces app.css with the contents of the file specified in the
6
+
// config file.
7
+
export const themePlugin = (): Plugin => {
8
+
const themeFolder = Config.THEME;
9
+
console.log(`Using theme folder: ${themeFolder}`);
10
+
return {
11
+
name: 'theme-generator',
12
+
enforce: 'pre', // Ensure this plugin runs first
13
+
transform(code, id) {
14
+
if (id.endsWith('app.css')) {
15
+
// Read the theme file and replace the contents of app.css with it
16
+
// Needs full path to the file
17
+
const themeCode = Deno.readTextFileSync(Deno.cwd() + '/themes/' + themeFolder + '/theme.css');
18
+
// Replace the contents of app.css with the theme code
19
+
20
+
// and add a comment at the top
21
+
const themeComment = `/* Generated from ${themeFolder} */\n`;
22
+
const themeCodeWithComment = themeComment + themeCode;
23
+
// Return the theme code as the new contents of app.css
24
+
return {
25
+
code: themeCodeWithComment,
26
+
map: null,
27
+
};
28
+
}
29
+
return null;
30
+
}
31
+
};
32
+
};
+20
tsconfig.app.json
+20
tsconfig.app.json
···
1
+
{
2
+
"extends": "@tsconfig/svelte/tsconfig.json",
3
+
"compilerOptions": {
4
+
"target": "ESNext",
5
+
"useDefineForClassFields": true,
6
+
"module": "ESNext",
7
+
"resolveJsonModule": true,
8
+
/**
9
+
* Typecheck JS in `.svelte` and `.js` files by default.
10
+
* Disable checkJs if you'd like to use dynamic types in JS.
11
+
* Note that setting allowJs false does not prevent the use
12
+
* of JS in `.svelte` files.
13
+
*/
14
+
"allowJs": true,
15
+
"checkJs": true,
16
+
"isolatedModules": true,
17
+
"moduleDetection": "force"
18
+
},
19
+
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
20
+
}
+7
tsconfig.json
+7
tsconfig.json
+24
tsconfig.node.json
+24
tsconfig.node.json
···
1
+
{
2
+
"compilerOptions": {
3
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
+
"target": "ES2022",
5
+
"lib": ["ES2023"],
6
+
"module": "ESNext",
7
+
"skipLibCheck": true,
8
+
9
+
/* Bundler mode */
10
+
"moduleResolution": "bundler",
11
+
"allowImportingTsExtensions": true,
12
+
"isolatedModules": true,
13
+
"moduleDetection": "force",
14
+
"noEmit": true,
15
+
16
+
/* Linting */
17
+
"strict": true,
18
+
"noUnusedLocals": true,
19
+
"noUnusedParameters": true,
20
+
"noFallthroughCasesInSwitch": true,
21
+
"noUncheckedSideEffectImports": true
22
+
},
23
+
"include": ["vite.config.ts"]
24
+
}
+11
vite.config.ts
+11
vite.config.ts