Compare commits

...
Sign in to create a new pull request.

15 commits

Author SHA1 Message Date
c348ed5d46 Fix users with no profile record not being shown (#7)
All checks were successful
Deploy / Deploy (push) Successful in 24s
Co-authored-by: Casey <hatkidchan@gmail.com>
Co-authored-by: Ari <ariadna@hey.com>
Reviewed-on: #7
Reviewed-by: Ariadna <ariadna@hey.com>
2025-05-30 03:05:05 +00:00
273741d221
Update the info on theming in README.md
All checks were successful
Deploy / Deploy (push) Successful in 28s
2025-05-29 20:19:25 +09:00
Ari
c77cdb4b79 Custom themes and config overrides (#9)
All checks were successful
Deploy / Deploy (push) Successful in 31s
Co-authored-by: Astra <me@astrra.space>
Reviewed-on: #9
Reviewed-by: Astra <me@astrra.space>
Co-authored-by: Ari <ariadna@hey.com>
Co-committed-by: Ari <ariadna@hey.com>
2025-05-29 08:38:03 +00:00
67af67ef49 Merge pull request 'Added mutex to fix duplicated posts' (#6) from ari/mutex-multithreading-modification into main
All checks were successful
Deploy / Deploy (push) Successful in 40s
Reviewed-on: #6
2025-05-10 18:14:10 +00:00
Ari
b3e8c7eac1 Added mutex to fix duplicated posts 2025-05-10 14:07:37 -04:00
c84ff140ca
GIF fix for safari
All checks were successful
Deploy / Deploy (push) Successful in 22s
2025-05-11 00:56:39 +09:00
c3dfb2e48d GIF support (#5)
All checks were successful
Deploy / Deploy (push) Successful in 28s
Reviewed-on: #5
2025-05-10 15:52:44 +00:00
79d5694dd1
Proper display name wrapping
All checks were successful
Deploy / Deploy (push) Successful in 20s
2025-05-10 20:00:20 +09:00
abeaa86096
Actual infinite scrolling fix
All checks were successful
Deploy / Deploy (push) Successful in 33s
2025-05-10 18:40:38 +09:00
a495f724b9 CSS text overflow and infinite loading fixes (#4)
All checks were successful
Deploy / Deploy (push) Successful in 41s
Reviewed-on: #4
2025-05-10 09:14:50 +00:00
614f2b4c30
Add links to the source code
All checks were successful
Deploy / Deploy (push) Successful in 39s
2025-04-26 14:46:30 +09:00
72ba577950 Proper post word wrapping (#3)
All checks were successful
Deploy / Deploy (push) Successful in 19s
Co-authored-by: ari <ariadna@omg.lol>
Reviewed-on: #3
2025-04-23 04:10:14 +00:00
ari
3d38e0f68c Update README.md
All checks were successful
Deploy / Deploy (push) Successful in 36s
2025-04-22 05:27:05 +00:00
5eca07724e Distance tweaks for hidden loads
All checks were successful
Deploy / Deploy (push) Successful in 28s
2025-04-22 03:53:05 +00:00
ari
cff9eed1a4 Dynamic post loading (#2)
All checks were successful
Deploy / Deploy (push) Successful in 33s
Dynamically load the posts so that you can scroll a chronologically sorted timeline infinitely

Reviewed-on: #2
Co-authored-by: ari <ariadna@omg.lol>
Co-committed-by: ari <ariadna@omg.lol>
2025-04-22 03:14:37 +00:00
17 changed files with 1713 additions and 532 deletions

View file

@ -6,15 +6,26 @@ on:
- main
- astra/ci
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
steps:
- name: Checkout repo
- name: Checkout main 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:

5
.gitignore vendored
View file

@ -149,4 +149,7 @@ dist
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
.pnp.*
# Config files
config.ts

View file

@ -10,7 +10,9 @@ a frontend dashboard with stats for your ATProto PDS.
### installing
clone the repo, install dependencies using deno:
clone the repo, copy `config.ts.example` to `config.ts` and edit it to your liking.
then, install dependencies using deno:
```sh
deno install
@ -44,14 +46,11 @@ we use our own CI/CD workflow at [`.forgejo/workflows/deploy.yaml`](.forgejo/wor
## theming
currently the only way to theme the app is to edit css in the components directly, glhf
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.
relevant files:
currently, the name of the theme is determined by the directory name, and the theme itself is defined in `theme.css` inside that directory.
- [`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)
you can switch themes by changing the `theme` property in `config.ts`.
the favicon is located at [`public/favicon.ico`](public/favicon.ico)

View file

@ -1,35 +0,0 @@
/**
* 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>";
/**
* Whether to show the posts that are in the future
* @default false
*/
static readonly SHOW_FUTURE_POSTS: boolean = false;
}

44
config.ts.example Normal file
View file

@ -0,0 +1,44 @@
/**
* 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": "4",
"version": "5",
"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,7 +7,9 @@
"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"
@ -53,79 +55,129 @@
"integrity": "sha512-GEhUCk9c4XbNxi+0YZHZsV4fYNd6HejfWuN4Ti4c02DauX+LyX5WY1Y3WfyZ8Pxxl0zqhs+MLtW98cMh86vv6g=="
},
"@esbuild/aix-ppc64@0.25.2": {
"integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag=="
"integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==",
"os": ["aix"],
"cpu": ["ppc64"]
},
"@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": {
"integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA=="
"integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==",
"os": ["android"],
"cpu": ["arm"]
},
"@esbuild/android-x64@0.25.2": {
"integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg=="
"integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==",
"os": ["android"],
"cpu": ["x64"]
},
"@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": {
"integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA=="
"integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==",
"os": ["darwin"],
"cpu": ["x64"]
},
"@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": {
"integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ=="
"integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==",
"os": ["freebsd"],
"cpu": ["x64"]
},
"@esbuild/linux-arm64@0.25.2": {
"integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g=="
"integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==",
"os": ["linux"],
"cpu": ["arm64"]
},
"@esbuild/linux-arm@0.25.2": {
"integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g=="
"integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==",
"os": ["linux"],
"cpu": ["arm"]
},
"@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": {
"integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w=="
"integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==",
"os": ["linux"],
"cpu": ["loong64"]
},
"@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": {
"integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g=="
"integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==",
"os": ["linux"],
"cpu": ["ppc64"]
},
"@esbuild/linux-riscv64@0.25.2": {
"integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw=="
"integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==",
"os": ["linux"],
"cpu": ["riscv64"]
},
"@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": {
"integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg=="
"integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==",
"os": ["linux"],
"cpu": ["x64"]
},
"@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": {
"integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg=="
"integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==",
"os": ["netbsd"],
"cpu": ["x64"]
},
"@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": {
"integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw=="
"integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==",
"os": ["openbsd"],
"cpu": ["x64"]
},
"@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": {
"integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q=="
"integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==",
"os": ["win32"],
"cpu": ["arm64"]
},
"@esbuild/win32-ia32@0.25.2": {
"integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg=="
"integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==",
"os": ["win32"],
"cpu": ["ia32"]
},
"@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": {
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
@ -152,64 +204,104 @@
]
},
"@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": {
"integrity": "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w=="
"integrity": "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==",
"os": ["android"],
"cpu": ["arm64"]
},
"@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": {
"integrity": "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA=="
"integrity": "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==",
"os": ["darwin"],
"cpu": ["x64"]
},
"@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": {
"integrity": "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw=="
"integrity": "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==",
"os": ["freebsd"],
"cpu": ["x64"]
},
"@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": {
"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": {
"integrity": "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg=="
"integrity": "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==",
"os": ["linux"],
"cpu": ["arm64"]
},
"@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": {
"integrity": "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg=="
"integrity": "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==",
"os": ["linux"],
"cpu": ["loong64"]
},
"@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": {
"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": {
"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": {
"integrity": "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw=="
"integrity": "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==",
"os": ["linux"],
"cpu": ["s390x"]
},
"@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": {
"integrity": "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw=="
"integrity": "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==",
"os": ["linux"],
"cpu": ["x64"]
},
"@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": {
"integrity": "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA=="
"integrity": "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==",
"os": ["win32"],
"cpu": ["ia32"]
},
"@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": {
"integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==",
@ -246,7 +338,8 @@
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="
},
"acorn@8.14.1": {
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
"bin": true
},
"aria-query@5.3.2": {
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="
@ -274,7 +367,7 @@
},
"esbuild@0.25.2": {
"integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==",
"dependencies": [
"optionalDependencies": [
"@esbuild/aix-ppc64",
"@esbuild/android-arm",
"@esbuild/android-arm64",
@ -300,7 +393,9 @@
"@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=="
@ -315,10 +410,15 @@
"integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
"dependencies": [
"picomatch"
],
"optionalPeers": [
"picomatch"
]
},
"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": {
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
@ -347,8 +447,12 @@
"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=="
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"bin": true
},
"picocolors@1.1.1": {
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
@ -370,6 +474,9 @@
"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",
@ -390,9 +497,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==",
@ -413,7 +520,11 @@
"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==",
@ -442,24 +553,31 @@
]
},
"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": {
"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": {
@ -475,7 +593,9 @@
"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

@ -13,7 +13,9 @@
"@atcute/bluesky": "^2.0.2",
"@atcute/client": "^3.0.1",
"@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": {
"@sveltejs/vite-plugin-svelte": "^5.0.3",

View file

@ -1,10 +1,56 @@
<script lang="ts">
import PostComponent from "./lib/PostComponent.svelte";
import AccountComponent from "./lib/AccountComponent.svelte";
import { fetchAllPosts, Post, getAllMetadataFromPds } from "./lib/pdsfetch";
import InfiniteLoading from "svelte-infinite-loading";
import { getNextPosts, 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>
@ -13,7 +59,7 @@
<p>Loading...</p>
{:then accountsData}
<div id="Account">
<h1 id="Header">ATProto PDS</h1>
<h1 onclick={carameldansenfusion} id="Header">ATProto PDS</h1>
<p>Home to {accountsData.length} accounts</p>
<div id="accountsList">
{#each accountsData as accountObject}
@ -26,105 +72,17 @@
<p>Error: {error.message}</p>
{/await}
{#await postsPromise}
<p>Loading...</p>
{:then postsData}
<div id="Feed">
<div id="spacer"></div>
{#each postsData as postObject}
<PostComponent post={postObject as Post} />
{/each}
<div id="spacer"></div>
</div>
{/await}
<div id="Feed">
<div id="spacer"></div>
{#each posts as postObject}
<PostComponent post={postObject as Post} />
{/each}
<InfiniteLoading on:infinite={onInfinite} distance={3000} />
<div id="spacer"></div>
</div>
</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,83 +1,4 @@
@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;
}
@import url('./themes/colors.css');
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;
}
background-color: red;
}

View file

@ -12,37 +12,17 @@
alt="avatar of {account.displayName}"
src="{Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did={account.did}&cid={account.avatarCid}"
/>
<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 id="accountName">
{account.displayName || account.handle || account.did}
</div>
</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"
on:click={prevImage}
onclick={prevImage}
disabled={currentImageIndex === 0}>←</button
>
<div id="carouselIndicators">
@ -125,7 +125,7 @@
</div>
<button
id="nextBtn"
on:click={nextImage}
onclick={nextImage}
disabled={currentImageIndex === post.imagesCid.length - 1}
>→</button
>
@ -141,150 +141,16 @@
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;
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;
}
#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,16 +13,21 @@ 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: string;
did: At.Did;
displayName: string;
handle: string;
avatarCid: string | null;
currentCursor?: string;
}
let accountsMetadata: AccountMetadata[] = [];
interface atUriObject {
repo: string;
collection: string;
@ -42,10 +47,11 @@ class Post {
replyingUri: atUriObject | null;
imagesCid: string[] | null;
videosLinkCid: string | null;
gifLink: string | null;
constructor(
record: ComAtprotoRepoListRecords.Record,
account: AccountMetadata
account: AccountMetadata,
) {
this.postCid = record.cid;
this.recordName = processAtUri(record.uri).rkey;
@ -65,10 +71,11 @@ 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
(imageRecord: any) => imageRecord.image.ref.$link,
);
break;
case "app.bsky.embed.video":
@ -82,7 +89,7 @@ class Post {
switch (post.embed.media.$type) {
case "app.bsky.embed.images":
this.imagesCid = post.embed.media.images.map(
(imageRecord) => imageRecord.image.ref.$link
(imageRecord) => imageRecord.image.ref.$link,
);
break;
@ -92,6 +99,11 @@ 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;
}
}
}
@ -118,9 +130,15 @@ const getDidsFromPDS = async (): Promise<At.Did[]> => {
return data.repos.map((repo: any) => repo.did) as At.Did[];
};
const getAccountMetadata = async (
did: `did:${string}:${string}`
): Promise<AccountMetadata> => {
// gonna assume self exists in the app.bsky.actor.profile
did: `did:${string}:${string}`,
) => {
const account: AccountMetadata = {
did: did,
handle: "", // Guaranteed to be filled out later
displayName: "",
avatarCid: null,
};
try {
const { data } = await rpc.get("com.atproto.repo.getRecord", {
params: {
@ -130,26 +148,22 @@ const getAccountMetadata = async (
},
});
const value = data.value as AppBskyActorProfile.Record;
const handle = await blueskyHandleFromDid(did);
const account: AccountMetadata = {
did: did,
handle: handle,
displayName: value.displayName || "",
avatarCid: null,
};
account.displayName = value.displayName || "";
if (value.avatar) {
account.avatarCid = value.avatar.ref["$link"];
}
return account;
} catch (e) {
console.error(`Error fetching metadata for ${did}:`, e);
return {
did: "error",
displayName: "",
avatarCid: null,
handle: "error",
};
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;
};
const getAllMetadataFromPds = async (): Promise<AccountMetadata[]> => {
@ -157,33 +171,9 @@ const getAllMetadataFromPds = async (): Promise<AccountMetadata[]> => {
const metadata = await Promise.all(
dids.map(async (repo: `did:${string}:${string}`) => {
return await getAccountMetadata(repo);
})
}),
);
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,
};
}
return metadata.filter((account) => account !== null) as AccountMetadata[];
};
const identityResolve = async (did: At.Did) => {
@ -196,7 +186,7 @@ const identityResolve = async (did: At.Did) => {
if (did.startsWith("did:plc:") || did.startsWith("did:web:")) {
const doc = await resolver.resolve(
did as `did:plc:${string}` | `did:web:${string}`
did as `did:plc:${string}` | `did:web:${string}`,
);
return doc;
} else {
@ -219,36 +209,151 @@ const blueskyHandleFromDid = async (did: At.Did) => {
}
};
const fetchAllPosts = async () => {
const users: AccountMetadata[] = await getAllMetadataFromPds();
const postRecords = await Promise.all(
users.map(
async (metadata: AccountMetadata) => await fetchPosts(metadata.did)
)
);
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`);
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,
);
if (!cutoffDate) {
cutoffDate = latestPost;
} else {
if (latestPost > cutoffDate) {
cutoffDate = latestPost;
}
return new Post(record, user);
})
);
}
});
if (cutoffDate) {
return cutoffDate;
} else {
return new Date(now);
}
};
posts.sort((a, b) => b.timestamp - a.timestamp);
if(!Config.SHOW_FUTURE_POSTS) {
// Filter out posts that are in the future
const now = Date.now();
const filteredPosts = posts.filter((post) => post.timestamp <= now);
return filteredPosts.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();
}
return posts.slice(0, Config.MAX_POSTS);
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;
};
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 };

423
themes/default/theme.css Normal file
View file

@ -0,0 +1,423 @@
/* 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 */
}

375
themes/express/theme.css Normal file
View file

@ -0,0 +1,375 @@
@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 */
}

373
themes/witchcraft/theme.css Normal file
View file

@ -0,0 +1,373 @@
@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 */
}

32
theming.ts Normal file
View file

@ -0,0 +1,32 @@
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,7 +1,11 @@
import { defineConfig } from "vite";
import { svelte } from "@sveltejs/vite-plugin-svelte";
import { themePlugin } from "./theming";
// https://vite.dev/config/
export default defineConfig({
plugins: [svelte()],
plugins: [
themePlugin(),
svelte(),
],
});