this repo has no description

Compare changes

Choose any two refs to compare.

+36 -12
.forgejo/workflows/deploy.yaml
··· 6 - main 7 - astra/ci 8 9 jobs: 10 deploy: 11 name: Deploy 12 runs-on: ubuntu-latest 13 14 steps: 15 - - name: Checkout repo 16 uses: actions/checkout@v4 17 18 - name: Setup Deno 19 - uses: https://github.com/denoland/setup-deno@v1 20 21 - name: Install dependencies 22 run: deno install ··· 24 - name: Build project 25 run: deno task build 26 27 - - name: Setup SCP 28 run: | 29 mkdir -p ~/.ssh 30 echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519 31 - chmod -R go-rwx ~/.ssh 32 ssh-keyscan -H ${{ vars.SERVER_HOST }} >> ~/.ssh/known_hosts 33 - echo "Deploying to ${{ vars.SERVER_HOST }} as ${{ vars.SERVER_USER }} to /home/common/www/${{ github.ref_name }}" 34 35 - name: Debug SSH Connection 36 - run: ssh -v -o PasswordAuthentication=no -i ~/.ssh/id_ed25519 ${{ vars.SERVER_USER }}@${{ vars.SERVER_HOST }} echo "SSH Connection Successful" 37 38 - - name: create folder if not exists 39 - run: | 40 - ssh -i ~/.ssh/id_ed25519 ${{ vars.SERVER_USER }}@${{ vars.SERVER_HOST }} "mkdir -p /var/www/pds/${{ github.ref_name }}" 41 42 - name: Deploy via SCP 43 - run: | 44 - scp -i ~/.ssh/id_ed25519 -rv ./dist/* ${{ vars.SERVER_USER }}@\[${{ vars.SERVER_HOST }}\]:/var/www/pds/${{ github.ref_name }} 45 -
··· 6 - main 7 - astra/ci 8 9 + 10 jobs: 11 deploy: 12 name: Deploy 13 runs-on: ubuntu-latest 14 15 steps: 16 + - name: Checkout main repo 17 uses: actions/checkout@v4 18 19 + - name: Checkout overrides repo 20 + uses: actions/checkout@v4 21 + with: 22 + repository: scientific-witchery/pds-dash-overrides 23 + token: ${{ secrets.OVERRIDES_TOKEN}} 24 + path: overrides 25 + 26 + - name: Copy config file to root 27 + run: cp overrides/config.ts ./config.ts 28 + 29 + - name: Setup Node.js 30 + uses: actions/setup-node@v3 31 + with: 32 + node-version: '20' 33 + 34 - name: Setup Deno 35 + uses: https://github.com/denoland/setup-deno@v2 36 37 - name: Install dependencies 38 run: deno install ··· 40 - name: Build project 41 run: deno task build 42 43 + - name: Setup SSH 44 run: | 45 mkdir -p ~/.ssh 46 echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519 47 + chmod 600 ~/.ssh/id_ed25519 48 + cat > ~/.ssh/config << EOF 49 + Host deploy 50 + HostName ${{ vars.SERVER_HOST }} 51 + User ${{ vars.SERVER_USER }} 52 + IdentityFile ~/.ssh/id_ed25519 53 + StrictHostKeyChecking accept-new 54 + BatchMode yes 55 + PasswordAuthentication no 56 + PubkeyAuthentication yes 57 + EOF 58 + chmod 600 ~/.ssh/config 59 ssh-keyscan -H ${{ vars.SERVER_HOST }} >> ~/.ssh/known_hosts 60 + echo "Deploying to ${{ vars.SERVER_HOST }} as ${{ vars.SERVER_USER }} to /var/www/pds/${{ github.ref_name }}" 61 62 - name: Debug SSH Connection 63 + run: ssh -v deploy echo "SSH Connection Successful" 64 65 + - name: Create folder if not exists 66 + run: ssh deploy "mkdir -p /var/www/pds/${{ github.ref_name }}" 67 68 - name: Deploy via SCP 69 + run: scp -r ./dist/* deploy:/var/www/pds/${{ github.ref_name }}
+4 -1
.gitignore
··· 149 .yarn/unplugged 150 .yarn/build-state.yml 151 .yarn/install-state.gz 152 - .pnp.*
··· 149 .yarn/unplugged 150 .yarn/build-state.yml 151 .yarn/install-state.gz 152 + .pnp.* 153 + 154 + # Config files 155 + config.ts
+21
LICENSE
···
··· 1 + # MIT License 2 + 3 + Copyright (c) 2025 Witchcraft Systems 4 + 5 + Permission is hereby granted, free of charge, to any person obtaining a copy 6 + of this software and associated documentation files (the "Software"), to deal 7 + in the Software without restriction, including without limitation the rights 8 + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + copies of the Software, and to permit persons to whom the Software is 10 + furnished to do so, subject to the following conditions: 11 + 12 + The above copyright notice and this permission notice shall be included in all 13 + copies or substantial portions of the Software. 14 + 15 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 + SOFTWARE.
+57 -1
README.md
··· 1 # pds-dash 2 3 - Frontend with stats for your ATProto PDS
··· 1 # pds-dash 2 3 + a frontend dashboard with stats for your ATProto PDS. 4 + 5 + ## setup 6 + 7 + ### prerequisites 8 + 9 + - [deno](https://deno.com/manual/getting_started/installation) 10 + 11 + ### installing 12 + 13 + clone the repo, copy `config.ts.example` to `config.ts` and edit it to your liking. 14 + 15 + then, install dependencies using deno: 16 + 17 + ```sh 18 + deno install 19 + ``` 20 + 21 + ### development server 22 + 23 + local develompent server with hot reloading: 24 + 25 + ```sh 26 + deno task dev 27 + ``` 28 + 29 + ### building 30 + 31 + to build the optimized bundle run: 32 + 33 + ```sh 34 + deno task build 35 + ``` 36 + 37 + the output will be in the `dist/` directory. 38 + 39 + ## deploying 40 + 41 + we use our own CI/CD workflow at [`.forgejo/workflows/deploy.yaml`](.forgejo/workflows/deploy.yaml), but it boils down to building the project bundle and deploying it to a web server. it'll probably make more sense to host it on the same domain as your PDS, but it doesn't affect anything if you host it somewhere else. 42 + 43 + ## configuring 44 + 45 + [`config.ts`](config.ts) is the main configuration file, you can find more information in the file itself. 46 + 47 + ## theming 48 + 49 + 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. 50 + 51 + currently, the name of the theme is determined by the directory name, and the theme itself is defined in `theme.css` inside that directory. 52 + 53 + you can switch themes by changing the `theme` property in `config.ts`. 54 + 55 + the favicon is located at [`public/favicon.ico`](public/favicon.ico) 56 + 57 + ## license 58 + 59 + MIT
-28
config.ts
··· 1 - /** 2 - * Configuration module for the PDS Dashboard 3 - */ 4 - export class Config { 5 - /** 6 - * The base URL of the PDS (Personal Data Server) 7 - * @default "https://pds.witchcraft.systems" 8 - */ 9 - static readonly PDS_URL: string = "https://pds.witchcraft.systems"; 10 - 11 - /** 12 - * The base URL of the frontend service for linking to replies 13 - * @default "https://deer.social" 14 - */ 15 - static readonly FRONTEND_URL: string = "https://deer.social"; 16 - 17 - /** 18 - * Maximum number of posts to fetch from the PDS per user 19 - * @default 10 20 - */ 21 - static readonly MAX_POSTS_PER_USER: number = 22; 22 - 23 - /** 24 - * Footer text for the dashboard 25 - * @default "Astrally projected from witchcraft.systems" 26 - */ 27 - static readonly FOOTER_TEXT: string = "Astrally projected from <a href='https://witchcraft.systems' target='_blank'>witchcraft.systems</a>"; 28 - }
···
+44
config.ts.example
···
··· 1 + /** 2 + * Configuration module for the PDS Dashboard 3 + */ 4 + export class Config { 5 + /** 6 + * The base URL of the PDS (Personal Data Server). 7 + * @default none 8 + */ 9 + static readonly PDS_URL: string = ""; 10 + 11 + /** 12 + * Theme to be used 13 + * @default "default" 14 + */ 15 + static readonly THEME: string = "default"; 16 + 17 + /** 18 + * The base URL of the frontend service for linking to replies/quotes/accounts etc. 19 + * @default "https://deer.social" // or https://bsky.app if you're boring 20 + */ 21 + static readonly FRONTEND_URL: string = "https://deer.social"; 22 + 23 + /** 24 + * Maximum number of posts to fetch from the PDS per request 25 + * Should be around 20 for about 10 users on the pds 26 + * The more users you have, the lower the number should be 27 + * since sorting is slow and is done on the frontend 28 + * @default 20 29 + */ 30 + static readonly MAX_POSTS: number = 20; 31 + 32 + /** 33 + * Footer text for the dashboard, you probably want to change this. Supports HTML. 34 + * @default "<a href='https://git.witchcraft.systems/scientific-witchery/pds-dash' target='_blank'>Source</a> (<a href='https://github.com/witchcraft-systems/pds-dash/' target='_blank'>github mirror</a>)" 35 + */ 36 + static readonly FOOTER_TEXT: string = 37 + "<a href='https://git.witchcraft.systems/scientific-witchery/pds-dash' target='_blank'>Source</a> (<a href='https://github.com/witchcraft-systems/pds-dash/' target='_blank'>github mirror</a>)"; 38 + 39 + /** 40 + * Whether to show the posts with timestamps that are in the future. 41 + * @default false 42 + */ 43 + static readonly SHOW_FUTURE_POSTS: boolean = false; 44 + }
+182 -57
deno.lock
··· 1 { 2 - "version": "4", 3 "specifiers": { 4 "npm:@atcute/bluesky@^2.0.2": "2.0.2_@atcute+client@3.0.1", 5 "npm:@atcute/client@^3.0.1": "3.0.1", 6 "npm:@atcute/identity-resolver@~0.1.2": "0.1.2_@atcute+identity@0.1.3", 7 "npm:@sveltejs/vite-plugin-svelte@^5.0.3": "5.0.3_svelte@5.28.1__acorn@8.14.1_vite@6.3.2__picomatch@4.0.2", 8 "npm:@tsconfig/svelte@^5.0.4": "5.0.4", 9 "npm:svelte-check@^4.1.5": "4.1.6_svelte@5.28.1__acorn@8.14.1_typescript@5.7.3", 10 "npm:svelte@^5.23.1": "5.28.1_acorn@8.14.1", 11 "npm:typescript@~5.7.2": "5.7.3", 12 "npm:vite@^6.3.1": "6.3.2_picomatch@4.0.2" ··· 52 "integrity": "sha512-GEhUCk9c4XbNxi+0YZHZsV4fYNd6HejfWuN4Ti4c02DauX+LyX5WY1Y3WfyZ8Pxxl0zqhs+MLtW98cMh86vv6g==" 53 }, 54 "@esbuild/aix-ppc64@0.25.2": { 55 - "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==" 56 }, 57 "@esbuild/android-arm64@0.25.2": { 58 - "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==" 59 }, 60 "@esbuild/android-arm@0.25.2": { 61 - "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==" 62 }, 63 "@esbuild/android-x64@0.25.2": { 64 - "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==" 65 }, 66 "@esbuild/darwin-arm64@0.25.2": { 67 - "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==" 68 }, 69 "@esbuild/darwin-x64@0.25.2": { 70 - "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==" 71 }, 72 "@esbuild/freebsd-arm64@0.25.2": { 73 - "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==" 74 }, 75 "@esbuild/freebsd-x64@0.25.2": { 76 - "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==" 77 }, 78 "@esbuild/linux-arm64@0.25.2": { 79 - "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==" 80 }, 81 "@esbuild/linux-arm@0.25.2": { 82 - "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==" 83 }, 84 "@esbuild/linux-ia32@0.25.2": { 85 - "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==" 86 }, 87 "@esbuild/linux-loong64@0.25.2": { 88 - "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==" 89 }, 90 "@esbuild/linux-mips64el@0.25.2": { 91 - "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==" 92 }, 93 "@esbuild/linux-ppc64@0.25.2": { 94 - "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==" 95 }, 96 "@esbuild/linux-riscv64@0.25.2": { 97 - "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==" 98 }, 99 "@esbuild/linux-s390x@0.25.2": { 100 - "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==" 101 }, 102 "@esbuild/linux-x64@0.25.2": { 103 - "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==" 104 }, 105 "@esbuild/netbsd-arm64@0.25.2": { 106 - "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==" 107 }, 108 "@esbuild/netbsd-x64@0.25.2": { 109 - "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==" 110 }, 111 "@esbuild/openbsd-arm64@0.25.2": { 112 - "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==" 113 }, 114 "@esbuild/openbsd-x64@0.25.2": { 115 - "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==" 116 }, 117 "@esbuild/sunos-x64@0.25.2": { 118 - "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==" 119 }, 120 "@esbuild/win32-arm64@0.25.2": { 121 - "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==" 122 }, 123 "@esbuild/win32-ia32@0.25.2": { 124 - "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==" 125 }, 126 "@esbuild/win32-x64@0.25.2": { 127 - "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==" 128 }, 129 "@jridgewell/gen-mapping@0.3.8": { 130 "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", ··· 151 ] 152 }, 153 "@rollup/rollup-android-arm-eabi@4.40.0": { 154 - "integrity": "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==" 155 }, 156 "@rollup/rollup-android-arm64@4.40.0": { 157 - "integrity": "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==" 158 }, 159 "@rollup/rollup-darwin-arm64@4.40.0": { 160 - "integrity": "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==" 161 }, 162 "@rollup/rollup-darwin-x64@4.40.0": { 163 - "integrity": "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==" 164 }, 165 "@rollup/rollup-freebsd-arm64@4.40.0": { 166 - "integrity": "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==" 167 }, 168 "@rollup/rollup-freebsd-x64@4.40.0": { 169 - "integrity": "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==" 170 }, 171 "@rollup/rollup-linux-arm-gnueabihf@4.40.0": { 172 - "integrity": "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==" 173 }, 174 "@rollup/rollup-linux-arm-musleabihf@4.40.0": { 175 - "integrity": "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==" 176 }, 177 "@rollup/rollup-linux-arm64-gnu@4.40.0": { 178 - "integrity": "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==" 179 }, 180 "@rollup/rollup-linux-arm64-musl@4.40.0": { 181 - "integrity": "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==" 182 }, 183 "@rollup/rollup-linux-loongarch64-gnu@4.40.0": { 184 - "integrity": "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==" 185 }, 186 "@rollup/rollup-linux-powerpc64le-gnu@4.40.0": { 187 - "integrity": "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==" 188 }, 189 "@rollup/rollup-linux-riscv64-gnu@4.40.0": { 190 - "integrity": "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==" 191 }, 192 "@rollup/rollup-linux-riscv64-musl@4.40.0": { 193 - "integrity": "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==" 194 }, 195 "@rollup/rollup-linux-s390x-gnu@4.40.0": { 196 - "integrity": "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==" 197 }, 198 "@rollup/rollup-linux-x64-gnu@4.40.0": { 199 - "integrity": "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==" 200 }, 201 "@rollup/rollup-linux-x64-musl@4.40.0": { 202 - "integrity": "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==" 203 }, 204 "@rollup/rollup-win32-arm64-msvc@4.40.0": { 205 - "integrity": "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==" 206 }, 207 "@rollup/rollup-win32-ia32-msvc@4.40.0": { 208 - "integrity": "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==" 209 }, 210 "@rollup/rollup-win32-x64-msvc@4.40.0": { 211 - "integrity": "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==" 212 }, 213 "@sveltejs/acorn-typescript@1.0.5_acorn@8.14.1": { 214 "integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==", ··· 245 "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==" 246 }, 247 "acorn@8.14.1": { 248 - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==" 249 }, 250 "aria-query@5.3.2": { 251 "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==" ··· 273 }, 274 "esbuild@0.25.2": { 275 "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", 276 - "dependencies": [ 277 "@esbuild/aix-ppc64", 278 "@esbuild/android-arm", 279 "@esbuild/android-arm64", ··· 299 "@esbuild/win32-arm64", 300 "@esbuild/win32-ia32", 301 "@esbuild/win32-x64" 302 - ] 303 }, 304 "esm-env@1.2.2": { 305 "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==" ··· 314 "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", 315 "dependencies": [ 316 "picomatch" 317 ] 318 }, 319 "fsevents@2.3.3": { 320 - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==" 321 }, 322 "is-reference@3.0.3": { 323 "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", ··· 337 "@jridgewell/sourcemap-codec" 338 ] 339 }, 340 "mri@1.2.0": { 341 "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==" 342 }, 343 "ms@2.1.3": { 344 "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 345 }, 346 "nanoid@3.3.11": { 347 - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==" 348 }, 349 "picocolors@1.1.1": { 350 "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" ··· 366 "rollup@4.40.0": { 367 "integrity": "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==", 368 "dependencies": [ 369 "@rollup/rollup-android-arm-eabi", 370 "@rollup/rollup-android-arm64", 371 "@rollup/rollup-darwin-arm64", ··· 386 "@rollup/rollup-win32-arm64-msvc", 387 "@rollup/rollup-win32-ia32-msvc", 388 "@rollup/rollup-win32-x64-msvc", 389 - "@types/estree", 390 "fsevents" 391 - ] 392 }, 393 "sade@1.8.1": { 394 "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", ··· 409 "sade", 410 "svelte", 411 "typescript" 412 - ] 413 }, 414 "svelte@5.28.1_acorn@8.14.1": { 415 "integrity": "sha512-iOa9WmfNG95lSOSJdMhdjJ4Afok7IRAQYXpbnxhd5EINnXseG0GVa9j6WPght4eX78XfFez45Fi+uRglGKPV/Q==", ··· 438 ] 439 }, 440 "typescript@5.7.3": { 441 - "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==" 442 }, 443 "vite@6.3.2_picomatch@4.0.2": { 444 "integrity": "sha512-ZSvGOXKGceizRQIZSz7TGJ0pS3QLlVY/9hwxVh17W3re67je1RKYzFHivZ/t0tubU78Vkyb9WnHPENSBCzbckg==", 445 "dependencies": [ 446 "esbuild", 447 "fdir", 448 - "fsevents", 449 "picomatch", 450 "postcss", 451 "rollup", 452 "tinyglobby" 453 - ] 454 }, 455 "vitefu@1.0.6_vite@6.3.2__picomatch@4.0.2": { 456 "integrity": "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA==", 457 "dependencies": [ 458 "vite" 459 ] 460 }, ··· 470 "npm:@atcute/identity-resolver@~0.1.2", 471 "npm:@sveltejs/vite-plugin-svelte@^5.0.3", 472 "npm:@tsconfig/svelte@^5.0.4", 473 "npm:svelte-check@^4.1.5", 474 "npm:svelte@^5.23.1", 475 "npm:typescript@~5.7.2", 476 "npm:vite@^6.3.1"
··· 1 { 2 + "version": "5", 3 "specifiers": { 4 "npm:@atcute/bluesky@^2.0.2": "2.0.2_@atcute+client@3.0.1", 5 "npm:@atcute/client@^3.0.1": "3.0.1", 6 "npm:@atcute/identity-resolver@~0.1.2": "0.1.2_@atcute+identity@0.1.3", 7 "npm:@sveltejs/vite-plugin-svelte@^5.0.3": "5.0.3_svelte@5.28.1__acorn@8.14.1_vite@6.3.2__picomatch@4.0.2", 8 "npm:@tsconfig/svelte@^5.0.4": "5.0.4", 9 + "npm:moment@^2.30.1": "2.30.1", 10 + "npm:mutex-ts@^1.2.1": "1.2.1", 11 "npm:svelte-check@^4.1.5": "4.1.6_svelte@5.28.1__acorn@8.14.1_typescript@5.7.3", 12 + "npm:svelte-infinite-loading@^1.4.0": "1.4.0", 13 "npm:svelte@^5.23.1": "5.28.1_acorn@8.14.1", 14 "npm:typescript@~5.7.2": "5.7.3", 15 "npm:vite@^6.3.1": "6.3.2_picomatch@4.0.2" ··· 55 "integrity": "sha512-GEhUCk9c4XbNxi+0YZHZsV4fYNd6HejfWuN4Ti4c02DauX+LyX5WY1Y3WfyZ8Pxxl0zqhs+MLtW98cMh86vv6g==" 56 }, 57 "@esbuild/aix-ppc64@0.25.2": { 58 + "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", 59 + "os": ["aix"], 60 + "cpu": ["ppc64"] 61 }, 62 "@esbuild/android-arm64@0.25.2": { 63 + "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", 64 + "os": ["android"], 65 + "cpu": ["arm64"] 66 }, 67 "@esbuild/android-arm@0.25.2": { 68 + "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", 69 + "os": ["android"], 70 + "cpu": ["arm"] 71 }, 72 "@esbuild/android-x64@0.25.2": { 73 + "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", 74 + "os": ["android"], 75 + "cpu": ["x64"] 76 }, 77 "@esbuild/darwin-arm64@0.25.2": { 78 + "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==", 79 + "os": ["darwin"], 80 + "cpu": ["arm64"] 81 }, 82 "@esbuild/darwin-x64@0.25.2": { 83 + "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", 84 + "os": ["darwin"], 85 + "cpu": ["x64"] 86 }, 87 "@esbuild/freebsd-arm64@0.25.2": { 88 + "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", 89 + "os": ["freebsd"], 90 + "cpu": ["arm64"] 91 }, 92 "@esbuild/freebsd-x64@0.25.2": { 93 + "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", 94 + "os": ["freebsd"], 95 + "cpu": ["x64"] 96 }, 97 "@esbuild/linux-arm64@0.25.2": { 98 + "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", 99 + "os": ["linux"], 100 + "cpu": ["arm64"] 101 }, 102 "@esbuild/linux-arm@0.25.2": { 103 + "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", 104 + "os": ["linux"], 105 + "cpu": ["arm"] 106 }, 107 "@esbuild/linux-ia32@0.25.2": { 108 + "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", 109 + "os": ["linux"], 110 + "cpu": ["ia32"] 111 }, 112 "@esbuild/linux-loong64@0.25.2": { 113 + "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", 114 + "os": ["linux"], 115 + "cpu": ["loong64"] 116 }, 117 "@esbuild/linux-mips64el@0.25.2": { 118 + "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", 119 + "os": ["linux"], 120 + "cpu": ["mips64el"] 121 }, 122 "@esbuild/linux-ppc64@0.25.2": { 123 + "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", 124 + "os": ["linux"], 125 + "cpu": ["ppc64"] 126 }, 127 "@esbuild/linux-riscv64@0.25.2": { 128 + "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", 129 + "os": ["linux"], 130 + "cpu": ["riscv64"] 131 }, 132 "@esbuild/linux-s390x@0.25.2": { 133 + "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", 134 + "os": ["linux"], 135 + "cpu": ["s390x"] 136 }, 137 "@esbuild/linux-x64@0.25.2": { 138 + "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", 139 + "os": ["linux"], 140 + "cpu": ["x64"] 141 }, 142 "@esbuild/netbsd-arm64@0.25.2": { 143 + "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", 144 + "os": ["netbsd"], 145 + "cpu": ["arm64"] 146 }, 147 "@esbuild/netbsd-x64@0.25.2": { 148 + "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", 149 + "os": ["netbsd"], 150 + "cpu": ["x64"] 151 }, 152 "@esbuild/openbsd-arm64@0.25.2": { 153 + "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", 154 + "os": ["openbsd"], 155 + "cpu": ["arm64"] 156 }, 157 "@esbuild/openbsd-x64@0.25.2": { 158 + "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", 159 + "os": ["openbsd"], 160 + "cpu": ["x64"] 161 }, 162 "@esbuild/sunos-x64@0.25.2": { 163 + "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", 164 + "os": ["sunos"], 165 + "cpu": ["x64"] 166 }, 167 "@esbuild/win32-arm64@0.25.2": { 168 + "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", 169 + "os": ["win32"], 170 + "cpu": ["arm64"] 171 }, 172 "@esbuild/win32-ia32@0.25.2": { 173 + "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", 174 + "os": ["win32"], 175 + "cpu": ["ia32"] 176 }, 177 "@esbuild/win32-x64@0.25.2": { 178 + "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", 179 + "os": ["win32"], 180 + "cpu": ["x64"] 181 }, 182 "@jridgewell/gen-mapping@0.3.8": { 183 "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", ··· 204 ] 205 }, 206 "@rollup/rollup-android-arm-eabi@4.40.0": { 207 + "integrity": "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==", 208 + "os": ["android"], 209 + "cpu": ["arm"] 210 }, 211 "@rollup/rollup-android-arm64@4.40.0": { 212 + "integrity": "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==", 213 + "os": ["android"], 214 + "cpu": ["arm64"] 215 }, 216 "@rollup/rollup-darwin-arm64@4.40.0": { 217 + "integrity": "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==", 218 + "os": ["darwin"], 219 + "cpu": ["arm64"] 220 }, 221 "@rollup/rollup-darwin-x64@4.40.0": { 222 + "integrity": "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==", 223 + "os": ["darwin"], 224 + "cpu": ["x64"] 225 }, 226 "@rollup/rollup-freebsd-arm64@4.40.0": { 227 + "integrity": "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==", 228 + "os": ["freebsd"], 229 + "cpu": ["arm64"] 230 }, 231 "@rollup/rollup-freebsd-x64@4.40.0": { 232 + "integrity": "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==", 233 + "os": ["freebsd"], 234 + "cpu": ["x64"] 235 }, 236 "@rollup/rollup-linux-arm-gnueabihf@4.40.0": { 237 + "integrity": "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==", 238 + "os": ["linux"], 239 + "cpu": ["arm"] 240 }, 241 "@rollup/rollup-linux-arm-musleabihf@4.40.0": { 242 + "integrity": "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==", 243 + "os": ["linux"], 244 + "cpu": ["arm"] 245 }, 246 "@rollup/rollup-linux-arm64-gnu@4.40.0": { 247 + "integrity": "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==", 248 + "os": ["linux"], 249 + "cpu": ["arm64"] 250 }, 251 "@rollup/rollup-linux-arm64-musl@4.40.0": { 252 + "integrity": "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==", 253 + "os": ["linux"], 254 + "cpu": ["arm64"] 255 }, 256 "@rollup/rollup-linux-loongarch64-gnu@4.40.0": { 257 + "integrity": "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==", 258 + "os": ["linux"], 259 + "cpu": ["loong64"] 260 }, 261 "@rollup/rollup-linux-powerpc64le-gnu@4.40.0": { 262 + "integrity": "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==", 263 + "os": ["linux"], 264 + "cpu": ["ppc64"] 265 }, 266 "@rollup/rollup-linux-riscv64-gnu@4.40.0": { 267 + "integrity": "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==", 268 + "os": ["linux"], 269 + "cpu": ["riscv64"] 270 }, 271 "@rollup/rollup-linux-riscv64-musl@4.40.0": { 272 + "integrity": "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==", 273 + "os": ["linux"], 274 + "cpu": ["riscv64"] 275 }, 276 "@rollup/rollup-linux-s390x-gnu@4.40.0": { 277 + "integrity": "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==", 278 + "os": ["linux"], 279 + "cpu": ["s390x"] 280 }, 281 "@rollup/rollup-linux-x64-gnu@4.40.0": { 282 + "integrity": "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==", 283 + "os": ["linux"], 284 + "cpu": ["x64"] 285 }, 286 "@rollup/rollup-linux-x64-musl@4.40.0": { 287 + "integrity": "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==", 288 + "os": ["linux"], 289 + "cpu": ["x64"] 290 }, 291 "@rollup/rollup-win32-arm64-msvc@4.40.0": { 292 + "integrity": "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==", 293 + "os": ["win32"], 294 + "cpu": ["arm64"] 295 }, 296 "@rollup/rollup-win32-ia32-msvc@4.40.0": { 297 + "integrity": "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==", 298 + "os": ["win32"], 299 + "cpu": ["ia32"] 300 }, 301 "@rollup/rollup-win32-x64-msvc@4.40.0": { 302 + "integrity": "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==", 303 + "os": ["win32"], 304 + "cpu": ["x64"] 305 }, 306 "@sveltejs/acorn-typescript@1.0.5_acorn@8.14.1": { 307 "integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==", ··· 338 "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==" 339 }, 340 "acorn@8.14.1": { 341 + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", 342 + "bin": true 343 }, 344 "aria-query@5.3.2": { 345 "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==" ··· 367 }, 368 "esbuild@0.25.2": { 369 "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", 370 + "optionalDependencies": [ 371 "@esbuild/aix-ppc64", 372 "@esbuild/android-arm", 373 "@esbuild/android-arm64", ··· 393 "@esbuild/win32-arm64", 394 "@esbuild/win32-ia32", 395 "@esbuild/win32-x64" 396 + ], 397 + "scripts": true, 398 + "bin": true 399 }, 400 "esm-env@1.2.2": { 401 "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==" ··· 410 "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", 411 "dependencies": [ 412 "picomatch" 413 + ], 414 + "optionalPeers": [ 415 + "picomatch" 416 ] 417 }, 418 "fsevents@2.3.3": { 419 + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 420 + "os": ["darwin"], 421 + "scripts": true 422 }, 423 "is-reference@3.0.3": { 424 "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", ··· 438 "@jridgewell/sourcemap-codec" 439 ] 440 }, 441 + "moment@2.30.1": { 442 + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==" 443 + }, 444 "mri@1.2.0": { 445 "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==" 446 }, 447 "ms@2.1.3": { 448 "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 449 + }, 450 + "mutex-ts@1.2.1": { 451 + "integrity": "sha512-OkcXgf0viuCgYdnm48kiNQ9PzC5OzISQ261svHr/Ybc2vBYC/5xfLXn44hQ+dYRX74v7MCSqV/LKPEbpYdDybw==" 452 }, 453 "nanoid@3.3.11": { 454 + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", 455 + "bin": true 456 }, 457 "picocolors@1.1.1": { 458 "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" ··· 474 "rollup@4.40.0": { 475 "integrity": "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==", 476 "dependencies": [ 477 + "@types/estree" 478 + ], 479 + "optionalDependencies": [ 480 "@rollup/rollup-android-arm-eabi", 481 "@rollup/rollup-android-arm64", 482 "@rollup/rollup-darwin-arm64", ··· 497 "@rollup/rollup-win32-arm64-msvc", 498 "@rollup/rollup-win32-ia32-msvc", 499 "@rollup/rollup-win32-x64-msvc", 500 "fsevents" 501 + ], 502 + "bin": true 503 }, 504 "sade@1.8.1": { 505 "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", ··· 520 "sade", 521 "svelte", 522 "typescript" 523 + ], 524 + "bin": true 525 + }, 526 + "svelte-infinite-loading@1.4.0": { 527 + "integrity": "sha512-Jo+f/yr/HmZQuIiiKKzAHVFXdAUWHW2RBbrcQTil8JVk1sCm/riy7KTJVzjBgQvHasrFQYKF84zvtc9/Y4lFYg==" 528 }, 529 "svelte@5.28.1_acorn@8.14.1": { 530 "integrity": "sha512-iOa9WmfNG95lSOSJdMhdjJ4Afok7IRAQYXpbnxhd5EINnXseG0GVa9j6WPght4eX78XfFez45Fi+uRglGKPV/Q==", ··· 553 ] 554 }, 555 "typescript@5.7.3": { 556 + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", 557 + "bin": true 558 }, 559 "vite@6.3.2_picomatch@4.0.2": { 560 "integrity": "sha512-ZSvGOXKGceizRQIZSz7TGJ0pS3QLlVY/9hwxVh17W3re67je1RKYzFHivZ/t0tubU78Vkyb9WnHPENSBCzbckg==", 561 "dependencies": [ 562 "esbuild", 563 "fdir", 564 "picomatch", 565 "postcss", 566 "rollup", 567 "tinyglobby" 568 + ], 569 + "optionalDependencies": [ 570 + "fsevents" 571 + ], 572 + "bin": true 573 }, 574 "vitefu@1.0.6_vite@6.3.2__picomatch@4.0.2": { 575 "integrity": "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA==", 576 "dependencies": [ 577 + "vite" 578 + ], 579 + "optionalPeers": [ 580 "vite" 581 ] 582 }, ··· 592 "npm:@atcute/identity-resolver@~0.1.2", 593 "npm:@sveltejs/vite-plugin-svelte@^5.0.3", 594 "npm:@tsconfig/svelte@^5.0.4", 595 + "npm:moment@^2.30.1", 596 + "npm:mutex-ts@^1.2.1", 597 "npm:svelte-check@^4.1.5", 598 + "npm:svelte-infinite-loading@^1.4.0", 599 "npm:svelte@^5.23.1", 600 "npm:typescript@~5.7.2", 601 "npm:vite@^6.3.1"
+1 -1
index.html
··· 1 - <!doctype html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8" />
··· 1 + <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8" />
+4 -1
package.json
··· 12 "dependencies": { 13 "@atcute/bluesky": "^2.0.2", 14 "@atcute/client": "^3.0.1", 15 - "@atcute/identity-resolver": "^0.1.2" 16 }, 17 "devDependencies": { 18 "@sveltejs/vite-plugin-svelte": "^5.0.3",
··· 12 "dependencies": { 13 "@atcute/bluesky": "^2.0.2", 14 "@atcute/client": "^3.0.1", 15 + "@atcute/identity-resolver": "^0.1.2", 16 + "moment": "^2.30.1", 17 + "mutex-ts": "^1.2.1", 18 + "svelte-infinite-loading": "^1.4.0" 19 }, 20 "devDependencies": { 21 "@sveltejs/vite-plugin-svelte": "^5.0.3",
-1
public/vite.svg
··· 1 - <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
···
+65 -107
src/App.svelte
··· 1 <script lang="ts"> 2 import PostComponent from "./lib/PostComponent.svelte"; 3 import AccountComponent from "./lib/AccountComponent.svelte"; 4 - import { fetchAllPosts, Post, getAllMetadataFromPds } from "./lib/pdsfetch"; 5 import { Config } from "../config"; 6 - const postsPromise = fetchAllPosts(); 7 const accountsPromise = getAllMetadataFromPds(); 8 </script> 9 10 <main> 11 <div id="Content"> 12 - {#await accountsPromise} 13 - <p>Loading...</p> 14 - {:then accountsData} 15 - <div id="Account"> 16 - <h1 id="Header">ATProto PDS</h1> 17 - <p>Home to {accountsData.length} accounts</p> 18 - <div id="accountsList"> 19 - {#each accountsData as accountObject} 20 - <AccountComponent account={accountObject} /> 21 - {/each} 22 </div> 23 - <p>{@html Config.FOOTER_TEXT}</p> 24 - </div> 25 - {:catch error} 26 - <p>Error: {error.message}</p> 27 - {/await} 28 29 - {#await postsPromise} 30 - <p>Loading...</p> 31 - {:then postsData} 32 <div id="Feed"> 33 <div id="spacer"></div> 34 - {#each postsData as postObject} 35 <PostComponent post={postObject as Post} /> 36 {/each} 37 <div id="spacer"></div> 38 </div> 39 - {/await} 40 </div> 41 </main> 42 43 <style> 44 - /* desktop style */ 45 46 - #Content { 47 - display: flex; 48 - /* split the screen in half, left for accounts, right for posts */ 49 - width: 100%; 50 - height: 100%; 51 - flex-direction: row; 52 - justify-content: space-between; 53 - align-items: center; 54 - background-color: #12082b; 55 - color: #ffffff; 56 - } 57 - #Feed { 58 - width: 65%; 59 - height: 100vh; 60 - overflow-y: scroll; 61 - padding: 20px; 62 - padding-bottom: 0; 63 - padding-top: 0; 64 - margin-top: 0; 65 - margin-bottom: 0; 66 - } 67 - #spacer { 68 - padding: 0; 69 - margin: 0; 70 - height: 10vh; 71 - width: 100%; 72 - } 73 - #Account { 74 - width: 35%; 75 - display: flex; 76 - flex-direction: column; 77 - border: 1px solid #8054f0; 78 - background-color: #0d0620; 79 - height: 80vh; 80 - padding: 20px; 81 - margin-left: 20px; 82 - } 83 - #accountsList { 84 - display: flex; 85 - flex-direction: column; 86 - overflow-y: scroll; 87 - height: 100%; 88 - width: 100%; 89 - padding: 0px; 90 - margin: 0px; 91 - } 92 - 93 - #Header { 94 - text-align: center; 95 - font-size: 2em; 96 - margin-bottom: 20px; 97 - } 98 - 99 - /* mobile style */ 100 - @media screen and (max-width: 600px) { 101 - #Content { 102 - flex-direction: column; 103 - width: auto; 104 - padding-left: 0px; 105 - padding-right: 0px; 106 - margin-top: 5%; 107 - } 108 - #Account { 109 - width: auto; 110 - padding-left: 5%; 111 - padding-right: 5%; 112 - margin-bottom: 20px; 113 - margin-left: 5%; 114 - margin-right: 5%; 115 - height: auto; 116 - } 117 - #Feed { 118 - width: 95%; 119 - margin: 0px; 120 - margin-left: 10%; 121 - margin-right: 10%; 122 - padding: 0px; 123 - height: auto; 124 - } 125 - 126 - #spacer { 127 - height: 0; 128 - } 129 - } 130 </style>
··· 1 <script lang="ts"> 2 import PostComponent from "./lib/PostComponent.svelte"; 3 import AccountComponent from "./lib/AccountComponent.svelte"; 4 + import InfiniteLoading from "svelte-infinite-loading"; 5 + import { getNextPosts, Post, getAllMetadataFromPds } from "./lib/pdsfetch"; 6 import { Config } from "../config"; 7 const accountsPromise = getAllMetadataFromPds(); 8 + import { onMount } from "svelte"; 9 + 10 + let posts: Post[] = []; 11 + 12 + let hue: number = 1; 13 + const cycleColors = async () => { 14 + while (true) { 15 + hue += 1; 16 + if (hue > 360) { 17 + hue = 0; 18 + } 19 + document.documentElement.style.setProperty("--primary-h", hue.toString()); 20 + await new Promise((resolve) => setTimeout(resolve, 10)); 21 + } 22 + } 23 + let clickCounter = 0; 24 + const carameldansenfusion = async () => { 25 + clickCounter++; 26 + if (clickCounter >= 10) { 27 + clickCounter = 0; 28 + cycleColors(); 29 + } 30 + }; 31 + 32 + onMount(() => { 33 + // Fetch initial posts 34 + getNextPosts().then((initialPosts) => { 35 + posts = initialPosts; 36 + }); 37 + }); 38 + // Infinite loading function 39 + const onInfinite = ({ 40 + detail: { loaded, complete }, 41 + }: { 42 + detail: { loaded: () => void; complete: () => void }; 43 + }) => { 44 + getNextPosts().then((newPosts) => { 45 + console.log("Loading next posts..."); 46 + if (newPosts.length > 0) { 47 + posts = [...posts, ...newPosts]; 48 + loaded(); 49 + } else { 50 + complete(); 51 + } 52 + }); 53 + }; 54 </script> 55 56 <main> 57 <div id="Content"> 58 + {#await accountsPromise} 59 + <p>Loading...</p> 60 + {:then accountsData} 61 + <div id="Account"> 62 + <h1 onclick={carameldansenfusion} id="Header">ATProto PDS</h1> 63 + <p>Home to {accountsData.length} accounts</p> 64 + <div id="accountsList"> 65 + {#each accountsData as accountObject} 66 + <AccountComponent account={accountObject} /> 67 + {/each} 68 + </div> 69 + <p>{@html Config.FOOTER_TEXT}</p> 70 </div> 71 + {:catch error} 72 + <p>Error: {error.message}</p> 73 + {/await} 74 75 <div id="Feed"> 76 <div id="spacer"></div> 77 + {#each posts as postObject} 78 <PostComponent post={postObject as Post} /> 79 {/each} 80 + <InfiniteLoading on:infinite={onInfinite} distance={3000} /> 81 <div id="spacer"></div> 82 </div> 83 </div> 84 </main> 85 86 <style> 87 88 </style>
+3 -72
src/app.css
··· 1 - @font-face { 2 - font-family: 'ProggyClean'; 3 - src: url(https://witchcraft.systems/ProggyCleanNerdFont-Regular.ttf); 4 - } 5 - 6 - ::-webkit-scrollbar { 7 - width: 0px; 8 - background: transparent; 9 - padding: 0; 10 - margin: 0; 11 - } 12 - ::-webkit-scrollbar-thumb { 13 - background: transparent; 14 - border-radius: 0; 15 - } 16 - ::-webkit-scrollbar-track { 17 - background: transparent; 18 - border-radius: 0; 19 - } 20 - ::-webkit-scrollbar-corner { 21 - background: transparent; 22 - border-radius: 0; 23 - } 24 - ::-webkit-scrollbar-button { 25 - background: transparent; 26 - border-radius: 0; 27 - } 28 - * { 29 - scrollbar-width: none; 30 - scrollbar-color: transparent transparent; 31 - -ms-overflow-style: none; /* IE and Edge */ 32 - -webkit-overflow-scrolling: touch; 33 - -webkit-scrollbar: none; /* Safari */ 34 - } 35 - 36 - a { 37 - font-weight: 500; 38 - color: #646cff; 39 - text-decoration: inherit; 40 - } 41 - a:hover { 42 - color: #535bf2; 43 - text-decoration: underline; 44 - } 45 - 46 body { 47 - margin: 0; 48 - display: flex; 49 - place-items: center; 50 - min-width: 320px; 51 - min-height: 100vh; 52 - background-color: #12082b; 53 - font-family: 'ProggyClean', monospace; 54 - font-size: 24px; 55 - color: white; 56 - border-color: #8054f0; 57 - } 58 - 59 - h1 { 60 - font-size: 3.2em; 61 - line-height: 1.1; 62 - } 63 - 64 - #app { 65 - max-width: 1400px; 66 - margin: 0; 67 - padding: 0; 68 - margin-left: auto; 69 - margin-right: auto; 70 - text-align: center; 71 - } 72 - 73 -
··· 1 + @import url('./themes/colors.css'); 2 body { 3 + background-color: red; 4 + }
-1
src/assets/svelte.svg
··· 1 - <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="26.6" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 308"><path fill="#FF3E00" d="M239.682 40.707C211.113-.182 154.69-12.301 113.895 13.69L42.247 59.356a82.198 82.198 0 0 0-37.135 55.056a86.566 86.566 0 0 0 8.536 55.576a82.425 82.425 0 0 0-12.296 30.719a87.596 87.596 0 0 0 14.964 66.244c28.574 40.893 84.997 53.007 125.787 27.016l71.648-45.664a82.182 82.182 0 0 0 37.135-55.057a86.601 86.601 0 0 0-8.53-55.577a82.409 82.409 0 0 0 12.29-30.718a87.573 87.573 0 0 0-14.963-66.244"></path><path fill="#FFF" d="M106.889 270.841c-23.102 6.007-47.497-3.036-61.103-22.648a52.685 52.685 0 0 1-9.003-39.85a49.978 49.978 0 0 1 1.713-6.693l1.35-4.115l3.671 2.697a92.447 92.447 0 0 0 28.036 14.007l2.663.808l-.245 2.659a16.067 16.067 0 0 0 2.89 10.656a17.143 17.143 0 0 0 18.397 6.828a15.786 15.786 0 0 0 4.403-1.935l71.67-45.672a14.922 14.922 0 0 0 6.734-9.977a15.923 15.923 0 0 0-2.713-12.011a17.156 17.156 0 0 0-18.404-6.832a15.78 15.78 0 0 0-4.396 1.933l-27.35 17.434a52.298 52.298 0 0 1-14.553 6.391c-23.101 6.007-47.497-3.036-61.101-22.649a52.681 52.681 0 0 1-9.004-39.849a49.428 49.428 0 0 1 22.34-33.114l71.664-45.677a52.218 52.218 0 0 1 14.563-6.398c23.101-6.007 47.497 3.036 61.101 22.648a52.685 52.685 0 0 1 9.004 39.85a50.559 50.559 0 0 1-1.713 6.692l-1.35 4.116l-3.67-2.693a92.373 92.373 0 0 0-28.037-14.013l-2.664-.809l.246-2.658a16.099 16.099 0 0 0-2.89-10.656a17.143 17.143 0 0 0-18.398-6.828a15.786 15.786 0 0 0-4.402 1.935l-71.67 45.674a14.898 14.898 0 0 0-6.73 9.975a15.9 15.9 0 0 0 2.709 12.012a17.156 17.156 0 0 0 18.404 6.832a15.841 15.841 0 0 0 4.402-1.935l27.345-17.427a52.147 52.147 0 0 1 14.552-6.397c23.101-6.006 47.497 3.037 61.102 22.65a52.681 52.681 0 0 1 9.003 39.848a49.453 49.453 0 0 1-22.34 33.12l-71.664 45.673a52.218 52.218 0 0 1-14.563 6.398"></path></svg>
···
+7 -27
src/lib/AccountComponent.svelte
··· 12 alt="avatar of {account.displayName}" 13 src="{Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did={account.did}&cid={account.avatarCid}" 14 /> 15 {/if} 16 - <div id="accountName"> 17 - {account.displayName || account.handle || account.did} 18 - </div> 19 </div> 20 </a> 21 22 <style> 23 - #accountContainer { 24 - display: flex; 25 - text-align: start; 26 - align-items: center; 27 - background-color: #12082b; 28 - padding: 0px; 29 - margin-bottom: 15px; 30 - border: 1px solid #8054f0; 31 - } 32 - #accountName { 33 - margin-left: 10px; 34 - font-size: 0.9em; 35 36 - /* replace overflow with ellipsis */ 37 - overflow: hidden; 38 - text-overflow: ellipsis; 39 - white-space: nowrap; 40 - max-width: 80%; 41 - } 42 - #avatar { 43 - width: 50px; 44 - height: 50px; 45 - margin: 0px; 46 - border-right: #8054f0 1px solid; 47 - } 48 </style>
··· 12 alt="avatar of {account.displayName}" 13 src="{Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did={account.did}&cid={account.avatarCid}" 14 /> 15 + <div id="accountName"> 16 + {account.displayName || account.handle || account.did} 17 + </div> 18 + {:else} 19 + <div id="accountName" class="no-avatar"> 20 + {account.displayName || account.handle || account.did} 21 + </div> 22 {/if} 23 </div> 24 </a> 25 26 <style> 27 28 </style>
+15 -144
src/lib/PostComponent.svelte
··· 2 import { Post } from "./pdsfetch"; 3 import { Config } from "../../config"; 4 import { onMount } from "svelte"; 5 6 let { post }: { post: Post } = $props(); 7 ··· 70 > 71 <p id="handle"> 72 <a href="{Config.FRONTEND_URL}/profile/{post.authorHandle}" 73 - >{post.authorHandle}</a 74 > 75 76 <a 77 id="postLink" 78 href="{Config.FRONTEND_URL}/profile/{post.authorDid}/post/{post.recordName}" 79 - >{post.timenotstamp}</a 80 > 81 </p> 82 </div> ··· 110 <div id="carouselControls"> 111 <button 112 id="prevBtn" 113 - on:click={prevImage} 114 disabled={currentImageIndex === 0}>โ†</button 115 > 116 <div id="carouselIndicators"> ··· 122 </div> 123 <button 124 id="nextBtn" 125 - on:click={nextImage} 126 disabled={currentImageIndex === post.imagesCid.length - 1} 127 >โ†’</button 128 > ··· 131 </div> 132 {/if} 133 {#if post.videosLinkCid} 134 <video 135 id="embedVideo" 136 src="{Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did={post.authorDid}&cid={post.videosLinkCid}" 137 controls 138 ></video> 139 {/if} 140 </div> 141 </div> 142 143 <style> 144 - a:hover { 145 - text-decoration: underline; 146 - } 147 - #postContainer { 148 - display: flex; 149 - flex-direction: column; 150 - border: 1px solid #8054f0; 151 - background-color: black; 152 - margin-bottom: 15px; 153 - overflow-wrap: break-word; 154 - } 155 - #postHeader { 156 - display: flex; 157 - flex-direction: row; 158 - align-items: center; 159 - justify-content: start; 160 - background-color: #1f1145; 161 - padding: 0px 0px; 162 - height: fit-content; 163 - border-bottom: 1px solid #8054f0; 164 - font-weight: bold; 165 - overflow-wrap: break-word; 166 - height: 60px; 167 - } 168 - #displayName { 169 - color: white; 170 - font-size: 1.2em; 171 - padding: 0; 172 - margin: 0; 173 - } 174 - #handle { 175 - color: #8054f0; 176 - font-size: 0.8em; 177 - padding: 0; 178 - margin: 0; 179 - } 180 181 - #postLink { 182 - color: #8054f0; 183 - font-size: 0.8em; 184 - padding: 0; 185 - margin: 0; 186 - } 187 - #postContent { 188 - display: flex; 189 - text-align: start; 190 - flex-direction: column; 191 - padding: 10px; 192 - background-color: #0d0620; 193 - color: white; 194 - overflow-wrap: break-word; 195 - } 196 - #replyingText { 197 - font-size: 0.7em; 198 - margin: 0; 199 - padding: 0; 200 - padding-bottom: 5px; 201 - } 202 - #quotingText { 203 - font-size: 0.7em; 204 - margin: 0; 205 - padding: 0; 206 - padding-bottom: 5px; 207 - } 208 - #postText { 209 - margin: 0; 210 - padding: 0; 211 - } 212 - #headerText { 213 - margin-left: 10px; 214 - font-size: 0.9em; 215 - text-align: start; 216 - overflow-wrap: break-word; 217 - overflow: hidden; 218 - } 219 - #avatar { 220 - height: 100%; 221 - margin: 0px; 222 - margin-left: 0px; 223 - border-right: #8054f0 1px solid; 224 - } 225 - #carouselContainer { 226 - position: relative; 227 - width: 100%; 228 - margin-top: 10px; 229 - display: flex; 230 - flex-direction: column; 231 - align-items: center; 232 - } 233 - #carouselControls { 234 - display: flex; 235 - justify-content: space-between; 236 - align-items: center; 237 - width: 100%; 238 - max-width: 500px; 239 - margin-top: 5px; 240 - } 241 - #carouselIndicators { 242 - display: flex; 243 - gap: 5px; 244 - } 245 - .indicator { 246 - width: 8px; 247 - height: 8px; 248 - background-color: #4a4a4a; 249 - } 250 - .indicator.active { 251 - background-color: #8054f0; 252 - } 253 - #prevBtn, 254 - #nextBtn { 255 - background-color: rgba(31, 17, 69, 0.7); 256 - color: white; 257 - border: 1px solid #8054f0; 258 - width: 30px; 259 - height: 30px; 260 - cursor: pointer; 261 - display: flex; 262 - align-items: center; 263 - justify-content: center; 264 - } 265 - #prevBtn:disabled, 266 - #nextBtn:disabled { 267 - opacity: 0.5; 268 - cursor: not-allowed; 269 - } 270 - #embedVideo { 271 - width: 100%; 272 - max-width: 500px; 273 - margin-top: 10px; 274 - align-self: center; 275 - } 276 - 277 - #embedImages { 278 - min-width: min(100%, 500px); 279 - max-width: min(100%, 500px); 280 - max-height: 500px; 281 - object-fit: contain; 282 - 283 - margin: 0; 284 - } 285 </style>
··· 2 import { Post } from "./pdsfetch"; 3 import { Config } from "../../config"; 4 import { onMount } from "svelte"; 5 + import moment from "moment"; 6 7 let { post }: { post: Post } = $props(); 8 ··· 71 > 72 <p id="handle"> 73 <a href="{Config.FRONTEND_URL}/profile/{post.authorHandle}" 74 + >@{post.authorHandle}</a 75 > 76 77 <a 78 id="postLink" 79 href="{Config.FRONTEND_URL}/profile/{post.authorDid}/post/{post.recordName}" 80 + >{moment(post.timenotstamp).isBefore(moment().subtract(1, "month")) 81 + ? moment(post.timenotstamp).format("MMM D, YYYY") 82 + : moment(post.timenotstamp).fromNow()}</a 83 > 84 </p> 85 </div> ··· 113 <div id="carouselControls"> 114 <button 115 id="prevBtn" 116 + onclick={prevImage} 117 disabled={currentImageIndex === 0}>โ†</button 118 > 119 <div id="carouselIndicators"> ··· 125 </div> 126 <button 127 id="nextBtn" 128 + onclick={nextImage} 129 disabled={currentImageIndex === post.imagesCid.length - 1} 130 >โ†’</button 131 > ··· 134 </div> 135 {/if} 136 {#if post.videosLinkCid} 137 + <!-- svelte-ignore a11y_media_has_caption --> 138 <video 139 id="embedVideo" 140 src="{Config.PDS_URL}/xrpc/com.atproto.sync.getBlob?did={post.authorDid}&cid={post.videosLinkCid}" 141 controls 142 ></video> 143 {/if} 144 + {#if post.gifLink} 145 + <img 146 + id="embedVideo" 147 + src="{post.gifLink}" 148 + alt="Post GIF" 149 + /> 150 + {/if} 151 </div> 152 </div> 153 154 <style> 155 156 </style>
+192 -83
src/lib/pdsfetch.ts
··· 13 WebDidDocumentResolver, 14 } from "@atcute/identity-resolver"; 15 import { Config } from "../../config"; 16 // import { ComAtprotoRepoListRecords.Record } from "@atcute/client/lexicons"; 17 // import { AppBskyFeedPost } from "@atcute/client/lexicons"; 18 // import { AppBskyActorDefs } from "@atcute/client/lexicons"; 19 20 interface AccountMetadata { 21 - did: string; 22 displayName: string; 23 handle: string; 24 avatarCid: string | null; 25 } 26 interface atUriObject { 27 repo: string; 28 collection: string; ··· 42 replyingUri: atUriObject | null; 43 imagesCid: string[] | null; 44 videosLinkCid: string | null; 45 46 constructor( 47 record: ComAtprotoRepoListRecords.Record, 48 account: AccountMetadata, 49 ) { 50 this.postCid = record.cid; 51 - this.recordName = record.uri.split("/").slice(-1)[0]; 52 this.authorDid = account.did; 53 this.authorAvatarCid = account.avatarCid; 54 this.authorHandle = account.handle; ··· 65 this.quotingUri = null; 66 this.imagesCid = null; 67 this.videosLinkCid = null; 68 switch (post.embed?.$type) { 69 case "app.bsky.embed.images": 70 - this.imagesCid = post.embed.images.map((imageRecord: any) => 71 - imageRecord.image.ref.$link 72 ); 73 break; 74 case "app.bsky.embed.video": ··· 81 this.quotingUri = processAtUri(post.embed.record.record.uri); 82 switch (post.embed.media.$type) { 83 case "app.bsky.embed.images": 84 - this.imagesCid = post.embed.media.images.map((imageRecord) => 85 - imageRecord.image.ref.$link 86 ); 87 88 break; ··· 90 this.videosLinkCid = post.embed.media.video.ref.$link; 91 92 break; 93 } 94 break; 95 } ··· 111 }), 112 }); 113 114 - const getDidsFromPDS = async () => { 115 const { data } = await rpc.get("com.atproto.sync.listRepos", { 116 params: {}, 117 }); 118 - return data.repos.map((repo: any) => (repo.did)); 119 }; 120 - const getAccountMetadata = async (did: `did:${string}:${string}`) => { 121 - // gonna assume self exists in the app.bsky.actor.profile 122 - try { 123 - const { data } = await rpc.get("com.atproto.repo.getRecord", { 124 - params: { 125 - repo: did, 126 - collection: "app.bsky.actor.profile", 127 - rkey: "self", 128 - }, 129 - }); 130 - const value = data.value as AppBskyActorProfile.Record; 131 - const handle = await blueskyHandleFromDid(did); 132 const account: AccountMetadata = { 133 did: did, 134 - handle: handle, 135 - displayName: value.displayName || "", 136 avatarCid: null, 137 }; 138 - if (value.avatar) { 139 - account.avatarCid = value.avatar.ref["$link"]; 140 - } 141 - return account; 142 } 143 - catch (e) { 144 - console.error(`Error fetching metadata for ${did}:`, e); 145 - return { 146 - did: "error", 147 - displayName: "", 148 - avatarCid: null, 149 - }; 150 } 151 }; 152 153 - const getAllMetadataFromPds = async () => { 154 const dids = await getDidsFromPDS(); 155 const metadata = await Promise.all( 156 dids.map(async (repo: `did:${string}:${string}`) => { 157 return await getAccountMetadata(repo); 158 }), 159 ); 160 - return metadata.filter(account => account.did !== "error"); 161 - }; 162 - 163 - const fetchPosts = async (did: string) => { 164 - try { 165 - const { data } = await rpc.get("com.atproto.repo.listRecords", { 166 - params: { 167 - repo: did as At.Identifier, 168 - collection: "app.bsky.feed.post", 169 - limit: Config.MAX_POSTS_PER_USER, 170 - }, 171 - }); 172 - return { 173 - records: data.records as ComAtprotoRepoListRecords.Record[], 174 - did: did, 175 - error: false 176 - }; 177 - } catch (e) { 178 - console.error(`Error fetching posts for ${did}:`, e); 179 - return { 180 - records: [], 181 - did: did, 182 - error: true 183 - }; 184 - } 185 }; 186 187 const identityResolve = async (did: At.Did) => { ··· 217 } 218 }; 219 220 - const fetchAllPosts = async () => { 221 - const users: AccountMetadata[] = await getAllMetadataFromPds(); 222 - const postRecords = await Promise.all( 223 - users.map(async (metadata: AccountMetadata) => 224 - await fetchPosts(metadata.did) 225 - ), 226 - ); 227 - const validPostRecords = postRecords.filter(record => !record.error); 228 - const posts: Post[] = validPostRecords.flatMap((userFetch) => 229 - userFetch.records.map((record) => { 230 - const user = users.find((user: AccountMetadata) => 231 - user.did == userFetch.did 232 ); 233 - if (!user) { 234 - throw new Error(`User with DID ${userFetch.did} not found`); 235 } 236 - return new Post(record, user); 237 - }) 238 ); 239 - posts.sort((a, b) => b.timestamp - a.timestamp); 240 - return posts; 241 - }; 242 243 - const testApiCall = async () => { 244 - const { data } = await rpc.get("com.atproto.sync.listRepos", { 245 - params: {}, 246 }); 247 - console.log(data); 248 }; 249 - export { fetchAllPosts, getAllMetadataFromPds, Post }; 250 export type { AccountMetadata };
··· 13 WebDidDocumentResolver, 14 } from "@atcute/identity-resolver"; 15 import { Config } from "../../config"; 16 + import { Mutex } from "mutex-ts" 17 // import { ComAtprotoRepoListRecords.Record } from "@atcute/client/lexicons"; 18 // import { AppBskyFeedPost } from "@atcute/client/lexicons"; 19 // import { AppBskyActorDefs } from "@atcute/client/lexicons"; 20 21 interface AccountMetadata { 22 + did: At.Did; 23 displayName: string; 24 handle: string; 25 avatarCid: string | null; 26 + currentCursor?: string; 27 } 28 + 29 + let accountsMetadata: AccountMetadata[] = []; 30 + 31 interface atUriObject { 32 repo: string; 33 collection: string; ··· 47 replyingUri: atUriObject | null; 48 imagesCid: string[] | null; 49 videosLinkCid: string | null; 50 + gifLink: string | null; 51 52 constructor( 53 record: ComAtprotoRepoListRecords.Record, 54 account: AccountMetadata, 55 ) { 56 this.postCid = record.cid; 57 + this.recordName = processAtUri(record.uri).rkey; 58 this.authorDid = account.did; 59 this.authorAvatarCid = account.avatarCid; 60 this.authorHandle = account.handle; ··· 71 this.quotingUri = null; 72 this.imagesCid = null; 73 this.videosLinkCid = null; 74 + this.gifLink = null; 75 switch (post.embed?.$type) { 76 case "app.bsky.embed.images": 77 + this.imagesCid = post.embed.images.map( 78 + (imageRecord: any) => imageRecord.image.ref.$link, 79 ); 80 break; 81 case "app.bsky.embed.video": ··· 88 this.quotingUri = processAtUri(post.embed.record.record.uri); 89 switch (post.embed.media.$type) { 90 case "app.bsky.embed.images": 91 + this.imagesCid = post.embed.media.images.map( 92 + (imageRecord) => imageRecord.image.ref.$link, 93 ); 94 95 break; ··· 97 this.videosLinkCid = post.embed.media.video.ref.$link; 98 99 break; 100 + } 101 + break; 102 + case "app.bsky.embed.external": // assuming that external embeds are gifs for now 103 + if (post.embed.external.uri.includes(".gif")) { 104 + this.gifLink = post.embed.external.uri; 105 } 106 break; 107 } ··· 123 }), 124 }); 125 126 + const getDidsFromPDS = async (): Promise<At.Did[]> => { 127 const { data } = await rpc.get("com.atproto.sync.listRepos", { 128 params: {}, 129 }); 130 + return data.repos.map((repo: any) => repo.did) as At.Did[]; 131 }; 132 + const getAccountMetadata = async ( 133 + did: `did:${string}:${string}`, 134 + ) => { 135 const account: AccountMetadata = { 136 did: did, 137 + handle: "", // Guaranteed to be filled out later 138 + displayName: "", 139 avatarCid: null, 140 }; 141 + 142 + try { 143 + const { data } = await rpc.get("com.atproto.repo.getRecord", { 144 + params: { 145 + repo: did, 146 + collection: "app.bsky.actor.profile", 147 + rkey: "self", 148 + }, 149 + }); 150 + const value = data.value as AppBskyActorProfile.Record; 151 + account.displayName = value.displayName || ""; 152 + if (value.avatar) { 153 + account.avatarCid = value.avatar.ref["$link"]; 154 + } 155 + } catch (e) { 156 + console.warn(`Error fetching profile for ${did}:`, e); 157 } 158 + 159 + try { 160 + account.handle = await blueskyHandleFromDid(did); 161 + } catch (e) { 162 + console.error(`Error fetching handle for ${did}:`, e); 163 + return null; 164 } 165 + 166 + return account; 167 }; 168 169 + const getAllMetadataFromPds = async (): Promise<AccountMetadata[]> => { 170 const dids = await getDidsFromPDS(); 171 const metadata = await Promise.all( 172 dids.map(async (repo: `did:${string}:${string}`) => { 173 return await getAccountMetadata(repo); 174 }), 175 ); 176 + return metadata.filter((account) => account !== null) as AccountMetadata[]; 177 }; 178 179 const identityResolve = async (did: At.Did) => { ··· 209 } 210 }; 211 212 + interface PostsAcc { 213 + posts: ComAtprotoRepoListRecords.Record[]; 214 + account: AccountMetadata; 215 + } 216 + const getCutoffDate = (postAccounts: PostsAcc[]) => { 217 + const now = Date.now(); 218 + let cutoffDate: Date | null = null; 219 + postAccounts.forEach((postAcc) => { 220 + const latestPost = new Date( 221 + (postAcc.posts[postAcc.posts.length - 1].value as AppBskyFeedPost.Record) 222 + .createdAt, 223 + ); 224 + if (!cutoffDate) { 225 + cutoffDate = latestPost; 226 + } else { 227 + if (latestPost > cutoffDate) { 228 + cutoffDate = latestPost; 229 + } 230 + } 231 + }); 232 + if (cutoffDate) { 233 + return cutoffDate; 234 + } else { 235 + return new Date(now); 236 + } 237 + }; 238 + 239 + const filterPostsByDate = (posts: PostsAcc[], cutoffDate: Date) => { 240 + // filter posts for each account that are older than the cutoff date and save the cursor of the last post included 241 + const filteredPosts: PostsAcc[] = posts.map((postAcc) => { 242 + const filtered = postAcc.posts.filter((post) => { 243 + const postDate = new Date( 244 + (post.value as AppBskyFeedPost.Record).createdAt, 245 + ); 246 + return postDate >= cutoffDate; 247 + }); 248 + if (filtered.length > 0) { 249 + postAcc.account.currentCursor = processAtUri(filtered[filtered.length - 1].uri).rkey; 250 + } 251 + return { 252 + posts: filtered, 253 + account: postAcc.account, 254 + }; 255 + }); 256 + return filteredPosts; 257 + }; 258 + 259 + const postsMutex = new Mutex(); 260 + // nightmare function. However it works so I am not touching it 261 + const getNextPosts = async () => { 262 + const release = await postsMutex.obtain(); 263 + if (!accountsMetadata.length) { 264 + accountsMetadata = await getAllMetadataFromPds(); 265 + } 266 + 267 + const postsAcc: PostsAcc[] = await Promise.all( 268 + accountsMetadata.map(async (account) => { 269 + const posts = await fetchPostsForUser( 270 + account.did, 271 + account.currentCursor || null, 272 ); 273 + if (posts) { 274 + return { 275 + posts: posts, 276 + account: account, 277 + }; 278 + } else { 279 + return { 280 + posts: [], 281 + account: account, 282 + }; 283 } 284 + }), 285 + ); 286 + const recordsFiltered = postsAcc.filter((postAcc) => 287 + postAcc.posts.length > 0 288 + ); 289 + const cutoffDate = getCutoffDate(recordsFiltered); 290 + const recordsCutoff = filterPostsByDate(recordsFiltered, cutoffDate); 291 + // update the accountMetadata with the new cursor 292 + accountsMetadata = accountsMetadata.map((account) => { 293 + const postAcc = recordsCutoff.find( 294 + (postAcc) => postAcc.account.did == account.did, 295 + ); 296 + if (postAcc) { 297 + account.currentCursor = postAcc.account.currentCursor; 298 + } 299 + return account; 300 + } 301 ); 302 + // throw the records in a big single array 303 + let records = recordsCutoff.flatMap((postAcc) => postAcc.posts); 304 + // sort the records by timestamp 305 + records = records.sort((a, b) => { 306 + const aDate = new Date( 307 + (a.value as AppBskyFeedPost.Record).createdAt, 308 + ).getTime(); 309 + const bDate = new Date( 310 + (b.value as AppBskyFeedPost.Record).createdAt, 311 + ).getTime(); 312 + return bDate - aDate; 313 + }); 314 + // filter out posts that are in the future 315 + if (!Config.SHOW_FUTURE_POSTS) { 316 + const now = Date.now(); 317 + records = records.filter((post) => { 318 + const postDate = new Date( 319 + (post.value as AppBskyFeedPost.Record).createdAt, 320 + ).getTime(); 321 + return postDate <= now; 322 + }); 323 + } 324 325 + const newPosts = records.map((record) => { 326 + const account = accountsMetadata.find( 327 + (account) => account.did == processAtUri(record.uri).repo, 328 + ); 329 + if (!account) { 330 + throw new Error( 331 + `Account with DID ${processAtUri(record.uri).repo} not found`, 332 + ); 333 + } 334 + return new Post(record, account); 335 }); 336 + // release the mutex 337 + release(); 338 + return newPosts; 339 }; 340 + 341 + const fetchPostsForUser = async (did: At.Did, cursor: string | null) => { 342 + try { 343 + const { data } = await rpc.get("com.atproto.repo.listRecords", { 344 + params: { 345 + repo: did as At.Identifier, 346 + collection: "app.bsky.feed.post", 347 + limit: Config.MAX_POSTS, 348 + cursor: cursor || undefined, 349 + }, 350 + }); 351 + return data.records as ComAtprotoRepoListRecords.Record[]; 352 + } catch (e) { 353 + console.error(`Error fetching posts for ${did}:`, e); 354 + return null; 355 + } 356 + }; 357 + 358 + export { getAllMetadataFromPds, getNextPosts, Post }; 359 export type { AccountMetadata };
+6 -6
src/main.ts
··· 1 - import { mount } from 'svelte' 2 - import './app.css' 3 - import App from './App.svelte' 4 5 const app = mount(App, { 6 - target: document.getElementById('app')!, 7 - }) 8 9 - export default app
··· 1 + import { mount } from "svelte"; 2 + import "./app.css"; 3 + import App from "./App.svelte"; 4 5 const app = mount(App, { 6 + target: document.getElementById("app")!, 7 + }); 8 9 + export default app;
+2 -2
svelte.config.js
··· 1 - import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' 2 3 export default { 4 // Consult https://svelte.dev/docs#compile-time-svelte-preprocess 5 // for more information about preprocessors 6 preprocess: vitePreprocess(), 7 - }
··· 1 + import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; 2 3 export default { 4 // Consult https://svelte.dev/docs#compile-time-svelte-preprocess 5 // for more information about preprocessors 6 preprocess: vitePreprocess(), 7 + };
+423
themes/default/theme.css
···
··· 1 + /* Modern Theme for pds-dash */ 2 + 3 + :root { 4 + /* Modern color palette */ 5 + --primary-h: 243; 6 + --link-color: hsl(var(--primary-h), 73%, 59%); 7 + --link-hover-color: #4338ca; 8 + --time-color: #8b5cf6; 9 + --background-color: #f8fafc; 10 + --header-background-color: #ffffff; 11 + --content-background-color: #ffffff; 12 + --text-color: #111827; 13 + --text-secondary-color: #4b5563; 14 + --border-color: #e2e8f0; 15 + --indicator-inactive-color: #cbd5e1; 16 + --indicator-active-color: #6366f1; 17 + 18 + /* Modern shadows */ 19 + --button-hover: #f3f4f6; 20 + } 21 + 22 + 23 + body { 24 + margin: 0; 25 + display: flex; 26 + place-items: center; 27 + min-width: 320px; 28 + min-height: 100vh; 29 + background-color: var(--background-color); 30 + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif; 31 + font-size: 18px; 32 + line-height: 1.5; 33 + color: var(--text-color); 34 + border-color: var(--border-color); 35 + overflow-wrap: break-word; 36 + word-break: break-word; 37 + hyphens: none; 38 + } 39 + 40 + a { 41 + font-weight: 500; 42 + color: var(--link-color); 43 + text-decoration: none; 44 + transition: color 0.15s ease; 45 + } 46 + a:hover { 47 + color: var(--link-hover-color); 48 + } 49 + 50 + h1 { 51 + font-size: 2.5em; 52 + line-height: 1.2; 53 + font-weight: 700; 54 + } 55 + 56 + #app { 57 + max-width: 1400px; 58 + width: 100%; 59 + margin: 0 auto; 60 + padding: 0; 61 + text-align: center; 62 + } 63 + 64 + /* Post Component */ 65 + #postContainer { 66 + display: flex; 67 + flex-direction: column; 68 + border-radius: 12px; 69 + border: 1px solid var(--border-color); 70 + background-color: var(--content-background-color); 71 + margin-bottom: 20px; 72 + overflow-wrap: break-word; 73 + overflow: hidden; 74 + box-shadow: var(--card-shadow); 75 + transition: transform 0.2s ease, box-shadow 0.2s ease; 76 + } 77 + 78 + #postContainer:hover { 79 + transform: translateY(-2px); 80 + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); 81 + } 82 + 83 + #postHeader { 84 + display: flex; 85 + flex-direction: row; 86 + align-items: center; 87 + justify-content: start; 88 + background-color: var(--header-background-color); 89 + padding: 12px 16px; 90 + height: 60px; 91 + border-bottom: 1px solid var(--border-color); 92 + font-weight: 600; 93 + overflow-wrap: break-word; 94 + } 95 + 96 + #displayName { 97 + display: block; 98 + color: var(--text-color); 99 + font-size: 1.1em; 100 + padding: 0; 101 + margin: 0 0 2px 0; 102 + text-overflow: ellipsis; 103 + overflow: hidden; 104 + white-space: nowrap; 105 + width: 100%; 106 + letter-spacing: -0.01em; 107 + } 108 + 109 + #handle { 110 + display: flex; 111 + align-items: center; 112 + color: #6b7280; 113 + font-size: 0.85em; 114 + font-weight: 400; 115 + padding: 0; 116 + margin: 0; 117 + gap: 8px; 118 + } 119 + 120 + #postLink { 121 + color: var(--time-color); 122 + font-size: 0.85em; 123 + padding: 0; 124 + margin: 0; 125 + opacity: 0.9; 126 + } 127 + 128 + #postContent { 129 + display: flex; 130 + text-align: start; 131 + flex-direction: column; 132 + padding: 16px; 133 + background-color: var(--content-background-color); 134 + color: var(--text-color); 135 + overflow-wrap: break-word; 136 + white-space: pre-line; 137 + line-height: 1.6; 138 + } 139 + 140 + #replyingText, #quotingText { 141 + font-size: 0.8em; 142 + margin: 0; 143 + padding: 0 0 10px 0; 144 + color: #6b7280; 145 + } 146 + 147 + #postText { 148 + margin: 0 0 8px 0; 149 + padding: 0; 150 + overflow-wrap: break-word; 151 + word-break: break-word; 152 + hyphens: none; 153 + font-size: 1.05em; 154 + } 155 + 156 + #headerText { 157 + margin-left: 12px; 158 + font-size: 0.9em; 159 + text-align: start; 160 + word-break: break-word; 161 + max-width: 80%; 162 + max-height: 95%; 163 + overflow: hidden; 164 + align-self: flex-start; 165 + margin-top: auto; 166 + margin-bottom: auto; 167 + } 168 + 169 + #carouselContainer { 170 + position: relative; 171 + width: 100%; 172 + margin-top: 12px; 173 + display: flex; 174 + flex-direction: column; 175 + align-items: center; 176 + border-radius: 8px; 177 + overflow: hidden; 178 + } 179 + 180 + #carouselControls { 181 + display: flex; 182 + justify-content: space-between; 183 + align-items: center; 184 + width: 100%; 185 + max-width: 500px; 186 + margin-top: 10px; 187 + } 188 + 189 + #carouselIndicators { 190 + display: flex; 191 + gap: 6px; 192 + } 193 + 194 + .indicator { 195 + width: 6px; 196 + height: 6px; 197 + background-color: var(--indicator-inactive-color); 198 + border-radius: 50%; 199 + transition: background-color 0.2s ease, transform 0.2s ease; 200 + } 201 + 202 + .indicator.active { 203 + background-color: var(--indicator-active-color); 204 + transform: scale(1.3); 205 + } 206 + 207 + #prevBtn, 208 + #nextBtn { 209 + background-color: var(--button-bg); 210 + color: var(--text-color); 211 + border: 1px solid var(--border-color); 212 + width: 32px; 213 + height: 32px; 214 + cursor: pointer; 215 + display: flex; 216 + align-items: center; 217 + justify-content: center; 218 + border-radius: 50%; 219 + transition: background-color 0.15s ease, transform 0.15s ease; 220 + font-size: 16px; 221 + } 222 + 223 + #prevBtn:hover:not(:disabled), 224 + #nextBtn:hover:not(:disabled) { 225 + background-color: var(--button-hover); 226 + transform: scale(1.05); 227 + } 228 + 229 + #prevBtn:disabled, 230 + #nextBtn:disabled { 231 + opacity: 0.4; 232 + cursor: not-allowed; 233 + } 234 + 235 + #embedVideo { 236 + width: 100%; 237 + max-width: 500px; 238 + margin-top: 12px; 239 + align-self: center; 240 + border-radius: 8px; 241 + overflow: hidden; 242 + } 243 + 244 + #embedImages { 245 + min-width: min(100%, 500px); 246 + max-width: min(100%, 500px); 247 + max-height: 500px; 248 + object-fit: contain; 249 + margin: 0; 250 + border-radius: 8px; 251 + } 252 + 253 + /* Account Component */ 254 + #accountContainer { 255 + display: flex; 256 + text-align: start; 257 + align-items: center; 258 + background-color: var(--content-background-color); 259 + padding: 12px; 260 + margin-bottom: 15px; 261 + border: 1px solid var(--border-color); 262 + border-radius: 12px; 263 + transition: background-color 0.15s ease; 264 + } 265 + 266 + #accountContainer:hover { 267 + background-color: var(--hover-bg); 268 + } 269 + 270 + #accountName { 271 + margin-left: 12px; 272 + font-size: 0.95em; 273 + max-width: 80%; 274 + overflow: hidden; 275 + text-overflow: ellipsis; 276 + white-space: nowrap; 277 + font-weight: 500; 278 + } 279 + 280 + #avatar { 281 + width: 48px; 282 + height: 48px; 283 + margin: 0; 284 + object-fit: cover; 285 + border-radius: 50%; 286 + border: 2px solid white; 287 + box-shadow: 0 1px 3px rgba(0,0,0,0.1); 288 + } 289 + 290 + /* App.Svelte Layout */ 291 + #Content { 292 + display: flex; 293 + width: 100%; 294 + height: 100%; 295 + flex-direction: row; 296 + justify-content: space-between; 297 + align-items: center; 298 + background-color: var(--background-color); 299 + color: var(--text-color); 300 + gap: 24px; 301 + } 302 + 303 + #Feed { 304 + overflow-y: auto; 305 + width: 65%; 306 + height: 100vh; 307 + padding-right: 16px; 308 + align-self: flex-start; 309 + } 310 + 311 + #spacer { 312 + padding: 0; 313 + margin: 0; 314 + height: 10vh; 315 + width: 100%; 316 + } 317 + 318 + #Account { 319 + width: 35%; 320 + display: flex; 321 + flex-direction: column; 322 + border: 1px solid var(--border-color); 323 + background-color: var(--content-background-color); 324 + max-height: 80vh; 325 + padding: 24px; 326 + margin-left: 16px; 327 + border-radius: 12px; 328 + box-shadow: var(--card-shadow); 329 + } 330 + 331 + #accountsList { 332 + display: flex; 333 + flex-direction: column; 334 + overflow-y: auto; 335 + height: 100%; 336 + width: 100%; 337 + padding: 8px 0; 338 + margin: 0; 339 + } 340 + 341 + #Header { 342 + text-align: center; 343 + font-size: 1.8em; 344 + margin-bottom: 16px; 345 + font-weight: 700; 346 + background: linear-gradient(to right, var(--link-color), #8b5cf6); 347 + -webkit-background-clip: text; 348 + -webkit-text-fill-color: transparent; 349 + background-clip: text; 350 + } 351 + 352 + /* Mobile Styles */ 353 + @media screen and (max-width: 768px) { 354 + #Content { 355 + flex-direction: column; 356 + width: auto; 357 + padding: 12px; 358 + margin-top: 0; 359 + } 360 + 361 + #Account { 362 + width: calc(100% - 32px); 363 + padding: 16px; 364 + margin-bottom: 20px; 365 + margin-left: 0; 366 + margin-right: 0; 367 + height: auto; 368 + order: -1; 369 + } 370 + 371 + #Feed { 372 + width: 100%; 373 + margin: 0; 374 + padding: 0; 375 + overflow-y: visible; 376 + } 377 + 378 + #spacer { 379 + height: 5vh; 380 + } 381 + 382 + body { 383 + font-size: 16px; 384 + } 385 + 386 + #postHeader { 387 + padding: 10px; 388 + height: auto; 389 + min-height: 50px; 390 + } 391 + } 392 + 393 + /* Scrollbar Styles */ 394 + ::-webkit-scrollbar { 395 + width: 0px; 396 + background: transparent; 397 + padding: 0; 398 + margin: 0; 399 + } 400 + ::-webkit-scrollbar-thumb { 401 + background: transparent; 402 + border-radius: 0; 403 + } 404 + ::-webkit-scrollbar-track { 405 + background: transparent; 406 + border-radius: 0; 407 + } 408 + ::-webkit-scrollbar-corner { 409 + background: transparent; 410 + border-radius: 0; 411 + } 412 + ::-webkit-scrollbar-button { 413 + background: transparent; 414 + border-radius: 0; 415 + } 416 + 417 + * { 418 + scrollbar-width: none; 419 + scrollbar-color: transparent transparent; 420 + -ms-overflow-style: none; /* IE and Edge */ 421 + -webkit-overflow-scrolling: touch; 422 + -webkit-scrollbar: none; /* Safari */ 423 + }
+375
themes/express/theme.css
···
··· 1 + @import url("https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap"); 2 + 3 + :root { 4 + /* Color overrides, edit to whatever you want */ 5 + --primary-h: 341; /* Hue */ 6 + --background-color: hsl(var(--primary-h), 62%, 30%); 7 + --text-color: hsl(var(--primary-h), 69%, 18%); 8 + --link-color: hsl(var(--primary-h), 100%, 20%); 9 + --link-hover-color: hsl(var(--primary-h), 20%, 20%); 10 + --border-color: hsl(var(--primary-h), 59%, 52%); 11 + --content-background-color: hsl(var(--primary-h), 97%, 73%); 12 + 13 + --header-background-color: hsl(var(--primary-h), 97%, 73%); 14 + --indicator-inactive-color: #4a4a4a; 15 + --indicator-active-color: var(--border-color); 16 + } 17 + 18 + a { 19 + font-weight: 500; 20 + color: var(--link-color); 21 + text-decoration: inherit; 22 + } 23 + a:hover { 24 + color: var(--link-hover-color); 25 + text-decoration: underline; 26 + } 27 + 28 + body { 29 + margin: 0; 30 + display: flex; 31 + place-items: center; 32 + min-width: 320px; 33 + min-height: 100vh; 34 + background-color: var(--background-color); 35 + font-family: "Share Tech Mono", monospace; 36 + font-size: 24px; 37 + color: var(--text-color); 38 + border-color: var(--border-color); 39 + overflow-wrap: break-word; 40 + word-wrap: normal; 41 + word-break: break-word; 42 + hyphens: none; 43 + } 44 + 45 + h1 { 46 + font-size: 3.2em; 47 + line-height: 1.1; 48 + } 49 + 50 + #app { 51 + max-width: 1400px; 52 + width: 100%; 53 + margin: 0; 54 + padding: 0; 55 + margin-left: auto; 56 + margin-right: auto; 57 + text-align: center; 58 + } 59 + 60 + /* Post Component */ 61 + a:hover { 62 + text-decoration: underline; 63 + } 64 + #postContainer { 65 + display: flex; 66 + flex-direction: column; 67 + border: 4px solid var(--border-color); 68 + background-color: var(--background-color); 69 + margin-bottom: 15px; 70 + overflow-wrap: break-word; 71 + box-shadow: var(--border-color) 10px 10px; 72 + } 73 + #postHeader { 74 + display: flex; 75 + flex-direction: row; 76 + align-items: center; 77 + justify-content: start; 78 + background-color: var(--header-background-color); 79 + padding: 0px 0px; 80 + height: fit-content; 81 + 82 + font-weight: bold; 83 + overflow-wrap: break-word; 84 + height: 64px; 85 + } 86 + #displayName { 87 + display: block; 88 + color: var(--text-color); 89 + font-size: 1.2em; 90 + padding: 0; 91 + margin: 0; 92 + overflow-wrap: normal; 93 + word-wrap: break-word; 94 + word-break: break-word; 95 + text-overflow: ellipsis; 96 + overflow: hidden; 97 + white-space: nowrap; 98 + width: 100%; 99 + } 100 + #handle { 101 + display: block; 102 + color: var(--border-color); 103 + font-size: 0.8em; 104 + padding: 0; 105 + margin: 0; 106 + } 107 + 108 + #postLink { 109 + color: var(--link-hover-color); 110 + font-size: 0.8em; 111 + padding: 0; 112 + margin: 0; 113 + } 114 + #postContent { 115 + display: flex; 116 + text-align: start; 117 + flex-direction: column; 118 + padding: 10px; 119 + background-color: var(--content-background-color); 120 + color: var(--text-color); 121 + overflow-wrap: break-word; 122 + white-space: pre-line; 123 + } 124 + #replyingText { 125 + font-size: 0.7em; 126 + margin: 0; 127 + padding: 0; 128 + padding-bottom: 5px; 129 + } 130 + #quotingText { 131 + font-size: 0.7em; 132 + margin: 0; 133 + padding: 0; 134 + padding-bottom: 5px; 135 + } 136 + #postText { 137 + margin: 0; 138 + padding: 0; 139 + overflow-wrap: break-word; 140 + word-wrap: normal; 141 + word-break: break-word; 142 + hyphens: none; 143 + } 144 + #headerText { 145 + margin-left: 10px; 146 + font-size: 0.9em; 147 + text-align: start; 148 + word-break: break-word; 149 + max-width: 80%; 150 + max-height: 95%; 151 + overflow: hidden; 152 + align-self: flex-start; 153 + margin-top: auto; 154 + margin-bottom: auto; 155 + } 156 + #avatar { 157 + height: 30px; 158 + width: 30px; 159 + overflow: hidden; 160 + object-fit: cover; 161 + } 162 + #postContainer #avatar { 163 + height: 60px; 164 + width: 60px; 165 + border-right: var(--border-color) 4px solid; 166 + border-bottom: var(--border-color) 4px solid; 167 + } 168 + #carouselContainer { 169 + position: relative; 170 + width: 100%; 171 + margin-top: 10px; 172 + display: flex; 173 + flex-direction: column; 174 + align-items: center; 175 + } 176 + #carouselControls { 177 + display: flex; 178 + justify-content: space-between; 179 + align-items: center; 180 + width: 100%; 181 + max-width: 500px; 182 + margin-top: 5px; 183 + } 184 + #carouselIndicators { 185 + display: flex; 186 + gap: 5px; 187 + } 188 + .indicator { 189 + width: 8px; 190 + height: 8px; 191 + background-color: var(--indicator-inactive-color); 192 + } 193 + .indicator.active { 194 + background-color: var(--indicator-active-color); 195 + } 196 + #prevBtn, 197 + #nextBtn { 198 + background-color: rgba(31, 17, 69, 0.7); 199 + color: var(--text-color); 200 + border: 4px solid var(--border-color); 201 + width: 30px; 202 + height: 30px; 203 + cursor: pointer; 204 + display: flex; 205 + align-items: center; 206 + justify-content: center; 207 + } 208 + #prevBtn:disabled, 209 + #nextBtn:disabled { 210 + opacity: 0.5; 211 + cursor: not-allowed; 212 + } 213 + #embedVideo { 214 + width: 100%; 215 + max-width: 500px; 216 + margin-top: 10px; 217 + align-self: center; 218 + } 219 + 220 + #embedImages { 221 + min-width: min(100%, 500px); 222 + max-width: min(100%, 500px); 223 + max-height: 500px; 224 + object-fit: contain; 225 + 226 + margin: 0; 227 + } 228 + 229 + /* Account Component */ 230 + #accountContainer { 231 + display: flex; 232 + text-align: start; 233 + align-items: center; 234 + background-color: var(--header-background-color); 235 + padding: 0px; 236 + margin-bottom: 15px; 237 + margin-right: 4px; 238 + border: 4px solid var(--border-color); 239 + box-shadow: var(--border-color) 10px 10px; 240 + min-height: 30px; 241 + } 242 + #accountName { 243 + margin-left: 10px; 244 + font-size: 1em; 245 + max-width: 80%; 246 + 247 + /* replace overflow with ellipsis */ 248 + overflow: hidden; 249 + text-overflow: ellipsis; 250 + white-space: nowrap; 251 + } 252 + 253 + .no-avatar { 254 + margin-left: 40px !important; 255 + } 256 + 257 + /* App.Svelte */ 258 + /* desktop style */ 259 + 260 + #Content { 261 + display: flex; 262 + /* split the screen in half, left for accounts, right for posts */ 263 + width: 100%; 264 + height: 100%; 265 + flex-direction: row; 266 + justify-content: space-between; 267 + align-items: center; 268 + background-color: var(--background-color); 269 + color: var(--text-color); 270 + } 271 + #Feed { 272 + overflow-y: scroll; 273 + width: 65%; 274 + height: 100vh; 275 + padding: 20px; 276 + padding-bottom: 0; 277 + padding-top: 0; 278 + margin-top: 0; 279 + margin-bottom: 0; 280 + } 281 + #spacer { 282 + padding: 0; 283 + margin: 0; 284 + height: 10vh; 285 + width: 100%; 286 + } 287 + #Account { 288 + width: 35%; 289 + display: flex; 290 + flex-direction: column; 291 + border: 4px solid var(--border-color); 292 + background-color: var(--content-background-color); 293 + box-shadow: var(--border-color) 10px 10px; 294 + height: 80vh; 295 + padding: 20px; 296 + margin-left: 20px; 297 + } 298 + #accountsList { 299 + display: flex; 300 + flex-direction: column; 301 + overflow-y: scroll; 302 + height: 100%; 303 + width: 100%; 304 + padding: 0px; 305 + margin: 0px; 306 + } 307 + 308 + #Header { 309 + text-align: center; 310 + font-size: 2em; 311 + margin-bottom: 20px; 312 + } 313 + 314 + /* mobile style */ 315 + @media screen and (max-width: 600px) { 316 + #Content { 317 + flex-direction: column; 318 + width: auto; 319 + padding-left: 0px; 320 + padding-right: 0px; 321 + margin-top: 5%; 322 + } 323 + #Account { 324 + width: 85%; 325 + padding-left: 5%; 326 + padding-right: 5%; 327 + margin-bottom: 20px; 328 + margin-left: 5%; 329 + margin-right: 5%; 330 + height: auto; 331 + } 332 + #Feed { 333 + width: 95%; 334 + margin: 0px; 335 + margin-left: 10%; 336 + margin-right: 10%; 337 + padding: 0px; 338 + overflow-y: visible; 339 + } 340 + 341 + #spacer { 342 + height: 0; 343 + } 344 + } 345 + 346 + ::-webkit-scrollbar { 347 + width: 0px; 348 + background: transparent; 349 + padding: 0; 350 + margin: 0; 351 + } 352 + ::-webkit-scrollbar-thumb { 353 + background: transparent; 354 + border-radius: 0; 355 + } 356 + ::-webkit-scrollbar-track { 357 + background: transparent; 358 + border-radius: 0; 359 + } 360 + ::-webkit-scrollbar-corner { 361 + background: transparent; 362 + border-radius: 0; 363 + } 364 + ::-webkit-scrollbar-button { 365 + background: transparent; 366 + border-radius: 0; 367 + } 368 + 369 + * { 370 + scrollbar-width: none; 371 + scrollbar-color: transparent transparent; 372 + -ms-overflow-style: none; /* IE and Edge */ 373 + -webkit-overflow-scrolling: touch; 374 + -webkit-scrollbar: none; /* Safari */ 375 + }
+373
themes/witchcraft/theme.css
···
··· 1 + @font-face { 2 + font-family: "ProggyClean"; 3 + src: url(https://witchcraft.systems/ProggyCleanNerdFont-Regular.ttf); 4 + } 5 + 6 + :root { 7 + /* Color overrides, edit to whatever you want */ 8 + --primary-h: 260; /* Hue */ 9 + 10 + --link-color: hsl(calc(var(--primary-h) - 30), 75%, 60%); 11 + --link-hover-color: hsl(calc(var(--primary-h) - 30), 75%, 50%); 12 + --background-color: hsl(var(--primary-h), 75%, 10%); 13 + --header-background-color: hsl(var(--primary-h), 75%, 18%); 14 + --content-background-color: hsl(var(--primary-h), 75%, 8%); 15 + --text-color: #fff; 16 + --border-color: hsl(var(--primary-h), 75%, 60%); 17 + --indicator-inactive-color: #4a4a4a; 18 + --indicator-active-color: var(--border-color); 19 + } 20 + 21 + 22 + a { 23 + font-weight: 500; 24 + color: var(--link-color); 25 + text-decoration: inherit; 26 + } 27 + a:hover { 28 + color: var(--link-hover-color); 29 + text-decoration: underline; 30 + } 31 + 32 + body { 33 + margin: 0; 34 + display: flex; 35 + place-items: center; 36 + min-width: 320px; 37 + min-height: 100vh; 38 + background-color: var(--background-color); 39 + font-family: "ProggyClean", monospace; 40 + font-size: 24px; 41 + color: var(--text-color); 42 + border-color: var(--border-color); 43 + overflow-wrap: break-word; 44 + word-wrap: normal; 45 + word-break: break-word; 46 + hyphens: none; 47 + } 48 + 49 + h1 { 50 + font-size: 3.2em; 51 + line-height: 1.1; 52 + } 53 + 54 + #app { 55 + max-width: 1400px; 56 + width: 100%; 57 + margin: 0; 58 + padding: 0; 59 + margin-left: auto; 60 + margin-right: auto; 61 + text-align: center; 62 + } 63 + 64 + /* Post Component */ 65 + a:hover { 66 + text-decoration: underline; 67 + } 68 + #postContainer { 69 + display: flex; 70 + flex-direction: column; 71 + border: 1px solid var(--border-color); 72 + background-color: var(--background-color); 73 + margin-bottom: 15px; 74 + overflow-wrap: break-word; 75 + } 76 + #postHeader { 77 + display: flex; 78 + flex-direction: row; 79 + align-items: center; 80 + justify-content: start; 81 + background-color: var(--header-background-color); 82 + padding: 0px 0px; 83 + height: fit-content; 84 + border-bottom: 1px solid var(--border-color); 85 + font-weight: bold; 86 + overflow-wrap: break-word; 87 + height: 60px; 88 + } 89 + #displayName { 90 + display: block; 91 + color: var(--text-color); 92 + font-size: 1.2em; 93 + padding: 0; 94 + margin: 0; 95 + overflow-wrap: normal; 96 + word-wrap: break-word; 97 + word-break: break-word; 98 + text-overflow: ellipsis; 99 + overflow: hidden; 100 + white-space: nowrap; 101 + width: 100%; 102 + } 103 + #handle { 104 + display: block; 105 + color: var(--border-color); 106 + font-size: 0.8em; 107 + padding: 0; 108 + margin: 0; 109 + } 110 + 111 + #postLink { 112 + color: var(--border-color); 113 + font-size: 0.8em; 114 + padding: 0; 115 + margin: 0; 116 + } 117 + #postContent { 118 + display: flex; 119 + text-align: start; 120 + flex-direction: column; 121 + padding: 10px; 122 + background-color: var(--content-background-color); 123 + color: var(--text-color); 124 + overflow-wrap: break-word; 125 + white-space: pre-line; 126 + } 127 + #replyingText { 128 + font-size: 0.7em; 129 + margin: 0; 130 + padding: 0; 131 + padding-bottom: 5px; 132 + } 133 + #quotingText { 134 + font-size: 0.7em; 135 + margin: 0; 136 + padding: 0; 137 + padding-bottom: 5px; 138 + } 139 + #postText { 140 + margin: 0; 141 + padding: 0; 142 + overflow-wrap: break-word; 143 + word-wrap: normal; 144 + word-break: break-word; 145 + hyphens: none; 146 + } 147 + #headerText { 148 + margin-left: 10px; 149 + font-size: 0.9em; 150 + text-align: start; 151 + word-break: break-word; 152 + max-width: 80%; 153 + max-height: 95%; 154 + overflow: hidden; 155 + align-self: flex-start; 156 + margin-top: auto; 157 + margin-bottom: auto; 158 + } 159 + #avatar { 160 + height: 60px; 161 + width: 60px; 162 + margin: 0px; 163 + margin-left: 0px; 164 + overflow: hidden; 165 + object-fit: cover; 166 + border-right: var(--border-color) 1px solid; 167 + } 168 + #carouselContainer { 169 + position: relative; 170 + width: 100%; 171 + margin-top: 10px; 172 + display: flex; 173 + flex-direction: column; 174 + align-items: center; 175 + } 176 + #carouselControls { 177 + display: flex; 178 + justify-content: space-between; 179 + align-items: center; 180 + width: 100%; 181 + max-width: 500px; 182 + margin-top: 5px; 183 + } 184 + #carouselIndicators { 185 + display: flex; 186 + gap: 5px; 187 + } 188 + .indicator { 189 + width: 8px; 190 + height: 8px; 191 + background-color: var(--indicator-inactive-color); 192 + } 193 + .indicator.active { 194 + background-color: var(--indicator-active-color); 195 + } 196 + #prevBtn, 197 + #nextBtn { 198 + background-color: rgba(31, 17, 69, 0.7); 199 + color: var(--text-color); 200 + border: 1px solid var(--border-color); 201 + width: 30px; 202 + height: 30px; 203 + cursor: pointer; 204 + display: flex; 205 + align-items: center; 206 + justify-content: center; 207 + } 208 + #prevBtn:disabled, 209 + #nextBtn:disabled { 210 + opacity: 0.5; 211 + cursor: not-allowed; 212 + } 213 + #embedVideo { 214 + width: 100%; 215 + max-width: 500px; 216 + margin-top: 10px; 217 + align-self: center; 218 + } 219 + 220 + #embedImages { 221 + min-width: min(100%, 500px); 222 + max-width: min(100%, 500px); 223 + max-height: 500px; 224 + object-fit: contain; 225 + 226 + margin: 0; 227 + } 228 + 229 + /* Account Component */ 230 + #accountContainer { 231 + display: flex; 232 + text-align: start; 233 + align-items: center; 234 + background-color: var(--background-color); 235 + padding: 0px; 236 + margin-bottom: 15px; 237 + border: 1px solid var(--border-color); 238 + min-height: 30px; 239 + } 240 + #accountName { 241 + margin-left: 10px; 242 + font-size: 1em; 243 + max-width: 80%; 244 + 245 + /* replace overflow with ellipsis */ 246 + overflow: hidden; 247 + text-overflow: ellipsis; 248 + white-space: nowrap; 249 + } 250 + 251 + 252 + .no-avatar { 253 + margin-left: 70px !important; 254 + } 255 + 256 + /* App.Svelte */ 257 + /* desktop style */ 258 + 259 + #Content { 260 + display: flex; 261 + /* split the screen in half, left for accounts, right for posts */ 262 + width: 100%; 263 + height: 100%; 264 + flex-direction: row; 265 + justify-content: space-between; 266 + align-items: center; 267 + background-color: var(--background-color); 268 + color: var(--text-color); 269 + } 270 + #Feed { 271 + overflow-y: scroll; 272 + width: 65%; 273 + height: 100vh; 274 + padding: 20px; 275 + padding-bottom: 0; 276 + padding-top: 0; 277 + margin-top: 0; 278 + margin-bottom: 0; 279 + } 280 + #spacer { 281 + padding: 0; 282 + margin: 0; 283 + height: 10vh; 284 + width: 100%; 285 + } 286 + #Account { 287 + width: 35%; 288 + display: flex; 289 + flex-direction: column; 290 + border: 1px solid var(--border-color); 291 + background-color: var(--content-background-color); 292 + height: 80vh; 293 + padding: 20px; 294 + margin-left: 20px; 295 + } 296 + #accountsList { 297 + display: flex; 298 + flex-direction: column; 299 + overflow-y: scroll; 300 + height: 100%; 301 + width: 100%; 302 + padding: 0px; 303 + margin: 0px; 304 + } 305 + 306 + #Header { 307 + text-align: center; 308 + font-size: 2em; 309 + margin-bottom: 20px; 310 + } 311 + 312 + /* mobile style */ 313 + @media screen and (max-width: 600px) { 314 + #Content { 315 + flex-direction: column; 316 + width: auto; 317 + padding-left: 0px; 318 + padding-right: 0px; 319 + margin-top: 5%; 320 + } 321 + #Account { 322 + width: 85%; 323 + padding-left: 5%; 324 + padding-right: 5%; 325 + margin-bottom: 20px; 326 + margin-left: 5%; 327 + margin-right: 5%; 328 + height: auto; 329 + } 330 + #Feed { 331 + width: 95%; 332 + margin: 0px; 333 + margin-left: 10%; 334 + margin-right: 10%; 335 + padding: 0px; 336 + overflow-y: visible; 337 + } 338 + 339 + #spacer { 340 + height: 0; 341 + } 342 + } 343 + 344 + ::-webkit-scrollbar { 345 + width: 0px; 346 + background: transparent; 347 + padding: 0; 348 + margin: 0; 349 + } 350 + ::-webkit-scrollbar-thumb { 351 + background: transparent; 352 + border-radius: 0; 353 + } 354 + ::-webkit-scrollbar-track { 355 + background: transparent; 356 + border-radius: 0; 357 + } 358 + ::-webkit-scrollbar-corner { 359 + background: transparent; 360 + border-radius: 0; 361 + } 362 + ::-webkit-scrollbar-button { 363 + background: transparent; 364 + border-radius: 0; 365 + } 366 + 367 + * { 368 + scrollbar-width: none; 369 + scrollbar-color: transparent transparent; 370 + -ms-overflow-style: none; /* IE and Edge */ 371 + -webkit-overflow-scrolling: touch; 372 + -webkit-scrollbar: none; /* Safari */ 373 + }
+32
theming.ts
···
··· 1 + import { Plugin } from 'vite'; 2 + import { Config } from './config'; 3 + 4 + 5 + // Replaces app.css with the contents of the file specified in the 6 + // config file. 7 + export const themePlugin = (): Plugin => { 8 + const themeFolder = Config.THEME; 9 + console.log(`Using theme folder: ${themeFolder}`); 10 + return { 11 + name: 'theme-generator', 12 + enforce: 'pre', // Ensure this plugin runs first 13 + transform(code, id) { 14 + if (id.endsWith('app.css')) { 15 + // Read the theme file and replace the contents of app.css with it 16 + // Needs full path to the file 17 + const themeCode = Deno.readTextFileSync(Deno.cwd() + '/themes/' + themeFolder + '/theme.css'); 18 + // Replace the contents of app.css with the theme code 19 + 20 + // and add a comment at the top 21 + const themeComment = `/* Generated from ${themeFolder} */\n`; 22 + const themeCodeWithComment = themeComment + themeCode; 23 + // Return the theme code as the new contents of app.css 24 + return { 25 + code: themeCodeWithComment, 26 + map: null, 27 + }; 28 + } 29 + return null; 30 + } 31 + }; 32 + };
+8 -4
vite.config.ts
··· 1 - import { defineConfig } from 'vite' 2 - import { svelte } from '@sveltejs/vite-plugin-svelte' 3 4 // https://vite.dev/config/ 5 export default defineConfig({ 6 - plugins: [svelte()], 7 - })
··· 1 + import { defineConfig } from "vite"; 2 + import { svelte } from "@sveltejs/vite-plugin-svelte"; 3 + import { themePlugin } from "./theming"; 4 5 // https://vite.dev/config/ 6 export default defineConfig({ 7 + plugins: [ 8 + themePlugin(), 9 + svelte(), 10 + ], 11 + });