Compare commits

..

2 commits

21 changed files with 542 additions and 1817 deletions

View file

@ -6,26 +6,15 @@ on:
- main
- astra/ci
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
steps:
- name: Checkout main repo
- name: Checkout repo
uses: actions/checkout@v4
- name: Checkout overrides repo
uses: actions/checkout@v4
with:
repository: scientific-witchery/pds-dash-overrides
token: ${{ secrets.OVERRIDES_TOKEN}}
path: overrides
- name: Copy config file to root
run: cp overrides/config.ts ./config.ts
- name: Setup Node.js
uses: actions/setup-node@v3
with:

3
.gitignore vendored
View file

@ -150,6 +150,3 @@ dist
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# Config files
config.ts

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,59 +1,3 @@
# pds-dash
a frontend dashboard with stats for your ATProto PDS.
## setup
### prerequisites
- [deno](https://deno.com/manual/getting_started/installation)
### installing
clone the repo, copy `config.ts.example` to `config.ts` and edit it to your liking.
then, 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
themes are located in the `themes/` directory, you can create your own theme by copying one of the existing themes and modifying it to your liking.
currently, the name of the theme is determined by the directory name, and the theme itself is defined in `theme.css` inside that directory.
you can switch themes by changing the `theme` property in `config.ts`.
the favicon is located at [`public/favicon.ico`](public/favicon.ico)
## license
MIT
Frontend with stats for your ATProto PDS

28
config.ts Normal file
View file

@ -0,0 +1,28 @@
/**
* Configuration module for the PDS Dashboard
*/
export class Config {
/**
* The base URL of the PDS (Personal Data Server)
* @default "https://pds.witchcraft.systems"
*/
static readonly PDS_URL: string = "https://pds.witchcraft.systems";
/**
* The base URL of the frontend service for linking to replies
* @default "https://deer.social"
*/
static readonly FRONTEND_URL: string = "https://deer.social";
/**
* Maximum number of posts to show in the feed (across all users)
* @default 100
*/
static readonly MAX_POSTS: number = 100;
/**
* 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>";
}

View file

@ -1,44 +0,0 @@
/**
* Configuration module for the PDS Dashboard
*/
export class Config {
/**
* The base URL of the PDS (Personal Data Server).
* @default none
*/
static readonly PDS_URL: string = "";
/**
* Theme to be used
* @default "default"
*/
static readonly THEME: string = "default";
/**
* The base URL of the frontend service for linking to replies/quotes/accounts etc.
* @default "https://deer.social" // or https://bsky.app if you're boring
*/
static readonly FRONTEND_URL: string = "https://deer.social";
/**
* Maximum number of posts to fetch from the PDS per request
* Should be around 20 for about 10 users on the pds
* The more users you have, the lower the number should be
* since sorting is slow and is done on the frontend
* @default 20
*/
static readonly MAX_POSTS: number = 20;
/**
* Footer text for the dashboard, you probably want to change this. Supports HTML.
* @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>)"
*/
static readonly FOOTER_TEXT: string =
"<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>)";
/**
* Whether to show the posts with timestamps 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": {
"npm:@atcute/bluesky@^2.0.2": "2.0.2_@atcute+client@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:@tsconfig/svelte@^5.0.4": "5.0.4",
"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-infinite-loading@^1.4.0": "1.4.0",
"npm:svelte@^5.23.1": "5.28.1_acorn@8.14.1",
"npm:typescript@~5.7.2": "5.7.3",
"npm:vite@^6.3.1": "6.3.2_picomatch@4.0.2"
@ -55,129 +53,79 @@
"integrity": "sha512-GEhUCk9c4XbNxi+0YZHZsV4fYNd6HejfWuN4Ti4c02DauX+LyX5WY1Y3WfyZ8Pxxl0zqhs+MLtW98cMh86vv6g=="
},
"@esbuild/aix-ppc64@0.25.2": {
"integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==",
"os": ["aix"],
"cpu": ["ppc64"]
"integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag=="
},
"@esbuild/android-arm64@0.25.2": {
"integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==",
"os": ["android"],
"cpu": ["arm64"]
"integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w=="
},
"@esbuild/android-arm@0.25.2": {
"integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==",
"os": ["android"],
"cpu": ["arm"]
"integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA=="
},
"@esbuild/android-x64@0.25.2": {
"integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==",
"os": ["android"],
"cpu": ["x64"]
"integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg=="
},
"@esbuild/darwin-arm64@0.25.2": {
"integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==",
"os": ["darwin"],
"cpu": ["arm64"]
"integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA=="
},
"@esbuild/darwin-x64@0.25.2": {
"integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==",
"os": ["darwin"],
"cpu": ["x64"]
"integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA=="
},
"@esbuild/freebsd-arm64@0.25.2": {
"integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==",
"os": ["freebsd"],
"cpu": ["arm64"]
"integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w=="
},
"@esbuild/freebsd-x64@0.25.2": {
"integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==",
"os": ["freebsd"],
"cpu": ["x64"]
"integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ=="
},
"@esbuild/linux-arm64@0.25.2": {
"integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==",
"os": ["linux"],
"cpu": ["arm64"]
"integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g=="
},
"@esbuild/linux-arm@0.25.2": {
"integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==",
"os": ["linux"],
"cpu": ["arm"]
"integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g=="
},
"@esbuild/linux-ia32@0.25.2": {
"integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==",
"os": ["linux"],
"cpu": ["ia32"]
"integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ=="
},
"@esbuild/linux-loong64@0.25.2": {
"integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==",
"os": ["linux"],
"cpu": ["loong64"]
"integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w=="
},
"@esbuild/linux-mips64el@0.25.2": {
"integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==",
"os": ["linux"],
"cpu": ["mips64el"]
"integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q=="
},
"@esbuild/linux-ppc64@0.25.2": {
"integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==",
"os": ["linux"],
"cpu": ["ppc64"]
"integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g=="
},
"@esbuild/linux-riscv64@0.25.2": {
"integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==",
"os": ["linux"],
"cpu": ["riscv64"]
"integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw=="
},
"@esbuild/linux-s390x@0.25.2": {
"integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==",
"os": ["linux"],
"cpu": ["s390x"]
"integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q=="
},
"@esbuild/linux-x64@0.25.2": {
"integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==",
"os": ["linux"],
"cpu": ["x64"]
"integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg=="
},
"@esbuild/netbsd-arm64@0.25.2": {
"integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==",
"os": ["netbsd"],
"cpu": ["arm64"]
"integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw=="
},
"@esbuild/netbsd-x64@0.25.2": {
"integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==",
"os": ["netbsd"],
"cpu": ["x64"]
"integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg=="
},
"@esbuild/openbsd-arm64@0.25.2": {
"integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==",
"os": ["openbsd"],
"cpu": ["arm64"]
"integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg=="
},
"@esbuild/openbsd-x64@0.25.2": {
"integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==",
"os": ["openbsd"],
"cpu": ["x64"]
"integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw=="
},
"@esbuild/sunos-x64@0.25.2": {
"integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==",
"os": ["sunos"],
"cpu": ["x64"]
"integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA=="
},
"@esbuild/win32-arm64@0.25.2": {
"integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==",
"os": ["win32"],
"cpu": ["arm64"]
"integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q=="
},
"@esbuild/win32-ia32@0.25.2": {
"integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==",
"os": ["win32"],
"cpu": ["ia32"]
"integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg=="
},
"@esbuild/win32-x64@0.25.2": {
"integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==",
"os": ["win32"],
"cpu": ["x64"]
"integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA=="
},
"@jridgewell/gen-mapping@0.3.8": {
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
@ -204,104 +152,64 @@
]
},
"@rollup/rollup-android-arm-eabi@4.40.0": {
"integrity": "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==",
"os": ["android"],
"cpu": ["arm"]
"integrity": "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg=="
},
"@rollup/rollup-android-arm64@4.40.0": {
"integrity": "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==",
"os": ["android"],
"cpu": ["arm64"]
"integrity": "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w=="
},
"@rollup/rollup-darwin-arm64@4.40.0": {
"integrity": "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==",
"os": ["darwin"],
"cpu": ["arm64"]
"integrity": "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ=="
},
"@rollup/rollup-darwin-x64@4.40.0": {
"integrity": "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==",
"os": ["darwin"],
"cpu": ["x64"]
"integrity": "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA=="
},
"@rollup/rollup-freebsd-arm64@4.40.0": {
"integrity": "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==",
"os": ["freebsd"],
"cpu": ["arm64"]
"integrity": "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg=="
},
"@rollup/rollup-freebsd-x64@4.40.0": {
"integrity": "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==",
"os": ["freebsd"],
"cpu": ["x64"]
"integrity": "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw=="
},
"@rollup/rollup-linux-arm-gnueabihf@4.40.0": {
"integrity": "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==",
"os": ["linux"],
"cpu": ["arm"]
"integrity": "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA=="
},
"@rollup/rollup-linux-arm-musleabihf@4.40.0": {
"integrity": "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==",
"os": ["linux"],
"cpu": ["arm"]
"integrity": "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg=="
},
"@rollup/rollup-linux-arm64-gnu@4.40.0": {
"integrity": "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==",
"os": ["linux"],
"cpu": ["arm64"]
"integrity": "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg=="
},
"@rollup/rollup-linux-arm64-musl@4.40.0": {
"integrity": "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==",
"os": ["linux"],
"cpu": ["arm64"]
"integrity": "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ=="
},
"@rollup/rollup-linux-loongarch64-gnu@4.40.0": {
"integrity": "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==",
"os": ["linux"],
"cpu": ["loong64"]
"integrity": "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg=="
},
"@rollup/rollup-linux-powerpc64le-gnu@4.40.0": {
"integrity": "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==",
"os": ["linux"],
"cpu": ["ppc64"]
"integrity": "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw=="
},
"@rollup/rollup-linux-riscv64-gnu@4.40.0": {
"integrity": "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==",
"os": ["linux"],
"cpu": ["riscv64"]
"integrity": "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA=="
},
"@rollup/rollup-linux-riscv64-musl@4.40.0": {
"integrity": "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==",
"os": ["linux"],
"cpu": ["riscv64"]
"integrity": "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ=="
},
"@rollup/rollup-linux-s390x-gnu@4.40.0": {
"integrity": "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==",
"os": ["linux"],
"cpu": ["s390x"]
"integrity": "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw=="
},
"@rollup/rollup-linux-x64-gnu@4.40.0": {
"integrity": "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==",
"os": ["linux"],
"cpu": ["x64"]
"integrity": "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ=="
},
"@rollup/rollup-linux-x64-musl@4.40.0": {
"integrity": "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==",
"os": ["linux"],
"cpu": ["x64"]
"integrity": "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw=="
},
"@rollup/rollup-win32-arm64-msvc@4.40.0": {
"integrity": "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==",
"os": ["win32"],
"cpu": ["arm64"]
"integrity": "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ=="
},
"@rollup/rollup-win32-ia32-msvc@4.40.0": {
"integrity": "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==",
"os": ["win32"],
"cpu": ["ia32"]
"integrity": "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA=="
},
"@rollup/rollup-win32-x64-msvc@4.40.0": {
"integrity": "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==",
"os": ["win32"],
"cpu": ["x64"]
"integrity": "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ=="
},
"@sveltejs/acorn-typescript@1.0.5_acorn@8.14.1": {
"integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==",
@ -338,8 +246,7 @@
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="
},
"acorn@8.14.1": {
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
"bin": true
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="
},
"aria-query@5.3.2": {
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="
@ -367,7 +274,7 @@
},
"esbuild@0.25.2": {
"integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==",
"optionalDependencies": [
"dependencies": [
"@esbuild/aix-ppc64",
"@esbuild/android-arm",
"@esbuild/android-arm64",
@ -393,9 +300,7 @@
"@esbuild/win32-arm64",
"@esbuild/win32-ia32",
"@esbuild/win32-x64"
],
"scripts": true,
"bin": true
]
},
"esm-env@1.2.2": {
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="
@ -410,15 +315,10 @@
"integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
"dependencies": [
"picomatch"
],
"optionalPeers": [
"picomatch"
]
},
"fsevents@2.3.3": {
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"os": ["darwin"],
"scripts": true
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="
},
"is-reference@3.0.3": {
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
@ -447,12 +347,8 @@
"ms@2.1.3": {
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"mutex-ts@1.2.1": {
"integrity": "sha512-OkcXgf0viuCgYdnm48kiNQ9PzC5OzISQ261svHr/Ybc2vBYC/5xfLXn44hQ+dYRX74v7MCSqV/LKPEbpYdDybw=="
},
"nanoid@3.3.11": {
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"bin": true
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="
},
"picocolors@1.1.1": {
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
@ -474,9 +370,6 @@
"rollup@4.40.0": {
"integrity": "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==",
"dependencies": [
"@types/estree"
],
"optionalDependencies": [
"@rollup/rollup-android-arm-eabi",
"@rollup/rollup-android-arm64",
"@rollup/rollup-darwin-arm64",
@ -497,9 +390,9 @@
"@rollup/rollup-win32-arm64-msvc",
"@rollup/rollup-win32-ia32-msvc",
"@rollup/rollup-win32-x64-msvc",
"@types/estree",
"fsevents"
],
"bin": true
]
},
"sade@1.8.1": {
"integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
@ -520,11 +413,7 @@
"sade",
"svelte",
"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": {
"integrity": "sha512-iOa9WmfNG95lSOSJdMhdjJ4Afok7IRAQYXpbnxhd5EINnXseG0GVa9j6WPght4eX78XfFez45Fi+uRglGKPV/Q==",
@ -553,31 +442,24 @@
]
},
"typescript@5.7.3": {
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
"bin": true
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="
},
"vite@6.3.2_picomatch@4.0.2": {
"integrity": "sha512-ZSvGOXKGceizRQIZSz7TGJ0pS3QLlVY/9hwxVh17W3re67je1RKYzFHivZ/t0tubU78Vkyb9WnHPENSBCzbckg==",
"dependencies": [
"esbuild",
"fdir",
"fsevents",
"picomatch",
"postcss",
"rollup",
"tinyglobby"
],
"optionalDependencies": [
"fsevents"
],
"bin": true
]
},
"vitefu@1.0.6_vite@6.3.2__picomatch@4.0.2": {
"integrity": "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA==",
"dependencies": [
"vite"
],
"optionalPeers": [
"vite"
]
},
"zimmerframe@1.1.2": {
@ -593,9 +475,7 @@
"npm:@sveltejs/vite-plugin-svelte@^5.0.3",
"npm:@tsconfig/svelte@^5.0.4",
"npm:moment@^2.30.1",
"npm:mutex-ts@^1.2.1",
"npm:svelte-check@^4.1.5",
"npm:svelte-infinite-loading@^1.4.0",
"npm:svelte@^5.23.1",
"npm:typescript@~5.7.2",
"npm:vite@^6.3.1"

View file

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

View file

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

View file

@ -1,56 +1,10 @@
<script lang="ts">
import PostComponent from "./lib/PostComponent.svelte";
import AccountComponent from "./lib/AccountComponent.svelte";
import InfiniteLoading from "svelte-infinite-loading";
import { getNextPosts, Post, getAllMetadataFromPds } from "./lib/pdsfetch";
import { fetchAllPosts, Post, getAllMetadataFromPds } from "./lib/pdsfetch";
import { Config } from "../config";
const postsPromise = fetchAllPosts();
const accountsPromise = getAllMetadataFromPds();
import { onMount } from "svelte";
let posts: Post[] = [];
let hue: number = 1;
const cycleColors = async () => {
while (true) {
hue += 1;
if (hue > 360) {
hue = 0;
}
document.documentElement.style.setProperty("--primary-h", hue.toString());
await new Promise((resolve) => setTimeout(resolve, 10));
}
}
let clickCounter = 0;
const carameldansenfusion = async () => {
clickCounter++;
if (clickCounter >= 10) {
clickCounter = 0;
cycleColors();
}
};
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>
<main>
@ -59,7 +13,7 @@
<p>Loading...</p>
{:then accountsData}
<div id="Account">
<h1 onclick={carameldansenfusion} id="Header">ATProto PDS</h1>
<h1 id="Header">ATProto PDS</h1>
<p>Home to {accountsData.length} accounts</p>
<div id="accountsList">
{#each accountsData as accountObject}
@ -72,17 +26,106 @@
<p>Error: {error.message}</p>
{/await}
{#await postsPromise}
<p>Loading...</p>
{:then postsData}
<div id="Feed">
<div id="spacer"></div>
{#each posts as postObject}
{#each postsData as postObject}
<PostComponent post={postObject as Post} />
{/each}
<InfiniteLoading on:infinite={onInfinite} distance={3000} />
<div id="spacer"></div>
</div>
{/await}
</div>
</main>
<style>
/* desktop style */
#Content {
display: flex;
/* split the screen in half, left for accounts, right for posts */
width: 100%;
height: 100%;
flex-direction: row;
justify-content: space-between;
align-items: center;
background-color: var(--background-color);
color: var(--text-color);
}
#Feed {
width: 65%;
height: 100vh;
overflow-y: scroll;
padding: 20px;
padding-bottom: 0;
padding-top: 0;
margin-top: 0;
margin-bottom: 0;
}
#spacer {
padding: 0;
margin: 0;
height: 10vh;
width: 100%;
}
#Account {
width: 35%;
display: flex;
flex-direction: column;
border: 1px solid var(--border-color);
background-color: var(--content-background-color);
height: 80vh;
padding: 20px;
margin-left: 20px;
}
#accountsList {
display: flex;
flex-direction: column;
overflow-y: scroll;
height: 100%;
width: 100%;
padding: 0px;
margin: 0px;
}
#Header {
text-align: center;
font-size: 2em;
margin-bottom: 20px;
}
/* mobile style */
@media screen and (max-width: 600px) {
#Content {
flex-direction: column;
width: auto;
padding-left: 0px;
padding-right: 0px;
margin-top: 5%;
}
#Account {
width: auto;
padding-left: 5%;
padding-right: 5%;
margin-bottom: 20px;
margin-left: 5%;
margin-right: 5%;
height: auto;
}
#Feed {
width: 95%;
margin: 0px;
margin-left: 10%;
margin-right: 10%;
padding: 0px;
height: auto;
}
#spacer {
height: 0;
}
}
</style>

View file

@ -1,4 +1,83 @@
@import url('./themes/colors.css');
body {
background-color: red;
@font-face {
font-family: 'ProggyClean';
src: url(https://witchcraft.systems/ProggyCleanNerdFont-Regular.ttf);
}
:root {
--link-color: #646cff;
--link-hover-color: #535bf2;
--background-color: #12082b;
--header-background-color: #1f1145;
--content-background-color: #0d0620;
--text-color: white;
--border-color: #8054f0;
--indicator-inactive-color: #4a4a4a;
--indicator-active-color: #8054f0;
}
::-webkit-scrollbar {
width: 0px;
background: transparent;
padding: 0;
margin: 0;
}
::-webkit-scrollbar-thumb {
background: transparent;
border-radius: 0;
}
::-webkit-scrollbar-track {
background: transparent;
border-radius: 0;
}
::-webkit-scrollbar-corner {
background: transparent;
border-radius: 0;
}
::-webkit-scrollbar-button {
background: transparent;
border-radius: 0;
}
* {
scrollbar-width: none;
scrollbar-color: transparent transparent;
-ms-overflow-style: none; /* IE and Edge */
-webkit-overflow-scrolling: touch;
-webkit-scrollbar: none; /* Safari */
}
a {
font-weight: 500;
color: var(--link-color);
text-decoration: inherit;
}
a:hover {
color: var(--link-hover-color);
text-decoration: underline;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
background-color: var(--background-color);
font-family: 'ProggyClean', monospace;
font-size: 24px;
color: var(--text-color);
border-color: var(--border-color);
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
#app {
max-width: 1400px;
margin: 0;
padding: 0;
margin-left: auto;
margin-right: auto;
text-align: center;
}

View file

@ -12,17 +12,37 @@
alt="avatar of {account.displayName}"
src="{Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did={account.did}&cid={account.avatarCid}"
/>
{/if}
<div id="accountName">
{account.displayName || account.handle || account.did}
</div>
{:else}
<div id="accountName" class="no-avatar">
{account.displayName || account.handle || account.did}
</div>
{/if}
</div>
</a>
<style>
#accountContainer {
display: flex;
text-align: start;
align-items: center;
background-color: var(--background-color);
padding: 0px;
margin-bottom: 15px;
border: 1px solid var(--border-color);
}
#accountName {
margin-left: 10px;
font-size: 0.9em;
/* replace overflow with ellipsis */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 80%;
}
#avatar {
width: 50px;
height: 50px;
margin: 0px;
border-right: var(--border-color) 1px solid;
}
</style>

View file

@ -71,7 +71,7 @@
>
<p id="handle">
<a href="{Config.FRONTEND_URL}/profile/{post.authorHandle}"
>@{post.authorHandle}</a
>{post.authorHandle}</a
>
<a
@ -113,7 +113,7 @@
<div id="carouselControls">
<button
id="prevBtn"
onclick={prevImage}
on:click={prevImage}
disabled={currentImageIndex === 0}>←</button
>
<div id="carouselIndicators">
@ -125,7 +125,7 @@
</div>
<button
id="nextBtn"
onclick={nextImage}
on:click={nextImage}
disabled={currentImageIndex === post.imagesCid.length - 1}
>→</button
>
@ -141,16 +141,150 @@
controls
></video>
{/if}
{#if post.gifLink}
<img
id="embedVideo"
src="{post.gifLink}"
alt="Post GIF"
/>
{/if}
</div>
</div>
<style>
a:hover {
text-decoration: underline;
}
#postContainer {
display: flex;
flex-direction: column;
border: 1px solid var(--border-color);
background-color: var(--background-color);
margin-bottom: 15px;
overflow-wrap: break-word;
}
#postHeader {
display: flex;
flex-direction: row;
align-items: center;
justify-content: start;
background-color: var(--header-background-color);
padding: 0px 0px;
height: fit-content;
border-bottom: 1px solid var(--border-color);
font-weight: bold;
overflow-wrap: break-word;
height: 60px;
}
#displayName {
color: var(--text-color);
font-size: 1.2em;
padding: 0;
margin: 0;
}
#handle {
color: var(--border-color);
font-size: 0.8em;
padding: 0;
margin: 0;
}
#postLink {
color: var(--border-color);
font-size: 0.8em;
padding: 0;
margin: 0;
}
#postContent {
display: flex;
text-align: start;
flex-direction: column;
padding: 10px;
background-color: var(--content-background-color);
color: var(--text-color);
overflow-wrap: break-word;
}
#replyingText {
font-size: 0.7em;
margin: 0;
padding: 0;
padding-bottom: 5px;
}
#quotingText {
font-size: 0.7em;
margin: 0;
padding: 0;
padding-bottom: 5px;
}
#postText {
margin: 0;
padding: 0;
}
#headerText {
margin-left: 10px;
font-size: 0.9em;
text-align: start;
overflow-wrap: break-word;
overflow: hidden;
}
#avatar {
height: 100%;
margin: 0px;
margin-left: 0px;
border-right: var(--border-color) 1px solid;
}
#carouselContainer {
position: relative;
width: 100%;
margin-top: 10px;
display: flex;
flex-direction: column;
align-items: center;
}
#carouselControls {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
max-width: 500px;
margin-top: 5px;
}
#carouselIndicators {
display: flex;
gap: 5px;
}
.indicator {
width: 8px;
height: 8px;
background-color: var(--indicator-inactive-color);
}
.indicator.active {
background-color: var(--indicator-active-color);
}
#prevBtn,
#nextBtn {
background-color: rgba(31, 17, 69, 0.7);
color: var(--text-color);
border: 1px solid var(--border-color);
width: 30px;
height: 30px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
#prevBtn:disabled,
#nextBtn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
#embedVideo {
width: 100%;
max-width: 500px;
margin-top: 10px;
align-self: center;
}
#embedImages {
min-width: min(100%, 500px);
max-width: min(100%, 500px);
max-height: 500px;
object-fit: contain;
margin: 0;
}
</style>

View file

@ -13,21 +13,16 @@ import {
WebDidDocumentResolver,
} from "@atcute/identity-resolver";
import { Config } from "../../config";
import { Mutex } from "mutex-ts"
// import { ComAtprotoRepoListRecords.Record } from "@atcute/client/lexicons";
// import { AppBskyFeedPost } from "@atcute/client/lexicons";
// import { AppBskyActorDefs } from "@atcute/client/lexicons";
interface AccountMetadata {
did: At.Did;
did: string;
displayName: string;
handle: string;
avatarCid: string | null;
currentCursor?: string;
}
let accountsMetadata: AccountMetadata[] = [];
interface atUriObject {
repo: string;
collection: string;
@ -47,7 +42,6 @@ class Post {
replyingUri: atUriObject | null;
imagesCid: string[] | null;
videosLinkCid: string | null;
gifLink: string | null;
constructor(
record: ComAtprotoRepoListRecords.Record,
@ -71,11 +65,10 @@ class Post {
this.quotingUri = null;
this.imagesCid = null;
this.videosLinkCid = null;
this.gifLink = null;
switch (post.embed?.$type) {
case "app.bsky.embed.images":
this.imagesCid = post.embed.images.map(
(imageRecord: any) => imageRecord.image.ref.$link,
this.imagesCid = post.embed.images.map((imageRecord: any) =>
imageRecord.image.ref.$link
);
break;
case "app.bsky.embed.video":
@ -88,8 +81,8 @@ class Post {
this.quotingUri = processAtUri(post.embed.record.record.uri);
switch (post.embed.media.$type) {
case "app.bsky.embed.images":
this.imagesCid = post.embed.media.images.map(
(imageRecord) => imageRecord.image.ref.$link,
this.imagesCid = post.embed.media.images.map((imageRecord) =>
imageRecord.image.ref.$link
);
break;
@ -99,11 +92,6 @@ class Post {
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;
}
}
}
@ -127,18 +115,10 @@ const getDidsFromPDS = async (): Promise<At.Did[]> => {
const { data } = await rpc.get("com.atproto.sync.listRepos", {
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 (
did: `did:${string}:${string}`,
) => {
const account: AccountMetadata = {
did: did,
handle: "", // Guaranteed to be filled out later
displayName: "",
avatarCid: null,
};
const getAccountMetadata = async (did: `did:${string}:${string}`) : Promise<AccountMetadata> => {
// gonna assume self exists in the app.bsky.actor.profile
try {
const { data } = await rpc.get("com.atproto.repo.getRecord", {
params: {
@ -148,22 +128,27 @@ const getAccountMetadata = async (
},
});
const value = data.value as AppBskyActorProfile.Record;
account.displayName = value.displayName || "";
const handle = await blueskyHandleFromDid(did);
const account: AccountMetadata = {
did: did,
handle: handle,
displayName: value.displayName || "",
avatarCid: null,
};
if (value.avatar) {
account.avatarCid = value.avatar.ref["$link"];
}
} catch (e) {
console.warn(`Error fetching profile for ${did}:`, e);
}
try {
account.handle = await blueskyHandleFromDid(did);
} catch (e) {
console.error(`Error fetching handle for ${did}:`, e);
return null;
}
return account;
}
catch (e) {
console.error(`Error fetching metadata for ${did}:`, e);
return {
did: "error",
displayName: "",
avatarCid: null,
handle: "error",
};
}
};
const getAllMetadataFromPds = async () : Promise<AccountMetadata[]> => {
@ -173,7 +158,31 @@ const getAllMetadataFromPds = async (): Promise<AccountMetadata[]> => {
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) => {
@ -209,151 +218,27 @@ const blueskyHandleFromDid = async (did: At.Did) => {
}
};
interface PostsAcc {
posts: ComAtprotoRepoListRecords.Record[];
account: AccountMetadata;
}
const getCutoffDate = (postAccounts: PostsAcc[]) => {
const now = Date.now();
let cutoffDate: Date | null = null;
postAccounts.forEach((postAcc) => {
const latestPost = new Date(
(postAcc.posts[postAcc.posts.length - 1].value as AppBskyFeedPost.Record)
.createdAt,
const fetchAllPosts = async () => {
const users: AccountMetadata[] = await getAllMetadataFromPds();
const postRecords = await Promise.all(
users.map(async (metadata: AccountMetadata) =>
await fetchPosts(metadata.did)
),
);
if (!cutoffDate) {
cutoffDate = latestPost;
} else {
if (latestPost > cutoffDate) {
cutoffDate = latestPost;
}
}
});
if (cutoffDate) {
return cutoffDate;
} else {
return new Date(now);
const validPostRecords = postRecords.filter(record => !record.error);
const posts: Post[] = validPostRecords.flatMap((userFetch) =>
userFetch.records.map((record) => {
const user = users.find((user: AccountMetadata) =>
user.did == userFetch.did
);
if (!user) {
throw new Error(`User with DID ${userFetch.did} not found`);
}
return new Post(record, user);
})
);
posts.sort((a, b) => b.timestamp - a.timestamp);
return posts.slice(0, Config.MAX_POSTS);
};
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) =>
postAcc.posts.length > 0
);
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;
};
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 { fetchAllPosts, getAllMetadataFromPds, Post };
export type { AccountMetadata };

View file

@ -1,9 +1,9 @@
import { mount } from "svelte";
import "./app.css";
import App from "./App.svelte";
import { mount } from 'svelte'
import './app.css'
import App from './App.svelte'
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 {
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
// for more information about preprocessors
preprocess: vitePreprocess(),
};
}

View file

@ -1,423 +0,0 @@
/* Modern Theme for pds-dash */
:root {
/* Modern color palette */
--primary-h: 243;
--link-color: hsl(var(--primary-h), 73%, 59%);
--link-hover-color: #4338ca;
--time-color: #8b5cf6;
--background-color: #f8fafc;
--header-background-color: #ffffff;
--content-background-color: #ffffff;
--text-color: #111827;
--text-secondary-color: #4b5563;
--border-color: #e2e8f0;
--indicator-inactive-color: #cbd5e1;
--indicator-active-color: #6366f1;
/* Modern shadows */
--button-hover: #f3f4f6;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
background-color: var(--background-color);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
font-size: 18px;
line-height: 1.5;
color: var(--text-color);
border-color: var(--border-color);
overflow-wrap: break-word;
word-break: break-word;
hyphens: none;
}
a {
font-weight: 500;
color: var(--link-color);
text-decoration: none;
transition: color 0.15s ease;
}
a:hover {
color: var(--link-hover-color);
}
h1 {
font-size: 2.5em;
line-height: 1.2;
font-weight: 700;
}
#app {
max-width: 1400px;
width: 100%;
margin: 0 auto;
padding: 0;
text-align: center;
}
/* Post Component */
#postContainer {
display: flex;
flex-direction: column;
border-radius: 12px;
border: 1px solid var(--border-color);
background-color: var(--content-background-color);
margin-bottom: 20px;
overflow-wrap: break-word;
overflow: hidden;
box-shadow: var(--card-shadow);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
#postContainer:hover {
transform: translateY(-2px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
#postHeader {
display: flex;
flex-direction: row;
align-items: center;
justify-content: start;
background-color: var(--header-background-color);
padding: 12px 16px;
height: 60px;
border-bottom: 1px solid var(--border-color);
font-weight: 600;
overflow-wrap: break-word;
}
#displayName {
display: block;
color: var(--text-color);
font-size: 1.1em;
padding: 0;
margin: 0 0 2px 0;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
width: 100%;
letter-spacing: -0.01em;
}
#handle {
display: flex;
align-items: center;
color: #6b7280;
font-size: 0.85em;
font-weight: 400;
padding: 0;
margin: 0;
gap: 8px;
}
#postLink {
color: var(--time-color);
font-size: 0.85em;
padding: 0;
margin: 0;
opacity: 0.9;
}
#postContent {
display: flex;
text-align: start;
flex-direction: column;
padding: 16px;
background-color: var(--content-background-color);
color: var(--text-color);
overflow-wrap: break-word;
white-space: pre-line;
line-height: 1.6;
}
#replyingText, #quotingText {
font-size: 0.8em;
margin: 0;
padding: 0 0 10px 0;
color: #6b7280;
}
#postText {
margin: 0 0 8px 0;
padding: 0;
overflow-wrap: break-word;
word-break: break-word;
hyphens: none;
font-size: 1.05em;
}
#headerText {
margin-left: 12px;
font-size: 0.9em;
text-align: start;
word-break: break-word;
max-width: 80%;
max-height: 95%;
overflow: hidden;
align-self: flex-start;
margin-top: auto;
margin-bottom: auto;
}
#carouselContainer {
position: relative;
width: 100%;
margin-top: 12px;
display: flex;
flex-direction: column;
align-items: center;
border-radius: 8px;
overflow: hidden;
}
#carouselControls {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
max-width: 500px;
margin-top: 10px;
}
#carouselIndicators {
display: flex;
gap: 6px;
}
.indicator {
width: 6px;
height: 6px;
background-color: var(--indicator-inactive-color);
border-radius: 50%;
transition: background-color 0.2s ease, transform 0.2s ease;
}
.indicator.active {
background-color: var(--indicator-active-color);
transform: scale(1.3);
}
#prevBtn,
#nextBtn {
background-color: var(--button-bg);
color: var(--text-color);
border: 1px solid var(--border-color);
width: 32px;
height: 32px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: background-color 0.15s ease, transform 0.15s ease;
font-size: 16px;
}
#prevBtn:hover:not(:disabled),
#nextBtn:hover:not(:disabled) {
background-color: var(--button-hover);
transform: scale(1.05);
}
#prevBtn:disabled,
#nextBtn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
#embedVideo {
width: 100%;
max-width: 500px;
margin-top: 12px;
align-self: center;
border-radius: 8px;
overflow: hidden;
}
#embedImages {
min-width: min(100%, 500px);
max-width: min(100%, 500px);
max-height: 500px;
object-fit: contain;
margin: 0;
border-radius: 8px;
}
/* Account Component */
#accountContainer {
display: flex;
text-align: start;
align-items: center;
background-color: var(--content-background-color);
padding: 12px;
margin-bottom: 15px;
border: 1px solid var(--border-color);
border-radius: 12px;
transition: background-color 0.15s ease;
}
#accountContainer:hover {
background-color: var(--hover-bg);
}
#accountName {
margin-left: 12px;
font-size: 0.95em;
max-width: 80%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 500;
}
#avatar {
width: 48px;
height: 48px;
margin: 0;
object-fit: cover;
border-radius: 50%;
border: 2px solid white;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
/* App.Svelte Layout */
#Content {
display: flex;
width: 100%;
height: 100%;
flex-direction: row;
justify-content: space-between;
align-items: center;
background-color: var(--background-color);
color: var(--text-color);
gap: 24px;
}
#Feed {
overflow-y: auto;
width: 65%;
height: 100vh;
padding-right: 16px;
align-self: flex-start;
}
#spacer {
padding: 0;
margin: 0;
height: 10vh;
width: 100%;
}
#Account {
width: 35%;
display: flex;
flex-direction: column;
border: 1px solid var(--border-color);
background-color: var(--content-background-color);
max-height: 80vh;
padding: 24px;
margin-left: 16px;
border-radius: 12px;
box-shadow: var(--card-shadow);
}
#accountsList {
display: flex;
flex-direction: column;
overflow-y: auto;
height: 100%;
width: 100%;
padding: 8px 0;
margin: 0;
}
#Header {
text-align: center;
font-size: 1.8em;
margin-bottom: 16px;
font-weight: 700;
background: linear-gradient(to right, var(--link-color), #8b5cf6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Mobile Styles */
@media screen and (max-width: 768px) {
#Content {
flex-direction: column;
width: auto;
padding: 12px;
margin-top: 0;
}
#Account {
width: calc(100% - 32px);
padding: 16px;
margin-bottom: 20px;
margin-left: 0;
margin-right: 0;
height: auto;
order: -1;
}
#Feed {
width: 100%;
margin: 0;
padding: 0;
overflow-y: visible;
}
#spacer {
height: 5vh;
}
body {
font-size: 16px;
}
#postHeader {
padding: 10px;
height: auto;
min-height: 50px;
}
}
/* Scrollbar Styles */
::-webkit-scrollbar {
width: 0px;
background: transparent;
padding: 0;
margin: 0;
}
::-webkit-scrollbar-thumb {
background: transparent;
border-radius: 0;
}
::-webkit-scrollbar-track {
background: transparent;
border-radius: 0;
}
::-webkit-scrollbar-corner {
background: transparent;
border-radius: 0;
}
::-webkit-scrollbar-button {
background: transparent;
border-radius: 0;
}
* {
scrollbar-width: none;
scrollbar-color: transparent transparent;
-ms-overflow-style: none; /* IE and Edge */
-webkit-overflow-scrolling: touch;
-webkit-scrollbar: none; /* Safari */
}

View file

@ -1,375 +0,0 @@
@import url("https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap");
:root {
/* Color overrides, edit to whatever you want */
--primary-h: 341; /* Hue */
--background-color: hsl(var(--primary-h), 62%, 30%);
--text-color: hsl(var(--primary-h), 69%, 18%);
--link-color: hsl(var(--primary-h), 100%, 20%);
--link-hover-color: hsl(var(--primary-h), 20%, 20%);
--border-color: hsl(var(--primary-h), 59%, 52%);
--content-background-color: hsl(var(--primary-h), 97%, 73%);
--header-background-color: hsl(var(--primary-h), 97%, 73%);
--indicator-inactive-color: #4a4a4a;
--indicator-active-color: var(--border-color);
}
a {
font-weight: 500;
color: var(--link-color);
text-decoration: inherit;
}
a:hover {
color: var(--link-hover-color);
text-decoration: underline;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
background-color: var(--background-color);
font-family: "Share Tech Mono", monospace;
font-size: 24px;
color: var(--text-color);
border-color: var(--border-color);
overflow-wrap: break-word;
word-wrap: normal;
word-break: break-word;
hyphens: none;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
#app {
max-width: 1400px;
width: 100%;
margin: 0;
padding: 0;
margin-left: auto;
margin-right: auto;
text-align: center;
}
/* Post Component */
a:hover {
text-decoration: underline;
}
#postContainer {
display: flex;
flex-direction: column;
border: 4px solid var(--border-color);
background-color: var(--background-color);
margin-bottom: 15px;
overflow-wrap: break-word;
box-shadow: var(--border-color) 10px 10px;
}
#postHeader {
display: flex;
flex-direction: row;
align-items: center;
justify-content: start;
background-color: var(--header-background-color);
padding: 0px 0px;
height: fit-content;
font-weight: bold;
overflow-wrap: break-word;
height: 64px;
}
#displayName {
display: block;
color: var(--text-color);
font-size: 1.2em;
padding: 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 {
display: block;
color: var(--border-color);
font-size: 0.8em;
padding: 0;
margin: 0;
}
#postLink {
color: var(--link-hover-color);
font-size: 0.8em;
padding: 0;
margin: 0;
}
#postContent {
display: flex;
text-align: start;
flex-direction: column;
padding: 10px;
background-color: var(--content-background-color);
color: var(--text-color);
overflow-wrap: break-word;
white-space: pre-line;
}
#replyingText {
font-size: 0.7em;
margin: 0;
padding: 0;
padding-bottom: 5px;
}
#quotingText {
font-size: 0.7em;
margin: 0;
padding: 0;
padding-bottom: 5px;
}
#postText {
margin: 0;
padding: 0;
overflow-wrap: break-word;
word-wrap: normal;
word-break: break-word;
hyphens: none;
}
#headerText {
margin-left: 10px;
font-size: 0.9em;
text-align: start;
word-break: break-word;
max-width: 80%;
max-height: 95%;
overflow: hidden;
align-self: flex-start;
margin-top: auto;
margin-bottom: auto;
}
#avatar {
height: 30px;
width: 30px;
overflow: hidden;
object-fit: cover;
}
#postContainer #avatar {
height: 60px;
width: 60px;
border-right: var(--border-color) 4px solid;
border-bottom: var(--border-color) 4px solid;
}
#carouselContainer {
position: relative;
width: 100%;
margin-top: 10px;
display: flex;
flex-direction: column;
align-items: center;
}
#carouselControls {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
max-width: 500px;
margin-top: 5px;
}
#carouselIndicators {
display: flex;
gap: 5px;
}
.indicator {
width: 8px;
height: 8px;
background-color: var(--indicator-inactive-color);
}
.indicator.active {
background-color: var(--indicator-active-color);
}
#prevBtn,
#nextBtn {
background-color: rgba(31, 17, 69, 0.7);
color: var(--text-color);
border: 4px solid var(--border-color);
width: 30px;
height: 30px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
#prevBtn:disabled,
#nextBtn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
#embedVideo {
width: 100%;
max-width: 500px;
margin-top: 10px;
align-self: center;
}
#embedImages {
min-width: min(100%, 500px);
max-width: min(100%, 500px);
max-height: 500px;
object-fit: contain;
margin: 0;
}
/* Account Component */
#accountContainer {
display: flex;
text-align: start;
align-items: center;
background-color: var(--header-background-color);
padding: 0px;
margin-bottom: 15px;
margin-right: 4px;
border: 4px solid var(--border-color);
box-shadow: var(--border-color) 10px 10px;
min-height: 30px;
}
#accountName {
margin-left: 10px;
font-size: 1em;
max-width: 80%;
/* replace overflow with ellipsis */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.no-avatar {
margin-left: 40px !important;
}
/* App.Svelte */
/* desktop style */
#Content {
display: flex;
/* split the screen in half, left for accounts, right for posts */
width: 100%;
height: 100%;
flex-direction: row;
justify-content: space-between;
align-items: center;
background-color: var(--background-color);
color: var(--text-color);
}
#Feed {
overflow-y: scroll;
width: 65%;
height: 100vh;
padding: 20px;
padding-bottom: 0;
padding-top: 0;
margin-top: 0;
margin-bottom: 0;
}
#spacer {
padding: 0;
margin: 0;
height: 10vh;
width: 100%;
}
#Account {
width: 35%;
display: flex;
flex-direction: column;
border: 4px solid var(--border-color);
background-color: var(--content-background-color);
box-shadow: var(--border-color) 10px 10px;
height: 80vh;
padding: 20px;
margin-left: 20px;
}
#accountsList {
display: flex;
flex-direction: column;
overflow-y: scroll;
height: 100%;
width: 100%;
padding: 0px;
margin: 0px;
}
#Header {
text-align: center;
font-size: 2em;
margin-bottom: 20px;
}
/* mobile style */
@media screen and (max-width: 600px) {
#Content {
flex-direction: column;
width: auto;
padding-left: 0px;
padding-right: 0px;
margin-top: 5%;
}
#Account {
width: 85%;
padding-left: 5%;
padding-right: 5%;
margin-bottom: 20px;
margin-left: 5%;
margin-right: 5%;
height: auto;
}
#Feed {
width: 95%;
margin: 0px;
margin-left: 10%;
margin-right: 10%;
padding: 0px;
overflow-y: visible;
}
#spacer {
height: 0;
}
}
::-webkit-scrollbar {
width: 0px;
background: transparent;
padding: 0;
margin: 0;
}
::-webkit-scrollbar-thumb {
background: transparent;
border-radius: 0;
}
::-webkit-scrollbar-track {
background: transparent;
border-radius: 0;
}
::-webkit-scrollbar-corner {
background: transparent;
border-radius: 0;
}
::-webkit-scrollbar-button {
background: transparent;
border-radius: 0;
}
* {
scrollbar-width: none;
scrollbar-color: transparent transparent;
-ms-overflow-style: none; /* IE and Edge */
-webkit-overflow-scrolling: touch;
-webkit-scrollbar: none; /* Safari */
}

View file

@ -1,373 +0,0 @@
@font-face {
font-family: "ProggyClean";
src: url(https://witchcraft.systems/ProggyCleanNerdFont-Regular.ttf);
}
:root {
/* Color overrides, edit to whatever you want */
--primary-h: 260; /* Hue */
--link-color: hsl(calc(var(--primary-h) - 30), 75%, 60%);
--link-hover-color: hsl(calc(var(--primary-h) - 30), 75%, 50%);
--background-color: hsl(var(--primary-h), 75%, 10%);
--header-background-color: hsl(var(--primary-h), 75%, 18%);
--content-background-color: hsl(var(--primary-h), 75%, 8%);
--text-color: #fff;
--border-color: hsl(var(--primary-h), 75%, 60%);
--indicator-inactive-color: #4a4a4a;
--indicator-active-color: var(--border-color);
}
a {
font-weight: 500;
color: var(--link-color);
text-decoration: inherit;
}
a:hover {
color: var(--link-hover-color);
text-decoration: underline;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
background-color: var(--background-color);
font-family: "ProggyClean", monospace;
font-size: 24px;
color: var(--text-color);
border-color: var(--border-color);
overflow-wrap: break-word;
word-wrap: normal;
word-break: break-word;
hyphens: none;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
#app {
max-width: 1400px;
width: 100%;
margin: 0;
padding: 0;
margin-left: auto;
margin-right: auto;
text-align: center;
}
/* Post Component */
a:hover {
text-decoration: underline;
}
#postContainer {
display: flex;
flex-direction: column;
border: 1px solid var(--border-color);
background-color: var(--background-color);
margin-bottom: 15px;
overflow-wrap: break-word;
}
#postHeader {
display: flex;
flex-direction: row;
align-items: center;
justify-content: start;
background-color: var(--header-background-color);
padding: 0px 0px;
height: fit-content;
border-bottom: 1px solid var(--border-color);
font-weight: bold;
overflow-wrap: break-word;
height: 60px;
}
#displayName {
display: block;
color: var(--text-color);
font-size: 1.2em;
padding: 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 {
display: block;
color: var(--border-color);
font-size: 0.8em;
padding: 0;
margin: 0;
}
#postLink {
color: var(--border-color);
font-size: 0.8em;
padding: 0;
margin: 0;
}
#postContent {
display: flex;
text-align: start;
flex-direction: column;
padding: 10px;
background-color: var(--content-background-color);
color: var(--text-color);
overflow-wrap: break-word;
white-space: pre-line;
}
#replyingText {
font-size: 0.7em;
margin: 0;
padding: 0;
padding-bottom: 5px;
}
#quotingText {
font-size: 0.7em;
margin: 0;
padding: 0;
padding-bottom: 5px;
}
#postText {
margin: 0;
padding: 0;
overflow-wrap: break-word;
word-wrap: normal;
word-break: break-word;
hyphens: none;
}
#headerText {
margin-left: 10px;
font-size: 0.9em;
text-align: start;
word-break: break-word;
max-width: 80%;
max-height: 95%;
overflow: hidden;
align-self: flex-start;
margin-top: auto;
margin-bottom: auto;
}
#avatar {
height: 60px;
width: 60px;
margin: 0px;
margin-left: 0px;
overflow: hidden;
object-fit: cover;
border-right: var(--border-color) 1px solid;
}
#carouselContainer {
position: relative;
width: 100%;
margin-top: 10px;
display: flex;
flex-direction: column;
align-items: center;
}
#carouselControls {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
max-width: 500px;
margin-top: 5px;
}
#carouselIndicators {
display: flex;
gap: 5px;
}
.indicator {
width: 8px;
height: 8px;
background-color: var(--indicator-inactive-color);
}
.indicator.active {
background-color: var(--indicator-active-color);
}
#prevBtn,
#nextBtn {
background-color: rgba(31, 17, 69, 0.7);
color: var(--text-color);
border: 1px solid var(--border-color);
width: 30px;
height: 30px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
#prevBtn:disabled,
#nextBtn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
#embedVideo {
width: 100%;
max-width: 500px;
margin-top: 10px;
align-self: center;
}
#embedImages {
min-width: min(100%, 500px);
max-width: min(100%, 500px);
max-height: 500px;
object-fit: contain;
margin: 0;
}
/* Account Component */
#accountContainer {
display: flex;
text-align: start;
align-items: center;
background-color: var(--background-color);
padding: 0px;
margin-bottom: 15px;
border: 1px solid var(--border-color);
min-height: 30px;
}
#accountName {
margin-left: 10px;
font-size: 1em;
max-width: 80%;
/* replace overflow with ellipsis */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.no-avatar {
margin-left: 70px !important;
}
/* App.Svelte */
/* desktop style */
#Content {
display: flex;
/* split the screen in half, left for accounts, right for posts */
width: 100%;
height: 100%;
flex-direction: row;
justify-content: space-between;
align-items: center;
background-color: var(--background-color);
color: var(--text-color);
}
#Feed {
overflow-y: scroll;
width: 65%;
height: 100vh;
padding: 20px;
padding-bottom: 0;
padding-top: 0;
margin-top: 0;
margin-bottom: 0;
}
#spacer {
padding: 0;
margin: 0;
height: 10vh;
width: 100%;
}
#Account {
width: 35%;
display: flex;
flex-direction: column;
border: 1px solid var(--border-color);
background-color: var(--content-background-color);
height: 80vh;
padding: 20px;
margin-left: 20px;
}
#accountsList {
display: flex;
flex-direction: column;
overflow-y: scroll;
height: 100%;
width: 100%;
padding: 0px;
margin: 0px;
}
#Header {
text-align: center;
font-size: 2em;
margin-bottom: 20px;
}
/* mobile style */
@media screen and (max-width: 600px) {
#Content {
flex-direction: column;
width: auto;
padding-left: 0px;
padding-right: 0px;
margin-top: 5%;
}
#Account {
width: 85%;
padding-left: 5%;
padding-right: 5%;
margin-bottom: 20px;
margin-left: 5%;
margin-right: 5%;
height: auto;
}
#Feed {
width: 95%;
margin: 0px;
margin-left: 10%;
margin-right: 10%;
padding: 0px;
overflow-y: visible;
}
#spacer {
height: 0;
}
}
::-webkit-scrollbar {
width: 0px;
background: transparent;
padding: 0;
margin: 0;
}
::-webkit-scrollbar-thumb {
background: transparent;
border-radius: 0;
}
::-webkit-scrollbar-track {
background: transparent;
border-radius: 0;
}
::-webkit-scrollbar-corner {
background: transparent;
border-radius: 0;
}
::-webkit-scrollbar-button {
background: transparent;
border-radius: 0;
}
* {
scrollbar-width: none;
scrollbar-color: transparent transparent;
-ms-overflow-style: none; /* IE and Edge */
-webkit-overflow-scrolling: touch;
-webkit-scrollbar: none; /* Safari */
}

View file

@ -1,32 +0,0 @@
import { Plugin } from 'vite';
import { Config } from './config';
// Replaces app.css with the contents of the file specified in the
// config file.
export const themePlugin = (): Plugin => {
const themeFolder = Config.THEME;
console.log(`Using theme folder: ${themeFolder}`);
return {
name: 'theme-generator',
enforce: 'pre', // Ensure this plugin runs first
transform(code, id) {
if (id.endsWith('app.css')) {
// Read the theme file and replace the contents of app.css with it
// Needs full path to the file
const themeCode = Deno.readTextFileSync(Deno.cwd() + '/themes/' + themeFolder + '/theme.css');
// Replace the contents of app.css with the theme code
// and add a comment at the top
const themeComment = `/* Generated from ${themeFolder} */\n`;
const themeCodeWithComment = themeComment + themeCode;
// Return the theme code as the new contents of app.css
return {
code: themeCodeWithComment,
map: null,
};
}
return null;
}
};
};

View file

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