Compare commits
47 commits
Author | SHA1 | Date | |
---|---|---|---|
f897f812c3 | |||
374207c594 | |||
c11069b187 | |||
1d4b121ea5 | |||
daaa819e6c | |||
ea1a492dc0 | |||
3b8ca902f1 | |||
99def58c8b | |||
c51a0b1e5d | |||
7db5ec7fae | |||
1b25e56d0a | |||
2d7c346577 | |||
|
8e9fb6598e | ||
faab37a53b | |||
7ed8ebf6e5 | |||
876e221400 | |||
e3586f4eec | |||
563541d0e6 | |||
00277741a8 | |||
6f446fd871 | |||
0dd903a4eb | |||
3d1f38bdce | |||
fe9d216552 | |||
a5a066be3d | |||
22d6c5b90a | |||
455679a525 | |||
f6901085f5 | |||
d8efaccb30 | |||
77665702b7 | |||
d0163ee094 | |||
449a11ee55 | |||
667b11f2f4 | |||
7752585488 | |||
b170a532f6 | |||
b74b19cc73 | |||
30f3aadeaa | |||
f4709a232d | |||
e326ac858e | |||
a820b40318 | |||
a1c1b5f4d0 | |||
970590497f | |||
b295b6f03a | |||
f771866a09 | |||
c402f329a7 | |||
0a563e6121 | |||
8d9c3cc4fe | |||
a1ec63b7ec |
48 changed files with 2059 additions and 2005 deletions
10
README.md
10
README.md
|
@ -35,11 +35,13 @@ will likely be incompatible, and the web console will get very upset.
|
|||
|
||||
## try it out!
|
||||
|
||||
- `git clone` this repo
|
||||
- `npm install` the dependencies
|
||||
- `npm run dev` to spin up the dev environment
|
||||
campfire uses [bun](https://bun.sh/) as a package manager and runtime.
|
||||
|
||||
if you wish to run this in production, you need only `npm run build` and
|
||||
- `git clone` this repo
|
||||
- `bun install` the dependencies
|
||||
- `bun run dev` to spin up the dev environment
|
||||
|
||||
if you wish to run this in production, you need only `bun run build` and
|
||||
place the static files somewhere accessible by a static webhost, such as
|
||||
nginx or apache! **note:** your web server should attempt to reach
|
||||
`/fallback.html` before erroring out.
|
||||
|
|
246
bun.lock
Normal file
246
bun.lock
Normal file
|
@ -0,0 +1,246 @@
|
|||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "campfire-client",
|
||||
"devDependencies": {
|
||||
"@poppanator/sveltekit-svg": "^4.2.1",
|
||||
"@sveltejs/adapter-auto": "^3.2.2",
|
||||
"@sveltejs/adapter-static": "^3.0.2",
|
||||
"@sveltejs/kit": "^2.16.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
||||
"svelte": "^4.2.18",
|
||||
"vite": "^5.3.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.12", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg=="],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="],
|
||||
|
||||
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
|
||||
|
||||
"@poppanator/sveltekit-svg": ["@poppanator/sveltekit-svg@4.2.1", "", { "dependencies": { "@rollup/pluginutils": "^5.1.0" }, "peerDependencies": { "svelte": ">=4.x", "svgo": ">=3.x", "vite": ">=4.x" } }, "sha512-w7jl4EVOOF+X+uv2BEUiMDJwds+GfbczwGpcS0+rsjIsKYmqmwMi4ts3bVZR9ZvdFHWy5rS84U+pSBClz6cbBg=="],
|
||||
|
||||
"@rollup/pluginutils": ["@rollup/pluginutils@5.2.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.45.0", "", { "os": "android", "cpu": "arm" }, "sha512-2o/FgACbji4tW1dzXOqAV15Eu7DdgbKsF2QKcxfG4xbh5iwU7yr5RRP5/U+0asQliSYv5M4o7BevlGIoSL0LXg=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.45.0", "", { "os": "android", "cpu": "arm64" }, "sha512-PSZ0SvMOjEAxwZeTx32eI/j5xSYtDCRxGu5k9zvzoY77xUNssZM+WV6HYBLROpY5CkXsbQjvz40fBb7WPwDqtQ=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.45.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BA4yPIPssPB2aRAWzmqzQ3y2/KotkLyZukVB7j3psK/U3nVJdceo6qr9pLM2xN6iRP/wKfxEbOb1yrlZH6sYZg=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.45.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Pr2o0lvTwsiG4HCr43Zy9xXrHspyMvsvEw4FwKYqhli4FuLE5FjcZzuQ4cfPe0iUFCvSQG6lACI0xj74FDZKRA=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.45.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lYE8LkE5h4a/+6VnnLiL14zWMPnx6wNbDG23GcYFpRW1V9hYWHAw9lBZ6ZUIrOaoK7NliF1sdwYGiVmziUF4vA=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.45.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-PVQWZK9sbzpvqC9Q0GlehNNSVHR+4m7+wET+7FgSnKG3ci5nAMgGmr9mGBXzAuE5SvguCKJ6mHL6vq1JaJ/gvw=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.45.0", "", { "os": "linux", "cpu": "arm" }, "sha512-hLrmRl53prCcD+YXTfNvXd776HTxNh8wPAMllusQ+amcQmtgo3V5i/nkhPN6FakW+QVLoUUr2AsbtIRPFU3xIA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.45.0", "", { "os": "linux", "cpu": "arm" }, "sha512-XBKGSYcrkdiRRjl+8XvrUR3AosXU0NvF7VuqMsm7s5nRy+nt58ZMB19Jdp1RdqewLcaYnpk8zeVs/4MlLZEJxw=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.45.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-fRvZZPUiBz7NztBE/2QnCS5AtqLVhXmUOPj9IHlfGEXkapgImf4W9+FSkL8cWqoAjozyUzqFmSc4zh2ooaeF6g=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.45.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Btv2WRZOcUGi8XU80XwIvzTg4U6+l6D0V6sZTrZx214nrwxw5nAi8hysaXj/mctyClWgesyuxbeLylCBNauimg=="],
|
||||
|
||||
"@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.45.0", "", { "os": "linux", "cpu": "none" }, "sha512-Li0emNnwtUZdLwHjQPBxn4VWztcrw/h7mgLyHiEI5Z0MhpeFGlzaiBHpSNVOMB/xucjXTTcO+dhv469Djr16KA=="],
|
||||
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.45.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sB8+pfkYx2kvpDCfd63d5ScYT0Fz1LO6jIb2zLZvmK9ob2D8DeVqrmBDE0iDK8KlBVmsTNzrjr3G1xV4eUZhSw=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.45.0", "", { "os": "linux", "cpu": "none" }, "sha512-5GQ6PFhh7E6jQm70p1aW05G2cap5zMOvO0se5JMecHeAdj5ZhWEHbJ4hiKpfi1nnnEdTauDXxPgXae/mqjow9w=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.45.0", "", { "os": "linux", "cpu": "none" }, "sha512-N/euLsBd1rekWcuduakTo/dJw6U6sBP3eUq+RXM9RNfPuWTvG2w/WObDkIvJ2KChy6oxZmOSC08Ak2OJA0UiAA=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.45.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-2l9sA7d7QdikL0xQwNMO3xURBUNEWyHVHfAsHsUdq+E/pgLTUcCE+gih5PCdmyHmfTDeXUWVhqL0WZzg0nua3g=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.45.0", "", { "os": "linux", "cpu": "x64" }, "sha512-XZdD3fEEQcwG2KrJDdEQu7NrHonPxxaV0/w2HpvINBdcqebz1aL+0vM2WFJq4DeiAVT6F5SUQas65HY5JDqoPw=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.45.0", "", { "os": "linux", "cpu": "x64" }, "sha512-7ayfgvtmmWgKWBkCGg5+xTQ0r5V1owVm67zTrsEY1008L5ro7mCyGYORomARt/OquB9KY7LpxVBZes+oSniAAQ=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.45.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-B+IJgcBnE2bm93jEW5kHisqvPITs4ddLOROAcOc/diBgrEiQJJ6Qcjby75rFSmH5eMGrqJryUgJDhrfj942apQ=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.45.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-+CXwwG66g0/FpWOnP/v1HnrGVSOygK/osUbu3wPRy8ECXjoYKjRAyfxYpDQOfghC5qPJYLPH0oN4MCOjwgdMug=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.45.0", "", { "os": "win32", "cpu": "x64" }, "sha512-SRf1cytG7wqcHVLrBc9VtPK4pU5wxiB/lNIkNmW2ApKXIg+RpqwHfsaEK+e7eH4A1BpI6BX/aBWXxZCIrJg3uA=="],
|
||||
|
||||
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.5", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ=="],
|
||||
|
||||
"@sveltejs/adapter-auto": ["@sveltejs/adapter-auto@3.3.1", "", { "dependencies": { "import-meta-resolve": "^4.1.0" }, "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-5Sc7WAxYdL6q9j/+D0jJKjGREGlfIevDyHSQ2eNETHcB1TKlQWHcAo8AS8H1QdjNvSXpvOwNjykDUHPEAyGgdQ=="],
|
||||
|
||||
"@sveltejs/adapter-static": ["@sveltejs/adapter-static@3.0.8", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-YaDrquRpZwfcXbnlDsSrBQNCChVOT9MGuSg+dMAyfsAa1SmiAhrA5jUYUiIMC59G92kIbY/AaQOWcBdq+lh+zg=="],
|
||||
|
||||
"@sveltejs/kit": ["@sveltejs/kit@2.22.5", "", { "dependencies": { "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.1.0", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", "sirv": "^3.0.0" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" }, "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-l5i+LcDaoymD2mg5ziptnHmzzF79+c9twJiDoLWAPKq7afMEe4mvGesJ+LVtm33A92mLzd2KUHgtGSqTrvfkvg=="],
|
||||
|
||||
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@3.1.2", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^2.1.0", "debug": "^4.3.4", "deepmerge": "^4.3.1", "kleur": "^4.1.5", "magic-string": "^0.30.10", "svelte-hmr": "^0.16.0", "vitefu": "^0.2.5" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.0" } }, "sha512-Txsm1tJvtiYeLUVRNqxZGKR/mI+CzuIQuc2gn+YCs9rMTowpNZ2Nqt53JdL8KF9bLhAf2ruR/dr9eZCwdTriRA=="],
|
||||
|
||||
"@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@2.1.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^3.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.0" } }, "sha512-9QX28IymvBlSCqsCll5t0kQVxipsfhFFL+L2t3nTWfXnddYwxBuAEtTtlaVQpRz9c37BhJjltSeY4AJSC03SSg=="],
|
||||
|
||||
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||
|
||||
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
|
||||
|
||||
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
||||
|
||||
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
|
||||
|
||||
"code-red": ["code-red@1.0.4", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15", "@types/estree": "^1.0.1", "acorn": "^8.10.0", "estree-walker": "^3.0.3", "periscopic": "^3.1.0" } }, "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw=="],
|
||||
|
||||
"commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
|
||||
|
||||
"cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
|
||||
|
||||
"css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="],
|
||||
|
||||
"css-tree": ["css-tree@2.3.1", "", { "dependencies": { "mdn-data": "2.0.30", "source-map-js": "^1.0.1" } }, "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw=="],
|
||||
|
||||
"css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="],
|
||||
|
||||
"csso": ["csso@5.0.5", "", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="],
|
||||
|
||||
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||
|
||||
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
|
||||
|
||||
"devalue": ["devalue@5.1.1", "", {}, "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw=="],
|
||||
|
||||
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
|
||||
|
||||
"domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
|
||||
|
||||
"domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
|
||||
|
||||
"domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
|
||||
|
||||
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
|
||||
"esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="],
|
||||
|
||||
"esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
|
||||
|
||||
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"import-meta-resolve": ["import-meta-resolve@4.1.0", "", {}, "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw=="],
|
||||
|
||||
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
|
||||
|
||||
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||
|
||||
"locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
|
||||
|
||||
"mdn-data": ["mdn-data@2.0.30", "", {}, "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="],
|
||||
|
||||
"mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
|
||||
|
||||
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
|
||||
|
||||
"periscopic": ["periscopic@3.1.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^3.0.0", "is-reference": "^3.0.0" } }, "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
|
||||
|
||||
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||
|
||||
"rollup": ["rollup@4.45.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.45.0", "@rollup/rollup-android-arm64": "4.45.0", "@rollup/rollup-darwin-arm64": "4.45.0", "@rollup/rollup-darwin-x64": "4.45.0", "@rollup/rollup-freebsd-arm64": "4.45.0", "@rollup/rollup-freebsd-x64": "4.45.0", "@rollup/rollup-linux-arm-gnueabihf": "4.45.0", "@rollup/rollup-linux-arm-musleabihf": "4.45.0", "@rollup/rollup-linux-arm64-gnu": "4.45.0", "@rollup/rollup-linux-arm64-musl": "4.45.0", "@rollup/rollup-linux-loongarch64-gnu": "4.45.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.45.0", "@rollup/rollup-linux-riscv64-gnu": "4.45.0", "@rollup/rollup-linux-riscv64-musl": "4.45.0", "@rollup/rollup-linux-s390x-gnu": "4.45.0", "@rollup/rollup-linux-x64-gnu": "4.45.0", "@rollup/rollup-linux-x64-musl": "4.45.0", "@rollup/rollup-win32-arm64-msvc": "4.45.0", "@rollup/rollup-win32-ia32-msvc": "4.45.0", "@rollup/rollup-win32-x64-msvc": "4.45.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-WLjEcJRIo7i3WDDgOIJqVI2d+lAC3EwvOGy+Xfq6hs+GQuAA4Di/H72xmXkOhrIWFg2PFYSKZYfH0f4vfKXN4A=="],
|
||||
|
||||
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
|
||||
|
||||
"sax": ["sax@1.4.1", "", {}, "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg=="],
|
||||
|
||||
"set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="],
|
||||
|
||||
"sirv": ["sirv@3.0.1", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"svelte": ["svelte@4.2.20", "", { "dependencies": { "@ampproject/remapping": "^2.2.1", "@jridgewell/sourcemap-codec": "^1.4.15", "@jridgewell/trace-mapping": "^0.3.18", "@types/estree": "^1.0.1", "acorn": "^8.9.0", "aria-query": "^5.3.0", "axobject-query": "^4.0.0", "code-red": "^1.0.3", "css-tree": "^2.3.1", "estree-walker": "^3.0.3", "is-reference": "^3.0.1", "locate-character": "^3.0.0", "magic-string": "^0.30.4", "periscopic": "^3.1.0" } }, "sha512-eeEgGc2DtiUil5ANdtd8vPwt9AgaMdnuUFnPft9F5oMvU/FHu5IHFic+p1dR/UOB7XU2mX2yHW+NcTch4DCh5Q=="],
|
||||
|
||||
"svelte-hmr": ["svelte-hmr@0.16.0", "", { "peerDependencies": { "svelte": "^3.19.0 || ^4.0.0" } }, "sha512-Gyc7cOS3VJzLlfj7wKS0ZnzDVdv3Pn2IuVeJPk9m2skfhcu5bq3wtIZyQGggr7/Iim5rH5cncyQft/kRLupcnA=="],
|
||||
|
||||
"svgo": ["svgo@4.0.0", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.4.1" }, "bin": "./bin/svgo.js" }, "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw=="],
|
||||
|
||||
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
|
||||
|
||||
"vite": ["vite@5.4.19", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA=="],
|
||||
|
||||
"vitefu": ["vitefu@0.2.5", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["vite"] }, "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q=="],
|
||||
|
||||
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
|
||||
"csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="],
|
||||
|
||||
"svgo/css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="],
|
||||
|
||||
"csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
|
||||
|
||||
"svgo/css-tree/mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="],
|
||||
}
|
||||
}
|
1664
package-lock.json
generated
1664
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,19 +1,19 @@
|
|||
@import url("../font/inter/inter.css");
|
||||
@import url("./font/inter/inter.css");
|
||||
|
||||
:root {
|
||||
--bg-1000: #fff6de;
|
||||
--bg-900: #f9f1db;
|
||||
--bg-800: #f1e8cf;
|
||||
--bg-700: #d2c9b1;
|
||||
--bg-600: #f0f6c2;
|
||||
--accent: #8d9936;
|
||||
--bg-1000: #fffcf7;
|
||||
--bg-900: #faf4e4;
|
||||
--bg-800: #f2e8d7;
|
||||
--bg-700: #d9ccad;
|
||||
--bg-600: #edf5ba;
|
||||
--accent: #92a40a;
|
||||
--text: #322e1f;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg-1000: #141016;
|
||||
--bg-900: #1B141E;
|
||||
--bg-1000: #0b090c;
|
||||
--bg-900: #120d14;
|
||||
--bg-800: #2A202F;
|
||||
--bg-700: #443749;
|
||||
--bg-600: #513D60;
|
||||
|
@ -31,10 +31,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
body { font-family: InterVariable, sans-serif; }
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
@ -48,6 +44,12 @@ body {
|
|||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
body {
|
||||
font-family: InterVariable, sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
|
@ -71,7 +73,7 @@ header, #widgets {
|
|||
}
|
||||
|
||||
main {
|
||||
width: 732px;
|
||||
width: 700px;
|
||||
}
|
||||
|
||||
img.emoji {
|
||||
|
@ -79,6 +81,10 @@ img.emoji {
|
|||
margin: -.2em 0;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-color: color-mix(in srgb, transparent, var(--accent) 50%);
|
||||
}
|
||||
|
||||
.throb {
|
||||
animation: .25s throb alternate infinite ease-in;
|
||||
}
|
7
src/img/icons/bot.svg
Normal file
7
src/img/icons/bot.svg
Normal file
|
@ -0,0 +1,7 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 32 32">
|
||||
<path d="M22 14a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2V16a2 2 0 0 1 2-2h14Zm-7 9a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm-3.293-5.707a1 1 0 0 0-1.338-.068l-.076.068-2 2a1 1 0 1 0 1.414 1.414L11 19.414l1.293 1.293.076.068a1 1 0 0 0 1.406-1.406l-.068-.076-2-2Zm8 0a1 1 0 0 0-1.338-.068l-.076.068-2 2a1 1 0 1 0 1.414 1.414L19 19.414l1.293 1.293.076.068a1 1 0 0 0 1.406-1.406l-.068-.076-2-2Z"/>
|
||||
<rect width="4" height="10" x="4" y="16" rx="2"/>
|
||||
<rect width="4" height="10" x="22" y="16" rx="2"/>
|
||||
<path d="M14 9h2v5h-2V9Z"/>
|
||||
<circle cx="15" cy="9" r="2"/>
|
||||
</svg>
|
After Width: | Height: | Size: 646 B |
3
src/img/icons/cross.svg
Normal file
3
src/img/icons/cross.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 32 32">
|
||||
<path d="M20.763 7.627a2 2 0 1 1 2.908 2.746l-5.236 5.545 5.547 5.236.141.149a2 2 0 0 1-2.887 2.76l-5.546-5.237-5.236 5.547a2 2 0 1 1-2.908-2.746l5.236-5.546-5.546-5.235-.142-.149a2 2 0 0 1 2.731-2.893l.157.133 5.545 5.236 5.236-5.546Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 329 B |
4
src/img/icons/lock.svg
Normal file
4
src/img/icons/lock.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 32 32">
|
||||
<rect width="18" height="13" x="7" y="15" rx="2"/>
|
||||
<path d="M16 7c1.608 0 2.895.534 3.873 1.382.95.822 1.535 1.875 1.902 2.83.369.96.547 1.898.634 2.582a11.746 11.746 0 0 1 .088 1.095l.002.074v.036l-3 .002v-.036c-.002-.034-.003-.09-.008-.162a8.703 8.703 0 0 0-.057-.628 8.303 8.303 0 0 0-.46-1.887c-.257-.67-.608-1.242-1.066-1.639C17.48 10.28 16.892 10 16 10c-.892 0-1.48.278-1.909.65-.457.396-.809.968-1.066 1.638a8.304 8.304 0 0 0-.46 1.887 8.799 8.799 0 0 0-.065.79v.037L9.5 15v-.036l.001-.074c.002-.061.004-.146.01-.25.011-.209.034-.5.078-.845.088-.684.267-1.622.636-2.582.367-.955.952-2.008 1.901-2.83C13.104 7.534 14.392 7 16 7Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 730 B |
3
src/img/icons/tick.svg
Normal file
3
src/img/icons/tick.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 32 32">
|
||||
<path d="M24.546 8.627a2 2 0 0 1 2.908 2.746l-.03.032-.03.032-.03.032-.03.032-.06.064-.06.063-.12.126c-.036.04-.02.023-.058.062l-.03.032-.059.062-.029.03-.059.063-.058.062-.03.03-.028.032-.116.122-.028.03-.03.032c-.097.103.04-.044-.057.06l-.453.48-.223.236-.11.117-.11.116-.856.907-.052.055-.053.056-.31.328-.051.055-.026.027-.026.027c-.114.122.064-.067-.05.054l-.026.027-.025.027-.051.055-.026.026-.025.027-.102.108-.05.052-.026.028-.024.026-.1.107-.051.052-.15.159-.024.026-.026.026-.049.053-.1.105-.023.026-.025.026-.025.027-.025.025-.097.104-.099.104-.024.026-.025.026-.146.155-.048.052-.049.05-.097.103-.049.052-.047.05-.097.103-.096.102c-.09.095.042-.045-.049.05l-.023.026-.024.025-.192.203-.023.026-.024.024-.024.026-.024.025-.38.403-.095.1-.095.1c-.04.043-.006.008-.047.05l-.023.026-.025.025-.093.1-.19.2-.023.025-.024.025-.046.05-.19.2-.047.05-.047.05-.19.2-.023.025-.023.025c-.084.089.036-.038-.048.05l-.047.05-.048.05a59.55 59.55 0 0 1-.094.1c-.104.11.057-.059-.047.051l-.048.05-.047.05-.023.025-.025.025c-.058.062.011-.01-.047.051-.042.046-.005.005-.048.05l-.094.1-.025.026-.023.025-.048.051-.048.05-.335.355-1.572 1.666-.102.107-.025.027-.025.027-.026.027-.025.026-.102.109-.026.027-.05.055-.027.026-.051.055-.026.027-.026.027-.103.11c-.033.035-.02.02-.052.056l-.053.054-.025.028-.053.055-.026.029-.105.11c-.052.056 0 .002-.053.056l-.026.029-.026.027-.027.028-.027.029-.053.056-.106.113c-.066.07.012-.014-.054.056l-.026.029-.028.028-.107.114c-.076.08-.032.035-.108.116l-.11.115-.11.116-.027.03-.083.087-.222.236-.226.238-.113.12-.03.031-.084.091-.03.03-.028.03-.03.03-.027.032-.06.06c-.123.132.067-.069-.057.062l-.029.031-.03.03-.028.032-.058.062-.03.03-.029.032-.03.031-.059.063-.06.062c-.09.097-.026.03-.117.126l-.24.255a2.002 2.002 0 0 1-2.91 0l-5.782-6.125-.133-.156a2 2 0 0 1 3.041-2.59l4.328 4.584.164-.173c.057-.06 0 .002.056-.058l.055-.06.054-.057.029-.029.027-.03.108-.114.028-.029.027-.03.027-.027.027-.03.054-.056.027-.029.027-.029.027-.028.027-.029.053-.056.027-.029.026-.028.054-.057.053-.055c.095-.1-.042.044.053-.057l.21-.223.21-.22.103-.11.05-.055.027-.027.05-.054.027-.028.051-.054.103-.108.203-.216.2-.212.1-.106.026-.026.024-.027.026-.026.024-.027.025-.026.05-.053.025-.026.098-.105.05-.052.049-.052.099-.104.39-.414c.107-.113-.01.012.097-.102.098-.103-.05.052.048-.052l.049-.05.095-.103.097-.102.048-.05.023-.026.025-.025.191-.203.024-.026.024-.024.023-.026.025-.025.38-.403.094-.1.095-.1.047-.05.024-.026.024-.025.047-.05.048-.05.188-.2.023-.025.025-.025.047-.05.047-.05.047-.05.094-.1.048-.05.047-.05.188-.2.025-.025.023-.025.047-.05.048-.05.047-.05.095-.1c.067-.072-.02.02.046-.051.05-.052 0 .002.048-.05l.048-.05.024-.025.023-.025c.054-.058.04-.044.095-.101l.096-.1.023-.026.024-.025.047-.051.025-.025.023-.025.096-.101.023-.026.025-.025.191-.203 1.572-1.666.026-.027.025-.026.152-.161.103-.109.05-.054.027-.028.026-.026.206-.22.052-.055.052-.054.052-.056.264-.278.212-.226.053-.056.054-.057.055-.058.053-.056.109-.115.109-.116.055-.058.027-.029.027-.029.11-.117.223-.236.454-.48c.046-.05.01-.011.057-.06l.058-.062.115-.122.03-.031.029-.03.087-.094.03-.03.117-.126.06-.062.029-.031.03-.033.06-.062.06-.064.03-.032.029-.031.06-.064.06-.064Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 3.2 KiB |
151
src/lang/de_DE.json
Normal file
151
src/lang/de_DE.json
Normal file
|
@ -0,0 +1,151 @@
|
|||
{
|
||||
"compose_placeholders": [
|
||||
"Was geht, %1?",
|
||||
"Hau raus!",
|
||||
"Leiste einen Beitrag!",
|
||||
"Ich liebe es zu Posten!",
|
||||
"Ein neuer Tag, ein neuer %1 post!"
|
||||
],
|
||||
|
||||
"login": {
|
||||
"welcome": "Willkommen, Fediverse-Nutzer!",
|
||||
"enter_domain": "Bitte die Server-Domäne eingeben.",
|
||||
"experimental": "Diese Software ist noch\n<strong><em>experimentele Software</em></strong>;\nFunktionen können jederzeit Probleme aufweisen!\n<br>\nWenn dies kein Problem ist, Willkommen am Bord!",
|
||||
"button": "Einloggen",
|
||||
"error": {
|
||||
"no_domain": "Bitte eine valide Server-Domäne eingeben.",
|
||||
"connection_failed": "Verbindung zum Server fehlgeschlagen! \nÜberprüfe die Browser-Konsole!",
|
||||
"create_app": "Fehler beim erstellen einer App zum Server."
|
||||
},
|
||||
"made_with_tagline": "Gemacht mit Liebe ❤ von <a href=\"https://bliss.town\">bliss town</a>"
|
||||
},
|
||||
|
||||
"navigation": {
|
||||
"timeline": "Timeline",
|
||||
"notifications": "Benachrichtigungen",
|
||||
"follow_requests": "Follower-Anfragen",
|
||||
"explore": "Entdecken",
|
||||
"lists": "Listen",
|
||||
|
||||
"favourites": "Favoriten",
|
||||
"bookmarks": "Lesezeichen",
|
||||
"hashtags": "Hashtags",
|
||||
|
||||
"profile_information": "Profil Informationen",
|
||||
"settings": "Einstellungen",
|
||||
"log_out": "Ausloggen",
|
||||
|
||||
"back": "Zurück"
|
||||
},
|
||||
|
||||
"follow_requests": {
|
||||
"none": "Gerade keine Follower-Anfragen für dich!"
|
||||
},
|
||||
|
||||
"timeline": {
|
||||
"home": "Home-Feed",
|
||||
"local": "Lokaler-Feed",
|
||||
"federated": "Federations-Feed",
|
||||
"fetching": "Feed wird geladen..."
|
||||
},
|
||||
|
||||
"notification": {
|
||||
"and_others": "und <strong>%1</strong> andere",
|
||||
"mention": "%1 hat dich erwähnt.",
|
||||
"reblog": "%1 hat deinen Post geboosted.",
|
||||
"reaction": "%1 hat auf deinen Poste reagiert.",
|
||||
"follow": "%1 hat die gefolgt.",
|
||||
"follow_request": "%1 hat angefragt dir zu folgen.",
|
||||
"favourite": "%1 hat deinen Post favorisiert.",
|
||||
"poll": "%1's Umfrage wurde beendet.",
|
||||
"update": "%1 hat deren Post bearbeitet.",
|
||||
"default": "%1 hat dich angestupst!",
|
||||
"fetching": "Benachrichtigungen werden geladen..."
|
||||
},
|
||||
|
||||
"post": {
|
||||
"loading": "Post wird geladen...",
|
||||
"pinned": "📌 Angepinnter Post",
|
||||
"by": "Post von %1",
|
||||
"boosted": "%1 hat diesen Post geboosted.",
|
||||
"actions": {
|
||||
"reply": "Antworten",
|
||||
"boost": "Boost",
|
||||
"favourite": "Favorisieren",
|
||||
"quote": "Zitieren",
|
||||
"react": "Reagieren",
|
||||
"more": "Mehr",
|
||||
"delete": "Löschen"
|
||||
},
|
||||
"warning": {
|
||||
"placeholder": "Inhaltswarnung",
|
||||
"show": "(Klicken zum Anzeigen)",
|
||||
"hide": "(Klicken zum Verbergen)"
|
||||
},
|
||||
"visibility": {
|
||||
"public": "Öffentlich",
|
||||
"unlisted": "Nicht gelistet",
|
||||
"follow_only": "Nur Follower",
|
||||
"private": "Privat",
|
||||
"direct": "Direktnachricht"
|
||||
}
|
||||
},
|
||||
|
||||
"profile": {
|
||||
"locked": "Dies ist ein privates Profil.",
|
||||
"bot": "Dies ist ein automatiesiertes Profil.",
|
||||
"followers": "Folgen",
|
||||
"following": "Gefolgt",
|
||||
"follow": "Folgen",
|
||||
"home_instance": "Auf Hauptinstanz ansehen",
|
||||
"more": "Mehr",
|
||||
"posts": "Posts",
|
||||
"replies": "Antworten",
|
||||
"media": "Medien",
|
||||
"loading": "Profil wird geladen..."
|
||||
},
|
||||
|
||||
"logs": {
|
||||
"logged_in": "Eingeloggt als %1",
|
||||
"server_detected": "Server entdeckt als %1 (%2) mit Funktionen: {%3}}",
|
||||
"server_unsupported": "Server %1 wird nicht unterstützt (%2). Funktionen können nicht garantiert werden und dein Haus wird vermutlich in Flammen aufgehen",
|
||||
"no_hostname": "Versucht mit Server ohne Hostnamen zu verbinden",
|
||||
"no_https": "Verbindung zu einem unsicheren Server feige verweigert.“",
|
||||
"connection_failed": "Verbindung zu %1 fehlgeschlagen",
|
||||
"post_fetch_failed": "Fehler beim Laden des Posts",
|
||||
"post_fetch_failed_id": "Fehler beim Laden des Posts %1",
|
||||
"post_parse_failed": "Fehler beim Analysieren des Posts",
|
||||
"post_parse_failed_id": "Fehler beim Analysieren des Posts %1",
|
||||
"profile_fetch_failed": "Fehler beim Laden des Profils",
|
||||
"profile_fetch_failed_id": "Fehler beim Laden des Profils %1",
|
||||
"token_revoke_failed": "Token widerrufen fehlgeschlagen! Daten werden trotzdem gedumpt",
|
||||
"sound_does_not_exist": "Versucht Sound \"%1\" abzuspielen, aber es existiert nicht!",
|
||||
"account_data_empty": "Profil Daten wurden analysiert, aber keine Daten wurden gefunden!",
|
||||
"timeline_fetch_failed": "Fehler beim Anfragen der Timeline"
|
||||
},
|
||||
|
||||
"error": {
|
||||
"bad_request": "Fehlgeschlagener Request",
|
||||
"invalid_auth_code": "Ungültiger Authcode",
|
||||
"connection_failed": "Verbindung fehlgeschlagen <code>%1</code>.",
|
||||
"post_fetch_failed": "Fehler beim Anzeigen des Posts <code>%1</code>."
|
||||
},
|
||||
|
||||
"time": {
|
||||
"in": "In %1",
|
||||
"ago": "Vor %1",
|
||||
"second": "s",
|
||||
"minute": "m",
|
||||
"hour": "h",
|
||||
"day": "t",
|
||||
"week": "w",
|
||||
"year": "j"
|
||||
},
|
||||
|
||||
"compose": "Posten",
|
||||
"search": "Suchen",
|
||||
"loading": "Nur ein Moment...",
|
||||
|
||||
"source": "Quellcode",
|
||||
"issues": "Issues"
|
||||
}
|
151
src/lang/en_GB.json
Normal file
151
src/lang/en_GB.json
Normal file
|
@ -0,0 +1,151 @@
|
|||
{
|
||||
"compose_placeholders": [
|
||||
"What's cooking, %1?",
|
||||
"Speak your mind!",
|
||||
"Federate something...",
|
||||
"I sure love posting!",
|
||||
"Another day, another %1 post!"
|
||||
],
|
||||
|
||||
"login": {
|
||||
"welcome": "Welcome, fediverse user!",
|
||||
"enter_domain": "Please enter your server domain to log in.",
|
||||
"experimental": "Please note this is\n<strong><em>extremely experimental software</em></strong>;\nthings are likely to break!\n<br>\nIf that's all cool with you, welcome aboard!",
|
||||
"button": "Log in",
|
||||
"error": {
|
||||
"no_domain": "Please enter a server domain.",
|
||||
"connection_failed": "Failed to connect to the server.\nCheck the browser console for details!",
|
||||
"create_app": "Failed to create an application for this server."
|
||||
},
|
||||
"made_with_tagline": "made with ❤ by <a href=\"https://bliss.town\">bliss town</a>"
|
||||
},
|
||||
|
||||
"navigation": {
|
||||
"timeline": "Timeline",
|
||||
"notifications": "Notifications",
|
||||
"follow_requests": "Follow requests",
|
||||
"explore": "Explore",
|
||||
"lists": "Lists",
|
||||
|
||||
"favourites": "Favourites",
|
||||
"bookmarks": "Bookmarks",
|
||||
"hashtags": "Hashtags",
|
||||
|
||||
"profile_information": "Profile information",
|
||||
"settings": "Settings",
|
||||
"log_out": "Log out",
|
||||
|
||||
"back": "Back"
|
||||
},
|
||||
|
||||
"follow_requests": {
|
||||
"none": "no follow requests to action right now!"
|
||||
},
|
||||
|
||||
"timeline": {
|
||||
"home": "Home",
|
||||
"local": "Local",
|
||||
"federated": "Federated",
|
||||
"fetching": "getting the feed..."
|
||||
},
|
||||
|
||||
"notification": {
|
||||
"and_others": "and <strong>%1</strong> others",
|
||||
"mention": "%1 mentioned you.",
|
||||
"reblog": "%1 boosted your post.",
|
||||
"reaction": "%1 reacted to your post.",
|
||||
"follow": "%1 followed you.",
|
||||
"follow_request": "%1 requested to follow you.",
|
||||
"favourite": "%1 favourited your post.",
|
||||
"poll": "%1's poll has ended.",
|
||||
"update": "%1 updated their post.",
|
||||
"default": "%1 poked you!",
|
||||
"fetching": "fetching notifications..."
|
||||
},
|
||||
|
||||
"post": {
|
||||
"loading": "loading post...",
|
||||
"pinned": "📌 Pinned post",
|
||||
"by": "Post by %1",
|
||||
"boosted": "%1 boosted this post.",
|
||||
"actions": {
|
||||
"reply": "Reply",
|
||||
"boost": "Boost",
|
||||
"favourite": "Favourite",
|
||||
"quote": "Quote",
|
||||
"react": "React",
|
||||
"more": "More",
|
||||
"delete": "Delete"
|
||||
},
|
||||
"warning": {
|
||||
"placeholder": "Content warning",
|
||||
"show": "(click to reveal)",
|
||||
"hide": "(click to hide)"
|
||||
},
|
||||
"visibility": {
|
||||
"public": "public",
|
||||
"unlisted": "unlisted",
|
||||
"follow_only": "followers only",
|
||||
"private": "private",
|
||||
"direct": "direct"
|
||||
}
|
||||
},
|
||||
|
||||
"profile": {
|
||||
"locked": "This is a private account.",
|
||||
"bot": "This is an automated account.",
|
||||
"followers": "Followers",
|
||||
"following": "Following",
|
||||
"follow": "Follow",
|
||||
"home_instance": "View on home instance",
|
||||
"more": "More",
|
||||
"posts": "Posts",
|
||||
"replies": "Replies",
|
||||
"media": "Media",
|
||||
"loading": "loading profile..."
|
||||
},
|
||||
|
||||
"logs": {
|
||||
"logged_in": "Logged in as %1",
|
||||
"server_detected": "Server detected as %1 (%2) with capabilities: {%3}}",
|
||||
"server_unsupported": "Server %1 is unsupported (%2). Things may break, or not work as expected",
|
||||
"no_hostname": "Attempted to connect to a server without providing a hostname",
|
||||
"no_https": "Cowardly refusing to connect to an insecure server",
|
||||
"connection_failed": "Failed to connect to %1",
|
||||
"post_fetch_failed": "Failed to fetch post",
|
||||
"post_fetch_failed_id": "Failed to fetch post %1",
|
||||
"post_parse_failed": "Failed to parse post",
|
||||
"post_parse_failed_id": "Failed to parse post %1",
|
||||
"profile_fetch_failed": "Failed to fetch profile",
|
||||
"profile_fetch_failed_id": "Failed to fetch profile %1",
|
||||
"token_revoke_failed": "Token revocation failed! Dumping data anyways",
|
||||
"sound_does_not_exist": "Attempted to play sound \"%1\", which does not exist!",
|
||||
"account_data_empty": "Attempted to parse account data but no data was provided",
|
||||
"timeline_fetch_failed": "Failed to retrieve timeline."
|
||||
},
|
||||
|
||||
"error": {
|
||||
"bad_request": "Bad request",
|
||||
"invalid_auth_code": "Invalid auth code provided",
|
||||
"connection_failed": "Failed to connect to <code>%1</code>.",
|
||||
"post_fetch_failed": "Failed to retrieve post <code>%1</code>."
|
||||
},
|
||||
|
||||
"time": {
|
||||
"in": "in %1",
|
||||
"ago": "%1 ago",
|
||||
"second": "s",
|
||||
"minute": "m",
|
||||
"hour": "h",
|
||||
"day": "d",
|
||||
"week": "w",
|
||||
"year": "y"
|
||||
},
|
||||
|
||||
"compose": "Post",
|
||||
"search": "Search",
|
||||
"loading": "just a moment...",
|
||||
|
||||
"source": "source",
|
||||
"issues": "issues"
|
||||
}
|
145
src/lang/ga_IE.json
Normal file
145
src/lang/ga_IE.json
Normal file
|
@ -0,0 +1,145 @@
|
|||
{
|
||||
"compose_placeholders": [
|
||||
"Céard atá ar bun, %1?",
|
||||
"Abair do thuairim!",
|
||||
"Cónaidhmigh rud éigin...",
|
||||
"Is breá liom postáil cinnte!",
|
||||
"Lá eile, postáil %1 eile!"
|
||||
],
|
||||
|
||||
"login": {
|
||||
"welcome": "Fáilte, a úsáideoir fediverse!",
|
||||
"enter_domain": "Cuir isteach fearann do fhreastalaí le logáil isteach.",
|
||||
"experimental": "Tabhair faoi deara gur\n<strong><em>bogearraí thar a bheith turgnamhach</em> iad seo</strong>;\nis dócha go mbrisfidh rudaí!\n<br>\nMás ceart go leor leat é sin, fáilte romhat!",
|
||||
"button": "Logáil isteach",
|
||||
"error": {
|
||||
"no_domain": "Cuir isteach fearann freastalaí.",
|
||||
"connection_failed": "Theip ar cheangal leis an bhfreastalaí.\nSeiceáil consól an bhrabhsálaí le haghaidh sonraí!",
|
||||
"create_app": "Theip ar fheidhmchlár a chruthú don fhreastalaí seo."
|
||||
},
|
||||
"made_with_tagline": "déanta le ❤ ag <a href=\"https://bliss.town\">bliss town</a>"
|
||||
},
|
||||
|
||||
"navigation": {
|
||||
"timeline": "Amlíne",
|
||||
"notifications": "Fógraí",
|
||||
"explore": "Féach Thart",
|
||||
"lists": "Liostaí",
|
||||
|
||||
"favourites": "Ceanáin",
|
||||
"bookmarks": "Leabharmharcanna",
|
||||
"hashtags": "Haischlibeanna",
|
||||
|
||||
"profile_information": "Faisnéis Phróifíle",
|
||||
"settings": "Socruithe",
|
||||
"log_out": "Logáil amach",
|
||||
|
||||
"back": "Fill"
|
||||
},
|
||||
|
||||
"timeline": {
|
||||
"home": "Baile",
|
||||
"local": "Áitiúil",
|
||||
"federated": "Cónaidhmithe",
|
||||
"fetching": "ag fáil an fhotha..."
|
||||
},
|
||||
|
||||
"notification": {
|
||||
"and_others": "agus <strong>%1</strong> eile",
|
||||
"mention": "Luaigh %1 thú.",
|
||||
"reblog": "Chuir %1 borradh faoi do phost",
|
||||
"reaction": "D'fhrithghníomhaigh %1 do do phost",
|
||||
"follow": "Lean %1 thú",
|
||||
"follow_request": "D'iarr %1 leanúint leat.",
|
||||
"favourite": "Chuir %1 do phost i bhfabhar.",
|
||||
"poll": "Chríochnaigh pobalbhreith %1.",
|
||||
"update": "Nuashonraigh %1 a bpost",
|
||||
"default": "Priocadh %1 thú!",
|
||||
"fetching": "ag fáil an fógraí..."
|
||||
},
|
||||
|
||||
"post": {
|
||||
"loading": "ag lódáil an postáil...",
|
||||
"by": "Postáil le %1",
|
||||
"boosted": "Chuir %1 borradh faoin bpost seo.",
|
||||
"actions": {
|
||||
"reply": "Freagair",
|
||||
"boost": "Borradh",
|
||||
"favourite": "Ceanán",
|
||||
"quote": "Sliocht",
|
||||
"react": "Frithghníomhaigh",
|
||||
"more": "Tuilleadh",
|
||||
"delete": "Scrios"
|
||||
},
|
||||
"warning": {
|
||||
"placeholder": "Rabhadh ábhair",
|
||||
"show": "(cliceáil chun nochtadh)",
|
||||
"hide": "(cliceáil chun a cheilt)"
|
||||
},
|
||||
"visibility": {
|
||||
"public": "poiblí",
|
||||
"unlisted": "neamhliostaithe",
|
||||
"follow_only": "leantóirí amháin",
|
||||
"private": "príobháideach",
|
||||
"direct": "díreach"
|
||||
}
|
||||
},
|
||||
|
||||
"profile": {
|
||||
"locked": "Is cuntas príobháideach é seo.",
|
||||
"bot": "Is cuntas uathoibrithe é seo.",
|
||||
"followers": "Leantóirí",
|
||||
"following": "Ag leanúint",
|
||||
"follow": "Lean",
|
||||
"home_instance": "Féach ar ásc baile",
|
||||
"more": "Tuilleadh",
|
||||
"posts": "Poist",
|
||||
"replies": "Freagraí",
|
||||
"media": "Meáin",
|
||||
"loading": "ag lódáil an próifíl..."
|
||||
},
|
||||
|
||||
"logs": {
|
||||
"logged_in": "Logáilte isteach mar %1",
|
||||
"server_detected": "Freastalaí braite mar %1 (%2) le cumais: {%3}}",
|
||||
"server_unsupported": "Ní thacaítear le freastalaí %1 (%2). D'fhéadfadh rudaí briseadh, nó gan oibriú mar a bhíothas ag súil leis.",
|
||||
"no_hostname": "Rinneadh iarracht ceangal le freastalaí gan ainm óstach a sholáthar",
|
||||
"no_https": "Diúltaíodh ceangal leis an bhfreastalaí neamhshábháilte cosúil le cladhaire",
|
||||
"connection_failed": "Theip ar cheangal le %1",
|
||||
"post_fetch_failed": "Theip ar an bpost a fháil",
|
||||
"post_fetch_failed_id": "Theip ar an bpost %1 a fháil",
|
||||
"post_parse_failed": "Theip ar an bpost a pharsáil",
|
||||
"post_parse_failed_id": "Theip ar an bpost %1 a pharsáil",
|
||||
"profile_fetch_failed": "Theip ar phróifíl a fháil",
|
||||
"profile_fetch_failed_id": "Theip ar phróifíl a fháil %1",
|
||||
"token_revoke_failed": "Theip ar chúlghairm an chomhartha! Ag dumpáil sonraí ar aon nós",
|
||||
"sound_does_not_exist": "Rinneadh iarracht fuaim \"%1\" a sheinm, ach níl sé ann!",
|
||||
"account_data_empty": "Rinneadh iarracht sonraí cuntais a pharsáil ach níor soláthraíodh aon sonraí",
|
||||
"timeline_fetch_failed": "Theip ar an amlíne a aisghabháil."
|
||||
},
|
||||
|
||||
"error": {
|
||||
"bad_request": "Droch-iarratas",
|
||||
"invalid_auth_code": "Cód údaraithe neamhbhailí curtha ar fáil",
|
||||
"connection_failed": "Theip ar cheangal le <code>%1</code>.",
|
||||
"post_fetch_failed": "Theip ar aisghabháil an phoist <code>%1</code>."
|
||||
},
|
||||
|
||||
"time": {
|
||||
"in": "i gceann %1",
|
||||
"ago": "%1 ó shin",
|
||||
"second": "so",
|
||||
"minute": "no",
|
||||
"hour": "ua",
|
||||
"day": "lá",
|
||||
"week": "se",
|
||||
"year": "bl"
|
||||
},
|
||||
|
||||
"compose": "Cruthaigh",
|
||||
"search": "Cuardaigh",
|
||||
"loading": "fan nóiméad...",
|
||||
|
||||
"source": "foinse",
|
||||
"issues": "saincheisteanna"
|
||||
}
|
|
@ -1,6 +1,9 @@
|
|||
import { server } from '$lib/client/server.js';
|
||||
import { parseEmoji, renderEmoji } from '$lib/emoji.js';
|
||||
import { get, writable } from 'svelte/store';
|
||||
import Lang from '$lib/lang';
|
||||
|
||||
const lang = Lang();
|
||||
|
||||
const cache = writable({});
|
||||
|
||||
|
@ -11,7 +14,7 @@ const cache = writable({});
|
|||
*/
|
||||
export function parseAccount(data) {
|
||||
if (!data) {
|
||||
console.error("Attempted to parse account data but no data was provided");
|
||||
console.error(lang.string('logs.account_data_empty'));
|
||||
return null;
|
||||
}
|
||||
let account = get(cache)[data.id];
|
||||
|
@ -20,17 +23,26 @@ export function parseAccount(data) {
|
|||
|
||||
account = {};
|
||||
account.id = data.id;
|
||||
account.nickname = data.display_name.trim();
|
||||
account.nickname = data.display_name.trim().replaceAll('<', '<').replaceAll('>', '>');
|
||||
account.username = data.username;
|
||||
account.name = account.nickname || account.username;
|
||||
account.avatar_url = data.avatar;
|
||||
account.banner_url = data.header;
|
||||
account.url = data.url;
|
||||
account.followers_count = data.followers_count;
|
||||
account.following_count = data.following_count;
|
||||
account.posts_count = data.statuses_count;
|
||||
account.bio = data.note;
|
||||
account.bot = data.bot;
|
||||
account.locked = data.locked;
|
||||
|
||||
if (data.acct.includes('@'))
|
||||
account.host = data.acct.split('@')[1];
|
||||
else
|
||||
account.host = get(server).host;
|
||||
|
||||
account.fqn = data.fqn || account.username + account.host;
|
||||
|
||||
account.mention = "@" + account.username;
|
||||
if (account.host != get(server).host)
|
||||
account.mention += "@" + account.host;
|
||||
|
@ -41,6 +53,7 @@ export function parseAccount(data) {
|
|||
});
|
||||
|
||||
account.rich_name = account.nickname ? renderEmoji(account.nickname, account.emojis) : account.username;
|
||||
account.rich_bio = renderEmoji(account.bio, account.emojis);
|
||||
|
||||
cache.update(cache => {
|
||||
cache[account.id] = account;
|
||||
|
|
204
src/lib/api.js
204
src/lib/api.js
|
@ -1,3 +1,32 @@
|
|||
const errors = {
|
||||
AUTHENTICATION_FAILED: "AUTHENTICATION_FAILED",
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses a HTTP Link header
|
||||
* @param {string} header - the HTTP Link header string
|
||||
*/
|
||||
function _parseLinkHeader(header) {
|
||||
// remove whitespace and split
|
||||
let links = header.replace(/\ /g, "").split(",");
|
||||
|
||||
return links.map(l => {
|
||||
let parts = l.split(";");
|
||||
|
||||
// assuming 0th is URL, removing <>
|
||||
let url = new URL(parts[0].slice(1, -1));
|
||||
|
||||
// get rel inbetween double-quotes
|
||||
let rel = parts[1].match(/"(.*?)"/g)[0].slice(1, -1);
|
||||
|
||||
return {
|
||||
url, rel
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
_parseLinkHeader(`<https://wetdry.world/api/v1/timelines/home?max_id=114857293229157171>; rel="next", <https://wetdry.world/api/v1/timelines/home?min_id=114857736990577458>; rel="prev"`)
|
||||
|
||||
/**
|
||||
* GET /api/v1/instance
|
||||
* @param {string} host - The domain of the target server.
|
||||
|
@ -172,16 +201,108 @@ export async function getNotifications(host, token, min_id, max_id, limit, types
|
|||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/follow_requests
|
||||
* @param {string} host - The domain of the target server.
|
||||
* @param {string} token - The application token.
|
||||
* @param {string} min_id - If provided, only shows follow requests since this ID.
|
||||
* @param {string} max_id - If provided, only shows follow requests before this ID.
|
||||
* @param {string} limit - The maximum number of follow requests to retrieve (default 40, max 80).
|
||||
*/
|
||||
export async function getFollowRequests(host, token, since_id, max_id, limit) {
|
||||
let url = `https://${host}/api/v1/follow_requests`;
|
||||
|
||||
let params = new URLSearchParams();
|
||||
if (since_id) params.append("since_id", since_id);
|
||||
if (max_id) params.append("max_id", max_id);
|
||||
if (limit) params.append("limit", limit);
|
||||
const params_string = params.toString();
|
||||
if (params_string) url += '?' + params_string;
|
||||
|
||||
const data = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: { "Authorization": "Bearer " + token }
|
||||
}).then(res => res.json());
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/follow_requests/:account_id/authorize
|
||||
* @param {string} host - The domain of the target server.
|
||||
* @param {string} token - The application token.
|
||||
* @param {string} account_id - The account ID of the follow request to accept
|
||||
*/
|
||||
export async function acceptFollowRequest(host, token, account_id) {
|
||||
let url = `https://${host}/api/v1/follow_requests/${account_id}/authorize`;
|
||||
|
||||
const data = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { "Authorization": "Bearer " + token }
|
||||
}).then(res => res.json());
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/follow_requests/:account_id/reject
|
||||
* @param {string} host - The domain of the target server.
|
||||
* @param {string} token - The application token.
|
||||
* @param {string} account_id - The account ID of the follow request to reject
|
||||
*/
|
||||
export async function rejectFollowRequest(host, token, account_id) {
|
||||
let url = `https://${host}/api/v1/follow_requests/${account_id}/reject`;
|
||||
|
||||
const data = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { "Authorization": "Bearer " + token }
|
||||
}).then(res => res.json());
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/timelines/{timeline}
|
||||
* @param {string} host - The domain of the target server.
|
||||
* @param {string} token - The application token.
|
||||
* @param {string} timeline - The name of the timeline to pull (default "home").
|
||||
* @param {string} max_id - If provided, only shows posts after this ID.
|
||||
* @param {boolean} local_only - If provided, only shows posts from the local instance
|
||||
* @param {boolean} remote_only - If provided, only shows posts from other instances
|
||||
*/
|
||||
export async function getTimeline(host, token, timeline, max_id) {
|
||||
export async function getTimeline(host, token, timeline, max_id, local_only, remote_only) {
|
||||
let url = `https://${host}/api/v1/timelines/${timeline || "home"}`;
|
||||
|
||||
let params = new URLSearchParams();
|
||||
if (max_id) params.append("max_id", max_id);
|
||||
if (remote_only) params.append("remote", remote_only);
|
||||
if (local_only) params.append("local", local_only);
|
||||
const params_string = params.toString();
|
||||
if (params_string) url += '?' + params_string;
|
||||
|
||||
const data = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: { "Authorization": token ? `Bearer ${token}` : null }
|
||||
})
|
||||
|
||||
let links = _parseLinkHeader(data.headers.get("Link"));
|
||||
|
||||
return {
|
||||
data: await data.json(),
|
||||
prev: links.find(f=>f.rel=="prev"),
|
||||
next: links.find(f=>f.rel=="next")
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/favourites
|
||||
* @param {string} host - The domain of the target server.
|
||||
* @param {string} token - The application token.
|
||||
* @param {string} max_id - If provided, only shows posts after this ID.
|
||||
*/
|
||||
export async function getFavourites(host, token, max_id) {
|
||||
let url = `https://${host}/api/v1/favourites`;
|
||||
|
||||
let params = new URLSearchParams();
|
||||
if (max_id) params.append("max_id", max_id);
|
||||
const params_string = params.toString();
|
||||
|
@ -190,9 +311,15 @@ export async function getTimeline(host, token, timeline, max_id) {
|
|||
const data = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: { "Authorization": token ? `Bearer ${token}` : null }
|
||||
}).then(res => res.json());
|
||||
})
|
||||
|
||||
return data;
|
||||
let links = _parseLinkHeader(data.headers.get("Link"));
|
||||
|
||||
return {
|
||||
data: await data.json(),
|
||||
prev: links.find(f=>f.rel=="prev"),
|
||||
next: links.find(f=>f.rel=="next")
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -421,3 +548,74 @@ export async function getUser(host, token, user_id) {
|
|||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/accounts/lookup?acct={handle}
|
||||
* @param {string} host - The domain of the target server.
|
||||
* @param {string} token - The application token.
|
||||
* @param {string} handle - The handle of the user to fetch.
|
||||
*/
|
||||
export async function lookupUser(host, token, handle) {
|
||||
let url = `https://${host}/api/v1/accounts/lookup?acct=${handle}`;
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: { "Authorization": token ? `Bearer ${token}` : null }
|
||||
});
|
||||
if (!res.ok) {
|
||||
const json = await res.json();
|
||||
if (json.error = errors.AUTHENTICATION_FAILED)
|
||||
throw new Error("This method requires authentication");
|
||||
}
|
||||
const data = await res.json();
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/accounts/{user_id}/statuses
|
||||
* @param {string} host - The domain of the target server.
|
||||
* @param {string} token - The application token.
|
||||
* @param {string} user_id - The ID of the user to fetch.
|
||||
* @param {string} max_id - If provided, only shows notifications before this ID.
|
||||
* @param {boolean} replies - If replies should be fetched.
|
||||
* @param {boolean} boosts - If boosts should be fetched.
|
||||
* @param {boolean} only_media - If only media should be fetched.
|
||||
*/
|
||||
export async function getUserPosts(host, token, user_id, max_id, show_replies, show_boosts, only_media) {
|
||||
let url = new URL(`https://${host}/api/v1/accounts/${user_id}/statuses`);
|
||||
let query = [];
|
||||
if (!show_replies)
|
||||
query.push('exclude_replies=true');
|
||||
if (!show_boosts)
|
||||
query.push('exclude_boosts=true');
|
||||
if (only_media)
|
||||
query.push('only_media=true');
|
||||
if (max_id)
|
||||
query.push(`max_id=${max_id}`);
|
||||
url.search = query.join('&');
|
||||
|
||||
const data = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: { "Authorization": token ? `Bearer ${token}` : null }
|
||||
}).then(res => res.json());
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/accounts/{user_id}/statuses?pinned=true
|
||||
* @param {string} host - The domain of the target server.
|
||||
* @param {string} token - The application token.
|
||||
* @param {string} user_id - The ID of the user to fetch.
|
||||
*/
|
||||
export async function getUserPinnedPosts(host, token, user_id) {
|
||||
let url = `https://${host}/api/v1/accounts/${user_id}/statuses?pinned=true`;
|
||||
|
||||
const data = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: { "Authorization": token ? `Bearer ${token}` : null }
|
||||
}).then(res => res.json());
|
||||
|
||||
return data;
|
||||
}
|
|
@ -2,6 +2,9 @@ import * as api from '$lib/api.js';
|
|||
import { writable } from 'svelte/store';
|
||||
import { app_name } from '$lib/config.js';
|
||||
import { browser } from "$app/environment";
|
||||
import Lang from '$lib/lang';
|
||||
|
||||
const lang = Lang();
|
||||
|
||||
const server_types = {
|
||||
UNSUPPORTED: "unsupported",
|
||||
|
@ -35,11 +38,11 @@ server.subscribe(server => {
|
|||
*/
|
||||
export async function createServer(host) {
|
||||
if (!host) {
|
||||
console.error("Attempted to create server without providing a hostname");
|
||||
console.error(lang.string('logs.no_hostname'));
|
||||
return false;
|
||||
}
|
||||
if (host.startsWith("http://")) {
|
||||
console.error("Cowardly refusing to connect to an insecure server");
|
||||
console.error(lang.string('logs.no_https'));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -49,7 +52,7 @@ export async function createServer(host) {
|
|||
if (host.startsWith("https://")) host = host.substring(8);
|
||||
const data = await api.getInstance(host);
|
||||
if (!data) {
|
||||
console.error(`Failed to connect to ${host}`);
|
||||
console.error(lang.string('logs.connection_failed', host));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -58,9 +61,9 @@ export async function createServer(host) {
|
|||
server.capabilities = getCapabilities(server.type);
|
||||
|
||||
if (server.type === server_types.UNSUPPORTED) {
|
||||
console.warn(`Server ${host} is unsupported (${server.version}). Things may break, or not work as expected`);
|
||||
console.warn(lang.string('logs.server_unsupported', host, server.version));
|
||||
} else {
|
||||
console.log(`Server detected as "${server.type}" (${server.version}) with capabilities: {${server.capabilities.join(', ')}}`);
|
||||
console.log(lang.string('logs.server_detected', server.type, server.version, server.capabilities.join(', ')));
|
||||
}
|
||||
|
||||
return server;
|
||||
|
|
28
src/lib/followRequests.js
Normal file
28
src/lib/followRequests.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { server } from './client/server.js';
|
||||
import { writable } from "svelte/store";
|
||||
import * as api from "./api.js";
|
||||
import { app } from './client/app.js';
|
||||
import { get } from 'svelte/store';
|
||||
import { parseAccount } from './account.js';
|
||||
|
||||
// Cache for all requests
|
||||
export let followRequests = writable();
|
||||
|
||||
/**
|
||||
* Gets all follow requests
|
||||
* @param {boolean} force
|
||||
*/
|
||||
export async function fetchFollowRequests(force) {
|
||||
// if already cached, return for now
|
||||
if(!get(followRequests) && !force) return;
|
||||
|
||||
let newReqs = await api.getFollowRequests(
|
||||
get(server).host,
|
||||
get(app).token
|
||||
);
|
||||
|
||||
// parse accounts
|
||||
newReqs = newReqs.map((r) => parseAccount(r));
|
||||
|
||||
followRequests.set(newReqs);
|
||||
}
|
64
src/lib/lang.js
Normal file
64
src/lib/lang.js
Normal file
|
@ -0,0 +1,64 @@
|
|||
import * as en_GB from '@cf/lang/en_GB.json';
|
||||
// import * as ga_IE from '@cf/lang/ga_IE.json';
|
||||
|
||||
/**
|
||||
* @returns Map<string, string | string[]>
|
||||
*/
|
||||
export default function init() {
|
||||
let i18n = new Object();
|
||||
|
||||
// TODO: dynamic loading of language files
|
||||
let language = en_GB;
|
||||
let lang_code = 'en_GB';
|
||||
|
||||
i18n.lang = language;
|
||||
i18n.lang_code = lang_code;
|
||||
i18n.string = function(/* @type string */ key, ...args) {
|
||||
const tokens = key.split('.');
|
||||
|
||||
let i = 0;
|
||||
let token = tokens[i];
|
||||
let res = this.lang;
|
||||
while (true) {
|
||||
res = res[token];
|
||||
if (res === undefined) {
|
||||
console.warn(`${key} not found for language ${this.lang_code}`);
|
||||
return key;
|
||||
}
|
||||
if (typeof res === 'string' || res instanceof String)
|
||||
break;
|
||||
i++;
|
||||
token = tokens[i];
|
||||
}
|
||||
|
||||
i = 1;
|
||||
while (true) {
|
||||
if (args.length < i || !res.includes('%' + i))
|
||||
break;
|
||||
res = res.replaceAll('%' + i, args[i - 1]);
|
||||
i++;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
i18n.stringArray = function(/* @type string */ key) {
|
||||
const tokens = key.split('.');
|
||||
|
||||
let i = 0;
|
||||
let token = tokens[i];
|
||||
let res = this.lang;
|
||||
while (true) {
|
||||
res = res[token];
|
||||
if (res === undefined) {
|
||||
console.warn(`${key} not found for language ${this.lang_code}`);
|
||||
return key;
|
||||
}
|
||||
if (Array.isArray(res))
|
||||
return res;
|
||||
i++;
|
||||
token = tokens[i];
|
||||
}
|
||||
}
|
||||
|
||||
return i18n;
|
||||
}
|
|
@ -1,3 +1,7 @@
|
|||
import Lang from '$lib/lang';
|
||||
|
||||
const lang = Lang();
|
||||
|
||||
import sound_log from '../sound/log.ogg';
|
||||
import sound_hello from '../sound/hello.ogg';
|
||||
import sound_success from '../sound/success.ogg';
|
||||
|
@ -16,7 +20,7 @@ export function playSound(name) {
|
|||
if (!name) name = "default";
|
||||
const sound = sounds[name];
|
||||
if (!sound) {
|
||||
console.warn(`Attempted to play sound "${name}", which does not exist!`);
|
||||
console.warn(lang.string('lang.sound_does_not_exist', name));
|
||||
return;
|
||||
}
|
||||
sound.pause();
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import Lang from '$lib/lang';
|
||||
const lang = Lang();
|
||||
|
||||
const denoms = [
|
||||
{ unit: 's', min: 0 },
|
||||
{ unit: 'm', min: 60 },
|
||||
{ unit: 'h', min: 60 },
|
||||
{ unit: 'd', min: 24 },
|
||||
{ unit: 'w', min: 7 },
|
||||
{ unit: 'y', min: 52 },
|
||||
{ unit: lang.string('time.second'), min: 0 },
|
||||
{ unit: lang.string('time.minute'), min: 60 },
|
||||
{ unit: lang.string('time.hour'), min: 60 },
|
||||
{ unit: lang.string('time.day'), min: 24 },
|
||||
{ unit: lang.string('time.week'), min: 7 },
|
||||
{ unit: lang.string('time.year'), min: 52 },
|
||||
];
|
||||
|
||||
export function shorthand(date) {
|
||||
|
@ -18,6 +21,6 @@ export function shorthand(date) {
|
|||
unit = denoms[index].unit;
|
||||
}
|
||||
if (value > 0)
|
||||
return Math.floor(value) + unit + " ago";
|
||||
return "in " + Math.floor(value) + unit;
|
||||
return lang.string('time.ago').replaceAll('%1', Math.floor(value) + unit);
|
||||
return lang.string('time.in').replaceAll('%1', Math.floor(value) + unit);
|
||||
}
|
||||
|
|
|
@ -3,44 +3,66 @@ import { server } from '$lib/client/server.js';
|
|||
import { app } from '$lib/client/app.js';
|
||||
import { get, writable } from 'svelte/store';
|
||||
import { parsePost } from '$lib/post.js';
|
||||
import Lang from '$lib/lang';
|
||||
|
||||
export const timeline = writable([]);
|
||||
|
||||
let loading = false;
|
||||
const lang = Lang();
|
||||
|
||||
export async function getTimeline(clean) {
|
||||
let loading = false;
|
||||
let last_post = false;
|
||||
|
||||
export async function getTimeline(timelineType = "home", clean, localOnly = false, remoteOnly = false) {
|
||||
if (loading) return; // no spamming!!
|
||||
loading = true;
|
||||
|
||||
let last_post = false;
|
||||
if (!clean && get(timeline).length > 0)
|
||||
last_post = get(timeline)[get(timeline).length - 1].id;
|
||||
if(clean) {
|
||||
timeline.set([]);
|
||||
last_post = false;
|
||||
}
|
||||
|
||||
const timeline_data = await api.getTimeline(
|
||||
get(server).host,
|
||||
get(app).token,
|
||||
"home",
|
||||
last_post
|
||||
);
|
||||
let timeline_data;
|
||||
switch(timelineType) {
|
||||
case "favourites":
|
||||
timeline_data = await api.getFavourites(
|
||||
get(server).host,
|
||||
get(app).token,
|
||||
last_post
|
||||
)
|
||||
break;
|
||||
|
||||
default:
|
||||
timeline_data = await api.getTimeline(
|
||||
get(server).host,
|
||||
get(app).token,
|
||||
timelineType,
|
||||
last_post,
|
||||
localOnly,
|
||||
remoteOnly
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!timeline_data) {
|
||||
console.error(`Failed to retrieve timeline.`);
|
||||
console.error(lang.string('logs.timeline_fetch_failed'));
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (clean) timeline.set([]);
|
||||
if (!clean) {
|
||||
last_post = timeline_data.next.url.searchParams.get("max_id")
|
||||
}
|
||||
|
||||
for (let i in timeline_data) {
|
||||
const post_data = timeline_data[i];
|
||||
for (let i in timeline_data.data) {
|
||||
const post_data = timeline_data.data[i];
|
||||
const post = await parsePost(post_data, 1);
|
||||
if (!post) {
|
||||
if (post === null || post === undefined) {
|
||||
if (post_data.id) {
|
||||
console.warn("Failed to parse post #" + post_data.id);
|
||||
console.warn(lang.string('logs.post_parse_failed_id', post_data.id));
|
||||
} else {
|
||||
console.warn("Failed to parse post:");
|
||||
console.warn(post_data);
|
||||
console.warn(lang.string('logs.post_parse_failed'));
|
||||
console.debug(post_data);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
|
|
|
@ -5,53 +5,64 @@
|
|||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let className = "";
|
||||
export { className as class };
|
||||
export let active = false;
|
||||
export let filled = false;
|
||||
export let disabled = false;
|
||||
export let centered = false;
|
||||
export let label = undefined;
|
||||
export let sound = "default";
|
||||
export let href = false;
|
||||
export let href = undefined;
|
||||
export let onClick = undefined;
|
||||
|
||||
let classes = [];
|
||||
|
||||
function click() {
|
||||
if (disabled) return;
|
||||
if (href) {
|
||||
location = href;
|
||||
return;
|
||||
}
|
||||
playSound(sound);
|
||||
dispatch('click');
|
||||
}
|
||||
|
||||
afterUpdate(() => {
|
||||
classes = [];
|
||||
if (active) classes = ["active"];
|
||||
if (filled) classes = ["filled"];
|
||||
if (disabled) classes = ["disabled"];
|
||||
classes = className.split(' ');
|
||||
if (active) classes.push("active");
|
||||
if (filled) classes.push("filled");
|
||||
if (disabled) classes.push("disabled");
|
||||
if (centered) classes.push("centered");
|
||||
});
|
||||
</script>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class={classes.join(' ')}
|
||||
title={label}
|
||||
aria-label={label}
|
||||
on:click={() => click()}>
|
||||
<span class="icon">
|
||||
<slot name="icon" />
|
||||
</span>
|
||||
<slot/>
|
||||
</button>
|
||||
{#if href}
|
||||
<a
|
||||
class={classes.join(' ')}
|
||||
title={label}
|
||||
aria-label={label}
|
||||
href={href}
|
||||
on:click={() => click()}>
|
||||
<span class="icon">
|
||||
<slot name="icon" />
|
||||
</span>
|
||||
<slot/>
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
class={classes.join(' ')}
|
||||
title={label}
|
||||
aria-label={label}
|
||||
on:click={() => click()}>
|
||||
<span class="icon">
|
||||
<slot name="icon" />
|
||||
</span>
|
||||
<slot/>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
button {
|
||||
/* min-width: 64px; */
|
||||
width: 100%;
|
||||
height: 54px;
|
||||
padding: 16px;
|
||||
a, button {
|
||||
height: fit-content;
|
||||
padding: .7em .8em;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
@ -60,14 +71,13 @@
|
|||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
text-decoration: none;
|
||||
|
||||
border-radius: 8px;
|
||||
border-width: 2px;
|
||||
border-style: solid;
|
||||
border: 2px solid var(--bg-700);
|
||||
|
||||
background-color: var(--bg-700);
|
||||
color: var(--text);
|
||||
border-color: transparent;
|
||||
|
||||
transition-property: border-color, background-color, color;
|
||||
transition-timing-function: ease-out;
|
||||
|
@ -75,22 +85,32 @@
|
|||
|
||||
cursor: pointer;
|
||||
}
|
||||
a {
|
||||
width: calc(100% - 1.6em);
|
||||
}
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
a.centered,
|
||||
button.centered {
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
a:hover,
|
||||
button:hover {
|
||||
background-color: color-mix(in srgb, var(--bg-700), var(--accent) 10%);
|
||||
border-color: color-mix(in srgb, var(--bg-700), var(--accent) 20%);
|
||||
border-color: color-mix(in srgb, var(--bg-700), black 10%);
|
||||
background-color: color-mix(in srgb, var(--bg-700), black 10%);
|
||||
}
|
||||
|
||||
a:active,
|
||||
button:active {
|
||||
background-color: color-mix(in srgb, var(--bg-700), var(--bg-800) 50%);
|
||||
border-color: color-mix(in srgb, var(--bg-700), var(--bg-800) 10%);
|
||||
border-color: color-mix(in srgb, var(--bg-700), black 20%);
|
||||
background-color: color-mix(in srgb, var(--bg-700), black 20%);
|
||||
}
|
||||
|
||||
a.active,
|
||||
button.active {
|
||||
background-color: var(--bg-600);
|
||||
color: var(--accent);
|
||||
|
@ -98,50 +118,49 @@
|
|||
text-shadow: 0px 2px 32px var(--accent);
|
||||
}
|
||||
|
||||
a.active:hover,
|
||||
button.active:hover {
|
||||
color: color-mix(in srgb, var(--accent), var(--bg-1000) 20%);
|
||||
border-color: color-mix(in srgb, var(--accent), var(--bg-1000) 20%);
|
||||
background-color: color-mix(in srgb, var(--bg-600), var(--accent) 10%);
|
||||
}
|
||||
|
||||
a.active:active,
|
||||
button.active:active {
|
||||
color: color-mix(in srgb, var(--accent), var(--bg-800) 10%);
|
||||
border-color: color-mix(in srgb, var(--accent), var(--bg-800) 10%);
|
||||
background-color: color-mix(in srgb, var(--bg-600), var(--bg-800) 10%);
|
||||
}
|
||||
|
||||
a.filled,
|
||||
button.filled {
|
||||
background-color: var(--accent);
|
||||
color: var(--bg-800);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
button.filled:hover {
|
||||
a.filled:not(.disabled):hover,
|
||||
button.filled:not(.disabled):hover {
|
||||
color: color-mix(in srgb, var(--bg-800), white 10%);
|
||||
background-color: color-mix(in srgb, var(--accent), white 20%);
|
||||
}
|
||||
|
||||
button.filled:active {
|
||||
a.filled:not(.disabled):active,
|
||||
button.filled:not(.disabled):active {
|
||||
color: color-mix(in srgb, var(--bg-800), black 10%);
|
||||
background-color: color-mix(in srgb, var(--accent), black 20%);
|
||||
}
|
||||
|
||||
a.disabled,
|
||||
button.disabled {
|
||||
background-color: var(--bg-700);
|
||||
color: var(--text);
|
||||
opacity: .5;
|
||||
opacity: .35;
|
||||
border-color: transparent;
|
||||
cursor: initial;
|
||||
}
|
||||
|
||||
button.disabled:hover {
|
||||
}
|
||||
|
||||
button.disabled:active {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.icon:not(:empty) {
|
||||
height: 150%;
|
||||
height: 1.8em;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import { timeline } from '$lib/timeline.js';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { playSound } from '$lib/sound';
|
||||
import Lang from '$lib/lang'
|
||||
|
||||
import Button from '@cf/ui/Button.svelte';
|
||||
import PostIcon from '@cf/icons/post.svg';
|
||||
|
@ -19,6 +20,8 @@
|
|||
import FollowersVisIcon from '@cf/icons/followers.svg';
|
||||
import PrivateVisIcon from '@cf/icons/dm.svg';
|
||||
|
||||
const lang = Lang();
|
||||
|
||||
export let reply_id;
|
||||
|
||||
let content_warning = ""
|
||||
|
@ -26,16 +29,11 @@
|
|||
// let media_ids = [];
|
||||
let show_cw = false;
|
||||
let visibility = "Public";
|
||||
let visibilityLocale = lang.string('post.visibility.public');
|
||||
|
||||
const placeholders = [
|
||||
"What's cooking, $1?",
|
||||
"Speak your mind!",
|
||||
"Federate something...",
|
||||
"I sure love posting!",
|
||||
"Another day, another $1 post!",
|
||||
];
|
||||
let placeholder = placeholders[Math.floor(placeholders.length * Math.random())]
|
||||
.replaceAll("$1", $account.username);
|
||||
const placeholders = lang.stringArray('compose_placeholders');
|
||||
let placeholder = Array.isArray(placeholders) ? placeholders[Math.floor(placeholders.length * Math.random())]
|
||||
.replaceAll("%1", $account.username) : placeholders;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
|
@ -77,15 +75,19 @@
|
|||
switch (visibility) {
|
||||
case "Public":
|
||||
visibility = "Unlisted";
|
||||
visibilityLocale = lang.string('post.visibility.unlisted');
|
||||
break;
|
||||
case "Unlisted":
|
||||
visibility = "Followers only";
|
||||
visibilityLocale = lang.string('post.visibility.follow_only');
|
||||
break;
|
||||
case "Followers only":
|
||||
visibility = "Private";
|
||||
visibilityLocale = lang.string('post.visibility.private');
|
||||
break;
|
||||
case "Private":
|
||||
visibility = "Public";
|
||||
visibilityLocale = lang.string('post.visibility.public');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -93,7 +95,8 @@
|
|||
|
||||
<div class="composer">
|
||||
<div class="composer-header-container">
|
||||
<a href={$account.url} target="_blank" class="composer-avatar-container" on:mouseup|stopPropagation>
|
||||
<!-- TODO: account switcher in composer -->
|
||||
<a href="" class="composer-avatar-container" on:mouseup|stopPropagation>
|
||||
<img src={$account.avatar_url} type={$account.avatar_type} alt="" width="48" height="48" class="composer-avatar" loading="lazy" decoding="async">
|
||||
</a>
|
||||
<header class="composer-header">
|
||||
|
@ -104,7 +107,7 @@
|
|||
<div class="composer-info" on:mouseup|stopPropagation>
|
||||
</div>
|
||||
</header>
|
||||
<div title={visibility}>
|
||||
<div title={visibilityLocale}>
|
||||
<Button centered={true} on:click={() => {cycleVisibility()}}>
|
||||
<svelte:fragment slot="icon">
|
||||
<!-- TODO: this should be a drop-down option!...later -->
|
||||
|
@ -122,7 +125,7 @@
|
|||
</div>
|
||||
</div>
|
||||
{#if show_cw}
|
||||
<input type="text" id="" placeholder="Content warning" bind:value={content_warning}/>
|
||||
<input type="text" id="" placeholder="{lang.string('post.warning.placeholder')}" bind:value={content_warning}/>
|
||||
{/if}
|
||||
<textarea placeholder="{placeholder}" class="textbox" bind:value={content}></textarea>
|
||||
<div class="composer-footer">
|
||||
|
|
|
@ -3,9 +3,12 @@
|
|||
import { server, createServer } from '$lib/client/server.js';
|
||||
import { app } from '$lib/client/app.js';
|
||||
import { get } from 'svelte/store';
|
||||
import Lang from '$lib/lang';
|
||||
|
||||
import Logo from '$lib/../img/campfire-logo.svg';
|
||||
|
||||
const lang = Lang();
|
||||
|
||||
let display_error = false;
|
||||
let logging_in = false;
|
||||
|
||||
|
@ -17,21 +20,21 @@
|
|||
const host = event.target.host.value;
|
||||
|
||||
if (!host || host === "") {
|
||||
display_error = "Please enter an server domain.";
|
||||
display_error = lang.string('login.error.no_domain');
|
||||
logging_in = false;
|
||||
return;
|
||||
}
|
||||
|
||||
server.set(await createServer(host));
|
||||
if (!get(server)) {
|
||||
display_error = "Failed to connect to the server.\nCheck the browser console for details!"
|
||||
display_error = lang.string('login.error.connection_failed');
|
||||
logging_in = false;
|
||||
return;
|
||||
}
|
||||
|
||||
app.set(await api.createApp(get(server).host));
|
||||
if (!get(app)) {
|
||||
display_error = "Failed to create an application for this server."
|
||||
display_error = lang.string('login.error.create_app');
|
||||
logging_in = false;
|
||||
return;
|
||||
}
|
||||
|
@ -44,8 +47,8 @@
|
|||
<div class="app-logo">
|
||||
<Logo />
|
||||
</div>
|
||||
<p>Welcome, fediverse user!</p>
|
||||
<p>Please enter your server domain to log in.</p>
|
||||
<p>{lang.string('login.welcome')}</p>
|
||||
<p>{lang.string('login.enter_domain')}</p>
|
||||
<div class="input-wrapper">
|
||||
<input type="text" id="host" aria-label="server domain" class={logging_in ? "throb" : ""}>
|
||||
{#if display_error}
|
||||
|
@ -53,16 +56,10 @@
|
|||
{/if}
|
||||
</div>
|
||||
<br>
|
||||
<button type="submit" id="login" class={logging_in ? "disabled" : ""}>Log in</button>
|
||||
<p><small>
|
||||
Please note this is
|
||||
<strong><em>extremely experimental software</em></strong>;
|
||||
things are likely to break!
|
||||
<br>
|
||||
If that's all cool with you, welcome aboard!
|
||||
</small></p>
|
||||
<button type="submit" id="login" class={logging_in ? "disabled" : ""}>{lang.string('login.button')}</button>
|
||||
<p><small>{@html lang.string('login.experimental')}</small></p>
|
||||
|
||||
<p class="form-footer">made with ❤ by <a href="https://bliss.town">bliss town</a>, 2024</p>
|
||||
<p class="form-footer">{@html lang.string('login.made_with_tagline')}</p>
|
||||
</form>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
z-index: 101;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
pointer-events: none;
|
||||
|
@ -38,7 +38,7 @@
|
|||
z-index: 101;
|
||||
|
||||
padding: 16px;
|
||||
width: 732px;
|
||||
width: 700px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0px 16px 64px 4px rgba(0,0,0,0.5);
|
||||
animation: modal_pop_up .15s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
|
@ -50,7 +50,7 @@
|
|||
.overlay {
|
||||
width: 100vw;
|
||||
height: 100vw;
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
|
|
|
@ -4,62 +4,46 @@
|
|||
import { server } from '$lib/client/server.js';
|
||||
import { app } from '$lib/client/app.js';
|
||||
import { playSound } from '$lib/sound.js';
|
||||
import { getTimeline } from '$lib/timeline.js';
|
||||
import { getNotifications } from '$lib/notifications.js';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { notifications, unread_notif_count } from '$lib/notifications.js';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { unread_notif_count } from '$lib/notifications.js';
|
||||
import { fetchFollowRequests, followRequests } from '$lib/followRequests.js'
|
||||
import Lang from '$lib/lang';
|
||||
|
||||
import Logo from '$lib/../img/campfire-logo.svg';
|
||||
import Button from './Button.svelte';
|
||||
|
||||
import TimelineIcon from '../../img/icons/timeline.svg';
|
||||
import NotificationsIcon from '../../img/icons/notifications.svg';
|
||||
import ExploreIcon from '../../img/icons/explore.svg';
|
||||
import ListIcon from '../../img/icons/lists.svg';
|
||||
import FavouritesIcon from '../../img/icons/like_fill.svg';
|
||||
import BookmarkIcon from '../../img/icons/bookmark.svg';
|
||||
import HashtagIcon from '../../img/icons/hashtag.svg';
|
||||
import PostIcon from '../../img/icons/post.svg';
|
||||
import InfoIcon from '../../img/icons/info.svg';
|
||||
import SettingsIcon from '../../img/icons/settings.svg';
|
||||
import LogoutIcon from '../../img/icons/logout.svg';
|
||||
import TimelineIcon from '@cf/icons/timeline.svg';
|
||||
import NotificationsIcon from '@cf/icons/notifications.svg';
|
||||
import ExploreIcon from '@cf/icons/explore.svg';
|
||||
import ListIcon from '@cf/icons/lists.svg';
|
||||
import FavouritesIcon from '@cf/icons/like_fill.svg';
|
||||
import BookmarkIcon from '@cf/icons/bookmark.svg';
|
||||
import HashtagIcon from '@cf/icons/hashtag.svg';
|
||||
import PostIcon from '@cf/icons/post.svg';
|
||||
import InfoIcon from '@cf/icons/info.svg';
|
||||
import SettingsIcon from '@cf/icons/settings.svg';
|
||||
import LogoutIcon from '@cf/icons/logout.svg';
|
||||
import FollowersIcon from '@cf/icons/followers.svg';
|
||||
|
||||
const VERSION = APP_VERSION;
|
||||
const COMMIT = APP_COMMIT;
|
||||
const lang = Lang();
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function handle_btn(name) {
|
||||
function gotoProfile() {
|
||||
if (!$account) return;
|
||||
let route;
|
||||
switch (name) {
|
||||
case "timeline":
|
||||
route = "/";
|
||||
getTimeline(true);
|
||||
break;
|
||||
case "notifications":
|
||||
route = "/notifications";
|
||||
notifications.set([]);
|
||||
getNotifications();
|
||||
break;
|
||||
case "explore":
|
||||
case "lists":
|
||||
case "favourites":
|
||||
case "bookmarks":
|
||||
case "hashtags":
|
||||
default:
|
||||
return;
|
||||
}
|
||||
if (!route) return;
|
||||
playSound();
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: "smooth"
|
||||
});
|
||||
goto(route);
|
||||
goto(`/${$server.host}/${$account.username}`);
|
||||
}
|
||||
|
||||
async function log_out() {
|
||||
async function logOut() {
|
||||
if (!confirm("This will log you out. Are you sure?")) return;
|
||||
|
||||
const res = await api.revokeToken(
|
||||
|
@ -70,7 +54,7 @@
|
|||
);
|
||||
|
||||
if (!res.ok)
|
||||
console.warn("Token revocation failed! Dumping data anyways");
|
||||
console.warn(lang.string('logs.token_revoke_failed'));
|
||||
|
||||
account.set(false);
|
||||
app.set(false);
|
||||
|
@ -78,6 +62,10 @@
|
|||
|
||||
goto("/");
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await fetchFollowRequests(true)
|
||||
})
|
||||
</script>
|
||||
|
||||
<div id="navigation">
|
||||
|
@ -90,78 +78,94 @@
|
|||
{#if $account}
|
||||
<div id="nav-items">
|
||||
<Button label="Timeline"
|
||||
on:click={() => handle_btn("timeline")}
|
||||
href="/")}
|
||||
active={$page.url.pathname === "/"}>
|
||||
<svelte:fragment slot="icon">
|
||||
<TimelineIcon/>
|
||||
</svelte:fragment>
|
||||
Timeline
|
||||
{lang.string('navigation.timeline')}
|
||||
</Button>
|
||||
<Button label="Notifications"
|
||||
on:click={() => handle_btn("notifications")}
|
||||
href="/notifications"}
|
||||
active={$page.url.pathname === "/notifications"}>
|
||||
<svelte:fragment slot="icon">
|
||||
<NotificationsIcon/>
|
||||
</svelte:fragment>
|
||||
Notifications
|
||||
{lang.string('navigation.notifications')}
|
||||
{#if $unread_notif_count}
|
||||
<span class="notification-count">
|
||||
{$unread_notif_count <= 99 ? $unread_notif_count : "99+"}
|
||||
</span>
|
||||
{/if}
|
||||
</Button>
|
||||
{#if $followRequests.length > 0}
|
||||
<Button label="Follow requests"
|
||||
href="/follow-requests"}
|
||||
active={$page.url.pathname === "/follow-requests"}>
|
||||
<svelte:fragment slot="icon">
|
||||
<FollowersIcon/>
|
||||
</svelte:fragment>
|
||||
{lang.string('navigation.follow_requests')}
|
||||
<span class="notification-count">
|
||||
{$followRequests.length}
|
||||
</span>
|
||||
</Button>
|
||||
{/if}
|
||||
<Button label="Explore" disabled>
|
||||
<svelte:fragment slot="icon">
|
||||
<ExploreIcon height="auto"/>
|
||||
</svelte:fragment>
|
||||
Explore
|
||||
{lang.string('navigation.explore')}
|
||||
</Button>
|
||||
<Button label="Lists" disabled>
|
||||
<svelte:fragment slot="icon">
|
||||
<ListIcon/>
|
||||
</svelte:fragment>
|
||||
Lists
|
||||
{lang.string('navigation.lists')}
|
||||
</Button>
|
||||
|
||||
<div class="flex-row">
|
||||
<Button centered label="Favourites" disabled>
|
||||
<Button centered
|
||||
label="{lang.string('navigation.favourites')}"
|
||||
href="/favourites"}
|
||||
active={$page.url.pathname === "/favourites"}>
|
||||
<svelte:fragment slot="icon">
|
||||
<FavouritesIcon/>
|
||||
</svelte:fragment>
|
||||
</Button>
|
||||
<Button centered label="Bookmarks" disabled>
|
||||
<Button centered label="{lang.string('navigation.bookmarks')}" disabled>
|
||||
<svelte:fragment slot="icon">
|
||||
<BookmarkIcon/>
|
||||
</svelte:fragment>
|
||||
</Button>
|
||||
<Button centered label="Hashtags" disabled>
|
||||
<Button centered label="{lang.string('navigation.hashtags')}" disabled>
|
||||
<svelte:fragment slot="icon">
|
||||
<HashtagIcon/>
|
||||
</svelte:fragment>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button filled label="Post" on:click={() => dispatch("compose")}>
|
||||
<Button filled label="{lang.string('compose')}" on:click={() => dispatch("compose")}>
|
||||
<svelte:fragment slot="icon">
|
||||
<PostIcon/>
|
||||
</svelte:fragment>
|
||||
Post
|
||||
{lang.string('compose')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div id="account-items">
|
||||
<div class="flex-row">
|
||||
<Button centered label="Profile information" disabled>
|
||||
<Button centered label="{lang.string('navigation.profile_information')}" disabled>
|
||||
<svelte:fragment slot="icon">
|
||||
<InfoIcon/>
|
||||
</svelte:fragment>
|
||||
</Button>
|
||||
<Button centered label="Settings" disabled>
|
||||
<Button centered label="{lang.string('navigation.settings')}" disabled>
|
||||
<svelte:fragment slot="icon">
|
||||
<SettingsIcon/>
|
||||
</svelte:fragment>
|
||||
</Button>
|
||||
<Button centered label="Log out" on:click={() => log_out()}>
|
||||
<Button centered label="{lang.string('navigation.log_out')}" on:click={() => logOut()}>
|
||||
<svelte:fragment slot="icon">
|
||||
<LogoutIcon/>
|
||||
</svelte:fragment>
|
||||
|
@ -169,11 +173,11 @@
|
|||
</div>
|
||||
|
||||
<div id="account-button">
|
||||
<img src={$account.avatar_url} class="account-avatar" height="64px" alt="" aria-hidden="true" on:click={() => playSound()}>
|
||||
<img src={$account.avatar_url} class="account-avatar" height="64px" alt="" aria-hidden="true" on:click={() => gotoProfile()}>
|
||||
<div class="account-name" aria-hidden="true">
|
||||
<a href={$account.url} class="nickname" title={$account.nickname}>{@html $account.rich_name}</a>
|
||||
<a href="/{$server.host}/@{$account.username}" class="nickname" title={$account.nickname}>{@html $account.rich_name}</a>
|
||||
<span class="username" title={`@${$account.username}@${$account.host}`}>
|
||||
{`@${$account.username}@${$account.host}`}
|
||||
{$account.fqn}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -181,11 +185,11 @@
|
|||
{/if}
|
||||
|
||||
<span class="version">
|
||||
campfire v{VERSION}
|
||||
campfire v{VERSION} ({COMMIT})
|
||||
<br>
|
||||
<ul>
|
||||
<li><a href="https://git.arimelody.me/blisstown/campfire">source</a></li>
|
||||
<li><a href="https://github.com/blisstown/campfire/issues">issues</a></li>
|
||||
<li><a href="https://git.arimelody.me/blisstown/campfire">{lang.string('source')}</a></li>
|
||||
<li><a href="https://codeberg.org/arimelody/campfire/issues">{lang.string('issues')}</a></li>
|
||||
</ul>
|
||||
</span>
|
||||
</div>
|
||||
|
@ -200,6 +204,8 @@
|
|||
height: calc(100vh - 32px);
|
||||
border-radius: 8px;
|
||||
background-color: var(--bg-800);
|
||||
transition: background-color .1s linear;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.server-header {
|
||||
|
@ -213,6 +219,7 @@
|
|||
background-size: cover;
|
||||
background-color: var(--bg-600);
|
||||
background-image: linear-gradient(to top, var(--bg-800), var(--bg-600));
|
||||
transition: background .1s linear;
|
||||
}
|
||||
|
||||
.server-icon {
|
||||
|
@ -270,6 +277,7 @@
|
|||
font-size: .9em;
|
||||
opacity: .6;
|
||||
text-align: center;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.version ul {
|
||||
|
|
|
@ -2,17 +2,20 @@
|
|||
import { server } from '$lib/client/server';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
import ReplyIcon from '$lib/../img/icons/reply.svg';
|
||||
import RepostIcon from '$lib/../img/icons/repost.svg';
|
||||
import FavouriteIcon from '$lib/../img/icons/like.svg';
|
||||
import ReactIcon from '$lib/../img/icons/react.svg';
|
||||
import ReplyIcon from '@cf/icons/reply.svg';
|
||||
import RepostIcon from '@cf/icons/repost.svg';
|
||||
import FavouriteIcon from '@cf/icons/like.svg';
|
||||
import ReactIcon from '@cf/icons/react.svg';
|
||||
// import QuoteIcon from '$lib/../img/icons/quote.svg';
|
||||
import ReactionBar from '$lib/ui/post/ReactionBar.svelte';
|
||||
import ActionBar from '$lib/ui/post/ActionBar.svelte';
|
||||
import Lang from '$lib/lang';
|
||||
|
||||
const lang = Lang();
|
||||
|
||||
let mention = (accounts) => {
|
||||
let res = `<a href=${account.url}>${account.rich_name}</a>`;
|
||||
if (accounts.length > 1) res += ` and <strong>${accounts.length - 1}</strong> others`;
|
||||
let res = `<a href="/${$server.host}/${account.fqn}">${account.rich_name}</a>`;
|
||||
if (accounts.length > 1) res += ' ' + lang.string('notification.and_others').replaceAll('%1', accounts.length - 1);
|
||||
return res;
|
||||
};
|
||||
|
||||
|
@ -20,23 +23,23 @@
|
|||
let activity_text = function (type) {
|
||||
switch (type) {
|
||||
case "mention":
|
||||
return `%1 mentioned you.`;
|
||||
return lang.string('notification.mention');
|
||||
case "reblog":
|
||||
return `%1 boosted your post.`;
|
||||
return lang.string('notification.reblog');
|
||||
case "reaction":
|
||||
return `%1 reacted to your post.`;
|
||||
return lang.string('notification.reaction');
|
||||
case "follow":
|
||||
return `%1 followed you.`;
|
||||
return lang.string('notification.follow');
|
||||
case "follow_request":
|
||||
return `%1 requested to follow you.`;
|
||||
return lang.string('notification.follow.request');
|
||||
case "favourite":
|
||||
return `%1 favourited your post.`;
|
||||
return lang.string('notification.favourite');
|
||||
case "poll":
|
||||
return `%1's poll as ended.`;
|
||||
return lang.string('notification.poll');
|
||||
case "update":
|
||||
return `%1 updated their post.`;
|
||||
return lang.string('notification.update');
|
||||
default:
|
||||
return `%1 poked you!`;
|
||||
return lang.string('notification.default');
|
||||
}
|
||||
}(data.type);
|
||||
|
||||
|
@ -87,7 +90,7 @@
|
|||
</span>
|
||||
<span class="notif-avatars">
|
||||
{#if data.accounts.length == 1}
|
||||
<a href={data.accounts[0].url} class="notif-avatar">
|
||||
<a href="/{$server.host}/{data.accounts[0].fqn}" class="notif-avatar">
|
||||
<img src={data.accounts[0].avatar_url} alt="" width="28" height="28" />
|
||||
</a>
|
||||
{:else}
|
||||
|
@ -141,18 +144,22 @@
|
|||
<style>
|
||||
.notification {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
border-top: 1px solid color-mix(in srgb, transparent, var(--text) 25%);
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
background: var(--bg-800);
|
||||
text-decoration: inherit;
|
||||
color: inherit;
|
||||
transition: background-color .1s;
|
||||
cursor: pointer;
|
||||
|
||||
background-color: var(--bg-900);
|
||||
}
|
||||
.notification:first-of-type {
|
||||
border-top: none;
|
||||
|
||||
}
|
||||
|
||||
.notification:hover {
|
||||
background-color: color-mix(in srgb, var(--bg-800), black 5%);
|
||||
background-color: color-mix(in srgb, var(--bg-800), transparent 35%);
|
||||
}
|
||||
|
||||
header {
|
||||
|
@ -280,7 +287,7 @@
|
|||
width: calc(100% - 16px);
|
||||
margin-bottom: 10px;
|
||||
padding: 4px 8px;
|
||||
--warn-bg: color-mix(in srgb, var(--bg-700), var(--accent) 1%);
|
||||
--warn-bg: color-mix(in srgb, transparent, var(--bg-700) 50%);
|
||||
background: repeating-linear-gradient(-45deg, transparent, transparent 10px, var(--warn-bg) 10px, var(--warn-bg) 20px);
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
<script>
|
||||
import Lang from '$lib/lang';
|
||||
|
||||
const lang = Lang();
|
||||
</script>
|
||||
|
||||
<div id="widgets">
|
||||
<input type="text" id="search" placeholder="Search">
|
||||
<input type="text" id="search" placeholder="{lang.string('search')}">
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
|
36
src/lib/ui/core/PageHeader.svelte
Normal file
36
src/lib/ui/core/PageHeader.svelte
Normal file
|
@ -0,0 +1,36 @@
|
|||
<script>
|
||||
export let title;
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<h1>{title}</h1>
|
||||
<div class="header-items">
|
||||
<slot/>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
header {
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
margin: 16px 0;
|
||||
padding: 0 8px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
user-select: none;
|
||||
box-sizing: border-box;
|
||||
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
header .header-items {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
|
@ -6,19 +6,22 @@
|
|||
import { timeline } from '$lib/timeline';
|
||||
import { parseReactions } from '$lib/post';
|
||||
import { playSound } from '$lib/sound';
|
||||
import Lang from '$lib/lang';
|
||||
|
||||
import ActionButton from './ActionButton.svelte';
|
||||
|
||||
import ReplyIcon from '../../../img/icons/reply.svg';
|
||||
import RepostIcon from '../../../img/icons/repost.svg';
|
||||
import FavouriteIcon from '../../../img/icons/like.svg';
|
||||
import FavouriteIconFill from '../../../img/icons/like_fill.svg';
|
||||
import QuoteIcon from '../../../img/icons/quote.svg';
|
||||
import MoreIcon from '../../../img/icons/more.svg';
|
||||
import DeleteIcon from '../../../img/icons/bin.svg';
|
||||
import ReplyIcon from '@cf/icons/reply.svg';
|
||||
import RepostIcon from '@cf/icons/repost.svg';
|
||||
import FavouriteIcon from '@cf/icons/like.svg';
|
||||
import FavouriteIconFill from '@cf/icons/like_fill.svg';
|
||||
import QuoteIcon from '@cf/icons/quote.svg';
|
||||
import MoreIcon from '@cf/icons/more.svg';
|
||||
import DeleteIcon from '@cf/icons/bin.svg';
|
||||
|
||||
export let post;
|
||||
|
||||
const lang = Lang();
|
||||
|
||||
async function toggleBoost() {
|
||||
if (!$app || !$app.token) return;
|
||||
|
||||
|
@ -74,29 +77,29 @@
|
|||
</script>
|
||||
|
||||
<div class="post-actions" aria-label="Post actions" role="toolbar" tabindex="0" on:mouseup|stopPropagation on:keydown|stopPropagation>
|
||||
<ActionButton type="reply" label="Reply" bind:count={post.reply_count} sound="post" disabled>
|
||||
<ActionButton type="reply" label="{lang.string('post.actions.reply')}" bind:count={post.reply_count} sound="post" disabled>
|
||||
<ReplyIcon/>
|
||||
</ActionButton>
|
||||
<ActionButton type="boost" label="Boost" on:click={toggleBoost} bind:active={post.boosted} bind:count={post.boost_count} disabled={!$account}>
|
||||
<ActionButton type="boost" label="{lang.string('post.actions.boost')}" on:click={toggleBoost} bind:active={post.boosted} bind:count={post.boost_count} disabled={!$account}>
|
||||
<RepostIcon/>
|
||||
<svelte:fragment slot="activeIcon">
|
||||
<RepostIcon/>
|
||||
</svelte:fragment>
|
||||
</ActionButton>
|
||||
<ActionButton type="favourite" label="Favourite" on:click={toggleFavourite} bind:active={post.favourited} bind:count={post.favourite_count} disabled={!$account}>
|
||||
<ActionButton type="favourite" label="{lang.string('post.actions.favourite')}" on:click={toggleFavourite} bind:active={post.favourited} bind:count={post.favourite_count} disabled={!$account}>
|
||||
<FavouriteIcon/>
|
||||
<svelte:fragment slot="activeIcon">
|
||||
<FavouriteIconFill/>
|
||||
</svelte:fragment>
|
||||
</ActionButton>
|
||||
<ActionButton type="quote" label="Quote" disabled>
|
||||
<ActionButton type="quote" label="{lang.string('post.actions.quote')}" disabled>
|
||||
<QuoteIcon/>
|
||||
</ActionButton>
|
||||
<ActionButton type="more" label="More" disabled>
|
||||
<ActionButton type="more" label="{lang.string('post.actions.more')}" disabled>
|
||||
<MoreIcon/>
|
||||
</ActionButton>
|
||||
{#if $account && post.account.id === $account.id}
|
||||
<ActionButton type="delete" label="Delete" on:click={deletePost}>
|
||||
<ActionButton type="delete" label="{lang.string('post.actions.delete')}" on:click={deletePost}>
|
||||
<DeleteIcon/>
|
||||
</ActionButton>
|
||||
{/if}
|
||||
|
|
|
@ -1,25 +1,29 @@
|
|||
<script>
|
||||
import Lang from '$lib/lang';
|
||||
|
||||
export let post;
|
||||
|
||||
let open_warned = false;
|
||||
const lang = Lang();
|
||||
|
||||
let open = false;
|
||||
</script>
|
||||
|
||||
<div class="post-body">
|
||||
{#if post.warning}
|
||||
<button class="post-warning" on:click|stopPropagation={() => { open_warned = !open_warned }} on:mouseup|stopPropagation>
|
||||
<button class="post-warning" on:click|stopPropagation={() => { open = !open }} on:mouseup|stopPropagation>
|
||||
<strong>
|
||||
{post.warning}
|
||||
<span class="warning-instructions">
|
||||
{#if !open_warned}
|
||||
(click to reveal)
|
||||
<span class="instructions">
|
||||
{#if !open}
|
||||
{lang.string('post.warning.show')}
|
||||
{:else}
|
||||
(click to hide)
|
||||
{lang.string('post.warning.hide')}
|
||||
{/if}
|
||||
</span>
|
||||
</strong>
|
||||
</button>
|
||||
{/if}
|
||||
{#if !post.warning || open_warned}
|
||||
{#if !post.warning || open}
|
||||
{#if post.rich_text}
|
||||
<span class="post-text">{@html post.rich_text}</span>
|
||||
{:else if post.html}
|
||||
|
@ -60,7 +64,7 @@
|
|||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
padding: 4px 8px;
|
||||
--warn-bg: color-mix(in srgb, var(--bg-700), var(--accent) 1%);
|
||||
--warn-bg: color-mix(in srgb, transparent, var(--bg-700) 50%);
|
||||
background: repeating-linear-gradient(-45deg, transparent, transparent 10px, var(--warn-bg) 10px, var(--warn-bg) 20px);
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
|
@ -78,13 +82,12 @@
|
|||
box-shadow: 0 0 8px var(--warn-bg);
|
||||
}
|
||||
|
||||
.post-warning .warning-instructions {
|
||||
.post-warning .instructions {
|
||||
font-weight: normal;
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.post-text {
|
||||
font-size: .9em;
|
||||
line-height: 1.45em;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
@ -129,7 +132,9 @@
|
|||
color: var(--accent);
|
||||
}
|
||||
|
||||
.post-text :global(a.mention) {
|
||||
/* mention gets used in other places (bios) so it's global */
|
||||
|
||||
:global(a.mention) {
|
||||
color: inherit;
|
||||
font-weight: 600;
|
||||
padding: 3px 6px;
|
||||
|
@ -138,7 +143,7 @@
|
|||
text-decoration: none;
|
||||
}
|
||||
|
||||
.post-text :global(a.mention:hover) {
|
||||
:global(a.mention:hover) {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
<script>
|
||||
import { shorthand as short_time } from '$lib/time.js';
|
||||
import { server } from '$lib/client/server';
|
||||
import RepostIcon from '@cf/icons/repost.svg';
|
||||
import Lang from '$lib/lang';
|
||||
|
||||
const lang = Lang();
|
||||
|
||||
export let post;
|
||||
|
||||
|
@ -7,17 +12,19 @@
|
|||
</script>
|
||||
|
||||
<div class="post-context">
|
||||
<span class="post-context-icon">🔁</span>
|
||||
<span class="post-context-icon">
|
||||
<RepostIcon width="22px" />
|
||||
</span>
|
||||
<span class="post-context-action">
|
||||
<a href={post.account.url} target="_blank"><span class="name">
|
||||
{@html post.account.rich_name}</span>
|
||||
</a>
|
||||
boosted this post.
|
||||
{ @html
|
||||
lang.string('post.boosted',
|
||||
`<a href="/${$server.host}/${post.account.fqn}"><span class="name">${post.account.rich_name}</span></a>`)
|
||||
}
|
||||
</span>
|
||||
<span class="post-context-time">
|
||||
<time title="{time_string}">{short_time(post.created_at)}</time>
|
||||
{#if post.visibility !== "public"}
|
||||
<span class="post-visibility">- {post.visibility}</span>
|
||||
<span class="post-visibility">- {lang.string(`post.visibility.${post.visibility}`)}</span>
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
|
@ -38,11 +45,13 @@
|
|||
}
|
||||
|
||||
.post-context-icon {
|
||||
margin-right: 4px;
|
||||
height: 1em;
|
||||
margin-right: .1em;
|
||||
transform: translateY(-.3em);
|
||||
}
|
||||
|
||||
.post-context a,
|
||||
.post-context a:visited {
|
||||
:global(.post-context a),
|
||||
:global(.post-context a:visited) {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { server } from '$lib/client/server';
|
||||
import Lang from '$lib/lang';
|
||||
|
||||
import BoostContext from './BoostContext.svelte';
|
||||
import ReplyContext from './ReplyContext.svelte';
|
||||
|
@ -12,6 +13,9 @@
|
|||
|
||||
export let post_data;
|
||||
export let focused = false;
|
||||
export let pinned = false;
|
||||
|
||||
const lang = Lang();
|
||||
|
||||
let post_context = undefined;
|
||||
let post = post_data;
|
||||
|
@ -41,8 +45,6 @@
|
|||
window.scrollTo(0, el.scrollHeight);
|
||||
}
|
||||
});
|
||||
|
||||
let aria_label = post.account.username + '; ' + post.text + '; ' + post.created_at;
|
||||
</script>
|
||||
|
||||
<div class="post-container">
|
||||
|
@ -51,12 +53,15 @@
|
|||
<ReplyContext post={reply} />
|
||||
{/await}
|
||||
{/if}
|
||||
{#if pinned}
|
||||
<p class="post-context pinned">{lang.string('post.pinned')}</p>
|
||||
{/if}
|
||||
{#if is_boost && !post_context.text}
|
||||
<BoostContext post={post_context} />
|
||||
{/if}
|
||||
<article
|
||||
class={"post" + (focused ? " focused" : "")}
|
||||
aria-label={aria_label}
|
||||
aria-label={post.account.username + '; ' + post.text + '; ' + post.created_at}
|
||||
bind:this={el}
|
||||
on:mousedown={e => {mouse_pos.left = e.pageX; mouse_pos.top = e.pageY}}
|
||||
on:mouseup={e => {if (e.pageX == mouse_pos.left && e.pageY == mouse_pos.top) gotoPost(e)}}
|
||||
|
@ -74,18 +79,24 @@
|
|||
|
||||
<style>
|
||||
.post-container {
|
||||
width: 732px;
|
||||
max-width: 732px;
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 8px;
|
||||
background-color: var(--bg-800);
|
||||
border-top: 1px solid color-mix(in srgb, transparent, var(--text) 20%);
|
||||
background: var(--bg-900);
|
||||
}
|
||||
.post-container:first-of-type {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.pinned {
|
||||
margin: .9em 1.2em .3em 1.2em;
|
||||
font-size: .8em;
|
||||
color: var(--accent);
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.post {
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
transition: background-color .1s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
@ -100,7 +111,7 @@
|
|||
}
|
||||
|
||||
.post:hover {
|
||||
background-color: color-mix(in srgb, var(--bg-800), black 5%);
|
||||
background-color: color-mix(in srgb, transparent, var(--bg-800) 25%);
|
||||
}
|
||||
|
||||
.post-container:has(.post-context) .post {
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
<script>
|
||||
import { shorthand as short_time } from '$lib/time.js';
|
||||
import { server } from '$lib/client/server';
|
||||
import Lang from '$lib/lang';
|
||||
|
||||
const lang = Lang();
|
||||
|
||||
export let post;
|
||||
export let reply = undefined;
|
||||
|
@ -8,21 +12,19 @@
|
|||
</script>
|
||||
|
||||
<div class={"post-header-container" + (reply ? " reply" : "")}>
|
||||
<a href={post.account.url} target="_blank" class="post-avatar-container" on:mouseup|stopPropagation>
|
||||
<a href="/{$server.host}/@{post.account.fqn}" class="post-avatar-container" on:mouseup|stopPropagation>
|
||||
<img src={post.account.avatar_url} type={post.account.avatar_type} alt="" width="48" height="48" class="post-avatar" loading="lazy" decoding="async">
|
||||
</a>
|
||||
<header class="post-header">
|
||||
<div class="post-user-info" on:mouseup|stopPropagation>
|
||||
<a href={post.account.url} target="_blank" class="name">{@html post.account.rich_name}</a>
|
||||
<a href="/{$server.host}/@{post.account.fqn}" class="name">{@html post.account.rich_name}</a>
|
||||
<span class="username">{post.account.mention}</span>
|
||||
</div>
|
||||
<div class="post-info" on:mouseup|stopPropagation>
|
||||
<a href={post.url} target="_blank" class="created-at">
|
||||
<time title={time_string}>{short_time(post.created_at)}</time>
|
||||
{#if post.visibility !== "public"}
|
||||
<br>
|
||||
<span class="post-visibility">{post.visibility}</span>
|
||||
{/if}
|
||||
<br>
|
||||
<span class="post-visibility">{lang.string('post.visibility.' + post.visibility)}</span>
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
|
|
@ -51,11 +51,7 @@
|
|||
{/if}
|
||||
</ReactionButton>
|
||||
{/each}
|
||||
<ReactionButton
|
||||
type="reaction"
|
||||
title="react"
|
||||
label="React"
|
||||
disabled>
|
||||
<ReactionButton disabled>
|
||||
<ReactIcon/>
|
||||
</ReactionButton>
|
||||
</div>
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
<script>
|
||||
import { playSound } from '../../sound.js';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import Lang from '$lib/lang';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const lang = Lang();
|
||||
|
||||
export let type = "react";
|
||||
export let label = "React";
|
||||
export let title = label;
|
||||
export let label = lang.string('post.actions.react');
|
||||
export let title = lang.string('post.actions.react');
|
||||
export let count = 0;
|
||||
export let active = false;
|
||||
export let disabled = false;
|
||||
|
|
|
@ -61,13 +61,12 @@
|
|||
flex-direction: row;
|
||||
color: var(--text);
|
||||
align-items: stretch;
|
||||
border-radius: 8px;
|
||||
transition: background-color .1s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.post-reply:hover {
|
||||
background-color: color-mix(in srgb, var(--bg-800), black 5%);
|
||||
background-color: color-mix(in srgb, var(--bg-800), transparent 50%);
|
||||
}
|
||||
|
||||
.post-avatar-container {
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
<script>
|
||||
import '$lib/app.css';
|
||||
import '../app.css';
|
||||
import * as api from '$lib/api.js';
|
||||
import { server } from '$lib/client/server.js';
|
||||
import { app } from '$lib/client/app.js';
|
||||
import { account } from '$lib/stores/account.js';
|
||||
import { parseAccount } from '$lib/account.js';
|
||||
import { unread_notif_count, last_read_notif_id } from '$lib/notifications.js';
|
||||
import Lang from '$lib/lang';
|
||||
|
||||
import Navigation from '$lib/ui/Navigation.svelte';
|
||||
import Modal from '@cf/ui/Modal.svelte';
|
||||
import Composer from '@cf/ui/Composer.svelte';
|
||||
import Widgets from '$lib/ui/Widgets.svelte';
|
||||
|
||||
const lang = Lang();
|
||||
|
||||
let show_composer = false;
|
||||
|
||||
async function init() {
|
||||
|
@ -25,7 +28,7 @@
|
|||
if (!data) return;
|
||||
|
||||
account.set(parseAccount(data));
|
||||
console.log(`Logged in as @${$account.username}@${$account.host}`);
|
||||
console.log(lang.string('logs.logged_in', $account.fqn));
|
||||
|
||||
// spin up async task to fetch notifications
|
||||
const notif_data = await api.getNotifications(
|
||||
|
@ -48,7 +51,7 @@
|
|||
<main>
|
||||
{#await init()}
|
||||
<div class="loading throb">
|
||||
<span>just a moment...</span>
|
||||
<span>{lang.string('loading')}</span>
|
||||
</div>
|
||||
{:then}
|
||||
<slot></slot>
|
||||
|
|
|
@ -2,37 +2,87 @@
|
|||
import { page } from '$app/stores';
|
||||
import { account } from '$lib/stores/account.js';
|
||||
import { timeline, getTimeline } from '$lib/timeline.js';
|
||||
import { app_name } from '$lib/config.js';
|
||||
import Lang from '$lib/lang';
|
||||
|
||||
import LoginForm from '$lib/ui/LoginForm.svelte';
|
||||
import Button from '$lib/ui/Button.svelte';
|
||||
import Post from '$lib/ui/post/Post.svelte';
|
||||
import PageHeader from '../lib/ui/core/PageHeader.svelte';
|
||||
|
||||
const lang = Lang();
|
||||
|
||||
// TODO: refactor to enum when moving to TS
|
||||
let timelineType = localStorage.getItem(app_name + '_selected_timeline') || "home";
|
||||
|
||||
$: {
|
||||
// awful hack to update timeline fresh
|
||||
// when timelineType is updated
|
||||
//
|
||||
// TODO: migrate to $effect when migrating to svelte 5
|
||||
timelineType = timelineType
|
||||
|
||||
// set in localStorage
|
||||
localStorage.setItem(app_name + '_selected_timeline', timelineType);
|
||||
|
||||
// erase the timeline here so the ui reacts instantly
|
||||
// mae: i could write an awesome undertale reference here
|
||||
timeline.set([]);
|
||||
|
||||
getCurrentTimeline()
|
||||
}
|
||||
|
||||
function getCurrentTimeline(clean = false) {
|
||||
switch(timelineType) {
|
||||
case "home":
|
||||
getTimeline("home", clean);
|
||||
break;
|
||||
|
||||
case "local":
|
||||
getTimeline("public", clean, true)
|
||||
break;
|
||||
|
||||
case "federated":
|
||||
getTimeline("public", clean, false, true)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
account.subscribe(account => {
|
||||
if (account) getTimeline();
|
||||
if (account) getCurrentTimeline();
|
||||
});
|
||||
|
||||
document.addEventListener("scroll", () => {
|
||||
document.addEventListener('scroll', () => {
|
||||
if ($account && $page.url.pathname !== "/") return;
|
||||
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 2048) {
|
||||
getTimeline();
|
||||
getCurrentTimeline();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if $account}
|
||||
<header>
|
||||
<h1>Home</h1>
|
||||
<nav>
|
||||
<Button centered active>Home</Button>
|
||||
<Button centered disabled>Local</Button>
|
||||
<Button centered disabled>Federated</Button>
|
||||
</nav>
|
||||
</header>
|
||||
<PageHeader title={lang.string(`timeline.${timelineType}`)}>
|
||||
<Button centered
|
||||
active={(timelineType == "home")}
|
||||
on:click={() => timelineType = "home"}>
|
||||
{lang.string('timeline.home')}
|
||||
</Button>
|
||||
<Button centered
|
||||
active={(timelineType == "local")}
|
||||
on:click={() => timelineType = "local"}>
|
||||
{lang.string('timeline.local')}
|
||||
</Button>
|
||||
<Button centered
|
||||
active={(timelineType == "federated")}
|
||||
on:click={() => timelineType = "federated"}>
|
||||
{lang.string('timeline.federated')}
|
||||
</Button>
|
||||
</PageHeader>
|
||||
|
||||
<div id="feed" role="feed">
|
||||
{#if $timeline.length <= 0}
|
||||
<div class="loading throb">
|
||||
<span>getting the feed...</span>
|
||||
<span>{lang.string('timeline.fetching')}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#each $timeline as post}
|
||||
|
@ -44,25 +94,6 @@
|
|||
{/if}
|
||||
|
||||
<style>
|
||||
header {
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
margin: 16px 0 8px 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
nav {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
#feed {
|
||||
margin-bottom: 20vh;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
export async function load({ params }) {
|
||||
return {
|
||||
server_domain: params.server
|
||||
server_host: params.server
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export async function load({ params }) {
|
||||
return error(404, 'Not Found');
|
||||
// return {
|
||||
// account_name: params.account
|
||||
// };
|
||||
let handle = params.account;
|
||||
if (handle.startsWith('@'))
|
||||
handle = handle.substring(1);
|
||||
|
||||
return {
|
||||
server_host: params.server,
|
||||
account_handle: handle,
|
||||
};
|
||||
}
|
||||
|
|
330
src/routes/[server]/[account]/+page.svelte
Normal file
330
src/routes/[server]/[account]/+page.svelte
Normal file
|
@ -0,0 +1,330 @@
|
|||
<script>
|
||||
import Button from '@cf/ui/Button.svelte';
|
||||
import HomeIcon from '@cf/icons/unlisted.svg';
|
||||
import MoreIcon from '@cf/icons/more.svg';
|
||||
import LockIcon from '@cf/icons/lock.svg';
|
||||
import BotIcon from '@cf/icons/bot.svg';
|
||||
import Lang from '$lib/lang';
|
||||
import * as api from '$lib/api.js';
|
||||
import { server, createServer } from '$lib/client/server.js';
|
||||
import { app } from '$lib/client/app.js';
|
||||
import { parseAccount } from '$lib/account.js';
|
||||
import { parsePost } from '$lib/post.js';
|
||||
import { account } from '$lib/stores/account';
|
||||
import { goto, afterNavigate } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import Post from '../../../lib/ui/post/Post.svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export let data;
|
||||
|
||||
const lang = Lang();
|
||||
|
||||
let profile_pinned_posts = writable([]);
|
||||
let profile_posts_max_id = null;
|
||||
let profile_posts = writable([]);
|
||||
let profile = fetchProfile(data.account_handle);
|
||||
let error = false;
|
||||
let previous_page = base;
|
||||
|
||||
let post_replies = false;
|
||||
let post_boosts = true;
|
||||
let post_media = false;
|
||||
|
||||
afterNavigate(({from}) => {
|
||||
previous_page = from?.url.pathname || previous_page;
|
||||
profile = fetchProfile(data.account_handle);
|
||||
})
|
||||
|
||||
async function getPosts(profile, max_id) {
|
||||
const posts = await api.getUserPosts(
|
||||
$server.host,
|
||||
$app.token,
|
||||
profile.id,
|
||||
max_id,
|
||||
post_replies,
|
||||
post_boosts,
|
||||
post_media
|
||||
);
|
||||
let parsed_posts = [];
|
||||
for (let post of posts) {
|
||||
parsed_posts.push(await parsePost(post, 1));
|
||||
}
|
||||
profile_posts.update(posts => {
|
||||
posts.push(...parsed_posts);
|
||||
return posts;
|
||||
});
|
||||
return parsed_posts.length > 0 ? parsed_posts[parsed_posts.length - 1].id : null;
|
||||
}
|
||||
|
||||
async function fetchProfile(handle) {
|
||||
let token = $app ? $app.token : null;
|
||||
|
||||
profile_posts.set([]);
|
||||
profile_pinned_posts.set([]);
|
||||
|
||||
if (!$server || $server.host !== data.server_host) {
|
||||
server.set(await createServer(data.server_host));
|
||||
if (!$server) {
|
||||
error = lang.string('error.connection_failed', data.server_host);
|
||||
throw new Error(lang.string('logs.connection_failed', data.server_host));
|
||||
}
|
||||
}
|
||||
|
||||
let profile_data;
|
||||
try {
|
||||
profile_data = await api.lookupUser($server.host, token, handle);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (!profile_data || profile_data.error) {
|
||||
error = lang.string('error.profile_fetch_failed_id', handle);
|
||||
throw new Error(lang.string('logs.profile_fetch_failed_id', handle));
|
||||
}
|
||||
let profile = await parseAccount(profile_data, 0);
|
||||
|
||||
api.getUserPinnedPosts(
|
||||
$server.host,
|
||||
token,
|
||||
profile.id,
|
||||
).then(async posts => {
|
||||
const parsed_posts = [];
|
||||
for (let post of posts) {
|
||||
parsed_posts.push(await parsePost(post, 1));
|
||||
}
|
||||
profile_pinned_posts.set(parsed_posts);
|
||||
});
|
||||
|
||||
let post_lock = false; // `true` == "locked"
|
||||
getPosts(profile, null).then(last_id => {
|
||||
profile_posts_max_id = last_id;
|
||||
post_lock = false;
|
||||
});
|
||||
|
||||
document.addEventListener("scroll", () => {
|
||||
if (window.innerHeight + window.scrollY < document.body.offsetHeight - 2048)
|
||||
return;
|
||||
if ($profile_posts.length == 0)
|
||||
return;
|
||||
if (profile_posts_max_id == null)
|
||||
return;
|
||||
if (profile_posts_max_id != $profile_posts[$profile_posts.length - 1].id)
|
||||
return;
|
||||
|
||||
if (post_lock) return;
|
||||
post_lock = true;
|
||||
|
||||
getPosts(profile, profile_posts_max_id).then(last_id => {
|
||||
profile_posts_max_id = last_id;
|
||||
post_lock = false;
|
||||
});
|
||||
});
|
||||
|
||||
return profile;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#await profile}
|
||||
<div class="loading throb">
|
||||
<span>{lang.string('profile.loading')}</span>
|
||||
</div>
|
||||
{:then profile}
|
||||
<header data-banner="{profile.banner_url}">
|
||||
<img src="{profile.banner_url}" class="profile-banner" alt="">
|
||||
<div class="profile-tag">
|
||||
<!-- svelte-ignore a11y-img-redundant-alt -->
|
||||
<img src="{profile.avatar_url}" alt="">
|
||||
<div class="profile-tag-names">
|
||||
<div class="profile-tag-display-name">
|
||||
<h1>
|
||||
{@html profile.rich_name}
|
||||
{#if profile.locked}
|
||||
<span title="{lang.string('profile.locked')}">
|
||||
<LockIcon width="22px"/>
|
||||
</span>
|
||||
{/if}
|
||||
{#if profile.bot}
|
||||
<span title="{lang.string('profile.bot')}">
|
||||
<BotIcon width="22px"/>
|
||||
</span>
|
||||
{/if}
|
||||
</h1>
|
||||
</div>
|
||||
<p>{profile.fqn}</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="profile-info">
|
||||
<p class="profile-bio">{@html profile.rich_bio}</p>
|
||||
<ul class="profile-counts">
|
||||
<li><b>{lang.string('profile.followers')}</b> {profile.followers_count}</li>
|
||||
<li><b>{lang.string('profile.following')}</b> {profile.following_count}</li>
|
||||
<li><b>{lang.string('profile.posts')}</b> {profile.posts_count}</li>
|
||||
</ul>
|
||||
<div class="profile-actions">
|
||||
{#if $account && profile.fqn !== $account.fqn}
|
||||
<Button filled disabled label="{lang.string('profile.follow')} {profile.nickname}" class="profile-btn-follow">
|
||||
{lang.string('profile.follow')}
|
||||
</Button>
|
||||
{/if}
|
||||
<Button label="{lang.string('profile.home_instance')}" href="{profile.url}">
|
||||
<HomeIcon width="24px"/>
|
||||
</Button>
|
||||
<Button>
|
||||
<MoreIcon width="24px"/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="profile-post-categories">
|
||||
<Button active>
|
||||
{lang.string('profile.posts')}
|
||||
</Button>
|
||||
<Button>
|
||||
{lang.string('profile.replies')}
|
||||
</Button>
|
||||
<Button>
|
||||
{lang.string('profile.media')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="profile-pinned-posts">
|
||||
{#if profile_pinned_posts}
|
||||
{#each $profile_pinned_posts as post}
|
||||
<Post post_data={post} pinned />
|
||||
{/each}
|
||||
<br/><hr/><br/>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="profile-posts">
|
||||
{#each $profile_posts as post}
|
||||
<Post post_data={post} />
|
||||
{/each}
|
||||
</div>
|
||||
{:catch error}
|
||||
<p class="error">{error}</p>
|
||||
{/await}
|
||||
|
||||
<style>
|
||||
header {
|
||||
margin-top: 1rem;
|
||||
width: 100%;
|
||||
height: 215px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.profile-banner {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
background-color: var(--bg-700);
|
||||
}
|
||||
|
||||
.profile-tag {
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
left: 16px;
|
||||
background: color-mix(in srgb, transparent, var(--bg-1000) 50%);
|
||||
backdrop-filter: blur(8px);
|
||||
width: fit-content;
|
||||
display: flex;
|
||||
height: 64px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.profile-tag img {
|
||||
aspect-ratio: 1;
|
||||
border-top-left-radius: 8px;
|
||||
border-bottom-left-radius: 8px;
|
||||
}
|
||||
|
||||
.profile-tag-names {
|
||||
padding: 8px 16px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.profile-tag-names * {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.profile-tag-names h1 {
|
||||
font-size: 1.15rem
|
||||
}
|
||||
|
||||
.profile-tag-names h1 :global(svg) {
|
||||
height: 1.2em;
|
||||
width: 1.2em;
|
||||
margin: -1em -.1em 0 -.1em;
|
||||
transform: translateY(.2em);
|
||||
}
|
||||
|
||||
.profile-info {
|
||||
background-color: var(--bg-800);
|
||||
padding: 16px;
|
||||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
}
|
||||
|
||||
.profile-bio {
|
||||
margin: 0;
|
||||
|
||||
/* !! may not be required in prod */
|
||||
white-space: pre-line;
|
||||
}
|
||||
:global(.profile-bio p:first-of-type) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.profile-counts {
|
||||
padding: 0;
|
||||
}
|
||||
.profile-counts li {
|
||||
display: inline-block;
|
||||
}
|
||||
.profile-counts > *:not(.profile-counts:first-child) {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.profile-actions {
|
||||
width: fit-content;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
}
|
||||
|
||||
.profile-actions :global(button.profile-btn-follow) {
|
||||
padding: 0 32px;
|
||||
}
|
||||
|
||||
.profile-actions :global(a) {
|
||||
width: fit-content;
|
||||
height: 16px;
|
||||
}
|
||||
.profile-actions :global(button) {
|
||||
width: fit-content;
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
.profile-post-categories {
|
||||
display: flex;
|
||||
padding: 1rem 0;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.loading {
|
||||
width: 100%;
|
||||
height: 80vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 2em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.error {
|
||||
padding: 4em 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
|
@ -1,4 +1,8 @@
|
|||
export async function load({ params }) {
|
||||
let handle = params.account;
|
||||
if (handle.startsWith('@'))
|
||||
handle = handle.substring(1);
|
||||
|
||||
return {
|
||||
server_host: params.server,
|
||||
account_handle: params.account,
|
||||
|
|
|
@ -4,13 +4,16 @@
|
|||
import { app } from '$lib/client/app.js';
|
||||
import { parsePost } from '$lib/post.js';
|
||||
import { goto, afterNavigate } from '$app/navigation';
|
||||
import { base } from '$app/paths'
|
||||
import { base } from '$app/paths';
|
||||
import Lang from '$lib/lang';
|
||||
|
||||
import Post from '$lib/ui/post/Post.svelte';
|
||||
import Button from '$lib/ui/Button.svelte';
|
||||
|
||||
export let data;
|
||||
|
||||
const lang = Lang();
|
||||
|
||||
let post = fetchPost(data.post_id);
|
||||
let error = false;
|
||||
let previous_page = base;
|
||||
|
@ -30,16 +33,16 @@
|
|||
// TODO: make `server` a key/value pair to support multiple servers
|
||||
server.set(await createServer(data.server_host));
|
||||
if (!$server) {
|
||||
error = `Failed to connect to <code>${data.server_host}</code>.`;
|
||||
console.error(`Failed to connect to ${data.server_host}.`);
|
||||
error = lang.string('error.connection_failed', data.server_host);
|
||||
console.error(lang.string('logs.connection_failed', data.server_host));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const post_data = await api.getPost($server.host, token, post_id);
|
||||
if (!post_data || post_data.error) {
|
||||
error = `Failed to retrieve post <code>${post_id}</code>.`;
|
||||
console.error(`Failed to retrieve post ${post_id}.`);
|
||||
error = lang.string('error.post_fetch_failed_id', post_id);
|
||||
console.error(lang.string('logs.post_fetch_failed_id', post_id));
|
||||
return;
|
||||
}
|
||||
let post = await parsePost(post_data, 0);
|
||||
|
@ -68,7 +71,7 @@
|
|||
|
||||
{#await post}
|
||||
<div class="loading throb">
|
||||
<span>loading post...</span>
|
||||
<span>{lang.string('post.loading')}</span>
|
||||
</div>
|
||||
{:then post}
|
||||
{#if error}
|
||||
|
@ -77,12 +80,12 @@
|
|||
<header>
|
||||
{#if previous_page}
|
||||
<nav>
|
||||
<Button centered on:click={() => {goto(previous_page)}}>Back</Button>
|
||||
<Button centered on:click={() => {goto(previous_page)}}>{lang.string('navigation.back')}</Button>
|
||||
</nav>
|
||||
{/if}
|
||||
<img src={post.account.avatar_url} type={post.account.avatar_type || "image/png"} alt="" width="40" height="40" class="header-avatar" loading="lazy" decoding="async">
|
||||
<img src={post.account.avatar_url} type={post.account.avatar_type || 'image/png'} alt="" width="40" height="40" class="header-avatar" loading="lazy" decoding="async">
|
||||
<h1>
|
||||
Post by {@html post.account.rich_name}
|
||||
{@html lang.string('post.by', post.account.rich_name)}
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
|
|
|
@ -8,17 +8,20 @@
|
|||
import { error } from '@sveltejs/kit';
|
||||
import { unread_notif_count, last_read_notif_id } from '$lib/notifications.js';
|
||||
import { account } from '$lib/stores/account.js';
|
||||
import Lang from '$lib/lang';
|
||||
|
||||
export let data;
|
||||
|
||||
const lang = Lang();
|
||||
|
||||
let auth_code = data.code;
|
||||
|
||||
if (!auth_code || !get(server) || !get(app)) {
|
||||
error(400, { message: "Bad request" });
|
||||
error(400, { message: lang.string('error.bad_request') });
|
||||
} else {
|
||||
api.getToken(get(server).host, get(app).id, get(app).secret, auth_code).then(token => {
|
||||
if (!token) {
|
||||
error(400, { message: "Invalid auth code provided" });
|
||||
error(400, { message: lang.string('error.invalid_auth_code') });
|
||||
}
|
||||
|
||||
app.update(app => {
|
||||
|
@ -30,7 +33,7 @@
|
|||
if (!data) return goto("/");
|
||||
|
||||
account.set(parseAccount(data));
|
||||
console.log(`Logged in as @${get(account).username}@${get(account).host}`);
|
||||
console.log(lang.string('logs.logged_in', get(account).fqn));
|
||||
|
||||
// spin up async task to fetch notifications
|
||||
return api.getNotifications(
|
||||
|
|
36
src/routes/favourites/+page.svelte
Normal file
36
src/routes/favourites/+page.svelte
Normal file
|
@ -0,0 +1,36 @@
|
|||
<script>
|
||||
import { page } from '$app/stores';
|
||||
import { account } from '@cf/store/account.js';
|
||||
import { timeline, getTimeline } from '$lib/timeline.js';
|
||||
|
||||
import Button from '@cf/ui/Button.svelte';
|
||||
import Post from '@cf/ui/post/Post.svelte';
|
||||
import PageHeader from '@cf/ui/core/PageHeader.svelte';
|
||||
|
||||
import Lang from '$lib/lang';
|
||||
|
||||
const lang = Lang();
|
||||
if (!$account) goto("/");
|
||||
|
||||
getTimeline("favourites", true);
|
||||
|
||||
document.addEventListener('scroll', () => {
|
||||
if ($account && $page.url.pathname !== "/favourites") return;
|
||||
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 2048) {
|
||||
getTimeline("favourites");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<PageHeader title={lang.string(`navigation.favourites`)}/>
|
||||
|
||||
<div id="feed" role="feed">
|
||||
{#if $timeline.length <= 0}
|
||||
<div class="loading throb">
|
||||
<span>{lang.string('timeline.fetching')}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#each $timeline as post}
|
||||
<Post post_data={post} />
|
||||
{/each}
|
||||
</div>
|
152
src/routes/follow-requests/+page.svelte
Normal file
152
src/routes/follow-requests/+page.svelte
Normal file
|
@ -0,0 +1,152 @@
|
|||
<script>
|
||||
import { followRequests } from '$lib/followRequests.js';
|
||||
import PageHeader from '../../lib/ui/core/PageHeader.svelte';
|
||||
import Lang from '$lib/lang';
|
||||
import {server} from '$lib/client/server';
|
||||
import {app} from '$lib/client/app';
|
||||
import Button from '../../lib/ui/Button.svelte';
|
||||
import * as api from '$lib/api'
|
||||
|
||||
import TickIcon from '@cf/icons/tick.svg'
|
||||
import CrossIcon from '@cf/icons/cross.svg'
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
const lang = Lang();
|
||||
|
||||
async function actionRequest(account_id, approved) {
|
||||
// remove item from array first - this updates the ui and
|
||||
// makes the interaction more seamless
|
||||
$followRequests.splice(
|
||||
$followRequests.indexOf(
|
||||
$followRequests.find(r => r.id)
|
||||
),
|
||||
1
|
||||
);
|
||||
|
||||
// hack: force the state to update now that we just spliced the array
|
||||
$followRequests = $followRequests
|
||||
|
||||
if(approved) {
|
||||
await api.acceptFollowRequest(
|
||||
get(server).host,
|
||||
get(app).token,
|
||||
account_id
|
||||
)
|
||||
} else {
|
||||
await api.rejectFollowRequest(
|
||||
get(server).host,
|
||||
get(app).token,
|
||||
account_id
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// aliases
|
||||
const acceptRequest = (id) => actionRequest(id, true);
|
||||
const denyRequest = (id) => actionRequest(id, false);
|
||||
</script>
|
||||
|
||||
<PageHeader title={lang.string('navigation.follow_requests')}/>
|
||||
|
||||
{#if $followRequests.length < 1}
|
||||
<p class="request-zero">{lang.string('follow_requests.none')}</p>
|
||||
{/if}
|
||||
|
||||
<div class="requests-container">
|
||||
{#each $followRequests as req}
|
||||
<div class="request">
|
||||
<a href="/{$server.host}/@{req.fqn}" class="request-avatar-container" on:mouseup|stopPropagation>
|
||||
<img src={req.avatar_url} alt="" width="48" height="48" class="post-avatar" loading="lazy" decoding="async">
|
||||
</a>
|
||||
<div class="info">
|
||||
<div class="request-user-info">
|
||||
<a href="/{$server.host}/@{req.fqn}" class="name">{@html req.rich_name}</a>
|
||||
<span class="username">{req.mention}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="request-options">
|
||||
<Button filled title="Yes" on:click={() => acceptRequest(req.id)}>
|
||||
<TickIcon width="24px"/>
|
||||
</Button>
|
||||
<Button title="No" on:click={() => denyRequest(req.id)}>
|
||||
<CrossIcon width="24px"/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.request {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
background: var(--bg-900);
|
||||
padding: .5rem;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.request a,
|
||||
.request a:visited {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
.request a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.request-avatar-container {
|
||||
margin-right: 12px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.post-avatar {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.request-user-info {
|
||||
margin-top: -2px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.request-user-info a {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.request-user-info .username {
|
||||
opacity: .8;
|
||||
font-size: .9em;
|
||||
}
|
||||
|
||||
.request-options {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.request-options :global(button) {
|
||||
width: fit-content;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.request-zero {
|
||||
opacity: 0.8;
|
||||
font-size: 0.95rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.requests-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .5rem;
|
||||
}
|
||||
</style>
|
|
@ -4,6 +4,10 @@
|
|||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import Notification from '$lib/ui/Notification.svelte';
|
||||
import PageHeader from '../../lib/ui/core/PageHeader.svelte';
|
||||
import Lang from '$lib/lang';
|
||||
|
||||
const lang = Lang();
|
||||
|
||||
if (!$account) goto("/");
|
||||
|
||||
|
@ -30,14 +34,12 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<h1>Notifications</h1>
|
||||
</header>
|
||||
<PageHeader title={lang.string('navigation.notifications')}/>
|
||||
|
||||
<div class="notifications">
|
||||
{#if $notifications.length === 0}
|
||||
<div class="loading throb">
|
||||
<span>fetching notifications...</span>
|
||||
<span>{lang.string('notification.fetching')}</span>
|
||||
</div>
|
||||
{:else}
|
||||
{#each $notifications as notif}
|
||||
|
@ -47,18 +49,6 @@
|
|||
</div>
|
||||
|
||||
<style>
|
||||
header {
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
margin: 16px 0 8px 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.loading {
|
||||
width: 100%;
|
||||
height: 80vh;
|
||||
|
|
|
@ -19,9 +19,10 @@ const config = {
|
|||
name: child_process.execSync('git rev-parse HEAD').toString().trim()
|
||||
},
|
||||
alias: {
|
||||
'@cf/ui/*': "./src/lib/ui",
|
||||
'@cf/icons/*': "./src/img/icons",
|
||||
'@cf/store/*': "./src/lib/stores"
|
||||
'@cf/ui/*': './src/lib/ui',
|
||||
'@cf/icons/*': './src/img/icons',
|
||||
'@cf/store/*': './src/lib/stores',
|
||||
'@cf/lang/*': './src/lang'
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -3,11 +3,16 @@ import { defineConfig } from 'vite';
|
|||
import { readFileSync } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import svg from '@poppanator/sveltekit-svg'
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
// get ver from package.json
|
||||
const packageFile = fileURLToPath(new URL('package.json', import.meta.url));
|
||||
const packageData = readFileSync(packageFile, 'utf8');
|
||||
const packageJSON = JSON.parse(packageData);
|
||||
|
||||
// get git commit hash
|
||||
const commitHash = execSync("git rev-parse HEAD")
|
||||
.toString().trim().slice(0, 10)
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
|
@ -15,7 +20,8 @@ export default defineConfig({
|
|||
svg()
|
||||
],
|
||||
define: {
|
||||
APP_VERSION: JSON.stringify(packageJSON.version)
|
||||
APP_VERSION: JSON.stringify(packageJSON.version),
|
||||
APP_COMMIT: `"${commitHash}"`
|
||||
}
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue