+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 }}
+4
-1
.gitignore
+4
-1
.gitignore
+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
-16
config.ts
-16
config.ts
···
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 "https://pds.witchcraft.systems"
8
-
*/
9
-
static readonly PDS_URL: string = "https://pds.witchcraft.systems";
10
-
11
-
/**
12
-
* The base URL of the frontend service for linking to replies
13
-
* @default "https://deer.social"
14
-
*/
15
-
static readonly FRONTEND_URL: string = "https://deer.social";
16
-
}
+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
+
}
+182
-57
deno.lock
+182
-57
deno.lock
···
1
1
{
2
-
"version": "4",
2
+
"version": "5",
3
3
"specifiers": {
4
4
"npm:@atcute/bluesky@^2.0.2": "2.0.2_@atcute+client@3.0.1",
5
5
"npm:@atcute/client@^3.0.1": "3.0.1",
6
6
"npm:@atcute/identity-resolver@~0.1.2": "0.1.2_@atcute+identity@0.1.3",
7
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
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",
9
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",
10
13
"npm:svelte@^5.23.1": "5.28.1_acorn@8.14.1",
11
14
"npm:typescript@~5.7.2": "5.7.3",
12
15
"npm:vite@^6.3.1": "6.3.2_picomatch@4.0.2"
···
52
55
"integrity": "sha512-GEhUCk9c4XbNxi+0YZHZsV4fYNd6HejfWuN4Ti4c02DauX+LyX5WY1Y3WfyZ8Pxxl0zqhs+MLtW98cMh86vv6g=="
53
56
},
54
57
"@esbuild/aix-ppc64@0.25.2": {
55
-
"integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag=="
58
+
"integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==",
59
+
"os": ["aix"],
60
+
"cpu": ["ppc64"]
56
61
},
57
62
"@esbuild/android-arm64@0.25.2": {
58
-
"integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w=="
63
+
"integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==",
64
+
"os": ["android"],
65
+
"cpu": ["arm64"]
59
66
},
60
67
"@esbuild/android-arm@0.25.2": {
61
-
"integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA=="
68
+
"integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==",
69
+
"os": ["android"],
70
+
"cpu": ["arm"]
62
71
},
63
72
"@esbuild/android-x64@0.25.2": {
64
-
"integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg=="
73
+
"integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==",
74
+
"os": ["android"],
75
+
"cpu": ["x64"]
65
76
},
66
77
"@esbuild/darwin-arm64@0.25.2": {
67
-
"integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA=="
78
+
"integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==",
79
+
"os": ["darwin"],
80
+
"cpu": ["arm64"]
68
81
},
69
82
"@esbuild/darwin-x64@0.25.2": {
70
-
"integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA=="
83
+
"integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==",
84
+
"os": ["darwin"],
85
+
"cpu": ["x64"]
71
86
},
72
87
"@esbuild/freebsd-arm64@0.25.2": {
73
-
"integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w=="
88
+
"integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==",
89
+
"os": ["freebsd"],
90
+
"cpu": ["arm64"]
74
91
},
75
92
"@esbuild/freebsd-x64@0.25.2": {
76
-
"integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ=="
93
+
"integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==",
94
+
"os": ["freebsd"],
95
+
"cpu": ["x64"]
77
96
},
78
97
"@esbuild/linux-arm64@0.25.2": {
79
-
"integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g=="
98
+
"integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==",
99
+
"os": ["linux"],
100
+
"cpu": ["arm64"]
80
101
},
81
102
"@esbuild/linux-arm@0.25.2": {
82
-
"integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g=="
103
+
"integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==",
104
+
"os": ["linux"],
105
+
"cpu": ["arm"]
83
106
},
84
107
"@esbuild/linux-ia32@0.25.2": {
85
-
"integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ=="
108
+
"integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==",
109
+
"os": ["linux"],
110
+
"cpu": ["ia32"]
86
111
},
87
112
"@esbuild/linux-loong64@0.25.2": {
88
-
"integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w=="
113
+
"integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==",
114
+
"os": ["linux"],
115
+
"cpu": ["loong64"]
89
116
},
90
117
"@esbuild/linux-mips64el@0.25.2": {
91
-
"integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q=="
118
+
"integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==",
119
+
"os": ["linux"],
120
+
"cpu": ["mips64el"]
92
121
},
93
122
"@esbuild/linux-ppc64@0.25.2": {
94
-
"integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g=="
123
+
"integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==",
124
+
"os": ["linux"],
125
+
"cpu": ["ppc64"]
95
126
},
96
127
"@esbuild/linux-riscv64@0.25.2": {
97
-
"integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw=="
128
+
"integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==",
129
+
"os": ["linux"],
130
+
"cpu": ["riscv64"]
98
131
},
99
132
"@esbuild/linux-s390x@0.25.2": {
100
-
"integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q=="
133
+
"integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==",
134
+
"os": ["linux"],
135
+
"cpu": ["s390x"]
101
136
},
102
137
"@esbuild/linux-x64@0.25.2": {
103
-
"integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg=="
138
+
"integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==",
139
+
"os": ["linux"],
140
+
"cpu": ["x64"]
104
141
},
105
142
"@esbuild/netbsd-arm64@0.25.2": {
106
-
"integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw=="
143
+
"integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==",
144
+
"os": ["netbsd"],
145
+
"cpu": ["arm64"]
107
146
},
108
147
"@esbuild/netbsd-x64@0.25.2": {
109
-
"integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg=="
148
+
"integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==",
149
+
"os": ["netbsd"],
150
+
"cpu": ["x64"]
110
151
},
111
152
"@esbuild/openbsd-arm64@0.25.2": {
112
-
"integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg=="
153
+
"integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==",
154
+
"os": ["openbsd"],
155
+
"cpu": ["arm64"]
113
156
},
114
157
"@esbuild/openbsd-x64@0.25.2": {
115
-
"integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw=="
158
+
"integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==",
159
+
"os": ["openbsd"],
160
+
"cpu": ["x64"]
116
161
},
117
162
"@esbuild/sunos-x64@0.25.2": {
118
-
"integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA=="
163
+
"integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==",
164
+
"os": ["sunos"],
165
+
"cpu": ["x64"]
119
166
},
120
167
"@esbuild/win32-arm64@0.25.2": {
121
-
"integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q=="
168
+
"integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==",
169
+
"os": ["win32"],
170
+
"cpu": ["arm64"]
122
171
},
123
172
"@esbuild/win32-ia32@0.25.2": {
124
-
"integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg=="
173
+
"integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==",
174
+
"os": ["win32"],
175
+
"cpu": ["ia32"]
125
176
},
126
177
"@esbuild/win32-x64@0.25.2": {
127
-
"integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA=="
178
+
"integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==",
179
+
"os": ["win32"],
180
+
"cpu": ["x64"]
128
181
},
129
182
"@jridgewell/gen-mapping@0.3.8": {
130
183
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
···
151
204
]
152
205
},
153
206
"@rollup/rollup-android-arm-eabi@4.40.0": {
154
-
"integrity": "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg=="
207
+
"integrity": "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==",
208
+
"os": ["android"],
209
+
"cpu": ["arm"]
155
210
},
156
211
"@rollup/rollup-android-arm64@4.40.0": {
157
-
"integrity": "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w=="
212
+
"integrity": "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==",
213
+
"os": ["android"],
214
+
"cpu": ["arm64"]
158
215
},
159
216
"@rollup/rollup-darwin-arm64@4.40.0": {
160
-
"integrity": "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ=="
217
+
"integrity": "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==",
218
+
"os": ["darwin"],
219
+
"cpu": ["arm64"]
161
220
},
162
221
"@rollup/rollup-darwin-x64@4.40.0": {
163
-
"integrity": "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA=="
222
+
"integrity": "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==",
223
+
"os": ["darwin"],
224
+
"cpu": ["x64"]
164
225
},
165
226
"@rollup/rollup-freebsd-arm64@4.40.0": {
166
-
"integrity": "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg=="
227
+
"integrity": "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==",
228
+
"os": ["freebsd"],
229
+
"cpu": ["arm64"]
167
230
},
168
231
"@rollup/rollup-freebsd-x64@4.40.0": {
169
-
"integrity": "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw=="
232
+
"integrity": "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==",
233
+
"os": ["freebsd"],
234
+
"cpu": ["x64"]
170
235
},
171
236
"@rollup/rollup-linux-arm-gnueabihf@4.40.0": {
172
-
"integrity": "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA=="
237
+
"integrity": "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==",
238
+
"os": ["linux"],
239
+
"cpu": ["arm"]
173
240
},
174
241
"@rollup/rollup-linux-arm-musleabihf@4.40.0": {
175
-
"integrity": "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg=="
242
+
"integrity": "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==",
243
+
"os": ["linux"],
244
+
"cpu": ["arm"]
176
245
},
177
246
"@rollup/rollup-linux-arm64-gnu@4.40.0": {
178
-
"integrity": "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg=="
247
+
"integrity": "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==",
248
+
"os": ["linux"],
249
+
"cpu": ["arm64"]
179
250
},
180
251
"@rollup/rollup-linux-arm64-musl@4.40.0": {
181
-
"integrity": "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ=="
252
+
"integrity": "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==",
253
+
"os": ["linux"],
254
+
"cpu": ["arm64"]
182
255
},
183
256
"@rollup/rollup-linux-loongarch64-gnu@4.40.0": {
184
-
"integrity": "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg=="
257
+
"integrity": "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==",
258
+
"os": ["linux"],
259
+
"cpu": ["loong64"]
185
260
},
186
261
"@rollup/rollup-linux-powerpc64le-gnu@4.40.0": {
187
-
"integrity": "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw=="
262
+
"integrity": "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==",
263
+
"os": ["linux"],
264
+
"cpu": ["ppc64"]
188
265
},
189
266
"@rollup/rollup-linux-riscv64-gnu@4.40.0": {
190
-
"integrity": "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA=="
267
+
"integrity": "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==",
268
+
"os": ["linux"],
269
+
"cpu": ["riscv64"]
191
270
},
192
271
"@rollup/rollup-linux-riscv64-musl@4.40.0": {
193
-
"integrity": "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ=="
272
+
"integrity": "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==",
273
+
"os": ["linux"],
274
+
"cpu": ["riscv64"]
194
275
},
195
276
"@rollup/rollup-linux-s390x-gnu@4.40.0": {
196
-
"integrity": "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw=="
277
+
"integrity": "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==",
278
+
"os": ["linux"],
279
+
"cpu": ["s390x"]
197
280
},
198
281
"@rollup/rollup-linux-x64-gnu@4.40.0": {
199
-
"integrity": "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ=="
282
+
"integrity": "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==",
283
+
"os": ["linux"],
284
+
"cpu": ["x64"]
200
285
},
201
286
"@rollup/rollup-linux-x64-musl@4.40.0": {
202
-
"integrity": "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw=="
287
+
"integrity": "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==",
288
+
"os": ["linux"],
289
+
"cpu": ["x64"]
203
290
},
204
291
"@rollup/rollup-win32-arm64-msvc@4.40.0": {
205
-
"integrity": "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ=="
292
+
"integrity": "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==",
293
+
"os": ["win32"],
294
+
"cpu": ["arm64"]
206
295
},
207
296
"@rollup/rollup-win32-ia32-msvc@4.40.0": {
208
-
"integrity": "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA=="
297
+
"integrity": "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==",
298
+
"os": ["win32"],
299
+
"cpu": ["ia32"]
209
300
},
210
301
"@rollup/rollup-win32-x64-msvc@4.40.0": {
211
-
"integrity": "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ=="
302
+
"integrity": "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==",
303
+
"os": ["win32"],
304
+
"cpu": ["x64"]
212
305
},
213
306
"@sveltejs/acorn-typescript@1.0.5_acorn@8.14.1": {
214
307
"integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==",
···
245
338
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="
246
339
},
247
340
"acorn@8.14.1": {
248
-
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="
341
+
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
342
+
"bin": true
249
343
},
250
344
"aria-query@5.3.2": {
251
345
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="
···
273
367
},
274
368
"esbuild@0.25.2": {
275
369
"integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==",
276
-
"dependencies": [
370
+
"optionalDependencies": [
277
371
"@esbuild/aix-ppc64",
278
372
"@esbuild/android-arm",
279
373
"@esbuild/android-arm64",
···
299
393
"@esbuild/win32-arm64",
300
394
"@esbuild/win32-ia32",
301
395
"@esbuild/win32-x64"
302
-
]
396
+
],
397
+
"scripts": true,
398
+
"bin": true
303
399
},
304
400
"esm-env@1.2.2": {
305
401
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="
···
314
410
"integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
315
411
"dependencies": [
316
412
"picomatch"
413
+
],
414
+
"optionalPeers": [
415
+
"picomatch"
317
416
]
318
417
},
319
418
"fsevents@2.3.3": {
320
-
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="
419
+
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
420
+
"os": ["darwin"],
421
+
"scripts": true
321
422
},
322
423
"is-reference@3.0.3": {
323
424
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
···
337
438
"@jridgewell/sourcemap-codec"
338
439
]
339
440
},
441
+
"moment@2.30.1": {
442
+
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="
443
+
},
340
444
"mri@1.2.0": {
341
445
"integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="
342
446
},
343
447
"ms@2.1.3": {
344
448
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
449
+
},
450
+
"mutex-ts@1.2.1": {
451
+
"integrity": "sha512-OkcXgf0viuCgYdnm48kiNQ9PzC5OzISQ261svHr/Ybc2vBYC/5xfLXn44hQ+dYRX74v7MCSqV/LKPEbpYdDybw=="
345
452
},
346
453
"nanoid@3.3.11": {
347
-
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="
454
+
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
455
+
"bin": true
348
456
},
349
457
"picocolors@1.1.1": {
350
458
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
···
366
474
"rollup@4.40.0": {
367
475
"integrity": "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==",
368
476
"dependencies": [
477
+
"@types/estree"
478
+
],
479
+
"optionalDependencies": [
369
480
"@rollup/rollup-android-arm-eabi",
370
481
"@rollup/rollup-android-arm64",
371
482
"@rollup/rollup-darwin-arm64",
···
386
497
"@rollup/rollup-win32-arm64-msvc",
387
498
"@rollup/rollup-win32-ia32-msvc",
388
499
"@rollup/rollup-win32-x64-msvc",
389
-
"@types/estree",
390
500
"fsevents"
391
-
]
501
+
],
502
+
"bin": true
392
503
},
393
504
"sade@1.8.1": {
394
505
"integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
···
409
520
"sade",
410
521
"svelte",
411
522
"typescript"
412
-
]
523
+
],
524
+
"bin": true
525
+
},
526
+
"svelte-infinite-loading@1.4.0": {
527
+
"integrity": "sha512-Jo+f/yr/HmZQuIiiKKzAHVFXdAUWHW2RBbrcQTil8JVk1sCm/riy7KTJVzjBgQvHasrFQYKF84zvtc9/Y4lFYg=="
413
528
},
414
529
"svelte@5.28.1_acorn@8.14.1": {
415
530
"integrity": "sha512-iOa9WmfNG95lSOSJdMhdjJ4Afok7IRAQYXpbnxhd5EINnXseG0GVa9j6WPght4eX78XfFez45Fi+uRglGKPV/Q==",
···
438
553
]
439
554
},
440
555
"typescript@5.7.3": {
441
-
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="
556
+
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
557
+
"bin": true
442
558
},
443
559
"vite@6.3.2_picomatch@4.0.2": {
444
560
"integrity": "sha512-ZSvGOXKGceizRQIZSz7TGJ0pS3QLlVY/9hwxVh17W3re67je1RKYzFHivZ/t0tubU78Vkyb9WnHPENSBCzbckg==",
445
561
"dependencies": [
446
562
"esbuild",
447
563
"fdir",
448
-
"fsevents",
449
564
"picomatch",
450
565
"postcss",
451
566
"rollup",
452
567
"tinyglobby"
453
-
]
568
+
],
569
+
"optionalDependencies": [
570
+
"fsevents"
571
+
],
572
+
"bin": true
454
573
},
455
574
"vitefu@1.0.6_vite@6.3.2__picomatch@4.0.2": {
456
575
"integrity": "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA==",
457
576
"dependencies": [
577
+
"vite"
578
+
],
579
+
"optionalPeers": [
458
580
"vite"
459
581
]
460
582
},
···
470
592
"npm:@atcute/identity-resolver@~0.1.2",
471
593
"npm:@sveltejs/vite-plugin-svelte@^5.0.3",
472
594
"npm:@tsconfig/svelte@^5.0.4",
595
+
"npm:moment@^2.30.1",
596
+
"npm:mutex-ts@^1.2.1",
473
597
"npm:svelte-check@^4.1.5",
598
+
"npm:svelte-infinite-loading@^1.4.0",
474
599
"npm:svelte@^5.23.1",
475
600
"npm:typescript@~5.7.2",
476
601
"npm:vite@^6.3.1"
+1
-1
index.html
+1
-1
index.html
+4
-1
package.json
+4
-1
package.json
···
12
12
"dependencies": {
13
13
"@atcute/bluesky": "^2.0.2",
14
14
"@atcute/client": "^3.0.1",
15
-
"@atcute/identity-resolver": "^0.1.2"
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"
16
19
},
17
20
"devDependencies": {
18
21
"@sveltejs/vite-plugin-svelte": "^5.0.3",
-1
public/vite.svg
-1
public/vite.svg
···
1
-
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
+70
-51
src/App.svelte
+70
-51
src/App.svelte
···
1
1
<script lang="ts">
2
2
import PostComponent from "./lib/PostComponent.svelte";
3
3
import AccountComponent from "./lib/AccountComponent.svelte";
4
-
import { fetchAllPosts, Post, getAllMetadataFromPds } from "./lib/pdsfetch";
5
-
const postsPromise = fetchAllPosts();
4
+
import InfiniteLoading from "svelte-infinite-loading";
5
+
import { getNextPosts, Post, getAllMetadataFromPds } from "./lib/pdsfetch";
6
+
import { Config } from "../config";
6
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
+
};
7
54
</script>
8
55
9
56
<main>
10
57
<div id="Content">
11
-
{#await accountsPromise}
12
-
<p>Loading...</p>
13
-
{:then accountsData}
14
-
<div id="Account">
15
-
<h1 id="Header">ATProto PDS</h1>
16
-
<p>Home to {accountsData.length} accounts</p>
17
-
{#each accountsData as accountObject}
18
-
<AccountComponent account={accountObject} />
19
-
{/each}
20
-
</div>
21
-
{:catch error}
22
-
<p>Error: {error.message}</p>
23
-
{/await}
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}
24
74
25
-
{#await postsPromise}
26
-
<p>Loading...</p>
27
-
{:then postsData}
28
75
<div id="Feed">
29
-
{#each postsData as postObject}
76
+
<div id="spacer"></div>
77
+
{#each posts as postObject}
30
78
<PostComponent post={postObject as Post} />
31
79
{/each}
80
+
<InfiniteLoading on:infinite={onInfinite} distance={3000} />
81
+
<div id="spacer"></div>
32
82
</div>
33
-
{/await}
34
83
</div>
35
84
</main>
36
85
37
86
<style>
38
-
#Content {
39
-
display: flex;
40
-
/* split the screen in half, left for accounts, right for posts */
41
-
width: 100%;
42
-
height: 100%;
43
-
flex-direction: row;
44
-
justify-content: space-between;
45
-
align-items: center;
46
-
background-color: #12082b;
47
-
color: #ffffff;
48
-
}
49
-
#Feed {
50
-
width: 65%;
51
-
height: 80vh;
52
-
overflow-y: scroll;
53
-
padding: 20px;
54
-
}
55
-
#Account {
56
-
width: 35%;
57
-
height: 80vh;
58
-
overflow-y: scroll;
59
-
padding: 20px;
60
-
background-color: #070311;
61
-
62
-
border-radius: 10px;
63
-
}
64
-
#Header {
65
-
text-align: center;
66
-
font-size: 2em;
67
-
margin-bottom: 20px;
68
-
}
87
+
69
88
</style>
+3
-52
src/app.css
+3
-52
src/app.css
···
1
-
@font-face {
2
-
font-family: 'ProggyClean';
3
-
src: url(https://witchcraft.systems/ProggyCleanNerdFont-Regular.ttf);
4
-
}
5
-
6
-
::-webkit-scrollbar {
7
-
width: 0px;
8
-
background: transparent;
9
-
}
10
-
11
-
* {
12
-
scrollbar-width: thin;
13
-
scrollbar-color: transparent transparent;
14
-
-ms-overflow-style: none; /* IE and Edge */
15
-
-webkit-overflow-scrolling: touch;
16
-
-webkit-scrollbar: none; /* Safari */
17
-
}
18
-
19
-
a {
20
-
font-weight: 500;
21
-
color: #646cff;
22
-
text-decoration: inherit;
23
-
}
24
-
a:hover {
25
-
color: #535bf2;
26
-
}
27
-
1
+
@import url('./themes/colors.css');
28
2
body {
29
-
margin: 0;
30
-
display: flex;
31
-
place-items: center;
32
-
min-width: 320px;
33
-
min-height: 100vh;
34
-
background-color: #12082b;
35
-
font-family: 'ProggyClean', monospace;
36
-
font-size: 24px;
37
-
color: white;
38
-
border-color: #8054f0;
39
-
}
40
-
41
-
h1 {
42
-
font-size: 3.2em;
43
-
line-height: 1.1;
44
-
}
45
-
46
-
#app {
47
-
max-width: 1400px;
48
-
margin: 0 auto;
49
-
padding: 2rem;
50
-
text-align: center;
51
-
}
52
-
53
-
3
+
background-color: red;
4
+
}
-1
src/assets/svelte.svg
-1
src/assets/svelte.svg
···
1
-
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="26.6" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 308"><path fill="#FF3E00" d="M239.682 40.707C211.113-.182 154.69-12.301 113.895 13.69L42.247 59.356a82.198 82.198 0 0 0-37.135 55.056a86.566 86.566 0 0 0 8.536 55.576a82.425 82.425 0 0 0-12.296 30.719a87.596 87.596 0 0 0 14.964 66.244c28.574 40.893 84.997 53.007 125.787 27.016l71.648-45.664a82.182 82.182 0 0 0 37.135-55.057a86.601 86.601 0 0 0-8.53-55.577a82.409 82.409 0 0 0 12.29-30.718a87.573 87.573 0 0 0-14.963-66.244"></path><path fill="#FFF" d="M106.889 270.841c-23.102 6.007-47.497-3.036-61.103-22.648a52.685 52.685 0 0 1-9.003-39.85a49.978 49.978 0 0 1 1.713-6.693l1.35-4.115l3.671 2.697a92.447 92.447 0 0 0 28.036 14.007l2.663.808l-.245 2.659a16.067 16.067 0 0 0 2.89 10.656a17.143 17.143 0 0 0 18.397 6.828a15.786 15.786 0 0 0 4.403-1.935l71.67-45.672a14.922 14.922 0 0 0 6.734-9.977a15.923 15.923 0 0 0-2.713-12.011a17.156 17.156 0 0 0-18.404-6.832a15.78 15.78 0 0 0-4.396 1.933l-27.35 17.434a52.298 52.298 0 0 1-14.553 6.391c-23.101 6.007-47.497-3.036-61.101-22.649a52.681 52.681 0 0 1-9.004-39.849a49.428 49.428 0 0 1 22.34-33.114l71.664-45.677a52.218 52.218 0 0 1 14.563-6.398c23.101-6.007 47.497 3.036 61.101 22.648a52.685 52.685 0 0 1 9.004 39.85a50.559 50.559 0 0 1-1.713 6.692l-1.35 4.116l-3.67-2.693a92.373 92.373 0 0 0-28.037-14.013l-2.664-.809l.246-2.658a16.099 16.099 0 0 0-2.89-10.656a17.143 17.143 0 0 0-18.398-6.828a15.786 15.786 0 0 0-4.402 1.935l-71.67 45.674a14.898 14.898 0 0 0-6.73 9.975a15.9 15.9 0 0 0 2.709 12.012a17.156 17.156 0 0 0 18.404 6.832a15.841 15.841 0 0 0 4.402-1.935l27.345-17.427a52.147 52.147 0 0 1 14.552-6.397c23.101-6.006 47.497 3.037 61.102 22.65a52.681 52.681 0 0 1 9.003 39.848a49.453 49.453 0 0 1-22.34 33.12l-71.664 45.673a52.218 52.218 0 0 1-14.563 6.398"></path></svg>
+14
-39
src/lib/AccountComponent.svelte
+14
-39
src/lib/AccountComponent.svelte
···
1
1
<script lang="ts">
2
-
import type { AccountMetadata } from "./pdsfetch";
3
-
const { account }: { account: AccountMetadata } = $props();
4
-
import { Config } from "../../config";
2
+
import type { AccountMetadata } from "./pdsfetch";
3
+
const { account }: { account: AccountMetadata } = $props();
4
+
import { Config } from "../../config";
5
5
</script>
6
6
7
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>
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}
19
18
</div>
19
+
</div>
20
20
</a>
21
21
22
22
<style>
23
-
#accountContainer {
24
-
display: flex;
25
-
text-align: start;
26
-
align-items: center;
27
-
background-color: #0d0620;
28
-
padding: 4%;
29
-
margin: 10px;
30
23
31
-
/* round corners */
32
-
border-radius: 10px;
33
-
}
34
-
#accountName {
35
-
margin-left: 10px;
36
-
font-size: 0.9em;
37
-
38
-
/* replace overflow with ellipsis */
39
-
overflow: hidden;
40
-
text-overflow: ellipsis;
41
-
white-space: nowrap;
42
-
max-width: 80%;
43
-
}
44
-
#avatar {
45
-
width: 50px;
46
-
height: 50px;
47
-
border-radius: 50%;
48
-
}
49
24
</style>
+111
-78
src/lib/PostComponent.svelte
+111
-78
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
+
import moment from "moment";
6
+
4
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
+
});
5
57
</script>
6
58
7
59
<div id="postContainer">
···
14
66
/>
15
67
{/if}
16
68
<div id="headerText">
17
-
<a href="{Config.FRONTEND_URL}/profile/{post.authorDid}"
18
-
>{post.displayName} ( {post.authorHandle} )</a
69
+
<a id="displayName" href="{Config.FRONTEND_URL}/profile/{post.authorDid}"
70
+
>{post.displayName}</a
19
71
>
20
-
|
21
-
<a href="{Config.FRONTEND_URL}/profile/{post.authorDid}/post/{post.cid}"
22
-
>{post.timenotstamp}</a
23
-
>
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>
24
85
</div>
25
86
</div>
26
87
<div id="postContent">
27
88
{#if post.replyingUri}
28
89
<a
29
90
id="replyingText"
30
-
href="https://deer.social/profile/{post.replyingUri.repo}/post/{post
91
+
href="{Config.FRONTEND_URL}/profile/{post.replyingUri.repo}/post/{post
31
92
.replyingUri.rkey}">replying to {post.replyingUri.repo}</a
32
93
>
33
94
{/if}
34
-
<p>{post.text}</p>
35
-
36
95
{#if post.quotingUri}
37
96
<a
38
97
id="quotingText"
39
-
href="https://deer.social/profile/{post.quotingUri.repo}/post/{post
98
+
href="{Config.FRONTEND_URL}/profile/{post.quotingUri.repo}/post/{post
40
99
.quotingUri.rkey}">quoting {post.quotingUri.repo}</a
41
100
>
42
101
{/if}
43
-
{#if post.imagesCid}
44
-
<div id="imagesContainer">
45
-
{#each post.imagesCid as imageLink}
46
-
<img
47
-
id="embedImages"
48
-
alt="Post Image"
49
-
src="{Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did={post.authorDid}&cid={imageLink}"
50
-
/>
51
-
{/each}
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}
52
134
</div>
53
135
{/if}
54
136
{#if post.videosLinkCid}
137
+
<!-- svelte-ignore a11y_media_has_caption -->
55
138
<video
56
139
id="embedVideo"
57
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"
58
149
/>
59
150
{/if}
60
151
</div>
61
152
</div>
62
153
63
154
<style>
64
-
#postContainer {
65
-
display: flex;
66
-
flex-direction: column;
67
-
border: 1px solid #8054f0;
68
-
background-color: black;
69
-
margin-bottom: -1px;
70
-
}
71
-
#postHeader {
72
-
display: flex;
73
-
flex-direction: row;
74
-
align-items: center;
75
-
justify-content: start;
76
-
background-color: #1f1145;
77
-
padding: 0px 0px;
78
-
height: fit-content;
79
-
border-bottom: 1px solid #8054f0;
80
-
font-weight: bold;
81
-
}
82
-
#postContent {
83
-
display: flex;
84
-
text-align: start;
85
-
flex-direction: column;
86
-
padding: 10px;
87
-
background-color: #0d0620;
88
-
color: white;
89
-
}
90
-
#replyingText {
91
-
font-size: 0.7em;
92
-
color: white;
93
-
margin: 0;
94
-
margin-bottom: 10px;
95
-
padding: 0;
96
-
}
97
-
#postText {
98
-
margin: 0;
99
-
padding: 0;
100
-
}
101
-
#headerText {
102
-
margin-left: 10px;
103
-
font-size: 0.9em;
104
-
text-align: start;
105
-
}
106
-
#avatar {
107
-
width: 50px;
108
-
height: 50px;
109
-
margin: 0px;
110
-
margin-left: 0px;
111
-
border-right: #8054f0 1px solid;
112
-
}
113
-
#embedImages {
114
-
width: 50%;
115
-
height: 50%;
116
-
margin-top: 0px;
117
-
margin-bottom: -5px;
118
-
}
119
-
#embedVideo {
120
-
width: 50%;
121
-
height: 50%;
122
-
}
155
+
123
156
</style>
+187
-83
src/lib/pdsfetch.ts
+187
-83
src/lib/pdsfetch.ts
···
13
13
WebDidDocumentResolver,
14
14
} from "@atcute/identity-resolver";
15
15
import { Config } from "../../config";
16
+
import { Mutex } from "mutex-ts"
16
17
// import { ComAtprotoRepoListRecords.Record } from "@atcute/client/lexicons";
17
18
// import { AppBskyFeedPost } from "@atcute/client/lexicons";
18
19
// import { AppBskyActorDefs } from "@atcute/client/lexicons";
19
20
20
21
interface AccountMetadata {
21
-
did: string;
22
+
did: At.Did;
22
23
displayName: string;
23
24
handle: string;
24
25
avatarCid: string | null;
26
+
currentCursor?: string;
25
27
}
28
+
29
+
let accountsMetadata: AccountMetadata[] = [];
30
+
26
31
interface atUriObject {
27
32
repo: string;
28
33
collection: string;
···
32
37
authorDid: string;
33
38
authorAvatarCid: string | null;
34
39
postCid: string;
40
+
recordName: string;
35
41
authorHandle: string;
36
42
displayName: string;
37
43
text: string;
···
41
47
replyingUri: atUriObject | null;
42
48
imagesCid: string[] | null;
43
49
videosLinkCid: string | null;
50
+
gifLink: string | null;
44
51
45
52
constructor(
46
53
record: ComAtprotoRepoListRecords.Record,
47
54
account: AccountMetadata,
48
55
) {
49
56
this.postCid = record.cid;
57
+
this.recordName = processAtUri(record.uri).rkey;
50
58
this.authorDid = account.did;
51
59
this.authorAvatarCid = account.avatarCid;
52
60
this.authorHandle = account.handle;
···
63
71
this.quotingUri = null;
64
72
this.imagesCid = null;
65
73
this.videosLinkCid = null;
74
+
this.gifLink = null;
66
75
switch (post.embed?.$type) {
67
76
case "app.bsky.embed.images":
68
-
this.imagesCid = post.embed.images.map((imageRecord: any) =>
69
-
imageRecord.image.ref.$link
77
+
this.imagesCid = post.embed.images.map(
78
+
(imageRecord: any) => imageRecord.image.ref.$link,
70
79
);
71
80
break;
72
81
case "app.bsky.embed.video":
···
79
88
this.quotingUri = processAtUri(post.embed.record.record.uri);
80
89
switch (post.embed.media.$type) {
81
90
case "app.bsky.embed.images":
82
-
this.imagesCid = post.embed.media.images.map((imageRecord) =>
83
-
imageRecord.image.ref.$link
91
+
this.imagesCid = post.embed.media.images.map(
92
+
(imageRecord) => imageRecord.image.ref.$link,
84
93
);
85
94
86
95
break;
···
88
97
this.videosLinkCid = post.embed.media.video.ref.$link;
89
98
90
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;
91
105
}
92
106
break;
93
107
}
···
109
123
}),
110
124
});
111
125
112
-
const getDidsFromPDS = async () => {
126
+
const getDidsFromPDS = async (): Promise<At.Did[]> => {
113
127
const { data } = await rpc.get("com.atproto.sync.listRepos", {
114
128
params: {},
115
129
});
116
-
return data.repos.map((repo: any) => (repo.did));
130
+
return data.repos.map((repo: any) => repo.did) as At.Did[];
117
131
};
118
-
const getAccountMetadata = async (did: `did:${string}:${string}`) => {
132
+
const getAccountMetadata = async (
133
+
did: `did:${string}:${string}`,
134
+
) => {
119
135
// gonna assume self exists in the app.bsky.actor.profile
120
136
try {
121
-
const { data } = await rpc.get("com.atproto.repo.getRecord", {
122
-
params: {
123
-
repo: did,
124
-
collection: "app.bsky.actor.profile",
125
-
rkey: "self",
126
-
},
127
-
});
128
-
const value = data.value as AppBskyActorProfile.Record;
129
-
const handle = await blueskyHandleFromDid(did);
130
-
const account: AccountMetadata = {
131
-
did: did,
132
-
handle: handle,
133
-
displayName: value.displayName || "",
134
-
avatarCid: null,
135
-
};
136
-
if (value.avatar) {
137
-
account.avatarCid = value.avatar.ref["$link"];
138
-
}
139
-
return account;
140
-
}
141
-
catch (e) {
142
-
console.error(`Error fetching metadata for ${did}:`, e);
143
-
return {
144
-
did: "error",
145
-
displayName: "",
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 || "",
146
150
avatarCid: null,
147
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;
148
159
}
149
160
};
150
161
151
-
const getAllMetadataFromPds = async () => {
162
+
const getAllMetadataFromPds = async (): Promise<AccountMetadata[]> => {
152
163
const dids = await getDidsFromPDS();
153
164
const metadata = await Promise.all(
154
165
dids.map(async (repo: `did:${string}:${string}`) => {
155
166
return await getAccountMetadata(repo);
156
167
}),
157
168
);
158
-
return metadata.filter(account => account.did !== "error");
159
-
};
160
-
161
-
const fetchPosts = async (did: string) => {
162
-
try {
163
-
const { data } = await rpc.get("com.atproto.repo.listRecords", {
164
-
params: {
165
-
repo: did as At.Identifier,
166
-
collection: "app.bsky.feed.post",
167
-
limit: 5,
168
-
},
169
-
});
170
-
return {
171
-
records: data.records as ComAtprotoRepoListRecords.Record[],
172
-
did: did,
173
-
error: false
174
-
};
175
-
} catch (e) {
176
-
console.error(`Error fetching posts for ${did}:`, e);
177
-
return {
178
-
records: [],
179
-
did: did,
180
-
error: true
181
-
};
182
-
}
169
+
return metadata.filter((account) => account !== null) as AccountMetadata[];
183
170
};
184
171
185
172
const identityResolve = async (did: At.Did) => {
···
215
202
}
216
203
};
217
204
218
-
const fetchAllPosts = async () => {
219
-
const users: AccountMetadata[] = await getAllMetadataFromPds();
220
-
const postRecords = await Promise.all(
221
-
users.map(async (metadata: AccountMetadata) =>
222
-
await fetchPosts(metadata.did)
223
-
),
224
-
);
225
-
const validPostRecords = postRecords.filter(record => !record.error);
226
-
const posts: Post[] = validPostRecords.flatMap((userFetch) =>
227
-
userFetch.records.map((record) => {
228
-
const user = users.find((user: AccountMetadata) =>
229
-
user.did == userFetch.did
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,
230
238
);
231
-
if (!user) {
232
-
throw new Error(`User with DID ${userFetch.did} not found`);
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
+
};
233
276
}
234
-
return new Post(record, user);
235
-
})
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
+
}
236
294
);
237
-
posts.sort((a, b) => b.timestamp - a.timestamp);
238
-
return posts;
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;
239
332
};
240
333
241
-
const testApiCall = async () => {
242
-
const { data } = await rpc.get("com.atproto.sync.listRepos", {
243
-
params: {},
244
-
});
245
-
console.log(data);
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
+
}
246
349
};
247
-
export { fetchAllPosts, getAllMetadataFromPds, Post };
350
+
351
+
export { getAllMetadataFromPds, getNextPosts, Post };
248
352
export type { AccountMetadata };
+6
-6
src/main.ts
+6
-6
src/main.ts
···
1
-
import { mount } from 'svelte'
2
-
import './app.css'
3
-
import App from './App.svelte'
1
+
import { mount } from "svelte";
2
+
import "./app.css";
3
+
import App from "./App.svelte";
4
4
5
5
const app = mount(App, {
6
-
target: document.getElementById('app')!,
7
-
})
6
+
target: document.getElementById("app")!,
7
+
});
8
8
9
-
export default app
9
+
export default app;
+2
-2
svelte.config.js
+2
-2
svelte.config.js
···
1
-
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
1
+
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
2
2
3
3
export default {
4
4
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
5
5
// for more information about preprocessors
6
6
preprocess: vitePreprocess(),
7
-
}
7
+
};
+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
+
};
+8
-4
vite.config.ts
+8
-4
vite.config.ts
···
1
-
import { defineConfig } from 'vite'
2
-
import { svelte } from '@sveltejs/vite-plugin-svelte'
1
+
import { defineConfig } from "vite";
2
+
import { svelte } from "@sveltejs/vite-plugin-svelte";
3
+
import { themePlugin } from "./theming";
3
4
4
5
// https://vite.dev/config/
5
6
export default defineConfig({
6
-
plugins: [svelte()],
7
-
})
7
+
plugins: [
8
+
themePlugin(),
9
+
svelte(),
10
+
],
11
+
});