Compare commits
	
		
			53 commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 4bad86822f | |||
| 19be3779e3 | |||
| cd5dc461f6 | |||
| 389f8dcaff | |||
| 671c45085d | |||
| 0f17d92314 | |||
| 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 | 
					 49 changed files with 2140 additions and 2028 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! | ## try it out! | ||||||
| 
 | 
 | ||||||
| - `git clone` this repo | campfire uses [bun](https://bun.sh/) as a package manager and runtime. | ||||||
| - `npm install` the dependencies |  | ||||||
| - `npm run dev` to spin up the dev environment |  | ||||||
| 
 | 
 | ||||||
| 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 | place the static files somewhere accessible by a static webhost, such as | ||||||
| nginx or apache! **note:** your web server should attempt to reach | nginx or apache! **note:** your web server should attempt to reach | ||||||
| `/fallback.html` before erroring out. | `/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 { | :root { | ||||||
|     --bg-1000: #fff6de; |     --bg-1000: #fffcf7; | ||||||
|     --bg-900: #f9f1db; |     --bg-900: #faf4e4; | ||||||
|     --bg-800: #f1e8cf; |     --bg-800: #f2e8d7; | ||||||
|     --bg-700: #d2c9b1; |     --bg-700: #d9ccad; | ||||||
|     --bg-600: #f0f6c2; |     --bg-600: #edf5ba; | ||||||
|     --accent: #8d9936; |     --accent: #92a40a; | ||||||
|     --text: #322e1f; |     --text: #322e1f; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @media (prefers-color-scheme: dark) { | @media (prefers-color-scheme: dark) { | ||||||
|     :root { |     :root { | ||||||
|         --bg-1000: #141016; |         --bg-1000: #0b090c; | ||||||
|         --bg-900: #1B141E; |         --bg-900: #120d14; | ||||||
|         --bg-800: #2A202F; |         --bg-800: #2A202F; | ||||||
|         --bg-700: #443749; |         --bg-700: #443749; | ||||||
|         --bg-600: #513D60; |         --bg-600: #513D60; | ||||||
|  | @ -31,10 +31,6 @@ | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @supports (font-variation-settings: normal) { |  | ||||||
|     body { font-family: InterVariable, sans-serif; } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| body { | body { | ||||||
|     margin: 0; |     margin: 0; | ||||||
|     padding: 0; |     padding: 0; | ||||||
|  | @ -48,6 +44,12 @@ body { | ||||||
|     box-sizing: border-box; |     box-sizing: border-box; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @supports (font-variation-settings: normal) { | ||||||
|  |     body { | ||||||
|  |         font-family: InterVariable, sans-serif; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| a { | a { | ||||||
|     color: var(--accent); |     color: var(--accent); | ||||||
|     text-decoration: none; |     text-decoration: none; | ||||||
|  | @ -71,7 +73,7 @@ header, #widgets { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| main { | main { | ||||||
|     width: 732px; |     width: 700px; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| img.emoji { | img.emoji { | ||||||
|  | @ -79,6 +81,10 @@ img.emoji { | ||||||
|     margin: -.2em 0; |     margin: -.2em 0; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | hr { | ||||||
|  |     border-color: color-mix(in srgb, transparent, var(--accent) 50%); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .throb { | .throb { | ||||||
|     animation: .25s throb alternate infinite ease-in; |     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 { server } from '$lib/client/server.js'; | ||||||
| import { parseEmoji, renderEmoji } from '$lib/emoji.js'; | import { parseEmoji, renderEmoji } from '$lib/emoji.js'; | ||||||
| import { get, writable } from 'svelte/store'; | import { get, writable } from 'svelte/store'; | ||||||
|  | import Lang from '$lib/lang'; | ||||||
|  | 
 | ||||||
|  | const lang = Lang(); | ||||||
| 
 | 
 | ||||||
| const cache = writable({}); | const cache = writable({}); | ||||||
| 
 | 
 | ||||||
|  | @ -11,7 +14,7 @@ const cache = writable({}); | ||||||
|  */ |  */ | ||||||
| export function parseAccount(data) { | export function parseAccount(data) { | ||||||
|     if (!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; |         return null; | ||||||
|     } |     } | ||||||
|     let account = get(cache)[data.id]; |     let account = get(cache)[data.id]; | ||||||
|  | @ -20,17 +23,26 @@ export function parseAccount(data) { | ||||||
| 
 | 
 | ||||||
|     account = {}; |     account = {}; | ||||||
|     account.id = data.id; |     account.id = data.id; | ||||||
|     account.nickname = data.display_name.trim(); |     account.nickname = data.display_name.trim().replaceAll('<', '<').replaceAll('>', '>'); | ||||||
|     account.username = data.username; |     account.username = data.username; | ||||||
|     account.name = account.nickname || account.username; |     account.name = account.nickname || account.username; | ||||||
|     account.avatar_url = data.avatar; |     account.avatar_url = data.avatar; | ||||||
|  |     account.banner_url = data.header; | ||||||
|     account.url = data.url; |     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('@')) |     if (data.acct.includes('@')) | ||||||
|         account.host = data.acct.split('@')[1]; |         account.host = data.acct.split('@')[1]; | ||||||
|     else |     else | ||||||
|         account.host = get(server).host; |         account.host = get(server).host; | ||||||
| 
 | 
 | ||||||
|  |     account.fqn = data.fqn || account.username + "@" + account.host; | ||||||
|  | 
 | ||||||
|     account.mention = "@" + account.username; |     account.mention = "@" + account.username; | ||||||
|     if (account.host != get(server).host) |     if (account.host != get(server).host) | ||||||
|         account.mention += "@" + account.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_name = account.nickname ? renderEmoji(account.nickname, account.emojis) : account.username; | ||||||
|  |     account.rich_bio = renderEmoji(account.bio, account.emojis); | ||||||
| 
 | 
 | ||||||
|     cache.update(cache => { |     cache.update(cache => { | ||||||
|         cache[account.id] = account; |         cache[account.id] = account; | ||||||
|  |  | ||||||
							
								
								
									
										212
									
								
								src/lib/api.js
									
										
									
									
									
								
							
							
						
						
									
										212
									
								
								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 |  * GET /api/v1/instance | ||||||
|  * @param {string} host - The domain of the target server. |  * @param {string} host - The domain of the target server. | ||||||
|  | @ -172,16 +201,112 @@ export async function getNotifications(host, token, min_id, max_id, limit, types | ||||||
|     return data; |     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} |  * GET /api/v1/timelines/{timeline} | ||||||
|  * @param {string} host - The domain of the target server. |  * @param {string} host - The domain of the target server. | ||||||
|  * @param {string} token - The application token. |  * @param {string} token - The application token. | ||||||
|  * @param {string} timeline - The name of the timeline to pull (default "home"). |  * @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 {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 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 res = { | ||||||
|  |         data: await data.json() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if(data.headers.has("Link")) { | ||||||
|  |         let links = _parseLinkHeader(data.headers.get("Link")); | ||||||
|  |         res["prev"] = links.find(f=>f.rel=="prev"); | ||||||
|  |         res["next"] = links.find(f=>f.rel=="next"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return res; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * 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(); |     let params = new URLSearchParams(); | ||||||
|     if (max_id) params.append("max_id", max_id); |     if (max_id) params.append("max_id", max_id); | ||||||
|     const params_string = params.toString(); |     const params_string = params.toString(); | ||||||
|  | @ -190,9 +315,19 @@ export async function getTimeline(host, token, timeline, max_id) { | ||||||
|     const data = await fetch(url, { |     const data = await fetch(url, { | ||||||
|         method: 'GET', |         method: 'GET', | ||||||
|         headers: { "Authorization": token ? `Bearer ${token}` : null } |         headers: { "Authorization": token ? `Bearer ${token}` : null } | ||||||
|     }).then(res => res.json()); |     }) | ||||||
|  |      | ||||||
|  |     let res = { | ||||||
|  |         data: await data.json() | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     return data; |     if(data.headers.has("Link")) { | ||||||
|  |         let links = _parseLinkHeader(data.headers.get("Link")); | ||||||
|  |         res["prev"] = links.find(f=>f.rel=="prev"); | ||||||
|  |         res["next"] = links.find(f=>f.rel=="next"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return res; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  | @ -421,3 +556,74 @@ export async function getUser(host, token, user_id) { | ||||||
| 
 | 
 | ||||||
|     return data; |     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 { writable } from 'svelte/store'; | ||||||
| import { app_name } from '$lib/config.js'; | import { app_name } from '$lib/config.js'; | ||||||
| import { browser } from "$app/environment"; | import { browser } from "$app/environment"; | ||||||
|  | import Lang from '$lib/lang'; | ||||||
|  | 
 | ||||||
|  | const lang = Lang(); | ||||||
| 
 | 
 | ||||||
| const server_types = { | const server_types = { | ||||||
|     UNSUPPORTED: "unsupported", |     UNSUPPORTED: "unsupported", | ||||||
|  | @ -35,11 +38,11 @@ server.subscribe(server => { | ||||||
|  */ |  */ | ||||||
| export async function createServer(host) { | export async function createServer(host) { | ||||||
|     if (!host) { |     if (!host) { | ||||||
|         console.error("Attempted to create server without providing a hostname"); |         console.error(lang.string('logs.no_hostname')); | ||||||
|         return false; |         return false; | ||||||
|     } |     } | ||||||
|     if (host.startsWith("http://")) { |     if (host.startsWith("http://")) { | ||||||
|         console.error("Cowardly refusing to connect to an insecure server"); |         console.error(lang.string('logs.no_https')); | ||||||
|         return false; |         return false; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -49,7 +52,7 @@ export async function createServer(host) { | ||||||
|     if (host.startsWith("https://")) host = host.substring(8); |     if (host.startsWith("https://")) host = host.substring(8); | ||||||
|     const data = await api.getInstance(host); |     const data = await api.getInstance(host); | ||||||
|     if (!data) { |     if (!data) { | ||||||
|         console.error(`Failed to connect to ${host}`); |         console.error(lang.string('logs.connection_failed', host)); | ||||||
|         return false; |         return false; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -58,9 +61,9 @@ export async function createServer(host) { | ||||||
|     server.capabilities = getCapabilities(server.type); |     server.capabilities = getCapabilities(server.type); | ||||||
| 
 | 
 | ||||||
|     if (server.type === server_types.UNSUPPORTED) { |     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 { |     } 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; |     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); | ||||||
|  | } | ||||||
							
								
								
									
										65
									
								
								src/lib/lang.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								src/lib/lang.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,65 @@ | ||||||
|  | import * as en_GB from '@cf/lang/en_GB.json'; | ||||||
|  | // import * as ga_IE from '@cf/lang/ga_IE.json';
 | ||||||
|  | // import * as de_DE from '@cf/lang/de_DE.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_log from '../sound/log.ogg'; | ||||||
| import sound_hello from '../sound/hello.ogg'; | import sound_hello from '../sound/hello.ogg'; | ||||||
| import sound_success from '../sound/success.ogg'; | import sound_success from '../sound/success.ogg'; | ||||||
|  | @ -16,7 +20,7 @@ export function playSound(name) { | ||||||
|     if (!name) name = "default"; |     if (!name) name = "default"; | ||||||
|     const sound = sounds[name]; |     const sound = sounds[name]; | ||||||
|     if (!sound) { |     if (!sound) { | ||||||
|         console.warn(`Attempted to play sound "${name}", which does not exist!`); |         console.warn(lang.string('lang.sound_does_not_exist', name)); | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
|     sound.pause(); |     sound.pause(); | ||||||
|  |  | ||||||
|  | @ -1,10 +1,13 @@ | ||||||
|  | import Lang from '$lib/lang'; | ||||||
|  | const lang = Lang(); | ||||||
|  | 
 | ||||||
| const denoms = [ | const denoms = [ | ||||||
|     { unit: 's', min: 0 }, |     { unit: lang.string('time.second'), min: 0 }, | ||||||
|     { unit: 'm', min: 60 }, |     { unit: lang.string('time.minute'), min: 60 }, | ||||||
|     { unit: 'h', min: 60 }, |     { unit: lang.string('time.hour'), min: 60 }, | ||||||
|     { unit: 'd', min: 24 }, |     { unit: lang.string('time.day'), min: 24 }, | ||||||
|     { unit: 'w', min: 7 }, |     { unit: lang.string('time.week'), min: 7 }, | ||||||
|     { unit: 'y', min: 52 }, |     { unit: lang.string('time.year'), min: 52 }, | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
| export function shorthand(date) { | export function shorthand(date) { | ||||||
|  | @ -18,6 +21,6 @@ export function shorthand(date) { | ||||||
|         unit = denoms[index].unit; |         unit = denoms[index].unit; | ||||||
|     } |     } | ||||||
|     if (value > 0) |     if (value > 0) | ||||||
|         return Math.floor(value) + unit + " ago"; |         return lang.string('time.ago').replaceAll('%1', Math.floor(value) + unit); | ||||||
|     return "in " + Math.floor(value) + unit; |     return lang.string('time.in').replaceAll('%1', Math.floor(value) + unit); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -3,44 +3,72 @@ import { server } from '$lib/client/server.js'; | ||||||
| import { app } from '$lib/client/app.js'; | import { app } from '$lib/client/app.js'; | ||||||
| import { get, writable } from 'svelte/store'; | import { get, writable } from 'svelte/store'; | ||||||
| import { parsePost } from '$lib/post.js'; | import { parsePost } from '$lib/post.js'; | ||||||
|  | import Lang from '$lib/lang'; | ||||||
| 
 | 
 | ||||||
| export const timeline = writable([]); | export const timeline = writable([]); | ||||||
| 
 | 
 | ||||||
| let loading = false; | const lang = Lang(); | ||||||
| 
 | 
 | ||||||
| export async function getTimeline(clean) { | let loading = false; | ||||||
|  | let last_post = false;      // last post marker, used for fetching next sequence of posts
 | ||||||
|  | let at_end = false;         // at end of timeline, no next param to paginate
 | ||||||
|  | 
 | ||||||
|  | export async function getTimeline(timelineType = "home", clean, localOnly = false, remoteOnly = false) { | ||||||
|     if (loading) return; // no spamming!!
 |     if (loading) return; // no spamming!!
 | ||||||
|     loading = true; |     loading = true; | ||||||
| 
 | 
 | ||||||
|     let last_post = false; |     if(at_end) return; | ||||||
|     if (!clean && get(timeline).length > 0) |  | ||||||
|         last_post = get(timeline)[get(timeline).length - 1].id; |  | ||||||
| 
 | 
 | ||||||
|     const timeline_data = await api.getTimeline( |     if(clean) { | ||||||
|         get(server).host, |         timeline.set([]); | ||||||
|         get(app).token, |         last_post = false; | ||||||
|         "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) { |     if (!timeline_data) { | ||||||
|         console.error(`Failed to retrieve timeline.`); |         console.error(lang.string('logs.timeline_fetch_failed')); | ||||||
|         loading = false; |         loading = false; | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (clean) timeline.set([]); |     if (!clean && timeline_data.next) { | ||||||
|  |         last_post = timeline_data.next.url.searchParams.get("max_id") | ||||||
|  |     } else if(!timeline_data.next) { | ||||||
|  |         console.log(timeline_data) | ||||||
|  |         at_end = true; | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     for (let i in timeline_data) { |     for (let i in timeline_data.data) { | ||||||
|         const post_data = timeline_data[i]; |         const post_data = timeline_data.data[i]; | ||||||
|         const post = await parsePost(post_data, 1); |         const post = await parsePost(post_data, 1); | ||||||
|         if (!post) { |         if (!post) { | ||||||
|             if (post === null || post === undefined) { |             if (post === null || post === undefined) { | ||||||
|                 if (post_data.id) { |                 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 { |                 } else { | ||||||
|                     console.warn("Failed to parse post:"); |                     console.warn(lang.string('logs.post_parse_failed')); | ||||||
|                     console.warn(post_data); |                     console.debug(post_data); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             continue; |             continue; | ||||||
|  | @ -49,3 +77,9 @@ export async function getTimeline(clean) { | ||||||
|     } |     } | ||||||
|     loading = false; |     loading = false; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | export function clearTimeline() { | ||||||
|  |     timeline.set([]); | ||||||
|  |     last_post = false; | ||||||
|  |     at_end = false; | ||||||
|  | } | ||||||
|  | @ -5,53 +5,64 @@ | ||||||
| 
 | 
 | ||||||
|     const dispatch = createEventDispatcher(); |     const dispatch = createEventDispatcher(); | ||||||
| 
 | 
 | ||||||
|  |     let className = ""; | ||||||
|  |     export { className as class }; | ||||||
|     export let active = false; |     export let active = false; | ||||||
|     export let filled = false; |     export let filled = false; | ||||||
|     export let disabled = false; |     export let disabled = false; | ||||||
|     export let centered = false; |     export let centered = false; | ||||||
|     export let label = undefined; |     export let label = undefined; | ||||||
|     export let sound = "default"; |     export let sound = "default"; | ||||||
|     export let href = false; |     export let href = undefined; | ||||||
|  |     export let onClick = undefined; | ||||||
| 
 | 
 | ||||||
|     let classes = []; |     let classes = []; | ||||||
| 
 | 
 | ||||||
|     function click() { |     function click() { | ||||||
|         if (disabled) return; |         if (disabled) return; | ||||||
|         if (href) { |  | ||||||
|             location = href; |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|         playSound(sound); |         playSound(sound); | ||||||
|         dispatch('click'); |         dispatch('click'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     afterUpdate(() => { |     afterUpdate(() => { | ||||||
|         classes = []; |         classes = className.split(' '); | ||||||
|         if (active) classes = ["active"]; |         if (active) classes.push("active"); | ||||||
|         if (filled) classes = ["filled"]; |         if (filled) classes.push("filled"); | ||||||
|         if (disabled) classes = ["disabled"]; |         if (disabled) classes.push("disabled"); | ||||||
|         if (centered) classes.push("centered"); |         if (centered) classes.push("centered"); | ||||||
|     }); |     }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <button | {#if href} | ||||||
|         type="button" |     <a | ||||||
|         class={classes.join(' ')} |             class={classes.join(' ')} | ||||||
|         title={label} |             title={label} | ||||||
|         aria-label={label} |             aria-label={label} | ||||||
|         on:click={() => click()}> |             href={href} | ||||||
|     <span class="icon"> |             on:click={() => click()}> | ||||||
|         <slot name="icon" /> |         <span class="icon"> | ||||||
|     </span> |             <slot name="icon" /> | ||||||
|     <slot/> |         </span> | ||||||
| </button> |         <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> | <style> | ||||||
|     button { |     a, button { | ||||||
|         /* min-width: 64px; */ |         height: fit-content; | ||||||
|         width: 100%; |         padding: .7em .8em; | ||||||
|         height: 54px; |  | ||||||
|         padding: 16px; |  | ||||||
|         display: flex; |         display: flex; | ||||||
|         flex-direction: row; |         flex-direction: row; | ||||||
|         align-items: center; |         align-items: center; | ||||||
|  | @ -60,14 +71,13 @@ | ||||||
|         font-size: 1rem; |         font-size: 1rem; | ||||||
|         font-weight: 600; |         font-weight: 600; | ||||||
|         text-align: left; |         text-align: left; | ||||||
|  |         text-decoration: none; | ||||||
| 
 | 
 | ||||||
|         border-radius: 8px; |         border-radius: 8px; | ||||||
|         border-width: 2px; |         border: 2px solid var(--bg-700); | ||||||
|         border-style: solid; |  | ||||||
| 
 | 
 | ||||||
|         background-color: var(--bg-700); |         background-color: var(--bg-700); | ||||||
|         color: var(--text); |         color: var(--text); | ||||||
|         border-color: transparent; |  | ||||||
| 
 | 
 | ||||||
|         transition-property: border-color, background-color, color; |         transition-property: border-color, background-color, color; | ||||||
|         transition-timing-function: ease-out; |         transition-timing-function: ease-out; | ||||||
|  | @ -75,22 +85,32 @@ | ||||||
| 
 | 
 | ||||||
|         cursor: pointer; |         cursor: pointer; | ||||||
|     } |     } | ||||||
|  |     a { | ||||||
|  |         width: calc(100% - 1.6em); | ||||||
|  |     } | ||||||
|  |     button { | ||||||
|  |         width: 100%; | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|  |     a.centered, | ||||||
|     button.centered { |     button.centered { | ||||||
|         text-align: center; |         text-align: center; | ||||||
|         justify-content: center; |         justify-content: center; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     a:hover, | ||||||
|     button:hover { |     button:hover { | ||||||
|         background-color: color-mix(in srgb, var(--bg-700), var(--accent) 10%); |         border-color: color-mix(in srgb, var(--bg-700), black 10%); | ||||||
|         border-color: color-mix(in srgb, var(--bg-700), var(--accent) 20%); |         background-color: color-mix(in srgb, var(--bg-700), black 10%); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     a:active, | ||||||
|     button: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), black 20%); | ||||||
|         border-color: color-mix(in srgb, var(--bg-700), var(--bg-800) 10%); |         background-color: color-mix(in srgb, var(--bg-700), black 20%); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     a.active, | ||||||
|     button.active { |     button.active { | ||||||
|         background-color: var(--bg-600); |         background-color: var(--bg-600); | ||||||
|         color: var(--accent); |         color: var(--accent); | ||||||
|  | @ -98,50 +118,49 @@ | ||||||
|         text-shadow: 0px 2px 32px var(--accent); |         text-shadow: 0px 2px 32px var(--accent); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     a.active:hover, | ||||||
|     button.active:hover { |     button.active:hover { | ||||||
|         color: color-mix(in srgb, var(--accent), var(--bg-1000) 20%); |         color: color-mix(in srgb, var(--accent), var(--bg-1000) 20%); | ||||||
|         border-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%); |         background-color: color-mix(in srgb, var(--bg-600), var(--accent) 10%); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     a.active:active, | ||||||
|     button.active:active { |     button.active:active { | ||||||
|         color: color-mix(in srgb, var(--accent), var(--bg-800) 10%); |         color: color-mix(in srgb, var(--accent), var(--bg-800) 10%); | ||||||
|         border-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%); |         background-color: color-mix(in srgb, var(--bg-600), var(--bg-800) 10%); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     a.filled, | ||||||
|     button.filled { |     button.filled { | ||||||
|         background-color: var(--accent); |         background-color: var(--accent); | ||||||
|         color: var(--bg-800); |         color: var(--bg-800); | ||||||
|         border-color: transparent; |         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%); |         color: color-mix(in srgb, var(--bg-800), white 10%); | ||||||
|         background-color: color-mix(in srgb, var(--accent), white 20%); |         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%); |         color: color-mix(in srgb, var(--bg-800), black 10%); | ||||||
|         background-color: color-mix(in srgb, var(--accent), black 20%); |         background-color: color-mix(in srgb, var(--accent), black 20%); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     a.disabled, | ||||||
|     button.disabled { |     button.disabled { | ||||||
|         background-color: var(--bg-700); |  | ||||||
|         color: var(--text); |         color: var(--text); | ||||||
|         opacity: .5; |         opacity: .35; | ||||||
|         border-color: transparent; |         border-color: transparent; | ||||||
|         cursor: initial; |         cursor: not-allowed; | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     button.disabled:hover { |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     button.disabled:active { |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .icon:not(:empty) { |     .icon:not(:empty) { | ||||||
|         height: 150%; |         height: 1.8em; | ||||||
|         margin-right: 8px; |         margin-right: 8px; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -7,6 +7,7 @@ | ||||||
|     import { timeline } from '$lib/timeline.js'; |     import { timeline } from '$lib/timeline.js'; | ||||||
|     import { createEventDispatcher } from 'svelte'; |     import { createEventDispatcher } from 'svelte'; | ||||||
|     import { playSound } from '$lib/sound'; |     import { playSound } from '$lib/sound'; | ||||||
|  |     import Lang from '$lib/lang' | ||||||
| 
 | 
 | ||||||
|     import Button from '@cf/ui/Button.svelte'; |     import Button from '@cf/ui/Button.svelte'; | ||||||
|     import PostIcon from '@cf/icons/post.svg'; |     import PostIcon from '@cf/icons/post.svg'; | ||||||
|  | @ -19,6 +20,8 @@ | ||||||
|     import FollowersVisIcon from '@cf/icons/followers.svg'; |     import FollowersVisIcon from '@cf/icons/followers.svg'; | ||||||
|     import PrivateVisIcon from '@cf/icons/dm.svg'; |     import PrivateVisIcon from '@cf/icons/dm.svg'; | ||||||
| 
 | 
 | ||||||
|  |     const lang = Lang(); | ||||||
|  | 
 | ||||||
|     export let reply_id; |     export let reply_id; | ||||||
| 
 | 
 | ||||||
|     let content_warning = "" |     let content_warning = "" | ||||||
|  | @ -26,16 +29,11 @@ | ||||||
|     // let media_ids = []; |     // let media_ids = []; | ||||||
|     let show_cw = false; |     let show_cw = false; | ||||||
|     let visibility = "Public"; |     let visibility = "Public"; | ||||||
|  |     let visibilityLocale = lang.string('post.visibility.public'); | ||||||
| 
 | 
 | ||||||
|     const placeholders = [ |     const placeholders = lang.stringArray('compose_placeholders'); | ||||||
|         "What's cooking, $1?", |     let placeholder = Array.isArray(placeholders) ? placeholders[Math.floor(placeholders.length * Math.random())] | ||||||
|         "Speak your mind!", |         .replaceAll("%1", $account.username) : placeholders; | ||||||
|         "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 dispatch = createEventDispatcher(); |     const dispatch = createEventDispatcher(); | ||||||
| 
 | 
 | ||||||
|  | @ -77,15 +75,19 @@ | ||||||
|         switch (visibility) { |         switch (visibility) { | ||||||
|             case "Public": |             case "Public": | ||||||
|                 visibility = "Unlisted"; |                 visibility = "Unlisted"; | ||||||
|  |                 visibilityLocale = lang.string('post.visibility.unlisted'); | ||||||
|                 break; |                 break; | ||||||
|             case "Unlisted": |             case "Unlisted": | ||||||
|                 visibility = "Followers only"; |                 visibility = "Followers only"; | ||||||
|  |                 visibilityLocale = lang.string('post.visibility.follow_only'); | ||||||
|                 break; |                 break; | ||||||
|             case "Followers only": |             case "Followers only": | ||||||
|                 visibility = "Private"; |                 visibility = "Private"; | ||||||
|  |                 visibilityLocale = lang.string('post.visibility.private'); | ||||||
|                 break; |                 break; | ||||||
|             case "Private": |             case "Private": | ||||||
|                 visibility = "Public"; |                 visibility = "Public"; | ||||||
|  |                 visibilityLocale = lang.string('post.visibility.public'); | ||||||
|                 break; |                 break; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | @ -93,7 +95,8 @@ | ||||||
| 
 | 
 | ||||||
| <div class="composer"> | <div class="composer"> | ||||||
|     <div class="composer-header-container"> |     <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"> |             <img src={$account.avatar_url} type={$account.avatar_type} alt="" width="48" height="48" class="composer-avatar" loading="lazy" decoding="async"> | ||||||
|         </a> |         </a> | ||||||
|         <header class="composer-header"> |         <header class="composer-header"> | ||||||
|  | @ -104,7 +107,7 @@ | ||||||
|             <div class="composer-info" on:mouseup|stopPropagation> |             <div class="composer-info" on:mouseup|stopPropagation> | ||||||
|             </div> |             </div> | ||||||
|         </header> |         </header> | ||||||
|         <div title={visibility}> |         <div title={visibilityLocale}> | ||||||
|             <Button centered={true} on:click={() => {cycleVisibility()}}> |             <Button centered={true} on:click={() => {cycleVisibility()}}> | ||||||
|                 <svelte:fragment slot="icon"> |                 <svelte:fragment slot="icon"> | ||||||
|                     <!-- TODO: this should be a drop-down option!...later --> |                     <!-- TODO: this should be a drop-down option!...later --> | ||||||
|  | @ -122,7 +125,7 @@ | ||||||
|         </div> |         </div> | ||||||
|     </div> |     </div> | ||||||
|     {#if show_cw} |     {#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} |     {/if} | ||||||
|     <textarea placeholder="{placeholder}" class="textbox" bind:value={content}></textarea> |     <textarea placeholder="{placeholder}" class="textbox" bind:value={content}></textarea> | ||||||
|     <div class="composer-footer"> |     <div class="composer-footer"> | ||||||
|  |  | ||||||
|  | @ -3,9 +3,12 @@ | ||||||
|     import { server, createServer } from '$lib/client/server.js'; |     import { server, createServer } from '$lib/client/server.js'; | ||||||
|     import { app } from '$lib/client/app.js'; |     import { app } from '$lib/client/app.js'; | ||||||
|     import { get } from 'svelte/store'; |     import { get } from 'svelte/store'; | ||||||
|  |     import Lang from '$lib/lang'; | ||||||
| 
 | 
 | ||||||
|     import Logo from '$lib/../img/campfire-logo.svg'; |     import Logo from '$lib/../img/campfire-logo.svg'; | ||||||
| 
 | 
 | ||||||
|  |     const lang = Lang(); | ||||||
|  | 
 | ||||||
|     let display_error = false; |     let display_error = false; | ||||||
|     let logging_in = false; |     let logging_in = false; | ||||||
| 
 | 
 | ||||||
|  | @ -17,21 +20,21 @@ | ||||||
|         const host = event.target.host.value; |         const host = event.target.host.value; | ||||||
| 
 | 
 | ||||||
|         if (!host || host === "") { |         if (!host || host === "") { | ||||||
|             display_error = "Please enter an server domain."; |             display_error = lang.string('login.error.no_domain'); | ||||||
|             logging_in = false; |             logging_in = false; | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         server.set(await createServer(host)); |         server.set(await createServer(host)); | ||||||
|         if (!get(server)) { |         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; |             logging_in = false; | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         app.set(await api.createApp(get(server).host)); |         app.set(await api.createApp(get(server).host)); | ||||||
|         if (!get(app)) { |         if (!get(app)) { | ||||||
|             display_error = "Failed to create an application for this server." |             display_error = lang.string('login.error.create_app'); | ||||||
|             logging_in = false; |             logging_in = false; | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  | @ -44,8 +47,8 @@ | ||||||
|     <div class="app-logo"> |     <div class="app-logo"> | ||||||
|         <Logo /> |         <Logo /> | ||||||
|     </div> |     </div> | ||||||
|     <p>Welcome, fediverse user!</p> |     <p>{lang.string('login.welcome')}</p> | ||||||
|     <p>Please enter your server domain to log in.</p> |     <p>{lang.string('login.enter_domain')}</p> | ||||||
|     <div class="input-wrapper"> |     <div class="input-wrapper"> | ||||||
|         <input type="text" id="host" aria-label="server domain" class={logging_in ? "throb" : ""}> |         <input type="text" id="host" aria-label="server domain" class={logging_in ? "throb" : ""}> | ||||||
|         {#if display_error} |         {#if display_error} | ||||||
|  | @ -53,16 +56,10 @@ | ||||||
|         {/if} |         {/if} | ||||||
|     </div> |     </div> | ||||||
|     <br> |     <br> | ||||||
|     <button type="submit" id="login" class={logging_in ? "disabled" : ""}>Log in</button> |     <button type="submit" id="login" class={logging_in ? "disabled" : ""}>{lang.string('login.button')}</button> | ||||||
|     <p><small> |     <p><small>{@html lang.string('login.experimental')}</small></p> | ||||||
|         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> |  | ||||||
| 
 | 
 | ||||||
|     <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> | </form> | ||||||
| 
 | 
 | ||||||
| <style> | <style> | ||||||
|  |  | ||||||
|  | @ -27,7 +27,7 @@ | ||||||
|         z-index: 101; |         z-index: 101; | ||||||
|         display: flex; |         display: flex; | ||||||
|         justify-content: center; |         justify-content: center; | ||||||
|         position: absolute; |         position: fixed; | ||||||
|         width: 100vw; |         width: 100vw; | ||||||
|         height: 100vh; |         height: 100vh; | ||||||
|         pointer-events: none; |         pointer-events: none; | ||||||
|  | @ -38,7 +38,7 @@ | ||||||
|         z-index: 101; |         z-index: 101; | ||||||
| 
 | 
 | ||||||
|         padding: 16px; |         padding: 16px; | ||||||
|         width: 732px; |         width: 700px; | ||||||
|         border-radius: 8px; |         border-radius: 8px; | ||||||
|         box-shadow: 0px 16px 64px 4px rgba(0,0,0,0.5); |         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); |         animation: modal_pop_up .15s cubic-bezier(0.22, 1, 0.36, 1); | ||||||
|  | @ -50,7 +50,7 @@ | ||||||
|     .overlay { |     .overlay { | ||||||
|         width: 100vw; |         width: 100vw; | ||||||
|         height: 100vw; |         height: 100vw; | ||||||
|         position: absolute; |         position: fixed; | ||||||
|         top: 0; |         top: 0; | ||||||
|         left: 0; |         left: 0; | ||||||
|         z-index: 100; |         z-index: 100; | ||||||
|  |  | ||||||
|  | @ -4,62 +4,45 @@ | ||||||
|     import { server } from '$lib/client/server.js'; |     import { server } from '$lib/client/server.js'; | ||||||
|     import { app } from '$lib/client/app.js'; |     import { app } from '$lib/client/app.js'; | ||||||
|     import { playSound } from '$lib/sound.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 { goto } from '$app/navigation'; | ||||||
|     import { page } from '$app/stores'; |     import { page } from '$app/stores'; | ||||||
|     import { createEventDispatcher } from 'svelte'; |     import { createEventDispatcher, onMount } from 'svelte'; | ||||||
|     import { notifications, unread_notif_count } from '$lib/notifications.js'; |     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 Logo from '$lib/../img/campfire-logo.svg'; | ||||||
|     import Button from './Button.svelte'; |     import Button from './Button.svelte'; | ||||||
| 
 | 
 | ||||||
|     import TimelineIcon from '../../img/icons/timeline.svg'; |     import TimelineIcon from '@cf/icons/timeline.svg'; | ||||||
|     import NotificationsIcon from '../../img/icons/notifications.svg'; |     import NotificationsIcon from '@cf/icons/notifications.svg'; | ||||||
|     import ExploreIcon from '../../img/icons/explore.svg'; |     import ExploreIcon from '@cf/icons/explore.svg'; | ||||||
|     import ListIcon from '../../img/icons/lists.svg'; |     import ListIcon from '@cf/icons/lists.svg'; | ||||||
|     import FavouritesIcon from '../../img/icons/like_fill.svg'; |     import FavouritesIcon from '@cf/icons/like_fill.svg'; | ||||||
|     import BookmarkIcon from '../../img/icons/bookmark.svg'; |     import BookmarkIcon from '@cf/icons/bookmark.svg'; | ||||||
|     import HashtagIcon from '../../img/icons/hashtag.svg'; |     import HashtagIcon from '@cf/icons/hashtag.svg'; | ||||||
|     import PostIcon from '../../img/icons/post.svg'; |     import PostIcon from '@cf/icons/post.svg'; | ||||||
|     import InfoIcon from '../../img/icons/info.svg'; |     import InfoIcon from '@cf/icons/info.svg'; | ||||||
|     import SettingsIcon from '../../img/icons/settings.svg'; |     import SettingsIcon from '@cf/icons/settings.svg'; | ||||||
|     import LogoutIcon from '../../img/icons/logout.svg'; |     import LogoutIcon from '@cf/icons/logout.svg'; | ||||||
|  |     import FollowersIcon from '@cf/icons/followers.svg'; | ||||||
| 
 | 
 | ||||||
|     const VERSION = APP_VERSION; |     const VERSION = APP_VERSION; | ||||||
| 
 |     const COMMIT = APP_COMMIT; | ||||||
|  |     const lang = Lang(); | ||||||
|     const dispatch = createEventDispatcher(); |     const dispatch = createEventDispatcher(); | ||||||
| 
 | 
 | ||||||
|     function handle_btn(name) { |     function gotoProfile() { | ||||||
|         if (!$account) return; |         if (!$account) return; | ||||||
|         let route; |         playSound(); | ||||||
|         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; |  | ||||||
|         window.scrollTo({ |         window.scrollTo({ | ||||||
|             top: 0, |             top: 0, | ||||||
|             behavior: "smooth" |             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; |         if (!confirm("This will log you out. Are you sure?")) return; | ||||||
|          |          | ||||||
|         const res = await api.revokeToken( |         const res = await api.revokeToken( | ||||||
|  | @ -70,7 +53,7 @@ | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
|         if (!res.ok) |         if (!res.ok) | ||||||
|             console.warn("Token revocation failed! Dumping data anyways"); |             console.warn(lang.string('logs.token_revoke_failed')); | ||||||
| 
 | 
 | ||||||
|         account.set(false); |         account.set(false); | ||||||
|         app.set(false); |         app.set(false); | ||||||
|  | @ -78,6 +61,14 @@ | ||||||
| 
 | 
 | ||||||
|         goto("/"); |         goto("/"); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     // fetch follow requests | ||||||
|  |     let frqsFetched = false; | ||||||
|  |      | ||||||
|  |     $: if($account && !frqsFetched) { | ||||||
|  |         fetchFollowRequests(true); | ||||||
|  |         frqsFetched = true; | ||||||
|  |     } | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <div id="navigation"> | <div id="navigation"> | ||||||
|  | @ -89,79 +80,95 @@ | ||||||
| 
 | 
 | ||||||
|     {#if $account} |     {#if $account} | ||||||
|     <div id="nav-items"> |     <div id="nav-items"> | ||||||
|         <Button label="Timeline" |         <Button label="{lang.string('navigation.timeline')}" | ||||||
|                 on:click={() => handle_btn("timeline")} |                 href="/")} | ||||||
|                 active={$page.url.pathname === "/"}> |                 active={$page.url.pathname === "/"}> | ||||||
|             <svelte:fragment slot="icon"> |             <svelte:fragment slot="icon"> | ||||||
|                 <TimelineIcon/> |                 <TimelineIcon/> | ||||||
|             </svelte:fragment> |             </svelte:fragment> | ||||||
|             Timeline |             {lang.string('navigation.timeline')} | ||||||
|         </Button> |         </Button> | ||||||
|         <Button label="Notifications" |         <Button label="{lang.string('navigation.notifications')}" | ||||||
|                 on:click={() => handle_btn("notifications")} |                 href="/notifications"} | ||||||
|                 active={$page.url.pathname === "/notifications"}> |                 active={$page.url.pathname === "/notifications"}> | ||||||
|             <svelte:fragment slot="icon"> |             <svelte:fragment slot="icon"> | ||||||
|                 <NotificationsIcon/> |                 <NotificationsIcon/> | ||||||
|             </svelte:fragment> |             </svelte:fragment> | ||||||
|             Notifications |             {lang.string('navigation.notifications')} | ||||||
|             {#if $unread_notif_count} |             {#if $unread_notif_count} | ||||||
|                 <span class="notification-count"> |                 <span class="notification-count"> | ||||||
|                     {$unread_notif_count <= 99 ? $unread_notif_count : "99+"} |                     {$unread_notif_count <= 99 ? $unread_notif_count : "99+"} | ||||||
|                 </span> |                 </span> | ||||||
|             {/if} |             {/if} | ||||||
|         </Button> |         </Button> | ||||||
|         <Button label="Explore" disabled> |         {#if $followRequests && $followRequests.length > 0} | ||||||
|  |             <Button label="{lang.string('navigation.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="{lang.string('navigation.explore')}" disabled> | ||||||
|             <svelte:fragment slot="icon"> |             <svelte:fragment slot="icon"> | ||||||
|                 <ExploreIcon height="auto"/> |                 <ExploreIcon height="auto"/> | ||||||
|             </svelte:fragment> |             </svelte:fragment> | ||||||
|             Explore |             {lang.string('navigation.explore')} | ||||||
|         </Button> |         </Button> | ||||||
|         <Button label="Lists" disabled> |         <Button label="{lang.string('navigation.lists')}" disabled> | ||||||
|             <svelte:fragment slot="icon"> |             <svelte:fragment slot="icon"> | ||||||
|                 <ListIcon/> |                 <ListIcon/> | ||||||
|             </svelte:fragment> |             </svelte:fragment> | ||||||
|             Lists |             {lang.string('navigation.lists')} | ||||||
|         </Button> |         </Button> | ||||||
| 
 | 
 | ||||||
|         <div class="flex-row"> |         <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"> |                 <svelte:fragment slot="icon"> | ||||||
|                     <FavouritesIcon/> |                     <FavouritesIcon/> | ||||||
|                 </svelte:fragment> |                 </svelte:fragment> | ||||||
|             </Button> |             </Button> | ||||||
|             <Button centered label="Bookmarks" disabled> |             <Button centered label="{lang.string('navigation.bookmarks')}" disabled> | ||||||
|                 <svelte:fragment slot="icon"> |                 <svelte:fragment slot="icon"> | ||||||
|                     <BookmarkIcon/> |                     <BookmarkIcon/> | ||||||
|                 </svelte:fragment> |                 </svelte:fragment> | ||||||
|             </Button> |             </Button> | ||||||
|             <Button centered label="Hashtags" disabled> |             <Button centered label="{lang.string('navigation.hashtags')}" disabled> | ||||||
|                 <svelte:fragment slot="icon"> |                 <svelte:fragment slot="icon"> | ||||||
|                     <HashtagIcon/> |                     <HashtagIcon/> | ||||||
|                 </svelte:fragment> |                 </svelte:fragment> | ||||||
|             </Button> |             </Button> | ||||||
|         </div> |         </div> | ||||||
| 
 | 
 | ||||||
|         <Button filled label="Post" on:click={() => dispatch("compose")}> |         <Button filled label="{lang.string('compose')}" on:click={() => dispatch("compose")}> | ||||||
|             <svelte:fragment slot="icon"> |             <svelte:fragment slot="icon"> | ||||||
|                 <PostIcon/> |                 <PostIcon/> | ||||||
|             </svelte:fragment> |             </svelte:fragment> | ||||||
|             Post |             {lang.string('compose')} | ||||||
|         </Button> |         </Button> | ||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
|     <div id="account-items"> |     <div id="account-items"> | ||||||
|         <div class="flex-row"> |         <div class="flex-row"> | ||||||
|             <Button centered label="Profile information" disabled> |             <Button centered label="{lang.string('navigation.profile_information')}" disabled> | ||||||
|                 <svelte:fragment slot="icon"> |                 <svelte:fragment slot="icon"> | ||||||
|                     <InfoIcon/> |                     <InfoIcon/> | ||||||
|                 </svelte:fragment> |                 </svelte:fragment> | ||||||
|             </Button> |             </Button> | ||||||
|             <Button centered label="Settings" disabled> |             <Button centered label="{lang.string('navigation.settings')}" disabled> | ||||||
|                 <svelte:fragment slot="icon"> |                 <svelte:fragment slot="icon"> | ||||||
|                     <SettingsIcon/> |                     <SettingsIcon/> | ||||||
|                 </svelte:fragment> |                 </svelte:fragment> | ||||||
|             </Button> |             </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"> |                 <svelte:fragment slot="icon"> | ||||||
|                     <LogoutIcon/> |                     <LogoutIcon/> | ||||||
|                 </svelte:fragment> |                 </svelte:fragment> | ||||||
|  | @ -169,11 +176,11 @@ | ||||||
|         </div> |         </div> | ||||||
| 
 | 
 | ||||||
|         <div id="account-button"> |         <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"> |             <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}`}> |                 <span class="username" title={`@${$account.username}@${$account.host}`}> | ||||||
|                     {`@${$account.username}@${$account.host}`} |                     {$account.fqn} | ||||||
|                 </span> |                 </span> | ||||||
|             </div> |             </div> | ||||||
|         </div> |         </div> | ||||||
|  | @ -181,11 +188,11 @@ | ||||||
|     {/if} |     {/if} | ||||||
| 
 | 
 | ||||||
|     <span class="version"> |     <span class="version"> | ||||||
|         campfire v{VERSION} |         campfire v{VERSION} ({COMMIT}) | ||||||
|         <br> |         <br> | ||||||
|         <ul> |         <ul> | ||||||
|             <li><a href="https://git.arimelody.me/blisstown/campfire">source</a></li> |             <li><a href="https://git.arimelody.me/blisstown/campfire">{lang.string('source')}</a></li> | ||||||
|             <li><a href="https://github.com/blisstown/campfire/issues">issues</a></li> |             <li><a href="https://codeberg.org/arimelody/campfire/issues">{lang.string('issues')}</a></li> | ||||||
|         </ul> |         </ul> | ||||||
|     </span> |     </span> | ||||||
| </div> | </div> | ||||||
|  | @ -200,6 +207,8 @@ | ||||||
|         height: calc(100vh - 32px); |         height: calc(100vh - 32px); | ||||||
|         border-radius: 8px; |         border-radius: 8px; | ||||||
|         background-color: var(--bg-800); |         background-color: var(--bg-800); | ||||||
|  |         transition: background-color .1s linear; | ||||||
|  |         user-select: none; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .server-header { |     .server-header { | ||||||
|  | @ -213,6 +222,7 @@ | ||||||
|         background-size: cover; |         background-size: cover; | ||||||
|         background-color: var(--bg-600); |         background-color: var(--bg-600); | ||||||
|         background-image: linear-gradient(to top, var(--bg-800), var(--bg-600)); |         background-image: linear-gradient(to top, var(--bg-800), var(--bg-600)); | ||||||
|  |         transition: background .1s linear; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .server-icon { |     .server-icon { | ||||||
|  | @ -270,6 +280,7 @@ | ||||||
|         font-size: .9em; |         font-size: .9em; | ||||||
|         opacity: .6; |         opacity: .6; | ||||||
|         text-align: center; |         text-align: center; | ||||||
|  |         user-select: text; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .version ul { |     .version ul { | ||||||
|  |  | ||||||
|  | @ -2,17 +2,20 @@ | ||||||
|     import { server } from '$lib/client/server'; |     import { server } from '$lib/client/server'; | ||||||
|     import { goto } from '$app/navigation'; |     import { goto } from '$app/navigation'; | ||||||
| 
 | 
 | ||||||
|     import ReplyIcon from '$lib/../img/icons/reply.svg'; |     import ReplyIcon from '@cf/icons/reply.svg'; | ||||||
|     import RepostIcon from '$lib/../img/icons/repost.svg'; |     import RepostIcon from '@cf/icons/repost.svg'; | ||||||
|     import FavouriteIcon from '$lib/../img/icons/like.svg'; |     import FavouriteIcon from '@cf/icons/like.svg'; | ||||||
|     import ReactIcon from '$lib/../img/icons/react.svg'; |     import ReactIcon from '@cf/icons/react.svg'; | ||||||
|     // import QuoteIcon from '$lib/../img/icons/quote.svg'; |     // import QuoteIcon from '$lib/../img/icons/quote.svg'; | ||||||
|     import ReactionBar from '$lib/ui/post/ReactionBar.svelte'; |     import ReactionBar from '$lib/ui/post/ReactionBar.svelte'; | ||||||
|     import ActionBar from '$lib/ui/post/ActionBar.svelte'; |     import ActionBar from '$lib/ui/post/ActionBar.svelte'; | ||||||
|  |     import Lang from '$lib/lang'; | ||||||
|  | 
 | ||||||
|  |     const lang = Lang(); | ||||||
| 
 | 
 | ||||||
|     let mention = (accounts) => { |     let mention = (accounts) => { | ||||||
|         let res = `<a href=${account.url}>${account.rich_name}</a>`; |         let res = `<a href="/${$server.host}/${account.fqn}">${account.rich_name}</a>`; | ||||||
|         if (accounts.length > 1) res += ` and <strong>${accounts.length - 1}</strong> others`; |         if (accounts.length > 1) res += ' ' + lang.string('notification.and_others').replaceAll('%1', accounts.length - 1); | ||||||
|         return res; |         return res; | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|  | @ -20,23 +23,23 @@ | ||||||
|     let activity_text = function (type) { |     let activity_text = function (type) { | ||||||
|         switch (type) { |         switch (type) { | ||||||
|             case "mention": |             case "mention": | ||||||
|                 return `%1 mentioned you.`; |                 return lang.string('notification.mention'); | ||||||
|             case "reblog": |             case "reblog": | ||||||
|                 return `%1 boosted your post.`; |                 return lang.string('notification.reblog'); | ||||||
|             case "reaction": |             case "reaction": | ||||||
|                 return `%1 reacted to your post.`; |                 return lang.string('notification.reaction'); | ||||||
|             case "follow": |             case "follow": | ||||||
|                 return `%1 followed you.`; |                 return lang.string('notification.follow'); | ||||||
|             case "follow_request": |             case "follow_request": | ||||||
|                 return `%1 requested to follow you.`; |                 return lang.string('notification.follow.request'); | ||||||
|             case "favourite": |             case "favourite": | ||||||
|                 return `%1 favourited your post.`; |                 return lang.string('notification.favourite'); | ||||||
|             case "poll": |             case "poll": | ||||||
|                 return `%1's poll as ended.`; |                 return lang.string('notification.poll'); | ||||||
|             case "update": |             case "update": | ||||||
|                 return `%1 updated their post.`; |                 return lang.string('notification.update'); | ||||||
|             default: |             default: | ||||||
|                 return `%1 poked you!`; |                 return lang.string('notification.default'); | ||||||
|         } |         } | ||||||
|     }(data.type); |     }(data.type); | ||||||
| 
 | 
 | ||||||
|  | @ -87,7 +90,7 @@ | ||||||
|         </span> |         </span> | ||||||
|         <span class="notif-avatars"> |         <span class="notif-avatars"> | ||||||
|             {#if data.accounts.length == 1} |             {#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" /> |                     <img src={data.accounts[0].avatar_url} alt="" width="28" height="28" /> | ||||||
|                 </a> |                 </a> | ||||||
|             {:else} |             {:else} | ||||||
|  | @ -141,18 +144,22 @@ | ||||||
| <style> | <style> | ||||||
|     .notification { |     .notification { | ||||||
|         display: block; |         display: block; | ||||||
|         margin-bottom: 8px; |         border-top: 1px solid color-mix(in srgb, transparent, var(--text) 25%); | ||||||
|         padding: 16px; |         padding: 16px; | ||||||
|         border-radius: 8px; |  | ||||||
|         background: var(--bg-800); |  | ||||||
|         text-decoration: inherit; |         text-decoration: inherit; | ||||||
|         color: inherit; |         color: inherit; | ||||||
|         transition: background-color .1s; |         transition: background-color .1s; | ||||||
|         cursor: pointer; |         cursor: pointer; | ||||||
|  | 
 | ||||||
|  |         background-color: var(--bg-900); | ||||||
|  |     } | ||||||
|  |     .notification:first-of-type { | ||||||
|  |         border-top: none; | ||||||
|  | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .notification:hover { |     .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 { |     header { | ||||||
|  | @ -280,7 +287,7 @@ | ||||||
|         width: calc(100% - 16px); |         width: calc(100% - 16px); | ||||||
|         margin-bottom: 10px; |         margin-bottom: 10px; | ||||||
|         padding: 4px 8px; |         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); |         background: repeating-linear-gradient(-45deg, transparent, transparent 10px, var(--warn-bg) 10px, var(--warn-bg) 20px); | ||||||
|         font-family: inherit; |         font-family: inherit; | ||||||
|         font-size: inherit; |         font-size: inherit; | ||||||
|  |  | ||||||
|  | @ -1,5 +1,11 @@ | ||||||
|  | <script> | ||||||
|  |     import Lang from '$lib/lang'; | ||||||
|  | 
 | ||||||
|  |     const lang = Lang(); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
| <div id="widgets"> | <div id="widgets"> | ||||||
|     <input type="text" id="search" placeholder="Search"> |     <input type="text" id="search" placeholder="{lang.string('search')}"> | ||||||
| </div> | </div> | ||||||
| 
 | 
 | ||||||
| <style> | <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 { timeline } from '$lib/timeline'; | ||||||
|     import { parseReactions } from '$lib/post'; |     import { parseReactions } from '$lib/post'; | ||||||
|     import { playSound } from '$lib/sound'; |     import { playSound } from '$lib/sound'; | ||||||
|  |     import Lang from '$lib/lang'; | ||||||
| 
 | 
 | ||||||
|     import ActionButton from './ActionButton.svelte'; |     import ActionButton from './ActionButton.svelte'; | ||||||
| 
 | 
 | ||||||
|     import ReplyIcon from '../../../img/icons/reply.svg'; |     import ReplyIcon from '@cf/icons/reply.svg'; | ||||||
|     import RepostIcon from '../../../img/icons/repost.svg'; |     import RepostIcon from '@cf/icons/repost.svg'; | ||||||
|     import FavouriteIcon from '../../../img/icons/like.svg'; |     import FavouriteIcon from '@cf/icons/like.svg'; | ||||||
|     import FavouriteIconFill from '../../../img/icons/like_fill.svg'; |     import FavouriteIconFill from '@cf/icons/like_fill.svg'; | ||||||
|     import QuoteIcon from '../../../img/icons/quote.svg'; |     import QuoteIcon from '@cf/icons/quote.svg'; | ||||||
|     import MoreIcon from '../../../img/icons/more.svg'; |     import MoreIcon from '@cf/icons/more.svg'; | ||||||
|     import DeleteIcon from '../../../img/icons/bin.svg'; |     import DeleteIcon from '@cf/icons/bin.svg'; | ||||||
| 
 | 
 | ||||||
|     export let post; |     export let post; | ||||||
| 
 | 
 | ||||||
|  |     const lang = Lang(); | ||||||
|  | 
 | ||||||
|     async function toggleBoost() { |     async function toggleBoost() { | ||||||
|         if (!$app || !$app.token) return; |         if (!$app || !$app.token) return; | ||||||
| 
 | 
 | ||||||
|  | @ -74,29 +77,29 @@ | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <div class="post-actions" aria-label="Post actions" role="toolbar" tabindex="0" on:mouseup|stopPropagation on:keydown|stopPropagation> | <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/> |         <ReplyIcon/> | ||||||
|     </ActionButton> |     </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/> |         <RepostIcon/> | ||||||
|         <svelte:fragment slot="activeIcon"> |         <svelte:fragment slot="activeIcon"> | ||||||
|             <RepostIcon/> |             <RepostIcon/> | ||||||
|         </svelte:fragment> |         </svelte:fragment> | ||||||
|     </ActionButton> |     </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/> |         <FavouriteIcon/> | ||||||
|         <svelte:fragment slot="activeIcon"> |         <svelte:fragment slot="activeIcon"> | ||||||
|             <FavouriteIconFill/> |             <FavouriteIconFill/> | ||||||
|         </svelte:fragment> |         </svelte:fragment> | ||||||
|     </ActionButton> |     </ActionButton> | ||||||
|     <ActionButton type="quote" label="Quote" disabled> |     <ActionButton type="quote" label="{lang.string('post.actions.quote')}" disabled> | ||||||
|         <QuoteIcon/> |         <QuoteIcon/> | ||||||
|     </ActionButton> |     </ActionButton> | ||||||
|     <ActionButton type="more" label="More" disabled> |     <ActionButton type="more" label="{lang.string('post.actions.more')}" disabled> | ||||||
|         <MoreIcon/> |         <MoreIcon/> | ||||||
|     </ActionButton> |     </ActionButton> | ||||||
|     {#if $account && post.account.id === $account.id} |     {#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/> |             <DeleteIcon/> | ||||||
|         </ActionButton> |         </ActionButton> | ||||||
|     {/if} |     {/if} | ||||||
|  |  | ||||||
|  | @ -1,25 +1,29 @@ | ||||||
| <script> | <script> | ||||||
|  |     import Lang from '$lib/lang'; | ||||||
|  | 
 | ||||||
|     export let post; |     export let post; | ||||||
| 
 | 
 | ||||||
|     let open_warned = false; |     const lang = Lang(); | ||||||
|  | 
 | ||||||
|  |     let open = false; | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <div class="post-body"> | <div class="post-body"> | ||||||
|     {#if post.warning} |     {#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> |         <strong> | ||||||
|             {post.warning} |             {post.warning} | ||||||
|             <span class="warning-instructions"> |             <span class="instructions"> | ||||||
|                 {#if !open_warned} |                 {#if !open} | ||||||
|                     (click to reveal) |                     {lang.string('post.warning.show')} | ||||||
|                 {:else} |                 {:else} | ||||||
|                     (click to hide) |                     {lang.string('post.warning.hide')} | ||||||
|                 {/if} |                 {/if} | ||||||
|             </span> |             </span> | ||||||
|         </strong> |         </strong> | ||||||
|         </button> |         </button> | ||||||
|     {/if} |     {/if} | ||||||
|     {#if !post.warning || open_warned} |     {#if !post.warning || open} | ||||||
|         {#if post.rich_text} |         {#if post.rich_text} | ||||||
|             <span class="post-text">{@html post.rich_text}</span> |             <span class="post-text">{@html post.rich_text}</span> | ||||||
|         {:else if post.html} |         {:else if post.html} | ||||||
|  | @ -60,7 +64,7 @@ | ||||||
|         width: 100%; |         width: 100%; | ||||||
|         margin-bottom: 10px; |         margin-bottom: 10px; | ||||||
|         padding: 4px 8px; |         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); |         background: repeating-linear-gradient(-45deg, transparent, transparent 10px, var(--warn-bg) 10px, var(--warn-bg) 20px); | ||||||
|         font-family: inherit; |         font-family: inherit; | ||||||
|         font-size: inherit; |         font-size: inherit; | ||||||
|  | @ -78,13 +82,12 @@ | ||||||
|         box-shadow: 0 0 8px var(--warn-bg); |         box-shadow: 0 0 8px var(--warn-bg); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .post-warning .warning-instructions { |     .post-warning .instructions { | ||||||
|         font-weight: normal; |         font-weight: normal; | ||||||
|         opacity: .5; |         opacity: .5; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .post-text { |     .post-text { | ||||||
|         font-size: .9em; |  | ||||||
|         line-height: 1.45em; |         line-height: 1.45em; | ||||||
|         word-wrap: break-word; |         word-wrap: break-word; | ||||||
|     } |     } | ||||||
|  | @ -129,7 +132,9 @@ | ||||||
|         color: var(--accent); |         color: var(--accent); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .post-text :global(a.mention) { |     /* mention gets used in other places (bios) so it's global */ | ||||||
|  |      | ||||||
|  |     :global(a.mention) { | ||||||
|         color: inherit; |         color: inherit; | ||||||
|         font-weight: 600; |         font-weight: 600; | ||||||
|         padding: 3px 6px; |         padding: 3px 6px; | ||||||
|  | @ -138,7 +143,7 @@ | ||||||
|         text-decoration: none; |         text-decoration: none; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .post-text :global(a.mention:hover) { |     :global(a.mention:hover) { | ||||||
|         text-decoration: underline; |         text-decoration: underline; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,10 @@ | ||||||
| <script> | <script> | ||||||
|     import { shorthand as short_time } from '$lib/time.js'; |     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; |     export let post; | ||||||
| 
 | 
 | ||||||
|  | @ -7,17 +12,19 @@ | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <div class="post-context"> | <div class="post-context"> | ||||||
|     <span class="post-context-icon">🔁</span> |     <span class="post-context-icon"> | ||||||
|  |         <RepostIcon width="22px" /> | ||||||
|  |     </span> | ||||||
|     <span class="post-context-action"> |     <span class="post-context-action"> | ||||||
|         <a href={post.account.url} target="_blank"><span class="name"> |         { @html | ||||||
|                 {@html post.account.rich_name}</span> |         lang.string('post.boosted', | ||||||
|         </a> |         `<a href="/${$server.host}/${post.account.fqn}"><span class="name">${post.account.rich_name}</span></a>`) | ||||||
|         boosted this post. |         } | ||||||
|     </span> |     </span> | ||||||
|     <span class="post-context-time"> |     <span class="post-context-time"> | ||||||
|         <time title="{time_string}">{short_time(post.created_at)}</time> |         <time title="{time_string}">{short_time(post.created_at)}</time> | ||||||
|         {#if post.visibility !== "public"} |         {#if post.visibility !== "public"} | ||||||
|             <span class="post-visibility">- {post.visibility}</span> |             <span class="post-visibility">- {lang.string(`post.visibility.${post.visibility}`)}</span> | ||||||
|         {/if} |         {/if} | ||||||
|     </span> |     </span> | ||||||
| </div> | </div> | ||||||
|  | @ -38,11 +45,13 @@ | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .post-context-icon { |     .post-context-icon { | ||||||
|         margin-right: 4px; |         height: 1em; | ||||||
|  |         margin-right: .1em; | ||||||
|  |         transform: translateY(-.3em); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .post-context a, |     :global(.post-context a), | ||||||
|     .post-context a:visited { |     :global(.post-context a:visited) { | ||||||
|         color: inherit; |         color: inherit; | ||||||
|         text-decoration: none; |         text-decoration: none; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -2,6 +2,7 @@ | ||||||
|     import { onMount } from 'svelte'; |     import { onMount } from 'svelte'; | ||||||
|     import { goto } from '$app/navigation'; |     import { goto } from '$app/navigation'; | ||||||
|     import { server } from '$lib/client/server'; |     import { server } from '$lib/client/server'; | ||||||
|  |     import Lang from '$lib/lang'; | ||||||
| 
 | 
 | ||||||
|     import BoostContext from './BoostContext.svelte'; |     import BoostContext from './BoostContext.svelte'; | ||||||
|     import ReplyContext from './ReplyContext.svelte'; |     import ReplyContext from './ReplyContext.svelte'; | ||||||
|  | @ -12,6 +13,9 @@ | ||||||
| 
 | 
 | ||||||
|     export let post_data; |     export let post_data; | ||||||
|     export let focused = false; |     export let focused = false; | ||||||
|  |     export let pinned = false; | ||||||
|  | 
 | ||||||
|  |     const lang = Lang(); | ||||||
| 
 | 
 | ||||||
|     let post_context = undefined; |     let post_context = undefined; | ||||||
|     let post = post_data; |     let post = post_data; | ||||||
|  | @ -41,8 +45,6 @@ | ||||||
|             window.scrollTo(0, el.scrollHeight); |             window.scrollTo(0, el.scrollHeight); | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
| 
 |  | ||||||
|     let aria_label = post.account.username + '; ' + post.text + '; ' + post.created_at; |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <div class="post-container"> | <div class="post-container"> | ||||||
|  | @ -51,12 +53,15 @@ | ||||||
|             <ReplyContext post={reply} /> |             <ReplyContext post={reply} /> | ||||||
|         {/await} |         {/await} | ||||||
|     {/if} |     {/if} | ||||||
|  |     {#if pinned} | ||||||
|  |         <p class="post-context pinned">{lang.string('post.pinned')}</p> | ||||||
|  |     {/if} | ||||||
|     {#if is_boost && !post_context.text} |     {#if is_boost && !post_context.text} | ||||||
|         <BoostContext post={post_context} /> |         <BoostContext post={post_context} /> | ||||||
|     {/if} |     {/if} | ||||||
|     <article |     <article | ||||||
|             class={"post" + (focused ? " focused" : "")} |             class={"post" + (focused ? " focused" : "")} | ||||||
|             aria-label={aria_label} |             aria-label={post.account.username + '; ' + post.text + '; ' + post.created_at} | ||||||
|             bind:this={el} |             bind:this={el} | ||||||
|             on:mousedown={e => {mouse_pos.left = e.pageX; mouse_pos.top = e.pageY}} |             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)}} |             on:mouseup={e => {if (e.pageX == mouse_pos.left && e.pageY == mouse_pos.top) gotoPost(e)}} | ||||||
|  | @ -74,18 +79,24 @@ | ||||||
| 
 | 
 | ||||||
| <style> | <style> | ||||||
|     .post-container { |     .post-container { | ||||||
|         width: 732px; |  | ||||||
|         max-width: 732px; |  | ||||||
|         margin-bottom: 8px; |  | ||||||
|         display: flex; |         display: flex; | ||||||
|         flex-direction: column; |         flex-direction: column; | ||||||
|         border-radius: 8px; |         border-top: 1px solid color-mix(in srgb, transparent, var(--text) 20%); | ||||||
|         background-color: var(--bg-800); |         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 { |     .post { | ||||||
|         padding: 16px; |         padding: 16px; | ||||||
|         border-radius: 8px; |  | ||||||
|         transition: background-color .1s; |         transition: background-color .1s; | ||||||
|         cursor: pointer; |         cursor: pointer; | ||||||
|     } |     } | ||||||
|  | @ -100,7 +111,7 @@ | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .post:hover { |     .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 { |     .post-container:has(.post-context) .post { | ||||||
|  |  | ||||||
|  | @ -1,5 +1,9 @@ | ||||||
| <script> | <script> | ||||||
|     import { shorthand as short_time } from '$lib/time.js'; |     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 post; | ||||||
|     export let reply = undefined; |     export let reply = undefined; | ||||||
|  | @ -8,21 +12,19 @@ | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <div class={"post-header-container" + (reply ? " reply" : "")}> | <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"> |         <img src={post.account.avatar_url} type={post.account.avatar_type} alt="" width="48" height="48" class="post-avatar" loading="lazy" decoding="async"> | ||||||
|     </a> |     </a> | ||||||
|     <header class="post-header"> |     <header class="post-header"> | ||||||
|         <div class="post-user-info" on:mouseup|stopPropagation> |         <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> |             <span class="username">{post.account.mention}</span> | ||||||
|         </div> |         </div> | ||||||
|         <div class="post-info" on:mouseup|stopPropagation> |         <div class="post-info" on:mouseup|stopPropagation> | ||||||
|             <a href={post.url} target="_blank" class="created-at"> |             <a href={post.url} target="_blank" class="created-at"> | ||||||
|                 <time title={time_string}>{short_time(post.created_at)}</time> |                 <time title={time_string}>{short_time(post.created_at)}</time> | ||||||
|                 {#if post.visibility !== "public"} |                 <br> | ||||||
|                     <br> |                 <span class="post-visibility">{lang.string('post.visibility.' + post.visibility)}</span> | ||||||
|                     <span class="post-visibility">{post.visibility}</span> |  | ||||||
|                 {/if} |  | ||||||
|             </a> |             </a> | ||||||
|         </div> |         </div> | ||||||
|     </header> |     </header> | ||||||
|  |  | ||||||
|  | @ -51,11 +51,7 @@ | ||||||
|                 {/if} |                 {/if} | ||||||
|         </ReactionButton> |         </ReactionButton> | ||||||
|     {/each} |     {/each} | ||||||
|     <ReactionButton |     <ReactionButton disabled> | ||||||
|             type="reaction" |  | ||||||
|             title="react" |  | ||||||
|             label="React" |  | ||||||
|             disabled> |  | ||||||
|     <ReactIcon/> |     <ReactIcon/> | ||||||
|     </ReactionButton> |     </ReactionButton> | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
|  | @ -1,11 +1,14 @@ | ||||||
| <script> | <script> | ||||||
|     import { playSound } from '../../sound.js'; |     import { playSound } from '../../sound.js'; | ||||||
|     import { createEventDispatcher } from 'svelte'; |     import { createEventDispatcher } from 'svelte'; | ||||||
|  |     import Lang from '$lib/lang'; | ||||||
|     const dispatch = createEventDispatcher(); |     const dispatch = createEventDispatcher(); | ||||||
| 
 | 
 | ||||||
|  |     const lang = Lang(); | ||||||
|  | 
 | ||||||
|     export let type = "react"; |     export let type = "react"; | ||||||
|     export let label = "React"; |     export let label = lang.string('post.actions.react'); | ||||||
|     export let title = label; |     export let title = lang.string('post.actions.react'); | ||||||
|     export let count = 0; |     export let count = 0; | ||||||
|     export let active = false; |     export let active = false; | ||||||
|     export let disabled = false; |     export let disabled = false; | ||||||
|  |  | ||||||
|  | @ -61,13 +61,12 @@ | ||||||
|         flex-direction: row; |         flex-direction: row; | ||||||
|         color: var(--text); |         color: var(--text); | ||||||
|         align-items: stretch; |         align-items: stretch; | ||||||
|         border-radius: 8px; |  | ||||||
|         transition: background-color .1s; |         transition: background-color .1s; | ||||||
|         cursor: pointer; |         cursor: pointer; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .post-reply:hover { |     .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 { |     .post-avatar-container { | ||||||
|  |  | ||||||
							
								
								
									
										78
									
								
								src/lib/ui/timeline/Timeline.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								src/lib/ui/timeline/Timeline.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,78 @@ | ||||||
|  | <script> | ||||||
|  |     import { page } from '$app/stores'; | ||||||
|  |     import { account } from '$lib/stores/account.js'; | ||||||
|  |     import { timeline, getTimeline, clearTimeline } from '$lib/timeline.js'; | ||||||
|  |     import { app_name } from '$lib/config.js'; | ||||||
|  |     import Post from '$lib/ui/post/Post.svelte'; | ||||||
|  |      | ||||||
|  |     import Lang from '$lib/lang'; | ||||||
|  |     const lang = Lang(); | ||||||
|  | 
 | ||||||
|  |     // TODO: refactor to enum when moving to TS | ||||||
|  |     export let timelineType; | ||||||
|  | 
 | ||||||
|  |     $: { | ||||||
|  |         // awful hack to update timeline fresh | ||||||
|  |         // when timelineType is updated | ||||||
|  |         // | ||||||
|  |         // TODO: migrate to $effect when migrating to svelte 5 | ||||||
|  |         timelineType = timelineType | ||||||
|  | 
 | ||||||
|  |         clearTimeline() | ||||||
|  |         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) getCurrentTimeline(); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     document.addEventListener('scroll', () => { | ||||||
|  |         if ($account && $page.url.pathname !== "/") return; | ||||||
|  |         if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 2048) { | ||||||
|  |             getCurrentTimeline(); | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  | </script> | ||||||
|  | <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> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | <style> | ||||||
|  |     #feed { | ||||||
|  |         margin-bottom: 20vh; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .loading { | ||||||
|  |         width: 100%; | ||||||
|  |         height: 80vh; | ||||||
|  |         display: flex; | ||||||
|  |         justify-content: center; | ||||||
|  |         align-items: center; | ||||||
|  |         font-size: 2em; | ||||||
|  |         font-weight: bold; | ||||||
|  |     } | ||||||
|  | </style> | ||||||
|  | @ -1,17 +1,20 @@ | ||||||
| <script> | <script> | ||||||
|     import '$lib/app.css'; |     import '../app.css'; | ||||||
|     import * as api from '$lib/api.js'; |     import * as api from '$lib/api.js'; | ||||||
|     import { server } from '$lib/client/server.js'; |     import { server } from '$lib/client/server.js'; | ||||||
|     import { app } from '$lib/client/app.js'; |     import { app } from '$lib/client/app.js'; | ||||||
|     import { account } from '$lib/stores/account.js'; |     import { account } from '$lib/stores/account.js'; | ||||||
|     import { parseAccount } from '$lib/account.js'; |     import { parseAccount } from '$lib/account.js'; | ||||||
|     import { unread_notif_count, last_read_notif_id } from '$lib/notifications.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 Navigation from '$lib/ui/Navigation.svelte'; | ||||||
|     import Modal from '@cf/ui/Modal.svelte'; |     import Modal from '@cf/ui/Modal.svelte'; | ||||||
|     import Composer from '@cf/ui/Composer.svelte'; |     import Composer from '@cf/ui/Composer.svelte'; | ||||||
|     import Widgets from '$lib/ui/Widgets.svelte'; |     import Widgets from '$lib/ui/Widgets.svelte'; | ||||||
| 
 | 
 | ||||||
|  |     const lang = Lang(); | ||||||
|  | 
 | ||||||
|     let show_composer = false; |     let show_composer = false; | ||||||
| 
 | 
 | ||||||
|     async function init() { |     async function init() { | ||||||
|  | @ -25,7 +28,7 @@ | ||||||
|         if (!data) return; |         if (!data) return; | ||||||
| 
 | 
 | ||||||
|         account.set(parseAccount(data)); |         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 |         // spin up async task to fetch notifications | ||||||
|         const notif_data = await api.getNotifications( |         const notif_data = await api.getNotifications( | ||||||
|  | @ -48,7 +51,7 @@ | ||||||
|     <main> |     <main> | ||||||
|         {#await init()} |         {#await init()} | ||||||
|             <div class="loading throb"> |             <div class="loading throb"> | ||||||
|                 <span>just a moment...</span> |                 <span>{lang.string('loading')}</span> | ||||||
|             </div> |             </div> | ||||||
|         {:then} |         {:then} | ||||||
|             <slot></slot> |             <slot></slot> | ||||||
|  |  | ||||||
|  | @ -2,70 +2,57 @@ | ||||||
|     import { page } from '$app/stores'; |     import { page } from '$app/stores'; | ||||||
|     import { account } from '$lib/stores/account.js'; |     import { account } from '$lib/stores/account.js'; | ||||||
|     import { timeline, getTimeline } from '$lib/timeline.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 LoginForm from '$lib/ui/LoginForm.svelte'; | ||||||
|     import Button from '$lib/ui/Button.svelte'; |     import Button from '$lib/ui/Button.svelte'; | ||||||
|     import Post from '$lib/ui/post/Post.svelte'; |     import Post from '$lib/ui/post/Post.svelte'; | ||||||
|  |     import PageHeader from '../lib/ui/core/PageHeader.svelte'; | ||||||
|  |     import Timeline from '../lib/ui/timeline/Timeline.svelte'; | ||||||
| 
 | 
 | ||||||
|     account.subscribe(account => { |     const lang = Lang(); | ||||||
|         if (account) getTimeline(); |  | ||||||
|     }); |  | ||||||
| 
 | 
 | ||||||
|     document.addEventListener("scroll", () => { |     // TODO: refactor to enum when moving to TS | ||||||
|         if ($account && $page.url.pathname !== "/") return; |     let timelineType = localStorage.getItem(app_name + '_selected_timeline') || "home"; | ||||||
|         if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 2048) { | 
 | ||||||
|             getTimeline(); |     $: { | ||||||
|         } |         // 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); | ||||||
|  |     } | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| {#if $account} | {#if $account} | ||||||
|     <header> |     <PageHeader title={lang.string(`timeline.${timelineType}`)}> | ||||||
|         <h1>Home</h1> |         <Button centered  | ||||||
|         <nav> |         active={(timelineType == "home")}  | ||||||
|             <Button centered active>Home</Button> |         on:click={() => timelineType = "home"}> | ||||||
|             <Button centered disabled>Local</Button> |             {lang.string('timeline.home')} | ||||||
|             <Button centered disabled>Federated</Button> |         </Button> | ||||||
|         </nav> |         <Button centered  | ||||||
|     </header> |         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"> |     <Timeline timelineType={timelineType}/> | ||||||
|         {#if $timeline.length <= 0} |  | ||||||
|             <div class="loading throb"> |  | ||||||
|                 <span>getting the feed...</span> |  | ||||||
|             </div> |  | ||||||
|         {/if} |  | ||||||
|         {#each $timeline as post} |  | ||||||
|             <Post post_data={post} /> |  | ||||||
|         {/each} |  | ||||||
|     </div> |  | ||||||
| {:else} | {:else} | ||||||
|     <LoginForm /> |     <LoginForm /> | ||||||
| {/if} | {/if} | ||||||
| 
 | 
 | ||||||
| <style> | <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; |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     .loading { |     .loading { | ||||||
|         width: 100%; |         width: 100%; | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| export async function load({ params }) { | export async function load({ params }) { | ||||||
|     return { |     return { | ||||||
|         server_domain: params.server |         server_host: params.server | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,8 +1,10 @@ | ||||||
| import { error } from '@sveltejs/kit'; |  | ||||||
| 
 |  | ||||||
| export async function load({ params }) { | export async function load({ params }) { | ||||||
|     return error(404, 'Not Found'); |     let handle = params.account; | ||||||
|     // return {
 |     if (handle.startsWith('@')) | ||||||
|     //     account_name: params.account
 |         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 }) { | export async function load({ params }) { | ||||||
|  |     let handle = params.account; | ||||||
|  |     if (handle.startsWith('@')) | ||||||
|  |         handle = handle.substring(1); | ||||||
|  | 
 | ||||||
|     return { |     return { | ||||||
|         server_host: params.server, |         server_host: params.server, | ||||||
|         account_handle: params.account, |         account_handle: params.account, | ||||||
|  |  | ||||||
|  | @ -4,13 +4,16 @@ | ||||||
|     import { app } from '$lib/client/app.js'; |     import { app } from '$lib/client/app.js'; | ||||||
|     import { parsePost } from '$lib/post.js'; |     import { parsePost } from '$lib/post.js'; | ||||||
|     import { goto, afterNavigate } from '$app/navigation'; |     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 Post from '$lib/ui/post/Post.svelte'; | ||||||
|     import Button from '$lib/ui/Button.svelte'; |     import Button from '$lib/ui/Button.svelte'; | ||||||
| 
 | 
 | ||||||
|     export let data; |     export let data; | ||||||
| 
 | 
 | ||||||
|  |     const lang = Lang(); | ||||||
|  | 
 | ||||||
|     let post = fetchPost(data.post_id); |     let post = fetchPost(data.post_id); | ||||||
|     let error = false; |     let error = false; | ||||||
|     let previous_page = base; |     let previous_page = base; | ||||||
|  | @ -30,16 +33,16 @@ | ||||||
|             // TODO: make `server` a key/value pair to support multiple servers |             // TODO: make `server` a key/value pair to support multiple servers | ||||||
|             server.set(await createServer(data.server_host)); |             server.set(await createServer(data.server_host)); | ||||||
|             if (!$server) { |             if (!$server) { | ||||||
|                 error = `Failed to connect to <code>${data.server_host}</code>.`; |                 error = lang.string('error.connection_failed', data.server_host); | ||||||
|                 console.error(`Failed to connect to ${data.server_host}.`); |                 console.error(lang.string('logs.connection_failed', data.server_host)); | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         const post_data = await api.getPost($server.host, token, post_id); |         const post_data = await api.getPost($server.host, token, post_id); | ||||||
|         if (!post_data || post_data.error) { |         if (!post_data || post_data.error) { | ||||||
|             error = `Failed to retrieve post <code>${post_id}</code>.`; |             error = lang.string('error.post_fetch_failed_id', post_id); | ||||||
|             console.error(`Failed to retrieve post ${post_id}.`); |             console.error(lang.string('logs.post_fetch_failed_id', post_id)); | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|         let post = await parsePost(post_data, 0); |         let post = await parsePost(post_data, 0); | ||||||
|  | @ -68,7 +71,7 @@ | ||||||
| 
 | 
 | ||||||
| {#await post} | {#await post} | ||||||
|     <div class="loading throb"> |     <div class="loading throb"> | ||||||
|         <span>loading post...</span> |         <span>{lang.string('post.loading')}</span> | ||||||
|     </div> |     </div> | ||||||
| {:then post} | {:then post} | ||||||
|     {#if error} |     {#if error} | ||||||
|  | @ -77,12 +80,12 @@ | ||||||
|         <header> |         <header> | ||||||
|             {#if previous_page} |             {#if previous_page} | ||||||
|                 <nav> |                 <nav> | ||||||
|                     <Button centered on:click={() => {goto(previous_page)}}>Back</Button> |                     <Button centered on:click={() => {goto(previous_page)}}>{lang.string('navigation.back')}</Button> | ||||||
|                 </nav> |                 </nav> | ||||||
|             {/if} |             {/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> |             <h1> | ||||||
|                 Post by {@html post.account.rich_name} |                 {@html lang.string('post.by', post.account.rich_name)} | ||||||
|             </h1> |             </h1> | ||||||
|         </header> |         </header> | ||||||
|          |          | ||||||
|  |  | ||||||
|  | @ -8,17 +8,20 @@ | ||||||
|     import { error } from '@sveltejs/kit'; |     import { error } from '@sveltejs/kit'; | ||||||
|     import { unread_notif_count, last_read_notif_id } from '$lib/notifications.js'; |     import { unread_notif_count, last_read_notif_id } from '$lib/notifications.js'; | ||||||
|     import { account } from '$lib/stores/account.js'; |     import { account } from '$lib/stores/account.js'; | ||||||
|  |     import Lang from '$lib/lang'; | ||||||
| 
 | 
 | ||||||
|     export let data; |     export let data; | ||||||
| 
 | 
 | ||||||
|  |     const lang = Lang(); | ||||||
|  | 
 | ||||||
|     let auth_code = data.code; |     let auth_code = data.code; | ||||||
| 
 | 
 | ||||||
|     if (!auth_code || !get(server) || !get(app)) { |     if (!auth_code || !get(server) || !get(app)) { | ||||||
|         error(400, { message: "Bad request" }); |         error(400, { message: lang.string('error.bad_request') }); | ||||||
|     } else { |     } else { | ||||||
|         api.getToken(get(server).host, get(app).id, get(app).secret, auth_code).then(token => { |         api.getToken(get(server).host, get(app).id, get(app).secret, auth_code).then(token => { | ||||||
|             if (!token) { |             if (!token) { | ||||||
|                 error(400, { message: "Invalid auth code provided" }); |                 error(400, { message: lang.string('error.invalid_auth_code') }); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             app.update(app => { |             app.update(app => { | ||||||
|  | @ -30,7 +33,7 @@ | ||||||
|                 if (!data) return goto("/"); |                 if (!data) return goto("/"); | ||||||
| 
 | 
 | ||||||
|                 account.set(parseAccount(data)); |                 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 |                 // spin up async task to fetch notifications | ||||||
|                 return api.getNotifications( |                 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 { goto } from '$app/navigation'; | ||||||
|     import { page } from '$app/stores'; |     import { page } from '$app/stores'; | ||||||
|     import Notification from '$lib/ui/Notification.svelte'; |     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("/"); |     if (!$account) goto("/"); | ||||||
| 
 | 
 | ||||||
|  | @ -30,14 +34,12 @@ | ||||||
|     }); |     }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <header> | <PageHeader title={lang.string('navigation.notifications')}/> | ||||||
|     <h1>Notifications</h1> |  | ||||||
| </header> |  | ||||||
| 
 | 
 | ||||||
| <div class="notifications"> | <div class="notifications"> | ||||||
|     {#if $notifications.length === 0} |     {#if $notifications.length === 0} | ||||||
|         <div class="loading throb"> |         <div class="loading throb"> | ||||||
|             <span>fetching notifications...</span> |             <span>{lang.string('notification.fetching')}</span> | ||||||
|         </div> |         </div> | ||||||
|     {:else} |     {:else} | ||||||
|         {#each $notifications as notif} |         {#each $notifications as notif} | ||||||
|  | @ -47,18 +49,6 @@ | ||||||
| </div> | </div> | ||||||
| 
 | 
 | ||||||
| <style> | <style> | ||||||
|     header { |  | ||||||
|         width: 100%; |  | ||||||
|         height: 64px; |  | ||||||
|         margin: 16px 0 8px 0; |  | ||||||
|         display: flex; |  | ||||||
|         flex-direction: row; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     h1 { |  | ||||||
|         font-size: 1.5em; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     .loading { |     .loading { | ||||||
|         width: 100%; |         width: 100%; | ||||||
|         height: 80vh; |         height: 80vh; | ||||||
|  |  | ||||||
|  | @ -19,9 +19,10 @@ const config = { | ||||||
|             name: child_process.execSync('git rev-parse HEAD').toString().trim() |             name: child_process.execSync('git rev-parse HEAD').toString().trim() | ||||||
|         }, |         }, | ||||||
|         alias: { |         alias: { | ||||||
|             '@cf/ui/*': "./src/lib/ui", |             '@cf/ui/*': './src/lib/ui', | ||||||
|             '@cf/icons/*': "./src/img/icons", |             '@cf/icons/*': './src/img/icons', | ||||||
|             '@cf/store/*': "./src/lib/stores" |             '@cf/store/*': './src/lib/stores', | ||||||
|  |             '@cf/lang/*': './src/lang' | ||||||
|         } |         } | ||||||
|     }, |     }, | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -3,11 +3,16 @@ import { defineConfig } from 'vite'; | ||||||
| import { readFileSync } from 'fs'; | import { readFileSync } from 'fs'; | ||||||
| import { fileURLToPath } from 'url'; | import { fileURLToPath } from 'url'; | ||||||
| import svg from '@poppanator/sveltekit-svg' | 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 packageFile = fileURLToPath(new URL('package.json', import.meta.url)); | ||||||
| const packageData = readFileSync(packageFile, 'utf8'); | const packageData = readFileSync(packageFile, 'utf8'); | ||||||
| const packageJSON = JSON.parse(packageData); | const packageJSON = JSON.parse(packageData); | ||||||
| 
 | 
 | ||||||
|  | // get git commit hash
 | ||||||
|  | const commitHash = execSync("git rev-parse HEAD") | ||||||
|  |     .toString().trim().slice(0, 10) | ||||||
| 
 | 
 | ||||||
| export default defineConfig({ | export default defineConfig({ | ||||||
| 	plugins: [ | 	plugins: [ | ||||||
|  | @ -15,7 +20,8 @@ export default defineConfig({ | ||||||
|         svg() |         svg() | ||||||
|     ], |     ], | ||||||
|     define: { |     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