Compare commits

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

24 commits

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

Reviewed-on: 
Co-authored-by: ari <ariadna@omg.lol>
Co-committed-by: ari <ariadna@omg.lol>
2025-04-22 03:14:37 +00:00
26aff55dc2
Let's not do that 2025-04-21 18:57:56 +09:00
bd7572dbd8
Let's fetch a bit more posts for now 2025-04-21 18:56:56 +09:00
2db2ca4a05
Formatting, cleanup, misc style changes, more config options 2025-04-21 16:20:43 +09:00
3af16a98e2
More typos in readme 2025-04-21 15:58:31 +09:00
8cf19c6a77
Typos and proper links 2025-04-21 15:55:34 +09:00
8a2ad07d8b
Draft of a proper readme and a license file 2025-04-21 15:52:32 +09:00
23cd738d71
Remove unused icons 2025-04-21 15:32:30 +09:00
b01b6b7f65 Cleanup and Maintenance part 1 ()
Co-authored-by: ari <ariadna@omg.lol>
Reviewed-on: 
2025-04-21 06:30:49 +00:00
7d850f0ab5
CI/CD 2025-04-21 14:31:09 +09:00
23 changed files with 1827 additions and 538 deletions

View file

@ -6,15 +6,26 @@ on:
- main - main
- astra/ci - astra/ci
jobs: jobs:
deploy: deploy:
name: Deploy name: Deploy
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repo - name: Checkout main repo
uses: actions/checkout@v4 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 - name: Setup Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:

5
.gitignore vendored
View file

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

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
# 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,3 +1,59 @@
# pds-dash # pds-dash
Frontend with stats for your ATProto PDS 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

View file

@ -1,28 +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 fetch from the PDS per user
* @default 10
*/
static readonly MAX_POSTS_PER_USER: number = 22;
/**
* 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>";
}

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;
}

239
deno.lock generated
View file

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

View file

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

View file

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

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

(image error) Size: 1.5 KiB

View file

@ -1,130 +1,88 @@
<script lang="ts"> <script lang="ts">
import PostComponent from "./lib/PostComponent.svelte"; import PostComponent from "./lib/PostComponent.svelte";
import AccountComponent from "./lib/AccountComponent.svelte"; import AccountComponent from "./lib/AccountComponent.svelte";
import { fetchAllPosts, Post, getAllMetadataFromPds } from "./lib/pdsfetch"; import InfiniteLoading from "svelte-infinite-loading";
import { getNextPosts, Post, getAllMetadataFromPds } from "./lib/pdsfetch";
import { Config } from "../config"; import { Config } from "../config";
const postsPromise = fetchAllPosts();
const accountsPromise = getAllMetadataFromPds(); const accountsPromise = getAllMetadataFromPds();
import { onMount } from "svelte";
let posts: Post[] = [];
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> </script>
<main> <main>
<div id="Content"> <div id="Content">
{#await accountsPromise} {#await accountsPromise}
<p>Loading...</p> <p>Loading...</p>
{:then accountsData} {:then accountsData}
<div id="Account"> <div id="Account">
<h1 id="Header">ATProto PDS</h1> <h1 onclick={carameldansenfusion} id="Header">ATProto PDS</h1>
<p>Home to {accountsData.length} accounts</p> <p>Home to {accountsData.length} accounts</p>
<div id="accountsList"> <div id="accountsList">
{#each accountsData as accountObject} {#each accountsData as accountObject}
<AccountComponent account={accountObject} /> <AccountComponent account={accountObject} />
{/each} {/each}
</div>
<p>{@html Config.FOOTER_TEXT}</p>
</div> </div>
<p>{@html Config.FOOTER_TEXT}</p> {:catch error}
</div> <p>Error: {error.message}</p>
{:catch error} {/await}
<p>Error: {error.message}</p>
{/await}
{#await postsPromise}
<p>Loading...</p>
{:then postsData}
<div id="Feed"> <div id="Feed">
<div id="spacer"></div> <div id="spacer"></div>
{#each postsData as postObject} {#each posts as postObject}
<PostComponent post={postObject as Post} /> <PostComponent post={postObject as Post} />
{/each} {/each}
<InfiniteLoading on:infinite={onInfinite} distance={3000} />
<div id="spacer"></div> <div id="spacer"></div>
</div> </div>
{/await}
</div> </div>
</main> </main>
<style> <style>
/* desktop style */
#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: #12082b;
color: #ffffff;
}
#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 #8054f0;
background-color: #0d0620;
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> </style>

View file

@ -1,73 +1,4 @@
@font-face { @import url('./themes/colors.css');
font-family: 'ProggyClean';
src: url(https://witchcraft.systems/ProggyCleanNerdFont-Regular.ttf);
}
::-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: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
text-decoration: underline;
}
body { body {
margin: 0; background-color: red;
display: flex; }
place-items: center;
min-width: 320px;
min-height: 100vh;
background-color: #12082b;
font-family: 'ProggyClean', monospace;
font-size: 24px;
color: white;
border-color: #8054f0;
}
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

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="26.6" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 308"><path fill="#FF3E00" d="M239.682 40.707C211.113-.182 154.69-12.301 113.895 13.69L42.247 59.356a82.198 82.198 0 0 0-37.135 55.056a86.566 86.566 0 0 0 8.536 55.576a82.425 82.425 0 0 0-12.296 30.719a87.596 87.596 0 0 0 14.964 66.244c28.574 40.893 84.997 53.007 125.787 27.016l71.648-45.664a82.182 82.182 0 0 0 37.135-55.057a86.601 86.601 0 0 0-8.53-55.577a82.409 82.409 0 0 0 12.29-30.718a87.573 87.573 0 0 0-14.963-66.244"></path><path fill="#FFF" d="M106.889 270.841c-23.102 6.007-47.497-3.036-61.103-22.648a52.685 52.685 0 0 1-9.003-39.85a49.978 49.978 0 0 1 1.713-6.693l1.35-4.115l3.671 2.697a92.447 92.447 0 0 0 28.036 14.007l2.663.808l-.245 2.659a16.067 16.067 0 0 0 2.89 10.656a17.143 17.143 0 0 0 18.397 6.828a15.786 15.786 0 0 0 4.403-1.935l71.67-45.672a14.922 14.922 0 0 0 6.734-9.977a15.923 15.923 0 0 0-2.713-12.011a17.156 17.156 0 0 0-18.404-6.832a15.78 15.78 0 0 0-4.396 1.933l-27.35 17.434a52.298 52.298 0 0 1-14.553 6.391c-23.101 6.007-47.497-3.036-61.101-22.649a52.681 52.681 0 0 1-9.004-39.849a49.428 49.428 0 0 1 22.34-33.114l71.664-45.677a52.218 52.218 0 0 1 14.563-6.398c23.101-6.007 47.497 3.036 61.101 22.648a52.685 52.685 0 0 1 9.004 39.85a50.559 50.559 0 0 1-1.713 6.692l-1.35 4.116l-3.67-2.693a92.373 92.373 0 0 0-28.037-14.013l-2.664-.809l.246-2.658a16.099 16.099 0 0 0-2.89-10.656a17.143 17.143 0 0 0-18.398-6.828a15.786 15.786 0 0 0-4.402 1.935l-71.67 45.674a14.898 14.898 0 0 0-6.73 9.975a15.9 15.9 0 0 0 2.709 12.012a17.156 17.156 0 0 0 18.404 6.832a15.841 15.841 0 0 0 4.402-1.935l27.345-17.427a52.147 52.147 0 0 1 14.552-6.397c23.101-6.006 47.497 3.037 61.102 22.65a52.681 52.681 0 0 1 9.003 39.848a49.453 49.453 0 0 1-22.34 33.12l-71.664 45.673a52.218 52.218 0 0 1-14.563 6.398"></path></svg>

Before

(image error) Size: 1.9 KiB

View file

@ -12,37 +12,17 @@
alt="avatar of {account.displayName}" alt="avatar of {account.displayName}"
src="{Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did={account.did}&cid={account.avatarCid}" 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} {/if}
<div id="accountName">
{account.displayName || account.handle || account.did}
</div>
</div> </div>
</a> </a>
<style> <style>
#accountContainer {
display: flex;
text-align: start;
align-items: center;
background-color: #12082b;
padding: 0px;
margin-bottom: 15px;
border: 1px solid #8054f0;
}
#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: #8054f0 1px solid;
}
</style> </style>

View file

@ -2,6 +2,7 @@
import { Post } from "./pdsfetch"; import { Post } from "./pdsfetch";
import { Config } from "../../config"; import { Config } from "../../config";
import { onMount } from "svelte"; import { onMount } from "svelte";
import moment from "moment";
let { post }: { post: Post } = $props(); let { post }: { post: Post } = $props();
@ -70,13 +71,15 @@
> >
<p id="handle"> <p id="handle">
<a href="{Config.FRONTEND_URL}/profile/{post.authorHandle}" <a href="{Config.FRONTEND_URL}/profile/{post.authorHandle}"
>{post.authorHandle}</a >@{post.authorHandle}</a
> >
<a <a
id="postLink" id="postLink"
href="{Config.FRONTEND_URL}/profile/{post.authorDid}/post/{post.recordName}" href="{Config.FRONTEND_URL}/profile/{post.authorDid}/post/{post.recordName}"
>{post.timenotstamp}</a >{moment(post.timenotstamp).isBefore(moment().subtract(1, "month"))
? moment(post.timenotstamp).format("MMM D, YYYY")
: moment(post.timenotstamp).fromNow()}</a
> >
</p> </p>
</div> </div>
@ -110,7 +113,7 @@
<div id="carouselControls"> <div id="carouselControls">
<button <button
id="prevBtn" id="prevBtn"
on:click={prevImage} onclick={prevImage}
disabled={currentImageIndex === 0}>←</button disabled={currentImageIndex === 0}>←</button
> >
<div id="carouselIndicators"> <div id="carouselIndicators">
@ -122,7 +125,7 @@
</div> </div>
<button <button
id="nextBtn" id="nextBtn"
on:click={nextImage} onclick={nextImage}
disabled={currentImageIndex === post.imagesCid.length - 1} disabled={currentImageIndex === post.imagesCid.length - 1}
>→</button >→</button
> >
@ -131,155 +134,23 @@
</div> </div>
{/if} {/if}
{#if post.videosLinkCid} {#if post.videosLinkCid}
<!-- svelte-ignore a11y_media_has_caption -->
<video <video
id="embedVideo" id="embedVideo"
src="{Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did={post.authorDid}&cid={post.videosLinkCid}" src="{Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did={post.authorDid}&cid={post.videosLinkCid}"
controls controls
></video> ></video>
{/if} {/if}
{#if post.gifLink}
<img
id="embedVideo"
src="{post.gifLink}"
alt="Post GIF"
/>
{/if}
</div> </div>
</div> </div>
<style> <style>
a:hover {
text-decoration: underline;
}
#postContainer {
display: flex;
flex-direction: column;
border: 1px solid #8054f0;
background-color: black;
margin-bottom: 15px;
overflow-wrap: break-word;
}
#postHeader {
display: flex;
flex-direction: row;
align-items: center;
justify-content: start;
background-color: #1f1145;
padding: 0px 0px;
height: fit-content;
border-bottom: 1px solid #8054f0;
font-weight: bold;
overflow-wrap: break-word;
height: 60px;
}
#displayName {
color: white;
font-size: 1.2em;
padding: 0;
margin: 0;
}
#handle {
color: #8054f0;
font-size: 0.8em;
padding: 0;
margin: 0;
}
#postLink {
color: #8054f0;
font-size: 0.8em;
padding: 0;
margin: 0;
}
#postContent {
display: flex;
text-align: start;
flex-direction: column;
padding: 10px;
background-color: #0d0620;
color: white;
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: #8054f0 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: #4a4a4a;
}
.indicator.active {
background-color: #8054f0;
}
#prevBtn,
#nextBtn {
background-color: rgba(31, 17, 69, 0.7);
color: white;
border: 1px solid #8054f0;
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> </style>

View file

@ -13,16 +13,21 @@ import {
WebDidDocumentResolver, WebDidDocumentResolver,
} from "@atcute/identity-resolver"; } from "@atcute/identity-resolver";
import { Config } from "../../config"; import { Config } from "../../config";
import { Mutex } from "mutex-ts"
// import { ComAtprotoRepoListRecords.Record } from "@atcute/client/lexicons"; // import { ComAtprotoRepoListRecords.Record } from "@atcute/client/lexicons";
// import { AppBskyFeedPost } from "@atcute/client/lexicons"; // import { AppBskyFeedPost } from "@atcute/client/lexicons";
// import { AppBskyActorDefs } from "@atcute/client/lexicons"; // import { AppBskyActorDefs } from "@atcute/client/lexicons";
interface AccountMetadata { interface AccountMetadata {
did: string; did: At.Did;
displayName: string; displayName: string;
handle: string; handle: string;
avatarCid: string | null; avatarCid: string | null;
currentCursor?: string;
} }
let accountsMetadata: AccountMetadata[] = [];
interface atUriObject { interface atUriObject {
repo: string; repo: string;
collection: string; collection: string;
@ -42,13 +47,14 @@ class Post {
replyingUri: atUriObject | null; replyingUri: atUriObject | null;
imagesCid: string[] | null; imagesCid: string[] | null;
videosLinkCid: string | null; videosLinkCid: string | null;
gifLink: string | null;
constructor( constructor(
record: ComAtprotoRepoListRecords.Record, record: ComAtprotoRepoListRecords.Record,
account: AccountMetadata, account: AccountMetadata,
) { ) {
this.postCid = record.cid; this.postCid = record.cid;
this.recordName = record.uri.split("/").slice(-1)[0]; this.recordName = processAtUri(record.uri).rkey;
this.authorDid = account.did; this.authorDid = account.did;
this.authorAvatarCid = account.avatarCid; this.authorAvatarCid = account.avatarCid;
this.authorHandle = account.handle; this.authorHandle = account.handle;
@ -65,10 +71,11 @@ class Post {
this.quotingUri = null; this.quotingUri = null;
this.imagesCid = null; this.imagesCid = null;
this.videosLinkCid = null; this.videosLinkCid = null;
this.gifLink = null;
switch (post.embed?.$type) { switch (post.embed?.$type) {
case "app.bsky.embed.images": case "app.bsky.embed.images":
this.imagesCid = post.embed.images.map((imageRecord: any) => this.imagesCid = post.embed.images.map(
imageRecord.image.ref.$link (imageRecord: any) => imageRecord.image.ref.$link,
); );
break; break;
case "app.bsky.embed.video": case "app.bsky.embed.video":
@ -81,8 +88,8 @@ class Post {
this.quotingUri = processAtUri(post.embed.record.record.uri); this.quotingUri = processAtUri(post.embed.record.record.uri);
switch (post.embed.media.$type) { switch (post.embed.media.$type) {
case "app.bsky.embed.images": case "app.bsky.embed.images":
this.imagesCid = post.embed.media.images.map((imageRecord) => this.imagesCid = post.embed.media.images.map(
imageRecord.image.ref.$link (imageRecord) => imageRecord.image.ref.$link,
); );
break; break;
@ -92,6 +99,11 @@ class Post {
break; break;
} }
break; break;
case "app.bsky.embed.external": // assuming that external embeds are gifs for now
if (post.embed.external.uri.includes(".gif")) {
this.gifLink = post.embed.external.uri;
}
break;
} }
} }
} }
@ -111,77 +123,57 @@ const rpc = new XRPC({
}), }),
}); });
const getDidsFromPDS = async () => { const getDidsFromPDS = async (): Promise<At.Did[]> => {
const { data } = await rpc.get("com.atproto.sync.listRepos", { const { data } = await rpc.get("com.atproto.sync.listRepos", {
params: {}, params: {},
}); });
return data.repos.map((repo: any) => (repo.did)); return data.repos.map((repo: any) => repo.did) as At.Did[];
}; };
const getAccountMetadata = async (did: `did:${string}:${string}`) => { const getAccountMetadata = async (
// gonna assume self exists in the app.bsky.actor.profile did: `did:${string}:${string}`,
try { ) => {
const { data } = await rpc.get("com.atproto.repo.getRecord", {
params: {
repo: did,
collection: "app.bsky.actor.profile",
rkey: "self",
},
});
const value = data.value as AppBskyActorProfile.Record;
const handle = await blueskyHandleFromDid(did);
const account: AccountMetadata = { const account: AccountMetadata = {
did: did, did: did,
handle: handle, handle: "", // Guaranteed to be filled out later
displayName: value.displayName || "", displayName: "",
avatarCid: null, avatarCid: null,
}; };
if (value.avatar) {
account.avatarCid = value.avatar.ref["$link"]; try {
const { data } = await rpc.get("com.atproto.repo.getRecord", {
params: {
repo: did,
collection: "app.bsky.actor.profile",
rkey: "self",
},
});
const value = data.value as AppBskyActorProfile.Record;
account.displayName = value.displayName || "";
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; return account;
}
catch (e) {
console.error(`Error fetching metadata for ${did}:`, e);
return {
did: "error",
displayName: "",
avatarCid: null,
};
}
}; };
const getAllMetadataFromPds = async () => { const getAllMetadataFromPds = async (): Promise<AccountMetadata[]> => {
const dids = await getDidsFromPDS(); const dids = await getDidsFromPDS();
const metadata = await Promise.all( const metadata = await Promise.all(
dids.map(async (repo: `did:${string}:${string}`) => { dids.map(async (repo: `did:${string}:${string}`) => {
return await getAccountMetadata(repo); return await getAccountMetadata(repo);
}), }),
); );
return metadata.filter(account => account.did !== "error"); return metadata.filter((account) => account !== null) as AccountMetadata[];
};
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_PER_USER,
},
});
return {
records: data.records as ComAtprotoRepoListRecords.Record[],
did: did,
error: false
};
} catch (e) {
console.error(`Error fetching posts for ${did}:`, e);
return {
records: [],
did: did,
error: true
};
}
}; };
const identityResolve = async (did: At.Did) => { const identityResolve = async (did: At.Did) => {
@ -217,34 +209,151 @@ const blueskyHandleFromDid = async (did: At.Did) => {
} }
}; };
const fetchAllPosts = async () => { interface PostsAcc {
const users: AccountMetadata[] = await getAllMetadataFromPds(); posts: ComAtprotoRepoListRecords.Record[];
const postRecords = await Promise.all( account: AccountMetadata;
users.map(async (metadata: AccountMetadata) => }
await fetchPosts(metadata.did) const getCutoffDate = (postAccounts: PostsAcc[]) => {
), const now = Date.now();
); let cutoffDate: Date | null = null;
const validPostRecords = postRecords.filter(record => !record.error); postAccounts.forEach((postAcc) => {
const posts: Post[] = validPostRecords.flatMap((userFetch) => const latestPost = new Date(
userFetch.records.map((record) => { (postAcc.posts[postAcc.posts.length - 1].value as AppBskyFeedPost.Record)
const user = users.find((user: AccountMetadata) => .createdAt,
user.did == userFetch.did );
); if (!cutoffDate) {
if (!user) { cutoffDate = latestPost;
throw new Error(`User with DID ${userFetch.did} not found`); } else {
if (latestPost > cutoffDate) {
cutoffDate = latestPost;
} }
return new Post(record, user); }
}) });
); if (cutoffDate) {
posts.sort((a, b) => b.timestamp - a.timestamp); return cutoffDate;
return posts; } else {
return new Date(now);
}
}; };
const testApiCall = async () => { const filterPostsByDate = (posts: PostsAcc[], cutoffDate: Date) => {
const { data } = await rpc.get("com.atproto.sync.listRepos", { // filter posts for each account that are older than the cutoff date and save the cursor of the last post included
params: {}, 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,
};
}); });
console.log(data); return filteredPosts;
}; };
export { fetchAllPosts, getAllMetadataFromPds, Post };
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 type { AccountMetadata }; export type { AccountMetadata };

View file

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

View file

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

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