Compare commits

..

2 commits

14 changed files with 209 additions and 584 deletions

21
LICENSE
View file

@ -1,21 +0,0 @@
# MIT License
Copyright (c) 2025 Witchcraft Systems
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,62 +1,3 @@
# pds-dash # pds-dash
a frontend dashboard with stats for your ATProto PDS. Frontend with stats for your ATProto PDS
## setup
### prerequisites
- [deno](https://deno.com/manual/getting_started/installation)
### installing
clone the repo, install dependencies using deno:
```sh
deno install
```
### development server
local develompent server with hot reloading:
```sh
deno task dev
```
### building
to build the optimized bundle run:
```sh
deno task build
```
the output will be in the `dist/` directory.
## deploying
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.
## configuring
[`config.ts`](config.ts) is the main configuration file, you can find more information in the file itself.
## theming
the colors are designated in [`src/app.css`](src/app.css) as variables, go crazy with them
the rest is done by editing the css files and style tags directly, good luck
relevant files:
- [`src/App.svelte`](src/App.svelte)
- [`src/app.css`](src/app.css)
- [`src/lib/AccountComponent.svelte`](src/lib/AccountComponent.svelte)
- [`src/lib/PostComponent.svelte`](src/lib/PostComponent.svelte)
the favicon is located at [`public/favicon.ico`](public/favicon.ico)
## license
MIT

View file

@ -2,36 +2,27 @@
* Configuration module for the PDS Dashboard * Configuration module for the PDS Dashboard
*/ */
export class Config { export class Config {
/** /**
* The base URL of the PDS (Personal Data Server) * The base URL of the PDS (Personal Data Server)
* @default "https://pds.witchcraft.systems" * @default "https://pds.witchcraft.systems"
*/ */
static readonly PDS_URL: string = "https://pds.witchcraft.systems"; static readonly PDS_URL: string = "https://pds.witchcraft.systems";
/** /**
* The base URL of the frontend service for linking to replies/quotes/accounts etc. * The base URL of the frontend service for linking to replies
* @default "https://deer.social" * @default "https://deer.social"
*/ */
static readonly FRONTEND_URL: string = "https://deer.social"; static readonly FRONTEND_URL: string = "https://deer.social";
/** /**
* Maximum number of posts to fetch from the PDS per request * Maximum number of posts to show in the feed (across all users)
* Should be around 20 for about 10 users on the pds * @default 100
* The more users you have, the lower the number should be */
* since sorting is slow and is done on the frontend static readonly MAX_POSTS: number = 100;
* @default 20
*/
static readonly MAX_POSTS: number = 20;
/** /**
* Footer text for the dashboard, you probably want to change this * Footer text for the dashboard
*/ * @default "Astrally projected from witchcraft.systems"
static readonly FOOTER_TEXT: string = */
"Astrally projected from <a href='https://witchcraft.systems' target='_blank'>witchcraft.systems</a><br><br><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>)"; static readonly FOOTER_TEXT: string = "Astrally projected from <a href='https://witchcraft.systems' target='_blank'>witchcraft.systems</a>";
}
/**
* Whether to show the posts that are in the future
* @default false
*/
static readonly SHOW_FUTURE_POSTS: boolean = false;
}

234
deno.lock generated
View file

@ -1,5 +1,5 @@
{ {
"version": "5", "version": "4",
"specifiers": { "specifiers": {
"npm:@atcute/bluesky@^2.0.2": "2.0.2_@atcute+client@3.0.1", "npm:@atcute/bluesky@^2.0.2": "2.0.2_@atcute+client@3.0.1",
"npm:@atcute/client@^3.0.1": "3.0.1", "npm:@atcute/client@^3.0.1": "3.0.1",
@ -7,9 +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", "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",
"npm:@tsconfig/svelte@^5.0.4": "5.0.4", "npm:@tsconfig/svelte@^5.0.4": "5.0.4",
"npm:moment@^2.30.1": "2.30.1", "npm:moment@^2.30.1": "2.30.1",
"npm:mutex-ts@^1.2.1": "1.2.1",
"npm:svelte-check@^4.1.5": "4.1.6_svelte@5.28.1__acorn@8.14.1_typescript@5.7.3", "npm:svelte-check@^4.1.5": "4.1.6_svelte@5.28.1__acorn@8.14.1_typescript@5.7.3",
"npm:svelte-infinite-loading@^1.4.0": "1.4.0",
"npm:svelte@^5.23.1": "5.28.1_acorn@8.14.1", "npm:svelte@^5.23.1": "5.28.1_acorn@8.14.1",
"npm:typescript@~5.7.2": "5.7.3", "npm:typescript@~5.7.2": "5.7.3",
"npm:vite@^6.3.1": "6.3.2_picomatch@4.0.2" "npm:vite@^6.3.1": "6.3.2_picomatch@4.0.2"
@ -55,129 +53,79 @@
"integrity": "sha512-GEhUCk9c4XbNxi+0YZHZsV4fYNd6HejfWuN4Ti4c02DauX+LyX5WY1Y3WfyZ8Pxxl0zqhs+MLtW98cMh86vv6g==" "integrity": "sha512-GEhUCk9c4XbNxi+0YZHZsV4fYNd6HejfWuN4Ti4c02DauX+LyX5WY1Y3WfyZ8Pxxl0zqhs+MLtW98cMh86vv6g=="
}, },
"@esbuild/aix-ppc64@0.25.2": { "@esbuild/aix-ppc64@0.25.2": {
"integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag=="
"os": ["aix"],
"cpu": ["ppc64"]
}, },
"@esbuild/android-arm64@0.25.2": { "@esbuild/android-arm64@0.25.2": {
"integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w=="
"os": ["android"],
"cpu": ["arm64"]
}, },
"@esbuild/android-arm@0.25.2": { "@esbuild/android-arm@0.25.2": {
"integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA=="
"os": ["android"],
"cpu": ["arm"]
}, },
"@esbuild/android-x64@0.25.2": { "@esbuild/android-x64@0.25.2": {
"integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg=="
"os": ["android"],
"cpu": ["x64"]
}, },
"@esbuild/darwin-arm64@0.25.2": { "@esbuild/darwin-arm64@0.25.2": {
"integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==", "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA=="
"os": ["darwin"],
"cpu": ["arm64"]
}, },
"@esbuild/darwin-x64@0.25.2": { "@esbuild/darwin-x64@0.25.2": {
"integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA=="
"os": ["darwin"],
"cpu": ["x64"]
}, },
"@esbuild/freebsd-arm64@0.25.2": { "@esbuild/freebsd-arm64@0.25.2": {
"integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w=="
"os": ["freebsd"],
"cpu": ["arm64"]
}, },
"@esbuild/freebsd-x64@0.25.2": { "@esbuild/freebsd-x64@0.25.2": {
"integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ=="
"os": ["freebsd"],
"cpu": ["x64"]
}, },
"@esbuild/linux-arm64@0.25.2": { "@esbuild/linux-arm64@0.25.2": {
"integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g=="
"os": ["linux"],
"cpu": ["arm64"]
}, },
"@esbuild/linux-arm@0.25.2": { "@esbuild/linux-arm@0.25.2": {
"integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g=="
"os": ["linux"],
"cpu": ["arm"]
}, },
"@esbuild/linux-ia32@0.25.2": { "@esbuild/linux-ia32@0.25.2": {
"integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ=="
"os": ["linux"],
"cpu": ["ia32"]
}, },
"@esbuild/linux-loong64@0.25.2": { "@esbuild/linux-loong64@0.25.2": {
"integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w=="
"os": ["linux"],
"cpu": ["loong64"]
}, },
"@esbuild/linux-mips64el@0.25.2": { "@esbuild/linux-mips64el@0.25.2": {
"integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q=="
"os": ["linux"],
"cpu": ["mips64el"]
}, },
"@esbuild/linux-ppc64@0.25.2": { "@esbuild/linux-ppc64@0.25.2": {
"integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g=="
"os": ["linux"],
"cpu": ["ppc64"]
}, },
"@esbuild/linux-riscv64@0.25.2": { "@esbuild/linux-riscv64@0.25.2": {
"integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw=="
"os": ["linux"],
"cpu": ["riscv64"]
}, },
"@esbuild/linux-s390x@0.25.2": { "@esbuild/linux-s390x@0.25.2": {
"integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q=="
"os": ["linux"],
"cpu": ["s390x"]
}, },
"@esbuild/linux-x64@0.25.2": { "@esbuild/linux-x64@0.25.2": {
"integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg=="
"os": ["linux"],
"cpu": ["x64"]
}, },
"@esbuild/netbsd-arm64@0.25.2": { "@esbuild/netbsd-arm64@0.25.2": {
"integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw=="
"os": ["netbsd"],
"cpu": ["arm64"]
}, },
"@esbuild/netbsd-x64@0.25.2": { "@esbuild/netbsd-x64@0.25.2": {
"integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg=="
"os": ["netbsd"],
"cpu": ["x64"]
}, },
"@esbuild/openbsd-arm64@0.25.2": { "@esbuild/openbsd-arm64@0.25.2": {
"integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg=="
"os": ["openbsd"],
"cpu": ["arm64"]
}, },
"@esbuild/openbsd-x64@0.25.2": { "@esbuild/openbsd-x64@0.25.2": {
"integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw=="
"os": ["openbsd"],
"cpu": ["x64"]
}, },
"@esbuild/sunos-x64@0.25.2": { "@esbuild/sunos-x64@0.25.2": {
"integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA=="
"os": ["sunos"],
"cpu": ["x64"]
}, },
"@esbuild/win32-arm64@0.25.2": { "@esbuild/win32-arm64@0.25.2": {
"integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q=="
"os": ["win32"],
"cpu": ["arm64"]
}, },
"@esbuild/win32-ia32@0.25.2": { "@esbuild/win32-ia32@0.25.2": {
"integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg=="
"os": ["win32"],
"cpu": ["ia32"]
}, },
"@esbuild/win32-x64@0.25.2": { "@esbuild/win32-x64@0.25.2": {
"integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA=="
"os": ["win32"],
"cpu": ["x64"]
}, },
"@jridgewell/gen-mapping@0.3.8": { "@jridgewell/gen-mapping@0.3.8": {
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
@ -204,104 +152,64 @@
] ]
}, },
"@rollup/rollup-android-arm-eabi@4.40.0": { "@rollup/rollup-android-arm-eabi@4.40.0": {
"integrity": "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==", "integrity": "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg=="
"os": ["android"],
"cpu": ["arm"]
}, },
"@rollup/rollup-android-arm64@4.40.0": { "@rollup/rollup-android-arm64@4.40.0": {
"integrity": "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==", "integrity": "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w=="
"os": ["android"],
"cpu": ["arm64"]
}, },
"@rollup/rollup-darwin-arm64@4.40.0": { "@rollup/rollup-darwin-arm64@4.40.0": {
"integrity": "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==", "integrity": "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ=="
"os": ["darwin"],
"cpu": ["arm64"]
}, },
"@rollup/rollup-darwin-x64@4.40.0": { "@rollup/rollup-darwin-x64@4.40.0": {
"integrity": "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==", "integrity": "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA=="
"os": ["darwin"],
"cpu": ["x64"]
}, },
"@rollup/rollup-freebsd-arm64@4.40.0": { "@rollup/rollup-freebsd-arm64@4.40.0": {
"integrity": "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==", "integrity": "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg=="
"os": ["freebsd"],
"cpu": ["arm64"]
}, },
"@rollup/rollup-freebsd-x64@4.40.0": { "@rollup/rollup-freebsd-x64@4.40.0": {
"integrity": "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==", "integrity": "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw=="
"os": ["freebsd"],
"cpu": ["x64"]
}, },
"@rollup/rollup-linux-arm-gnueabihf@4.40.0": { "@rollup/rollup-linux-arm-gnueabihf@4.40.0": {
"integrity": "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==", "integrity": "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA=="
"os": ["linux"],
"cpu": ["arm"]
}, },
"@rollup/rollup-linux-arm-musleabihf@4.40.0": { "@rollup/rollup-linux-arm-musleabihf@4.40.0": {
"integrity": "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==", "integrity": "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg=="
"os": ["linux"],
"cpu": ["arm"]
}, },
"@rollup/rollup-linux-arm64-gnu@4.40.0": { "@rollup/rollup-linux-arm64-gnu@4.40.0": {
"integrity": "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==", "integrity": "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg=="
"os": ["linux"],
"cpu": ["arm64"]
}, },
"@rollup/rollup-linux-arm64-musl@4.40.0": { "@rollup/rollup-linux-arm64-musl@4.40.0": {
"integrity": "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==", "integrity": "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ=="
"os": ["linux"],
"cpu": ["arm64"]
}, },
"@rollup/rollup-linux-loongarch64-gnu@4.40.0": { "@rollup/rollup-linux-loongarch64-gnu@4.40.0": {
"integrity": "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==", "integrity": "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg=="
"os": ["linux"],
"cpu": ["loong64"]
}, },
"@rollup/rollup-linux-powerpc64le-gnu@4.40.0": { "@rollup/rollup-linux-powerpc64le-gnu@4.40.0": {
"integrity": "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==", "integrity": "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw=="
"os": ["linux"],
"cpu": ["ppc64"]
}, },
"@rollup/rollup-linux-riscv64-gnu@4.40.0": { "@rollup/rollup-linux-riscv64-gnu@4.40.0": {
"integrity": "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==", "integrity": "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA=="
"os": ["linux"],
"cpu": ["riscv64"]
}, },
"@rollup/rollup-linux-riscv64-musl@4.40.0": { "@rollup/rollup-linux-riscv64-musl@4.40.0": {
"integrity": "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==", "integrity": "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ=="
"os": ["linux"],
"cpu": ["riscv64"]
}, },
"@rollup/rollup-linux-s390x-gnu@4.40.0": { "@rollup/rollup-linux-s390x-gnu@4.40.0": {
"integrity": "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==", "integrity": "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw=="
"os": ["linux"],
"cpu": ["s390x"]
}, },
"@rollup/rollup-linux-x64-gnu@4.40.0": { "@rollup/rollup-linux-x64-gnu@4.40.0": {
"integrity": "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==", "integrity": "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ=="
"os": ["linux"],
"cpu": ["x64"]
}, },
"@rollup/rollup-linux-x64-musl@4.40.0": { "@rollup/rollup-linux-x64-musl@4.40.0": {
"integrity": "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==", "integrity": "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw=="
"os": ["linux"],
"cpu": ["x64"]
}, },
"@rollup/rollup-win32-arm64-msvc@4.40.0": { "@rollup/rollup-win32-arm64-msvc@4.40.0": {
"integrity": "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==", "integrity": "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ=="
"os": ["win32"],
"cpu": ["arm64"]
}, },
"@rollup/rollup-win32-ia32-msvc@4.40.0": { "@rollup/rollup-win32-ia32-msvc@4.40.0": {
"integrity": "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==", "integrity": "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA=="
"os": ["win32"],
"cpu": ["ia32"]
}, },
"@rollup/rollup-win32-x64-msvc@4.40.0": { "@rollup/rollup-win32-x64-msvc@4.40.0": {
"integrity": "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==", "integrity": "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ=="
"os": ["win32"],
"cpu": ["x64"]
}, },
"@sveltejs/acorn-typescript@1.0.5_acorn@8.14.1": { "@sveltejs/acorn-typescript@1.0.5_acorn@8.14.1": {
"integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==", "integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==",
@ -338,8 +246,7 @@
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==" "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="
}, },
"acorn@8.14.1": { "acorn@8.14.1": {
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="
"bin": true
}, },
"aria-query@5.3.2": { "aria-query@5.3.2": {
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==" "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="
@ -367,7 +274,7 @@
}, },
"esbuild@0.25.2": { "esbuild@0.25.2": {
"integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==",
"optionalDependencies": [ "dependencies": [
"@esbuild/aix-ppc64", "@esbuild/aix-ppc64",
"@esbuild/android-arm", "@esbuild/android-arm",
"@esbuild/android-arm64", "@esbuild/android-arm64",
@ -393,9 +300,7 @@
"@esbuild/win32-arm64", "@esbuild/win32-arm64",
"@esbuild/win32-ia32", "@esbuild/win32-ia32",
"@esbuild/win32-x64" "@esbuild/win32-x64"
], ]
"scripts": true,
"bin": true
}, },
"esm-env@1.2.2": { "esm-env@1.2.2": {
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==" "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="
@ -410,15 +315,10 @@
"integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
"dependencies": [ "dependencies": [
"picomatch" "picomatch"
],
"optionalPeers": [
"picomatch"
] ]
}, },
"fsevents@2.3.3": { "fsevents@2.3.3": {
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="
"os": ["darwin"],
"scripts": true
}, },
"is-reference@3.0.3": { "is-reference@3.0.3": {
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
@ -447,12 +347,8 @@
"ms@2.1.3": { "ms@2.1.3": {
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
}, },
"mutex-ts@1.2.1": {
"integrity": "sha512-OkcXgf0viuCgYdnm48kiNQ9PzC5OzISQ261svHr/Ybc2vBYC/5xfLXn44hQ+dYRX74v7MCSqV/LKPEbpYdDybw=="
},
"nanoid@3.3.11": { "nanoid@3.3.11": {
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="
"bin": true
}, },
"picocolors@1.1.1": { "picocolors@1.1.1": {
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
@ -474,9 +370,6 @@
"rollup@4.40.0": { "rollup@4.40.0": {
"integrity": "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==", "integrity": "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==",
"dependencies": [ "dependencies": [
"@types/estree"
],
"optionalDependencies": [
"@rollup/rollup-android-arm-eabi", "@rollup/rollup-android-arm-eabi",
"@rollup/rollup-android-arm64", "@rollup/rollup-android-arm64",
"@rollup/rollup-darwin-arm64", "@rollup/rollup-darwin-arm64",
@ -497,9 +390,9 @@
"@rollup/rollup-win32-arm64-msvc", "@rollup/rollup-win32-arm64-msvc",
"@rollup/rollup-win32-ia32-msvc", "@rollup/rollup-win32-ia32-msvc",
"@rollup/rollup-win32-x64-msvc", "@rollup/rollup-win32-x64-msvc",
"@types/estree",
"fsevents" "fsevents"
], ]
"bin": true
}, },
"sade@1.8.1": { "sade@1.8.1": {
"integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
@ -520,11 +413,7 @@
"sade", "sade",
"svelte", "svelte",
"typescript" "typescript"
], ]
"bin": true
},
"svelte-infinite-loading@1.4.0": {
"integrity": "sha512-Jo+f/yr/HmZQuIiiKKzAHVFXdAUWHW2RBbrcQTil8JVk1sCm/riy7KTJVzjBgQvHasrFQYKF84zvtc9/Y4lFYg=="
}, },
"svelte@5.28.1_acorn@8.14.1": { "svelte@5.28.1_acorn@8.14.1": {
"integrity": "sha512-iOa9WmfNG95lSOSJdMhdjJ4Afok7IRAQYXpbnxhd5EINnXseG0GVa9j6WPght4eX78XfFez45Fi+uRglGKPV/Q==", "integrity": "sha512-iOa9WmfNG95lSOSJdMhdjJ4Afok7IRAQYXpbnxhd5EINnXseG0GVa9j6WPght4eX78XfFez45Fi+uRglGKPV/Q==",
@ -553,31 +442,24 @@
] ]
}, },
"typescript@5.7.3": { "typescript@5.7.3": {
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="
"bin": true
}, },
"vite@6.3.2_picomatch@4.0.2": { "vite@6.3.2_picomatch@4.0.2": {
"integrity": "sha512-ZSvGOXKGceizRQIZSz7TGJ0pS3QLlVY/9hwxVh17W3re67je1RKYzFHivZ/t0tubU78Vkyb9WnHPENSBCzbckg==", "integrity": "sha512-ZSvGOXKGceizRQIZSz7TGJ0pS3QLlVY/9hwxVh17W3re67je1RKYzFHivZ/t0tubU78Vkyb9WnHPENSBCzbckg==",
"dependencies": [ "dependencies": [
"esbuild", "esbuild",
"fdir", "fdir",
"fsevents",
"picomatch", "picomatch",
"postcss", "postcss",
"rollup", "rollup",
"tinyglobby" "tinyglobby"
], ]
"optionalDependencies": [
"fsevents"
],
"bin": true
}, },
"vitefu@1.0.6_vite@6.3.2__picomatch@4.0.2": { "vitefu@1.0.6_vite@6.3.2__picomatch@4.0.2": {
"integrity": "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA==", "integrity": "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA==",
"dependencies": [ "dependencies": [
"vite" "vite"
],
"optionalPeers": [
"vite"
] ]
}, },
"zimmerframe@1.1.2": { "zimmerframe@1.1.2": {
@ -593,9 +475,7 @@
"npm:@sveltejs/vite-plugin-svelte@^5.0.3", "npm:@sveltejs/vite-plugin-svelte@^5.0.3",
"npm:@tsconfig/svelte@^5.0.4", "npm:@tsconfig/svelte@^5.0.4",
"npm:moment@^2.30.1", "npm:moment@^2.30.1",
"npm:mutex-ts@^1.2.1",
"npm:svelte-check@^4.1.5", "npm:svelte-check@^4.1.5",
"npm:svelte-infinite-loading@^1.4.0",
"npm:svelte@^5.23.1", "npm:svelte@^5.23.1",
"npm:typescript@~5.7.2", "npm:typescript@~5.7.2",
"npm:vite@^6.3.1" "npm:vite@^6.3.1"

View file

@ -1,4 +1,4 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />

View file

@ -13,9 +13,7 @@
"@atcute/bluesky": "^2.0.2", "@atcute/bluesky": "^2.0.2",
"@atcute/client": "^3.0.1", "@atcute/client": "^3.0.1",
"@atcute/identity-resolver": "^0.1.2", "@atcute/identity-resolver": "^0.1.2",
"moment": "^2.30.1", "moment": "^2.30.1"
"mutex-ts": "^1.2.1",
"svelte-infinite-loading": "^1.4.0"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/vite-plugin-svelte": "^5.0.3", "@sveltejs/vite-plugin-svelte": "^5.0.3",

View file

@ -1,69 +1,47 @@
<script lang="ts"> <script lang="ts">
import PostComponent from "./lib/PostComponent.svelte"; import PostComponent from "./lib/PostComponent.svelte";
import AccountComponent from "./lib/AccountComponent.svelte"; import AccountComponent from "./lib/AccountComponent.svelte";
import InfiniteLoading from "svelte-infinite-loading"; import { fetchAllPosts, Post, getAllMetadataFromPds } from "./lib/pdsfetch";
import { getNextPosts, Post, getAllMetadataFromPds } from "./lib/pdsfetch";
import { Config } from "../config"; import { Config } from "../config";
const postsPromise = fetchAllPosts();
const accountsPromise = getAllMetadataFromPds(); const accountsPromise = getAllMetadataFromPds();
import { onMount } from "svelte";
let posts: Post[] = [];
onMount(() => {
// Fetch initial posts
getNextPosts().then((initialPosts) => {
posts = initialPosts;
});
});
// Infinite loading function
const onInfinite = ({
detail: { loaded, complete },
}: {
detail: { loaded: () => void; complete: () => void };
}) => {
getNextPosts().then((newPosts) => {
console.log("Loading next posts...");
if (newPosts.length > 0) {
posts = [...posts, ...newPosts];
loaded();
} else {
complete();
}
});
};
</script> </script>
<main> <main>
<div id="Content"> <div id="Content">
{#await accountsPromise} {#await accountsPromise}
<p>Loading...</p> <p>Loading...</p>
{:then accountsData} {:then accountsData}
<div id="Account"> <div id="Account">
<h1 id="Header">ATProto PDS</h1> <h1 id="Header">ATProto PDS</h1>
<p>Home to {accountsData.length} accounts</p> <p>Home to {accountsData.length} accounts</p>
<div id="accountsList"> <div id="accountsList">
{#each accountsData as accountObject} {#each accountsData as accountObject}
<AccountComponent account={accountObject} /> <AccountComponent account={accountObject} />
{/each} {/each}
</div>
<p>{@html Config.FOOTER_TEXT}</p>
</div> </div>
{:catch error} <p>{@html Config.FOOTER_TEXT}</p>
<p>Error: {error.message}</p> </div>
{/await} {:catch error}
<p>Error: {error.message}</p>
{/await}
{#await postsPromise}
<p>Loading...</p>
{:then postsData}
<div id="Feed"> <div id="Feed">
<div id="spacer"></div> <div id="spacer"></div>
{#each posts as postObject} {#each postsData as postObject}
<PostComponent post={postObject as Post} /> <PostComponent post={postObject as Post} />
{/each} {/each}
<InfiniteLoading on:infinite={onInfinite} distance={3000} />
<div id="spacer"></div> <div id="spacer"></div>
</div> </div>
{/await}
</div> </div>
</main> </main>
<style> <style>
/* desktop style */ /* desktop style */
#Content { #Content {
@ -78,9 +56,9 @@
color: var(--text-color); color: var(--text-color);
} }
#Feed { #Feed {
overflow-y: scroll;
width: 65%; width: 65%;
height: 100vh; height: 100vh;
overflow-y: scroll;
padding: 20px; padding: 20px;
padding-bottom: 0; padding-bottom: 0;
padding-top: 0; padding-top: 0;
@ -96,7 +74,7 @@
#Account { #Account {
width: 35%; width: 35%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
background-color: var(--content-background-color); background-color: var(--content-background-color);
height: 80vh; height: 80vh;
@ -129,7 +107,7 @@
margin-top: 5%; margin-top: 5%;
} }
#Account { #Account {
width: 85%; width: auto;
padding-left: 5%; padding-left: 5%;
padding-right: 5%; padding-right: 5%;
margin-bottom: 20px; margin-bottom: 20px;
@ -143,7 +121,7 @@
margin-left: 10%; margin-left: 10%;
margin-right: 10%; margin-right: 10%;
padding: 0px; padding: 0px;
overflow-y: visible; height: auto;
} }
#spacer { #spacer {

View file

@ -1,5 +1,5 @@
@font-face { @font-face {
font-family: "ProggyClean"; font-family: 'ProggyClean';
src: url(https://witchcraft.systems/ProggyCleanNerdFont-Regular.ttf); src: url(https://witchcraft.systems/ProggyCleanNerdFont-Regular.ttf);
} }
@ -62,14 +62,10 @@ body {
min-width: 320px; min-width: 320px;
min-height: 100vh; min-height: 100vh;
background-color: var(--background-color); background-color: var(--background-color);
font-family: "ProggyClean", monospace; font-family: 'ProggyClean', monospace;
font-size: 24px; font-size: 24px;
color: var(--text-color); color: var(--text-color);
border-color: var(--border-color); border-color: var(--border-color);
overflow-wrap: break-word;
word-wrap: normal;
word-break: break-word;
hyphens: none;
} }
h1 { h1 {
@ -79,10 +75,9 @@ h1 {
#app { #app {
max-width: 1400px; max-width: 1400px;
width: 100%;
margin: 0; margin: 0;
padding: 0; padding: 0;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
text-align: center; text-align: center;
} }

View file

@ -32,18 +32,17 @@
#accountName { #accountName {
margin-left: 10px; margin-left: 10px;
font-size: 0.9em; font-size: 0.9em;
max-width: 80%;
/* replace overflow with ellipsis */ /* replace overflow with ellipsis */
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
max-width: 80%;
} }
#avatar { #avatar {
width: 50px; width: 50px;
height: 50px; height: 50px;
margin: 0px; margin: 0px;
object-fit: cover;
border-right: var(--border-color) 1px solid; border-right: var(--border-color) 1px solid;
} }
</style> </style>

View file

@ -113,7 +113,7 @@
<div id="carouselControls"> <div id="carouselControls">
<button <button
id="prevBtn" id="prevBtn"
onclick={prevImage} on:click={prevImage}
disabled={currentImageIndex === 0}>←</button disabled={currentImageIndex === 0}>←</button
> >
<div id="carouselIndicators"> <div id="carouselIndicators">
@ -125,7 +125,7 @@
</div> </div>
<button <button
id="nextBtn" id="nextBtn"
onclick={nextImage} on:click={nextImage}
disabled={currentImageIndex === post.imagesCid.length - 1} disabled={currentImageIndex === post.imagesCid.length - 1}
>→</button >→</button
> >
@ -141,17 +141,11 @@
controls controls
></video> ></video>
{/if} {/if}
{#if post.gifLink}
<img
id="embedVideo"
src="{post.gifLink}"
alt="Post GIF"
/>
{/if}
</div> </div>
</div> </div>
<style> <style>
a:hover { a:hover {
text-decoration: underline; text-decoration: underline;
} }
@ -177,21 +171,12 @@
height: 60px; height: 60px;
} }
#displayName { #displayName {
display: block;
color: var(--text-color); color: var(--text-color);
font-size: 1.2em; font-size: 1.2em;
padding: 0; padding: 0;
margin: 0; margin: 0;
overflow-wrap:normal;
word-wrap: break-word;
word-break: break-word;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
width: 100%;
} }
#handle { #handle {
display: block;
color: var(--border-color); color: var(--border-color);
font-size: 0.8em; font-size: 0.8em;
padding: 0; padding: 0;
@ -212,7 +197,6 @@
background-color: var(--content-background-color); background-color: var(--content-background-color);
color: var(--text-color); color: var(--text-color);
overflow-wrap: break-word; overflow-wrap: break-word;
white-space: pre-line;
} }
#replyingText { #replyingText {
font-size: 0.7em; font-size: 0.7em;
@ -229,30 +213,18 @@
#postText { #postText {
margin: 0; margin: 0;
padding: 0; padding: 0;
overflow-wrap: break-word;
word-wrap: normal;
word-break: break-word;
hyphens: none;
} }
#headerText { #headerText {
margin-left: 10px; margin-left: 10px;
font-size: 0.9em; font-size: 0.9em;
text-align: start; text-align: start;
word-break: break-word; overflow-wrap: break-word;
max-width: 80%;
max-height: 95%;
overflow: hidden; overflow: hidden;
align-self: flex-start;
margin-top: auto;
margin-bottom: auto;
} }
#avatar { #avatar {
height: 60px; height: 100%;
width: 60px;
margin: 0px; margin: 0px;
margin-left: 0px; margin-left: 0px;
overflow: hidden;
object-fit: cover;
border-right: var(--border-color) 1px solid; border-right: var(--border-color) 1px solid;
} }
#carouselContainer { #carouselContainer {

View file

@ -13,21 +13,16 @@ import {
WebDidDocumentResolver, WebDidDocumentResolver,
} from "@atcute/identity-resolver"; } from "@atcute/identity-resolver";
import { Config } from "../../config"; import { Config } from "../../config";
import { Mutex } from "mutex-ts"
// import { ComAtprotoRepoListRecords.Record } from "@atcute/client/lexicons"; // import { ComAtprotoRepoListRecords.Record } from "@atcute/client/lexicons";
// import { AppBskyFeedPost } from "@atcute/client/lexicons"; // import { AppBskyFeedPost } from "@atcute/client/lexicons";
// import { AppBskyActorDefs } from "@atcute/client/lexicons"; // import { AppBskyActorDefs } from "@atcute/client/lexicons";
interface AccountMetadata { interface AccountMetadata {
did: At.Did; did: string;
displayName: string; displayName: string;
handle: string; handle: string;
avatarCid: string | null; avatarCid: string | null;
currentCursor?: string;
} }
let accountsMetadata: AccountMetadata[] = [];
interface atUriObject { interface atUriObject {
repo: string; repo: string;
collection: string; collection: string;
@ -47,7 +42,6 @@ class Post {
replyingUri: atUriObject | null; replyingUri: atUriObject | null;
imagesCid: string[] | null; imagesCid: string[] | null;
videosLinkCid: string | null; videosLinkCid: string | null;
gifLink: string | null;
constructor( constructor(
record: ComAtprotoRepoListRecords.Record, record: ComAtprotoRepoListRecords.Record,
@ -71,11 +65,10 @@ class Post {
this.quotingUri = null; this.quotingUri = null;
this.imagesCid = null; this.imagesCid = null;
this.videosLinkCid = null; this.videosLinkCid = null;
this.gifLink = null;
switch (post.embed?.$type) { switch (post.embed?.$type) {
case "app.bsky.embed.images": case "app.bsky.embed.images":
this.imagesCid = post.embed.images.map( this.imagesCid = post.embed.images.map((imageRecord: any) =>
(imageRecord: any) => imageRecord.image.ref.$link, imageRecord.image.ref.$link
); );
break; break;
case "app.bsky.embed.video": case "app.bsky.embed.video":
@ -88,8 +81,8 @@ class Post {
this.quotingUri = processAtUri(post.embed.record.record.uri); this.quotingUri = processAtUri(post.embed.record.record.uri);
switch (post.embed.media.$type) { switch (post.embed.media.$type) {
case "app.bsky.embed.images": case "app.bsky.embed.images":
this.imagesCid = post.embed.media.images.map( this.imagesCid = post.embed.media.images.map((imageRecord) =>
(imageRecord) => imageRecord.image.ref.$link, imageRecord.image.ref.$link
); );
break; break;
@ -99,11 +92,6 @@ class Post {
break; break;
} }
break; break;
case "app.bsky.embed.external": // assuming that external embeds are gifs for now
if (post.embed.external.uri.includes(".gif")) {
this.gifLink = post.embed.external.uri;
}
break;
} }
} }
} }
@ -123,50 +111,78 @@ const rpc = new XRPC({
}), }),
}); });
const getDidsFromPDS = async (): Promise<At.Did[]> => { const getDidsFromPDS = async () : Promise<At.Did[]> => {
const { data } = await rpc.get("com.atproto.sync.listRepos", { const { data } = await rpc.get("com.atproto.sync.listRepos", {
params: {}, params: {},
}); });
return data.repos.map((repo: any) => repo.did) as At.Did[]; return data.repos.map((repo: any) => (repo.did)) as At.Did[];
}; };
const getAccountMetadata = async ( const getAccountMetadata = async (did: `did:${string}:${string}`) : Promise<AccountMetadata> => {
did: `did:${string}:${string}`,
) => {
// gonna assume self exists in the app.bsky.actor.profile // gonna assume self exists in the app.bsky.actor.profile
try { try {
const { data } = await rpc.get("com.atproto.repo.getRecord", { const { data } = await rpc.get("com.atproto.repo.getRecord", {
params: { params: {
repo: did, repo: did,
collection: "app.bsky.actor.profile", collection: "app.bsky.actor.profile",
rkey: "self", rkey: "self",
}, },
}); });
const value = data.value as AppBskyActorProfile.Record; const value = data.value as AppBskyActorProfile.Record;
const handle = await blueskyHandleFromDid(did); const handle = await blueskyHandleFromDid(did);
const account: AccountMetadata = { const account: AccountMetadata = {
did: did, did: did,
handle: handle, handle: handle,
displayName: value.displayName || "", displayName: value.displayName || "",
avatarCid: null, avatarCid: null,
}; };
if (value.avatar) { if (value.avatar) {
account.avatarCid = value.avatar.ref["$link"]; account.avatarCid = value.avatar.ref["$link"];
} }
return account; return account;
} catch (e) { }
catch (e) {
console.error(`Error fetching metadata for ${did}:`, e); console.error(`Error fetching metadata for ${did}:`, e);
return null; return {
did: "error",
displayName: "",
avatarCid: null,
handle: "error",
};
} }
}; };
const getAllMetadataFromPds = async (): Promise<AccountMetadata[]> => { const getAllMetadataFromPds = async () : Promise<AccountMetadata[]> => {
const dids = await getDidsFromPDS(); const dids = await getDidsFromPDS();
const metadata = await Promise.all( const metadata = await Promise.all(
dids.map(async (repo: `did:${string}:${string}`) => { dids.map(async (repo: `did:${string}:${string}`) => {
return await getAccountMetadata(repo); return await getAccountMetadata(repo);
}), }),
); );
return metadata.filter((account) => account !== null) as AccountMetadata[]; return metadata.filter(account => account.did !== "error");
};
const fetchPosts = async (did: string) => {
try {
const { data } = await rpc.get("com.atproto.repo.listRecords", {
params: {
repo: did as At.Identifier,
collection: "app.bsky.feed.post",
limit: Config.MAX_POSTS,
},
});
return {
records: data.records as ComAtprotoRepoListRecords.Record[],
did: did,
error: false
};
} catch (e) {
console.error(`Error fetching posts for ${did}:`, e);
return {
records: [],
did: did,
error: true
};
}
}; };
const identityResolve = async (did: At.Did) => { const identityResolve = async (did: At.Did) => {
@ -202,151 +218,27 @@ const blueskyHandleFromDid = async (did: At.Did) => {
} }
}; };
interface PostsAcc { const fetchAllPosts = async () => {
posts: ComAtprotoRepoListRecords.Record[]; const users: AccountMetadata[] = await getAllMetadataFromPds();
account: AccountMetadata; const postRecords = await Promise.all(
} users.map(async (metadata: AccountMetadata) =>
const getCutoffDate = (postAccounts: PostsAcc[]) => { await fetchPosts(metadata.did)
const now = Date.now(); ),
let cutoffDate: Date | null = null; );
postAccounts.forEach((postAcc) => { const validPostRecords = postRecords.filter(record => !record.error);
const latestPost = new Date( const posts: Post[] = validPostRecords.flatMap((userFetch) =>
(postAcc.posts[postAcc.posts.length - 1].value as AppBskyFeedPost.Record) userFetch.records.map((record) => {
.createdAt, const user = users.find((user: AccountMetadata) =>
); user.did == userFetch.did
if (!cutoffDate) { );
cutoffDate = latestPost; if (!user) {
} else { throw new Error(`User with DID ${userFetch.did} not found`);
if (latestPost > cutoffDate) {
cutoffDate = latestPost;
} }
} return new Post(record, user);
}); })
if (cutoffDate) {
return cutoffDate;
} else {
return new Date(now);
}
};
const filterPostsByDate = (posts: PostsAcc[], cutoffDate: Date) => {
// filter posts for each account that are older than the cutoff date and save the cursor of the last post included
const filteredPosts: PostsAcc[] = posts.map((postAcc) => {
const filtered = postAcc.posts.filter((post) => {
const postDate = new Date(
(post.value as AppBskyFeedPost.Record).createdAt,
);
return postDate >= cutoffDate;
});
if (filtered.length > 0) {
postAcc.account.currentCursor = processAtUri(filtered[filtered.length - 1].uri).rkey;
}
return {
posts: filtered,
account: postAcc.account,
};
});
return filteredPosts;
};
const postsMutex = new Mutex();
// nightmare function. However it works so I am not touching it
const getNextPosts = async () => {
const release = await postsMutex.obtain();
if (!accountsMetadata.length) {
accountsMetadata = await getAllMetadataFromPds();
}
const postsAcc: PostsAcc[] = await Promise.all(
accountsMetadata.map(async (account) => {
const posts = await fetchPostsForUser(
account.did,
account.currentCursor || null,
);
if (posts) {
return {
posts: posts,
account: account,
};
} else {
return {
posts: [],
account: account,
};
}
}),
); );
const recordsFiltered = postsAcc.filter((postAcc) => posts.sort((a, b) => b.timestamp - a.timestamp);
postAcc.posts.length > 0 return posts.slice(0, Config.MAX_POSTS);
);
const cutoffDate = getCutoffDate(recordsFiltered);
const recordsCutoff = filterPostsByDate(recordsFiltered, cutoffDate);
// update the accountMetadata with the new cursor
accountsMetadata = accountsMetadata.map((account) => {
const postAcc = recordsCutoff.find(
(postAcc) => postAcc.account.did == account.did,
);
if (postAcc) {
account.currentCursor = postAcc.account.currentCursor;
}
return account;
}
);
// throw the records in a big single array
let records = recordsCutoff.flatMap((postAcc) => postAcc.posts);
// sort the records by timestamp
records = records.sort((a, b) => {
const aDate = new Date(
(a.value as AppBskyFeedPost.Record).createdAt,
).getTime();
const bDate = new Date(
(b.value as AppBskyFeedPost.Record).createdAt,
).getTime();
return bDate - aDate;
});
// filter out posts that are in the future
if (!Config.SHOW_FUTURE_POSTS) {
const now = Date.now();
records = records.filter((post) => {
const postDate = new Date(
(post.value as AppBskyFeedPost.Record).createdAt,
).getTime();
return postDate <= now;
});
}
const newPosts = records.map((record) => {
const account = accountsMetadata.find(
(account) => account.did == processAtUri(record.uri).repo,
);
if (!account) {
throw new Error(
`Account with DID ${processAtUri(record.uri).repo} not found`,
);
}
return new Post(record, account);
});
// release the mutex
release();
return newPosts;
}; };
export { fetchAllPosts, getAllMetadataFromPds, Post };
const fetchPostsForUser = async (did: At.Did, cursor: string | null) => {
try {
const { data } = await rpc.get("com.atproto.repo.listRecords", {
params: {
repo: did as At.Identifier,
collection: "app.bsky.feed.post",
limit: Config.MAX_POSTS,
cursor: cursor || undefined,
},
});
return data.records as ComAtprotoRepoListRecords.Record[];
} catch (e) {
console.error(`Error fetching posts for ${did}:`, e);
return null;
}
};
export { getAllMetadataFromPds, getNextPosts, Post };
export type { AccountMetadata }; export type { AccountMetadata };

View file

@ -1,9 +1,9 @@
import { mount } from "svelte"; import { mount } from 'svelte'
import "./app.css"; import './app.css'
import App from "./App.svelte"; import App from './App.svelte'
const app = mount(App, { const app = mount(App, {
target: document.getElementById("app")!, target: document.getElementById('app')!,
}); })
export default app; export default app

View file

@ -1,7 +1,7 @@
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
export default { export default {
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess // Consult https://svelte.dev/docs#compile-time-svelte-preprocess
// for more information about preprocessors // for more information about preprocessors
preprocess: vitePreprocess(), preprocess: vitePreprocess(),
}; }

View file

@ -1,7 +1,7 @@
import { defineConfig } from "vite"; import { defineConfig } from 'vite'
import { svelte } from "@sveltejs/vite-plugin-svelte"; import { svelte } from '@sveltejs/vite-plugin-svelte'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [svelte()], plugins: [svelte()],
}); })