Compare commits
	
		
			428 Commits
		
	
	
		
			v0.4.1
			...
			9b4f001660
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					9b4f001660 | ||
| 
						 | 
					a4e20aa62a | ||
| 
						 | 
					1cc4862d51 | ||
| 
						 | 
					c6b64ced91 | ||
| 
						 | 
					38cbf093e5 | ||
| 
						 | 
					d8ddfa31e3 | ||
| 
						 | 
					c3e4f676fc | ||
| 
						 | 
					e668b828ea | ||
| 
						 | 
					eaeac2cb78 | ||
| 
						 | 
					5d74ec59c1 | ||
| 
						 | 
					645b29e5d1 | ||
| 
						 | 
					aee9677029 | ||
| 
						 | 
					da982dc831 | ||
| 
						 | 
					a64eaa3fc0 | ||
| 
						 | 
					a63651f715 | ||
| 
						 | 
					bb67b708e7 | ||
| 
						 | 
					dc0d37c71e | ||
| 
						 | 
					0287c4d458 | ||
| 
						 | 
					47be1061b7 | ||
| 
						 | 
					1c79de2f37 | ||
| 
						 | 
					9696cc7188 | ||
| 
						 | 
					082dd8c1f2 | ||
| 
						 | 
					43524dcdb1 | ||
| 
						 | 
					45a0540edf | ||
| 
						 | 
					2b784d1edc | ||
| 
						 | 
					8650cf9a63 | ||
| 
						 | 
					76c840dbaa | ||
| 
						 | 
					e78de6f6de | ||
| 
						 | 
					554edf5a27 | ||
| 
						 | 
					6fca398a12 | ||
| 
						 | 
					5bf3fbc10e | ||
| 
						 | 
					d994c38219 | ||
| 
						 | 
					3dccbfc797 | ||
| 
						 | 
					c3d461f102 | ||
| 
						 | 
					c0105889ab | ||
| 
						 | 
					c6006b58d2 | ||
| 
						 | 
					178f009458 | ||
| 
						 | 
					9c24cf4aba | ||
| 
						 | 
					af68498494 | ||
| 
						 | 
					eac22d53d3 | ||
| 
						 | 
					e5ac60c187 | ||
| 
						 | 
					4b42a5fbda | ||
| 
						 | 
					08a833f1cf | ||
| 
						 | 
					c0f0dc5192 | ||
| 
						 | 
					6452d0b357 | ||
| 
						 | 
					9f6b815197 | ||
| 
						 | 
					d8cbc0aaee | ||
| 
						 | 
					b1f70ec36c | ||
| 
						 | 
					311d2516ce | ||
| 
						 | 
					5957873534 | ||
| 
						 | 
					7524f304fe | ||
| 
						 | 
					f1730ede97 | ||
| 
						 | 
					7f9c8868fd | ||
| 
						 | 
					72b484a480 | ||
| 
						 | 
					c47c1dd4f4 | ||
| 
						 | 
					3975c70de7 | ||
| 
						 | 
					fd4e73e76c | ||
| 
						 | 
					301fab5c17 | ||
| 
						 | 
					2c90454244 | ||
| 
						 | 
					2db99edeaf | ||
| 
						 | 
					4fa471263f | ||
| 
						 | 
					435f654cbe | ||
| 
						 | 
					15b03d7561 | ||
| 
						 | 
					219e6a29e4 | ||
| 
						 | 
					a85edb6cee | ||
| 
						 | 
					43081c5179 | ||
| 
						 | 
					d390dce843 | ||
| 
						 | 
					e5939aaa5d | ||
| 
						 | 
					1db0c0f531 | ||
| 
						 | 
					eefd33ac88 | ||
| 
						 | 
					de10436c1a | ||
| 
						 | 
					81f109f830 | ||
| 
						 | 
					858ee28ef2 | ||
| 
						 | 
					b89afe1b0c | ||
| 
						 | 
					b4fedc1c1d | ||
| 
						 | 
					d24e8b4027 | ||
| 
						 | 
					93fbdbe0f3 | ||
| 
						 | 
					068d9b8716 | ||
| 
						 | 
					2295f23725 | ||
| 
						 | 
					fed587b4a4 | ||
| 
						 | 
					8f73f9c365 | ||
| 
						 | 
					363efc2e7f | ||
| 
						 | 
					cf93fed64b | ||
| 
						 | 
					99c689657f | ||
| 
						 | 
					394c98c65a | ||
| 
						 | 
					8f93ac29dd | ||
| 
						 | 
					5ffb7f4a01 | ||
| 
						 | 
					f5f718a84a | ||
| 
						 | 
					4c36a950a7 | ||
| 
						 | 
					a9bc9d7e8d | ||
| 
						 | 
					18fed70ddf | ||
| 
						 | 
					dd9d117ab8 | ||
| 
						 | 
					0e94fe354f | ||
| 
						 | 
					3b99c79495 | ||
| 
						 | 
					20e914c85b | ||
| 
						 | 
					efc4b3f84c | ||
| 
						 | 
					2bc6b52e99 | ||
| 
						 | 
					feb59e560b | ||
| 
						 | 
					17be8f3601 | ||
| 
						 | 
					3b053e8222 | ||
| 
						 | 
					9d5050d3ee | ||
| 
						 | 
					1bd56e1d0e | ||
| 
						 | 
					3f46abf261 | ||
| 
						 | 
					6a248e17be | ||
| 
						 | 
					8273a2a6a0 | ||
| 
						 | 
					78f52c769d | ||
| 
						 | 
					fe22b2f8fb | ||
| 
						 | 
					482421f10e | ||
| 
						 | 
					dcb15aee0e | ||
| 
						 | 
					827f22e2fc | ||
| 
						 | 
					bd36314f00 | ||
| 
						 | 
					4ad7892eab | ||
| 
						 | 
					31b7e62983 | ||
| 
						 | 
					0f5ef2f49c | ||
| 
						 | 
					2ce3fee70b | ||
| 
						 | 
					33e5bee9fb | ||
| 
						 | 
					b32f7dba5d | ||
| 
						 | 
					2661acbadb | ||
| 
						 | 
					d28c079a57 | ||
| 
						 | 
					29a159c094 | ||
| 
						 | 
					68dad51948 | ||
| 
						 | 
					3bf82b5b86 | ||
| 
						 | 
					e52e8c12cf | ||
| 
						 | 
					8f7a7faa91 | ||
| 
						 | 
					ff0edec652 | ||
| 
						 | 
					bbcecf274f | ||
| 
						 | 
					cc41be6856 | ||
| 
						 | 
					761f56b869 | ||
| 
						 | 
					d4e8eaadd7 | ||
| 
						 | 
					9f2bdadde7 | ||
| 
						 | 
					f789d9dfe3 | ||
| 
						 | 
					ce41ee2387 | ||
| 
						 | 
					01c8fad012 | ||
| 
						 | 
					908e91cb91 | ||
| 
						 | 
					f1c5cd9f6b | ||
| 
						 | 
					6ea3058e66 | ||
| 
						 | 
					a4e741cc0a | ||
| 
						 | 
					0f2172d61f | ||
| 
						 | 
					2baa69ca17 | ||
| 
						 | 
					3bbfa9186e | ||
| 
						 | 
					c1428f5c2b | ||
| 
						 | 
					1be11708c4 | ||
| 
						 | 
					8c04b318fd | ||
| 
						 | 
					ff2c0057e8 | ||
| 
						 | 
					c830721e02 | ||
| 
						 | 
					625e1a51f6 | ||
| 
						 | 
					6af1e8f326 | ||
| 
						 | 
					82f0e14abf | ||
| 
						 | 
					9e759a75de | ||
| 
						 | 
					33388cf209 | ||
| 
						 | 
					2490c3a7e7 | ||
| 
						 | 
					7f86c352e3 | ||
| 
						 | 
					2a3b08487e | ||
| 
						 | 
					29ba229bc2 | ||
| 
						 | 
					50725edd02 | ||
| 
						 | 
					40d1d8a191 | ||
| 
						 | 
					3417564278 | ||
| 
						 | 
					9a49dedaca | ||
| 
						 | 
					d9076bf42a | ||
| 
						 | 
					b9bbf7792f | ||
| 
						 | 
					5cc6678ceb | ||
| 
						 | 
					b47e5755f6 | ||
| 
						 | 
					af5c768dc7 | ||
| 
						 | 
					3b573cccae | ||
| 
						 | 
					0c6f6d6904 | ||
| 
						 | 
					6e2fe27f31 | ||
| 
						 | 
					b6cdd3741a | ||
| 
						 | 
					00f95b6daa | ||
| 
						 | 
					2d05bbf86b | ||
| 
						 | 
					2c87a6c8c2 | ||
| 
						 | 
					254509db5e | ||
| 
						 | 
					4e4c029cb8 | ||
| 
						 | 
					6dc60679bb | ||
| 
						 | 
					6e5d5d9de0 | ||
| 
						 | 
					6289c033c8 | ||
| 
						 | 
					b200049a81 | ||
| 
						 | 
					5646f79f99 | ||
| 
						 | 
					5083968b80 | ||
| 
						 | 
					2eb9b8fe96 | ||
| 
						 | 
					8dc60b41ff | ||
| 
						 | 
					b4be479d02 | ||
| 
						 | 
					f56a93a1b2 | ||
| 
						 | 
					ff8b9fca67 | ||
| 
						 | 
					0579f1852b | ||
| 
						 | 
					52d4cc0d03 | ||
| 
						 | 
					2c68016ca6 | ||
| 
						 | 
					7914194856 | ||
| 
						 | 
					2dac7f1362 | ||
| 
						 | 
					a17e5fd614 | ||
| 
						 | 
					21994fb6a2 | ||
| 
						 | 
					a5eaaa422a | ||
| 
						 | 
					ff2ef74135 | ||
| 
						 | 
					70705c1850 | ||
| 
						 | 
					fd9c151e01 | ||
| 
						 | 
					4f0573963f | ||
| 
						 | 
					6bb6bce8a4 | ||
| 
						 | 
					448557bece | ||
| 
						 | 
					bdbd4a122c | ||
| 
						 | 
					cb9d0ec680 | ||
| 
						 | 
					fb60ef66f5 | ||
| 
						 | 
					c1ae43075f | ||
| 
						 | 
					377f69ae8d | ||
| 
						 | 
					cb131cd0a0 | ||
| 
						 | 
					fcc83c5ea8 | ||
| 
						 | 
					96d4717d13 | ||
| 
						 | 
					4d73bf9760 | ||
| 
						 | 
					725a94bc95 | ||
| 
						 | 
					0a366b447a | ||
| 
						 | 
					4a27a7bc03 | ||
| 
						 | 
					3ca5803bda | ||
| 
						 | 
					239041294c | ||
| 
						 | 
					31fdd8f214 | ||
| 
						 | 
					c3319c09eb | ||
| 
						 | 
					d460e94d52 | ||
| 
						 | 
					4b5c732380 | ||
| 
						 | 
					f42665ca40 | ||
| 
						 | 
					bed52cef17 | ||
| 
						 | 
					9d1c93155c | ||
| 
						 | 
					794cc7c474 | ||
| 
						 | 
					d7d584e497 | ||
| 
						 | 
					f5320df86e | ||
| 
						 | 
					056fd4ba93 | ||
| 
						 | 
					5b6e70eb3a | ||
| 
						 | 
					f437a8e7e2 | ||
| 
						 | 
					cdae798fcf | ||
| 
						 | 
					bcc827a81b | ||
| 
						 | 
					84274b9c55 | ||
| 
						 | 
					20c6f8249e | ||
| 
						 | 
					8f0ea2a592 | ||
| 
						 | 
					a29e4a930a | ||
| 
						 | 
					4549c96ae3 | ||
| 
						 | 
					bc64094c04 | ||
| 
						 | 
					fa58827ad5 | ||
| 
						 | 
					8f27be0e3d | ||
| 
						 | 
					df43df1178 | ||
| 
						 | 
					c2beb4a227 | ||
| 
						 | 
					9277c27a50 | ||
| 
						 | 
					171ecd6884 | ||
| 
						 | 
					dca29f7e5a | ||
| 
						 | 
					318acc20bd | ||
| 
						 | 
					f433493d57 | ||
| 
						 | 
					19970fc132 | ||
| 
						 | 
					24394ca3c5 | ||
| 
						 | 
					10ff0b464a | ||
| 
						 | 
					9263d17609 | ||
| 
						 | 
					c1b75a13fd | ||
| 
						 | 
					a8ed60d48f | ||
| 
						 | 
					dc82a438d4 | ||
| 
						 | 
					cc54bdcbe7 | ||
| 
						 | 
					ae4bbc8baa | ||
| 
						 | 
					ad98499da0 | ||
| 
						 | 
					db60f355b2 | ||
| 
						 | 
					eb91d8b298 | ||
| 
						 | 
					b8312be4b7 | ||
| 
						 | 
					326a8e3404 | ||
| 
						 | 
					f017e13ac1 | ||
| 
						 | 
					67a5fe353e | ||
| 
						 | 
					51d49d7ff3 | ||
| 
						 | 
					d42b820b36 | ||
| 
						 | 
					07d32776d3 | ||
| 
						 | 
					ef027e81b5 | ||
| 
						 | 
					a75e4b495d | ||
| 
						 | 
					fba5e212e8 | ||
| 
						 | 
					62f44fb052 | ||
| 
						 | 
					6b9254047c | ||
| 
						 | 
					decfea5dc9 | ||
| 
						 | 
					eacded6848 | ||
| 
						 | 
					279ca72c64 | ||
| 
						 | 
					b8fc9383ca | ||
| 
						 | 
					bec58ac59f | ||
| 
						 | 
					83d7126820 | ||
| 
						 | 
					f0e9c6d794 | ||
| 
						 | 
					0e61051fc6 | ||
| 
						 | 
					480ba77ebe | ||
| 
						 | 
					16f27c13bb | ||
| 
						 | 
					afe5c50d66 | ||
| 
						 | 
					72ea859ebb | ||
| 
						 | 
					8edf3834c4 | ||
| 
						 | 
					e595014fcd | ||
| 
						 | 
					8bebf7e569 | ||
| 
						 | 
					c825ec06e2 | ||
| 
						 | 
					8c75f273fb | ||
| 
						 | 
					0ba776c129 | ||
| 
						 | 
					2bbbd03554 | ||
| 
						 | 
					0a5d0487b1 | ||
| 
						 | 
					583cd2dd3b | ||
| 
						 | 
					e1f7fc1ecb | ||
| 
						 | 
					961a55cbe5 | ||
| 
						 | 
					cdf9bad903 | ||
| 
						 | 
					6769fa2f83 | ||
| 
						 | 
					3b7ea88b73 | ||
| 
						 | 
					59310c095d | ||
| 
						 | 
					c47f0c12fe | ||
| 
						 | 
					ca71a40485 | ||
| 
						 | 
					d4e8f376c1 | ||
| 
						 | 
					14c6ea1e6b | ||
| 
						 | 
					d6e4d8fbd6 | ||
| 
						 | 
					460bda62d5 | ||
| 
						 | 
					d2702ab673 | ||
| 
						 | 
					e2581f42f5 | ||
| 
						 | 
					f0fcfc159f | ||
| 
						 | 
					538c5b60c9 | ||
| 
						 | 
					2fabb7bbb2 | ||
| 
						 | 
					e7c34a9c94 | ||
| 
						 | 
					618f9fce5a | ||
| 
						 | 
					95dbc9f678 | ||
| 
						 | 
					aa87bc5c51 | ||
| 
						 | 
					815de531ed | ||
| 
						 | 
					cf2b026dc4 | ||
| 
						 | 
					9ce46aefba | ||
| 
						 | 
					98b2db7818 | ||
| 
						 | 
					0229851bf9 | ||
| 
						 | 
					9e15114fe8 | ||
| 
						 | 
					7f66a76bb0 | ||
| 
						 | 
					e9cc8392bb | ||
| 
						 | 
					d0b89ce74f | ||
| 
						 | 
					f537c81db7 | ||
| 
						 | 
					03d3edfff6 | ||
| 
						 | 
					447b4c5e5c | ||
| 
						 | 
					cb143209ae | ||
| 
						 | 
					9c24fe73b5 | ||
| 
						 | 
					19ae85424b | ||
| 
						 | 
					22fad99552 | ||
| 
						 | 
					8144bbef74 | ||
| 
						 | 
					aad6da0ae8 | ||
| 
						 | 
					c5f8162a22 | ||
| 
						 | 
					f0f30224b5 | ||
| 
						 | 
					d0d888e356 | ||
| 
						 | 
					2c64122224 | ||
| 
						 | 
					3b2eee96a9 | ||
| 
						 | 
					465aacbf9b | ||
| 
						 | 
					d1a2a66170 | ||
| 
						 | 
					4c05fd72bb | ||
| 
						 | 
					f04fe760e3 | ||
| 
						 | 
					834d19bcc6 | ||
| 
						 | 
					6808c4642c | ||
| 
						 | 
					d0ce307f94 | ||
| 
						 | 
					f3740e9ded | ||
| 
						 | 
					b485bc9445 | ||
| 
						 | 
					2d14c1bb26 | ||
| 
						 | 
					1a442d6e69 | ||
| 
						 | 
					2386543e5c | ||
| 
						 | 
					58e220e82d | ||
| 
						 | 
					24bea6e4d2 | ||
| 
						 | 
					43497ad8d1 | ||
| 
						 | 
					f22b61fe4c | ||
| 
						 | 
					5b08f4cd19 | ||
| 
						 | 
					1589f8d24e | ||
| 
						 | 
					7d1db72cf5 | ||
| 
						 | 
					53a8f66414 | ||
| 
						 | 
					36cb6cc589 | ||
| 
						 | 
					f3a4aece46 | ||
| 
						 | 
					580a6a869a | ||
| 
						 | 
					008eaac493 | ||
| 
						 | 
					b450623bb4 | ||
| 
						 | 
					8ac2ecb673 | ||
| 
						 | 
					0a10a56ae3 | ||
| 
						 | 
					9378ba9208 | ||
| 
						 | 
					0c586e324b | ||
| 
						 | 
					91c4a64284 | ||
| 
						 | 
					c599e98d9d | ||
| 
						 | 
					d2cd6706c9 | ||
| 
						 | 
					e8ed10dde8 | ||
| 
						 | 
					5fe0b79802 | ||
| 
						 | 
					34a6722a68 | ||
| 
						 | 
					5b0d769c63 | ||
| 
						 | 
					718401a28b | ||
| 
						 | 
					3112cd57f6 | ||
| 
						 | 
					410fc777a7 | ||
| 
						 | 
					8eed99e732 | ||
| 
						 | 
					663b1d4171 | ||
| 
						 | 
					c3067ca12d | ||
| 
						 | 
					4561ca3760 | ||
| 
						 | 
					698cce58ce | ||
| 
						 | 
					339b79f786 | ||
| 
						 | 
					4f98f778f0 | ||
| 
						 | 
					8479b33a47 | ||
| 
						 | 
					78844d7bd5 | ||
| 
						 | 
					64e4a271e1 | ||
| 
						 | 
					5fb8c3575b | ||
| 
						 | 
					a6b8bcecae | ||
| 
						 | 
					bc9c820820 | ||
| 
						 | 
					ee9207a7f4 | ||
| 
						 | 
					a34e215202 | ||
| 
						 | 
					b4e53dbb8e | ||
| 
						 | 
					b5e8d82bfa | ||
| 
						 | 
					5d9000bb33 | ||
| 
						 | 
					ccb065ef0f | ||
| 
						 | 
					883fad806b | ||
| 
						 | 
					feacd1b816 | ||
| 
						 | 
					094e7a0d1c | ||
| 
						 | 
					72636c5059 | ||
| 
						 | 
					291cfc80c6 | ||
| 
						 | 
					ae1dfafc9d | ||
| 
						 | 
					6caa583c35 | ||
| 
						 | 
					2057167576 | ||
| 
						 | 
					1c9e67fc32 | ||
| 
						 | 
					d3af9688c6 | ||
| 
						 | 
					7d0cbb9844 | ||
| 
						 | 
					88173891ba | ||
| 
						 | 
					2b4b8f9551 | ||
| 
						 | 
					63a4328d4a | ||
| 
						 | 
					413f5dc7b4 | ||
| 
						 | 
					ebccdf9169 | ||
| 
						 | 
					47139a550b | ||
| 
						 | 
					fa5446c446 | ||
| 
						 | 
					8772e582b0 | ||
| 
						 | 
					45922ed3a3 | ||
| 
						 | 
					4c747e8908 | ||
| 
						 | 
					e573997aa9 | ||
| 
						 | 
					c57b69991c | ||
| 
						 | 
					eee983a56a | ||
| 
						 | 
					22f823c535 | ||
| 
						 | 
					ed59cd7aa4 | ||
| 
						 | 
					b28977ffe2 | ||
| 
						 | 
					a47bb682a5 | ||
| 
						 | 
					a17eca0a09 | ||
| 
						 | 
					ea9250543e | ||
| 
						 | 
					317c932c2a | ||
| 
						 | 
					5b1703db68 | ||
| 
						 | 
					60ba7c93fb | ||
| 
						 | 
					22227130dd | ||
| 
						 | 
					5daf66f5d0 | ||
| 
						 | 
					aee1962607 | ||
| 
						 | 
					0d42762b36 | ||
| 
						 | 
					b97b12b449 | ||
| 
						 | 
					bdf651df82 | ||
| 
						 | 
					267ef14789 | 
							
								
								
									
										1
									
								
								.bun-version
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
			
		||||
1.2.2
 | 
			
		||||
@@ -1,16 +1,25 @@
 | 
			
		||||
node_modules
 | 
			
		||||
Dockerfile*
 | 
			
		||||
docker-compose*
 | 
			
		||||
.dockerignore
 | 
			
		||||
.git
 | 
			
		||||
.gitignore
 | 
			
		||||
README.md
 | 
			
		||||
LICENSE
 | 
			
		||||
.vscode
 | 
			
		||||
Makefile
 | 
			
		||||
helm-charts
 | 
			
		||||
.env
 | 
			
		||||
.editorconfig
 | 
			
		||||
.idea
 | 
			
		||||
coverage*
 | 
			
		||||
data
 | 
			
		||||
.dockerignore
 | 
			
		||||
.editorconfig
 | 
			
		||||
.env
 | 
			
		||||
.git
 | 
			
		||||
.gitignore
 | 
			
		||||
.github
 | 
			
		||||
.idea
 | 
			
		||||
.vscode
 | 
			
		||||
biome.json
 | 
			
		||||
CHANGELOG.md
 | 
			
		||||
compose.yaml
 | 
			
		||||
coverage*
 | 
			
		||||
data
 | 
			
		||||
docker-compose*
 | 
			
		||||
Dockerfile*
 | 
			
		||||
eslint.config.js
 | 
			
		||||
helm-charts
 | 
			
		||||
images
 | 
			
		||||
LICENSE
 | 
			
		||||
Makefile
 | 
			
		||||
node_modules
 | 
			
		||||
prettier.config.js
 | 
			
		||||
README.md
 | 
			
		||||
renovate.json
 | 
			
		||||
SECURITY.md
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,15 @@
 | 
			
		||||
# These are supported funding model platforms
 | 
			
		||||
 | 
			
		||||
github: [C4illin] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
 | 
			
		||||
patreon: # Replace with a single Patreon username
 | 
			
		||||
open_collective: # Replace with a single Open Collective username
 | 
			
		||||
ko_fi: # Replace with a single Ko-fi username
 | 
			
		||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
 | 
			
		||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
 | 
			
		||||
liberapay: # Replace with a single Liberapay username
 | 
			
		||||
issuehunt: # Replace with a single IssueHunt username
 | 
			
		||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
 | 
			
		||||
polar: # Replace with a single Polar username
 | 
			
		||||
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
 | 
			
		||||
thanks_dev: # Replace with a single thanks.dev username
 | 
			
		||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
 | 
			
		||||
							
								
								
									
										22
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,22 @@
 | 
			
		||||
---
 | 
			
		||||
name: Bug report
 | 
			
		||||
about: Create a report to help us improve
 | 
			
		||||
title: ""
 | 
			
		||||
labels: bug
 | 
			
		||||
assignees: ""
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
**Describe the bug**
 | 
			
		||||
A clear and concise description of what the bug is.
 | 
			
		||||
 | 
			
		||||
**To Reproduce**
 | 
			
		||||
Steps to reproduce the behavior:
 | 
			
		||||
 | 
			
		||||
1. Go to '...'
 | 
			
		||||
2. Click on '....'
 | 
			
		||||
3. Scroll down to '....'
 | 
			
		||||
4. See error
 | 
			
		||||
 | 
			
		||||
**Checklist:**
 | 
			
		||||
 | 
			
		||||
- [ ] I am accessing ConvertX over HTTPS or have `HTTP_ALLOWED=true`
 | 
			
		||||
							
								
								
									
										26
									
								
								.github/ISSUE_TEMPLATE/converter_request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,26 @@
 | 
			
		||||
---
 | 
			
		||||
name: Converter request
 | 
			
		||||
about: Suggest a converter for this project
 | 
			
		||||
title: "[Converter Request]"
 | 
			
		||||
labels: "converter request"
 | 
			
		||||
assignees: ""
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
**What file formats are missing?**
 | 
			
		||||
 | 
			
		||||
<!-- Provide an example of what you would like to convert -->
 | 
			
		||||
 | 
			
		||||
**What converter should be added**
 | 
			
		||||
 | 
			
		||||
<!-- It has to be free and preferably open source -->
 | 
			
		||||
 | 
			
		||||
**Are you willing to add it?**
 | 
			
		||||
 | 
			
		||||
<!-- Adding a converter is very easy just copy one of the existing and modify it -->
 | 
			
		||||
 | 
			
		||||
- [ ] Yes
 | 
			
		||||
- [ ] No
 | 
			
		||||
 | 
			
		||||
**Additional context**
 | 
			
		||||
 | 
			
		||||
<!-- Add any other context or screenshots about the feature request here. -->
 | 
			
		||||
							
								
								
									
										13
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,13 @@
 | 
			
		||||
---
 | 
			
		||||
name: Feature request
 | 
			
		||||
about: Suggest an idea for this project
 | 
			
		||||
title: "[Feature Request]"
 | 
			
		||||
labels: enhancement
 | 
			
		||||
assignees: ""
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
**Describe the solution you'd like**
 | 
			
		||||
A clear and concise description of what you want to happen.
 | 
			
		||||
 | 
			
		||||
**Additional context**
 | 
			
		||||
Add any other context or screenshots about the feature request here.
 | 
			
		||||
							
								
								
									
										31
									
								
								.github/workflows/check-lint.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,31 @@
 | 
			
		||||
name: Check Lint
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches: ["main"]
 | 
			
		||||
  pull_request:
 | 
			
		||||
    branches: ["main"]
 | 
			
		||||
  workflow_dispatch:
 | 
			
		||||
 | 
			
		||||
concurrency:
 | 
			
		||||
  group: ${{ github.workflow }}-${{ github.ref }}
 | 
			
		||||
  cancel-in-progress: true
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  lint:
 | 
			
		||||
    name: Run linting checks
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout code
 | 
			
		||||
        uses: actions/checkout@v5
 | 
			
		||||
 | 
			
		||||
      - name: Set up Bun
 | 
			
		||||
        uses: oven-sh/setup-bun@v2
 | 
			
		||||
        with:
 | 
			
		||||
          bun-version: 1.2.2
 | 
			
		||||
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        run: bun install
 | 
			
		||||
 | 
			
		||||
      - name: Run lint
 | 
			
		||||
        run: bun run lint
 | 
			
		||||
							
								
								
									
										178
									
								
								.github/workflows/docker-publish.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1,69 +1,173 @@
 | 
			
		||||
name: Docker
 | 
			
		||||
 | 
			
		||||
# This workflow uses actions that are not certified by GitHub.
 | 
			
		||||
# They are provided by a third-party and are governed by
 | 
			
		||||
# separate terms of service, privacy policy, and support
 | 
			
		||||
# documentation.
 | 
			
		||||
# thanks to https://github.com/sredevopsorg/multi-arch-docker-github-workflow
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches: [ "main" ]
 | 
			
		||||
    # Publish semver tags as releases.
 | 
			
		||||
    tags: [ 'v*.*.*' ]
 | 
			
		||||
    branches: ["main"]
 | 
			
		||||
    tags: ["v*.*.*"]
 | 
			
		||||
  pull_request:
 | 
			
		||||
    branches: [ "main" ]
 | 
			
		||||
    branches: ["main"]
 | 
			
		||||
  workflow_dispatch:
 | 
			
		||||
 | 
			
		||||
env:
 | 
			
		||||
  # Use docker.io for Docker Hub if empty
 | 
			
		||||
  REGISTRY: ghcr.io
 | 
			
		||||
  # github.repository as <account>/<repo>
 | 
			
		||||
  IMAGE_NAME: ${{ github.repository }}
 | 
			
		||||
  DOCKERHUB_USERNAME: c4illin
 | 
			
		||||
 | 
			
		||||
concurrency:
 | 
			
		||||
  group: ${{ github.workflow }}-${{ github.ref }}
 | 
			
		||||
  cancel-in-progress: true
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  # The build job builds the Docker image for each platform specified in the matrix.
 | 
			
		||||
  build:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      matrix:
 | 
			
		||||
        platform:
 | 
			
		||||
          - linux/amd64
 | 
			
		||||
          - linux/arm64
 | 
			
		||||
 | 
			
		||||
    permissions:
 | 
			
		||||
      contents: read
 | 
			
		||||
      contents: write
 | 
			
		||||
      packages: write
 | 
			
		||||
      attestations: write
 | 
			
		||||
      id-token: write
 | 
			
		||||
 | 
			
		||||
    runs-on: ${{ matrix.platform == 'linux/amd64' && 'ubuntu-24.04' || matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' }}
 | 
			
		||||
 | 
			
		||||
    name: Build Docker image for ${{ matrix.platform }}
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Prepare environment for current platform
 | 
			
		||||
        # This step sets up the environment for the current platform being built.
 | 
			
		||||
        # It replaces the '/' character in the platform name with '-' and sets it as an environment variable.
 | 
			
		||||
        # This is useful for naming artifacts and other resources that cannot contain '/'.
 | 
			
		||||
        # The environment variable PLATFORMS_PAIR will be used later in the workflow.
 | 
			
		||||
        id: prepare
 | 
			
		||||
        run: |
 | 
			
		||||
          platform=${{ matrix.platform }}
 | 
			
		||||
          echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
 | 
			
		||||
 | 
			
		||||
      - name: Checkout repository
 | 
			
		||||
        uses: actions/checkout@v4
 | 
			
		||||
        uses: actions/checkout@v5
 | 
			
		||||
 | 
			
		||||
      # Workaround: https://github.com/docker/build-push-action/issues/461
 | 
			
		||||
      - name: Setup Docker buildx
 | 
			
		||||
      - name: downcase REPO
 | 
			
		||||
        run: |
 | 
			
		||||
          echo "REPO=${GITHUB_REPOSITORY@L}" >> "${GITHUB_ENV}"
 | 
			
		||||
 | 
			
		||||
      - name: Docker meta default
 | 
			
		||||
        id: meta
 | 
			
		||||
        uses: docker/metadata-action@v5
 | 
			
		||||
        with:
 | 
			
		||||
          images: ghcr.io/${{ env.REPO }}
 | 
			
		||||
 | 
			
		||||
      - name: Set up Docker Buildx
 | 
			
		||||
        uses: docker/setup-buildx-action@v3
 | 
			
		||||
        with:
 | 
			
		||||
          platforms: ${{ matrix.platform }}
 | 
			
		||||
 | 
			
		||||
      # Login against a Docker registry except on PR
 | 
			
		||||
      # https://github.com/docker/login-action
 | 
			
		||||
      - name: Log into registry ${{ env.REGISTRY }}
 | 
			
		||||
        if: github.event_name != 'pull_request'
 | 
			
		||||
      - name: Login to GitHub Container Registry
 | 
			
		||||
        # here we only login to ghcr.io since the this only pushes internal images
 | 
			
		||||
        uses: docker/login-action@v3
 | 
			
		||||
        with:
 | 
			
		||||
          registry: ${{ env.REGISTRY }}
 | 
			
		||||
          registry: ghcr.io
 | 
			
		||||
          username: ${{ github.actor }}
 | 
			
		||||
          password: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
 | 
			
		||||
      # Extract metadata (tags, labels) for Docker
 | 
			
		||||
      # https://github.com/docker/metadata-action
 | 
			
		||||
      - name: Build and push by digest
 | 
			
		||||
        id: build
 | 
			
		||||
        uses: docker/build-push-action@v6
 | 
			
		||||
        env:
 | 
			
		||||
          DOCKER_BUILDKIT: 1
 | 
			
		||||
        with:
 | 
			
		||||
          context: .
 | 
			
		||||
          platforms: ${{ matrix.platform }}
 | 
			
		||||
          labels: ${{ steps.meta.outputs.labels }}
 | 
			
		||||
          annotations: ${{ steps.meta.outputs.annotations }}
 | 
			
		||||
          outputs: type=image,name=ghcr.io/${{ env.REPO }},push-by-digest=true,name-canonical=true,oci-mediatypes=true
 | 
			
		||||
          push: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
 | 
			
		||||
          cache-from: type=gha,scope=${{ matrix.platform }}
 | 
			
		||||
          cache-to: type=gha,mode=max,scope=${{ matrix.platform }}
 | 
			
		||||
 | 
			
		||||
      - name: Export digest
 | 
			
		||||
        run: |
 | 
			
		||||
          mkdir -p /tmp/digests
 | 
			
		||||
          digest="${{ steps.build.outputs.digest }}"
 | 
			
		||||
          touch "/tmp/digests/${digest#sha256:}"
 | 
			
		||||
 | 
			
		||||
      - name: Upload digest
 | 
			
		||||
        uses: actions/upload-artifact@v4
 | 
			
		||||
        with:
 | 
			
		||||
          name: digests-${{ env.PLATFORM_PAIR }}
 | 
			
		||||
          path: /tmp/digests/*
 | 
			
		||||
          if-no-files-found: error
 | 
			
		||||
          retention-days: 1
 | 
			
		||||
 | 
			
		||||
  merge:
 | 
			
		||||
    if: github.event.pull_request.head.repo.full_name == github.repository
 | 
			
		||||
    name: Merge Docker manifests
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
 | 
			
		||||
    permissions:
 | 
			
		||||
      contents: write
 | 
			
		||||
      packages: write
 | 
			
		||||
      attestations: write
 | 
			
		||||
      id-token: write
 | 
			
		||||
 | 
			
		||||
    needs:
 | 
			
		||||
      - build
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Download digests
 | 
			
		||||
        uses: actions/download-artifact@v5
 | 
			
		||||
        with:
 | 
			
		||||
          path: /tmp/digests
 | 
			
		||||
          pattern: digests-*
 | 
			
		||||
          merge-multiple: true
 | 
			
		||||
 | 
			
		||||
      - name: downcase REPO
 | 
			
		||||
        run: |
 | 
			
		||||
          echo "REPO=${GITHUB_REPOSITORY@L}" >> "${GITHUB_ENV}"
 | 
			
		||||
 | 
			
		||||
      - name: Extract Docker metadata
 | 
			
		||||
        id: meta
 | 
			
		||||
        uses: docker/metadata-action@v5
 | 
			
		||||
        with:
 | 
			
		||||
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
 | 
			
		||||
          images: |
 | 
			
		||||
            ghcr.io/${{ env.REPO }}
 | 
			
		||||
            ${{ env.IMAGE_NAME }}
 | 
			
		||||
 | 
			
		||||
      # Build and push Docker image with Buildx (don't push on PR)
 | 
			
		||||
      # https://github.com/docker/build-push-action
 | 
			
		||||
      - name: Build and push Docker image
 | 
			
		||||
        id: build-and-push
 | 
			
		||||
        uses: docker/build-push-action@v6
 | 
			
		||||
      - name: Set up Docker Buildx
 | 
			
		||||
        uses: docker/setup-buildx-action@v3
 | 
			
		||||
 | 
			
		||||
      - name: Login to GitHub Container Registry
 | 
			
		||||
        uses: docker/login-action@v3
 | 
			
		||||
        with:
 | 
			
		||||
          context: .
 | 
			
		||||
          platforms: linux/amd64,linux/arm64
 | 
			
		||||
          push: ${{ github.event_name != 'pull_request' }}
 | 
			
		||||
          tags: ${{ steps.meta.outputs.tags }}
 | 
			
		||||
          labels: ${{ steps.meta.outputs.labels }}
 | 
			
		||||
          cache-from: type=gha
 | 
			
		||||
          cache-to: type=gha,mode=max
 | 
			
		||||
          registry: ghcr.io
 | 
			
		||||
          username: ${{ github.actor }}
 | 
			
		||||
          password: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
 | 
			
		||||
      - name: Login to Docker Hub
 | 
			
		||||
        uses: docker/login-action@v3
 | 
			
		||||
        with:
 | 
			
		||||
          username: ${{ env.DOCKERHUB_USERNAME }}
 | 
			
		||||
          password: ${{ secrets.DOCKERHUB_TOKEN }}
 | 
			
		||||
 | 
			
		||||
      - name: Get execution timestamp with RFC3339 format
 | 
			
		||||
        id: timestamp
 | 
			
		||||
        run: |
 | 
			
		||||
          echo "timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> $GITHUB_OUTPUT
 | 
			
		||||
 | 
			
		||||
      - name: Create manifest list and push
 | 
			
		||||
        working-directory: /tmp/digests
 | 
			
		||||
        run: |
 | 
			
		||||
          docker buildx imagetools create \
 | 
			
		||||
            $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
 | 
			
		||||
            --annotation='index:org.opencontainers.image.description=${{ github.event.repository.description }}' \
 | 
			
		||||
            --annotation='index:org.opencontainers.image.created=${{ steps.timestamp.outputs.timestamp }}' \
 | 
			
		||||
            --annotation='index:org.opencontainers.image.url=${{ github.event.repository.url }}' \
 | 
			
		||||
            --annotation='index:org.opencontainers.image.source=${{ github.event.repository.url }}' \
 | 
			
		||||
            $(printf 'ghcr.io/${{ env.REPO }}@sha256:%s ' *)
 | 
			
		||||
 | 
			
		||||
      - name: Inspect image
 | 
			
		||||
        run: |
 | 
			
		||||
          docker buildx imagetools inspect 'ghcr.io/${{ env.REPO }}:${{ steps.meta.outputs.version }}'
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										27
									
								
								.github/workflows/dockerhub-description.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,27 @@
 | 
			
		||||
name: Update Docker Hub Description
 | 
			
		||||
 | 
			
		||||
env:
 | 
			
		||||
  IMAGE_NAME: ${{ github.repository }}
 | 
			
		||||
  DOCKERHUB_USERNAME: c4illin
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches:
 | 
			
		||||
      - main
 | 
			
		||||
    paths:
 | 
			
		||||
      - README.md
 | 
			
		||||
      - .github/workflows/dockerhub-description.yml
 | 
			
		||||
jobs:
 | 
			
		||||
  dockerHubDescription:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5
 | 
			
		||||
 | 
			
		||||
      - name: Docker Hub Description
 | 
			
		||||
        uses: peter-evans/dockerhub-description@v4
 | 
			
		||||
        with:
 | 
			
		||||
          username: ${{ env.DOCKERHUB_USERNAME }}
 | 
			
		||||
          password: ${{ secrets.DOCKERHUB_TOKEN }}
 | 
			
		||||
          repository: ${{ env.IMAGE_NAME }}
 | 
			
		||||
          short-description: ${{ github.event.repository.description }}
 | 
			
		||||
          enable-url-completion: true
 | 
			
		||||
							
								
								
									
										10
									
								
								.github/workflows/remove-docker-tag.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -14,8 +14,8 @@ jobs:
 | 
			
		||||
      packages: write
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
    - name: Remove Docker Tag
 | 
			
		||||
      uses: ArchieAtkinson/remove-dockertag-action@v0.0
 | 
			
		||||
      with:
 | 
			
		||||
        tag_name: master
 | 
			
		||||
        github_token: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
      - name: Remove Docker Tag
 | 
			
		||||
        uses: ArchieAtkinson/remove-dockertag-action@v0.0
 | 
			
		||||
        with:
 | 
			
		||||
          tag_name: master
 | 
			
		||||
          github_token: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										31
									
								
								.github/workflows/run-bun-test.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,31 @@
 | 
			
		||||
name: Check Tests
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches: ["main"]
 | 
			
		||||
  pull_request:
 | 
			
		||||
    branches: ["main"]
 | 
			
		||||
  workflow_dispatch:
 | 
			
		||||
 | 
			
		||||
concurrency:
 | 
			
		||||
  group: ${{ github.workflow }}-${{ github.ref }}
 | 
			
		||||
  cancel-in-progress: true
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  test:
 | 
			
		||||
    name: Run tests
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout code
 | 
			
		||||
        uses: actions/checkout@v5
 | 
			
		||||
 | 
			
		||||
      - name: Set up Bun
 | 
			
		||||
        uses: oven-sh/setup-bun@v2
 | 
			
		||||
        with:
 | 
			
		||||
          bun-version: 1.2.2
 | 
			
		||||
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        run: bun install
 | 
			
		||||
 | 
			
		||||
      - name: Run tests
 | 
			
		||||
        run: bun test
 | 
			
		||||
							
								
								
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -46,5 +46,7 @@ package-lock.json
 | 
			
		||||
/output
 | 
			
		||||
/db
 | 
			
		||||
/data
 | 
			
		||||
/dist
 | 
			
		||||
/Bruno
 | 
			
		||||
/tsconfig.tsbuildinfo
 | 
			
		||||
/tsconfig.tsbuildinfo
 | 
			
		||||
/public/generated.css
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1,4 +1,4 @@
 | 
			
		||||
{
 | 
			
		||||
  "typescript.tsdk": "node_modules/typescript/lib",
 | 
			
		||||
  "typescript.enablePromptUseWorkspaceTsdk": true
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										224
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						@@ -1,89 +1,249 @@
 | 
			
		||||
# Changelog
 | 
			
		||||
 | 
			
		||||
## [0.15.0](https://github.com/C4illin/ConvertX/compare/v0.14.1...v0.15.0) (2025-09-09)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Features
 | 
			
		||||
 | 
			
		||||
* vtracer implemented and added docker file binaries install ([76c840d](https://github.com/C4illin/ConvertX/commit/76c840dbaa4a26d0623422b61581bb761ad6a6bc))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* add language env ([f789d9d](https://github.com/C4illin/ConvertX/commit/f789d9dfe381780dcc715b70bcf304d570a73e3f))
 | 
			
		||||
* add lmodern ([761f56b](https://github.com/C4illin/ConvertX/commit/761f56b869d3a4faa7550d90b3da2d853baf8a1d)), closes [#320](https://github.com/C4illin/ConvertX/issues/320)
 | 
			
		||||
* move color variables to seperate directory ([3bf82b5](https://github.com/C4illin/ConvertX/commit/3bf82b5b86177f95531293cab1dfee1e12c898a1)), closes [#53](https://github.com/C4illin/ConvertX/issues/53)
 | 
			
		||||
* run qtwebengine without sandbox ([9f2bdad](https://github.com/C4illin/ConvertX/commit/9f2bdadde779d88973296e81af103ed0016f5411))
 | 
			
		||||
* update favicon ([827f22e](https://github.com/C4illin/ConvertX/commit/827f22e2fc33bf32a02befb3c5bd519511826b38)), closes [#158](https://github.com/C4illin/ConvertX/issues/158)
 | 
			
		||||
 | 
			
		||||
## [0.14.1](https://github.com/C4illin/ConvertX/compare/v0.14.0...v0.14.1) (2025-06-04)
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
- change to baseline build ([6ea3058](https://github.com/C4illin/ConvertX/commit/6ea3058e66262f7a14633bddcecd5573948f524a)), closes [#311](https://github.com/C4illin/ConvertX/issues/311)
 | 
			
		||||
 | 
			
		||||
## [0.14.0](https://github.com/C4illin/ConvertX/compare/v0.13.0...v0.14.0) (2025-06-03)
 | 
			
		||||
 | 
			
		||||
### Features
 | 
			
		||||
 | 
			
		||||
- add dvisvgm ([625e1a5](https://github.com/C4illin/ConvertX/commit/625e1a51f620fe9da79d0127eb6c95f468d9ea2b))
 | 
			
		||||
- add ImageMagick ([b47e575](https://github.com/C4illin/ConvertX/commit/b47e5755f677056e8acecad54c0c2e28a5e137f3)), closes [#295](https://github.com/C4illin/ConvertX/issues/295), closes [#269](https://github.com/C4illin/ConvertX/issues/269)
 | 
			
		||||
- enhance job details display with file information ([50725ed](https://github.com/C4illin/ConvertX/commit/50725edd021bb9a7f58c85b79c1eab355ad22ced)), closes [#251](https://github.com/C4illin/ConvertX/issues/251)
 | 
			
		||||
- improve job details interaction and accessibility ([29ba229](https://github.com/C4illin/ConvertX/commit/29ba229bc23d2019d2ee9829da7852f884ffa611))
 | 
			
		||||
- show version in footer ([9a49ded](https://github.com/C4illin/ConvertX/commit/9a49dedacac7e67a432b6da0daf1967038d97d26))
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
- add av1 and h26X with containers ([af5c768](https://github.com/C4illin/ConvertX/commit/af5c768dc74b3124fd7ef4b29e27c83a5d19ad49)), closes [#287](https://github.com/C4illin/ConvertX/issues/287), closes [#293](https://github.com/C4illin/ConvertX/issues/293)
 | 
			
		||||
- progress bars on firefox ([ff2c005](https://github.com/C4illin/ConvertX/commit/ff2c0057e890b9ecb552df30914333349ea20eb7))
 | 
			
		||||
- register button style ([b9bbf77](https://github.com/C4illin/ConvertX/commit/b9bbf7792f01fcaa77e3520925de107e856926f1))
 | 
			
		||||
- switch from alpine to debian trixie ([4e4c029](https://github.com/C4illin/ConvertX/commit/4e4c029cb800df86affb99c3a82dda9e6708bdde)), closes [#234](https://github.com/C4illin/ConvertX/issues/234), closes [#199](https://github.com/C4illin/ConvertX/issues/199)
 | 
			
		||||
 | 
			
		||||
## [0.13.0](https://github.com/C4illin/ConvertX/compare/v0.12.1...v0.13.0) (2025-05-14)
 | 
			
		||||
 | 
			
		||||
### Features
 | 
			
		||||
 | 
			
		||||
- add HIDE_HISTORY option to control visibility of history page ([9d1c931](https://github.com/C4illin/ConvertX/commit/9d1c93155cc33ed6c83f9e5122afff8f28d0e4bf))
 | 
			
		||||
- add potrace converter ([bdbd4a1](https://github.com/C4illin/ConvertX/commit/bdbd4a122c09559b089b985ea12c5f3e085107da))
 | 
			
		||||
- Add support for .HIF files ([70705c1](https://github.com/C4illin/ConvertX/commit/70705c1850d470296df85958c02a01fb5bc3a25f))
 | 
			
		||||
- add support for drag/drop of images ([ff2ef74](https://github.com/C4illin/ConvertX/commit/ff2ef7413542cf10ba7a6e246763bcecd6829ec1))
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
- add timezone support ([4b5c732](https://github.com/C4illin/ConvertX/commit/4b5c732380bc844dccf340ea1eb4f8bfe3bb44a5)), closes [#258](https://github.com/C4illin/ConvertX/issues/258)
 | 
			
		||||
 | 
			
		||||
## [0.12.1](https://github.com/C4illin/ConvertX/compare/v0.12.0...v0.12.1) (2025-03-20)
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
- rollback to bun 1.2.2 ([cdae798](https://github.com/C4illin/ConvertX/commit/cdae798fcf5879e4adea87386a38748b9a1e1ddc))
 | 
			
		||||
 | 
			
		||||
## [0.12.0](https://github.com/C4illin/ConvertX/compare/v0.11.1...v0.12.0) (2025-03-06)
 | 
			
		||||
 | 
			
		||||
### Features
 | 
			
		||||
 | 
			
		||||
- added progress bar for file upload ([db60f35](https://github.com/C4illin/ConvertX/commit/db60f355b2973f43f8e5990e6fe4e351b959b659))
 | 
			
		||||
- made every upload file independent ([cc54bdc](https://github.com/C4illin/ConvertX/commit/cc54bdcbe764c41cc3273485d072fd3178ad2dca))
 | 
			
		||||
- replace exec with execFile ([9263d17](https://github.com/C4illin/ConvertX/commit/9263d17609dc4b2b367eb7fee67b3182e283b3a3))
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
- add libheif ([6b92540](https://github.com/C4illin/ConvertX/commit/6b9254047c0598963aee1d99e20ba1650a0368bf))
 | 
			
		||||
- add libheif ([decfea5](https://github.com/C4illin/ConvertX/commit/decfea5dc9627b216bb276a9e1578c32cfa1deb6)), closes [#202](https://github.com/C4illin/ConvertX/issues/202)
 | 
			
		||||
- added onerror log ([ae4bbc8](https://github.com/C4illin/ConvertX/commit/ae4bbc8baacbaf67763c62ea44140bb21cc17230))
 | 
			
		||||
- refactored uploadFile to only accept a single file instead of multiple ([dc82a43](https://github.com/C4illin/ConvertX/commit/dc82a438d4104b79ff423d502a6779a43928968a))
 | 
			
		||||
- update libheif to 1.19.5 ([fba5e21](https://github.com/C4illin/ConvertX/commit/fba5e212e8d0eaba8971e239e35aeb521f3cd813)), closes [#202](https://github.com/C4illin/ConvertX/issues/202)
 | 
			
		||||
 | 
			
		||||
## [0.11.1](https://github.com/C4illin/ConvertX/compare/v0.11.0...v0.11.1) (2025-02-07)
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
- mobile view overflow ([bec58ac](https://github.com/C4illin/ConvertX/commit/bec58ac59f9600e35385b9e21d174f3ab1b42b1d))
 | 
			
		||||
 | 
			
		||||
## [0.11.0](https://github.com/C4illin/ConvertX/compare/v0.10.1...v0.11.0) (2025-02-05)
 | 
			
		||||
 | 
			
		||||
### Features
 | 
			
		||||
 | 
			
		||||
- add deps for vaapi ([2bbbd03](https://github.com/C4illin/ConvertX/commit/2bbbd03554d384a4488143f29e5fc863cfdf333b)), closes [#192](https://github.com/C4illin/ConvertX/issues/192)
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
- don't crash if file is not found ([16f27c1](https://github.com/C4illin/ConvertX/commit/16f27c13bbc1c0e5fa2316f3db11d0918524053b))
 | 
			
		||||
- install numpy for inkscape ([0e61051](https://github.com/C4illin/ConvertX/commit/0e61051fc6be188164c3865b4fb579c140859fdc))
 | 
			
		||||
 | 
			
		||||
## [0.10.1](https://github.com/C4illin/ConvertX/compare/v0.10.0...v0.10.1) (2025-01-21)
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
- ffmpeg works without ffmpeg_args ([3b7ea88](https://github.com/C4illin/ConvertX/commit/3b7ea88b7382f7c21b120bdc9bda5bb10547f55d)), closes [#212](https://github.com/C4illin/ConvertX/issues/212)
 | 
			
		||||
 | 
			
		||||
## [0.10.0](https://github.com/C4illin/ConvertX/compare/v0.9.0...v0.10.0) (2025-01-18)
 | 
			
		||||
 | 
			
		||||
### Features
 | 
			
		||||
 | 
			
		||||
- add calibre ([03d3edf](https://github.com/C4illin/ConvertX/commit/03d3edfff65c252dd4b8922fc98257c089c1ff74)), closes [#191](https://github.com/C4illin/ConvertX/issues/191)
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
- add FFMPEG_ARGS env variable ([f537c81](https://github.com/C4illin/ConvertX/commit/f537c81db7815df8017f834e3162291197e1c40f)), closes [#190](https://github.com/C4illin/ConvertX/issues/190)
 | 
			
		||||
- add qt6-qtbase-private-dev from community repo ([95dbc9f](https://github.com/C4illin/ConvertX/commit/95dbc9f678bec7e6e2c03587e1473fb8ff708ea3))
 | 
			
		||||
- skip account setup when ALLOW_UNAUTHENTICATED is true ([538c5b6](https://github.com/C4illin/ConvertX/commit/538c5b60c9e27a8184740305475245da79bae143))
 | 
			
		||||
 | 
			
		||||
## [0.9.0](https://github.com/C4illin/ConvertX/compare/v0.8.1...v0.9.0) (2024-11-21)
 | 
			
		||||
 | 
			
		||||
### Features
 | 
			
		||||
 | 
			
		||||
- add inkscape for vector images ([f3740e9](https://github.com/C4illin/ConvertX/commit/f3740e9ded100b8500f3613517960248bbd3c210))
 | 
			
		||||
- Allow to chose webroot ([36cb6cc](https://github.com/C4illin/ConvertX/commit/36cb6cc589d80d0a87fa8dbe605db71a9a2570f9)), closes [#180](https://github.com/C4illin/ConvertX/issues/180)
 | 
			
		||||
- disable convert when uploading ([58e220e](https://github.com/C4illin/ConvertX/commit/58e220e82d7f9c163d6ea4dc31092c08a3e254f4)), closes [#177](https://github.com/C4illin/ConvertX/issues/177)
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
- treat unknown as m4a ([1a442d6](https://github.com/C4illin/ConvertX/commit/1a442d6e69606afef63b1e7df36aa83d111fa23d)), closes [#178](https://github.com/C4illin/ConvertX/issues/178)
 | 
			
		||||
- wait for both upload and selection ([4c05fd7](https://github.com/C4illin/ConvertX/commit/4c05fd72bbbf91ee02327f6fcbf749b78272376b)), closes [#177](https://github.com/C4illin/ConvertX/issues/177)
 | 
			
		||||
 | 
			
		||||
## [0.8.1](https://github.com/C4illin/ConvertX/compare/v0.8.0...v0.8.1) (2024-10-05)
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
- disable convert button when input is empty ([78844d7](https://github.com/C4illin/ConvertX/commit/78844d7bd55990789ed07c81e49043e688cbe656)), closes [#151](https://github.com/C4illin/ConvertX/issues/151)
 | 
			
		||||
- resize to fit for ico ([b4e53db](https://github.com/C4illin/ConvertX/commit/b4e53dbb8e70b3a95b44e5b756759d16117a87e1)), closes [#157](https://github.com/C4illin/ConvertX/issues/157)
 | 
			
		||||
- treat jfif as jpeg ([339b79f](https://github.com/C4illin/ConvertX/commit/339b79f786131deb93f0d5683e03178fdcab1ef5)), closes [#163](https://github.com/C4illin/ConvertX/issues/163)
 | 
			
		||||
 | 
			
		||||
## [0.8.0](https://github.com/C4illin/ConvertX/compare/v0.7.0...v0.8.0) (2024-09-30)
 | 
			
		||||
 | 
			
		||||
### Features
 | 
			
		||||
 | 
			
		||||
- add light theme, fixes [#156](https://github.com/C4illin/ConvertX/issues/156) ([72636c5](https://github.com/C4illin/ConvertX/commit/72636c5059ebf09c8fece2e268293650b2f8ccf6))
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
- add support for usd for assimp, [#144](https://github.com/C4illin/ConvertX/issues/144) ([2057167](https://github.com/C4illin/ConvertX/commit/20571675766209ad1251f07e687d29a6791afc8b))
 | 
			
		||||
- cleanup formats and add opus, fixes [#159](https://github.com/C4illin/ConvertX/issues/159) ([ae1dfaf](https://github.com/C4illin/ConvertX/commit/ae1dfafc9d9116a57b08c2f7fc326990e00824b0))
 | 
			
		||||
- support .awb and clean up, fixes [#153](https://github.com/C4illin/ConvertX/issues/153), [#92](https://github.com/C4illin/ConvertX/issues/92) ([1c9e67f](https://github.com/C4illin/ConvertX/commit/1c9e67fc3201e0e5dee91e8981adf34daaabf33a))
 | 
			
		||||
 | 
			
		||||
## [0.7.0](https://github.com/C4illin/ConvertX/compare/v0.6.0...v0.7.0) (2024-09-26)
 | 
			
		||||
 | 
			
		||||
### Features
 | 
			
		||||
 | 
			
		||||
- Add support for 3d assets through assimp converter ([63a4328](https://github.com/C4illin/ConvertX/commit/63a4328d4a1e01df3e0ec4a877bad8c8ffe71129))
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
- wrong layout on search with few options ([8817389](https://github.com/C4illin/ConvertX/commit/88173891ba2d69da46eda46f3f598a9b54f26f96))
 | 
			
		||||
 | 
			
		||||
## [0.6.0](https://github.com/C4illin/ConvertX/compare/v0.5.0...v0.6.0) (2024-09-25)
 | 
			
		||||
 | 
			
		||||
### Features
 | 
			
		||||
 | 
			
		||||
- ui remake with tailwind ([22f823c](https://github.com/C4illin/ConvertX/commit/22f823c535b20382981f86a13616b830a1f3392f))
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
- rename css file to force update cache, fixes [#141](https://github.com/C4illin/ConvertX/issues/141) ([47139a5](https://github.com/C4illin/ConvertX/commit/47139a550bd3d847da288c61bf8f88953b79c673))
 | 
			
		||||
 | 
			
		||||
## [0.5.0](https://github.com/C4illin/ConvertX/compare/v0.4.1...v0.5.0) (2024-09-20)
 | 
			
		||||
 | 
			
		||||
### Features
 | 
			
		||||
 | 
			
		||||
- add option to customize how often files are automatically deleted ([317c932](https://github.com/C4illin/ConvertX/commit/317c932c2a26280bf37ed3d3bf9b879413590f5a))
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
- improve file name replacement logic ([60ba7c9](https://github.com/C4illin/ConvertX/commit/60ba7c93fbdc961f3569882fade7cc13dee7a7a5))
 | 
			
		||||
 | 
			
		||||
## [0.4.1](https://github.com/C4illin/ConvertX/compare/v0.4.0...v0.4.1) (2024-09-15)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* allow non lowercase true and false values, fixes [#122](https://github.com/C4illin/ConvertX/issues/122) ([bef1710](https://github.com/C4illin/ConvertX/commit/bef1710e3376baa7e25c107ded20a40d18b8c6b0))
 | 
			
		||||
- allow non lowercase true and false values, fixes [#122](https://github.com/C4illin/ConvertX/issues/122) ([bef1710](https://github.com/C4illin/ConvertX/commit/bef1710e3376baa7e25c107ded20a40d18b8c6b0))
 | 
			
		||||
 | 
			
		||||
## [0.4.0](https://github.com/C4illin/ConvertX/compare/v0.3.3...v0.4.0) (2024-08-26)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Features
 | 
			
		||||
 | 
			
		||||
* add option for unauthenticated file conversions [#114](https://github.com/C4illin/ConvertX/issues/114) ([f0d0e43](https://github.com/C4illin/ConvertX/commit/f0d0e4392983c3e4c530304ea88e023fda9bcac0))
 | 
			
		||||
* add resvg converter ([d5eeef9](https://github.com/C4illin/ConvertX/commit/d5eeef9f6884b2bb878508bed97ea9ceaa662995))
 | 
			
		||||
* add robots.txt ([6597c1d](https://github.com/C4illin/ConvertX/commit/6597c1d7caeb4dfb6bc47b442e4dfc9840ad12b7))
 | 
			
		||||
* Add search bar for formats ([53fff59](https://github.com/C4illin/ConvertX/commit/53fff594fc4d69306abcb2a5cad890fcd0953a58))
 | 
			
		||||
 | 
			
		||||
- add option for unauthenticated file conversions [#114](https://github.com/C4illin/ConvertX/issues/114) ([f0d0e43](https://github.com/C4illin/ConvertX/commit/f0d0e4392983c3e4c530304ea88e023fda9bcac0))
 | 
			
		||||
- add resvg converter ([d5eeef9](https://github.com/C4illin/ConvertX/commit/d5eeef9f6884b2bb878508bed97ea9ceaa662995))
 | 
			
		||||
- add robots.txt ([6597c1d](https://github.com/C4illin/ConvertX/commit/6597c1d7caeb4dfb6bc47b442e4dfc9840ad12b7))
 | 
			
		||||
- Add search bar for formats ([53fff59](https://github.com/C4illin/ConvertX/commit/53fff594fc4d69306abcb2a5cad890fcd0953a58))
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* keep unauthenticated user logged in if allowed [#114](https://github.com/C4illin/ConvertX/issues/114) ([bc4ad49](https://github.com/C4illin/ConvertX/commit/bc4ad492852fad8cb832a0c03485cccdd7f7b117))
 | 
			
		||||
* pdf support in vips ([8ca4f15](https://github.com/C4illin/ConvertX/commit/8ca4f1587df7f358893941c656d78d75f04dac93))
 | 
			
		||||
* Slow click on conversion popup does not work ([4d9c4d6](https://github.com/C4illin/ConvertX/commit/4d9c4d64aa0266f3928935ada68d91ac81f638aa))
 | 
			
		||||
- keep unauthenticated user logged in if allowed [#114](https://github.com/C4illin/ConvertX/issues/114) ([bc4ad49](https://github.com/C4illin/ConvertX/commit/bc4ad492852fad8cb832a0c03485cccdd7f7b117))
 | 
			
		||||
- pdf support in vips ([8ca4f15](https://github.com/C4illin/ConvertX/commit/8ca4f1587df7f358893941c656d78d75f04dac93))
 | 
			
		||||
- Slow click on conversion popup does not work ([4d9c4d6](https://github.com/C4illin/ConvertX/commit/4d9c4d64aa0266f3928935ada68d91ac81f638aa))
 | 
			
		||||
 | 
			
		||||
## [0.3.3](https://github.com/C4illin/ConvertX/compare/v0.3.2...v0.3.3) (2024-07-30)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* downgrade @elysiajs/html dependency to version 1.0.2 ([c714ade](https://github.com/C4illin/ConvertX/commit/c714ade3e23865ba6cfaf76c9e7259df1cda222c))
 | 
			
		||||
- downgrade @elysiajs/html dependency to version 1.0.2 ([c714ade](https://github.com/C4illin/ConvertX/commit/c714ade3e23865ba6cfaf76c9e7259df1cda222c))
 | 
			
		||||
 | 
			
		||||
## [0.3.2](https://github.com/C4illin/ConvertX/compare/v0.3.1...v0.3.2) (2024-07-09)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* increase max request body to support large uploads ([3ae2db5](https://github.com/C4illin/ConvertX/commit/3ae2db5d9b36fe3dcd4372ddcd32aa573ea59aa6)), closes [#64](https://github.com/C4illin/ConvertX/issues/64)
 | 
			
		||||
- increase max request body to support large uploads ([3ae2db5](https://github.com/C4illin/ConvertX/commit/3ae2db5d9b36fe3dcd4372ddcd32aa573ea59aa6)), closes [#64](https://github.com/C4illin/ConvertX/issues/64)
 | 
			
		||||
 | 
			
		||||
## [0.3.1](https://github.com/C4illin/ConvertX/compare/v0.3.0...v0.3.1) (2024-06-27)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* release releases ([4d4c13a](https://github.com/C4illin/ConvertX/commit/4d4c13a8d85ec7c9209ad41cdbea7d4380b0edbf))
 | 
			
		||||
- release releases ([4d4c13a](https://github.com/C4illin/ConvertX/commit/4d4c13a8d85ec7c9209ad41cdbea7d4380b0edbf))
 | 
			
		||||
 | 
			
		||||
## [0.3.0](https://github.com/C4illin/ConvertX/compare/v0.2.0...v0.3.0) (2024-06-27)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Features
 | 
			
		||||
 | 
			
		||||
* add version number to log ([4dcb796](https://github.com/C4illin/ConvertX/commit/4dcb796e1bd27badc078d0638076cd9f1e81c4a4)), closes [#44](https://github.com/C4illin/ConvertX/issues/44)
 | 
			
		||||
* change to xelatex ([fae2ba9](https://github.com/C4illin/ConvertX/commit/fae2ba9c54461dccdccd1bfb5e76398540d11d0b))
 | 
			
		||||
* print version of installed converters to log ([801cf28](https://github.com/C4illin/ConvertX/commit/801cf28d1e5edac9353b0b16be75a4fb48470b8a))
 | 
			
		||||
- add version number to log ([4dcb796](https://github.com/C4illin/ConvertX/commit/4dcb796e1bd27badc078d0638076cd9f1e81c4a4)), closes [#44](https://github.com/C4illin/ConvertX/issues/44)
 | 
			
		||||
- change to xelatex ([fae2ba9](https://github.com/C4illin/ConvertX/commit/fae2ba9c54461dccdccd1bfb5e76398540d11d0b))
 | 
			
		||||
- print version of installed converters to log ([801cf28](https://github.com/C4illin/ConvertX/commit/801cf28d1e5edac9353b0b16be75a4fb48470b8a))
 | 
			
		||||
 | 
			
		||||
## [0.2.0](https://github.com/C4illin/ConvertX/compare/v0.1.2...v0.2.0) (2024-06-20)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Features
 | 
			
		||||
 | 
			
		||||
* add libjxl for jpegxl conversion ([ff680cb](https://github.com/C4illin/ConvertX/commit/ff680cb29534a25c3148a90fd064bb86c71fb482))
 | 
			
		||||
* change from debian to alpine ([1316957](https://github.com/C4illin/ConvertX/commit/13169574f0134ae236f8d41287bb73930b575e82)), closes [#34](https://github.com/C4illin/ConvertX/issues/34)
 | 
			
		||||
- add libjxl for jpegxl conversion ([ff680cb](https://github.com/C4illin/ConvertX/commit/ff680cb29534a25c3148a90fd064bb86c71fb482))
 | 
			
		||||
- change from debian to alpine ([1316957](https://github.com/C4illin/ConvertX/commit/13169574f0134ae236f8d41287bb73930b575e82)), closes [#34](https://github.com/C4illin/ConvertX/issues/34)
 | 
			
		||||
 | 
			
		||||
## [0.1.2](https://github.com/C4illin/ConvertX/compare/v0.1.1...v0.1.2) (2024-06-10)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* fix incorrect redirect ([25df58b](https://github.com/C4illin/ConvertX/commit/25df58ba82321aaa6617811a6995cb96c2a00a40)), closes [#23](https://github.com/C4illin/ConvertX/issues/23)
 | 
			
		||||
- fix incorrect redirect ([25df58b](https://github.com/C4illin/ConvertX/commit/25df58ba82321aaa6617811a6995cb96c2a00a40)), closes [#23](https://github.com/C4illin/ConvertX/issues/23)
 | 
			
		||||
 | 
			
		||||
## [0.1.1](https://github.com/C4illin/ConvertX/compare/v0.1.0...v0.1.1) (2024-05-30)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* :bug: make sure all redirects are 302 ([9970fd3](https://github.com/C4illin/ConvertX/commit/9970fd3f89190af96f8762edc3817d1e03082b3a)), closes [#12](https://github.com/C4illin/ConvertX/issues/12)
 | 
			
		||||
- :bug: make sure all redirects are 302 ([9970fd3](https://github.com/C4illin/ConvertX/commit/9970fd3f89190af96f8762edc3817d1e03082b3a)), closes [#12](https://github.com/C4illin/ConvertX/issues/12)
 | 
			
		||||
 | 
			
		||||
## 0.1.0 (2024-05-30)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Features
 | 
			
		||||
 | 
			
		||||
* remove file from file list in index.html ([787ff97](https://github.com/C4illin/ConvertX/commit/787ff9741ecbbf4fb4c02b43bd22a214a173fd7b))
 | 
			
		||||
 | 
			
		||||
- remove file from file list in index.html ([787ff97](https://github.com/C4illin/ConvertX/commit/787ff9741ecbbf4fb4c02b43bd22a214a173fd7b))
 | 
			
		||||
 | 
			
		||||
### Miscellaneous Chores
 | 
			
		||||
 | 
			
		||||
* release 0.1.0 ([54d9aec](https://github.com/C4illin/ConvertX/commit/54d9aecbf949689b12aa7e5e8e9be7b9032f4431))
 | 
			
		||||
- release 0.1.0 ([54d9aec](https://github.com/C4illin/ConvertX/commit/54d9aecbf949689b12aa7e5e8e9be7b9032f4431))
 | 
			
		||||
 
 | 
			
		||||
@@ -1,63 +0,0 @@
 | 
			
		||||
FROM oven/bun:1-debian as base
 | 
			
		||||
WORKDIR /app
 | 
			
		||||
 | 
			
		||||
# install dependencies into temp directory
 | 
			
		||||
# this will cache them and speed up future builds
 | 
			
		||||
FROM base AS install
 | 
			
		||||
RUN mkdir -p /temp/dev
 | 
			
		||||
COPY package.json bun.lockb /temp/dev/
 | 
			
		||||
RUN cd /temp/dev && bun install --frozen-lockfile
 | 
			
		||||
 | 
			
		||||
# install with --production (exclude devDependencies)
 | 
			
		||||
RUN mkdir -p /temp/prod
 | 
			
		||||
COPY package.json bun.lockb /temp/prod/
 | 
			
		||||
RUN cd /temp/prod && bun install --frozen-lockfile --production
 | 
			
		||||
 | 
			
		||||
# FROM base AS install-libjxl-tools
 | 
			
		||||
# download
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# copy node_modules from temp directory
 | 
			
		||||
# then copy all (non-ignored) project files into the image
 | 
			
		||||
# FROM base AS prerelease
 | 
			
		||||
# COPY --from=install /temp/dev/node_modules node_modules
 | 
			
		||||
# COPY . .
 | 
			
		||||
 | 
			
		||||
# # [optional] tests & build
 | 
			
		||||
# ENV NODE_ENV=production
 | 
			
		||||
# RUN bun test
 | 
			
		||||
# RUN bun run build
 | 
			
		||||
 | 
			
		||||
# copy production dependencies and source code into final image
 | 
			
		||||
FROM base AS release
 | 
			
		||||
LABEL maintainer="Emrik Östling (C4illin)"
 | 
			
		||||
LABEL description="ConvertX: self-hosted online file converter supporting 700+ file formats."
 | 
			
		||||
LABEL repo="https://github.com/C4illin/ConvertX"
 | 
			
		||||
 | 
			
		||||
# install additional dependencies
 | 
			
		||||
RUN rm -rf /var/lib/apt/lists/partial && apt-get update -o Acquire::CompressionTypes::Order::=gz \
 | 
			
		||||
  && apt-get install -y \
 | 
			
		||||
  pandoc \
 | 
			
		||||
  texlive-latex-recommended \
 | 
			
		||||
  texlive-fonts-recommended \
 | 
			
		||||
  texlive-latex-extra \
 | 
			
		||||
  ffmpeg \
 | 
			
		||||
  graphicsmagick \
 | 
			
		||||
  ghostscript \
 | 
			
		||||
  libvips-tools
 | 
			
		||||
 | 
			
		||||
# # libjxl is not available in the official debian repositories
 | 
			
		||||
# RUN wget https://github.com/libjxl/libjxl/releases/download/v0.10.2/jxl-debs-amd64-debian-bullseye-v0.10.2.tar.gz -O /tmp/jxl-debs-amd64-debian-bullseye-v0.10.2.tar.gz \
 | 
			
		||||
#   && mkdir -p /tmp/libjxl \
 | 
			
		||||
#   && tar -xvf /tmp/jxl-debs-amd64-debian-bullseye-v0.10.2.tar.gz -C /tmp/libjxl \
 | 
			
		||||
#   && dpkg -i /tmp/libjxl/libjxl_0.10.2_amd64.deb /tmp/libjxl/jxl_0.10.2_amd64.deb \
 | 
			
		||||
#   && rm -rf /tmp/jxl-debs-amd64-debian-bullseye-v0.10.2.tar.gz /tmp/libjxl
 | 
			
		||||
 | 
			
		||||
COPY --from=install /temp/prod/node_modules node_modules
 | 
			
		||||
# COPY --from=prerelease /app/src/index.tsx /app/src/
 | 
			
		||||
# COPY --from=prerelease /app/package.json .
 | 
			
		||||
COPY . .
 | 
			
		||||
 | 
			
		||||
EXPOSE 3000/tcp
 | 
			
		||||
ENTRYPOINT [ "bun", "run", "./src/index.tsx" ]
 | 
			
		||||
							
								
								
									
										115
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						@@ -1,65 +1,104 @@
 | 
			
		||||
FROM oven/bun:1.1.27-alpine AS base
 | 
			
		||||
FROM debian:trixie-slim AS base
 | 
			
		||||
LABEL org.opencontainers.image.source="https://github.com/C4illin/ConvertX"
 | 
			
		||||
WORKDIR /app
 | 
			
		||||
 | 
			
		||||
# install bun
 | 
			
		||||
RUN apt-get update && apt-get install -y \
 | 
			
		||||
  curl \
 | 
			
		||||
  unzip \
 | 
			
		||||
  && rm -rf /var/lib/apt/lists/*
 | 
			
		||||
 | 
			
		||||
# if architecture is arm64, use the arm64 version of bun
 | 
			
		||||
RUN ARCH=$(uname -m) && \
 | 
			
		||||
  if [ "$ARCH" = "aarch64" ]; then \
 | 
			
		||||
    curl -fsSL -o bun-linux-aarch64.zip https://github.com/oven-sh/bun/releases/download/bun-v1.2.2/bun-linux-aarch64.zip; \
 | 
			
		||||
  else \
 | 
			
		||||
    curl -fsSL -o bun-linux-x64-baseline.zip https://github.com/oven-sh/bun/releases/download/bun-v1.2.2/bun-linux-x64-baseline.zip; \
 | 
			
		||||
  fi
 | 
			
		||||
 | 
			
		||||
RUN unzip -j bun-linux-*.zip -d /usr/local/bin && \
 | 
			
		||||
  rm bun-linux-*.zip && \
 | 
			
		||||
  chmod +x /usr/local/bin/bun
 | 
			
		||||
 | 
			
		||||
# install dependencies into temp directory
 | 
			
		||||
# this will cache them and speed up future builds
 | 
			
		||||
FROM base AS install
 | 
			
		||||
RUN mkdir -p /temp/dev
 | 
			
		||||
COPY package.json bun.lockb /temp/dev/
 | 
			
		||||
COPY package.json bun.lock /temp/dev/
 | 
			
		||||
RUN cd /temp/dev && bun install --frozen-lockfile
 | 
			
		||||
 | 
			
		||||
# install with --production (exclude devDependencies)
 | 
			
		||||
RUN mkdir -p /temp/prod
 | 
			
		||||
COPY package.json bun.lockb /temp/prod/
 | 
			
		||||
COPY package.json bun.lock /temp/prod/
 | 
			
		||||
RUN cd /temp/prod && bun install --frozen-lockfile --production
 | 
			
		||||
 | 
			
		||||
FROM base AS builder
 | 
			
		||||
RUN apk --no-cache add curl gcc
 | 
			
		||||
ENV PATH=/root/.cargo/bin:$PATH
 | 
			
		||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
 | 
			
		||||
RUN cargo install resvg
 | 
			
		||||
FROM base AS prerelease
 | 
			
		||||
WORKDIR /app
 | 
			
		||||
COPY --from=install /temp/dev/node_modules node_modules
 | 
			
		||||
COPY . .
 | 
			
		||||
 | 
			
		||||
# copy node_modules from temp directory
 | 
			
		||||
# then copy all (non-ignored) project files into the image
 | 
			
		||||
# FROM base AS prerelease
 | 
			
		||||
# COPY --from=install /temp/dev/node_modules node_modules
 | 
			
		||||
# COPY . .
 | 
			
		||||
 | 
			
		||||
# # [optional] tests & build
 | 
			
		||||
# ENV NODE_ENV=production
 | 
			
		||||
# RUN bun test
 | 
			
		||||
# RUN bun run build
 | 
			
		||||
RUN bun run build
 | 
			
		||||
 | 
			
		||||
# copy production dependencies and source code into final image
 | 
			
		||||
FROM base AS release
 | 
			
		||||
LABEL maintainer="Emrik Östling (C4illin)"
 | 
			
		||||
LABEL description="ConvertX: self-hosted online file converter supporting 700+ file formats."
 | 
			
		||||
LABEL repo="https://github.com/C4illin/ConvertX"
 | 
			
		||||
 | 
			
		||||
# install additional dependencies
 | 
			
		||||
RUN apk --no-cache add  \
 | 
			
		||||
  pandoc \
 | 
			
		||||
  texlive \
 | 
			
		||||
  texlive-xetex \
 | 
			
		||||
  texmf-dist-latexextra \
 | 
			
		||||
# install additional dependencies 
 | 
			
		||||
RUN apt-get update && apt-get install -y \
 | 
			
		||||
  assimp-utils \
 | 
			
		||||
  calibre \
 | 
			
		||||
  dasel \
 | 
			
		||||
  dcraw \
 | 
			
		||||
  dvisvgm \
 | 
			
		||||
  ffmpeg \
 | 
			
		||||
  graphicsmagick \
 | 
			
		||||
  ghostscript \
 | 
			
		||||
  vips-tools \
 | 
			
		||||
  vips-poppler \
 | 
			
		||||
  vips-jxl \
 | 
			
		||||
  libjxl-tools
 | 
			
		||||
  graphicsmagick \
 | 
			
		||||
  imagemagick-7.q16 \
 | 
			
		||||
  inkscape \
 | 
			
		||||
  latexmk \
 | 
			
		||||
  libheif-examples \
 | 
			
		||||
  libjxl-tools \
 | 
			
		||||
  libreoffice \
 | 
			
		||||
  libva2 \
 | 
			
		||||
  libvips-tools \
 | 
			
		||||
  libemail-outlook-message-perl \
 | 
			
		||||
  lmodern \
 | 
			
		||||
  mupdf-tools \
 | 
			
		||||
  pandoc \
 | 
			
		||||
  poppler-utils \
 | 
			
		||||
  potrace \
 | 
			
		||||
  python3-numpy \
 | 
			
		||||
  resvg \
 | 
			
		||||
  texlive \
 | 
			
		||||
  texlive-fonts-recommended \
 | 
			
		||||
  texlive-latex-extra \
 | 
			
		||||
  texlive-latex-recommended \
 | 
			
		||||
  texlive-xetex \
 | 
			
		||||
  --no-install-recommends \
 | 
			
		||||
  && rm -rf /var/lib/apt/lists/*
 | 
			
		||||
 | 
			
		||||
# this might be needed for some latex use cases, will add it if needed.
 | 
			
		||||
#   texmf-dist-fontsextra \
 | 
			
		||||
# Install VTracer binary
 | 
			
		||||
RUN ARCH=$(uname -m) && \
 | 
			
		||||
  if [ "$ARCH" = "aarch64" ]; then \
 | 
			
		||||
    VTRACER_ASSET="vtracer-aarch64-unknown-linux-musl.tar.gz"; \
 | 
			
		||||
  else \
 | 
			
		||||
    VTRACER_ASSET="vtracer-x86_64-unknown-linux-musl.tar.gz"; \
 | 
			
		||||
  fi && \
 | 
			
		||||
  curl -L -o /tmp/vtracer.tar.gz "https://github.com/visioncortex/vtracer/releases/download/0.6.4/${VTRACER_ASSET}" && \
 | 
			
		||||
  tar -xzf /tmp/vtracer.tar.gz -C /tmp/ && \
 | 
			
		||||
  mv /tmp/vtracer /usr/local/bin/vtracer && \
 | 
			
		||||
  chmod +x /usr/local/bin/vtracer && \
 | 
			
		||||
  rm /tmp/vtracer.tar.gz
 | 
			
		||||
 | 
			
		||||
COPY --from=install /temp/prod/node_modules node_modules
 | 
			
		||||
COPY --from=builder /root/.cargo/bin/resvg /usr/local/bin/resvg
 | 
			
		||||
# COPY --from=prerelease /app/src/index.tsx /app/src/
 | 
			
		||||
# COPY --from=prerelease /app/package.json .
 | 
			
		||||
COPY . .
 | 
			
		||||
COPY --from=prerelease /app/public/generated.css /app/public/
 | 
			
		||||
COPY --from=prerelease /app/dist /app/dist
 | 
			
		||||
 | 
			
		||||
# COPY . .
 | 
			
		||||
RUN mkdir data
 | 
			
		||||
 | 
			
		||||
EXPOSE 3000/tcp
 | 
			
		||||
# used for calibre
 | 
			
		||||
ENV QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox"
 | 
			
		||||
ENV NODE_ENV=production
 | 
			
		||||
ENTRYPOINT [ "bun", "run", "./src/index.tsx" ]
 | 
			
		||||
ENTRYPOINT [ "bun", "run", "dist/src/index.js" ]
 | 
			
		||||
							
								
								
									
										117
									
								
								README.md
									
									
									
									
									
								
							
							
						
						@@ -1,31 +1,48 @@
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
# ConvertX
 | 
			
		||||
 | 
			
		||||
[](https://github.com/C4illin/ConvertX/actions/workflows/docker-publish.yml)
 | 
			
		||||
[](https://github.com/C4illin/ConvertX/pkgs/container/ConvertX)
 | 
			
		||||
[](https://hub.docker.com/r/c4illin/convertx)
 | 
			
		||||
[](https://github.com/C4illin/ConvertX/pkgs/container/convertx)
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
A self-hosted online file converter. Supports 831 different formats. Written with TypeScript, Bun and Elysia.
 | 
			
		||||
<a href="https://trendshift.io/repositories/13818" target="_blank"><img src="https://trendshift.io/api/badge/repositories/13818" alt="C4illin%2FConvertX | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
 | 
			
		||||
 | 
			
		||||
<!--  -->
 | 
			
		||||
 | 
			
		||||
A self-hosted online file converter. Supports over a thousand different formats. Written with TypeScript, Bun and Elysia.
 | 
			
		||||
 | 
			
		||||
## Features
 | 
			
		||||
 | 
			
		||||
- Convert files to different formats
 | 
			
		||||
- Process multiple files at once
 | 
			
		||||
- Password protection
 | 
			
		||||
- Multiple accounts
 | 
			
		||||
 | 
			
		||||
## Converters supported
 | 
			
		||||
 | 
			
		||||
| Converter                                                                    | Use case      | Converts from | Converts to |
 | 
			
		||||
|------------------------------------------------------------------------------|---------------|---------------|-------------|
 | 
			
		||||
| [libjxl](https://github.com/libjxl/libjxl)                                   | JPEG XL       | 11            | 11          |
 | 
			
		||||
| [resvg](https://github.com/RazrFalcon/resvg)                                 | SVG           | 1             | 1           |
 | 
			
		||||
| [Vips](https://github.com/libvips/libvips)                                   | Images        | 45            | 23          |
 | 
			
		||||
| [XeLaTeX](https://tug.org/xetex/)                                            | LaTeX         | 1             | 1           |
 | 
			
		||||
| [Pandoc](https://pandoc.org/)                                                | Documents     | 43            | 65          |
 | 
			
		||||
| [GraphicsMagick](http://www.graphicsmagick.org/)                             | Images        | 166           | 133         |
 | 
			
		||||
| [FFmpeg](https://ffmpeg.org/)                                                | Video         | ~473          | ~280        |
 | 
			
		||||
| Converter                                          | Use case         | Converts from | Converts to |
 | 
			
		||||
| -------------------------------------------------- | ---------------- | ------------- | ----------- |
 | 
			
		||||
| [libjxl](https://github.com/libjxl/libjxl)         | JPEG XL          | 11            | 11          |
 | 
			
		||||
| [resvg](https://github.com/RazrFalcon/resvg)       | SVG              | 1             | 1           |
 | 
			
		||||
| [Vips](https://github.com/libvips/libvips)         | Images           | 45            | 23          |
 | 
			
		||||
| [libheif](https://github.com/strukturag/libheif)   | HEIF             | 2             | 4           |
 | 
			
		||||
| [XeLaTeX](https://tug.org/xetex/)                  | LaTeX            | 1             | 1           |
 | 
			
		||||
| [Calibre](https://calibre-ebook.com/)              | E-books          | 26            | 19          |
 | 
			
		||||
| [Pandoc](https://pandoc.org/)                      | Documents        | 43            | 65          |
 | 
			
		||||
| [dvisvgm](https://dvisvgm.de/)                     | Vector images    | 4             | 2           |
 | 
			
		||||
| [ImageMagick](https://imagemagick.org/)            | Images           | 245           | 183         |
 | 
			
		||||
| [GraphicsMagick](http://www.graphicsmagick.org/)   | Images           | 167           | 130         |
 | 
			
		||||
| [Inkscape](https://inkscape.org/)                  | Vector images    | 7             | 17          |
 | 
			
		||||
| [Assimp](https://github.com/assimp/assimp)         | 3D Assets        | 77            | 23          |
 | 
			
		||||
| [FFmpeg](https://ffmpeg.org/)                      | Video            | ~472          | ~199        |
 | 
			
		||||
| [Potrace](https://potrace.sourceforge.net/)        | Raster to vector | 4             | 11          |
 | 
			
		||||
| [VTracer](https://github.com/visioncortex/vtracer) | Raster to vector | 8             | 1           |
 | 
			
		||||
| [Dasel](https://github.com/TomWright/dasel)        | Data Files       | 5             | 4           |
 | 
			
		||||
 | 
			
		||||
<!-- many ffmpeg fileformats are duplicates -->
 | 
			
		||||
 | 
			
		||||
@@ -33,22 +50,23 @@ Any missing converter? Open an issue or pull request!
 | 
			
		||||
 | 
			
		||||
## Deployment
 | 
			
		||||
 | 
			
		||||
> [!WARNING]
 | 
			
		||||
> If you can't login, make sure you are accessing the service over localhost or https otherwise set HTTP_ALLOWED=true
 | 
			
		||||
 | 
			
		||||
```yml
 | 
			
		||||
# docker-compose.yml
 | 
			
		||||
services:
 | 
			
		||||
  convertx: 
 | 
			
		||||
  convertx:
 | 
			
		||||
    image: ghcr.io/c4illin/convertx
 | 
			
		||||
    container_name: convertx
 | 
			
		||||
    restart: unless-stopped
 | 
			
		||||
    ports:
 | 
			
		||||
      - "3000:3000"
 | 
			
		||||
    environment: # Defaults are listed below. All are optional.
 | 
			
		||||
      - ACCOUNT_REGISTRATION=false # true or false, doesn't matter for the first account (e.g. keep this to false if you only want one account)
 | 
			
		||||
      - JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234 # will use randomUUID() by default
 | 
			
		||||
      - HTTP_ALLOWED=false # setting this to true is unsafe, only set this to true locally
 | 
			
		||||
      - ALLOW_UNAUTHENTICATED=false # allows anyone to use the service without logging in, only set this to true locally
 | 
			
		||||
    environment:
 | 
			
		||||
      - JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234 # will use randomUUID() if unset
 | 
			
		||||
      # - HTTP_ALLOWED=true # uncomment this if accessing it over a non-https connection
 | 
			
		||||
    volumes:
 | 
			
		||||
      - convertx:/app/data
 | 
			
		||||
      - ./data:/app/data
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
or
 | 
			
		||||
@@ -61,9 +79,53 @@ Then visit `http://localhost:3000` in your browser and create your account. Don'
 | 
			
		||||
 | 
			
		||||
If you get unable to open database file run `chown -R $USER:$USER path` on the path you choose.
 | 
			
		||||
 | 
			
		||||
### Environment variables
 | 
			
		||||
 | 
			
		||||
All are optional, JWT_SECRET is recommended to be set.
 | 
			
		||||
 | 
			
		||||
| Name                         | Default                                            | Description                                                                                                               |
 | 
			
		||||
| ---------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
 | 
			
		||||
| JWT_SECRET                   | when unset it will use the value from randomUUID() | A long and secret string used to sign the JSON Web Token                                                                  |
 | 
			
		||||
| ACCOUNT_REGISTRATION         | false                                              | Allow users to register accounts                                                                                          |
 | 
			
		||||
| HTTP_ALLOWED                 | false                                              | Allow HTTP connections, only set this to true locally                                                                     |
 | 
			
		||||
| ALLOW_UNAUTHENTICATED        | false                                              | Allow unauthenticated users to use the service, only set this to true locally                                             |
 | 
			
		||||
| AUTO_DELETE_EVERY_N_HOURS    | 24                                                 | Checks every n hours for files older then n hours and deletes them, set to 0 to disable                                   |
 | 
			
		||||
| WEBROOT                      |                                                    | The address to the root path setting this to "/convert" will serve the website on "example.com/convert/"                  |
 | 
			
		||||
| FFMPEG_ARGS                  |                                                    | Arguments to pass to ffmpeg, e.g. `-preset veryfast`                                                                      |
 | 
			
		||||
| HIDE_HISTORY                 | false                                              | Hide the history page                                                                                                     |
 | 
			
		||||
| LANGUAGE                     | en                                                 | Language to format date strings in, specified as a [BCP 47 language tag](https://en.wikipedia.org/wiki/IETF_language_tag) |
 | 
			
		||||
| UNAUTHENTICATED_USER_SHARING | false                                              | Shares conversion history between all unauthenticated users                                                               |
 | 
			
		||||
 | 
			
		||||
### Docker images
 | 
			
		||||
 | 
			
		||||
There is a `:latest` tag that is updated with every release and a `:main` tag that is updated with every push to the main branch. `:latest` is recommended for normal use.
 | 
			
		||||
 | 
			
		||||
The image is available on [GitHub Container Registry](https://github.com/C4illin/ConvertX/pkgs/container/ConvertX) and [Docker Hub](https://hub.docker.com/r/c4illin/convertx).
 | 
			
		||||
 | 
			
		||||
| Image                                  | What it is                       |
 | 
			
		||||
| -------------------------------------- | -------------------------------- |
 | 
			
		||||
| `image: ghcr.io/c4illin/convertx`      | The latest release on ghcr       |
 | 
			
		||||
| `image: ghcr.io/c4illin/convertx:main` | The latest commit on ghcr        |
 | 
			
		||||
| `image: c4illin/convertx`              | The latest release on docker hub |
 | 
			
		||||
| `image: c4illin/convertx:main`         | The latest commit on docker hub  |
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
<!-- Dockerhub was introduced in 0.9.0 and older releases -->
 | 
			
		||||
 | 
			
		||||
### Tutorial
 | 
			
		||||
 | 
			
		||||
Tutorial in french: https://belginux.com/installer-convertx-avec-docker/
 | 
			
		||||
> [!NOTE]
 | 
			
		||||
> These are written by other people, and may be outdated, incorrect or wrong.
 | 
			
		||||
 | 
			
		||||
Tutorial in french: <https://belginux.com/installer-convertx-avec-docker/>
 | 
			
		||||
 | 
			
		||||
Tutorial in chinese: <https://xzllll.com/24092901/>
 | 
			
		||||
 | 
			
		||||
## Screenshots
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
## Development
 | 
			
		||||
 | 
			
		||||
@@ -72,25 +134,14 @@ Tutorial in french: https://belginux.com/installer-convertx-avec-docker/
 | 
			
		||||
2. `bun install`
 | 
			
		||||
3. `bun run dev`
 | 
			
		||||
 | 
			
		||||
Pull requests are welcome! See below and open issues for the list of todos.
 | 
			
		||||
Pull requests are welcome! See open issues for the list of todos. The ones tagged with "converter request" are quite easy. Help with docs and cleaning up in issues are also very welcome!
 | 
			
		||||
 | 
			
		||||
## Todo
 | 
			
		||||
- [x] Add messages for errors in converters
 | 
			
		||||
- [x] Add searchable list of formats
 | 
			
		||||
- [ ] Add options for converters
 | 
			
		||||
- [ ] Divide index.tsx into smaller components
 | 
			
		||||
- [ ] Add tests
 | 
			
		||||
- [ ] Make the upload button nicer and more easy to drop files on. Support copy paste as well if possible.
 | 
			
		||||
- [ ] Make errors logs visible from the web ui
 | 
			
		||||
- [ ] Add more converters:
 | 
			
		||||
  - [ ] [deark](https://github.com/jsummers/deark)
 | 
			
		||||
  - [ ] LibreOffice
 | 
			
		||||
  - [ ] [dvisvgm](https://github.com/mgieseki/dvisvgm)
 | 
			
		||||
Use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) for commit messages.
 | 
			
		||||
 | 
			
		||||
## Contributors
 | 
			
		||||
 | 
			
		||||
<a href="https://github.com/C4illin/ConvertX/graphs/contributors">
 | 
			
		||||
  <img src="https://contrib.rocks/image?repo=C4illin/ConvertX" />
 | 
			
		||||
  <img src="https://contrib.rocks/image?repo=C4illin/ConvertX" alt="Image with all contributors"/>
 | 
			
		||||
</a>
 | 
			
		||||
 | 
			
		||||
## Star History
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										9
									
								
								SECURITY.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,9 @@
 | 
			
		||||
# Security Policy
 | 
			
		||||
 | 
			
		||||
## Supported Versions
 | 
			
		||||
 | 
			
		||||
Only the latest release is supported
 | 
			
		||||
 | 
			
		||||
## Reporting a Vulnerability
 | 
			
		||||
 | 
			
		||||
To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/C4illin/ConvertX/security/advisories/new) tab.
 | 
			
		||||
							
								
								
									
										15
									
								
								biome.json
									
									
									
									
									
								
							
							
						
						@@ -10,9 +10,11 @@
 | 
			
		||||
    "attributePosition": "auto"
 | 
			
		||||
  },
 | 
			
		||||
  "files": {
 | 
			
		||||
    "ignore": ["**/node_modules/**"]
 | 
			
		||||
    "ignore": ["**/node_modules/**", "**/pico.lime.min.css"]
 | 
			
		||||
  },
 | 
			
		||||
  "organizeImports": {
 | 
			
		||||
    "enabled": true
 | 
			
		||||
  },
 | 
			
		||||
  "organizeImports": { "enabled": true },
 | 
			
		||||
  "linter": {
 | 
			
		||||
    "enabled": true,
 | 
			
		||||
    "rules": {
 | 
			
		||||
@@ -25,7 +27,11 @@
 | 
			
		||||
        "useLiteralKeys": "error",
 | 
			
		||||
        "useOptionalChain": "error"
 | 
			
		||||
      },
 | 
			
		||||
      "correctness": { "noPrecisionLoss": "error", "noUnusedVariables": "off" },
 | 
			
		||||
      "correctness": {
 | 
			
		||||
        "noPrecisionLoss": "error",
 | 
			
		||||
        "noUnusedVariables": "off",
 | 
			
		||||
        "useJsxKeyInIterable": "off"
 | 
			
		||||
      },
 | 
			
		||||
      "style": {
 | 
			
		||||
        "noInferrableTypes": "error",
 | 
			
		||||
        "noNamespace": "error",
 | 
			
		||||
@@ -45,6 +51,9 @@
 | 
			
		||||
        "noUnsafeDeclarationMerging": "error",
 | 
			
		||||
        "useAwait": "error",
 | 
			
		||||
        "useNamespaceKeyword": "error"
 | 
			
		||||
      },
 | 
			
		||||
      "nursery": {
 | 
			
		||||
        "useSortedClasses": "error"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										733
									
								
								bun.lock
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,733 @@
 | 
			
		||||
{
 | 
			
		||||
  "lockfileVersion": 1,
 | 
			
		||||
  "workspaces": {
 | 
			
		||||
    "": {
 | 
			
		||||
      "name": "convertx-frontend",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@elysiajs/html": "^1.3.1",
 | 
			
		||||
        "@elysiajs/jwt": "^1.3.2",
 | 
			
		||||
        "@elysiajs/static": "^1.3.0",
 | 
			
		||||
        "@kitajs/html": "^4.2.9",
 | 
			
		||||
        "elysia": "^1.3.8",
 | 
			
		||||
        "sanitize-filename": "^1.6.3",
 | 
			
		||||
        "tar": "^7.4.3",
 | 
			
		||||
      },
 | 
			
		||||
      "devDependencies": {
 | 
			
		||||
        "@eslint/js": "^9.33.0",
 | 
			
		||||
        "@ianvs/prettier-plugin-sort-imports": "^4.6.2",
 | 
			
		||||
        "@kitajs/ts-html-plugin": "^4.1.2",
 | 
			
		||||
        "@tailwindcss/cli": "^4.1.11",
 | 
			
		||||
        "@tailwindcss/postcss": "^4.1.11",
 | 
			
		||||
        "@total-typescript/ts-reset": "^0.6.1",
 | 
			
		||||
        "@types/bun": "latest",
 | 
			
		||||
        "@types/node": "^24.2.1",
 | 
			
		||||
        "@typescript-eslint/parser": "^8.39.1",
 | 
			
		||||
        "eslint": "^9.33.0",
 | 
			
		||||
        "eslint-plugin-better-tailwindcss": "^3.7.4",
 | 
			
		||||
        "eslint-plugin-simple-import-sort": "^12.1.1",
 | 
			
		||||
        "globals": "^16.3.0",
 | 
			
		||||
        "knip": "^5.62.0",
 | 
			
		||||
        "npm-run-all2": "^8.0.4",
 | 
			
		||||
        "postcss": "^8.5.6",
 | 
			
		||||
        "prettier": "^3.6.2",
 | 
			
		||||
        "tailwind-scrollbar": "^4.0.2",
 | 
			
		||||
        "tailwindcss": "^4.1.11",
 | 
			
		||||
        "typescript": "^5.9.2",
 | 
			
		||||
        "typescript-eslint": "^8.39.1",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  "trustedDependencies": [
 | 
			
		||||
    "@tailwindcss/oxide",
 | 
			
		||||
    "@parcel/watcher",
 | 
			
		||||
  ],
 | 
			
		||||
  "packages": {
 | 
			
		||||
    "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
 | 
			
		||||
 | 
			
		||||
    "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/generator": ["@babel/generator@7.26.5", "", { "dependencies": { "@babel/parser": "^7.26.5", "@babel/types": "^7.26.5", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/parser": ["@babel/parser@7.26.7", "", { "dependencies": { "@babel/types": "^7.26.7" }, "bin": "./bin/babel-parser.js" }, "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/template": ["@babel/template@7.25.9", "", { "dependencies": { "@babel/code-frame": "^7.25.9", "@babel/parser": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/traverse": ["@babel/traverse@7.26.7", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.26.5", "@babel/parser": "^7.26.7", "@babel/template": "^7.25.9", "@babel/types": "^7.26.7", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/types": ["@babel/types@7.26.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg=="],
 | 
			
		||||
 | 
			
		||||
    "@elysiajs/html": ["@elysiajs/html@1.3.1", "", { "dependencies": { "@kitajs/html": "^4.1.0", "@kitajs/ts-html-plugin": "^4.0.1" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-jOWUfvL9vZ2Gs3uCx2w4Po+jxOwRD/sXW3JgvOAD3rEjX0NuygwcvixtbONSzAH8lFhaDBbHAtmCfpue46X9IQ=="],
 | 
			
		||||
 | 
			
		||||
    "@elysiajs/jwt": ["@elysiajs/jwt@1.3.2", "", { "dependencies": { "jose": "^6.0.11" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-1Ysb+THWmwy/AKqn9Q1SaBeYK6f499VEVV0E+YifKQjadJT5W+0qKhncOdfqrb4NufUtd65BxULdPQGKJwYo1Q=="],
 | 
			
		||||
 | 
			
		||||
    "@elysiajs/static": ["@elysiajs/static@1.3.0", "", { "dependencies": { "node-cache": "^5.1.2" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-7mWlj2U/AZvH27IfRKqpUjDP1W9ZRldF9NmdnatFEtx0AOy7YYgyk0rt5hXrH6wPcR//2gO2Qy+k5rwswpEhJA=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/config-array": ["@eslint/config-array@0.21.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/config-helpers": ["@eslint/config-helpers@0.3.1", "", {}, "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/core": ["@eslint/core@0.15.2", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/css-tree": ["@eslint/css-tree@3.6.3", "", { "dependencies": { "mdn-data": "2.21.0", "source-map-js": "^1.0.1" } }, "sha512-M9iq4Brt/MG+5/B4Jrla5XZqaCgaHjfZyMSUJM3KNpBU61u8gMYg4TTaNTP/mUGR/rnRrVV7RXmh5qI4pIk0Yw=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/js": ["@eslint/js@9.33.0", "", {}, "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.5", "", { "dependencies": { "@eslint/core": "^0.15.2", "levn": "^0.4.1" } }, "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w=="],
 | 
			
		||||
 | 
			
		||||
    "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
 | 
			
		||||
 | 
			
		||||
    "@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="],
 | 
			
		||||
 | 
			
		||||
    "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
 | 
			
		||||
 | 
			
		||||
    "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.2", "", {}, "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ=="],
 | 
			
		||||
 | 
			
		||||
    "@ianvs/prettier-plugin-sort-imports": ["@ianvs/prettier-plugin-sort-imports@4.6.2", "", { "dependencies": { "@babel/generator": "^7.26.2", "@babel/parser": "^7.26.2", "@babel/traverse": "^7.25.9", "@babel/types": "^7.26.0", "semver": "^7.5.2" }, "peerDependencies": { "@prettier/plugin-oxc": "^0.0.4", "@vue/compiler-sfc": "2.7.x || 3.x", "content-tag": "^4.0.0", "prettier": "2 || 3 || ^4.0.0-0", "prettier-plugin-ember-template-tag": "^2.1.0" }, "optionalPeers": ["@prettier/plugin-oxc", "@vue/compiler-sfc", "content-tag", "prettier-plugin-ember-template-tag"] }, "sha512-kHiL1IghIodo43clNQaJJU2rPqXEioPG+Ink4/T5za46A0ggSNvIx4NM3hGgciQ2VpDaR/X8cTJIZDKRurWjPw=="],
 | 
			
		||||
 | 
			
		||||
    "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
 | 
			
		||||
 | 
			
		||||
    "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
 | 
			
		||||
 | 
			
		||||
    "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
 | 
			
		||||
 | 
			
		||||
    "@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="],
 | 
			
		||||
 | 
			
		||||
    "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
 | 
			
		||||
 | 
			
		||||
    "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
 | 
			
		||||
 | 
			
		||||
    "@kitajs/html": ["@kitajs/html@4.2.9", "", { "dependencies": { "csstype": "^3.1.3" } }, "sha512-FDHHf5Mi5nR0D+Btq86IV1O9XfsePVCiC5rwU4PXjw2aHja16FmIiwLZBO0CS16rJxKkibjMldyRLAW2ni2mzA=="],
 | 
			
		||||
 | 
			
		||||
    "@kitajs/ts-html-plugin": ["@kitajs/ts-html-plugin@4.1.2", "", { "dependencies": { "chalk": "^4.1.2", "tslib": "^2.8.1", "yargs": "^17.7.2" }, "peerDependencies": { "@kitajs/html": "^4.2.5", "typescript": "^5.6.2" }, "bin": { "ts-html-plugin": "dist/cli.js", "xss-scan": "dist/cli.js" } }, "sha512-XE9iIe93TELBdQSvNC3xxXOPDhkcK7on4Oi2HUKhln3jAc5hzn1o33uzjHCYhLeW36r/LXCT70beoXRCFcuTxQ=="],
 | 
			
		||||
 | 
			
		||||
    "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.11", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.9.0" } }, "sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA=="],
 | 
			
		||||
 | 
			
		||||
    "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
 | 
			
		||||
 | 
			
		||||
    "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
 | 
			
		||||
 | 
			
		||||
    "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-darwin-arm64": ["@oxc-resolver/binding-darwin-arm64@11.2.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ruKLkS+Dm/YIJaUhzEB7zPI+jh3EXxu0QnNV8I7t9jf0lpD2VnltuyRbhrbJEkksklZj//xCMyFFsILGjiU2Mg=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-darwin-x64": ["@oxc-resolver/binding-darwin-x64@11.2.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-0zhgNUm5bYezdSFOg3FYhtVP83bAq7FTV/3suGQDl/43MixfQG7+bl+hlrP4mz6WlD2SUb2u9BomnJWl1uey9w=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-freebsd-x64": ["@oxc-resolver/binding-freebsd-x64@11.2.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-SHOxfCcZV1axeIGfyeD1BkdLvfQgjmPy18tO0OUXGElcdScxD6MqU5rj/AVtiuBT+51GtFfOKlwl1+BdVwhD1A=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-linux-arm-gnueabihf": ["@oxc-resolver/binding-linux-arm-gnueabihf@11.2.0", "", { "os": "linux", "cpu": "arm" }, "sha512-mgEkYrJ+N90sgEDqEZ07zH+4I1D28WjqAhdzfW3aS2x2vynVpoY9jWfHuH8S62vZt3uATJrTKTRa8CjPWEsrdw=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-linux-arm64-gnu": ["@oxc-resolver/binding-linux-arm64-gnu@11.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-BhEzNLjn4HjP8+Q18D3/jeIDBxW7OgoJYIjw2CaaysnYneoTlij8hPTKxHfyqq4IGM3fFs9TLR/k338M3zkQ7g=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-linux-arm64-musl": ["@oxc-resolver/binding-linux-arm64-musl@11.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yxbMYUgRmN2V8x8XoxmD/Qq6aG7YIW3ToMDILfmcfeeRRVieEJ3DOWBT0JSE+YgrOy79OyFDH/1lO8VnqLmDQQ=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-linux-riscv64-gnu": ["@oxc-resolver/binding-linux-riscv64-gnu@11.2.0", "", { "os": "linux", "cpu": "none" }, "sha512-QG1UfgC2N2qhW1tOnDCgB/26vn1RCshR5sYPhMeaxO1gMQ3kEKbZ3QyBXxrG1IX5qsXYj5hPDJLDYNYUjRcOpg=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-linux-s390x-gnu": ["@oxc-resolver/binding-linux-s390x-gnu@11.2.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-uqTDsQdi6mrkSV1gvwbuT8jf/WFl6qVDVjNlx7IPSaAByrNiJfPrhTmH8b+Do58Dylz7QIRZgxQ8CHIZSyBUdg=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-linux-x64-gnu": ["@oxc-resolver/binding-linux-x64-gnu@11.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-GZdHXhJ7p6GaQg9MjRqLebwBf8BLvGIagccI6z5yMj4fV3LU4QuDfwSEERG+R6oQ/Su9672MBqWwncvKcKT68w=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-linux-x64-musl": ["@oxc-resolver/binding-linux-x64-musl@11.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-YBAC3GOicYznReG2twE7oFPSeK9Z1f507z1EYWKg6HpGYRYRlJyszViu7PrhMT85r/MumDTs429zm+CNqpFWOA=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-wasm32-wasi": ["@oxc-resolver/binding-wasm32-wasi@11.2.0", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.11" }, "cpu": "none" }, "sha512-+qlIg45CPVPy+Jn3vqU1zkxA/AAv6e/2Ax/ImX8usZa8Tr2JmQn/93bmSOOOnr9fXRV9d0n4JyqYzSWxWPYDEw=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-win32-arm64-msvc": ["@oxc-resolver/binding-win32-arm64-msvc@11.2.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-AI4KIpS8Zf6vwfOPk0uQPSC0pQ1m5HU4hCbtrgL21JgJSlnJaeEu3/aoOBB45AXKiExBU9R+CDR7aSnW7uhc5A=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@11.2.0", "", { "os": "win32", "cpu": "x64" }, "sha512-r19cQc7HaEJ76HFsMsbiKMTIV2YqFGSof8H5hB7e5Jkb/23Y8Isv1YrSzkDaGhcw02I/COsrPo+eEmjy35eFuA=="],
 | 
			
		||||
 | 
			
		||||
    "@parcel/watcher": ["@parcel/watcher@2.5.1", "", { "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", "micromatch": "^4.0.5", "node-addon-api": "^7.0.0" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-freebsd-x64": "2.5.1", "@parcel/watcher-linux-arm-glibc": "2.5.1", "@parcel/watcher-linux-arm-musl": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", "@parcel/watcher-linux-arm64-musl": "2.5.1", "@parcel/watcher-linux-x64-glibc": "2.5.1", "@parcel/watcher-linux-x64-musl": "2.5.1", "@parcel/watcher-win32-arm64": "2.5.1", "@parcel/watcher-win32-ia32": "2.5.1", "@parcel/watcher-win32-x64": "2.5.1" } }, "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg=="],
 | 
			
		||||
 | 
			
		||||
    "@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.1", "", { "os": "android", "cpu": "arm64" }, "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA=="],
 | 
			
		||||
 | 
			
		||||
    "@parcel/watcher-darwin-arm64": ["@parcel/watcher-darwin-arm64@2.5.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw=="],
 | 
			
		||||
 | 
			
		||||
    "@parcel/watcher-darwin-x64": ["@parcel/watcher-darwin-x64@2.5.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg=="],
 | 
			
		||||
 | 
			
		||||
    "@parcel/watcher-freebsd-x64": ["@parcel/watcher-freebsd-x64@2.5.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ=="],
 | 
			
		||||
 | 
			
		||||
    "@parcel/watcher-linux-arm-glibc": ["@parcel/watcher-linux-arm-glibc@2.5.1", "", { "os": "linux", "cpu": "arm" }, "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA=="],
 | 
			
		||||
 | 
			
		||||
    "@parcel/watcher-linux-arm-musl": ["@parcel/watcher-linux-arm-musl@2.5.1", "", { "os": "linux", "cpu": "arm" }, "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q=="],
 | 
			
		||||
 | 
			
		||||
    "@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w=="],
 | 
			
		||||
 | 
			
		||||
    "@parcel/watcher-linux-arm64-musl": ["@parcel/watcher-linux-arm64-musl@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg=="],
 | 
			
		||||
 | 
			
		||||
    "@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A=="],
 | 
			
		||||
 | 
			
		||||
    "@parcel/watcher-linux-x64-musl": ["@parcel/watcher-linux-x64-musl@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg=="],
 | 
			
		||||
 | 
			
		||||
    "@parcel/watcher-win32-arm64": ["@parcel/watcher-win32-arm64@2.5.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw=="],
 | 
			
		||||
 | 
			
		||||
    "@parcel/watcher-win32-ia32": ["@parcel/watcher-win32-ia32@2.5.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ=="],
 | 
			
		||||
 | 
			
		||||
    "@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="],
 | 
			
		||||
 | 
			
		||||
    "@pkgr/core": ["@pkgr/core@0.2.9", "", {}, "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA=="],
 | 
			
		||||
 | 
			
		||||
    "@sinclair/typebox": ["@sinclair/typebox@0.34.33", "", {}, "sha512-5HAV9exOMcXRUxo+9iYB5n09XxzCXnfy4VTNW4xnDv+FgjzAGY989C28BIdljKqmF+ZltUwujE3aossvcVtq6g=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/cli": ["@tailwindcss/cli@4.1.11", "", { "dependencies": { "@parcel/watcher": "^2.5.1", "@tailwindcss/node": "4.1.11", "@tailwindcss/oxide": "4.1.11", "enhanced-resolve": "^5.18.1", "mri": "^1.2.0", "picocolors": "^1.1.1", "tailwindcss": "4.1.11" }, "bin": { "tailwindcss": "dist/index.mjs" } }, "sha512-7RAFOrVaXCFz5ooEG36Kbh+sMJiI2j4+Ozp71smgjnLfBRu7DTfoq8DsTvzse2/6nDeo2M3vS/FGaxfDgr3rtQ=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/node": ["@tailwindcss/node@4.1.11", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.11" } }, "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.11", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.11", "@tailwindcss/oxide-darwin-arm64": "4.1.11", "@tailwindcss/oxide-darwin-x64": "4.1.11", "@tailwindcss/oxide-freebsd-x64": "4.1.11", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", "@tailwindcss/oxide-linux-x64-musl": "4.1.11", "@tailwindcss/oxide-wasm32-wasi": "4.1.11", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" } }, "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.11", "", { "os": "android", "cpu": "arm64" }, "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.11", "", { "os": "freebsd", "cpu": "x64" }, "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11", "", { "os": "linux", "cpu": "arm" }, "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.11", "", { "os": "linux", "cpu": "x64" }, "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.11", "", { "os": "linux", "cpu": "x64" }, "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.11", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@emnapi/wasi-threads": "^1.0.2", "@napi-rs/wasm-runtime": "^0.2.11", "@tybys/wasm-util": "^0.9.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.11", "", { "os": "win32", "cpu": "x64" }, "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.11", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.11", "@tailwindcss/oxide": "4.1.11", "postcss": "^8.4.41", "tailwindcss": "4.1.11" } }, "sha512-q/EAIIpF6WpLhKEuQSEVMZNMIY8KhWoAemZ9eylNAih9jxMGAYPPWBn3I9QL/2jZ+e7OEz/tZkX5HwbBR4HohA=="],
 | 
			
		||||
 | 
			
		||||
    "@tokenizer/inflate": ["@tokenizer/inflate@0.2.7", "", { "dependencies": { "debug": "^4.4.0", "fflate": "^0.8.2", "token-types": "^6.0.0" } }, "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg=="],
 | 
			
		||||
 | 
			
		||||
    "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
 | 
			
		||||
 | 
			
		||||
    "@total-typescript/ts-reset": ["@total-typescript/ts-reset@0.6.1", "", {}, "sha512-cka47fVSo6lfQDIATYqb/vO1nvFfbPw7uWLayIXIhGETj0wcOOlrlkobOMDNQOFr9QOafegUPq13V2+6vtD7yg=="],
 | 
			
		||||
 | 
			
		||||
    "@types/bun": ["@types/bun@1.2.20", "", { "dependencies": { "bun-types": "1.2.20" } }, "sha512-dX3RGzQ8+KgmMw7CsW4xT5ITBSCrSbfHc36SNT31EOUg/LA9JWq0VDdEXDRSe1InVWpd2yLUM1FUF/kEOyTzYA=="],
 | 
			
		||||
 | 
			
		||||
    "@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="],
 | 
			
		||||
 | 
			
		||||
    "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
 | 
			
		||||
 | 
			
		||||
    "@types/node": ["@types/node@24.2.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ=="],
 | 
			
		||||
 | 
			
		||||
    "@types/prismjs": ["@types/prismjs@1.26.5", "", {}, "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ=="],
 | 
			
		||||
 | 
			
		||||
    "@types/react": ["@types/react@19.1.10", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.39.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.39.1", "@typescript-eslint/type-utils": "8.39.1", "@typescript-eslint/utils": "8.39.1", "@typescript-eslint/visitor-keys": "8.39.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.39.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-yYegZ5n3Yr6eOcqgj2nJH8cH/ZZgF+l0YIdKILSDjYFRjgYQMgv/lRjV5Z7Up04b9VYUondt8EPMqg7kTWgJ2g=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/parser": ["@typescript-eslint/parser@8.39.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.39.1", "@typescript-eslint/types": "8.39.1", "@typescript-eslint/typescript-estree": "8.39.1", "@typescript-eslint/visitor-keys": "8.39.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.39.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.39.1", "@typescript-eslint/types": "^8.39.1", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-8fZxek3ONTwBu9ptw5nCKqZOSkXshZB7uAxuFF0J/wTMkKydjXCzqqga7MlFMpHi9DoG4BadhmTkITBcg8Aybw=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.39.1", "", { "dependencies": { "@typescript-eslint/types": "8.39.1", "@typescript-eslint/visitor-keys": "8.39.1" } }, "sha512-RkBKGBrjgskFGWuyUGz/EtD8AF/GW49S21J8dvMzpJitOF1slLEbbHnNEtAHtnDAnx8qDEdRrULRnWVx27wGBw=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.39.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ePUPGVtTMR8XMU2Hee8kD0Pu4NDE1CN9Q1sxGSGd/mbOtGZDM7pnhXNJnzW63zk/q+Z54zVzj44HtwXln5CvHA=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.39.1", "", { "dependencies": { "@typescript-eslint/types": "8.39.1", "@typescript-eslint/typescript-estree": "8.39.1", "@typescript-eslint/utils": "8.39.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-gu9/ahyatyAdQbKeHnhT4R+y3YLtqqHyvkfDxaBYk97EcbfChSJXyaJnIL3ygUv7OuZatePHmQvuH5ru0lnVeA=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/types": ["@typescript-eslint/types@8.39.1", "", {}, "sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.39.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.39.1", "@typescript-eslint/tsconfig-utils": "8.39.1", "@typescript-eslint/types": "8.39.1", "@typescript-eslint/visitor-keys": "8.39.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/utils": ["@typescript-eslint/utils@8.39.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.39.1", "@typescript-eslint/types": "8.39.1", "@typescript-eslint/typescript-estree": "8.39.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-VF5tZ2XnUSTuiqZFXCZfZs1cgkdd3O/sSYmdo2EpSyDlC86UM/8YytTmKnehOW3TGAlivqTDT6bS87B/GQ/jyg=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.39.1", "", { "dependencies": { "@typescript-eslint/types": "8.39.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A=="],
 | 
			
		||||
 | 
			
		||||
    "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
 | 
			
		||||
 | 
			
		||||
    "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
 | 
			
		||||
 | 
			
		||||
    "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
 | 
			
		||||
 | 
			
		||||
    "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
 | 
			
		||||
 | 
			
		||||
    "ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
 | 
			
		||||
 | 
			
		||||
    "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
 | 
			
		||||
 | 
			
		||||
    "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
 | 
			
		||||
 | 
			
		||||
    "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
 | 
			
		||||
 | 
			
		||||
    "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
 | 
			
		||||
 | 
			
		||||
    "bun-types": ["bun-types@1.2.20", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-pxTnQYOrKvdOwyiyd/7sMt9yFOenN004Y6O4lCcCUoKVej48FS5cvTw9geRaEcB9TsDZaJKAxPTVvi8tFsVuXA=="],
 | 
			
		||||
 | 
			
		||||
    "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
 | 
			
		||||
 | 
			
		||||
    "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
 | 
			
		||||
 | 
			
		||||
    "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
 | 
			
		||||
 | 
			
		||||
    "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
 | 
			
		||||
 | 
			
		||||
    "clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="],
 | 
			
		||||
 | 
			
		||||
    "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
 | 
			
		||||
 | 
			
		||||
    "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
 | 
			
		||||
 | 
			
		||||
    "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
 | 
			
		||||
 | 
			
		||||
    "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
 | 
			
		||||
 | 
			
		||||
    "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
 | 
			
		||||
 | 
			
		||||
    "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
 | 
			
		||||
 | 
			
		||||
    "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
 | 
			
		||||
 | 
			
		||||
    "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
 | 
			
		||||
 | 
			
		||||
    "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
 | 
			
		||||
 | 
			
		||||
    "detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
 | 
			
		||||
 | 
			
		||||
    "elysia": ["elysia@1.3.8", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.1.3", "fast-decode-uri-component": "^1.0.1" }, "optionalDependencies": { "@sinclair/typebox": "^0.34.33", "openapi-types": "^12.1.3" }, "peerDependencies": { "file-type": ">= 20.0.0", "typescript": ">= 5.0.0" } }, "sha512-kxYFhegJbUEf5otzmisEvGt3R7d/dPBNVERO2nHo0kFqKBHyj5slArc90mSRKLfi1vamMtPcz67rL6Zeg5F2yg=="],
 | 
			
		||||
 | 
			
		||||
    "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
 | 
			
		||||
 | 
			
		||||
    "enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="],
 | 
			
		||||
 | 
			
		||||
    "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
 | 
			
		||||
 | 
			
		||||
    "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
 | 
			
		||||
 | 
			
		||||
    "eslint": ["eslint@9.33.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.33.0", "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA=="],
 | 
			
		||||
 | 
			
		||||
    "eslint-plugin-better-tailwindcss": ["eslint-plugin-better-tailwindcss@3.7.4", "", { "dependencies": { "@eslint/css-tree": "^3.6.3", "enhanced-resolve": "^5.18.2", "jiti": "^2.5.1", "postcss": "^8.5.6", "postcss-import": "^16.1.1", "synckit": "^0.11.11", "tailwind-csstree": "^0.1.2", "tsconfig-paths-webpack-plugin": "^4.2.0" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0", "tailwindcss": "^3.3.0 || ^4.1.6" } }, "sha512-jlLHLoqrNbcqqROVjFojGDv1m2LGiJwFqynbARbyeRj9rc1Hmh46EeQhmYVQihhD4j+DSxG/bcwoA9PABIpLmw=="],
 | 
			
		||||
 | 
			
		||||
    "eslint-plugin-simple-import-sort": ["eslint-plugin-simple-import-sort@12.1.1", "", { "peerDependencies": { "eslint": ">=5.0.0" } }, "sha512-6nuzu4xwQtE3332Uz0to+TxDQYRLTKRESSc2hefVT48Zc8JthmN23Gx9lnYhu0FtkRSL1oxny3kJ2aveVhmOVA=="],
 | 
			
		||||
 | 
			
		||||
    "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
 | 
			
		||||
 | 
			
		||||
    "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
 | 
			
		||||
 | 
			
		||||
    "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
 | 
			
		||||
 | 
			
		||||
    "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="],
 | 
			
		||||
 | 
			
		||||
    "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
 | 
			
		||||
 | 
			
		||||
    "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
 | 
			
		||||
 | 
			
		||||
    "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
 | 
			
		||||
 | 
			
		||||
    "exact-mirror": ["exact-mirror@0.1.3", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-yI62LpSby0ItzPJF05C4DRycVAoknRiCIDOLOCCs9zaEKylOXQtOFM3flX54S44swpRz584vk3P70yWQodsLlg=="],
 | 
			
		||||
 | 
			
		||||
    "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
 | 
			
		||||
 | 
			
		||||
    "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
 | 
			
		||||
 | 
			
		||||
    "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
 | 
			
		||||
 | 
			
		||||
    "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
 | 
			
		||||
 | 
			
		||||
    "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
 | 
			
		||||
 | 
			
		||||
    "fastq": ["fastq@1.19.0", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA=="],
 | 
			
		||||
 | 
			
		||||
    "fd-package-json": ["fd-package-json@2.0.0", "", { "dependencies": { "walk-up-path": "^4.0.0" } }, "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ=="],
 | 
			
		||||
 | 
			
		||||
    "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
 | 
			
		||||
 | 
			
		||||
    "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
 | 
			
		||||
 | 
			
		||||
    "file-type": ["file-type@20.5.0", "", { "dependencies": { "@tokenizer/inflate": "^0.2.6", "strtok3": "^10.2.0", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-BfHZtG/l9iMm4Ecianu7P8HRD2tBHLtjXinm4X62XBOYzi7CYA7jyqfJzOvXHqzVrVPYqBo2/GvbARMaaJkKVg=="],
 | 
			
		||||
 | 
			
		||||
    "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
 | 
			
		||||
 | 
			
		||||
    "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
 | 
			
		||||
 | 
			
		||||
    "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
 | 
			
		||||
 | 
			
		||||
    "flatted": ["flatted@3.3.2", "", {}, "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA=="],
 | 
			
		||||
 | 
			
		||||
    "formatly": ["formatly@0.2.4", "", { "dependencies": { "fd-package-json": "^2.0.0" }, "bin": { "formatly": "bin/index.mjs" } }, "sha512-lIN7GpcvX/l/i24r/L9bnJ0I8Qn01qijWpQpDDvTLL29nKqSaJJu4h20+7VJ6m2CAhQ2/En/GbxDiHCzq/0MyA=="],
 | 
			
		||||
 | 
			
		||||
    "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
 | 
			
		||||
 | 
			
		||||
    "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
 | 
			
		||||
 | 
			
		||||
    "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
 | 
			
		||||
 | 
			
		||||
    "globals": ["globals@16.3.0", "", {}, "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ=="],
 | 
			
		||||
 | 
			
		||||
    "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
 | 
			
		||||
 | 
			
		||||
    "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
 | 
			
		||||
 | 
			
		||||
    "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
 | 
			
		||||
 | 
			
		||||
    "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
 | 
			
		||||
 | 
			
		||||
    "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
 | 
			
		||||
 | 
			
		||||
    "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
 | 
			
		||||
 | 
			
		||||
    "import-fresh": ["import-fresh@3.3.0", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw=="],
 | 
			
		||||
 | 
			
		||||
    "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
 | 
			
		||||
 | 
			
		||||
    "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
 | 
			
		||||
 | 
			
		||||
    "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
 | 
			
		||||
 | 
			
		||||
    "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
 | 
			
		||||
 | 
			
		||||
    "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
 | 
			
		||||
 | 
			
		||||
    "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
 | 
			
		||||
 | 
			
		||||
    "isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="],
 | 
			
		||||
 | 
			
		||||
    "jiti": ["jiti@2.5.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w=="],
 | 
			
		||||
 | 
			
		||||
    "jose": ["jose@6.0.11", "", {}, "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg=="],
 | 
			
		||||
 | 
			
		||||
    "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
 | 
			
		||||
 | 
			
		||||
    "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
 | 
			
		||||
 | 
			
		||||
    "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
 | 
			
		||||
 | 
			
		||||
    "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
 | 
			
		||||
 | 
			
		||||
    "json-parse-even-better-errors": ["json-parse-even-better-errors@4.0.0", "", {}, "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA=="],
 | 
			
		||||
 | 
			
		||||
    "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
 | 
			
		||||
 | 
			
		||||
    "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
 | 
			
		||||
 | 
			
		||||
    "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
 | 
			
		||||
 | 
			
		||||
    "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
 | 
			
		||||
 | 
			
		||||
    "knip": ["knip@5.62.0", "", { "dependencies": { "@nodelib/fs.walk": "^1.2.3", "fast-glob": "^3.3.3", "formatly": "^0.2.4", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "minimist": "^1.2.8", "oxc-resolver": "^11.1.0", "picocolors": "^1.1.1", "picomatch": "^4.0.1", "smol-toml": "^1.3.4", "strip-json-comments": "5.0.2", "zod": "^3.22.4", "zod-validation-error": "^3.0.3" }, "peerDependencies": { "@types/node": ">=18", "typescript": ">=5.0.4" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-hfTUVzmrMNMT1khlZfAYmBABeehwWUUrizLQoLamoRhSFkygsGIXWx31kaWKBgEaIVL77T3Uz7IxGvSw+CvQ6A=="],
 | 
			
		||||
 | 
			
		||||
    "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
 | 
			
		||||
 | 
			
		||||
    "lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="],
 | 
			
		||||
 | 
			
		||||
    "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="],
 | 
			
		||||
 | 
			
		||||
    "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="],
 | 
			
		||||
 | 
			
		||||
    "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig=="],
 | 
			
		||||
 | 
			
		||||
    "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q=="],
 | 
			
		||||
 | 
			
		||||
    "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw=="],
 | 
			
		||||
 | 
			
		||||
    "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ=="],
 | 
			
		||||
 | 
			
		||||
    "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw=="],
 | 
			
		||||
 | 
			
		||||
    "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ=="],
 | 
			
		||||
 | 
			
		||||
    "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA=="],
 | 
			
		||||
 | 
			
		||||
    "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="],
 | 
			
		||||
 | 
			
		||||
    "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
 | 
			
		||||
 | 
			
		||||
    "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
 | 
			
		||||
 | 
			
		||||
    "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
 | 
			
		||||
 | 
			
		||||
    "mdn-data": ["mdn-data@2.21.0", "", {}, "sha512-+ZKPQezM5vYJIkCxaC+4DTnRrVZR1CgsKLu5zsQERQx6Tea8Y+wMx5A24rq8A8NepCeatIQufVAekKNgiBMsGQ=="],
 | 
			
		||||
 | 
			
		||||
    "memorystream": ["memorystream@0.3.1", "", {}, "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw=="],
 | 
			
		||||
 | 
			
		||||
    "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
 | 
			
		||||
 | 
			
		||||
    "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
 | 
			
		||||
 | 
			
		||||
    "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
 | 
			
		||||
 | 
			
		||||
    "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
 | 
			
		||||
 | 
			
		||||
    "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
 | 
			
		||||
 | 
			
		||||
    "minizlib": ["minizlib@3.0.2", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA=="],
 | 
			
		||||
 | 
			
		||||
    "mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="],
 | 
			
		||||
 | 
			
		||||
    "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
 | 
			
		||||
 | 
			
		||||
    "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=="],
 | 
			
		||||
 | 
			
		||||
    "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
 | 
			
		||||
 | 
			
		||||
    "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
 | 
			
		||||
 | 
			
		||||
    "node-cache": ["node-cache@5.1.2", "", { "dependencies": { "clone": "2.x" } }, "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg=="],
 | 
			
		||||
 | 
			
		||||
    "npm-normalize-package-bin": ["npm-normalize-package-bin@4.0.0", "", {}, "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w=="],
 | 
			
		||||
 | 
			
		||||
    "npm-run-all2": ["npm-run-all2@8.0.4", "", { "dependencies": { "ansi-styles": "^6.2.1", "cross-spawn": "^7.0.6", "memorystream": "^0.3.1", "picomatch": "^4.0.2", "pidtree": "^0.6.0", "read-package-json-fast": "^4.0.0", "shell-quote": "^1.7.3", "which": "^5.0.0" }, "bin": { "run-p": "bin/run-p/index.js", "run-s": "bin/run-s/index.js", "npm-run-all": "bin/npm-run-all/index.js", "npm-run-all2": "bin/npm-run-all/index.js" } }, "sha512-wdbB5My48XKp2ZfJUlhnLVihzeuA1hgBnqB2J9ahV77wLS+/YAJAlN8I+X3DIFIPZ3m5L7nplmlbhNiFDmXRDA=="],
 | 
			
		||||
 | 
			
		||||
    "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
 | 
			
		||||
 | 
			
		||||
    "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
 | 
			
		||||
 | 
			
		||||
    "oxc-resolver": ["oxc-resolver@11.2.0", "", { "optionalDependencies": { "@oxc-resolver/binding-darwin-arm64": "11.2.0", "@oxc-resolver/binding-darwin-x64": "11.2.0", "@oxc-resolver/binding-freebsd-x64": "11.2.0", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.2.0", "@oxc-resolver/binding-linux-arm64-gnu": "11.2.0", "@oxc-resolver/binding-linux-arm64-musl": "11.2.0", "@oxc-resolver/binding-linux-riscv64-gnu": "11.2.0", "@oxc-resolver/binding-linux-s390x-gnu": "11.2.0", "@oxc-resolver/binding-linux-x64-gnu": "11.2.0", "@oxc-resolver/binding-linux-x64-musl": "11.2.0", "@oxc-resolver/binding-wasm32-wasi": "11.2.0", "@oxc-resolver/binding-win32-arm64-msvc": "11.2.0", "@oxc-resolver/binding-win32-x64-msvc": "11.2.0" } }, "sha512-3iJYyIdDZMDoj0ZSVBrI1gUvPBMkDC4gxonBG+7uqUyK5EslG0mCwnf6qhxK8oEU7jLHjbRBNyzflPSd3uvH7Q=="],
 | 
			
		||||
 | 
			
		||||
    "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
 | 
			
		||||
 | 
			
		||||
    "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
 | 
			
		||||
 | 
			
		||||
    "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
 | 
			
		||||
 | 
			
		||||
    "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
 | 
			
		||||
 | 
			
		||||
    "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
 | 
			
		||||
 | 
			
		||||
    "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
 | 
			
		||||
 | 
			
		||||
    "peek-readable": ["peek-readable@7.0.0", "", {}, "sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ=="],
 | 
			
		||||
 | 
			
		||||
    "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
 | 
			
		||||
 | 
			
		||||
    "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
 | 
			
		||||
 | 
			
		||||
    "pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="],
 | 
			
		||||
 | 
			
		||||
    "pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="],
 | 
			
		||||
 | 
			
		||||
    "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=="],
 | 
			
		||||
 | 
			
		||||
    "postcss-import": ["postcss-import@16.1.1", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-2xVS1NCZAfjtVdvXiyegxzJ447GyqCeEI5V7ApgQVOWnros1p5lGNovJNapwPpMombyFBfqDwt7AD3n2l0KOfQ=="],
 | 
			
		||||
 | 
			
		||||
    "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
 | 
			
		||||
 | 
			
		||||
    "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
 | 
			
		||||
 | 
			
		||||
    "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
 | 
			
		||||
 | 
			
		||||
    "prism-react-renderer": ["prism-react-renderer@2.4.1", "", { "dependencies": { "@types/prismjs": "^1.26.0", "clsx": "^2.0.0" }, "peerDependencies": { "react": ">=16.0.0" } }, "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig=="],
 | 
			
		||||
 | 
			
		||||
    "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
 | 
			
		||||
 | 
			
		||||
    "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
 | 
			
		||||
 | 
			
		||||
    "react": ["react@19.0.0", "", {}, "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="],
 | 
			
		||||
 | 
			
		||||
    "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="],
 | 
			
		||||
 | 
			
		||||
    "read-package-json-fast": ["read-package-json-fast@4.0.0", "", { "dependencies": { "json-parse-even-better-errors": "^4.0.0", "npm-normalize-package-bin": "^4.0.0" } }, "sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg=="],
 | 
			
		||||
 | 
			
		||||
    "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
 | 
			
		||||
 | 
			
		||||
    "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
 | 
			
		||||
 | 
			
		||||
    "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
 | 
			
		||||
 | 
			
		||||
    "reusify": ["reusify@1.0.4", "", {}, "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="],
 | 
			
		||||
 | 
			
		||||
    "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
 | 
			
		||||
 | 
			
		||||
    "sanitize-filename": ["sanitize-filename@1.6.3", "", { "dependencies": { "truncate-utf8-bytes": "^1.0.0" } }, "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg=="],
 | 
			
		||||
 | 
			
		||||
    "semver": ["semver@7.7.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ=="],
 | 
			
		||||
 | 
			
		||||
    "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
 | 
			
		||||
 | 
			
		||||
    "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
 | 
			
		||||
 | 
			
		||||
    "shell-quote": ["shell-quote@1.8.2", "", {}, "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA=="],
 | 
			
		||||
 | 
			
		||||
    "smol-toml": ["smol-toml@1.3.4", "", {}, "sha512-UOPtVuYkzYGee0Bd2Szz8d2G3RfMfJ2t3qVdZUAozZyAk+a0Sxa+QKix0YCwjL/A1RR0ar44nCxaoN9FxdJGwA=="],
 | 
			
		||||
 | 
			
		||||
    "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
 | 
			
		||||
 | 
			
		||||
    "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
 | 
			
		||||
 | 
			
		||||
    "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
 | 
			
		||||
 | 
			
		||||
    "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
 | 
			
		||||
 | 
			
		||||
    "strip-json-comments": ["strip-json-comments@5.0.2", "", {}, "sha512-4X2FR3UwhNUE9G49aIsJW5hRRR3GXGTBTZRMfv568O60ojM8HcWjV/VxAxCDW3SUND33O6ZY66ZuRcdkj73q2g=="],
 | 
			
		||||
 | 
			
		||||
    "strtok3": ["strtok3@10.2.2", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^7.0.0" } }, "sha512-Xt18+h4s7Z8xyZ0tmBoRmzxcop97R4BAh+dXouUDCYn+Em+1P3qpkUfI5ueWLT8ynC5hZ+q4iPEmGG1urvQGBg=="],
 | 
			
		||||
 | 
			
		||||
    "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
 | 
			
		||||
 | 
			
		||||
    "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
 | 
			
		||||
 | 
			
		||||
    "synckit": ["synckit@0.11.11", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw=="],
 | 
			
		||||
 | 
			
		||||
    "tailwind-csstree": ["tailwind-csstree@0.1.3", "", {}, "sha512-LfOT807005OVfyxAjHpOajlIgoEaE894jqjkrhONC/HqBLS8OAhhNifnNs3Y5wD26eIdf0vk1zu9gja2oI3/1Q=="],
 | 
			
		||||
 | 
			
		||||
    "tailwind-scrollbar": ["tailwind-scrollbar@4.0.2", "", { "dependencies": { "prism-react-renderer": "^2.4.1" }, "peerDependencies": { "tailwindcss": "4.x" } }, "sha512-wAQiIxAPqk0MNTPptVe/xoyWi27y+NRGnTwvn4PQnbvB9kp8QUBiGl/wsfoVBHnQxTmhXJSNt9NHTmcz9EivFA=="],
 | 
			
		||||
 | 
			
		||||
    "tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="],
 | 
			
		||||
 | 
			
		||||
    "tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="],
 | 
			
		||||
 | 
			
		||||
    "tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="],
 | 
			
		||||
 | 
			
		||||
    "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
 | 
			
		||||
 | 
			
		||||
    "token-types": ["token-types@6.0.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA=="],
 | 
			
		||||
 | 
			
		||||
    "truncate-utf8-bytes": ["truncate-utf8-bytes@1.0.2", "", { "dependencies": { "utf8-byte-length": "^1.0.1" } }, "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ=="],
 | 
			
		||||
 | 
			
		||||
    "ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="],
 | 
			
		||||
 | 
			
		||||
    "tsconfig-paths": ["tsconfig-paths@4.2.0", "", { "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg=="],
 | 
			
		||||
 | 
			
		||||
    "tsconfig-paths-webpack-plugin": ["tsconfig-paths-webpack-plugin@4.2.0", "", { "dependencies": { "chalk": "^4.1.0", "enhanced-resolve": "^5.7.0", "tapable": "^2.2.1", "tsconfig-paths": "^4.1.2" } }, "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA=="],
 | 
			
		||||
 | 
			
		||||
    "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
 | 
			
		||||
 | 
			
		||||
    "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
 | 
			
		||||
 | 
			
		||||
    "typescript-eslint": ["typescript-eslint@8.39.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.39.1", "@typescript-eslint/parser": "8.39.1", "@typescript-eslint/typescript-estree": "8.39.1", "@typescript-eslint/utils": "8.39.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-GDUv6/NDYngUlNvwaHM1RamYftxf782IyEDbdj3SeaIHHv8fNQVRC++fITT7kUJV/5rIA/tkoRSSskt6osEfqg=="],
 | 
			
		||||
 | 
			
		||||
    "uint8array-extras": ["uint8array-extras@1.4.0", "", {}, "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ=="],
 | 
			
		||||
 | 
			
		||||
    "undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
 | 
			
		||||
 | 
			
		||||
    "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
 | 
			
		||||
 | 
			
		||||
    "utf8-byte-length": ["utf8-byte-length@1.0.5", "", {}, "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA=="],
 | 
			
		||||
 | 
			
		||||
    "walk-up-path": ["walk-up-path@4.0.0", "", {}, "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A=="],
 | 
			
		||||
 | 
			
		||||
    "which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="],
 | 
			
		||||
 | 
			
		||||
    "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
 | 
			
		||||
 | 
			
		||||
    "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
 | 
			
		||||
 | 
			
		||||
    "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
 | 
			
		||||
 | 
			
		||||
    "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
 | 
			
		||||
 | 
			
		||||
    "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
 | 
			
		||||
 | 
			
		||||
    "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
 | 
			
		||||
 | 
			
		||||
    "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
 | 
			
		||||
 | 
			
		||||
    "zod": ["zod@3.24.4", "", {}, "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg=="],
 | 
			
		||||
 | 
			
		||||
    "zod-validation-error": ["zod-validation-error@3.4.0", "", { "peerDependencies": { "zod": "^3.18.0" } }, "sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ=="],
 | 
			
		||||
 | 
			
		||||
    "@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/eslintrc/espree": ["espree@10.3.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.0" } }, "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/eslintrc/strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
 | 
			
		||||
 | 
			
		||||
    "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
 | 
			
		||||
 | 
			
		||||
    "@napi-rs/wasm-runtime/@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="],
 | 
			
		||||
 | 
			
		||||
    "@napi-rs/wasm-runtime/@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="],
 | 
			
		||||
 | 
			
		||||
    "@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide/detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.11", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.9.0" }, "bundled": true }, "sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.4", "", {}, "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
 | 
			
		||||
 | 
			
		||||
    "chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
 | 
			
		||||
 | 
			
		||||
    "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
 | 
			
		||||
 | 
			
		||||
    "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
 | 
			
		||||
 | 
			
		||||
    "lightningcss/detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
 | 
			
		||||
 | 
			
		||||
    "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
 | 
			
		||||
 | 
			
		||||
    "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/eslintrc/espree/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/eslintrc/espree/eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="],
 | 
			
		||||
 | 
			
		||||
    "@napi-rs/wasm-runtime/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="],
 | 
			
		||||
 | 
			
		||||
    "@napi-rs/wasm-runtime/@emnapi/core/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@napi-rs/wasm-runtime/@emnapi/runtime/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@napi-rs/wasm-runtime/@tybys/wasm-util/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@emnapi/core/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
 | 
			
		||||
 | 
			
		||||
    "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
 | 
			
		||||
 | 
			
		||||
    "@napi-rs/wasm-runtime/@emnapi/core/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/runtime/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										15
									
								
								compose.yaml
									
									
									
									
									
								
							
							
						
						@@ -5,9 +5,16 @@ services:
 | 
			
		||||
      # dockerfile: Debian.Dockerfile
 | 
			
		||||
    volumes:
 | 
			
		||||
      - ./data:/app/data
 | 
			
		||||
    environment:
 | 
			
		||||
      - ACCOUNT_REGISTRATION=true
 | 
			
		||||
      - JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234
 | 
			
		||||
      - ALLOW_UNAUTHENTICATED=true
 | 
			
		||||
    environment: # Defaults are listed below. All are optional.
 | 
			
		||||
      - ACCOUNT_REGISTRATION=true # true or false, doesn't matter for the first account (e.g. keep this to false if you only want one account)
 | 
			
		||||
      - JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234 # will use randomUUID() by default
 | 
			
		||||
      - HTTP_ALLOWED=false # setting this to true is unsafe, only set this to true locally
 | 
			
		||||
      - ALLOW_UNAUTHENTICATED=false # allows anyone to use the service without logging in, only set this to true locally
 | 
			
		||||
      - AUTO_DELETE_EVERY_N_HOURS=1 # checks every n hours for files older then n hours and deletes them, set to 0 to disable
 | 
			
		||||
      # - FFMPEG_ARGS=-hwaccel vulkan # additional arguments to pass to ffmpeg
 | 
			
		||||
      # - WEBROOT=/convertx # the root path of the web interface, leave empty to disable
 | 
			
		||||
      # - HIDE_HISTORY=true # hides the history tab in the web interface, defaults to false
 | 
			
		||||
      - TZ=Europe/Stockholm # set your timezone, defaults to UTC
 | 
			
		||||
      # - UNAUTHENTICATED_USER_SHARING=true # for use with ALLOW_UNAUTHENTICATED=true to share history with all unauthenticated users / devices
 | 
			
		||||
    ports:
 | 
			
		||||
      - 3000:3000
 | 
			
		||||
 
 | 
			
		||||
@@ -1,36 +0,0 @@
 | 
			
		||||
import { fixupPluginRules } from "@eslint/compat";
 | 
			
		||||
import tseslint from "typescript-eslint";
 | 
			
		||||
import eslint from "@eslint/js";
 | 
			
		||||
import deprecationPlugin from "eslint-plugin-deprecation";
 | 
			
		||||
import eslintCommentsPlugin from "eslint-plugin-eslint-comments";
 | 
			
		||||
import importPlugin from "eslint-plugin-import";
 | 
			
		||||
import simpleImportSortPlugin from "eslint-plugin-simple-import-sort";
 | 
			
		||||
 | 
			
		||||
export default tseslint.config(
 | 
			
		||||
  {
 | 
			
		||||
    plugins: {
 | 
			
		||||
      "@typescript-eslint": tseslint.plugin,
 | 
			
		||||
      deprecation: fixupPluginRules(deprecationPlugin),
 | 
			
		||||
      "eslint-comments": eslintCommentsPlugin,
 | 
			
		||||
      import: fixupPluginRules(importPlugin),
 | 
			
		||||
      "simple-import-sort": simpleImportSortPlugin,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    ignores: ["**/node_modules/**", "**/public/**"],
 | 
			
		||||
  },
 | 
			
		||||
  eslint.configs.recommended,
 | 
			
		||||
  ...tseslint.configs.recommendedTypeChecked,
 | 
			
		||||
  ...tseslint.configs.stylisticTypeChecked,
 | 
			
		||||
  {
 | 
			
		||||
    languageOptions: {
 | 
			
		||||
      parserOptions: {
 | 
			
		||||
        projectService: true,
 | 
			
		||||
        tsconfigRootDir: import.meta.dirname,
 | 
			
		||||
        ecmaVersion: "latest",
 | 
			
		||||
        sourceType: "module",
 | 
			
		||||
        project: ["./tsconfig.json"],
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
							
								
								
									
										72
									
								
								eslint.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,72 @@
 | 
			
		||||
import js from "@eslint/js";
 | 
			
		||||
import eslintParserTypeScript from "@typescript-eslint/parser";
 | 
			
		||||
import eslintPluginBetterTailwindcss from "eslint-plugin-better-tailwindcss";
 | 
			
		||||
import simpleImportSortPlugin from "eslint-plugin-simple-import-sort";
 | 
			
		||||
import globals from "globals";
 | 
			
		||||
import tseslint from "typescript-eslint";
 | 
			
		||||
 | 
			
		||||
export default tseslint.config(
 | 
			
		||||
  js.configs.recommended,
 | 
			
		||||
  tseslint.configs.recommended,
 | 
			
		||||
  {
 | 
			
		||||
    plugins: {
 | 
			
		||||
      "simple-import-sort": simpleImportSortPlugin,
 | 
			
		||||
      "better-tailwindcss": eslintPluginBetterTailwindcss,
 | 
			
		||||
    },
 | 
			
		||||
    ignores: ["**/node_modules/**", "eslint.config.ts"],
 | 
			
		||||
    languageOptions: {
 | 
			
		||||
      parser: eslintParserTypeScript,
 | 
			
		||||
      parserOptions: {
 | 
			
		||||
        project: true,
 | 
			
		||||
        ecmaFeatures: {
 | 
			
		||||
          jsx: true,
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      globals: {
 | 
			
		||||
        ...globals.node,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    files: ["**/*.{tsx,ts}"],
 | 
			
		||||
    settings: {
 | 
			
		||||
      "better-tailwindcss": {
 | 
			
		||||
        entryPoint: "src/main.css",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    rules: {
 | 
			
		||||
      ...(eslintPluginBetterTailwindcss.configs["recommended-warn"] ?? {}).rules,
 | 
			
		||||
      ...(eslintPluginBetterTailwindcss.configs["stylistic-warn"] ?? {}).rules,
 | 
			
		||||
      // "tailwindcss/classnames-order": "off",
 | 
			
		||||
      "better-tailwindcss/enforce-consistent-line-wrapping": [
 | 
			
		||||
        "warn",
 | 
			
		||||
        {
 | 
			
		||||
          group: "newLine",
 | 
			
		||||
          printWidth: 100,
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
      "better-tailwindcss/no-unregistered-classes": [
 | 
			
		||||
        "warn",
 | 
			
		||||
        {
 | 
			
		||||
          ignore: [
 | 
			
		||||
            "^group(?:\\/(\\S*))?$",
 | 
			
		||||
            "^peer(?:\\/(\\S*))?$",
 | 
			
		||||
            "select_container",
 | 
			
		||||
            "convert_to_popup",
 | 
			
		||||
            "convert_to_group",
 | 
			
		||||
            "target",
 | 
			
		||||
            "convert_to_target",
 | 
			
		||||
            "job-details-toggle",
 | 
			
		||||
          ],
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    files: ["**/*.{js,cjs,mjs,jsx}"],
 | 
			
		||||
    extends: [tseslint.configs.disableTypeChecked],
 | 
			
		||||
    languageOptions: {
 | 
			
		||||
      globals: {
 | 
			
		||||
        ...globals.browser,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								images/preview.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 53 KiB  | 
							
								
								
									
										9
									
								
								knip.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,9 @@
 | 
			
		||||
{
 | 
			
		||||
  "$schema": "https://unpkg.com/knip@5/schema.json",
 | 
			
		||||
  "entry": ["tests/**/*.test.ts"],
 | 
			
		||||
  "project": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts"],
 | 
			
		||||
  "tailwind": {
 | 
			
		||||
    "entry": ["src/main.css"]
 | 
			
		||||
  },
 | 
			
		||||
  "ignoreDependencies": ["tailwind-scrollbar"]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										70
									
								
								package.json
									
									
									
									
									
								
							
							
						
						@@ -1,22 +1,28 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "convertx-frontend",
 | 
			
		||||
  "version": "0.4.1",
 | 
			
		||||
  "version": "0.15.0",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "dev": "bun run --watch src/index.tsx",
 | 
			
		||||
    "hot": "bun run --hot src/index.tsx",
 | 
			
		||||
    "format": "biome format --write ./src",
 | 
			
		||||
    "css": "cpy 'node_modules/@picocss/pico/css/pico.lime.min.css' 'src/public/' --flat",
 | 
			
		||||
    "format": "run-p 'format:*'",
 | 
			
		||||
    "format:eslint": "eslint --fix .",
 | 
			
		||||
    "format:prettier": "prettier --write .",
 | 
			
		||||
    "build:js": "tsc",
 | 
			
		||||
    "build": "bun x @tailwindcss/cli -i ./src/main.css -o ./public/generated.css && bun run build:js",
 | 
			
		||||
    "lint": "run-p 'lint:*'",
 | 
			
		||||
    "lint:tsc": "tsc --noEmit",
 | 
			
		||||
    "lint:knip": "knip",
 | 
			
		||||
    "lint:biome": "biome lint --error-on-warnings ./src"
 | 
			
		||||
    "lint:eslint": "eslint .",
 | 
			
		||||
    "lint:prettier": "prettier --check ."
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@elysiajs/cookie": "^0.8.0",
 | 
			
		||||
    "@elysiajs/html": "1.0.2",
 | 
			
		||||
    "@elysiajs/jwt": "^1.1.1",
 | 
			
		||||
    "@elysiajs/static": "1.0.3",
 | 
			
		||||
    "elysia": "^1.1.12"
 | 
			
		||||
    "@elysiajs/html": "^1.3.1",
 | 
			
		||||
    "@elysiajs/jwt": "^1.3.2",
 | 
			
		||||
    "@elysiajs/static": "^1.3.0",
 | 
			
		||||
    "@kitajs/html": "^4.2.9",
 | 
			
		||||
    "elysia": "^1.3.8",
 | 
			
		||||
    "sanitize-filename": "^1.6.3",
 | 
			
		||||
    "tar": "^7.4.3"
 | 
			
		||||
  },
 | 
			
		||||
  "module": "src/index.tsx",
 | 
			
		||||
  "type": "module",
 | 
			
		||||
@@ -24,34 +30,30 @@
 | 
			
		||||
    "start": "bun run src/index.tsx"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@biomejs/biome": "1.9.1",
 | 
			
		||||
    "@eslint/compat": "^1.1.1",
 | 
			
		||||
    "@eslint/js": "^9.9.1",
 | 
			
		||||
    "@ianvs/prettier-plugin-sort-imports": "^4.3.1",
 | 
			
		||||
    "@kitajs/ts-html-plugin": "^4.0.2",
 | 
			
		||||
    "@picocss/pico": "^2.0.6",
 | 
			
		||||
    "@eslint/js": "^9.33.0",
 | 
			
		||||
    "@ianvs/prettier-plugin-sort-imports": "^4.6.2",
 | 
			
		||||
    "@kitajs/ts-html-plugin": "^4.1.2",
 | 
			
		||||
    "@tailwindcss/cli": "^4.1.11",
 | 
			
		||||
    "@tailwindcss/postcss": "^4.1.11",
 | 
			
		||||
    "@total-typescript/ts-reset": "^0.6.1",
 | 
			
		||||
    "@types/bun": "^1.1.8",
 | 
			
		||||
    "@types/eslint": "^9.6.1",
 | 
			
		||||
    "@types/node": "^22.5.4",
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": "^8.4.0",
 | 
			
		||||
    "@typescript-eslint/parser": "^8.4.0",
 | 
			
		||||
    "cpy-cli": "^5.0.0",
 | 
			
		||||
    "eslint": "^9.9.1",
 | 
			
		||||
    "eslint-config-prettier": "^9.1.0",
 | 
			
		||||
    "eslint-plugin-deprecation": "^3.0.0",
 | 
			
		||||
    "eslint-plugin-eslint-comments": "^3.2.0",
 | 
			
		||||
    "eslint-plugin-import": "^2.30.0",
 | 
			
		||||
    "eslint-plugin-isaacscript": "^4.0.0",
 | 
			
		||||
    "eslint-plugin-prettier": "^5.2.1",
 | 
			
		||||
    "@types/bun": "latest",
 | 
			
		||||
    "@types/node": "^24.2.1",
 | 
			
		||||
    "@typescript-eslint/parser": "^8.39.1",
 | 
			
		||||
    "eslint": "^9.33.0",
 | 
			
		||||
    "eslint-plugin-better-tailwindcss": "^3.7.4",
 | 
			
		||||
    "eslint-plugin-simple-import-sort": "^12.1.1",
 | 
			
		||||
    "knip": "^5.29.2",
 | 
			
		||||
    "npm-run-all2": "^6.2.2",
 | 
			
		||||
    "prettier": "^3.3.3",
 | 
			
		||||
    "typescript": "^5.5.4",
 | 
			
		||||
    "typescript-eslint": "^8.4.0"
 | 
			
		||||
    "globals": "^16.3.0",
 | 
			
		||||
    "knip": "^5.62.0",
 | 
			
		||||
    "npm-run-all2": "^8.0.4",
 | 
			
		||||
    "postcss": "^8.5.6",
 | 
			
		||||
    "prettier": "^3.6.2",
 | 
			
		||||
    "tailwind-scrollbar": "^4.0.2",
 | 
			
		||||
    "tailwindcss": "^4.1.11",
 | 
			
		||||
    "typescript": "^5.9.2",
 | 
			
		||||
    "typescript-eslint": "^8.39.1"
 | 
			
		||||
  },
 | 
			
		||||
  "trustedDependencies": [
 | 
			
		||||
    "@biomejs/biome"
 | 
			
		||||
    "@parcel/watcher",
 | 
			
		||||
    "@tailwindcss/oxide"
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5
									
								
								postcss.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,5 @@
 | 
			
		||||
export default {
 | 
			
		||||
  plugins: {
 | 
			
		||||
    "@tailwindcss/postcss": {},
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
@@ -3,7 +3,7 @@
 | 
			
		||||
 */
 | 
			
		||||
const config = {
 | 
			
		||||
  arrowParens: "always",
 | 
			
		||||
  printWidth: 80,
 | 
			
		||||
  printWidth: 100,
 | 
			
		||||
  singleQuote: false,
 | 
			
		||||
  semi: true,
 | 
			
		||||
  tabWidth: 2,
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								public/android-chrome-192x192.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 7.7 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								public/android-chrome-512x512.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 22 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								public/apple-touch-icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 6.6 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								public/favicon-16x16.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 405 B  | 
							
								
								
									
										
											BIN
										
									
								
								public/favicon-32x32.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 831 B  | 
							
								
								
									
										
											BIN
										
									
								
								public/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 15 KiB  | 
							
								
								
									
										1
									
								
								public/favicon.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="512" height="512" style="cursor:default" viewBox="0 0 96.983 132.292"><g transform="translate(-56.568 -82.29)"><path d="M124.878 83.82h-60.91a5.86 5.86 0 0 0-5.87 5.87v117.496a5.855 5.855 0 0 0 5.87 5.866h82.182a5.855 5.855 0 0 0 5.87-5.866v-92.55z" style="display:inline;fill:#111827;stroke:#aeb9d0;stroke-width:3.06006;stroke-dasharray:none;stroke-opacity:1"/><circle cx="84.331" cy="128.904" r="6.653" style="fill:#84cc16;fill-opacity:1;stroke-width:2.13082"/><circle cx="105.059" cy="128.904" r="6.653" style="fill:#fff;fill-opacity:1;stroke-width:2.13082"/><circle cx="125.786" cy="128.904" r="6.653" style="fill:#84cc16;fill-opacity:1;stroke-width:2.13082"/><circle cx="84.331" cy="148.438" r="6.653" style="fill:#fff;fill-opacity:1;stroke-width:2.13082"/><circle cx="105.059" cy="148.438" r="6.653" style="display:inline;fill:#84cc16;fill-opacity:1;stroke-width:2.13082"/><circle cx="125.786" cy="148.438" r="6.653" style="fill:#fff;fill-opacity:1;stroke-width:2.13082"/><circle cx="84.331" cy="167.971" r="6.653" style="fill:#84cc16;fill-opacity:1;stroke-width:2.13082"/><circle cx="105.059" cy="167.971" r="6.653" style="fill:#fff;fill-opacity:1;stroke-width:2.13082"/><circle cx="125.786" cy="167.971" r="6.653" style="fill:#84cc16;fill-opacity:1;stroke-width:2.13082"/><path d="M119.124 161.326h13.287v13.287h-13.287z" style="fill:#84cc16;fill-opacity:1;stroke:none;stroke-width:3.04496;stroke-opacity:1"/></g></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 1.5 KiB  | 
@@ -1,16 +1,4 @@
 | 
			
		||||
window.downloadAll = function () {
 | 
			
		||||
  // Get all download links
 | 
			
		||||
  const downloadLinks = document.querySelectorAll("a[download]");
 | 
			
		||||
 | 
			
		||||
  // Trigger download for each link
 | 
			
		||||
  downloadLinks.forEach((link, index) => {
 | 
			
		||||
    // We add a delay for each download to prevent them from starting at the same time
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      const event = new MouseEvent("click");
 | 
			
		||||
      link.dispatchEvent(event);
 | 
			
		||||
    }, index * 100);
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
const webroot = document.querySelector("meta[name='webroot']").content;
 | 
			
		||||
const jobId = window.location.pathname.split("/").pop();
 | 
			
		||||
const main = document.querySelector("main");
 | 
			
		||||
let progressElem = document.querySelector("progress");
 | 
			
		||||
@@ -18,7 +6,7 @@ let progressElem = document.querySelector("progress");
 | 
			
		||||
const refreshData = () => {
 | 
			
		||||
  // console.log("Refreshing data...", progressElem.value, progressElem.max);
 | 
			
		||||
  if (progressElem.value !== progressElem.max) {
 | 
			
		||||
    fetch(`/progress/${jobId}`, {
 | 
			
		||||
    fetch(`${webroot}/progress/${jobId}`, {
 | 
			
		||||
      method: "POST",
 | 
			
		||||
    })
 | 
			
		||||
      .then((res) => res.text())
 | 
			
		||||
							
								
								
									
										251
									
								
								public/script.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,251 @@
 | 
			
		||||
const webroot = document.querySelector("meta[name='webroot']").content;
 | 
			
		||||
const fileInput = document.querySelector('input[type="file"]');
 | 
			
		||||
const dropZone = document.getElementById("dropzone");
 | 
			
		||||
const convertButton = document.querySelector("input[type='submit']");
 | 
			
		||||
const fileNames = [];
 | 
			
		||||
let fileType;
 | 
			
		||||
let pendingFiles = 0;
 | 
			
		||||
let formatSelected = false;
 | 
			
		||||
 | 
			
		||||
dropZone.addEventListener("dragover", (e) => {
 | 
			
		||||
  e.preventDefault();
 | 
			
		||||
  dropZone.classList.add("dragover");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
dropZone.addEventListener("dragleave", () => {
 | 
			
		||||
  dropZone.classList.remove("dragover");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
dropZone.addEventListener("drop", (e) => {
 | 
			
		||||
  e.preventDefault();
 | 
			
		||||
  dropZone.classList.remove("dragover");
 | 
			
		||||
 | 
			
		||||
  const files = e.dataTransfer.files;
 | 
			
		||||
 | 
			
		||||
  if (files.length === 0) {
 | 
			
		||||
    console.warn("No files dropped — likely a URL or unsupported source.");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  for (const file of files) {
 | 
			
		||||
    console.log("Handling dropped file:", file.name);
 | 
			
		||||
    handleFile(file);
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Extracted handleFile function for reusability in drag-and-drop and file input
 | 
			
		||||
function handleFile(file) {
 | 
			
		||||
  const fileList = document.querySelector("#file-list");
 | 
			
		||||
 | 
			
		||||
  const row = document.createElement("tr");
 | 
			
		||||
  row.innerHTML = `
 | 
			
		||||
    <td>${file.name}</td>
 | 
			
		||||
    <td><progress max="100" class="inline-block h-2 appearance-none overflow-hidden rounded-full border-0 bg-neutral-700 bg-none text-accent-500 accent-accent-500 [&::-moz-progress-bar]:bg-accent-500 [&::-webkit-progress-value]:rounded-full [&::-webkit-progress-value]:[background:none] [&[value]::-webkit-progress-value]:bg-accent-500 [&[value]::-webkit-progress-value]:transition-[inline-size]"></progress></td>
 | 
			
		||||
    <td>${(file.size / 1024).toFixed(2)} kB</td>
 | 
			
		||||
    <td><a onclick="deleteRow(this)">Remove</a></td>
 | 
			
		||||
  `;
 | 
			
		||||
 | 
			
		||||
  if (!fileType) {
 | 
			
		||||
    fileType = file.name.split(".").pop();
 | 
			
		||||
    fileInput.setAttribute("accept", `.${fileType}`);
 | 
			
		||||
    setTitle();
 | 
			
		||||
 | 
			
		||||
    fetch(`${webroot}/conversions`, {
 | 
			
		||||
      method: "POST",
 | 
			
		||||
      body: JSON.stringify({ fileType }),
 | 
			
		||||
      headers: { "Content-Type": "application/json" },
 | 
			
		||||
    })
 | 
			
		||||
      .then((res) => res.text())
 | 
			
		||||
      .then((html) => {
 | 
			
		||||
        selectContainer.innerHTML = html;
 | 
			
		||||
        updateSearchBar();
 | 
			
		||||
      })
 | 
			
		||||
      .catch(console.error);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  fileList.appendChild(row);
 | 
			
		||||
  file.htmlRow = row;
 | 
			
		||||
  fileNames.push(file.name);
 | 
			
		||||
  uploadFile(file);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const selectContainer = document.querySelector("form .select_container");
 | 
			
		||||
 | 
			
		||||
const updateSearchBar = () => {
 | 
			
		||||
  const convertToInput = document.querySelector("input[name='convert_to_search']");
 | 
			
		||||
  const convertToPopup = document.querySelector(".convert_to_popup");
 | 
			
		||||
  const convertToGroupElements = document.querySelectorAll(".convert_to_group");
 | 
			
		||||
  const convertToGroups = {};
 | 
			
		||||
  const convertToElement = document.querySelector("select[name='convert_to']");
 | 
			
		||||
 | 
			
		||||
  const showMatching = (search) => {
 | 
			
		||||
    for (const [targets, groupElement] of Object.values(convertToGroups)) {
 | 
			
		||||
      let matchingTargetsFound = 0;
 | 
			
		||||
      for (const target of targets) {
 | 
			
		||||
        if (target.dataset.target.includes(search)) {
 | 
			
		||||
          matchingTargetsFound++;
 | 
			
		||||
          target.classList.remove("hidden");
 | 
			
		||||
          target.classList.add("flex");
 | 
			
		||||
        } else {
 | 
			
		||||
          target.classList.add("hidden");
 | 
			
		||||
          target.classList.remove("flex");
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (matchingTargetsFound === 0) {
 | 
			
		||||
        groupElement.classList.add("hidden");
 | 
			
		||||
        groupElement.classList.remove("flex");
 | 
			
		||||
      } else {
 | 
			
		||||
        groupElement.classList.remove("hidden");
 | 
			
		||||
        groupElement.classList.add("flex");
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  for (const groupElement of convertToGroupElements) {
 | 
			
		||||
    const groupName = groupElement.dataset.converter;
 | 
			
		||||
 | 
			
		||||
    const targetElements = groupElement.querySelectorAll(".target");
 | 
			
		||||
    const targets = Array.from(targetElements);
 | 
			
		||||
 | 
			
		||||
    for (const target of targets) {
 | 
			
		||||
      target.onmousedown = () => {
 | 
			
		||||
        convertToElement.value = target.dataset.value;
 | 
			
		||||
        convertToInput.value = `${target.dataset.target} using ${target.dataset.converter}`;
 | 
			
		||||
        formatSelected = true;
 | 
			
		||||
        if (pendingFiles === 0 && fileNames.length > 0) {
 | 
			
		||||
          convertButton.disabled = false;
 | 
			
		||||
        }
 | 
			
		||||
        showMatching("");
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    convertToGroups[groupName] = [targets, groupElement];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  convertToInput.addEventListener("input", (e) => {
 | 
			
		||||
    showMatching(e.target.value.toLowerCase());
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  convertToInput.addEventListener("search", () => {
 | 
			
		||||
    // when the user clears the search bar using the 'x' button
 | 
			
		||||
    convertButton.disabled = true;
 | 
			
		||||
    formatSelected = false;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  convertToInput.addEventListener("blur", (e) => {
 | 
			
		||||
    // Keep the popup open even when clicking on a target button
 | 
			
		||||
    // for a split second to allow the click to go through
 | 
			
		||||
    if (e?.relatedTarget?.classList?.contains("target")) {
 | 
			
		||||
      convertToPopup.classList.add("hidden");
 | 
			
		||||
      convertToPopup.classList.remove("flex");
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    convertToPopup.classList.add("hidden");
 | 
			
		||||
    convertToPopup.classList.remove("flex");
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  convertToInput.addEventListener("focus", () => {
 | 
			
		||||
    convertToPopup.classList.remove("hidden");
 | 
			
		||||
    convertToPopup.classList.add("flex");
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Add a 'change' event listener to the file input element
 | 
			
		||||
fileInput.addEventListener("change", (e) => {
 | 
			
		||||
  const files = e.target.files;
 | 
			
		||||
  for (const file of files) {
 | 
			
		||||
    handleFile(file);
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const setTitle = () => {
 | 
			
		||||
  const title = document.querySelector("h1");
 | 
			
		||||
  title.textContent = `Convert ${fileType ? `.${fileType}` : ""}`;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Add a onclick for the delete button
 | 
			
		||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
const deleteRow = (target) => {
 | 
			
		||||
  const filename = target.parentElement.parentElement.children[0].textContent;
 | 
			
		||||
  const row = target.parentElement.parentElement;
 | 
			
		||||
  row.remove();
 | 
			
		||||
 | 
			
		||||
  // remove from fileNames
 | 
			
		||||
  const index = fileNames.indexOf(filename);
 | 
			
		||||
  fileNames.splice(index, 1);
 | 
			
		||||
 | 
			
		||||
  // reset fileInput
 | 
			
		||||
  fileInput.value = "";
 | 
			
		||||
 | 
			
		||||
  // if fileNames is empty, reset fileType
 | 
			
		||||
  if (fileNames.length === 0) {
 | 
			
		||||
    fileType = null;
 | 
			
		||||
    fileInput.removeAttribute("accept");
 | 
			
		||||
    convertButton.disabled = true;
 | 
			
		||||
    setTitle();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  fetch(`${webroot}/delete`, {
 | 
			
		||||
    method: "POST",
 | 
			
		||||
    body: JSON.stringify({ filename: filename }),
 | 
			
		||||
    headers: {
 | 
			
		||||
      "Content-Type": "application/json",
 | 
			
		||||
    },
 | 
			
		||||
  }).catch((err) => console.log(err));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const uploadFile = (file) => {
 | 
			
		||||
  convertButton.disabled = true;
 | 
			
		||||
  convertButton.textContent = "Uploading...";
 | 
			
		||||
  pendingFiles += 1;
 | 
			
		||||
 | 
			
		||||
  const formData = new FormData();
 | 
			
		||||
  formData.append("file", file, file.name);
 | 
			
		||||
 | 
			
		||||
  let xhr = new XMLHttpRequest();
 | 
			
		||||
 | 
			
		||||
  xhr.open("POST", `${webroot}/upload`, true);
 | 
			
		||||
 | 
			
		||||
  xhr.onload = () => {
 | 
			
		||||
    let data = JSON.parse(xhr.responseText);
 | 
			
		||||
 | 
			
		||||
    pendingFiles -= 1;
 | 
			
		||||
    if (pendingFiles === 0) {
 | 
			
		||||
      if (formatSelected) {
 | 
			
		||||
        convertButton.disabled = false;
 | 
			
		||||
      }
 | 
			
		||||
      convertButton.textContent = "Convert";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    //Remove the progress bar when upload is done
 | 
			
		||||
    let progressbar = file.htmlRow.getElementsByTagName("progress");
 | 
			
		||||
    progressbar[0].parentElement.remove();
 | 
			
		||||
    console.log(data);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  xhr.upload.onprogress = (e) => {
 | 
			
		||||
    let sent = e.loaded;
 | 
			
		||||
    let total = e.total;
 | 
			
		||||
    console.log(`upload progress (${file.name}):`, (100 * sent) / total);
 | 
			
		||||
 | 
			
		||||
    let progressbar = file.htmlRow.getElementsByTagName("progress");
 | 
			
		||||
    progressbar[0].value = (100 * sent) / total;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  xhr.onerror = (e) => {
 | 
			
		||||
    console.log(e);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  xhr.send(formData);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const formConvert = document.querySelector(`form[action='${webroot}/convert']`);
 | 
			
		||||
 | 
			
		||||
formConvert.addEventListener("submit", () => {
 | 
			
		||||
  const hiddenInput = document.querySelector("input[name='file_names']");
 | 
			
		||||
  hiddenInput.value = JSON.stringify(fileNames);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
updateSearchBar();
 | 
			
		||||
@@ -1,6 +1,9 @@
 | 
			
		||||
{
 | 
			
		||||
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
 | 
			
		||||
  "extends": [
 | 
			
		||||
    "config:recommended"
 | 
			
		||||
  ]
 | 
			
		||||
  "extends": ["config:recommended", ":disableDependencyDashboard"],
 | 
			
		||||
  "lockFileMaintenance": {
 | 
			
		||||
    "enabled": true,
 | 
			
		||||
    "automerge": true
 | 
			
		||||
  },
 | 
			
		||||
  "ignoreDeps": ["bun-types", "@types/bun"]
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								reset.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1 +1 @@
 | 
			
		||||
import "@total-typescript/ts-reset";
 | 
			
		||||
import "@total-typescript/ts-reset";
 | 
			
		||||
 
 | 
			
		||||
@@ -1,33 +1,44 @@
 | 
			
		||||
import { Html } from "@elysiajs/html";
 | 
			
		||||
import { version } from "../../package.json";
 | 
			
		||||
 | 
			
		||||
export const BaseHtml = ({
 | 
			
		||||
  children,
 | 
			
		||||
  title = "ConvertX",
 | 
			
		||||
}: { children: JSX.Element; title?: string }) => (
 | 
			
		||||
  webroot = "",
 | 
			
		||||
}: {
 | 
			
		||||
  children: JSX.Element;
 | 
			
		||||
  title?: string;
 | 
			
		||||
  webroot?: string;
 | 
			
		||||
}) => (
 | 
			
		||||
  <html lang="en">
 | 
			
		||||
    <head>
 | 
			
		||||
      <meta charset="UTF-8" />
 | 
			
		||||
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
			
		||||
      <meta name="webroot" content={webroot} />
 | 
			
		||||
      <title safe>{title}</title>
 | 
			
		||||
      <link rel="stylesheet" href="/pico.lime.min.css" />
 | 
			
		||||
      <link rel="stylesheet" href="/style.css" />
 | 
			
		||||
      <link
 | 
			
		||||
        rel="apple-touch-icon"
 | 
			
		||||
        sizes="180x180"
 | 
			
		||||
        href="/apple-touch-icon.png"
 | 
			
		||||
      />
 | 
			
		||||
      <link
 | 
			
		||||
        rel="icon"
 | 
			
		||||
        type="image/png"
 | 
			
		||||
        sizes="32x32"
 | 
			
		||||
        href="/favicon-32x32.png"
 | 
			
		||||
      />
 | 
			
		||||
      <link
 | 
			
		||||
        rel="icon"
 | 
			
		||||
        type="image/png"
 | 
			
		||||
        sizes="16x16"
 | 
			
		||||
        href="/favicon-16x16.png"
 | 
			
		||||
      />
 | 
			
		||||
      <link rel="manifest" href="/site.webmanifest" />
 | 
			
		||||
      <link rel="stylesheet" href={`${webroot}/generated.css`} />
 | 
			
		||||
      <link rel="apple-touch-icon" sizes="180x180" href={`${webroot}/apple-touch-icon.png`} />
 | 
			
		||||
      <link rel="icon" type="image/png" sizes="32x32" href={`${webroot}/favicon-32x32.png`} />
 | 
			
		||||
      <link rel="icon" type="image/png" sizes="16x16" href={`${webroot}/favicon-16x16.png`} />
 | 
			
		||||
      <link rel="manifest" href={`${webroot}/site.webmanifest`} />
 | 
			
		||||
    </head>
 | 
			
		||||
    <body>{children}</body>
 | 
			
		||||
    <body class={`flex min-h-screen w-full flex-col bg-neutral-900 text-neutral-200`}>
 | 
			
		||||
      {children}
 | 
			
		||||
      <footer class="w-full">
 | 
			
		||||
        <div class="p-4 text-center text-sm text-neutral-500">
 | 
			
		||||
          <span>Powered by </span>
 | 
			
		||||
          <a
 | 
			
		||||
            href="https://github.com/C4illin/ConvertX"
 | 
			
		||||
            class={`
 | 
			
		||||
              text-neutral-400
 | 
			
		||||
              hover:text-accent-500
 | 
			
		||||
            `}
 | 
			
		||||
          >
 | 
			
		||||
            ConvertX{" "}
 | 
			
		||||
          </a>
 | 
			
		||||
          <span safe>v{version || ""}</span>
 | 
			
		||||
        </div>
 | 
			
		||||
      </footer>
 | 
			
		||||
    </body>
 | 
			
		||||
  </html>
 | 
			
		||||
);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,48 +1,101 @@
 | 
			
		||||
import { Html } from "@kitajs/html";
 | 
			
		||||
 | 
			
		||||
export const Header = ({
 | 
			
		||||
  loggedIn,
 | 
			
		||||
  accountRegistration,
 | 
			
		||||
}: { loggedIn?: boolean; accountRegistration?: boolean }) => {
 | 
			
		||||
  allowUnauthenticated,
 | 
			
		||||
  hideHistory,
 | 
			
		||||
  webroot = "",
 | 
			
		||||
}: {
 | 
			
		||||
  loggedIn?: boolean;
 | 
			
		||||
  accountRegistration?: boolean;
 | 
			
		||||
  allowUnauthenticated?: boolean;
 | 
			
		||||
  hideHistory?: boolean;
 | 
			
		||||
  webroot?: string;
 | 
			
		||||
}) => {
 | 
			
		||||
  let rightNav: JSX.Element;
 | 
			
		||||
  if (loggedIn) {
 | 
			
		||||
    rightNav = (
 | 
			
		||||
      <ul>
 | 
			
		||||
        <li>
 | 
			
		||||
          <a href="/history">History</a>
 | 
			
		||||
        </li>
 | 
			
		||||
        <li>
 | 
			
		||||
          <a href="/logoff">Logout</a>
 | 
			
		||||
        </li>
 | 
			
		||||
      <ul class="flex gap-4">
 | 
			
		||||
        {!hideHistory && (
 | 
			
		||||
          <li>
 | 
			
		||||
            <a
 | 
			
		||||
              class={`
 | 
			
		||||
                text-accent-600 transition-all
 | 
			
		||||
                hover:text-accent-500 hover:underline
 | 
			
		||||
              `}
 | 
			
		||||
              href={`${webroot}/history`}
 | 
			
		||||
            >
 | 
			
		||||
              History
 | 
			
		||||
            </a>
 | 
			
		||||
          </li>
 | 
			
		||||
        )}
 | 
			
		||||
        {!allowUnauthenticated ? (
 | 
			
		||||
          <li>
 | 
			
		||||
            <a
 | 
			
		||||
              class={`
 | 
			
		||||
                text-accent-600 transition-all
 | 
			
		||||
                hover:text-accent-500 hover:underline
 | 
			
		||||
              `}
 | 
			
		||||
              href={`${webroot}/account`}
 | 
			
		||||
            >
 | 
			
		||||
              Account
 | 
			
		||||
            </a>
 | 
			
		||||
          </li>
 | 
			
		||||
        ) : null}
 | 
			
		||||
        {!allowUnauthenticated ? (
 | 
			
		||||
          <li>
 | 
			
		||||
            <a
 | 
			
		||||
              class={`
 | 
			
		||||
                text-accent-600 transition-all
 | 
			
		||||
                hover:text-accent-500 hover:underline
 | 
			
		||||
              `}
 | 
			
		||||
              href={`${webroot}/logoff`}
 | 
			
		||||
            >
 | 
			
		||||
              Logout
 | 
			
		||||
            </a>
 | 
			
		||||
          </li>
 | 
			
		||||
        ) : null}
 | 
			
		||||
      </ul>
 | 
			
		||||
    );
 | 
			
		||||
  } else {
 | 
			
		||||
    rightNav = (
 | 
			
		||||
      <ul>
 | 
			
		||||
      <ul class="flex gap-4">
 | 
			
		||||
        <li>
 | 
			
		||||
          <a href="/login">Login</a>
 | 
			
		||||
          <a
 | 
			
		||||
            class={`
 | 
			
		||||
              text-accent-600 transition-all
 | 
			
		||||
              hover:text-accent-500 hover:underline
 | 
			
		||||
            `}
 | 
			
		||||
            href={`${webroot}/login`}
 | 
			
		||||
          >
 | 
			
		||||
            Login
 | 
			
		||||
          </a>
 | 
			
		||||
        </li>
 | 
			
		||||
        {accountRegistration && (
 | 
			
		||||
        {accountRegistration ? (
 | 
			
		||||
          <li>
 | 
			
		||||
            <a href="/register">Register</a>
 | 
			
		||||
            <a
 | 
			
		||||
              class={`
 | 
			
		||||
                text-accent-600 transition-all
 | 
			
		||||
                hover:text-accent-500 hover:underline
 | 
			
		||||
              `}
 | 
			
		||||
              href={`${webroot}/register`}
 | 
			
		||||
            >
 | 
			
		||||
              Register
 | 
			
		||||
            </a>
 | 
			
		||||
          </li>
 | 
			
		||||
        )}
 | 
			
		||||
        ) : null}
 | 
			
		||||
      </ul>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <header class="container">
 | 
			
		||||
      <nav>
 | 
			
		||||
    <header class="w-full p-4">
 | 
			
		||||
      <nav class={`mx-auto flex max-w-4xl justify-between rounded-sm bg-neutral-900 p-4`}>
 | 
			
		||||
        <ul>
 | 
			
		||||
          <li>
 | 
			
		||||
            <strong>
 | 
			
		||||
              <a
 | 
			
		||||
                href="/"
 | 
			
		||||
                style={{
 | 
			
		||||
                  textDecoration: "none",
 | 
			
		||||
                  color: "inherit",
 | 
			
		||||
                }}>
 | 
			
		||||
                ConvertX
 | 
			
		||||
              </a>
 | 
			
		||||
              <a href={`${webroot}/`}>ConvertX</a>
 | 
			
		||||
            </strong>
 | 
			
		||||
          </li>
 | 
			
		||||
        </ul>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										140
									
								
								src/converters/assimp.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,140 @@
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
    object: [
 | 
			
		||||
      "3d",
 | 
			
		||||
      "3ds",
 | 
			
		||||
      "3mf",
 | 
			
		||||
      "ac",
 | 
			
		||||
      "ac3d",
 | 
			
		||||
      "acc",
 | 
			
		||||
      "amf",
 | 
			
		||||
      "amj",
 | 
			
		||||
      "ase",
 | 
			
		||||
      "ask",
 | 
			
		||||
      "assbin",
 | 
			
		||||
      "b3d",
 | 
			
		||||
      "blend",
 | 
			
		||||
      "bsp",
 | 
			
		||||
      "bvh",
 | 
			
		||||
      "cob",
 | 
			
		||||
      "csm",
 | 
			
		||||
      "dae",
 | 
			
		||||
      "dxf",
 | 
			
		||||
      "enff",
 | 
			
		||||
      "fbx",
 | 
			
		||||
      "glb",
 | 
			
		||||
      "gltf",
 | 
			
		||||
      "hmb",
 | 
			
		||||
      "hmp",
 | 
			
		||||
      "ifc",
 | 
			
		||||
      "ifczip",
 | 
			
		||||
      "iqm",
 | 
			
		||||
      "irr",
 | 
			
		||||
      "irrmesh",
 | 
			
		||||
      "lwo",
 | 
			
		||||
      "lws",
 | 
			
		||||
      "lxo",
 | 
			
		||||
      "m3d",
 | 
			
		||||
      "md2",
 | 
			
		||||
      "md3",
 | 
			
		||||
      "md5anim",
 | 
			
		||||
      "md5camera",
 | 
			
		||||
      "md5mesh",
 | 
			
		||||
      "mdc",
 | 
			
		||||
      "mdl",
 | 
			
		||||
      "mesh.xml",
 | 
			
		||||
      "mesh",
 | 
			
		||||
      "mot",
 | 
			
		||||
      "ms3d",
 | 
			
		||||
      "ndo",
 | 
			
		||||
      "nff",
 | 
			
		||||
      "obj",
 | 
			
		||||
      "off",
 | 
			
		||||
      "ogex",
 | 
			
		||||
      "pk3",
 | 
			
		||||
      "ply",
 | 
			
		||||
      "pmx",
 | 
			
		||||
      "prj",
 | 
			
		||||
      "q3o",
 | 
			
		||||
      "q3s",
 | 
			
		||||
      "raw",
 | 
			
		||||
      "scn",
 | 
			
		||||
      "sib",
 | 
			
		||||
      "smd",
 | 
			
		||||
      "step",
 | 
			
		||||
      "stl",
 | 
			
		||||
      "stp",
 | 
			
		||||
      "ter",
 | 
			
		||||
      "uc",
 | 
			
		||||
      "usd",
 | 
			
		||||
      "usda",
 | 
			
		||||
      "usdc",
 | 
			
		||||
      "usdz",
 | 
			
		||||
      "vta",
 | 
			
		||||
      "x",
 | 
			
		||||
      "x3d",
 | 
			
		||||
      "x3db",
 | 
			
		||||
      "xgl",
 | 
			
		||||
      "xml",
 | 
			
		||||
      "zae",
 | 
			
		||||
      "zgl",
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  to: {
 | 
			
		||||
    object: [
 | 
			
		||||
      "3ds",
 | 
			
		||||
      "3mf",
 | 
			
		||||
      "assbin",
 | 
			
		||||
      "assjson",
 | 
			
		||||
      "assxml",
 | 
			
		||||
      "collada",
 | 
			
		||||
      "dae",
 | 
			
		||||
      "fbx",
 | 
			
		||||
      "fbxa",
 | 
			
		||||
      "glb",
 | 
			
		||||
      "glb2",
 | 
			
		||||
      "gltf",
 | 
			
		||||
      "gltf2",
 | 
			
		||||
      "json",
 | 
			
		||||
      "obj",
 | 
			
		||||
      "objnomtl",
 | 
			
		||||
      "pbrt",
 | 
			
		||||
      "ply",
 | 
			
		||||
      "plyb",
 | 
			
		||||
      "stl",
 | 
			
		||||
      "stlb",
 | 
			
		||||
      "stp",
 | 
			
		||||
      "x",
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export async function convert(
 | 
			
		||||
  filePath: string,
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal, // to make it mockable
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    execFile("assimp", ["export", filePath, targetPath], (error, stdout, stderr) => {
 | 
			
		||||
      if (error) {
 | 
			
		||||
        reject(`error: ${error}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (stdout) {
 | 
			
		||||
        console.log(`stdout: ${stdout}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (stderr) {
 | 
			
		||||
        console.error(`stderr: ${stderr}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      resolve("Done");
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										86
									
								
								src/converters/calibre.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,86 @@
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
    document: [
 | 
			
		||||
      "azw4",
 | 
			
		||||
      "chm",
 | 
			
		||||
      "cbr",
 | 
			
		||||
      "cbz",
 | 
			
		||||
      "cbt",
 | 
			
		||||
      "cba",
 | 
			
		||||
      "cb7",
 | 
			
		||||
      "djvu",
 | 
			
		||||
      "docx",
 | 
			
		||||
      "epub",
 | 
			
		||||
      "fb2",
 | 
			
		||||
      "htlz",
 | 
			
		||||
      "html",
 | 
			
		||||
      "lit",
 | 
			
		||||
      "lrf",
 | 
			
		||||
      "mobi",
 | 
			
		||||
      "odt",
 | 
			
		||||
      "pdb",
 | 
			
		||||
      "pdf",
 | 
			
		||||
      "pml",
 | 
			
		||||
      "rb",
 | 
			
		||||
      "rtf",
 | 
			
		||||
      "recipe",
 | 
			
		||||
      "snb",
 | 
			
		||||
      "tcr",
 | 
			
		||||
      "txt",
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  to: {
 | 
			
		||||
    document: [
 | 
			
		||||
      "azw3",
 | 
			
		||||
      "docx",
 | 
			
		||||
      "epub",
 | 
			
		||||
      "fb2",
 | 
			
		||||
      "html",
 | 
			
		||||
      "htmlz",
 | 
			
		||||
      "kepub.epub",
 | 
			
		||||
      "lit",
 | 
			
		||||
      "lrf",
 | 
			
		||||
      "mobi",
 | 
			
		||||
      "oeb",
 | 
			
		||||
      "pdb",
 | 
			
		||||
      "pdf",
 | 
			
		||||
      "pml",
 | 
			
		||||
      "rb",
 | 
			
		||||
      "rtf",
 | 
			
		||||
      "snb",
 | 
			
		||||
      "tcr",
 | 
			
		||||
      "txt",
 | 
			
		||||
      "txtz",
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export async function convert(
 | 
			
		||||
  filePath: string,
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal, // to make it mockable
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    execFile("ebook-convert", [filePath, targetPath], (error, stdout, stderr) => {
 | 
			
		||||
      if (error) {
 | 
			
		||||
        reject(`error: ${error}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (stdout) {
 | 
			
		||||
        console.log(`stdout: ${stdout}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (stderr) {
 | 
			
		||||
        console.error(`stderr: ${stderr}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      resolve("Done");
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										48
									
								
								src/converters/dasel.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,48 @@
 | 
			
		||||
import fs from "fs";
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
    document: ["yaml", "toml", "json", "xml", "csv"],
 | 
			
		||||
  },
 | 
			
		||||
  to: {
 | 
			
		||||
    document: ["yaml", "toml", "json", "csv"],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export async function convert(
 | 
			
		||||
  filePath: string,
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal, // to make it mockable
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  const args: string[] = [];
 | 
			
		||||
 | 
			
		||||
  args.push("--file", filePath);
 | 
			
		||||
  args.push("--read", fileType);
 | 
			
		||||
  args.push("--write", convertTo);
 | 
			
		||||
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    execFile("dasel", args, (error, stdout, stderr) => {
 | 
			
		||||
      if (error) {
 | 
			
		||||
        reject(`error: ${error}`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (stderr) {
 | 
			
		||||
        console.error(`stderr: ${stderr}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      fs.writeFile(targetPath, stdout, (err: NodeJS.ErrnoException | null) => {
 | 
			
		||||
        if (err) {
 | 
			
		||||
          reject(`Failed to write output: ${err}`);
 | 
			
		||||
        } else {
 | 
			
		||||
          resolve("Done");
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										49
									
								
								src/converters/dvisvgm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,49 @@
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
    images: ["dvi", "xdv", "pdf", "eps"],
 | 
			
		||||
  },
 | 
			
		||||
  to: {
 | 
			
		||||
    images: ["svg", "svgz"],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function convert(
 | 
			
		||||
  filePath: string,
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal, // to make it mockable
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  const inputArgs: string[] = [];
 | 
			
		||||
  if (fileType === "eps") {
 | 
			
		||||
    inputArgs.push("--eps");
 | 
			
		||||
  }
 | 
			
		||||
  if (fileType === "pdf") {
 | 
			
		||||
    inputArgs.push("--pdf");
 | 
			
		||||
  }
 | 
			
		||||
  if (convertTo === "svgz") {
 | 
			
		||||
    inputArgs.push("-z");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    execFile("dvisvgm", [...inputArgs, filePath, "-o", targetPath], (error, stdout, stderr) => {
 | 
			
		||||
      if (error) {
 | 
			
		||||
        reject(`error: ${error}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (stdout) {
 | 
			
		||||
        console.log(`stdout: ${stdout}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (stderr) {
 | 
			
		||||
        console.error(`stderr: ${stderr}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      resolve("Done");
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { exec } from "node:child_process";
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
// This could be done dynamically by running `ffmpeg -formats` and parsing the output
 | 
			
		||||
export const properties = {
 | 
			
		||||
@@ -6,6 +7,7 @@ export const properties = {
 | 
			
		||||
    muxer: [
 | 
			
		||||
      "264",
 | 
			
		||||
      "265",
 | 
			
		||||
      "266",
 | 
			
		||||
      "302",
 | 
			
		||||
      "3dostr",
 | 
			
		||||
      "3g2",
 | 
			
		||||
@@ -18,6 +20,7 @@ export const properties = {
 | 
			
		||||
      "aac",
 | 
			
		||||
      "aax",
 | 
			
		||||
      "ac3",
 | 
			
		||||
      "ac4",
 | 
			
		||||
      "ace",
 | 
			
		||||
      "acm",
 | 
			
		||||
      "act",
 | 
			
		||||
@@ -48,7 +51,6 @@ export const properties = {
 | 
			
		||||
      "apng",
 | 
			
		||||
      "aptx",
 | 
			
		||||
      "aptxhd",
 | 
			
		||||
      "aptx_hd",
 | 
			
		||||
      "aqt",
 | 
			
		||||
      "aqtitle",
 | 
			
		||||
      "argo_asf",
 | 
			
		||||
@@ -63,10 +65,12 @@ export const properties = {
 | 
			
		||||
      "av1",
 | 
			
		||||
      "avc",
 | 
			
		||||
      "avi",
 | 
			
		||||
      "avif",
 | 
			
		||||
      "avr",
 | 
			
		||||
      "avs",
 | 
			
		||||
      "avs2",
 | 
			
		||||
      "avs3",
 | 
			
		||||
      "awb",
 | 
			
		||||
      "bcstm",
 | 
			
		||||
      "bethsoftvid",
 | 
			
		||||
      "bfi",
 | 
			
		||||
@@ -75,8 +79,10 @@ export const properties = {
 | 
			
		||||
      "bink",
 | 
			
		||||
      "binka",
 | 
			
		||||
      "bit",
 | 
			
		||||
      "bmp_pipe",
 | 
			
		||||
      "bitpacked",
 | 
			
		||||
      "bmv",
 | 
			
		||||
      "bmp",
 | 
			
		||||
      "bonk",
 | 
			
		||||
      "boa",
 | 
			
		||||
      "brender_pix",
 | 
			
		||||
      "brstm",
 | 
			
		||||
@@ -93,7 +99,7 @@ export const properties = {
 | 
			
		||||
      "codec2",
 | 
			
		||||
      "codec2raw",
 | 
			
		||||
      "concat",
 | 
			
		||||
      "cri_pipe",
 | 
			
		||||
      "cri",
 | 
			
		||||
      "dash",
 | 
			
		||||
      "dat",
 | 
			
		||||
      "data",
 | 
			
		||||
@@ -101,8 +107,9 @@ export const properties = {
 | 
			
		||||
      "dav",
 | 
			
		||||
      "dbm",
 | 
			
		||||
      "dcstr",
 | 
			
		||||
      "dds_pipe",
 | 
			
		||||
      "dds",
 | 
			
		||||
      "derf",
 | 
			
		||||
      "dfpwm",
 | 
			
		||||
      "dfa",
 | 
			
		||||
      "dhav",
 | 
			
		||||
      "dif",
 | 
			
		||||
@@ -131,6 +138,8 @@ export const properties = {
 | 
			
		||||
      "exr_pipe",
 | 
			
		||||
      "f32be",
 | 
			
		||||
      "f32le",
 | 
			
		||||
      "ec3",
 | 
			
		||||
      "evc",
 | 
			
		||||
      "f4v",
 | 
			
		||||
      "f64be",
 | 
			
		||||
      "f64le",
 | 
			
		||||
@@ -157,13 +166,13 @@ export const properties = {
 | 
			
		||||
      "gdv",
 | 
			
		||||
      "genh",
 | 
			
		||||
      "gif",
 | 
			
		||||
      "gif_pipe",
 | 
			
		||||
      "gsm",
 | 
			
		||||
      "gxf",
 | 
			
		||||
      "h261",
 | 
			
		||||
      "h263",
 | 
			
		||||
      "h264",
 | 
			
		||||
      "h265",
 | 
			
		||||
      "h266",
 | 
			
		||||
      "h26l",
 | 
			
		||||
      "hca",
 | 
			
		||||
      "hcom",
 | 
			
		||||
@@ -180,7 +189,6 @@ export const properties = {
 | 
			
		||||
      "ifv",
 | 
			
		||||
      "ilbc",
 | 
			
		||||
      "image2",
 | 
			
		||||
      "image2pipe",
 | 
			
		||||
      "imf",
 | 
			
		||||
      "imx",
 | 
			
		||||
      "ingenient",
 | 
			
		||||
@@ -197,21 +205,17 @@ export const properties = {
 | 
			
		||||
      "ivr",
 | 
			
		||||
      "j2b",
 | 
			
		||||
      "j2k",
 | 
			
		||||
      "j2k_pipe",
 | 
			
		||||
      "jack",
 | 
			
		||||
      "jacosub",
 | 
			
		||||
      "jpegls_pipe",
 | 
			
		||||
      "jpeg_pipe",
 | 
			
		||||
      "jv",
 | 
			
		||||
      "jpegls",
 | 
			
		||||
      "jpeg",
 | 
			
		||||
      "jxl",
 | 
			
		||||
      "kmsgrab",
 | 
			
		||||
      "kux",
 | 
			
		||||
      "kvag",
 | 
			
		||||
      "lavfi",
 | 
			
		||||
      "libcdio",
 | 
			
		||||
      "libdc1394",
 | 
			
		||||
      "libgme",
 | 
			
		||||
      "libopenmpt",
 | 
			
		||||
      "live_flv",
 | 
			
		||||
      "laf",
 | 
			
		||||
      "lmlm4",
 | 
			
		||||
      "loas",
 | 
			
		||||
      "lrc",
 | 
			
		||||
@@ -224,16 +228,13 @@ export const properties = {
 | 
			
		||||
      "m4b",
 | 
			
		||||
      "m4v",
 | 
			
		||||
      "mac",
 | 
			
		||||
      "matroska",
 | 
			
		||||
      "mca",
 | 
			
		||||
      "mcc",
 | 
			
		||||
      "mdl",
 | 
			
		||||
      "med",
 | 
			
		||||
      "mgsts",
 | 
			
		||||
      "microdvd",
 | 
			
		||||
      "mj2",
 | 
			
		||||
      "mjpeg",
 | 
			
		||||
      "mjpeg_2000",
 | 
			
		||||
      "mjpg",
 | 
			
		||||
      "mk3d",
 | 
			
		||||
      "mka",
 | 
			
		||||
@@ -257,9 +258,6 @@ export const properties = {
 | 
			
		||||
      "mpc",
 | 
			
		||||
      "mpc8",
 | 
			
		||||
      "mpeg",
 | 
			
		||||
      "mpegts",
 | 
			
		||||
      "mpegtsraw",
 | 
			
		||||
      "mpegvideo",
 | 
			
		||||
      "mpg",
 | 
			
		||||
      "mpjpeg",
 | 
			
		||||
      "mpl2",
 | 
			
		||||
@@ -294,25 +292,27 @@ export const properties = {
 | 
			
		||||
      "okt",
 | 
			
		||||
      "oma",
 | 
			
		||||
      "omg",
 | 
			
		||||
      "opus",
 | 
			
		||||
      "openal",
 | 
			
		||||
      "oss",
 | 
			
		||||
      "osq",
 | 
			
		||||
      "paf",
 | 
			
		||||
      "pam_pipe",
 | 
			
		||||
      "pbm_pipe",
 | 
			
		||||
      "pcx_pipe",
 | 
			
		||||
      "pgmyuv_pipe",
 | 
			
		||||
      "pgm_pipe",
 | 
			
		||||
      "pgx_pipe",
 | 
			
		||||
      "photocd_pipe",
 | 
			
		||||
      "pictor_pipe",
 | 
			
		||||
      "pdv",
 | 
			
		||||
      "pam",
 | 
			
		||||
      "pbm",
 | 
			
		||||
      "pcx",
 | 
			
		||||
      "pgmyuv",
 | 
			
		||||
      "pgm",
 | 
			
		||||
      "pgx",
 | 
			
		||||
      "photocd",
 | 
			
		||||
      "pictor",
 | 
			
		||||
      "pjs",
 | 
			
		||||
      "plm",
 | 
			
		||||
      "pmp",
 | 
			
		||||
      "png_pipe",
 | 
			
		||||
      "png",
 | 
			
		||||
      "ppm",
 | 
			
		||||
      "ppm_pipe",
 | 
			
		||||
      "pp_bnk",
 | 
			
		||||
      "psd_pipe",
 | 
			
		||||
      "pp",
 | 
			
		||||
      "psd",
 | 
			
		||||
      "psm",
 | 
			
		||||
      "psp",
 | 
			
		||||
      "psxstr",
 | 
			
		||||
@@ -323,7 +323,7 @@ export const properties = {
 | 
			
		||||
      "pvf",
 | 
			
		||||
      "qcif",
 | 
			
		||||
      "qcp",
 | 
			
		||||
      "qdraw_pipe",
 | 
			
		||||
      "qdraw",
 | 
			
		||||
      "r3d",
 | 
			
		||||
      "rawvideo",
 | 
			
		||||
      "rco",
 | 
			
		||||
@@ -335,6 +335,7 @@ export const properties = {
 | 
			
		||||
      "rm",
 | 
			
		||||
      "roq",
 | 
			
		||||
      "rpl",
 | 
			
		||||
      "rka",
 | 
			
		||||
      "rsd",
 | 
			
		||||
      "rso",
 | 
			
		||||
      "rt",
 | 
			
		||||
@@ -355,6 +356,7 @@ export const properties = {
 | 
			
		||||
      "sbc",
 | 
			
		||||
      "sbg",
 | 
			
		||||
      "scc",
 | 
			
		||||
      "sdns",
 | 
			
		||||
      "sdp",
 | 
			
		||||
      "sdr2",
 | 
			
		||||
      "sds",
 | 
			
		||||
@@ -364,10 +366,9 @@ export const properties = {
 | 
			
		||||
      "sfx",
 | 
			
		||||
      "sfx2",
 | 
			
		||||
      "sga",
 | 
			
		||||
      "sgi_pipe",
 | 
			
		||||
      "sgi",
 | 
			
		||||
      "shn",
 | 
			
		||||
      "siff",
 | 
			
		||||
      "simbiosis_imx",
 | 
			
		||||
      "sln",
 | 
			
		||||
      "smi",
 | 
			
		||||
      "smjpeg",
 | 
			
		||||
@@ -389,12 +390,9 @@ export const properties = {
 | 
			
		||||
      "stp",
 | 
			
		||||
      "str",
 | 
			
		||||
      "sub",
 | 
			
		||||
      "subviewer",
 | 
			
		||||
      "subviewer1",
 | 
			
		||||
      "sunrast_pipe",
 | 
			
		||||
      "sup",
 | 
			
		||||
      "svag",
 | 
			
		||||
      "svg_pipe",
 | 
			
		||||
      "svg",
 | 
			
		||||
      "svs",
 | 
			
		||||
      "sw",
 | 
			
		||||
      "swf",
 | 
			
		||||
@@ -404,7 +402,8 @@ export const properties = {
 | 
			
		||||
      "thd",
 | 
			
		||||
      "thp",
 | 
			
		||||
      "tiertexseq",
 | 
			
		||||
      "tiff_pipe",
 | 
			
		||||
      "tif",
 | 
			
		||||
      "tiff",
 | 
			
		||||
      "tmv",
 | 
			
		||||
      "truehd",
 | 
			
		||||
      "tta",
 | 
			
		||||
@@ -424,6 +423,7 @@ export const properties = {
 | 
			
		||||
      "ul",
 | 
			
		||||
      "ult",
 | 
			
		||||
      "umx",
 | 
			
		||||
      "usm",
 | 
			
		||||
      "uw",
 | 
			
		||||
      "v",
 | 
			
		||||
      "v210",
 | 
			
		||||
@@ -447,12 +447,14 @@ export const properties = {
 | 
			
		||||
      "vql",
 | 
			
		||||
      "vt",
 | 
			
		||||
      "vtt",
 | 
			
		||||
      "vvc",
 | 
			
		||||
      "w64",
 | 
			
		||||
      "wa",
 | 
			
		||||
      "wav",
 | 
			
		||||
      "way",
 | 
			
		||||
      "wc3movie",
 | 
			
		||||
      "webm",
 | 
			
		||||
      "webm_dash_manifest",
 | 
			
		||||
      "webp_pipe",
 | 
			
		||||
      "webp",
 | 
			
		||||
      "webvtt",
 | 
			
		||||
      "wow",
 | 
			
		||||
      "wsaud",
 | 
			
		||||
@@ -464,32 +466,31 @@ export const properties = {
 | 
			
		||||
      "x11grab",
 | 
			
		||||
      "xa",
 | 
			
		||||
      "xbin",
 | 
			
		||||
      "xbm_pipe",
 | 
			
		||||
      "xl",
 | 
			
		||||
      "xm",
 | 
			
		||||
      "xmd",
 | 
			
		||||
      "xmv",
 | 
			
		||||
      "xpk",
 | 
			
		||||
      "xpm_pipe",
 | 
			
		||||
      "xvag",
 | 
			
		||||
      "xwd_pipe",
 | 
			
		||||
      "xwma",
 | 
			
		||||
      "y4m",
 | 
			
		||||
      "yop",
 | 
			
		||||
      "yuv",
 | 
			
		||||
      "yuv10",
 | 
			
		||||
      "yuv4mpegpipe",
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  to: {
 | 
			
		||||
    muxer: [
 | 
			
		||||
      "264",
 | 
			
		||||
      "265",
 | 
			
		||||
      "266",
 | 
			
		||||
      "302",
 | 
			
		||||
      "3g2",
 | 
			
		||||
      "3gp",
 | 
			
		||||
      "a64",
 | 
			
		||||
      "aac",
 | 
			
		||||
      "ac3",
 | 
			
		||||
      "ac4",
 | 
			
		||||
      "adts",
 | 
			
		||||
      "adx",
 | 
			
		||||
      "afc",
 | 
			
		||||
@@ -497,43 +498,33 @@ export const properties = {
 | 
			
		||||
      "aifc",
 | 
			
		||||
      "aiff",
 | 
			
		||||
      "al",
 | 
			
		||||
      "alaw",
 | 
			
		||||
      "alp",
 | 
			
		||||
      "alsa",
 | 
			
		||||
      "amr",
 | 
			
		||||
      "amv",
 | 
			
		||||
      "apm",
 | 
			
		||||
      "apng",
 | 
			
		||||
      "aptx",
 | 
			
		||||
      "aptxhd",
 | 
			
		||||
      "aptx_hd",
 | 
			
		||||
      "argo_asf",
 | 
			
		||||
      "asf",
 | 
			
		||||
      "asf_stream",
 | 
			
		||||
      "ass",
 | 
			
		||||
      "ast",
 | 
			
		||||
      "au",
 | 
			
		||||
      "aud",
 | 
			
		||||
      "av1.mkv",
 | 
			
		||||
      "av1.mp4",
 | 
			
		||||
      "avi",
 | 
			
		||||
      "avm2",
 | 
			
		||||
      "avif",
 | 
			
		||||
      "avs",
 | 
			
		||||
      "avs2",
 | 
			
		||||
      "avs3",
 | 
			
		||||
      "bit",
 | 
			
		||||
      "bmp",
 | 
			
		||||
      "c2",
 | 
			
		||||
      "caca",
 | 
			
		||||
      "caf",
 | 
			
		||||
      "cavs",
 | 
			
		||||
      "cavsvideo",
 | 
			
		||||
      "chk",
 | 
			
		||||
      "chromaprint",
 | 
			
		||||
      "codec2",
 | 
			
		||||
      "codec2raw",
 | 
			
		||||
      "cpk",
 | 
			
		||||
      "crc",
 | 
			
		||||
      "dash",
 | 
			
		||||
      "data",
 | 
			
		||||
      "daud",
 | 
			
		||||
      "dirac",
 | 
			
		||||
      "cvg",
 | 
			
		||||
      "dfpwm",
 | 
			
		||||
      "dnxhd",
 | 
			
		||||
      "dnxhr",
 | 
			
		||||
      "dpx",
 | 
			
		||||
@@ -542,63 +533,45 @@ export const properties = {
 | 
			
		||||
      "dv",
 | 
			
		||||
      "dvd",
 | 
			
		||||
      "eac3",
 | 
			
		||||
      "ec3",
 | 
			
		||||
      "evc",
 | 
			
		||||
      "exr",
 | 
			
		||||
      "f32be",
 | 
			
		||||
      "f32le",
 | 
			
		||||
      "f4v",
 | 
			
		||||
      "f64be",
 | 
			
		||||
      "f64le",
 | 
			
		||||
      "fbdev",
 | 
			
		||||
      "ffmeta",
 | 
			
		||||
      "ffmetadata",
 | 
			
		||||
      "fifo",
 | 
			
		||||
      "fifo_test",
 | 
			
		||||
      "filmstrip",
 | 
			
		||||
      "film_cpk",
 | 
			
		||||
      "fits",
 | 
			
		||||
      "flac",
 | 
			
		||||
      "flm",
 | 
			
		||||
      "flv",
 | 
			
		||||
      "framecrc",
 | 
			
		||||
      "framehash",
 | 
			
		||||
      "framemd5",
 | 
			
		||||
      "g722",
 | 
			
		||||
      "g723_1",
 | 
			
		||||
      "g726",
 | 
			
		||||
      "g726le",
 | 
			
		||||
      "gif",
 | 
			
		||||
      "gsm",
 | 
			
		||||
      "gxf",
 | 
			
		||||
      "h261",
 | 
			
		||||
      "h263",
 | 
			
		||||
      "h264",
 | 
			
		||||
      "h265",
 | 
			
		||||
      "hash",
 | 
			
		||||
      "hds",
 | 
			
		||||
      "h264.mkv",
 | 
			
		||||
      "h264.mp4",
 | 
			
		||||
      "h265.mkv",
 | 
			
		||||
      "h265.mp4",
 | 
			
		||||
      "h266.mkv",
 | 
			
		||||
      "hdr",
 | 
			
		||||
      "hevc",
 | 
			
		||||
      "hls",
 | 
			
		||||
      "ico",
 | 
			
		||||
      "ilbc",
 | 
			
		||||
      "im1",
 | 
			
		||||
      "im24",
 | 
			
		||||
      "im8",
 | 
			
		||||
      "image2",
 | 
			
		||||
      "image2pipe",
 | 
			
		||||
      "ipod",
 | 
			
		||||
      "ircam",
 | 
			
		||||
      "isma",
 | 
			
		||||
      "ismv",
 | 
			
		||||
      "ivf",
 | 
			
		||||
      "j2c",
 | 
			
		||||
      "j2k",
 | 
			
		||||
      "jacosub",
 | 
			
		||||
      "jls",
 | 
			
		||||
      "jp2",
 | 
			
		||||
      "jpeg",
 | 
			
		||||
      "jpg",
 | 
			
		||||
      "js",
 | 
			
		||||
      "jss",
 | 
			
		||||
      "kvag",
 | 
			
		||||
      "jxl",
 | 
			
		||||
      "latm",
 | 
			
		||||
      "lbc",
 | 
			
		||||
      "ljpg",
 | 
			
		||||
@@ -613,13 +586,9 @@ export const properties = {
 | 
			
		||||
      "m4a",
 | 
			
		||||
      "m4b",
 | 
			
		||||
      "m4v",
 | 
			
		||||
      "matroska",
 | 
			
		||||
      "md5",
 | 
			
		||||
      "microdvd",
 | 
			
		||||
      "mjpeg",
 | 
			
		||||
      "mjpg",
 | 
			
		||||
      "mkv",
 | 
			
		||||
      "mkvtimestamp_v2",
 | 
			
		||||
      "mlp",
 | 
			
		||||
      "mmf",
 | 
			
		||||
      "mov",
 | 
			
		||||
@@ -629,26 +598,17 @@ export const properties = {
 | 
			
		||||
      "mpa",
 | 
			
		||||
      "mpd",
 | 
			
		||||
      "mpeg",
 | 
			
		||||
      "mpeg1video",
 | 
			
		||||
      "mpeg2video",
 | 
			
		||||
      "mpegts",
 | 
			
		||||
      "mpg",
 | 
			
		||||
      "mpjpeg",
 | 
			
		||||
      "msbc",
 | 
			
		||||
      "mts",
 | 
			
		||||
      "mulaw",
 | 
			
		||||
      "mxf",
 | 
			
		||||
      "mxf_d10",
 | 
			
		||||
      "mxf_opatom",
 | 
			
		||||
      "null",
 | 
			
		||||
      "nut",
 | 
			
		||||
      "obu",
 | 
			
		||||
      "oga",
 | 
			
		||||
      "ogg",
 | 
			
		||||
      "ogv",
 | 
			
		||||
      "oma",
 | 
			
		||||
      "opengl",
 | 
			
		||||
      "opus",
 | 
			
		||||
      "oss",
 | 
			
		||||
      "pam",
 | 
			
		||||
      "pbm",
 | 
			
		||||
      "pcm",
 | 
			
		||||
@@ -656,14 +616,14 @@ export const properties = {
 | 
			
		||||
      "pfm",
 | 
			
		||||
      "pgm",
 | 
			
		||||
      "pgmyuv",
 | 
			
		||||
      "phm",
 | 
			
		||||
      "pix",
 | 
			
		||||
      "png",
 | 
			
		||||
      "ppm",
 | 
			
		||||
      "psp",
 | 
			
		||||
      "pulse",
 | 
			
		||||
      "qoi",
 | 
			
		||||
      "ra",
 | 
			
		||||
      "ras",
 | 
			
		||||
      "rawvideo",
 | 
			
		||||
      "rco",
 | 
			
		||||
      "rcv",
 | 
			
		||||
      "rgb",
 | 
			
		||||
@@ -671,84 +631,47 @@ export const properties = {
 | 
			
		||||
      "roq",
 | 
			
		||||
      "rs",
 | 
			
		||||
      "rso",
 | 
			
		||||
      "rtp",
 | 
			
		||||
      "rtp_mpegts",
 | 
			
		||||
      "rtsp",
 | 
			
		||||
      "s16be",
 | 
			
		||||
      "s16le",
 | 
			
		||||
      "s24be",
 | 
			
		||||
      "s24le",
 | 
			
		||||
      "s32be",
 | 
			
		||||
      "s32le",
 | 
			
		||||
      "s8",
 | 
			
		||||
      "sap",
 | 
			
		||||
      "sb",
 | 
			
		||||
      "sbc",
 | 
			
		||||
      "scc",
 | 
			
		||||
      "sdl",
 | 
			
		||||
      "sdl2",
 | 
			
		||||
      "segment",
 | 
			
		||||
      "sf",
 | 
			
		||||
      "sgi",
 | 
			
		||||
      "singlejpeg",
 | 
			
		||||
      "smjpeg",
 | 
			
		||||
      "smoothstreaming",
 | 
			
		||||
      "sndio",
 | 
			
		||||
      "sox",
 | 
			
		||||
      "spdif",
 | 
			
		||||
      "spx",
 | 
			
		||||
      "srt",
 | 
			
		||||
      "ssa",
 | 
			
		||||
      "ssegment",
 | 
			
		||||
      "streamhash",
 | 
			
		||||
      "stream_segment",
 | 
			
		||||
      "sub",
 | 
			
		||||
      "sun",
 | 
			
		||||
      "sunras",
 | 
			
		||||
      "sup",
 | 
			
		||||
      "svcd",
 | 
			
		||||
      "sw",
 | 
			
		||||
      "swf",
 | 
			
		||||
      "tco",
 | 
			
		||||
      "tee",
 | 
			
		||||
      "tga",
 | 
			
		||||
      "thd",
 | 
			
		||||
      "tif",
 | 
			
		||||
      "tiff",
 | 
			
		||||
      "truehd",
 | 
			
		||||
      "ts",
 | 
			
		||||
      "tta",
 | 
			
		||||
      "ttml",
 | 
			
		||||
      "tun",
 | 
			
		||||
      "u16be",
 | 
			
		||||
      "u16le",
 | 
			
		||||
      "u24be",
 | 
			
		||||
      "u24le",
 | 
			
		||||
      "u32be",
 | 
			
		||||
      "u32le",
 | 
			
		||||
      "u8",
 | 
			
		||||
      "ub",
 | 
			
		||||
      "ul",
 | 
			
		||||
      "uncodedframecrc",
 | 
			
		||||
      "uw",
 | 
			
		||||
      "v4l2",
 | 
			
		||||
      "vag",
 | 
			
		||||
      "vbn",
 | 
			
		||||
      "vc1",
 | 
			
		||||
      "vc1test",
 | 
			
		||||
      "vc2",
 | 
			
		||||
      "vcd",
 | 
			
		||||
      "vidc",
 | 
			
		||||
      "video4linux2",
 | 
			
		||||
      "vob",
 | 
			
		||||
      "voc",
 | 
			
		||||
      "vtt",
 | 
			
		||||
      "vvc",
 | 
			
		||||
      "w64",
 | 
			
		||||
      "wav",
 | 
			
		||||
      "wbmp",
 | 
			
		||||
      "webm",
 | 
			
		||||
      "webm_chunk",
 | 
			
		||||
      "webm_dash_manifest",
 | 
			
		||||
      "webp",
 | 
			
		||||
      "webvtt",
 | 
			
		||||
      "wma",
 | 
			
		||||
      "wmv",
 | 
			
		||||
      "wtv",
 | 
			
		||||
@@ -756,12 +679,10 @@ export const properties = {
 | 
			
		||||
      "xbm",
 | 
			
		||||
      "xface",
 | 
			
		||||
      "xml",
 | 
			
		||||
      "xv",
 | 
			
		||||
      "xwd",
 | 
			
		||||
      "y",
 | 
			
		||||
      "y4m",
 | 
			
		||||
      "yuv",
 | 
			
		||||
      "yuv4mpegpipe",
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
@@ -771,58 +692,64 @@ export async function convert(
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  // biome-ignore lint/suspicious/noExplicitAny: <explanation>
 | 
			
		||||
  options?: any,
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal, // to make it mockable
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  // let command = "ffmpeg";
 | 
			
		||||
  let extraArgs: string[] = [];
 | 
			
		||||
  let message = "Done";
 | 
			
		||||
 | 
			
		||||
  // these are containers that can contain multiple formats
 | 
			
		||||
  // const autoDetect = [
 | 
			
		||||
  //   "mp4",
 | 
			
		||||
  //   "mkv",
 | 
			
		||||
  //   "avi",
 | 
			
		||||
  //   "mov",
 | 
			
		||||
  //   "m4a",
 | 
			
		||||
  //   "3gp",
 | 
			
		||||
  //   "3g2",
 | 
			
		||||
  //   "mj2",
 | 
			
		||||
  //   "psp",
 | 
			
		||||
  //   "m4b",
 | 
			
		||||
  //   "ism",
 | 
			
		||||
  //   "ismv",
 | 
			
		||||
  //   "isma",
 | 
			
		||||
  //   "f4v",
 | 
			
		||||
  // ];
 | 
			
		||||
  if (convertTo === "ico") {
 | 
			
		||||
    // make sure image is 256x256 or smaller
 | 
			
		||||
    extraArgs = [
 | 
			
		||||
      "-filter:v",
 | 
			
		||||
      "scale='min(256,iw)':min'(256,ih)':force_original_aspect_ratio=decrease",
 | 
			
		||||
    ];
 | 
			
		||||
    message = "Done: resized to 256x256";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // if (!(fileType in autoDetect)) {
 | 
			
		||||
  //   command += ` -f "${fileType}"`;
 | 
			
		||||
  // }
 | 
			
		||||
  if (convertTo.split(".").length > 1) {
 | 
			
		||||
    // support av1.mkv and av1.mp4 and h265.mp4 etc.
 | 
			
		||||
    const split = convertTo.split(".");
 | 
			
		||||
    const codec_short = split[0];
 | 
			
		||||
 | 
			
		||||
  // command += ` -i "${filePath}"`;
 | 
			
		||||
    switch (codec_short) {
 | 
			
		||||
      case "av1":
 | 
			
		||||
        extraArgs.push("-c:v", "libaom-av1");
 | 
			
		||||
        break;
 | 
			
		||||
      case "h264":
 | 
			
		||||
        extraArgs.push("-c:v", "libx264");
 | 
			
		||||
        break;
 | 
			
		||||
      case "h265":
 | 
			
		||||
        extraArgs.push("-c:v", "libx265");
 | 
			
		||||
        break;
 | 
			
		||||
      case "h266":
 | 
			
		||||
        extraArgs.push("-c:v", "libx266");
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // if (!(convertTo in autoDetect)) {
 | 
			
		||||
  //   command += ` -f "${convertTo}"`;
 | 
			
		||||
  // }
 | 
			
		||||
 | 
			
		||||
  // command += ` "${targetPath}"`;
 | 
			
		||||
 | 
			
		||||
  const command = `ffmpeg -i "${filePath}" "${targetPath}"`;
 | 
			
		||||
  // Parse FFMPEG_ARGS environment variable into array
 | 
			
		||||
  const ffmpegArgs = process.env.FFMPEG_ARGS ? process.env.FFMPEG_ARGS.split(/\s+/) : [];
 | 
			
		||||
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    exec(command, (error, stdout, stderr) => {
 | 
			
		||||
      if (error) {
 | 
			
		||||
        reject(`error: ${error}`);
 | 
			
		||||
      }
 | 
			
		||||
    execFile(
 | 
			
		||||
      "ffmpeg",
 | 
			
		||||
      [...ffmpegArgs, "-i", filePath, ...extraArgs, targetPath],
 | 
			
		||||
      (error, stdout, stderr) => {
 | 
			
		||||
        if (error) {
 | 
			
		||||
          reject(`error: ${error}`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
      if (stdout) {
 | 
			
		||||
        console.log(`stdout: ${stdout}`);
 | 
			
		||||
      }
 | 
			
		||||
        if (stdout) {
 | 
			
		||||
          console.log(`stdout: ${stdout}`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
      if (stderr) {
 | 
			
		||||
        console.error(`stderr: ${stderr}`);
 | 
			
		||||
      }
 | 
			
		||||
        if (stderr) {
 | 
			
		||||
          console.error(`stderr: ${stderr}`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
      resolve("success");
 | 
			
		||||
    });
 | 
			
		||||
        resolve(message);
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { exec } from "node:child_process";
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
@@ -143,6 +144,7 @@ export const properties = {
 | 
			
		||||
      "svgz",
 | 
			
		||||
      "text",
 | 
			
		||||
      "tga",
 | 
			
		||||
      "tif",
 | 
			
		||||
      "tiff",
 | 
			
		||||
      "tile",
 | 
			
		||||
      "tim",
 | 
			
		||||
@@ -227,7 +229,6 @@ export const properties = {
 | 
			
		||||
      "jbig",
 | 
			
		||||
      "jng",
 | 
			
		||||
      "jpeg",
 | 
			
		||||
      "jpg",
 | 
			
		||||
      "k",
 | 
			
		||||
      "m",
 | 
			
		||||
      "m2v",
 | 
			
		||||
@@ -313,27 +314,24 @@ export function convert(
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  // biome-ignore lint/suspicious/noExplicitAny: <explanation>
 | 
			
		||||
  options?: any,
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal, // to make it mockable
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    exec(
 | 
			
		||||
      `gm convert "${filePath}" "${targetPath}"`,
 | 
			
		||||
      (error, stdout, stderr) => {
 | 
			
		||||
        if (error) {
 | 
			
		||||
          reject(`error: ${error}`);
 | 
			
		||||
        }
 | 
			
		||||
    execFile("gm", ["convert", filePath, targetPath], (error, stdout, stderr) => {
 | 
			
		||||
      if (error) {
 | 
			
		||||
        reject(`error: ${error}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
        if (stdout) {
 | 
			
		||||
          console.log(`stdout: ${stdout}`);
 | 
			
		||||
        }
 | 
			
		||||
      if (stdout) {
 | 
			
		||||
        console.log(`stdout: ${stdout}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
        if (stderr) {
 | 
			
		||||
          console.error(`stderr: ${stderr}`);
 | 
			
		||||
        }
 | 
			
		||||
      if (stderr) {
 | 
			
		||||
        console.error(`stderr: ${stderr}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
        resolve("success");
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
      resolve("Done");
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										492
									
								
								src/converters/imagemagick.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,492 @@
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
// declare possible conversions
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
    images: [
 | 
			
		||||
      "3fr",
 | 
			
		||||
      "3g2",
 | 
			
		||||
      "3gp",
 | 
			
		||||
      "aai",
 | 
			
		||||
      "ai",
 | 
			
		||||
      "apng",
 | 
			
		||||
      "art",
 | 
			
		||||
      "arw",
 | 
			
		||||
      "avci",
 | 
			
		||||
      "avi",
 | 
			
		||||
      "avif",
 | 
			
		||||
      "avs",
 | 
			
		||||
      "bayer",
 | 
			
		||||
      "bayera",
 | 
			
		||||
      "bgr",
 | 
			
		||||
      "bgra",
 | 
			
		||||
      "bgro",
 | 
			
		||||
      "bmp",
 | 
			
		||||
      "bmp2",
 | 
			
		||||
      "bmp3",
 | 
			
		||||
      "cal",
 | 
			
		||||
      "cals",
 | 
			
		||||
      "canvas",
 | 
			
		||||
      "caption",
 | 
			
		||||
      "cin",
 | 
			
		||||
      "clip",
 | 
			
		||||
      "clipboard",
 | 
			
		||||
      "cmyk",
 | 
			
		||||
      "cmyka",
 | 
			
		||||
      "cr2",
 | 
			
		||||
      "cr3",
 | 
			
		||||
      "crw",
 | 
			
		||||
      "cube",
 | 
			
		||||
      "cur",
 | 
			
		||||
      "cut",
 | 
			
		||||
      "data",
 | 
			
		||||
      "dcm",
 | 
			
		||||
      "dcr",
 | 
			
		||||
      "dcraw",
 | 
			
		||||
      "dcx",
 | 
			
		||||
      "dds",
 | 
			
		||||
      "dfont",
 | 
			
		||||
      "dng",
 | 
			
		||||
      "dpx",
 | 
			
		||||
      "dxt1",
 | 
			
		||||
      "dxt5",
 | 
			
		||||
      "emf",
 | 
			
		||||
      "epdf",
 | 
			
		||||
      "epi",
 | 
			
		||||
      "eps",
 | 
			
		||||
      "epsf",
 | 
			
		||||
      "epsi",
 | 
			
		||||
      "ept",
 | 
			
		||||
      "ept2",
 | 
			
		||||
      "ept3",
 | 
			
		||||
      "erf",
 | 
			
		||||
      "exr",
 | 
			
		||||
      "farbfeld",
 | 
			
		||||
      "fax",
 | 
			
		||||
      "ff",
 | 
			
		||||
      "fff",
 | 
			
		||||
      "file",
 | 
			
		||||
      "fits",
 | 
			
		||||
      "fl32",
 | 
			
		||||
      "flif",
 | 
			
		||||
      "flv",
 | 
			
		||||
      "fractal",
 | 
			
		||||
      "ftp",
 | 
			
		||||
      "fts",
 | 
			
		||||
      "ftxt",
 | 
			
		||||
      "g3",
 | 
			
		||||
      "g4",
 | 
			
		||||
      "gif",
 | 
			
		||||
      "gif87",
 | 
			
		||||
      "gradient",
 | 
			
		||||
      "gray",
 | 
			
		||||
      "graya",
 | 
			
		||||
      "group4",
 | 
			
		||||
      "hald",
 | 
			
		||||
      "hdr",
 | 
			
		||||
      "heic",
 | 
			
		||||
      "heif",
 | 
			
		||||
      "hrz",
 | 
			
		||||
      "http",
 | 
			
		||||
      "https",
 | 
			
		||||
      "icb",
 | 
			
		||||
      "ico",
 | 
			
		||||
      "icon",
 | 
			
		||||
      "iiq",
 | 
			
		||||
      "inline",
 | 
			
		||||
      "ipl",
 | 
			
		||||
      "j2c",
 | 
			
		||||
      "j2k",
 | 
			
		||||
      "jng",
 | 
			
		||||
      "jnx",
 | 
			
		||||
      "jp2",
 | 
			
		||||
      "jpc",
 | 
			
		||||
      "jpe",
 | 
			
		||||
      "jpeg",
 | 
			
		||||
      "jpg",
 | 
			
		||||
      "jpm",
 | 
			
		||||
      "jps",
 | 
			
		||||
      "jpt",
 | 
			
		||||
      "jxl",
 | 
			
		||||
      "k25",
 | 
			
		||||
      "kdc",
 | 
			
		||||
      "label",
 | 
			
		||||
      "m2v",
 | 
			
		||||
      "m4v",
 | 
			
		||||
      "mac",
 | 
			
		||||
      "map",
 | 
			
		||||
      "mask",
 | 
			
		||||
      "mat",
 | 
			
		||||
      "mdc",
 | 
			
		||||
      "mef",
 | 
			
		||||
      "miff",
 | 
			
		||||
      "mkv",
 | 
			
		||||
      "mng",
 | 
			
		||||
      "mono",
 | 
			
		||||
      "mos",
 | 
			
		||||
      "mov",
 | 
			
		||||
      "mp4",
 | 
			
		||||
      "mpc",
 | 
			
		||||
      "mpeg",
 | 
			
		||||
      "mpg",
 | 
			
		||||
      "mpo",
 | 
			
		||||
      "mrw",
 | 
			
		||||
      "msl",
 | 
			
		||||
      "msvg",
 | 
			
		||||
      "mtv",
 | 
			
		||||
      "mvg",
 | 
			
		||||
      "nef",
 | 
			
		||||
      "nrw",
 | 
			
		||||
      "null",
 | 
			
		||||
      "ora",
 | 
			
		||||
      "orf",
 | 
			
		||||
      "otb",
 | 
			
		||||
      "otf",
 | 
			
		||||
      "pal",
 | 
			
		||||
      "palm",
 | 
			
		||||
      "pam",
 | 
			
		||||
      "pango",
 | 
			
		||||
      "pattern",
 | 
			
		||||
      "pbm",
 | 
			
		||||
      "pcd",
 | 
			
		||||
      "pcds",
 | 
			
		||||
      "pcl",
 | 
			
		||||
      "pct",
 | 
			
		||||
      "pcx",
 | 
			
		||||
      "pdb",
 | 
			
		||||
      "pdf",
 | 
			
		||||
      "pdfa",
 | 
			
		||||
      "pef",
 | 
			
		||||
      "pes",
 | 
			
		||||
      "pfa",
 | 
			
		||||
      "pfb",
 | 
			
		||||
      "pfm",
 | 
			
		||||
      "pgm",
 | 
			
		||||
      "pgx",
 | 
			
		||||
      "phm",
 | 
			
		||||
      "picon",
 | 
			
		||||
      "pict",
 | 
			
		||||
      "pix",
 | 
			
		||||
      "pjpeg",
 | 
			
		||||
      "plasma",
 | 
			
		||||
      "png",
 | 
			
		||||
      "png00",
 | 
			
		||||
      "png24",
 | 
			
		||||
      "png32",
 | 
			
		||||
      "png48",
 | 
			
		||||
      "png64",
 | 
			
		||||
      "png8",
 | 
			
		||||
      "pnm",
 | 
			
		||||
      "pocketmod",
 | 
			
		||||
      "ppm",
 | 
			
		||||
      "ps",
 | 
			
		||||
      "psb",
 | 
			
		||||
      "psd",
 | 
			
		||||
      "ptif",
 | 
			
		||||
      "pwp",
 | 
			
		||||
      "qoi",
 | 
			
		||||
      "radial",
 | 
			
		||||
      "raf",
 | 
			
		||||
      "ras",
 | 
			
		||||
      "raw",
 | 
			
		||||
      "rgb",
 | 
			
		||||
      "rgb565",
 | 
			
		||||
      "rgba",
 | 
			
		||||
      "rgbo",
 | 
			
		||||
      "rgf",
 | 
			
		||||
      "rla",
 | 
			
		||||
      "rle",
 | 
			
		||||
      "rmf",
 | 
			
		||||
      "rsvg",
 | 
			
		||||
      "rw2",
 | 
			
		||||
      "rwl",
 | 
			
		||||
      "scr",
 | 
			
		||||
      "screenshot",
 | 
			
		||||
      "sct",
 | 
			
		||||
      "sfw",
 | 
			
		||||
      "sgi",
 | 
			
		||||
      "six",
 | 
			
		||||
      "sixel",
 | 
			
		||||
      "sr2",
 | 
			
		||||
      "srf",
 | 
			
		||||
      "srw",
 | 
			
		||||
      "stegano",
 | 
			
		||||
      "sti",
 | 
			
		||||
      "strimg",
 | 
			
		||||
      "sun",
 | 
			
		||||
      "svg",
 | 
			
		||||
      "svgz",
 | 
			
		||||
      "text",
 | 
			
		||||
      "tga",
 | 
			
		||||
      "tiff",
 | 
			
		||||
      "tiff64",
 | 
			
		||||
      "tile",
 | 
			
		||||
      "tim",
 | 
			
		||||
      "tm2",
 | 
			
		||||
      "ttc",
 | 
			
		||||
      "ttf",
 | 
			
		||||
      "txt",
 | 
			
		||||
      "uyvy",
 | 
			
		||||
      "vda",
 | 
			
		||||
      "vicar",
 | 
			
		||||
      "vid",
 | 
			
		||||
      "viff",
 | 
			
		||||
      "vips",
 | 
			
		||||
      "vst",
 | 
			
		||||
      "wbmp",
 | 
			
		||||
      "webm",
 | 
			
		||||
      "webp",
 | 
			
		||||
      "wmf",
 | 
			
		||||
      "wmv",
 | 
			
		||||
      "wpg",
 | 
			
		||||
      "x3f",
 | 
			
		||||
      "xbm",
 | 
			
		||||
      "xc",
 | 
			
		||||
      "xcf",
 | 
			
		||||
      "xpm",
 | 
			
		||||
      "xps",
 | 
			
		||||
      "xv",
 | 
			
		||||
      "ycbcr",
 | 
			
		||||
      "ycbcra",
 | 
			
		||||
      "yuv",
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  to: {
 | 
			
		||||
    images: [
 | 
			
		||||
      "aai",
 | 
			
		||||
      "ai",
 | 
			
		||||
      "apng",
 | 
			
		||||
      "art",
 | 
			
		||||
      "ashlar",
 | 
			
		||||
      "avif",
 | 
			
		||||
      "avs",
 | 
			
		||||
      "bayer",
 | 
			
		||||
      "bayera",
 | 
			
		||||
      "bgr",
 | 
			
		||||
      "bgra",
 | 
			
		||||
      "bgro",
 | 
			
		||||
      "bmp",
 | 
			
		||||
      "bmp2",
 | 
			
		||||
      "bmp3",
 | 
			
		||||
      "brf",
 | 
			
		||||
      "cal",
 | 
			
		||||
      "cals",
 | 
			
		||||
      "cin",
 | 
			
		||||
      "cip",
 | 
			
		||||
      "clip",
 | 
			
		||||
      "clipboard",
 | 
			
		||||
      "cmyk",
 | 
			
		||||
      "cmyka",
 | 
			
		||||
      "cur",
 | 
			
		||||
      "data",
 | 
			
		||||
      "dcx",
 | 
			
		||||
      "dds",
 | 
			
		||||
      "dpx",
 | 
			
		||||
      "dxt1",
 | 
			
		||||
      "dxt5",
 | 
			
		||||
      "epdf",
 | 
			
		||||
      "epi",
 | 
			
		||||
      "eps",
 | 
			
		||||
      "eps2",
 | 
			
		||||
      "eps3",
 | 
			
		||||
      "epsf",
 | 
			
		||||
      "epsi",
 | 
			
		||||
      "ept",
 | 
			
		||||
      "ept2",
 | 
			
		||||
      "ept3",
 | 
			
		||||
      "exr",
 | 
			
		||||
      "farbfeld",
 | 
			
		||||
      "fax",
 | 
			
		||||
      "ff",
 | 
			
		||||
      "fits",
 | 
			
		||||
      "fl32",
 | 
			
		||||
      "flif",
 | 
			
		||||
      "flv",
 | 
			
		||||
      "fts",
 | 
			
		||||
      "ftxt",
 | 
			
		||||
      "g3",
 | 
			
		||||
      "g4",
 | 
			
		||||
      "gif",
 | 
			
		||||
      "gif87",
 | 
			
		||||
      "gray",
 | 
			
		||||
      "graya",
 | 
			
		||||
      "group4",
 | 
			
		||||
      "hdr",
 | 
			
		||||
      "histogram",
 | 
			
		||||
      "hrz",
 | 
			
		||||
      "htm",
 | 
			
		||||
      "html",
 | 
			
		||||
      "icb",
 | 
			
		||||
      "ico",
 | 
			
		||||
      "icon",
 | 
			
		||||
      "info",
 | 
			
		||||
      "inline",
 | 
			
		||||
      "ipl",
 | 
			
		||||
      "isobrl",
 | 
			
		||||
      "isobrl6",
 | 
			
		||||
      "j2c",
 | 
			
		||||
      "j2k",
 | 
			
		||||
      "jng",
 | 
			
		||||
      "jp2",
 | 
			
		||||
      "jpc",
 | 
			
		||||
      "jpe",
 | 
			
		||||
      "jpeg",
 | 
			
		||||
      "jpg",
 | 
			
		||||
      "jpm",
 | 
			
		||||
      "jps",
 | 
			
		||||
      "jpt",
 | 
			
		||||
      "json",
 | 
			
		||||
      "jxl",
 | 
			
		||||
      "m2v",
 | 
			
		||||
      "m4v",
 | 
			
		||||
      "map",
 | 
			
		||||
      "mask",
 | 
			
		||||
      "mat",
 | 
			
		||||
      "matte",
 | 
			
		||||
      "miff",
 | 
			
		||||
      "mkv",
 | 
			
		||||
      "mng",
 | 
			
		||||
      "mono",
 | 
			
		||||
      "mov",
 | 
			
		||||
      "mp4",
 | 
			
		||||
      "mpc",
 | 
			
		||||
      "mpeg",
 | 
			
		||||
      "mpg",
 | 
			
		||||
      "msl",
 | 
			
		||||
      "msvg",
 | 
			
		||||
      "mtv",
 | 
			
		||||
      "mvg",
 | 
			
		||||
      "null",
 | 
			
		||||
      "otb",
 | 
			
		||||
      "pal",
 | 
			
		||||
      "palm",
 | 
			
		||||
      "pam",
 | 
			
		||||
      "pbm",
 | 
			
		||||
      "pcd",
 | 
			
		||||
      "pcds",
 | 
			
		||||
      "pcl",
 | 
			
		||||
      "pct",
 | 
			
		||||
      "pcx",
 | 
			
		||||
      "pdb",
 | 
			
		||||
      "pdf",
 | 
			
		||||
      "pdfa",
 | 
			
		||||
      "pfm",
 | 
			
		||||
      "pgm",
 | 
			
		||||
      "pgx",
 | 
			
		||||
      "phm",
 | 
			
		||||
      "picon",
 | 
			
		||||
      "pict",
 | 
			
		||||
      "pjpeg",
 | 
			
		||||
      "png",
 | 
			
		||||
      "png00",
 | 
			
		||||
      "png24",
 | 
			
		||||
      "png32",
 | 
			
		||||
      "png48",
 | 
			
		||||
      "png64",
 | 
			
		||||
      "png8",
 | 
			
		||||
      "pnm",
 | 
			
		||||
      "pocketmod",
 | 
			
		||||
      "ppm",
 | 
			
		||||
      "ps",
 | 
			
		||||
      "ps2",
 | 
			
		||||
      "ps3",
 | 
			
		||||
      "psb",
 | 
			
		||||
      "psd",
 | 
			
		||||
      "ptif",
 | 
			
		||||
      "qoi",
 | 
			
		||||
      "ras",
 | 
			
		||||
      "rgb",
 | 
			
		||||
      "rgba",
 | 
			
		||||
      "rgbo",
 | 
			
		||||
      "rgf",
 | 
			
		||||
      "rsvg",
 | 
			
		||||
      "sgi",
 | 
			
		||||
      "shtml",
 | 
			
		||||
      "six",
 | 
			
		||||
      "sixel",
 | 
			
		||||
      "sparse",
 | 
			
		||||
      "strimg",
 | 
			
		||||
      "sun",
 | 
			
		||||
      "svg",
 | 
			
		||||
      "svgz",
 | 
			
		||||
      "tga",
 | 
			
		||||
      "thumbnail",
 | 
			
		||||
      "tiff",
 | 
			
		||||
      "tiff64",
 | 
			
		||||
      "txt",
 | 
			
		||||
      "ubrl",
 | 
			
		||||
      "ubrl6",
 | 
			
		||||
      "uil",
 | 
			
		||||
      "uyvy",
 | 
			
		||||
      "vda",
 | 
			
		||||
      "vicar",
 | 
			
		||||
      "vid",
 | 
			
		||||
      "viff",
 | 
			
		||||
      "vips",
 | 
			
		||||
      "vst",
 | 
			
		||||
      "wbmp",
 | 
			
		||||
      "webm",
 | 
			
		||||
      "webp",
 | 
			
		||||
      "wmv",
 | 
			
		||||
      "wpg",
 | 
			
		||||
      "xbm",
 | 
			
		||||
      "xpm",
 | 
			
		||||
      "xv",
 | 
			
		||||
      "yaml",
 | 
			
		||||
      "ycbcr",
 | 
			
		||||
      "ycbcra",
 | 
			
		||||
      "yuv",
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function convert(
 | 
			
		||||
  filePath: string,
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal, // to make it mockable
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  let outputArgs: string[] = [];
 | 
			
		||||
  let inputArgs: string[] = [];
 | 
			
		||||
 | 
			
		||||
  if (convertTo === "ico") {
 | 
			
		||||
    outputArgs = ["-define", "icon:auto-resize=256,128,64,48,32,16", "-background", "none"];
 | 
			
		||||
 | 
			
		||||
    if (fileType === "svg") {
 | 
			
		||||
      // this might be a bit too much, but it works
 | 
			
		||||
      inputArgs = ["-background", "none", "-density", "512"];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Handle EMF files specifically to avoid LibreOffice delegate issues
 | 
			
		||||
  if (fileType === "emf") {
 | 
			
		||||
    // Use direct conversion without delegates for EMF files
 | 
			
		||||
    inputArgs.push("-define", "emf:delegate=false", "-density", "300");
 | 
			
		||||
    outputArgs.push("-background", "white", "-alpha", "remove");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    execFile(
 | 
			
		||||
      "magick",
 | 
			
		||||
      [...inputArgs, filePath, ...outputArgs, targetPath],
 | 
			
		||||
      (error, stdout, stderr) => {
 | 
			
		||||
        if (error) {
 | 
			
		||||
          reject(`error: ${error}`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (stdout) {
 | 
			
		||||
          console.log(`stdout: ${stdout}`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (stderr) {
 | 
			
		||||
          console.error(`stderr: ${stderr}`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resolve("Done");
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										56
									
								
								src/converters/inkscape.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,56 @@
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
    images: ["svg", "pdf", "eps", "ps", "wmf", "emf", "png"],
 | 
			
		||||
  },
 | 
			
		||||
  to: {
 | 
			
		||||
    images: [
 | 
			
		||||
      "dxf",
 | 
			
		||||
      "emf",
 | 
			
		||||
      "eps",
 | 
			
		||||
      "fxg",
 | 
			
		||||
      "gpl",
 | 
			
		||||
      "hpgl",
 | 
			
		||||
      "html",
 | 
			
		||||
      "odg",
 | 
			
		||||
      "pdf",
 | 
			
		||||
      "png",
 | 
			
		||||
      "pov",
 | 
			
		||||
      "ps",
 | 
			
		||||
      "sif",
 | 
			
		||||
      "svg",
 | 
			
		||||
      "svgz",
 | 
			
		||||
      "tex",
 | 
			
		||||
      "wmf",
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function convert(
 | 
			
		||||
  filePath: string,
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal, // to make it mockable
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    execFile("inkscape", [filePath, "-o", targetPath], (error, stdout, stderr) => {
 | 
			
		||||
      if (error) {
 | 
			
		||||
        reject(`error: ${error}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (stdout) {
 | 
			
		||||
        console.log(`stdout: ${stdout}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (stderr) {
 | 
			
		||||
        console.error(`stderr: ${stderr}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      resolve("Done");
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										38
									
								
								src/converters/libheif.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,38 @@
 | 
			
		||||
import { execFile as execFileOriginal } from "child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
    images: ["avci", "avcs", "avif", "h264", "heic", "heics", "heif", "heifs", "hif", "mkv", "mp4"],
 | 
			
		||||
  },
 | 
			
		||||
  to: {
 | 
			
		||||
    images: ["jpeg", "png", "y4m"],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function convert(
 | 
			
		||||
  filePath: string,
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal, // to make it mockable
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    execFile("heif-convert", [filePath, targetPath], (error, stdout, stderr) => {
 | 
			
		||||
      if (error) {
 | 
			
		||||
        reject(`error: ${error}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (stdout) {
 | 
			
		||||
        console.log(`stdout: ${stdout}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (stderr) {
 | 
			
		||||
        console.error(`stderr: ${stderr}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      resolve("Done");
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
@@ -1,35 +1,14 @@
 | 
			
		||||
import { exec } from "node:child_process";
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
// declare possible conversions
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
    jxl: ["jxl"],
 | 
			
		||||
    images: [
 | 
			
		||||
      "apng",
 | 
			
		||||
      "exr",
 | 
			
		||||
      "gif",
 | 
			
		||||
      "jpeg",
 | 
			
		||||
      "pam",
 | 
			
		||||
      "pfm",
 | 
			
		||||
      "pgm",
 | 
			
		||||
      "pgx",
 | 
			
		||||
      "png",
 | 
			
		||||
      "ppm",
 | 
			
		||||
    ],
 | 
			
		||||
    images: ["apng", "exr", "gif", "jpeg", "pam", "pfm", "pgm", "pgx", "png", "ppm"],
 | 
			
		||||
  },
 | 
			
		||||
  to: {
 | 
			
		||||
    jxl: [
 | 
			
		||||
      "apng",
 | 
			
		||||
      "exr",
 | 
			
		||||
      "gif",
 | 
			
		||||
      "jpeg",
 | 
			
		||||
      "pam",
 | 
			
		||||
      "pfm",
 | 
			
		||||
      "pgm",
 | 
			
		||||
      "pgx",
 | 
			
		||||
      "png",
 | 
			
		||||
      "ppm",
 | 
			
		||||
    ],
 | 
			
		||||
    jxl: ["apng", "exr", "gif", "jpeg", "pam", "pfm", "pgm", "pgx", "png", "ppm"],
 | 
			
		||||
    images: ["jxl"],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
@@ -39,8 +18,8 @@ export function convert(
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  // biome-ignore lint/suspicious/noExplicitAny: <explanation>
 | 
			
		||||
  options?: any,
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal, // to make it mockable
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  let tool = "";
 | 
			
		||||
  if (fileType === "jxl") {
 | 
			
		||||
@@ -52,7 +31,7 @@ export function convert(
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    exec(`${tool} "${filePath}" "${targetPath}"`, (error, stdout, stderr) => {
 | 
			
		||||
    execFile(tool, [filePath, targetPath], (error, stdout, stderr) => {
 | 
			
		||||
      if (error) {
 | 
			
		||||
        reject(`error: ${error}`);
 | 
			
		||||
      }
 | 
			
		||||
@@ -65,7 +44,7 @@ export function convert(
 | 
			
		||||
        console.error(`stderr: ${stderr}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      resolve("success");
 | 
			
		||||
      resolve("Done");
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										177
									
								
								src/converters/libreoffice.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,177 @@
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
    text: [
 | 
			
		||||
      "602",
 | 
			
		||||
      "abw",
 | 
			
		||||
      "csv",
 | 
			
		||||
      "cwk",
 | 
			
		||||
      "doc",
 | 
			
		||||
      "docm",
 | 
			
		||||
      "docx",
 | 
			
		||||
      "dot",
 | 
			
		||||
      "dotx",
 | 
			
		||||
      "dotm",
 | 
			
		||||
      "epub",
 | 
			
		||||
      "fb2",
 | 
			
		||||
      "fodt",
 | 
			
		||||
      "htm",
 | 
			
		||||
      "html",
 | 
			
		||||
      "hwp",
 | 
			
		||||
      "mcw",
 | 
			
		||||
      "mw",
 | 
			
		||||
      "mwd",
 | 
			
		||||
      "lwp",
 | 
			
		||||
      "lrf",
 | 
			
		||||
      "odt",
 | 
			
		||||
      "ott",
 | 
			
		||||
      "pages",
 | 
			
		||||
      "pdf",
 | 
			
		||||
      "psw",
 | 
			
		||||
      "rtf",
 | 
			
		||||
      "sdw",
 | 
			
		||||
      "stw",
 | 
			
		||||
      "sxw",
 | 
			
		||||
      "tab",
 | 
			
		||||
      "tsv",
 | 
			
		||||
      "txt",
 | 
			
		||||
      "wn",
 | 
			
		||||
      "wpd",
 | 
			
		||||
      "wps",
 | 
			
		||||
      "wpt",
 | 
			
		||||
      "wri",
 | 
			
		||||
      "xhtml",
 | 
			
		||||
      "xml",
 | 
			
		||||
      "zabw",
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  to: {
 | 
			
		||||
    text: [
 | 
			
		||||
      "csv",
 | 
			
		||||
      "doc",
 | 
			
		||||
      "docm",
 | 
			
		||||
      "docx",
 | 
			
		||||
      "dot",
 | 
			
		||||
      "dotx",
 | 
			
		||||
      "dotm",
 | 
			
		||||
      "epub",
 | 
			
		||||
      "fodt",
 | 
			
		||||
      "htm",
 | 
			
		||||
      "html",
 | 
			
		||||
      "odt",
 | 
			
		||||
      "ott",
 | 
			
		||||
      "pdf",
 | 
			
		||||
      "rtf",
 | 
			
		||||
      "tab",
 | 
			
		||||
      "tsv",
 | 
			
		||||
      "txt",
 | 
			
		||||
      "wps",
 | 
			
		||||
      "wpt",
 | 
			
		||||
      "xhtml",
 | 
			
		||||
      "xml",
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type FileCategories = "text" | "calc";
 | 
			
		||||
 | 
			
		||||
const filters: Record<FileCategories, Record<string, string>> = {
 | 
			
		||||
  text: {
 | 
			
		||||
    "602": "T602Document",
 | 
			
		||||
    abw: "AbiWord",
 | 
			
		||||
    csv: "Text",
 | 
			
		||||
    doc: "MS Word 97",
 | 
			
		||||
    docm: "MS Word 2007 XML VBA",
 | 
			
		||||
    docx: "MS Word 2007 XML",
 | 
			
		||||
    dot: "MS Word 97 Vorlage",
 | 
			
		||||
    dotx: "MS Word 2007 XML Template",
 | 
			
		||||
    dotm: "MS Word 2007 XML Template",
 | 
			
		||||
    epub: "EPUB",
 | 
			
		||||
    fb2: "Fictionbook 2",
 | 
			
		||||
    fodt: "OpenDocument Text Flat XML",
 | 
			
		||||
    htm: "HTML (StarWriter)",
 | 
			
		||||
    html: "HTML (StarWriter)",
 | 
			
		||||
    hwp: "writer_MIZI_Hwp_97",
 | 
			
		||||
    mcw: "MacWrite",
 | 
			
		||||
    mw: "MacWrite",
 | 
			
		||||
    mwd: "Mariner_Write",
 | 
			
		||||
    lwp: "LotusWordPro",
 | 
			
		||||
    lrf: "BroadBand eBook",
 | 
			
		||||
    odt: "writer8",
 | 
			
		||||
    ott: "writer8_template",
 | 
			
		||||
    pages: "Apple Pages",
 | 
			
		||||
    // pdf: "writer_pdf_import",
 | 
			
		||||
    psw: "PocketWord File",
 | 
			
		||||
    rtf: "Rich Text Format",
 | 
			
		||||
    sdw: "StarOffice_Writer",
 | 
			
		||||
    stw: "writer_StarOffice_XML_Writer_Template",
 | 
			
		||||
    sxw: "StarOffice XML (Writer)",
 | 
			
		||||
    tab: "Text",
 | 
			
		||||
    tsv: "Text",
 | 
			
		||||
    txt: "Text",
 | 
			
		||||
    wn: "WriteNow",
 | 
			
		||||
    wpd: "WordPerfect",
 | 
			
		||||
    wps: "MS Word 97",
 | 
			
		||||
    wpt: "MS Word 97 Vorlage",
 | 
			
		||||
    wri: "MS_Write",
 | 
			
		||||
    xhtml: "HTML (StarWriter)",
 | 
			
		||||
    xml: "OpenDocument Text Flat XML",
 | 
			
		||||
    zabw: "AbiWord",
 | 
			
		||||
  },
 | 
			
		||||
  calc: {},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getFilters = (fileType: string, converto: string) => {
 | 
			
		||||
  if (fileType in filters.text && converto in filters.text) {
 | 
			
		||||
    return [filters.text[fileType], filters.text[converto]];
 | 
			
		||||
  } else if (fileType in filters.calc && converto in filters.calc) {
 | 
			
		||||
    return [filters.calc[fileType], filters.calc[converto]];
 | 
			
		||||
  }
 | 
			
		||||
  return [null, null];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function convert(
 | 
			
		||||
  filePath: string,
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal,
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  const outputPath = targetPath.split("/").slice(0, -1).join("/").replace("./", "") ?? targetPath;
 | 
			
		||||
 | 
			
		||||
  // Build arguments array
 | 
			
		||||
  const args: string[] = [];
 | 
			
		||||
  args.push("--headless");
 | 
			
		||||
  const [inFilter, outFilter] = getFilters(fileType, convertTo);
 | 
			
		||||
 | 
			
		||||
  if (inFilter) {
 | 
			
		||||
    args.push(`--infilter="${inFilter}"`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (outFilter) {
 | 
			
		||||
    args.push("--convert-to", `${convertTo}:${outFilter}`, "--outdir", outputPath, filePath);
 | 
			
		||||
  } else {
 | 
			
		||||
    args.push("--convert-to", convertTo, "--outdir", outputPath, filePath);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    execFile("soffice", args, (error, stdout, stderr) => {
 | 
			
		||||
      if (error) {
 | 
			
		||||
        reject(`error: ${error}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (stdout) {
 | 
			
		||||
        console.log(`stdout: ${stdout}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (stderr) {
 | 
			
		||||
        console.error(`stderr: ${stderr}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      resolve("Done");
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
@@ -1,65 +1,64 @@
 | 
			
		||||
import { convert as convertImage, properties as propertiesImage } from "./vips";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  convert as convertPandoc,
 | 
			
		||||
  properties as propertiesPandoc,
 | 
			
		||||
} from "./pandoc";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  convert as convertFFmpeg,
 | 
			
		||||
  properties as propertiesFFmpeg,
 | 
			
		||||
} from "./ffmpeg";
 | 
			
		||||
 | 
			
		||||
import { Cookie } from "elysia";
 | 
			
		||||
import db from "../db/db";
 | 
			
		||||
import { MAX_CONVERT_PROCESS } from "../helpers/env";
 | 
			
		||||
import { normalizeFiletype, normalizeOutputFiletype } from "../helpers/normalizeFiletype";
 | 
			
		||||
import { convert as convertassimp, properties as propertiesassimp } from "./assimp";
 | 
			
		||||
import { convert as convertCalibre, properties as propertiesCalibre } from "./calibre";
 | 
			
		||||
import { convert as convertDasel, properties as propertiesDasel } from "./dasel";
 | 
			
		||||
import { convert as convertDvisvgm, properties as propertiesDvisvgm } from "./dvisvgm";
 | 
			
		||||
import { convert as convertFFmpeg, properties as propertiesFFmpeg } from "./ffmpeg";
 | 
			
		||||
import {
 | 
			
		||||
  convert as convertGraphicsmagick,
 | 
			
		||||
  properties as propertiesGraphicsmagick,
 | 
			
		||||
} from "./graphicsmagick";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  convert as convertxelatex,
 | 
			
		||||
  properties as propertiesxelatex,
 | 
			
		||||
} from "./xelatex";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  convert as convertLibjxl,
 | 
			
		||||
  properties as propertiesLibjxl,
 | 
			
		||||
} from "./libjxl";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  convert as convertresvg,
 | 
			
		||||
  properties as propertiesresvg,
 | 
			
		||||
} from "./resvg";
 | 
			
		||||
 | 
			
		||||
import { normalizeFiletype } from "../helpers/normalizeFiletype";
 | 
			
		||||
import { convert as convertImagemagick, properties as propertiesImagemagick } from "./imagemagick";
 | 
			
		||||
import { convert as convertInkscape, properties as propertiesInkscape } from "./inkscape";
 | 
			
		||||
import { convert as convertLibheif, properties as propertiesLibheif } from "./libheif";
 | 
			
		||||
import { convert as convertLibjxl, properties as propertiesLibjxl } from "./libjxl";
 | 
			
		||||
import { convert as convertLibreOffice, properties as propertiesLibreOffice } from "./libreoffice";
 | 
			
		||||
import { convert as convertMsgconvert, properties as propertiesMsgconvert } from "./msgconvert";
 | 
			
		||||
import { convert as convertPandoc, properties as propertiesPandoc } from "./pandoc";
 | 
			
		||||
import { convert as convertPotrace, properties as propertiesPotrace } from "./potrace";
 | 
			
		||||
import { convert as convertresvg, properties as propertiesresvg } from "./resvg";
 | 
			
		||||
import { convert as convertImage, properties as propertiesImage } from "./vips";
 | 
			
		||||
import { convert as convertVtracer, properties as propertiesVtracer } from "./vtracer";
 | 
			
		||||
import { convert as convertxelatex, properties as propertiesxelatex } from "./xelatex";
 | 
			
		||||
 | 
			
		||||
// This should probably be reconstructed so that the functions are not imported instead the functions hook into this to make the converters more modular
 | 
			
		||||
 | 
			
		||||
const properties: {
 | 
			
		||||
  [key: string]: {
 | 
			
		||||
const properties: Record<
 | 
			
		||||
  string,
 | 
			
		||||
  {
 | 
			
		||||
    properties: {
 | 
			
		||||
      from: { [key: string]: string[] };
 | 
			
		||||
      to: { [key: string]: string[] };
 | 
			
		||||
      options?: {
 | 
			
		||||
        [key: string]: {
 | 
			
		||||
          [key: string]: {
 | 
			
		||||
      from: Record<string, string[]>;
 | 
			
		||||
      to: Record<string, string[]>;
 | 
			
		||||
      options?: Record<
 | 
			
		||||
        string,
 | 
			
		||||
        Record<
 | 
			
		||||
          string,
 | 
			
		||||
          {
 | 
			
		||||
            description: string;
 | 
			
		||||
            type: string;
 | 
			
		||||
            default: number;
 | 
			
		||||
          };
 | 
			
		||||
        };
 | 
			
		||||
      };
 | 
			
		||||
          }
 | 
			
		||||
        >
 | 
			
		||||
      >;
 | 
			
		||||
    };
 | 
			
		||||
    converter: (
 | 
			
		||||
      filePath: string,
 | 
			
		||||
      fileType: string,
 | 
			
		||||
      convertTo: string,
 | 
			
		||||
      targetPath: string,
 | 
			
		||||
      // biome-ignore lint/suspicious/noExplicitAny: <explanation>
 | 
			
		||||
      options?: any,
 | 
			
		||||
      // biome-ignore lint/suspicious/noExplicitAny: <explanation>
 | 
			
		||||
    ) => any;
 | 
			
		||||
  };
 | 
			
		||||
} = {
 | 
			
		||||
 | 
			
		||||
      options?: unknown,
 | 
			
		||||
    ) => unknown;
 | 
			
		||||
  }
 | 
			
		||||
> = {
 | 
			
		||||
  // Prioritize Inkscape for EMF files as it handles them better than ImageMagick
 | 
			
		||||
  inkscape: {
 | 
			
		||||
    properties: propertiesInkscape,
 | 
			
		||||
    converter: convertInkscape,
 | 
			
		||||
  },
 | 
			
		||||
  libjxl: {
 | 
			
		||||
    properties: propertiesLibjxl,
 | 
			
		||||
    converter: convertLibjxl,
 | 
			
		||||
@@ -72,45 +71,130 @@ const properties: {
 | 
			
		||||
    properties: propertiesImage,
 | 
			
		||||
    converter: convertImage,
 | 
			
		||||
  },
 | 
			
		||||
  libheif: {
 | 
			
		||||
    properties: propertiesLibheif,
 | 
			
		||||
    converter: convertLibheif,
 | 
			
		||||
  },
 | 
			
		||||
  xelatex: {
 | 
			
		||||
    properties: propertiesxelatex,
 | 
			
		||||
    converter: convertxelatex,
 | 
			
		||||
  },
 | 
			
		||||
  calibre: {
 | 
			
		||||
    properties: propertiesCalibre,
 | 
			
		||||
    converter: convertCalibre,
 | 
			
		||||
  },
 | 
			
		||||
  dasel: {
 | 
			
		||||
    properties: propertiesDasel,
 | 
			
		||||
    converter: convertDasel,
 | 
			
		||||
  },
 | 
			
		||||
  libreoffice: {
 | 
			
		||||
    properties: propertiesLibreOffice,
 | 
			
		||||
    converter: convertLibreOffice,
 | 
			
		||||
  },
 | 
			
		||||
  pandoc: {
 | 
			
		||||
    properties: propertiesPandoc,
 | 
			
		||||
    converter: convertPandoc,
 | 
			
		||||
  },
 | 
			
		||||
  msgconvert: {
 | 
			
		||||
    properties: propertiesMsgconvert,
 | 
			
		||||
    converter: convertMsgconvert,
 | 
			
		||||
  },
 | 
			
		||||
  dvisvgm: {
 | 
			
		||||
    properties: propertiesDvisvgm,
 | 
			
		||||
    converter: convertDvisvgm,
 | 
			
		||||
  },
 | 
			
		||||
  imagemagick: {
 | 
			
		||||
    properties: propertiesImagemagick,
 | 
			
		||||
    converter: convertImagemagick,
 | 
			
		||||
  },
 | 
			
		||||
  graphicsmagick: {
 | 
			
		||||
    properties: propertiesGraphicsmagick,
 | 
			
		||||
    converter: convertGraphicsmagick,
 | 
			
		||||
  },
 | 
			
		||||
  assimp: {
 | 
			
		||||
    properties: propertiesassimp,
 | 
			
		||||
    converter: convertassimp,
 | 
			
		||||
  },
 | 
			
		||||
  ffmpeg: {
 | 
			
		||||
    properties: propertiesFFmpeg,
 | 
			
		||||
    converter: convertFFmpeg,
 | 
			
		||||
  },
 | 
			
		||||
  potrace: {
 | 
			
		||||
    properties: propertiesPotrace,
 | 
			
		||||
    converter: convertPotrace,
 | 
			
		||||
  },
 | 
			
		||||
  vtracer: {
 | 
			
		||||
    properties: propertiesVtracer,
 | 
			
		||||
    converter: convertVtracer,
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export async function mainConverter(
 | 
			
		||||
function chunks<T>(arr: T[], size: number): T[][] {
 | 
			
		||||
  if (size <= 0) {
 | 
			
		||||
    return [arr];
 | 
			
		||||
  }
 | 
			
		||||
  return Array.from({ length: Math.ceil(arr.length / size) }, (_: T, i: number) =>
 | 
			
		||||
    arr.slice(i * size, i * size + size),
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function handleConvert(
 | 
			
		||||
  fileNames: string[],
 | 
			
		||||
  userUploadsDir: string,
 | 
			
		||||
  userOutputDir: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  converterName: string,
 | 
			
		||||
  jobId: Cookie<string | undefined>,
 | 
			
		||||
) {
 | 
			
		||||
  const query = db.query(
 | 
			
		||||
    "INSERT INTO file_names (job_id, file_name, output_file_name, status) VALUES (?1, ?2, ?3, ?4)",
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  for (const chunk of chunks(fileNames, MAX_CONVERT_PROCESS)) {
 | 
			
		||||
    const toProcess: Promise<string>[] = [];
 | 
			
		||||
    for (const fileName of chunk) {
 | 
			
		||||
      const filePath = `${userUploadsDir}${fileName}`;
 | 
			
		||||
      const fileTypeOrig = fileName.split(".").pop() ?? "";
 | 
			
		||||
      const fileType = normalizeFiletype(fileTypeOrig);
 | 
			
		||||
      const newFileExt = normalizeOutputFiletype(convertTo);
 | 
			
		||||
      const newFileName = fileName.replace(
 | 
			
		||||
        new RegExp(`${fileTypeOrig}(?!.*${fileTypeOrig})`),
 | 
			
		||||
        newFileExt,
 | 
			
		||||
      );
 | 
			
		||||
      const targetPath = `${userOutputDir}${newFileName}`;
 | 
			
		||||
      toProcess.push(
 | 
			
		||||
        new Promise((resolve, reject) => {
 | 
			
		||||
          mainConverter(filePath, fileType, convertTo, targetPath, {}, converterName)
 | 
			
		||||
            .then((r) => {
 | 
			
		||||
              if (jobId.value) {
 | 
			
		||||
                query.run(jobId.value, fileName, newFileName, r);
 | 
			
		||||
              }
 | 
			
		||||
              resolve(r);
 | 
			
		||||
            })
 | 
			
		||||
            .catch((c) => reject(c));
 | 
			
		||||
        }),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    await Promise.all(toProcess);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function mainConverter(
 | 
			
		||||
  inputFilePath: string,
 | 
			
		||||
  fileTypeOriginal: string,
 | 
			
		||||
  // biome-ignore lint/suspicious/noExplicitAny: <explanation>
 | 
			
		||||
  convertTo: any,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  // biome-ignore lint/suspicious/noExplicitAny: <explanation>
 | 
			
		||||
  options?: any,
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  converterName?: string,
 | 
			
		||||
) {
 | 
			
		||||
  const fileType = normalizeFiletype(fileTypeOriginal);
 | 
			
		||||
 | 
			
		||||
  // biome-ignore lint/suspicious/noExplicitAny: <explanation>
 | 
			
		||||
  let converterFunc: any;
 | 
			
		||||
  // let converterName = converterName;
 | 
			
		||||
  let converterFunc: (typeof properties)["libjxl"]["converter"] | undefined;
 | 
			
		||||
 | 
			
		||||
  if (converterName) {
 | 
			
		||||
    converterFunc = properties[converterName]?.converter;
 | 
			
		||||
  } else {
 | 
			
		||||
    // Iterate over each converter in properties
 | 
			
		||||
    // biome-ignore lint/style/noParameterAssign: <explanation>
 | 
			
		||||
    for (converterName in properties) {
 | 
			
		||||
      const converterObj = properties[converterName];
 | 
			
		||||
 | 
			
		||||
@@ -131,24 +215,22 @@ export async function mainConverter(
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!converterFunc) {
 | 
			
		||||
    console.log(
 | 
			
		||||
      `No available converter supports converting from ${fileType} to ${convertTo}.`,
 | 
			
		||||
    );
 | 
			
		||||
    console.log(`No available converter supports converting from ${fileType} to ${convertTo}.`);
 | 
			
		||||
    return "File type not supported";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    await converterFunc(
 | 
			
		||||
      inputFilePath,
 | 
			
		||||
      fileType,
 | 
			
		||||
      convertTo,
 | 
			
		||||
      targetPath,
 | 
			
		||||
      options,
 | 
			
		||||
    );
 | 
			
		||||
    const result = await converterFunc(inputFilePath, fileType, convertTo, targetPath, options);
 | 
			
		||||
 | 
			
		||||
    console.log(
 | 
			
		||||
      `Converted ${inputFilePath} from ${fileType} to ${convertTo} successfully using ${converterName}.`,
 | 
			
		||||
      result,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (typeof result === "string") {
 | 
			
		||||
      return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return "Done";
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error(
 | 
			
		||||
@@ -159,7 +241,7 @@ export async function mainConverter(
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const possibleTargets: { [key: string]: { [key: string]: string[] } } = {};
 | 
			
		||||
const possibleTargets: Record<string, Record<string, string[]>> = {};
 | 
			
		||||
 | 
			
		||||
for (const converterName in properties) {
 | 
			
		||||
  const converterProperties = properties[converterName]?.properties;
 | 
			
		||||
@@ -178,15 +260,12 @@ for (const converterName in properties) {
 | 
			
		||||
        possibleTargets[extension] = {};
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      possibleTargets[extension][converterName] =
 | 
			
		||||
        converterProperties.to[key] || [];
 | 
			
		||||
      possibleTargets[extension][converterName] = converterProperties.to[key] || [];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const getPossibleTargets = (
 | 
			
		||||
  from: string,
 | 
			
		||||
): { [key: string]: string[] } => {
 | 
			
		||||
export const getPossibleTargets = (from: string): Record<string, string[]> => {
 | 
			
		||||
  const fromClean = normalizeFiletype(from);
 | 
			
		||||
 | 
			
		||||
  return possibleTargets[fromClean] || {};
 | 
			
		||||
@@ -210,11 +289,12 @@ for (const converterName in properties) {
 | 
			
		||||
}
 | 
			
		||||
possibleInputs.sort();
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
const getPossibleInputs = () => {
 | 
			
		||||
  return possibleInputs;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const allTargets: { [key: string]: string[] } = {};
 | 
			
		||||
const allTargets: Record<string, string[]> = {};
 | 
			
		||||
 | 
			
		||||
for (const converterName in properties) {
 | 
			
		||||
  const converterProperties = properties[converterName]?.properties;
 | 
			
		||||
@@ -236,7 +316,7 @@ export const getAllTargets = () => {
 | 
			
		||||
  return allTargets;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const allInputs: { [key: string]: string[] } = {};
 | 
			
		||||
const allInputs: Record<string, string[]> = {};
 | 
			
		||||
for (const converterName in properties) {
 | 
			
		||||
  const converterProperties = properties[converterName]?.properties;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										52
									
								
								src/converters/msgconvert.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,52 @@
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
    email: ["msg"],
 | 
			
		||||
  },
 | 
			
		||||
  to: {
 | 
			
		||||
    email: ["eml"],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function convert(
 | 
			
		||||
  filePath: string,
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal,
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    if (fileType === "msg" && convertTo === "eml") {
 | 
			
		||||
      // Convert MSG to EML using msgconvert
 | 
			
		||||
      // msgconvert will output to the same directory as the input file with .eml extension
 | 
			
		||||
      // We need to use --outfile to specify the target path
 | 
			
		||||
      const args = ["--outfile", targetPath, filePath];
 | 
			
		||||
 | 
			
		||||
      execFile("msgconvert", args, (error, stdout, stderr) => {
 | 
			
		||||
        if (error) {
 | 
			
		||||
          reject(new Error(`msgconvert failed: ${error.message}`));
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (stderr) {
 | 
			
		||||
          // Log sanitized stderr to avoid exposing sensitive paths
 | 
			
		||||
          const sanitizedStderr = stderr.replace(/(\/[^\s]+)/g, "[REDACTED_PATH]");
 | 
			
		||||
          console.warn(
 | 
			
		||||
            `msgconvert stderr: ${sanitizedStderr.length > 200 ? sanitizedStderr.slice(0, 200) + "..." : sanitizedStderr}`,
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resolve(targetPath);
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      reject(
 | 
			
		||||
        new Error(
 | 
			
		||||
          `Unsupported conversion from ${fileType} to ${convertTo}. Only MSG to EML conversion is currently supported.`,
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { exec } from "node:child_process";
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
@@ -124,33 +125,39 @@ export function convert(
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  // biome-ignore lint/suspicious/noExplicitAny: <explanation>
 | 
			
		||||
  options?: any,
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal,
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  // set xelatex here
 | 
			
		||||
  const xelatex = ["pdf", "latex"];
 | 
			
		||||
  let option = "";
 | 
			
		||||
 | 
			
		||||
  // Build arguments array
 | 
			
		||||
  const args: string[] = [];
 | 
			
		||||
 | 
			
		||||
  if (xelatex.includes(convertTo)) {
 | 
			
		||||
    option = "--pdf-engine=xelatex";
 | 
			
		||||
    args.push("--pdf-engine=xelatex");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  args.push(filePath);
 | 
			
		||||
  args.push("-f", fileType);
 | 
			
		||||
  args.push("-t", convertTo);
 | 
			
		||||
  args.push("-o", targetPath);
 | 
			
		||||
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    exec(
 | 
			
		||||
      `pandoc ${option} "${filePath}" -f ${fileType} -t ${convertTo} -o "${targetPath}"`,
 | 
			
		||||
      (error, stdout, stderr) => {
 | 
			
		||||
        if (error) {
 | 
			
		||||
          reject(`error: ${error}`);
 | 
			
		||||
        }
 | 
			
		||||
    execFile("pandoc", args, (error, stdout, stderr) => {
 | 
			
		||||
      if (error) {
 | 
			
		||||
        reject(`error: ${error}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
        if (stdout) {
 | 
			
		||||
          console.log(`stdout: ${stdout}`);
 | 
			
		||||
        }
 | 
			
		||||
      if (stdout) {
 | 
			
		||||
        console.log(`stdout: ${stdout}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
        if (stderr) {
 | 
			
		||||
          console.error(`stderr: ${stderr}`);
 | 
			
		||||
        }
 | 
			
		||||
      if (stderr) {
 | 
			
		||||
        console.error(`stderr: ${stderr}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
        resolve("success");
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
      resolve("Done");
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										50
									
								
								src/converters/potrace.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,50 @@
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
    images: ["pnm", "pbm", "pgm", "bmp"],
 | 
			
		||||
  },
 | 
			
		||||
  to: {
 | 
			
		||||
    images: [
 | 
			
		||||
      "svg",
 | 
			
		||||
      "pdf",
 | 
			
		||||
      "pdfpage",
 | 
			
		||||
      "eps",
 | 
			
		||||
      "postscript",
 | 
			
		||||
      "ps",
 | 
			
		||||
      "dxf",
 | 
			
		||||
      "geojson",
 | 
			
		||||
      "pgm",
 | 
			
		||||
      "gimppath",
 | 
			
		||||
      "xfig",
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function convert(
 | 
			
		||||
  filePath: string,
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal, // to make it mockable
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    execFile("potrace", [filePath, "-o", targetPath, "-b", convertTo], (error, stdout, stderr) => {
 | 
			
		||||
      if (error) {
 | 
			
		||||
        reject(`error: ${error}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (stdout) {
 | 
			
		||||
        console.log(`stdout: ${stdout}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (stderr) {
 | 
			
		||||
        console.error(`stderr: ${stderr}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      resolve("Done");
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { exec } from "node:child_process";
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
@@ -14,11 +15,11 @@ export function convert(
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  // biome-ignore lint/suspicious/noExplicitAny: <explanation>
 | 
			
		||||
  options?: any,
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal, // to make it mockable
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    exec(`resvg "${filePath}" "${targetPath}"`, (error, stdout, stderr) => {
 | 
			
		||||
    execFile("resvg", [filePath, targetPath], (error, stdout, stderr) => {
 | 
			
		||||
      if (error) {
 | 
			
		||||
        reject(`error: ${error}`);
 | 
			
		||||
      }
 | 
			
		||||
@@ -31,7 +32,7 @@ export function convert(
 | 
			
		||||
        console.error(`stderr: ${stderr}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      resolve("success");
 | 
			
		||||
      resolve("Done");
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15
									
								
								src/converters/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,15 @@
 | 
			
		||||
export type ExecFileFn = (
 | 
			
		||||
  cmd: string,
 | 
			
		||||
  args: string[],
 | 
			
		||||
  callback: (err: Error | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  options?: import("child_process").ExecFileOptions,
 | 
			
		||||
) => void;
 | 
			
		||||
 | 
			
		||||
export type ConvertFnWithExecFile = (
 | 
			
		||||
  filePath: string,
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  options: unknown,
 | 
			
		||||
  execFileOverride?: ExecFileFn,
 | 
			
		||||
) => Promise<string>;
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { exec } from "node:child_process";
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
// declare possible conversions
 | 
			
		||||
export const properties = {
 | 
			
		||||
@@ -94,8 +95,8 @@ export function convert(
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  // biome-ignore lint/suspicious/noExplicitAny: <explanation>
 | 
			
		||||
  options?: any,
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal,
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  // if (fileType === "svg") {
 | 
			
		||||
  //   const scale = options.scale || 1;
 | 
			
		||||
@@ -119,23 +120,20 @@ export function convert(
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    exec(
 | 
			
		||||
      `vips ${action} "${filePath}" "${targetPath}"`,
 | 
			
		||||
      (error, stdout, stderr) => {
 | 
			
		||||
        if (error) {
 | 
			
		||||
          reject(`error: ${error}`);
 | 
			
		||||
        }
 | 
			
		||||
    execFile("vips", [action, filePath, targetPath], (error, stdout, stderr) => {
 | 
			
		||||
      if (error) {
 | 
			
		||||
        reject(`error: ${error}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
        if (stdout) {
 | 
			
		||||
          console.log(`stdout: ${stdout}`);
 | 
			
		||||
        }
 | 
			
		||||
      if (stdout) {
 | 
			
		||||
        console.log(`stdout: ${stdout}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
        if (stderr) {
 | 
			
		||||
          console.error(`stderr: ${stderr}`);
 | 
			
		||||
        }
 | 
			
		||||
      if (stderr) {
 | 
			
		||||
        console.error(`stderr: ${stderr}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
        resolve("success");
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
      resolve("Done");
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										80
									
								
								src/converters/vtracer.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,80 @@
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
    images: ["jpg", "jpeg", "png", "bmp", "gif", "tiff", "tif", "webp"],
 | 
			
		||||
  },
 | 
			
		||||
  to: {
 | 
			
		||||
    images: ["svg"],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface VTracerOptions {
 | 
			
		||||
  colormode?: string;
 | 
			
		||||
  hierarchical?: string;
 | 
			
		||||
  mode?: string;
 | 
			
		||||
  filter_speckle?: string | number;
 | 
			
		||||
  color_precision?: string | number;
 | 
			
		||||
  layer_difference?: string | number;
 | 
			
		||||
  corner_threshold?: string | number;
 | 
			
		||||
  length_threshold?: string | number;
 | 
			
		||||
  max_iterations?: string | number;
 | 
			
		||||
  splice_threshold?: string | number;
 | 
			
		||||
  path_precision?: string | number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function convert(
 | 
			
		||||
  filePath: string,
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal, // to make it mockable
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    // Build vtracer arguments
 | 
			
		||||
    const args = ["--input", filePath, "--output", targetPath];
 | 
			
		||||
 | 
			
		||||
    // Add optional parameter if provided
 | 
			
		||||
    if (options && typeof options === "object") {
 | 
			
		||||
      const opts = options as VTracerOptions;
 | 
			
		||||
      const validOptions: Array<keyof VTracerOptions> = [
 | 
			
		||||
        "colormode",
 | 
			
		||||
        "hierarchical",
 | 
			
		||||
        "mode",
 | 
			
		||||
        "filter_speckle",
 | 
			
		||||
        "color_precision",
 | 
			
		||||
        "layer_difference",
 | 
			
		||||
        "corner_threshold",
 | 
			
		||||
        "length_threshold",
 | 
			
		||||
        "max_iterations",
 | 
			
		||||
        "splice_threshold",
 | 
			
		||||
        "path_precision",
 | 
			
		||||
      ];
 | 
			
		||||
 | 
			
		||||
      for (const option of validOptions) {
 | 
			
		||||
        if (opts[option] !== undefined) {
 | 
			
		||||
          args.push(`--${option}`, String(opts[option]));
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    execFile("vtracer", args, (error, stdout, stderr) => {
 | 
			
		||||
      if (error) {
 | 
			
		||||
        reject(`error: ${error}${stderr ? `\nstderr: ${stderr}` : ""}`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (stdout) {
 | 
			
		||||
        console.log(`stdout: ${stdout}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (stderr) {
 | 
			
		||||
        console.log(`stderr: ${stderr}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      resolve("Done");
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { exec } from "node:child_process";
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
@@ -14,18 +15,16 @@ export function convert(
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: string,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  // biome-ignore lint/suspicious/noExplicitAny: <explanation>
 | 
			
		||||
  options?: any,
 | 
			
		||||
  options?: unknown,
 | 
			
		||||
  execFile: ExecFileFn = execFileOriginal,
 | 
			
		||||
): Promise<string> {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    // const fileName: string = (targetPath.split("/").pop() as string).replace(".pdf", "")
 | 
			
		||||
    const outputPath = targetPath
 | 
			
		||||
      .split("/")
 | 
			
		||||
      .slice(0, -1)
 | 
			
		||||
      .join("/")
 | 
			
		||||
      .replace("./", "");
 | 
			
		||||
    exec(
 | 
			
		||||
      `latexmk -xelatex -interaction=nonstopmode -output-directory="${outputPath}" "${filePath}"`,
 | 
			
		||||
    const outputPath = targetPath.split("/").slice(0, -1).join("/").replace("./", "");
 | 
			
		||||
 | 
			
		||||
    execFile(
 | 
			
		||||
      "latexmk",
 | 
			
		||||
      ["-xelatex", "-interaction=nonstopmode", `-output-directory=${outputPath}`, filePath],
 | 
			
		||||
      (error, stdout, stderr) => {
 | 
			
		||||
        if (error) {
 | 
			
		||||
          reject(`error: ${error}`);
 | 
			
		||||
@@ -39,7 +38,7 @@ export function convert(
 | 
			
		||||
          console.error(`stderr: ${stderr}`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        resolve("success");
 | 
			
		||||
        resolve("Done");
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										41
									
								
								src/db/db.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,41 @@
 | 
			
		||||
import { Database } from "bun:sqlite";
 | 
			
		||||
 | 
			
		||||
const db = new Database("./data/mydb.sqlite", { create: true });
 | 
			
		||||
 | 
			
		||||
if (!db.query("SELECT * FROM sqlite_master WHERE type='table'").get()) {
 | 
			
		||||
  db.exec(`
 | 
			
		||||
CREATE TABLE IF NOT EXISTS users (
 | 
			
		||||
	id INTEGER PRIMARY KEY AUTOINCREMENT,
 | 
			
		||||
	email TEXT NOT NULL,
 | 
			
		||||
	password TEXT NOT NULL
 | 
			
		||||
);
 | 
			
		||||
CREATE TABLE IF NOT EXISTS file_names (
 | 
			
		||||
  id INTEGER PRIMARY KEY AUTOINCREMENT,
 | 
			
		||||
  job_id INTEGER NOT NULL,
 | 
			
		||||
  file_name TEXT NOT NULL,
 | 
			
		||||
  output_file_name TEXT NOT NULL,
 | 
			
		||||
  status TEXT DEFAULT 'not started',
 | 
			
		||||
  FOREIGN KEY (job_id) REFERENCES jobs(id)
 | 
			
		||||
);
 | 
			
		||||
CREATE TABLE IF NOT EXISTS jobs (
 | 
			
		||||
	id INTEGER PRIMARY KEY AUTOINCREMENT,
 | 
			
		||||
	user_id INTEGER NOT NULL,
 | 
			
		||||
	date_created TEXT NOT NULL,
 | 
			
		||||
  status TEXT DEFAULT 'not started',
 | 
			
		||||
  num_files INTEGER DEFAULT 0,
 | 
			
		||||
  FOREIGN KEY (user_id) REFERENCES users(id)
 | 
			
		||||
);
 | 
			
		||||
PRAGMA user_version = 1;`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const dbVersion = (db.query("PRAGMA user_version").get() as { user_version?: number }).user_version;
 | 
			
		||||
if (dbVersion === 0) {
 | 
			
		||||
  db.exec("ALTER TABLE file_names ADD COLUMN status TEXT DEFAULT 'not started';");
 | 
			
		||||
  db.exec("PRAGMA user_version = 1;");
 | 
			
		||||
  console.log("Updated database to version 1.");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// enable WAL mode
 | 
			
		||||
db.exec("PRAGMA journal_mode = WAL;");
 | 
			
		||||
 | 
			
		||||
export default db;
 | 
			
		||||
							
								
								
									
										23
									
								
								src/db/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,23 @@
 | 
			
		||||
export class Filename {
 | 
			
		||||
  id!: number;
 | 
			
		||||
  job_id!: number;
 | 
			
		||||
  file_name!: string;
 | 
			
		||||
  output_file_name!: string;
 | 
			
		||||
  status!: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class Jobs {
 | 
			
		||||
  finished_files!: number;
 | 
			
		||||
  id!: number;
 | 
			
		||||
  user_id!: number;
 | 
			
		||||
  date_created!: string;
 | 
			
		||||
  status!: string;
 | 
			
		||||
  num_files!: number;
 | 
			
		||||
  files_detailed!: Filename[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class User {
 | 
			
		||||
  id!: number;
 | 
			
		||||
  email!: string;
 | 
			
		||||
  password!: string;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										25
									
								
								src/helpers/env.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,25 @@
 | 
			
		||||
export const ACCOUNT_REGISTRATION =
 | 
			
		||||
  process.env.ACCOUNT_REGISTRATION?.toLowerCase() === "true" || false;
 | 
			
		||||
 | 
			
		||||
export const HTTP_ALLOWED = process.env.HTTP_ALLOWED?.toLowerCase() === "true" || false;
 | 
			
		||||
 | 
			
		||||
export const ALLOW_UNAUTHENTICATED =
 | 
			
		||||
  process.env.ALLOW_UNAUTHENTICATED?.toLowerCase() === "true" || false;
 | 
			
		||||
 | 
			
		||||
export const AUTO_DELETE_EVERY_N_HOURS = process.env.AUTO_DELETE_EVERY_N_HOURS
 | 
			
		||||
  ? Number(process.env.AUTO_DELETE_EVERY_N_HOURS)
 | 
			
		||||
  : 24;
 | 
			
		||||
 | 
			
		||||
export const HIDE_HISTORY = process.env.HIDE_HISTORY?.toLowerCase() === "true" || false;
 | 
			
		||||
 | 
			
		||||
export const WEBROOT = process.env.WEBROOT ?? "";
 | 
			
		||||
 | 
			
		||||
export const LANGUAGE = process.env.LANGUAGE?.toLowerCase() || "en";
 | 
			
		||||
 | 
			
		||||
export const MAX_CONVERT_PROCESS =
 | 
			
		||||
  process.env.MAX_CONVERT_PROCESS && Number(process.env.MAX_CONVERT_PROCESS) > 0
 | 
			
		||||
    ? Number(process.env.MAX_CONVERT_PROCESS)
 | 
			
		||||
    : 0;
 | 
			
		||||
 | 
			
		||||
export const UNAUTHENTICATED_USER_SHARING =
 | 
			
		||||
  process.env.UNAUTHENTICATED_USER_SHARING?.toLowerCase() === "true" || false;
 | 
			
		||||
@@ -2,6 +2,7 @@ export const normalizeFiletype = (filetype: string): string => {
 | 
			
		||||
  const lowercaseFiletype = filetype.toLowerCase();
 | 
			
		||||
 | 
			
		||||
  switch (lowercaseFiletype) {
 | 
			
		||||
    case "jfif":
 | 
			
		||||
    case "jpg":
 | 
			
		||||
      return "jpeg";
 | 
			
		||||
    case "htm":
 | 
			
		||||
@@ -10,6 +11,8 @@ export const normalizeFiletype = (filetype: string): string => {
 | 
			
		||||
      return "latex";
 | 
			
		||||
    case "md":
 | 
			
		||||
      return "markdown";
 | 
			
		||||
    case "unknown":
 | 
			
		||||
      return "m4a";
 | 
			
		||||
    default:
 | 
			
		||||
      return lowercaseFiletype;
 | 
			
		||||
  }
 | 
			
		||||
@@ -23,6 +26,9 @@ export const normalizeOutputFiletype = (filetype: string): string => {
 | 
			
		||||
      return "jpg";
 | 
			
		||||
    case "latex":
 | 
			
		||||
      return "tex";
 | 
			
		||||
    case "markdown_phpextra":
 | 
			
		||||
    case "markdown_strict":
 | 
			
		||||
    case "markdown_mmd":
 | 
			
		||||
    case "markdown":
 | 
			
		||||
      return "md";
 | 
			
		||||
    default:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import { exec } from "node:child_process";
 | 
			
		||||
import { version } from "../../package.json";
 | 
			
		||||
 | 
			
		||||
console.log(`ConvertX v${version}`);
 | 
			
		||||
 | 
			
		||||
if (process.env.NODE_ENV === "production") {
 | 
			
		||||
@@ -43,6 +44,16 @@ if (process.env.NODE_ENV === "production") {
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  exec("magick --version", (error, stdout) => {
 | 
			
		||||
    if (error) {
 | 
			
		||||
      console.error("ImageMagick is not installed.");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (stdout) {
 | 
			
		||||
      console.log(stdout.split("\n")[0]?.replace("Version: ", ""));
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  exec("gm version", (error, stdout) => {
 | 
			
		||||
    if (error) {
 | 
			
		||||
      console.error("GraphicsMagick is not installed.");
 | 
			
		||||
@@ -53,6 +64,16 @@ if (process.env.NODE_ENV === "production") {
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  exec("inkscape --version", (error, stdout) => {
 | 
			
		||||
    if (error) {
 | 
			
		||||
      console.error("Inkscape is not installed.");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (stdout) {
 | 
			
		||||
      console.log(stdout.split("\n")[0]);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  exec("djxl --version", (error, stdout) => {
 | 
			
		||||
    if (error) {
 | 
			
		||||
      console.error("libjxl-tools is not installed.");
 | 
			
		||||
@@ -63,6 +84,16 @@ if (process.env.NODE_ENV === "production") {
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  exec("dasel --version", (error, stdout) => {
 | 
			
		||||
    if (error) {
 | 
			
		||||
      console.error("dasel is not installed.");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (stdout) {
 | 
			
		||||
      console.log(stdout.split("\n")[0]);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  exec("xelatex -version", (error, stdout) => {
 | 
			
		||||
    if (error) {
 | 
			
		||||
      console.error("Tex Live with XeTeX is not installed.");
 | 
			
		||||
@@ -83,6 +114,66 @@ if (process.env.NODE_ENV === "production") {
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  exec("assimp version", (error, stdout) => {
 | 
			
		||||
    if (error) {
 | 
			
		||||
      console.error("assimp is not installed");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (stdout) {
 | 
			
		||||
      console.log(`assimp ${stdout.split("\n")[5]}`);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  exec("ebook-convert --version", (error, stdout) => {
 | 
			
		||||
    if (error) {
 | 
			
		||||
      console.error("ebook-convert (calibre) is not installed");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (stdout) {
 | 
			
		||||
      console.log(stdout.split("\n")[0]);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  exec("heif-info -v", (error, stdout) => {
 | 
			
		||||
    if (error) {
 | 
			
		||||
      console.error("libheif is not installed");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (stdout) {
 | 
			
		||||
      console.log(`libheif v${stdout.split("\n")[0]}`);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  exec("potrace -v", (error, stdout) => {
 | 
			
		||||
    if (error) {
 | 
			
		||||
      console.error("potrace is not installed");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (stdout) {
 | 
			
		||||
      console.log(stdout.split("\n")[0]);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  exec("soffice --version", (error, stdout) => {
 | 
			
		||||
    if (error) {
 | 
			
		||||
      console.error("libreoffice is not installed");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (stdout) {
 | 
			
		||||
      console.log(stdout.split("\n")[0]);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  exec("msgconvert --version", (error, stdout) => {
 | 
			
		||||
    if (error) {
 | 
			
		||||
      console.error("msgconvert (libemail-outlook-message-perl) is not installed");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (stdout) {
 | 
			
		||||
      console.log(stdout.split("\n")[0]);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  exec("bun -v", (error, stdout) => {
 | 
			
		||||
    if (error) {
 | 
			
		||||
      console.error("Bun is not installed. wait what");
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15
									
								
								src/helpers/tailwind.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,15 @@
 | 
			
		||||
import tailwind from "@tailwindcss/postcss";
 | 
			
		||||
import postcss from "postcss";
 | 
			
		||||
 | 
			
		||||
export const generateTailwind = async () => {
 | 
			
		||||
  const result = await Bun.file("./src/main.css")
 | 
			
		||||
    .text()
 | 
			
		||||
    .then((sourceText) => {
 | 
			
		||||
      return postcss([tailwind]).process(sourceText, {
 | 
			
		||||
        from: "./src/main.css",
 | 
			
		||||
        to: "./public/generated.css",
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
  return result;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										1303
									
								
								src/index.tsx
									
									
									
									
									
								
							
							
						
						
							
								
								
									
										32
									
								
								src/main.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,32 @@
 | 
			
		||||
@import "./theme/theme.css";
 | 
			
		||||
@import "tailwindcss";
 | 
			
		||||
 | 
			
		||||
@plugin "tailwind-scrollbar";
 | 
			
		||||
 | 
			
		||||
@theme {
 | 
			
		||||
  --color-contrast: var(--contrast);
 | 
			
		||||
  --color-neutral-900: var(--neutral-900);
 | 
			
		||||
  --color-neutral-800: var(--neutral-800);
 | 
			
		||||
  --color-neutral-700: var(--neutral-700);
 | 
			
		||||
  --color-neutral-600: var(--neutral-600);
 | 
			
		||||
  --color-neutral-500: var(--neutral-500);
 | 
			
		||||
  --color-neutral-400: var(--neutral-400);
 | 
			
		||||
  --color-neutral-300: var(--neutral-300);
 | 
			
		||||
  --color-neutral-200: var(--neutral-200);
 | 
			
		||||
  --color-neutral-100: var(--neutral-100);
 | 
			
		||||
  --color-accent-600: var(--accent-600);
 | 
			
		||||
  --color-accent-500: var(--accent-500);
 | 
			
		||||
  --color-accent-400: var(--accent-400);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@utility article {
 | 
			
		||||
  @apply px-2 sm:px-4 py-4 mb-4 bg-neutral-800/40 w-full mx-auto max-w-4xl rounded-sm;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@utility btn-primary {
 | 
			
		||||
  @apply bg-accent-500 text-contrast rounded-sm p-2 sm:p-4 hover:bg-accent-400 cursor-pointer transition-colors;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@utility btn-secondary {
 | 
			
		||||
  @apply bg-neutral-400 text-contrast rounded-sm p-2 sm:p-4 hover:bg-neutral-300 cursor-pointer transition-colors;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										67
									
								
								src/pages/chooseConverter.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,67 @@
 | 
			
		||||
import { Html } from "@elysiajs/html";
 | 
			
		||||
import Elysia, { t } from "elysia";
 | 
			
		||||
import { getPossibleTargets } from "../converters/main";
 | 
			
		||||
import { userService } from "./user";
 | 
			
		||||
 | 
			
		||||
export const chooseConverter = new Elysia().use(userService).post(
 | 
			
		||||
  "/conversions",
 | 
			
		||||
  ({ body }) => {
 | 
			
		||||
    return (
 | 
			
		||||
      <>
 | 
			
		||||
        <article
 | 
			
		||||
          class={`
 | 
			
		||||
            convert_to_popup absolute z-2 m-0 hidden h-[50vh] max-h-[50vh] w-full flex-col
 | 
			
		||||
            overflow-x-hidden overflow-y-auto rounded bg-neutral-800
 | 
			
		||||
            sm:h-[30vh]
 | 
			
		||||
          `}
 | 
			
		||||
        >
 | 
			
		||||
          {Object.entries(getPossibleTargets(body.fileType)).map(([converter, targets]) => (
 | 
			
		||||
            <article
 | 
			
		||||
              class={`convert_to_group flex w-full flex-col border-b border-neutral-700 p-4`}
 | 
			
		||||
              data-converter={converter}
 | 
			
		||||
            >
 | 
			
		||||
              <header class="mb-2 w-full text-xl font-bold" safe>
 | 
			
		||||
                {converter}
 | 
			
		||||
              </header>
 | 
			
		||||
              <ul class="convert_to_target flex flex-row flex-wrap gap-1">
 | 
			
		||||
                {targets.map((target) => (
 | 
			
		||||
                  <button
 | 
			
		||||
                    // https://stackoverflow.com/questions/121499/when-a-blur-event-occurs-how-can-i-find-out-which-element-focus-went-to#comment82388679_33325953
 | 
			
		||||
                    tabindex={0}
 | 
			
		||||
                    class={`
 | 
			
		||||
                      target rounded bg-neutral-700 p-1 text-base
 | 
			
		||||
                      hover:bg-neutral-600
 | 
			
		||||
                    `}
 | 
			
		||||
                    data-value={`${target},${converter}`}
 | 
			
		||||
                    data-target={target}
 | 
			
		||||
                    data-converter={converter}
 | 
			
		||||
                    type="button"
 | 
			
		||||
                    safe
 | 
			
		||||
                  >
 | 
			
		||||
                    {target}
 | 
			
		||||
                  </button>
 | 
			
		||||
                ))}
 | 
			
		||||
              </ul>
 | 
			
		||||
            </article>
 | 
			
		||||
          ))}
 | 
			
		||||
        </article>
 | 
			
		||||
 | 
			
		||||
        <select name="convert_to" aria-label="Convert to" required hidden>
 | 
			
		||||
          <option selected disabled value="">
 | 
			
		||||
            Convert to
 | 
			
		||||
          </option>
 | 
			
		||||
          {Object.entries(getPossibleTargets(body.fileType)).map(([converter, targets]) => (
 | 
			
		||||
            <optgroup label={converter}>
 | 
			
		||||
              {targets.map((target) => (
 | 
			
		||||
                <option value={`${target},${converter}`} safe>
 | 
			
		||||
                  {target}
 | 
			
		||||
                </option>
 | 
			
		||||
              ))}
 | 
			
		||||
            </optgroup>
 | 
			
		||||
          ))}
 | 
			
		||||
        </select>
 | 
			
		||||
      </>
 | 
			
		||||
    );
 | 
			
		||||
  },
 | 
			
		||||
  { body: t.Object({ fileType: t.String() }) },
 | 
			
		||||
);
 | 
			
		||||
							
								
								
									
										93
									
								
								src/pages/convert.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,93 @@
 | 
			
		||||
import { mkdir } from "node:fs/promises";
 | 
			
		||||
import { Elysia, t } from "elysia";
 | 
			
		||||
import sanitize from "sanitize-filename";
 | 
			
		||||
import { outputDir, uploadsDir } from "..";
 | 
			
		||||
import { handleConvert } from "../converters/main";
 | 
			
		||||
import db from "../db/db";
 | 
			
		||||
import { Jobs } from "../db/types";
 | 
			
		||||
import { WEBROOT } from "../helpers/env";
 | 
			
		||||
import { normalizeFiletype } from "../helpers/normalizeFiletype";
 | 
			
		||||
import { userService } from "./user";
 | 
			
		||||
 | 
			
		||||
export const convert = new Elysia().use(userService).post(
 | 
			
		||||
  "/convert",
 | 
			
		||||
  async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
 | 
			
		||||
    if (!auth?.value) {
 | 
			
		||||
      return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const user = await jwt.verify(auth.value);
 | 
			
		||||
    if (!user) {
 | 
			
		||||
      return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!jobId?.value) {
 | 
			
		||||
      return redirect(`${WEBROOT}/`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const existingJob = db
 | 
			
		||||
      .query("SELECT * FROM jobs WHERE id = ? AND user_id = ?")
 | 
			
		||||
      .as(Jobs)
 | 
			
		||||
      .get(jobId.value, user.id);
 | 
			
		||||
 | 
			
		||||
    if (!existingJob) {
 | 
			
		||||
      return redirect(`${WEBROOT}/`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
 | 
			
		||||
    const userOutputDir = `${outputDir}${user.id}/${jobId.value}/`;
 | 
			
		||||
 | 
			
		||||
    // create the output directory
 | 
			
		||||
    try {
 | 
			
		||||
      await mkdir(userOutputDir, { recursive: true });
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error(`Failed to create the output directory: ${userOutputDir}.`, error);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const convertTo = normalizeFiletype(body.convert_to.split(",")[0] ?? "");
 | 
			
		||||
    const converterName = body.convert_to.split(",")[1];
 | 
			
		||||
 | 
			
		||||
    if (!converterName) {
 | 
			
		||||
      return redirect(`${WEBROOT}/`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const fileNames = JSON.parse(body.file_names) as string[];
 | 
			
		||||
 | 
			
		||||
    for (let i = 0; i < fileNames.length; i++) {
 | 
			
		||||
      fileNames[i] = sanitize(fileNames[i] || "");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!Array.isArray(fileNames) || fileNames.length === 0) {
 | 
			
		||||
      return redirect(`${WEBROOT}/`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    db.query("UPDATE jobs SET num_files = ?1, status = 'pending' WHERE id = ?2").run(
 | 
			
		||||
      fileNames.length,
 | 
			
		||||
      jobId.value,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Start the conversion process in the background
 | 
			
		||||
    handleConvert(fileNames, userUploadsDir, userOutputDir, convertTo, converterName, jobId)
 | 
			
		||||
      .then(() => {
 | 
			
		||||
        // All conversions are done, update the job status to 'completed'
 | 
			
		||||
        if (jobId.value) {
 | 
			
		||||
          db.query("UPDATE jobs SET status = 'completed' WHERE id = ?1").run(jobId.value);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // delete all uploaded files in userUploadsDir
 | 
			
		||||
        // rmSync(userUploadsDir, { recursive: true, force: true });
 | 
			
		||||
      })
 | 
			
		||||
      .catch((error) => {
 | 
			
		||||
        console.error("Error in conversion process:", error);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
    // Redirect the client immediately
 | 
			
		||||
    return redirect(`${WEBROOT}/results/${jobId.value}`, 302);
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    body: t.Object({
 | 
			
		||||
      convert_to: t.String(),
 | 
			
		||||
      file_names: t.String(),
 | 
			
		||||
    }),
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
							
								
								
									
										41
									
								
								src/pages/deleteFile.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,41 @@
 | 
			
		||||
import { unlink } from "node:fs/promises";
 | 
			
		||||
import { Elysia, t } from "elysia";
 | 
			
		||||
import { uploadsDir } from "..";
 | 
			
		||||
import db from "../db/db";
 | 
			
		||||
import { WEBROOT } from "../helpers/env";
 | 
			
		||||
import { userService } from "./user";
 | 
			
		||||
 | 
			
		||||
export const deleteFile = new Elysia().use(userService).post(
 | 
			
		||||
  "/delete",
 | 
			
		||||
  async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
 | 
			
		||||
    if (!auth?.value) {
 | 
			
		||||
      return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const user = await jwt.verify(auth.value);
 | 
			
		||||
    if (!user) {
 | 
			
		||||
      return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!jobId?.value) {
 | 
			
		||||
      return redirect(`${WEBROOT}/`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const existingJob = await db
 | 
			
		||||
      .query("SELECT * FROM jobs WHERE id = ? AND user_id = ?")
 | 
			
		||||
      .get(jobId.value, user.id);
 | 
			
		||||
 | 
			
		||||
    if (!existingJob) {
 | 
			
		||||
      return redirect(`${WEBROOT}/`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
 | 
			
		||||
 | 
			
		||||
    await unlink(`${userUploadsDir}${body.filename}`);
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      message: "File deleted successfully.",
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  { body: t.Object({ filename: t.String() }) },
 | 
			
		||||
);
 | 
			
		||||
							
								
								
									
										74
									
								
								src/pages/download.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,74 @@
 | 
			
		||||
import path from "node:path";
 | 
			
		||||
import { Elysia } from "elysia";
 | 
			
		||||
import sanitize from "sanitize-filename";
 | 
			
		||||
import * as tar from "tar";
 | 
			
		||||
import { outputDir } from "..";
 | 
			
		||||
import db from "../db/db";
 | 
			
		||||
import { WEBROOT } from "../helpers/env";
 | 
			
		||||
import { userService } from "./user";
 | 
			
		||||
 | 
			
		||||
export const download = new Elysia()
 | 
			
		||||
  .use(userService)
 | 
			
		||||
  .get(
 | 
			
		||||
    "/download/:userId/:jobId/:fileName",
 | 
			
		||||
    async ({ params, jwt, redirect, cookie: { auth } }) => {
 | 
			
		||||
      if (!auth?.value) {
 | 
			
		||||
        return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const user = await jwt.verify(auth.value);
 | 
			
		||||
      if (!user) {
 | 
			
		||||
        return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const job = await db
 | 
			
		||||
        .query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
 | 
			
		||||
        .get(user.id, params.jobId);
 | 
			
		||||
 | 
			
		||||
      if (!job) {
 | 
			
		||||
        return redirect(`${WEBROOT}/results`, 302);
 | 
			
		||||
      }
 | 
			
		||||
      // parse from url encoded string
 | 
			
		||||
      const userId = decodeURIComponent(params.userId);
 | 
			
		||||
      const jobId = decodeURIComponent(params.jobId);
 | 
			
		||||
      const fileName = sanitize(decodeURIComponent(params.fileName));
 | 
			
		||||
 | 
			
		||||
      const filePath = `${outputDir}${userId}/${jobId}/${fileName}`;
 | 
			
		||||
      return Bun.file(filePath);
 | 
			
		||||
    },
 | 
			
		||||
  )
 | 
			
		||||
  .get("/archive/:userId/:jobId", async ({ params, jwt, redirect, cookie: { auth } }) => {
 | 
			
		||||
    if (!auth?.value) {
 | 
			
		||||
      return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const user = await jwt.verify(auth.value);
 | 
			
		||||
    if (!user) {
 | 
			
		||||
      return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const job = await db
 | 
			
		||||
      .query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
 | 
			
		||||
      .get(user.id, params.jobId);
 | 
			
		||||
 | 
			
		||||
    if (!job) {
 | 
			
		||||
      return redirect(`${WEBROOT}/results`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const userId = decodeURIComponent(params.userId);
 | 
			
		||||
    const jobId = decodeURIComponent(params.jobId);
 | 
			
		||||
    const outputPath = `${outputDir}${userId}/${jobId}`;
 | 
			
		||||
    const outputTar = path.join(outputPath, `converted_files_${jobId}.tar`);
 | 
			
		||||
 | 
			
		||||
    await tar.create(
 | 
			
		||||
      {
 | 
			
		||||
        file: outputTar,
 | 
			
		||||
        cwd: outputPath,
 | 
			
		||||
        filter: (path) => {
 | 
			
		||||
          return !path.match(".*\\.tar");
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      ["."],
 | 
			
		||||
    );
 | 
			
		||||
    return Bun.file(outputTar);
 | 
			
		||||
  });
 | 
			
		||||
							
								
								
									
										216
									
								
								src/pages/history.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,216 @@
 | 
			
		||||
import { Html } from "@elysiajs/html";
 | 
			
		||||
import { Elysia } from "elysia";
 | 
			
		||||
import { BaseHtml } from "../components/base";
 | 
			
		||||
import { Header } from "../components/header";
 | 
			
		||||
import db from "../db/db";
 | 
			
		||||
import { Filename, Jobs } from "../db/types";
 | 
			
		||||
import { ALLOW_UNAUTHENTICATED, HIDE_HISTORY, LANGUAGE, WEBROOT } from "../helpers/env";
 | 
			
		||||
import { userService } from "./user";
 | 
			
		||||
 | 
			
		||||
export const history = new Elysia()
 | 
			
		||||
  .use(userService)
 | 
			
		||||
  .get("/history", async ({ jwt, redirect, cookie: { auth } }) => {
 | 
			
		||||
    if (HIDE_HISTORY) {
 | 
			
		||||
      return redirect(`${WEBROOT}/`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!auth?.value) {
 | 
			
		||||
      return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
    }
 | 
			
		||||
    const user = await jwt.verify(auth.value);
 | 
			
		||||
 | 
			
		||||
    if (!user) {
 | 
			
		||||
      return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let userJobs = db.query("SELECT * FROM jobs WHERE user_id = ?").as(Jobs).all(user.id).reverse();
 | 
			
		||||
 | 
			
		||||
    for (const job of userJobs) {
 | 
			
		||||
      const files = db.query("SELECT * FROM file_names WHERE job_id = ?").as(Filename).all(job.id);
 | 
			
		||||
 | 
			
		||||
      job.finished_files = files.length;
 | 
			
		||||
      job.files_detailed = files;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // filter out jobs with no files
 | 
			
		||||
    userJobs = userJobs.filter((job) => job.num_files > 0);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <BaseHtml webroot={WEBROOT} title="ConvertX | Results">
 | 
			
		||||
        <>
 | 
			
		||||
          <Header
 | 
			
		||||
            webroot={WEBROOT}
 | 
			
		||||
            allowUnauthenticated={ALLOW_UNAUTHENTICATED}
 | 
			
		||||
            hideHistory={HIDE_HISTORY}
 | 
			
		||||
            loggedIn
 | 
			
		||||
          />
 | 
			
		||||
          <main
 | 
			
		||||
            class={`
 | 
			
		||||
              w-full flex-1 px-2
 | 
			
		||||
              sm:px-4
 | 
			
		||||
            `}
 | 
			
		||||
          >
 | 
			
		||||
            <article class="article">
 | 
			
		||||
              <h1 class="mb-4 text-xl">Results</h1>
 | 
			
		||||
              <table
 | 
			
		||||
                class={`
 | 
			
		||||
                  w-full table-auto overflow-y-auto rounded bg-neutral-900 text-left
 | 
			
		||||
                  [&_td]:p-4
 | 
			
		||||
                  [&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
 | 
			
		||||
                `}
 | 
			
		||||
              >
 | 
			
		||||
                <thead>
 | 
			
		||||
                  <tr>
 | 
			
		||||
                    <th
 | 
			
		||||
                      class={`
 | 
			
		||||
                        px-2 py-2
 | 
			
		||||
                        sm:px-4
 | 
			
		||||
                      `}
 | 
			
		||||
                    >
 | 
			
		||||
                      <span class="sr-only">Expand details</span>
 | 
			
		||||
                    </th>
 | 
			
		||||
                    <th
 | 
			
		||||
                      class={`
 | 
			
		||||
                        px-2 py-2
 | 
			
		||||
                        sm:px-4
 | 
			
		||||
                      `}
 | 
			
		||||
                    >
 | 
			
		||||
                      Time
 | 
			
		||||
                    </th>
 | 
			
		||||
                    <th
 | 
			
		||||
                      class={`
 | 
			
		||||
                        px-2 py-2
 | 
			
		||||
                        sm:px-4
 | 
			
		||||
                      `}
 | 
			
		||||
                    >
 | 
			
		||||
                      Files
 | 
			
		||||
                    </th>
 | 
			
		||||
                    <th
 | 
			
		||||
                      class={`
 | 
			
		||||
                        px-2 py-2
 | 
			
		||||
                        max-sm:hidden
 | 
			
		||||
                        sm:px-4
 | 
			
		||||
                      `}
 | 
			
		||||
                    >
 | 
			
		||||
                      Files Done
 | 
			
		||||
                    </th>
 | 
			
		||||
                    <th
 | 
			
		||||
                      class={`
 | 
			
		||||
                        px-2 py-2
 | 
			
		||||
                        sm:px-4
 | 
			
		||||
                      `}
 | 
			
		||||
                    >
 | 
			
		||||
                      Status
 | 
			
		||||
                    </th>
 | 
			
		||||
                    <th
 | 
			
		||||
                      class={`
 | 
			
		||||
                        px-2 py-2
 | 
			
		||||
                        sm:px-4
 | 
			
		||||
                      `}
 | 
			
		||||
                    >
 | 
			
		||||
                      View
 | 
			
		||||
                    </th>
 | 
			
		||||
                  </tr>
 | 
			
		||||
                </thead>
 | 
			
		||||
                <tbody>
 | 
			
		||||
                  {userJobs.map((job) => (
 | 
			
		||||
                    <>
 | 
			
		||||
                      <tr id={`job-row-${job.id}`}>
 | 
			
		||||
                        <td class="job-details-toggle cursor-pointer" data-job-id={job.id}>
 | 
			
		||||
                          <svg
 | 
			
		||||
                            id={`arrow-${job.id}`}
 | 
			
		||||
                            xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
                            fill="none"
 | 
			
		||||
                            viewBox="0 0 24 24"
 | 
			
		||||
                            stroke-width="1.5"
 | 
			
		||||
                            stroke="currentColor"
 | 
			
		||||
                            class="inline-block h-4 w-4"
 | 
			
		||||
                          >
 | 
			
		||||
                            <path
 | 
			
		||||
                              stroke-linecap="round"
 | 
			
		||||
                              stroke-linejoin="round"
 | 
			
		||||
                              d="M8.25 4.5l7.5 7.5-7.5 7.5"
 | 
			
		||||
                            />
 | 
			
		||||
                          </svg>
 | 
			
		||||
                        </td>
 | 
			
		||||
                        <td safe>{new Date(job.date_created).toLocaleTimeString(LANGUAGE)}</td>
 | 
			
		||||
                        <td>{job.num_files}</td>
 | 
			
		||||
                        <td class="max-sm:hidden">{job.finished_files}</td>
 | 
			
		||||
                        <td safe>{job.status}</td>
 | 
			
		||||
                        <td>
 | 
			
		||||
                          <a
 | 
			
		||||
                            class={`
 | 
			
		||||
                              text-accent-500 underline
 | 
			
		||||
                              hover:text-accent-400
 | 
			
		||||
                            `}
 | 
			
		||||
                            href={`${WEBROOT}/results/${job.id}`}
 | 
			
		||||
                          >
 | 
			
		||||
                            View
 | 
			
		||||
                          </a>
 | 
			
		||||
                        </td>
 | 
			
		||||
                      </tr>
 | 
			
		||||
                      <tr id={`details-${job.id}`} class="hidden">
 | 
			
		||||
                        <td colspan="6">
 | 
			
		||||
                          <div class="p-2 text-sm text-neutral-500">
 | 
			
		||||
                            <div class="mb-1 font-semibold">Detailed File Information:</div>
 | 
			
		||||
                            {job.files_detailed.map((file: Filename) => (
 | 
			
		||||
                              <div class="flex items-center">
 | 
			
		||||
                                <span class="w-5/12 truncate" title={file.file_name} safe>
 | 
			
		||||
                                  {file.file_name}
 | 
			
		||||
                                </span>
 | 
			
		||||
                                <svg
 | 
			
		||||
                                  xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
                                  viewBox="0 0 20 20"
 | 
			
		||||
                                  fill="currentColor"
 | 
			
		||||
                                  class={`mx-2 inline-block h-4 w-4 text-neutral-500`}
 | 
			
		||||
                                >
 | 
			
		||||
                                  <path
 | 
			
		||||
                                    fill-rule="evenodd"
 | 
			
		||||
                                    d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z"
 | 
			
		||||
                                    clip-rule="evenodd"
 | 
			
		||||
                                  />
 | 
			
		||||
                                </svg>
 | 
			
		||||
                                <span class="w-5/12 truncate" title={file.output_file_name} safe>
 | 
			
		||||
                                  {file.output_file_name}
 | 
			
		||||
                                </span>
 | 
			
		||||
                              </div>
 | 
			
		||||
                            ))}
 | 
			
		||||
                          </div>
 | 
			
		||||
                        </td>
 | 
			
		||||
                      </tr>
 | 
			
		||||
                    </>
 | 
			
		||||
                  ))}
 | 
			
		||||
                </tbody>
 | 
			
		||||
              </table>
 | 
			
		||||
            </article>
 | 
			
		||||
          </main>
 | 
			
		||||
          <script>
 | 
			
		||||
            {`
 | 
			
		||||
              document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
                const toggles = document.querySelectorAll('.job-details-toggle');
 | 
			
		||||
                toggles.forEach(toggle => {
 | 
			
		||||
                  toggle.addEventListener('click', function() {
 | 
			
		||||
                    const jobId = this.dataset.jobId;
 | 
			
		||||
                    const detailsRow = document.getElementById(\`details-\${jobId}\`);
 | 
			
		||||
                    // The arrow SVG itself has the ID arrow-\${jobId}
 | 
			
		||||
                    const arrow = document.getElementById(\`arrow-\${jobId}\`);
 | 
			
		||||
 | 
			
		||||
                    if (detailsRow && arrow) {
 | 
			
		||||
                      detailsRow.classList.toggle("hidden");
 | 
			
		||||
                      if (detailsRow.classList.contains("hidden")) {
 | 
			
		||||
                        // Right-facing arrow (collapsed)
 | 
			
		||||
                        arrow.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />';
 | 
			
		||||
                      } else {
 | 
			
		||||
                        // Down-facing arrow (expanded)
 | 
			
		||||
                        arrow.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />';
 | 
			
		||||
                      }
 | 
			
		||||
                    }
 | 
			
		||||
                  });
 | 
			
		||||
                });
 | 
			
		||||
              });
 | 
			
		||||
            `}
 | 
			
		||||
          </script>
 | 
			
		||||
        </>
 | 
			
		||||
      </BaseHtml>
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
							
								
								
									
										80
									
								
								src/pages/listConverters.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,80 @@
 | 
			
		||||
import { Html } from "@elysiajs/html";
 | 
			
		||||
import Elysia from "elysia";
 | 
			
		||||
import { BaseHtml } from "../components/base";
 | 
			
		||||
import { Header } from "../components/header";
 | 
			
		||||
import { getAllInputs, getAllTargets } from "../converters/main";
 | 
			
		||||
import { ALLOW_UNAUTHENTICATED, WEBROOT } from "../helpers/env";
 | 
			
		||||
import { userService } from "./user";
 | 
			
		||||
 | 
			
		||||
export const listConverters = new Elysia()
 | 
			
		||||
  .use(userService)
 | 
			
		||||
  .get("/converters", async ({ jwt, redirect, cookie: { auth } }) => {
 | 
			
		||||
    if (!auth?.value) {
 | 
			
		||||
      return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const user = await jwt.verify(auth.value);
 | 
			
		||||
    if (!user) {
 | 
			
		||||
      return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <BaseHtml webroot={WEBROOT} title="ConvertX | Converters">
 | 
			
		||||
        <>
 | 
			
		||||
          <Header webroot={WEBROOT} allowUnauthenticated={ALLOW_UNAUTHENTICATED} loggedIn />
 | 
			
		||||
          <main
 | 
			
		||||
            class={`
 | 
			
		||||
              w-full flex-1 px-2
 | 
			
		||||
              sm:px-4
 | 
			
		||||
            `}
 | 
			
		||||
          >
 | 
			
		||||
            <article class="article">
 | 
			
		||||
              <h1 class="mb-4 text-xl">Converters</h1>
 | 
			
		||||
              <table
 | 
			
		||||
                class={`
 | 
			
		||||
                  w-full table-auto rounded bg-neutral-900 text-left
 | 
			
		||||
                  [&_td]:p-4
 | 
			
		||||
                  [&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
 | 
			
		||||
                  [&_ul]:list-inside [&_ul]:list-disc
 | 
			
		||||
                `}
 | 
			
		||||
              >
 | 
			
		||||
                <thead>
 | 
			
		||||
                  <tr>
 | 
			
		||||
                    <th class="mx-4 my-2">Converter</th>
 | 
			
		||||
                    <th class="mx-4 my-2">From (Count)</th>
 | 
			
		||||
                    <th class="mx-4 my-2">To (Count)</th>
 | 
			
		||||
                  </tr>
 | 
			
		||||
                </thead>
 | 
			
		||||
                <tbody>
 | 
			
		||||
                  {Object.entries(getAllTargets()).map(([converter, targets]) => {
 | 
			
		||||
                    const inputs = getAllInputs(converter);
 | 
			
		||||
                    return (
 | 
			
		||||
                      <tr>
 | 
			
		||||
                        <td safe>{converter}</td>
 | 
			
		||||
                        <td>
 | 
			
		||||
                          Count: {inputs.length}
 | 
			
		||||
                          <ul>
 | 
			
		||||
                            {inputs.map((input) => (
 | 
			
		||||
                              <li safe>{input}</li>
 | 
			
		||||
                            ))}
 | 
			
		||||
                          </ul>
 | 
			
		||||
                        </td>
 | 
			
		||||
                        <td>
 | 
			
		||||
                          Count: {targets.length}
 | 
			
		||||
                          <ul>
 | 
			
		||||
                            {targets.map((target) => (
 | 
			
		||||
                              <li safe>{target}</li>
 | 
			
		||||
                            ))}
 | 
			
		||||
                          </ul>
 | 
			
		||||
                        </td>
 | 
			
		||||
                      </tr>
 | 
			
		||||
                    );
 | 
			
		||||
                  })}
 | 
			
		||||
                </tbody>
 | 
			
		||||
              </table>
 | 
			
		||||
            </article>
 | 
			
		||||
          </main>
 | 
			
		||||
        </>
 | 
			
		||||
      </BaseHtml>
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
							
								
								
									
										225
									
								
								src/pages/results.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,225 @@
 | 
			
		||||
import { Html } from "@elysiajs/html";
 | 
			
		||||
import { JWTPayloadSpec } from "@elysiajs/jwt";
 | 
			
		||||
import { Elysia } from "elysia";
 | 
			
		||||
import { BaseHtml } from "../components/base";
 | 
			
		||||
import { Header } from "../components/header";
 | 
			
		||||
import db from "../db/db";
 | 
			
		||||
import { Filename, Jobs } from "../db/types";
 | 
			
		||||
import { ALLOW_UNAUTHENTICATED, WEBROOT } from "../helpers/env";
 | 
			
		||||
import { userService } from "./user";
 | 
			
		||||
 | 
			
		||||
function ResultsArticle({
 | 
			
		||||
  user,
 | 
			
		||||
  job,
 | 
			
		||||
  files,
 | 
			
		||||
  outputPath,
 | 
			
		||||
}: {
 | 
			
		||||
  user: {
 | 
			
		||||
    id: string;
 | 
			
		||||
  } & JWTPayloadSpec;
 | 
			
		||||
  job: Jobs;
 | 
			
		||||
  files: Filename[];
 | 
			
		||||
  outputPath: string;
 | 
			
		||||
}) {
 | 
			
		||||
  return (
 | 
			
		||||
    <article class="article">
 | 
			
		||||
      <div class="mb-4 flex items-center justify-between">
 | 
			
		||||
        <h1 class="text-xl">Results</h1>
 | 
			
		||||
        <div>
 | 
			
		||||
          <a
 | 
			
		||||
            style={files.length !== job.num_files ? "pointer-events: none;" : ""}
 | 
			
		||||
            href={`${WEBROOT}/archive/${user.id}/${job.id}`}
 | 
			
		||||
            download={`converted_files_${job.id}.tar`}
 | 
			
		||||
          >
 | 
			
		||||
            <button
 | 
			
		||||
              type="button"
 | 
			
		||||
              class="float-right w-40 btn-primary"
 | 
			
		||||
              {...(files.length !== job.num_files ? { disabled: true, "aria-busy": "true" } : "")}
 | 
			
		||||
            >
 | 
			
		||||
              {files.length === job.num_files ? "Download All" : "Converting..."}
 | 
			
		||||
            </button>
 | 
			
		||||
          </a>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <progress
 | 
			
		||||
        max={job.num_files}
 | 
			
		||||
        value={files.length}
 | 
			
		||||
        class={`
 | 
			
		||||
          mb-4 inline-block h-2 w-full appearance-none overflow-hidden rounded-full border-0
 | 
			
		||||
          bg-neutral-700 bg-none text-accent-500 accent-accent-500
 | 
			
		||||
          [&::-moz-progress-bar]:bg-accent-500 [&::-webkit-progress-value]:rounded-full
 | 
			
		||||
          [&::-webkit-progress-value]:[background:none]
 | 
			
		||||
          [&[value]::-webkit-progress-value]:bg-accent-500
 | 
			
		||||
          [&[value]::-webkit-progress-value]:transition-[inline-size]
 | 
			
		||||
        `}
 | 
			
		||||
      />
 | 
			
		||||
      <table
 | 
			
		||||
        class={`
 | 
			
		||||
          w-full table-auto rounded bg-neutral-900 text-left
 | 
			
		||||
          [&_td]:p-4
 | 
			
		||||
          [&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
 | 
			
		||||
        `}
 | 
			
		||||
      >
 | 
			
		||||
        <thead>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <th
 | 
			
		||||
              class={`
 | 
			
		||||
                px-2 py-2
 | 
			
		||||
                sm:px-4
 | 
			
		||||
              `}
 | 
			
		||||
            >
 | 
			
		||||
              Converted File Name
 | 
			
		||||
            </th>
 | 
			
		||||
            <th
 | 
			
		||||
              class={`
 | 
			
		||||
                px-2 py-2
 | 
			
		||||
                sm:px-4
 | 
			
		||||
              `}
 | 
			
		||||
            >
 | 
			
		||||
              Status
 | 
			
		||||
            </th>
 | 
			
		||||
            <th
 | 
			
		||||
              class={`
 | 
			
		||||
                px-2 py-2
 | 
			
		||||
                sm:px-4
 | 
			
		||||
              `}
 | 
			
		||||
            >
 | 
			
		||||
              View
 | 
			
		||||
            </th>
 | 
			
		||||
            <th
 | 
			
		||||
              class={`
 | 
			
		||||
                px-2 py-2
 | 
			
		||||
                sm:px-4
 | 
			
		||||
              `}
 | 
			
		||||
            >
 | 
			
		||||
              Download
 | 
			
		||||
            </th>
 | 
			
		||||
          </tr>
 | 
			
		||||
        </thead>
 | 
			
		||||
        <tbody>
 | 
			
		||||
          {files.map((file) => (
 | 
			
		||||
            <tr>
 | 
			
		||||
              <td safe class="max-w-[20vw] truncate">
 | 
			
		||||
                {file.output_file_name}
 | 
			
		||||
              </td>
 | 
			
		||||
              <td safe>{file.status}</td>
 | 
			
		||||
              <td>
 | 
			
		||||
                <a
 | 
			
		||||
                  class={`
 | 
			
		||||
                    text-accent-500 underline
 | 
			
		||||
                    hover:text-accent-400
 | 
			
		||||
                  `}
 | 
			
		||||
                  href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
 | 
			
		||||
                >
 | 
			
		||||
                  View
 | 
			
		||||
                </a>
 | 
			
		||||
              </td>
 | 
			
		||||
              <td>
 | 
			
		||||
                <a
 | 
			
		||||
                  class={`
 | 
			
		||||
                    text-accent-500 underline
 | 
			
		||||
                    hover:text-accent-400
 | 
			
		||||
                  `}
 | 
			
		||||
                  href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
 | 
			
		||||
                  download={file.output_file_name}
 | 
			
		||||
                >
 | 
			
		||||
                  Download
 | 
			
		||||
                </a>
 | 
			
		||||
              </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          ))}
 | 
			
		||||
        </tbody>
 | 
			
		||||
      </table>
 | 
			
		||||
    </article>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const results = new Elysia()
 | 
			
		||||
  .use(userService)
 | 
			
		||||
  .get("/results/:jobId", async ({ params, jwt, set, redirect, cookie: { auth, job_id } }) => {
 | 
			
		||||
    if (!auth?.value) {
 | 
			
		||||
      return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (job_id?.value) {
 | 
			
		||||
      // clear the job_id cookie since we are viewing the results
 | 
			
		||||
      job_id.remove();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const user = await jwt.verify(auth.value);
 | 
			
		||||
    if (!user) {
 | 
			
		||||
      return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const job = db
 | 
			
		||||
      .query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
 | 
			
		||||
      .as(Jobs)
 | 
			
		||||
      .get(user.id, params.jobId);
 | 
			
		||||
 | 
			
		||||
    if (!job) {
 | 
			
		||||
      set.status = 404;
 | 
			
		||||
      return {
 | 
			
		||||
        message: "Job not found.",
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const outputPath = `${user.id}/${params.jobId}/`;
 | 
			
		||||
 | 
			
		||||
    const files = db
 | 
			
		||||
      .query("SELECT * FROM file_names WHERE job_id = ?")
 | 
			
		||||
      .as(Filename)
 | 
			
		||||
      .all(params.jobId);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <BaseHtml webroot={WEBROOT} title="ConvertX | Result">
 | 
			
		||||
        <>
 | 
			
		||||
          <Header webroot={WEBROOT} allowUnauthenticated={ALLOW_UNAUTHENTICATED} loggedIn />
 | 
			
		||||
          <main
 | 
			
		||||
            class={`
 | 
			
		||||
              w-full flex-1 px-2
 | 
			
		||||
              sm:px-4
 | 
			
		||||
            `}
 | 
			
		||||
          >
 | 
			
		||||
            <ResultsArticle user={user} job={job} files={files} outputPath={outputPath} />
 | 
			
		||||
          </main>
 | 
			
		||||
          <script src={`${WEBROOT}/results.js`} defer />
 | 
			
		||||
        </>
 | 
			
		||||
      </BaseHtml>
 | 
			
		||||
    );
 | 
			
		||||
  })
 | 
			
		||||
  .post("/progress/:jobId", async ({ jwt, set, params, redirect, cookie: { auth, job_id } }) => {
 | 
			
		||||
    if (!auth?.value) {
 | 
			
		||||
      return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (job_id?.value) {
 | 
			
		||||
      // clear the job_id cookie since we are viewing the results
 | 
			
		||||
      job_id.remove();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const user = await jwt.verify(auth.value);
 | 
			
		||||
    if (!user) {
 | 
			
		||||
      return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const job = db
 | 
			
		||||
      .query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
 | 
			
		||||
      .as(Jobs)
 | 
			
		||||
      .get(user.id, params.jobId);
 | 
			
		||||
 | 
			
		||||
    if (!job) {
 | 
			
		||||
      set.status = 404;
 | 
			
		||||
      return {
 | 
			
		||||
        message: "Job not found.",
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const outputPath = `${user.id}/${params.jobId}/`;
 | 
			
		||||
 | 
			
		||||
    const files = db
 | 
			
		||||
      .query("SELECT * FROM file_names WHERE job_id = ?")
 | 
			
		||||
      .as(Filename)
 | 
			
		||||
      .all(params.jobId);
 | 
			
		||||
 | 
			
		||||
    return <ResultsArticle user={user} job={job} files={files} outputPath={outputPath} />;
 | 
			
		||||
  });
 | 
			
		||||
							
								
								
									
										243
									
								
								src/pages/root.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,243 @@
 | 
			
		||||
import { randomInt } from "node:crypto";
 | 
			
		||||
import { Html } from "@elysiajs/html";
 | 
			
		||||
import { JWTPayloadSpec } from "@elysiajs/jwt";
 | 
			
		||||
import { Elysia } from "elysia";
 | 
			
		||||
import { BaseHtml } from "../components/base";
 | 
			
		||||
import { Header } from "../components/header";
 | 
			
		||||
import { getAllTargets } from "../converters/main";
 | 
			
		||||
import db from "../db/db";
 | 
			
		||||
import { User } from "../db/types";
 | 
			
		||||
import {
 | 
			
		||||
  ACCOUNT_REGISTRATION,
 | 
			
		||||
  ALLOW_UNAUTHENTICATED,
 | 
			
		||||
  HIDE_HISTORY,
 | 
			
		||||
  HTTP_ALLOWED,
 | 
			
		||||
  UNAUTHENTICATED_USER_SHARING,
 | 
			
		||||
  WEBROOT,
 | 
			
		||||
} from "../helpers/env";
 | 
			
		||||
import { FIRST_RUN, userService } from "./user";
 | 
			
		||||
 | 
			
		||||
export const root = new Elysia()
 | 
			
		||||
  .use(userService)
 | 
			
		||||
  .get("/", async ({ jwt, redirect, cookie: { auth, jobId } }) => {
 | 
			
		||||
    if (!ALLOW_UNAUTHENTICATED) {
 | 
			
		||||
      if (FIRST_RUN) {
 | 
			
		||||
        return redirect(`${WEBROOT}/setup`, 302);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!auth?.value) {
 | 
			
		||||
        return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // validate jwt
 | 
			
		||||
    let user: ({ id: string } & JWTPayloadSpec) | false = false;
 | 
			
		||||
    if (ALLOW_UNAUTHENTICATED) {
 | 
			
		||||
      const newUserId = String(
 | 
			
		||||
        UNAUTHENTICATED_USER_SHARING
 | 
			
		||||
          ? 0
 | 
			
		||||
          : randomInt(2 ** 24, Math.min(2 ** 48 + 2 ** 24 - 1, Number.MAX_SAFE_INTEGER)),
 | 
			
		||||
      );
 | 
			
		||||
      const accessToken = await jwt.sign({
 | 
			
		||||
        id: newUserId,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      user = { id: newUserId };
 | 
			
		||||
      if (!auth) {
 | 
			
		||||
        return {
 | 
			
		||||
          message: "No auth cookie, perhaps your browser is blocking cookies.",
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // set cookie
 | 
			
		||||
      auth.set({
 | 
			
		||||
        value: accessToken,
 | 
			
		||||
        httpOnly: true,
 | 
			
		||||
        secure: !HTTP_ALLOWED,
 | 
			
		||||
        maxAge: 24 * 60 * 60,
 | 
			
		||||
        sameSite: "strict",
 | 
			
		||||
      });
 | 
			
		||||
    } else if (auth?.value) {
 | 
			
		||||
      user = await jwt.verify(auth.value);
 | 
			
		||||
 | 
			
		||||
      if (
 | 
			
		||||
        user !== false &&
 | 
			
		||||
        user.id &&
 | 
			
		||||
        (Number.parseInt(user.id) < 2 ** 24 || !ALLOW_UNAUTHENTICATED)
 | 
			
		||||
      ) {
 | 
			
		||||
        // make sure user exists in db
 | 
			
		||||
        const existingUser = db.query("SELECT * FROM users WHERE id = ?").as(User).get(user.id);
 | 
			
		||||
 | 
			
		||||
        if (!existingUser) {
 | 
			
		||||
          if (auth?.value) {
 | 
			
		||||
            auth.remove();
 | 
			
		||||
          }
 | 
			
		||||
          return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!user) {
 | 
			
		||||
      return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // create a new job
 | 
			
		||||
    db.query("INSERT INTO jobs (user_id, date_created) VALUES (?, ?)").run(
 | 
			
		||||
      user.id,
 | 
			
		||||
      new Date().toISOString(),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const { id } = db
 | 
			
		||||
      .query("SELECT id FROM jobs WHERE user_id = ? ORDER BY id DESC")
 | 
			
		||||
      .get(user.id) as { id: number };
 | 
			
		||||
 | 
			
		||||
    if (!jobId) {
 | 
			
		||||
      return { message: "Cookies should be enabled to use this app." };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    jobId.set({
 | 
			
		||||
      value: id,
 | 
			
		||||
      httpOnly: true,
 | 
			
		||||
      secure: !HTTP_ALLOWED,
 | 
			
		||||
      maxAge: 24 * 60 * 60,
 | 
			
		||||
      sameSite: "strict",
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    console.log("jobId set to:", id);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <BaseHtml webroot={WEBROOT}>
 | 
			
		||||
        <>
 | 
			
		||||
          <Header
 | 
			
		||||
            webroot={WEBROOT}
 | 
			
		||||
            accountRegistration={ACCOUNT_REGISTRATION}
 | 
			
		||||
            allowUnauthenticated={ALLOW_UNAUTHENTICATED}
 | 
			
		||||
            hideHistory={HIDE_HISTORY}
 | 
			
		||||
            loggedIn
 | 
			
		||||
          />
 | 
			
		||||
          <main
 | 
			
		||||
            class={`
 | 
			
		||||
              w-full flex-1 px-2
 | 
			
		||||
              sm:px-4
 | 
			
		||||
            `}
 | 
			
		||||
          >
 | 
			
		||||
            <article class="article">
 | 
			
		||||
              <h1 class="mb-4 text-xl">Convert</h1>
 | 
			
		||||
              <div class="mb-4 scrollbar-thin max-h-[50vh] overflow-y-auto">
 | 
			
		||||
                <table
 | 
			
		||||
                  id="file-list"
 | 
			
		||||
                  class={`
 | 
			
		||||
                    w-full table-auto rounded bg-neutral-900
 | 
			
		||||
                    [&_td]:p-4 [&_td]:first:max-w-[30vw] [&_td]:first:truncate
 | 
			
		||||
                    [&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
 | 
			
		||||
                  `}
 | 
			
		||||
                />
 | 
			
		||||
              </div>
 | 
			
		||||
              <div
 | 
			
		||||
                id="dropzone"
 | 
			
		||||
                class={`
 | 
			
		||||
                  relative flex h-48 w-full items-center justify-center rounded border border-dashed
 | 
			
		||||
                  border-neutral-700 transition-all
 | 
			
		||||
                  hover:border-neutral-600
 | 
			
		||||
                  [&.dragover]:border-4 [&.dragover]:border-neutral-500
 | 
			
		||||
                `}
 | 
			
		||||
              >
 | 
			
		||||
                <span>
 | 
			
		||||
                  <b>Choose a file</b> or drag it here
 | 
			
		||||
                </span>
 | 
			
		||||
                <input
 | 
			
		||||
                  type="file"
 | 
			
		||||
                  name="file"
 | 
			
		||||
                  multiple
 | 
			
		||||
                  class="absolute inset-0 size-full cursor-pointer opacity-0"
 | 
			
		||||
                />
 | 
			
		||||
              </div>
 | 
			
		||||
            </article>
 | 
			
		||||
            <form
 | 
			
		||||
              method="post"
 | 
			
		||||
              action={`${WEBROOT}/convert`}
 | 
			
		||||
              class="relative mx-auto mb-[35vh] w-full max-w-4xl"
 | 
			
		||||
            >
 | 
			
		||||
              <input type="hidden" name="file_names" id="file_names" />
 | 
			
		||||
              <article class="article w-full">
 | 
			
		||||
                <input
 | 
			
		||||
                  type="search"
 | 
			
		||||
                  name="convert_to_search"
 | 
			
		||||
                  placeholder="Search for conversions"
 | 
			
		||||
                  autocomplete="off"
 | 
			
		||||
                  class="w-full rounded-sm bg-neutral-800 p-4"
 | 
			
		||||
                />
 | 
			
		||||
                <div class="select_container relative">
 | 
			
		||||
                  <article
 | 
			
		||||
                    class={`
 | 
			
		||||
                      convert_to_popup absolute z-2 m-0 hidden h-[30vh] max-h-[50vh] w-full flex-col
 | 
			
		||||
                      overflow-x-hidden overflow-y-auto rounded bg-neutral-800
 | 
			
		||||
                      sm:h-[30vh]
 | 
			
		||||
                    `}
 | 
			
		||||
                  >
 | 
			
		||||
                    {Object.entries(getAllTargets()).map(([converter, targets]) => (
 | 
			
		||||
                      <article
 | 
			
		||||
                        class={`
 | 
			
		||||
                          convert_to_group flex w-full flex-col border-b border-neutral-700 p-4
 | 
			
		||||
                        `}
 | 
			
		||||
                        data-converter={converter}
 | 
			
		||||
                      >
 | 
			
		||||
                        <header class="mb-2 w-full text-xl font-bold" safe>
 | 
			
		||||
                          {converter}
 | 
			
		||||
                        </header>
 | 
			
		||||
                        <ul class={`convert_to_target flex flex-row flex-wrap gap-1`}>
 | 
			
		||||
                          {targets.map((target) => (
 | 
			
		||||
                            <button
 | 
			
		||||
                              // https://stackoverflow.com/questions/121499/when-a-blur-event-occurs-how-can-i-find-out-which-element-focus-went-to#comment82388679_33325953
 | 
			
		||||
                              tabindex={0}
 | 
			
		||||
                              class={`
 | 
			
		||||
                                target rounded bg-neutral-700 p-1 text-base
 | 
			
		||||
                                hover:bg-neutral-600
 | 
			
		||||
                              `}
 | 
			
		||||
                              data-value={`${target},${converter}`}
 | 
			
		||||
                              data-target={target}
 | 
			
		||||
                              data-converter={converter}
 | 
			
		||||
                              type="button"
 | 
			
		||||
                              safe
 | 
			
		||||
                            >
 | 
			
		||||
                              {target}
 | 
			
		||||
                            </button>
 | 
			
		||||
                          ))}
 | 
			
		||||
                        </ul>
 | 
			
		||||
                      </article>
 | 
			
		||||
                    ))}
 | 
			
		||||
                  </article>
 | 
			
		||||
 | 
			
		||||
                  {/* Hidden element which determines the format to convert the file too and the converter to use */}
 | 
			
		||||
                  <select name="convert_to" aria-label="Convert to" required hidden>
 | 
			
		||||
                    <option selected disabled value="">
 | 
			
		||||
                      Convert to
 | 
			
		||||
                    </option>
 | 
			
		||||
                    {Object.entries(getAllTargets()).map(([converter, targets]) => (
 | 
			
		||||
                      <optgroup label={converter}>
 | 
			
		||||
                        {targets.map((target) => (
 | 
			
		||||
                          <option value={`${target},${converter}`} safe>
 | 
			
		||||
                            {target}
 | 
			
		||||
                          </option>
 | 
			
		||||
                        ))}
 | 
			
		||||
                      </optgroup>
 | 
			
		||||
                    ))}
 | 
			
		||||
                  </select>
 | 
			
		||||
                </div>
 | 
			
		||||
              </article>
 | 
			
		||||
              <input
 | 
			
		||||
                class={`
 | 
			
		||||
                  w-full btn-primary opacity-100
 | 
			
		||||
                  disabled:cursor-not-allowed disabled:opacity-50
 | 
			
		||||
                `}
 | 
			
		||||
                type="submit"
 | 
			
		||||
                value="Convert"
 | 
			
		||||
                disabled
 | 
			
		||||
              />
 | 
			
		||||
            </form>
 | 
			
		||||
          </main>
 | 
			
		||||
          <script src="script.js" defer />
 | 
			
		||||
        </>
 | 
			
		||||
      </BaseHtml>
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
							
								
								
									
										48
									
								
								src/pages/upload.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,48 @@
 | 
			
		||||
import { Elysia, t } from "elysia";
 | 
			
		||||
import db from "../db/db";
 | 
			
		||||
import { WEBROOT } from "../helpers/env";
 | 
			
		||||
import { uploadsDir } from "../index";
 | 
			
		||||
import { userService } from "./user";
 | 
			
		||||
 | 
			
		||||
export const upload = new Elysia().use(userService).post(
 | 
			
		||||
  "/upload",
 | 
			
		||||
  async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
 | 
			
		||||
    if (!auth?.value) {
 | 
			
		||||
      return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const user = await jwt.verify(auth.value);
 | 
			
		||||
    if (!user) {
 | 
			
		||||
      return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!jobId?.value) {
 | 
			
		||||
      return redirect(`${WEBROOT}/`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const existingJob = await db
 | 
			
		||||
      .query("SELECT * FROM jobs WHERE id = ? AND user_id = ?")
 | 
			
		||||
      .get(jobId.value, user.id);
 | 
			
		||||
 | 
			
		||||
    if (!existingJob) {
 | 
			
		||||
      return redirect(`${WEBROOT}/`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
 | 
			
		||||
 | 
			
		||||
    if (body?.file) {
 | 
			
		||||
      if (Array.isArray(body.file)) {
 | 
			
		||||
        for (const file of body.file) {
 | 
			
		||||
          await Bun.write(`${userUploadsDir}${file.name}`, file);
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        await Bun.write(`${userUploadsDir}${body.file["name"]}`, body.file);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      message: "Files uploaded successfully.",
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  { body: t.Object({ file: t.Files() }) },
 | 
			
		||||
);
 | 
			
		||||
							
								
								
									
										509
									
								
								src/pages/user.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,509 @@
 | 
			
		||||
import { randomUUID } from "node:crypto";
 | 
			
		||||
import { Html } from "@elysiajs/html";
 | 
			
		||||
import { jwt } from "@elysiajs/jwt";
 | 
			
		||||
import { Elysia, t } from "elysia";
 | 
			
		||||
import { BaseHtml } from "../components/base";
 | 
			
		||||
import { Header } from "../components/header";
 | 
			
		||||
import db from "../db/db";
 | 
			
		||||
import { User } from "../db/types";
 | 
			
		||||
import {
 | 
			
		||||
  ACCOUNT_REGISTRATION,
 | 
			
		||||
  ALLOW_UNAUTHENTICATED,
 | 
			
		||||
  HIDE_HISTORY,
 | 
			
		||||
  HTTP_ALLOWED,
 | 
			
		||||
  WEBROOT,
 | 
			
		||||
} from "../helpers/env";
 | 
			
		||||
 | 
			
		||||
export let FIRST_RUN = db.query("SELECT * FROM users").get() === null || false;
 | 
			
		||||
 | 
			
		||||
export const userService = new Elysia({ name: "user/service" })
 | 
			
		||||
  .use(
 | 
			
		||||
    jwt({
 | 
			
		||||
      name: "jwt",
 | 
			
		||||
      schema: t.Object({
 | 
			
		||||
        id: t.String(),
 | 
			
		||||
      }),
 | 
			
		||||
      secret: process.env.JWT_SECRET ?? randomUUID(),
 | 
			
		||||
      exp: "7d",
 | 
			
		||||
    }),
 | 
			
		||||
  )
 | 
			
		||||
  .model({
 | 
			
		||||
    signIn: t.Object({
 | 
			
		||||
      email: t.String(),
 | 
			
		||||
      password: t.String(),
 | 
			
		||||
    }),
 | 
			
		||||
  })
 | 
			
		||||
  .macro({
 | 
			
		||||
    isSignIn(enabled: boolean) {
 | 
			
		||||
      if (!enabled) return;
 | 
			
		||||
 | 
			
		||||
      return {
 | 
			
		||||
        async beforeHandle({ status, jwt, cookie: { auth } }) {
 | 
			
		||||
          if (auth?.value) {
 | 
			
		||||
            const user = await jwt.verify(auth.value);
 | 
			
		||||
            return {
 | 
			
		||||
              success: true,
 | 
			
		||||
              user,
 | 
			
		||||
            };
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          return status(401, {
 | 
			
		||||
            success: false,
 | 
			
		||||
            message: "Unauthorized",
 | 
			
		||||
          });
 | 
			
		||||
        },
 | 
			
		||||
      };
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
export const user = new Elysia()
 | 
			
		||||
  .use(userService)
 | 
			
		||||
  .get("/setup", ({ redirect }) => {
 | 
			
		||||
    if (!FIRST_RUN) {
 | 
			
		||||
      return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <BaseHtml title="ConvertX | Setup" webroot={WEBROOT}>
 | 
			
		||||
        <main
 | 
			
		||||
          class={`
 | 
			
		||||
            mx-auto w-full max-w-4xl flex-1 px-2
 | 
			
		||||
            sm:px-4
 | 
			
		||||
          `}
 | 
			
		||||
        >
 | 
			
		||||
          <h1 class="my-8 text-3xl">Welcome to ConvertX!</h1>
 | 
			
		||||
          <article class="article p-0">
 | 
			
		||||
            <header class="w-full bg-neutral-800 p-4">Create your account</header>
 | 
			
		||||
            <form method="post" action={`${WEBROOT}/register`} class="p-4">
 | 
			
		||||
              <fieldset class="mb-4 flex flex-col gap-4">
 | 
			
		||||
                <label class="flex flex-col gap-1">
 | 
			
		||||
                  Email
 | 
			
		||||
                  <input
 | 
			
		||||
                    type="email"
 | 
			
		||||
                    name="email"
 | 
			
		||||
                    class="rounded-sm bg-neutral-800 p-3"
 | 
			
		||||
                    placeholder="Email"
 | 
			
		||||
                    autocomplete="email"
 | 
			
		||||
                    required
 | 
			
		||||
                  />
 | 
			
		||||
                </label>
 | 
			
		||||
                <label class="flex flex-col gap-1">
 | 
			
		||||
                  Password
 | 
			
		||||
                  <input
 | 
			
		||||
                    type="password"
 | 
			
		||||
                    name="password"
 | 
			
		||||
                    class="rounded-sm bg-neutral-800 p-3"
 | 
			
		||||
                    placeholder="Password"
 | 
			
		||||
                    autocomplete="current-password"
 | 
			
		||||
                    required
 | 
			
		||||
                  />
 | 
			
		||||
                </label>
 | 
			
		||||
              </fieldset>
 | 
			
		||||
              <input type="submit" value="Create account" class="btn-primary" />
 | 
			
		||||
            </form>
 | 
			
		||||
            <footer class="p-4">
 | 
			
		||||
              Report any issues on{" "}
 | 
			
		||||
              <a
 | 
			
		||||
                class={`
 | 
			
		||||
                  text-accent-500 underline
 | 
			
		||||
                  hover:text-accent-400
 | 
			
		||||
                `}
 | 
			
		||||
                href="https://github.com/C4illin/ConvertX"
 | 
			
		||||
              >
 | 
			
		||||
                GitHub
 | 
			
		||||
              </a>
 | 
			
		||||
              .
 | 
			
		||||
            </footer>
 | 
			
		||||
          </article>
 | 
			
		||||
        </main>
 | 
			
		||||
      </BaseHtml>
 | 
			
		||||
    );
 | 
			
		||||
  })
 | 
			
		||||
  .get("/register", ({ redirect }) => {
 | 
			
		||||
    if (!ACCOUNT_REGISTRATION) {
 | 
			
		||||
      return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <BaseHtml webroot={WEBROOT} title="ConvertX | Register">
 | 
			
		||||
        <>
 | 
			
		||||
          <Header
 | 
			
		||||
            webroot={WEBROOT}
 | 
			
		||||
            accountRegistration={ACCOUNT_REGISTRATION}
 | 
			
		||||
            allowUnauthenticated={ALLOW_UNAUTHENTICATED}
 | 
			
		||||
            hideHistory={HIDE_HISTORY}
 | 
			
		||||
          />
 | 
			
		||||
          <main
 | 
			
		||||
            class={`
 | 
			
		||||
              w-full flex-1 px-2
 | 
			
		||||
              sm:px-4
 | 
			
		||||
            `}
 | 
			
		||||
          >
 | 
			
		||||
            <article class="article">
 | 
			
		||||
              <form method="post" class="flex flex-col gap-4">
 | 
			
		||||
                <fieldset class="mb-4 flex flex-col gap-4">
 | 
			
		||||
                  <label class="flex flex-col gap-1">
 | 
			
		||||
                    Email
 | 
			
		||||
                    <input
 | 
			
		||||
                      type="email"
 | 
			
		||||
                      name="email"
 | 
			
		||||
                      class="rounded-sm bg-neutral-800 p-3"
 | 
			
		||||
                      placeholder="Email"
 | 
			
		||||
                      autocomplete="email"
 | 
			
		||||
                      required
 | 
			
		||||
                    />
 | 
			
		||||
                  </label>
 | 
			
		||||
                  <label class="flex flex-col gap-1">
 | 
			
		||||
                    Password
 | 
			
		||||
                    <input
 | 
			
		||||
                      type="password"
 | 
			
		||||
                      name="password"
 | 
			
		||||
                      class="rounded-sm bg-neutral-800 p-3"
 | 
			
		||||
                      placeholder="Password"
 | 
			
		||||
                      autocomplete="current-password"
 | 
			
		||||
                      required
 | 
			
		||||
                    />
 | 
			
		||||
                  </label>
 | 
			
		||||
                </fieldset>
 | 
			
		||||
                <input type="submit" value="Register" class="w-full btn-primary" />
 | 
			
		||||
              </form>
 | 
			
		||||
            </article>
 | 
			
		||||
          </main>
 | 
			
		||||
        </>
 | 
			
		||||
      </BaseHtml>
 | 
			
		||||
    );
 | 
			
		||||
  })
 | 
			
		||||
  .post(
 | 
			
		||||
    "/register",
 | 
			
		||||
    async ({ body: { email, password }, set, redirect, jwt, cookie: { auth } }) => {
 | 
			
		||||
      if (!ACCOUNT_REGISTRATION && !FIRST_RUN) {
 | 
			
		||||
        return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (FIRST_RUN) {
 | 
			
		||||
        FIRST_RUN = false;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const existingUser = await db.query("SELECT * FROM users WHERE email = ?").get(email);
 | 
			
		||||
      if (existingUser) {
 | 
			
		||||
        set.status = 400;
 | 
			
		||||
        return {
 | 
			
		||||
          message: "Email already in use.",
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
      const savedPassword = await Bun.password.hash(password);
 | 
			
		||||
 | 
			
		||||
      db.query("INSERT INTO users (email, password) VALUES (?, ?)").run(email, savedPassword);
 | 
			
		||||
 | 
			
		||||
      const user = db.query("SELECT * FROM users WHERE email = ?").as(User).get(email);
 | 
			
		||||
 | 
			
		||||
      if (!user) {
 | 
			
		||||
        set.status = 500;
 | 
			
		||||
        return {
 | 
			
		||||
          message: "Failed to create user.",
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const accessToken = await jwt.sign({
 | 
			
		||||
        id: String(user.id),
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      if (!auth) {
 | 
			
		||||
        set.status = 500;
 | 
			
		||||
        return {
 | 
			
		||||
          message: "No auth cookie, perhaps your browser is blocking cookies.",
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // set cookie
 | 
			
		||||
      auth.set({
 | 
			
		||||
        value: accessToken,
 | 
			
		||||
        httpOnly: true,
 | 
			
		||||
        secure: !HTTP_ALLOWED,
 | 
			
		||||
        maxAge: 60 * 60 * 24 * 7,
 | 
			
		||||
        sameSite: "strict",
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return redirect(`${WEBROOT}/`, 302);
 | 
			
		||||
    },
 | 
			
		||||
    { body: "signIn" },
 | 
			
		||||
  )
 | 
			
		||||
  .get("/login", async ({ jwt, redirect, cookie: { auth } }) => {
 | 
			
		||||
    if (FIRST_RUN) {
 | 
			
		||||
      return redirect(`${WEBROOT}/setup`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // if already logged in, redirect to home
 | 
			
		||||
    if (auth?.value) {
 | 
			
		||||
      const user = await jwt.verify(auth.value);
 | 
			
		||||
 | 
			
		||||
      if (user) {
 | 
			
		||||
        return redirect(`${WEBROOT}/`, 302);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      auth.remove();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <BaseHtml webroot={WEBROOT} title="ConvertX | Login">
 | 
			
		||||
        <>
 | 
			
		||||
          <Header
 | 
			
		||||
            webroot={WEBROOT}
 | 
			
		||||
            accountRegistration={ACCOUNT_REGISTRATION}
 | 
			
		||||
            allowUnauthenticated={ALLOW_UNAUTHENTICATED}
 | 
			
		||||
            hideHistory={HIDE_HISTORY}
 | 
			
		||||
          />
 | 
			
		||||
          <main
 | 
			
		||||
            class={`
 | 
			
		||||
              w-full flex-1 px-2
 | 
			
		||||
              sm:px-4
 | 
			
		||||
            `}
 | 
			
		||||
          >
 | 
			
		||||
            <article class="article">
 | 
			
		||||
              <form method="post" class="flex flex-col gap-4">
 | 
			
		||||
                <fieldset class="mb-4 flex flex-col gap-4">
 | 
			
		||||
                  <label class="flex flex-col gap-1">
 | 
			
		||||
                    Email
 | 
			
		||||
                    <input
 | 
			
		||||
                      type="email"
 | 
			
		||||
                      name="email"
 | 
			
		||||
                      class="rounded-sm bg-neutral-800 p-3"
 | 
			
		||||
                      placeholder="Email"
 | 
			
		||||
                      autocomplete="email"
 | 
			
		||||
                      required
 | 
			
		||||
                    />
 | 
			
		||||
                  </label>
 | 
			
		||||
                  <label class="flex flex-col gap-1">
 | 
			
		||||
                    Password
 | 
			
		||||
                    <input
 | 
			
		||||
                      type="password"
 | 
			
		||||
                      name="password"
 | 
			
		||||
                      class="rounded-sm bg-neutral-800 p-3"
 | 
			
		||||
                      placeholder="Password"
 | 
			
		||||
                      autocomplete="current-password"
 | 
			
		||||
                      required
 | 
			
		||||
                    />
 | 
			
		||||
                  </label>
 | 
			
		||||
                </fieldset>
 | 
			
		||||
                <div class="flex flex-row gap-4">
 | 
			
		||||
                  {ACCOUNT_REGISTRATION ? (
 | 
			
		||||
                    <a
 | 
			
		||||
                      href={`${WEBROOT}/register`}
 | 
			
		||||
                      role="button"
 | 
			
		||||
                      class="w-full btn-secondary text-center"
 | 
			
		||||
                    >
 | 
			
		||||
                      Register
 | 
			
		||||
                    </a>
 | 
			
		||||
                  ) : null}
 | 
			
		||||
                  <input type="submit" value="Login" class="w-full btn-primary" />
 | 
			
		||||
                </div>
 | 
			
		||||
              </form>
 | 
			
		||||
            </article>
 | 
			
		||||
          </main>
 | 
			
		||||
        </>
 | 
			
		||||
      </BaseHtml>
 | 
			
		||||
    );
 | 
			
		||||
  })
 | 
			
		||||
  .post(
 | 
			
		||||
    "/login",
 | 
			
		||||
    async function handler({ body, set, redirect, jwt, cookie: { auth } }) {
 | 
			
		||||
      const existingUser = db.query("SELECT * FROM users WHERE email = ?").as(User).get(body.email);
 | 
			
		||||
 | 
			
		||||
      if (!existingUser) {
 | 
			
		||||
        set.status = 403;
 | 
			
		||||
        return {
 | 
			
		||||
          message: "Invalid credentials.",
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const validPassword = await Bun.password.verify(body.password, existingUser.password);
 | 
			
		||||
 | 
			
		||||
      if (!validPassword) {
 | 
			
		||||
        set.status = 403;
 | 
			
		||||
        return {
 | 
			
		||||
          message: "Invalid credentials.",
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const accessToken = await jwt.sign({
 | 
			
		||||
        id: String(existingUser.id),
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      if (!auth) {
 | 
			
		||||
        set.status = 500;
 | 
			
		||||
        return {
 | 
			
		||||
          message: "No auth cookie, perhaps your browser is blocking cookies.",
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // set cookie
 | 
			
		||||
      auth.set({
 | 
			
		||||
        value: accessToken,
 | 
			
		||||
        httpOnly: true,
 | 
			
		||||
        secure: !HTTP_ALLOWED,
 | 
			
		||||
        maxAge: 60 * 60 * 24 * 7,
 | 
			
		||||
        sameSite: "strict",
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return redirect(`${WEBROOT}/`, 302);
 | 
			
		||||
    },
 | 
			
		||||
    { body: "signIn" },
 | 
			
		||||
  )
 | 
			
		||||
  .get("/logoff", ({ redirect, cookie: { auth } }) => {
 | 
			
		||||
    if (auth?.value) {
 | 
			
		||||
      auth.remove();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
  })
 | 
			
		||||
  .post("/logoff", ({ redirect, cookie: { auth } }) => {
 | 
			
		||||
    if (auth?.value) {
 | 
			
		||||
      auth.remove();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
  })
 | 
			
		||||
  .get("/account", async ({ jwt, redirect, cookie: { auth } }) => {
 | 
			
		||||
    if (!auth?.value) {
 | 
			
		||||
      return redirect(`${WEBROOT}/`);
 | 
			
		||||
    }
 | 
			
		||||
    const user = await jwt.verify(auth.value);
 | 
			
		||||
 | 
			
		||||
    if (!user) {
 | 
			
		||||
      return redirect(`${WEBROOT}/`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const userData = db.query("SELECT * FROM users WHERE id = ?").as(User).get(user.id);
 | 
			
		||||
 | 
			
		||||
    if (!userData) {
 | 
			
		||||
      return redirect(`${WEBROOT}/`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <BaseHtml webroot={WEBROOT} title="ConvertX | Account">
 | 
			
		||||
        <>
 | 
			
		||||
          <Header
 | 
			
		||||
            webroot={WEBROOT}
 | 
			
		||||
            accountRegistration={ACCOUNT_REGISTRATION}
 | 
			
		||||
            allowUnauthenticated={ALLOW_UNAUTHENTICATED}
 | 
			
		||||
            hideHistory={HIDE_HISTORY}
 | 
			
		||||
            loggedIn
 | 
			
		||||
          />
 | 
			
		||||
          <main
 | 
			
		||||
            class={`
 | 
			
		||||
              w-full flex-1 px-2
 | 
			
		||||
              sm:px-4
 | 
			
		||||
            `}
 | 
			
		||||
          >
 | 
			
		||||
            <article class="article">
 | 
			
		||||
              <form method="post" class="flex flex-col gap-4">
 | 
			
		||||
                <fieldset class="mb-4 flex flex-col gap-4">
 | 
			
		||||
                  <label class="flex flex-col gap-1">
 | 
			
		||||
                    Email
 | 
			
		||||
                    <input
 | 
			
		||||
                      type="email"
 | 
			
		||||
                      name="email"
 | 
			
		||||
                      class="rounded-sm bg-neutral-800 p-3"
 | 
			
		||||
                      placeholder="Email"
 | 
			
		||||
                      autocomplete="email"
 | 
			
		||||
                      value={userData.email}
 | 
			
		||||
                      required
 | 
			
		||||
                    />
 | 
			
		||||
                  </label>
 | 
			
		||||
                  <label class="flex flex-col gap-1">
 | 
			
		||||
                    Password (leave blank for unchanged)
 | 
			
		||||
                    <input
 | 
			
		||||
                      type="password"
 | 
			
		||||
                      name="newPassword"
 | 
			
		||||
                      class="rounded-sm bg-neutral-800 p-3"
 | 
			
		||||
                      placeholder="Password"
 | 
			
		||||
                      autocomplete="new-password"
 | 
			
		||||
                    />
 | 
			
		||||
                  </label>
 | 
			
		||||
                  <label class="flex flex-col gap-1">
 | 
			
		||||
                    Current Password
 | 
			
		||||
                    <input
 | 
			
		||||
                      type="password"
 | 
			
		||||
                      name="password"
 | 
			
		||||
                      class="rounded-sm bg-neutral-800 p-3"
 | 
			
		||||
                      placeholder="Password"
 | 
			
		||||
                      autocomplete="current-password"
 | 
			
		||||
                      required
 | 
			
		||||
                    />
 | 
			
		||||
                  </label>
 | 
			
		||||
                </fieldset>
 | 
			
		||||
                <div role="group">
 | 
			
		||||
                  <input type="submit" value="Update" class="w-full btn-primary" />
 | 
			
		||||
                </div>
 | 
			
		||||
              </form>
 | 
			
		||||
            </article>
 | 
			
		||||
          </main>
 | 
			
		||||
        </>
 | 
			
		||||
      </BaseHtml>
 | 
			
		||||
    );
 | 
			
		||||
  })
 | 
			
		||||
  .post(
 | 
			
		||||
    "/account",
 | 
			
		||||
    async function handler({ body, set, redirect, jwt, cookie: { auth } }) {
 | 
			
		||||
      if (!auth?.value) {
 | 
			
		||||
        return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const user = await jwt.verify(auth.value);
 | 
			
		||||
      if (!user) {
 | 
			
		||||
        return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
      }
 | 
			
		||||
      const existingUser = db.query("SELECT * FROM users WHERE id = ?").as(User).get(user.id);
 | 
			
		||||
 | 
			
		||||
      if (!existingUser) {
 | 
			
		||||
        if (auth?.value) {
 | 
			
		||||
          auth.remove();
 | 
			
		||||
        }
 | 
			
		||||
        return redirect(`${WEBROOT}/login`, 302);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const validPassword = await Bun.password.verify(body.password, existingUser.password);
 | 
			
		||||
 | 
			
		||||
      if (!validPassword) {
 | 
			
		||||
        set.status = 403;
 | 
			
		||||
        return {
 | 
			
		||||
          message: "Invalid credentials.",
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const fields = [];
 | 
			
		||||
      const values = [];
 | 
			
		||||
 | 
			
		||||
      if (body.email) {
 | 
			
		||||
        const existingUser = await db
 | 
			
		||||
          .query("SELECT id FROM users WHERE email = ?")
 | 
			
		||||
          .as(User)
 | 
			
		||||
          .get(body.email);
 | 
			
		||||
        if (existingUser && existingUser.id.toString() !== user.id) {
 | 
			
		||||
          set.status = 409;
 | 
			
		||||
          return { message: "Email already in use." };
 | 
			
		||||
        }
 | 
			
		||||
        fields.push("email");
 | 
			
		||||
        values.push(body.email);
 | 
			
		||||
      }
 | 
			
		||||
      if (body.newPassword) {
 | 
			
		||||
        fields.push("password");
 | 
			
		||||
        values.push(await Bun.password.hash(body.newPassword));
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (fields.length > 0) {
 | 
			
		||||
        db.query(
 | 
			
		||||
          `UPDATE users SET ${fields.map((field) => `${field}=?`).join(", ")} WHERE id=?`,
 | 
			
		||||
        ).run(...values, user.id);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return redirect(`${WEBROOT}/`, 302);
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      body: t.Object({
 | 
			
		||||
        email: t.MaybeEmpty(t.String()),
 | 
			
		||||
        newPassword: t.MaybeEmpty(t.String()),
 | 
			
		||||
        password: t.String(),
 | 
			
		||||
      }),
 | 
			
		||||
    },
 | 
			
		||||
  );
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 8.1 KiB  | 
| 
		 Before Width: | Height: | Size: 23 KiB  | 
| 
		 Before Width: | Height: | Size: 7.3 KiB  | 
| 
		 Before Width: | Height: | Size: 476 B  | 
| 
		 Before Width: | Height: | Size: 960 B  | 
| 
		 Before Width: | Height: | Size: 15 KiB  | 
							
								
								
									
										4
									
								
								src/public/pico.lime.min.css
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1,194 +0,0 @@
 | 
			
		||||
// Select the file input element
 | 
			
		||||
const fileInput = document.querySelector('input[type="file"]');
 | 
			
		||||
const fileNames = [];
 | 
			
		||||
let fileType;
 | 
			
		||||
 | 
			
		||||
const selectContainer = document.querySelector("form .select_container");
 | 
			
		||||
 | 
			
		||||
const updateSearchBar = () => {
 | 
			
		||||
  const convertToInput = document.querySelector(
 | 
			
		||||
    "input[name='convert_to_search']",
 | 
			
		||||
  );
 | 
			
		||||
  const convertToPopup = document.querySelector(".convert_to_popup");
 | 
			
		||||
  const convertToGroupElements = document.querySelectorAll(".convert_to_group");
 | 
			
		||||
  const convertToGroups = {};
 | 
			
		||||
  const convertToElement = document.querySelector("select[name='convert_to']");
 | 
			
		||||
 | 
			
		||||
  const showMatching = (search) => {
 | 
			
		||||
    for (const [targets, groupElement] of Object.values(convertToGroups)) {
 | 
			
		||||
      let matchingTargetsFound = 0;
 | 
			
		||||
      for (const target of targets) {
 | 
			
		||||
        if (target.dataset.target.includes(search)) {
 | 
			
		||||
          matchingTargetsFound++;
 | 
			
		||||
          target.hidden = false;
 | 
			
		||||
        } else {
 | 
			
		||||
          target.hidden = true;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (matchingTargetsFound === 0) {
 | 
			
		||||
        groupElement.hidden = true;
 | 
			
		||||
      } else {
 | 
			
		||||
        groupElement.hidden = false;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  for (const groupElement of convertToGroupElements) {
 | 
			
		||||
    const groupName = groupElement.dataset.converter;
 | 
			
		||||
 | 
			
		||||
    const targetElements = groupElement.querySelectorAll(".target");
 | 
			
		||||
    const targets = Array.from(targetElements);
 | 
			
		||||
 | 
			
		||||
    for (const target of targets) {
 | 
			
		||||
      target.onmousedown = () => {
 | 
			
		||||
        convertToElement.value = target.dataset.value;
 | 
			
		||||
        convertToInput.value = `${target.dataset.target} using ${target.dataset.converter}`;
 | 
			
		||||
        showMatching("");
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    convertToGroups[groupName] = [targets, groupElement];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  convertToInput.addEventListener("input", (e) => {
 | 
			
		||||
    showMatching(e.target.value.toLowerCase());
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  convertToInput.addEventListener("blur", (e) => {
 | 
			
		||||
    // Keep the popup open even when clicking on a target button
 | 
			
		||||
    // for a split second to allow the click to go through
 | 
			
		||||
    if (e?.relatedTarget?.classList?.contains("target")) {
 | 
			
		||||
      convertToPopup.hidden = true;
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    convertToPopup.hidden = true;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  convertToInput.addEventListener("focus", () => {
 | 
			
		||||
    convertToPopup.hidden = false;
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// const convertFromSelect = document.querySelector("select[name='convert_from']");
 | 
			
		||||
 | 
			
		||||
// Add a 'change' event listener to the file input element
 | 
			
		||||
fileInput.addEventListener("change", (e) => {
 | 
			
		||||
  // console.log(e.target.files);
 | 
			
		||||
  // Get the selected files from the event target
 | 
			
		||||
  const files = e.target.files;
 | 
			
		||||
 | 
			
		||||
  // Select the file-list table
 | 
			
		||||
  const fileList = document.querySelector("#file-list");
 | 
			
		||||
 | 
			
		||||
  // Loop through the selected files
 | 
			
		||||
  for (const file of files) {
 | 
			
		||||
    // Create a new table row for each file
 | 
			
		||||
    const row = document.createElement("tr");
 | 
			
		||||
    row.innerHTML = `
 | 
			
		||||
      <td>${file.name}</td>
 | 
			
		||||
      <td>${(file.size / 1024).toFixed(2)} kB</td>
 | 
			
		||||
      <td><a class="secondary" onclick="deleteRow(this)" style="cursor: pointer">Remove</a></td>
 | 
			
		||||
    `;
 | 
			
		||||
 | 
			
		||||
    if (!fileType) {
 | 
			
		||||
      fileType = file.name.split(".").pop();
 | 
			
		||||
      fileInput.setAttribute("accept", `.${fileType}`);
 | 
			
		||||
      setTitle();
 | 
			
		||||
 | 
			
		||||
      // choose the option that matches the file type
 | 
			
		||||
      // for (const option of convertFromSelect.children) {
 | 
			
		||||
      //   console.log(option.value);
 | 
			
		||||
      //   if (option.value === fileType) {
 | 
			
		||||
      //     option.selected = true;
 | 
			
		||||
      //   }
 | 
			
		||||
      // }
 | 
			
		||||
 | 
			
		||||
      fetch("/conversions", {
 | 
			
		||||
        method: "POST",
 | 
			
		||||
        body: JSON.stringify({ fileType: fileType }),
 | 
			
		||||
        headers: {
 | 
			
		||||
          "Content-Type": "application/json",
 | 
			
		||||
        },
 | 
			
		||||
      })
 | 
			
		||||
        .then((res) => res.text())
 | 
			
		||||
        .then((html) => {
 | 
			
		||||
          selectContainer.innerHTML = html;
 | 
			
		||||
          updateSearchBar();
 | 
			
		||||
        })
 | 
			
		||||
        .catch((err) => console.log(err));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Append the row to the file-list table
 | 
			
		||||
    fileList.appendChild(row);
 | 
			
		||||
 | 
			
		||||
    // Append the file to the hidden input
 | 
			
		||||
    fileNames.push(file.name);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  uploadFiles(files);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const setTitle = () => {
 | 
			
		||||
  const title = document.querySelector("h1");
 | 
			
		||||
  title.textContent = `Convert ${fileType ? `.${fileType}` : ""}`;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Add a onclick for the delete button
 | 
			
		||||
const deleteRow = (target) => {
 | 
			
		||||
  const filename = target.parentElement.parentElement.children[0].textContent;
 | 
			
		||||
  const row = target.parentElement.parentElement;
 | 
			
		||||
  row.remove();
 | 
			
		||||
 | 
			
		||||
  // remove from fileNames
 | 
			
		||||
  const index = fileNames.indexOf(filename);
 | 
			
		||||
  fileNames.splice(index, 1);
 | 
			
		||||
 | 
			
		||||
  // if fileNames is empty, reset fileType
 | 
			
		||||
  if (fileNames.length === 0) {
 | 
			
		||||
    fileType = null;
 | 
			
		||||
    fileInput.removeAttribute("accept");
 | 
			
		||||
    setTitle();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  fetch("/delete", {
 | 
			
		||||
    method: "POST",
 | 
			
		||||
    body: JSON.stringify({ filename: filename }),
 | 
			
		||||
    headers: {
 | 
			
		||||
      "Content-Type": "application/json",
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
    .then((res) => res.json())
 | 
			
		||||
    .then((data) => {
 | 
			
		||||
      console.log(data);
 | 
			
		||||
    })
 | 
			
		||||
    .catch((err) => console.log(err));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const uploadFiles = (files) => {
 | 
			
		||||
  const formData = new FormData();
 | 
			
		||||
 | 
			
		||||
  for (const file of files) {
 | 
			
		||||
    formData.append("file", file, file.name);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  fetch("/upload", {
 | 
			
		||||
    method: "POST",
 | 
			
		||||
    body: formData,
 | 
			
		||||
  })
 | 
			
		||||
    .then((res) => res.json())
 | 
			
		||||
    .then((data) => {
 | 
			
		||||
      console.log(data);
 | 
			
		||||
    })
 | 
			
		||||
    .catch((err) => console.log(err));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const formConvert = document.querySelector("form[action='/convert']");
 | 
			
		||||
 | 
			
		||||
formConvert.addEventListener("submit", (e) => {
 | 
			
		||||
  const hiddenInput = document.querySelector("input[name='file_names']");
 | 
			
		||||
  hiddenInput.value = JSON.stringify(fileNames);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
updateSearchBar();
 | 
			
		||||
@@ -1,59 +0,0 @@
 | 
			
		||||
div.icon {
 | 
			
		||||
  height: 100px;
 | 
			
		||||
  width: 100px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
button[type="submit"] {
 | 
			
		||||
  width: 50%
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
div.center {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (max-width: 99999999999px) {
 | 
			
		||||
  .convert_to_popup {
 | 
			
		||||
    width: 50vw !important;
 | 
			
		||||
    height: 50vh;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (max-width: 850px) {
 | 
			
		||||
  .convert_to_popup {
 | 
			
		||||
    width: 60vw !important;
 | 
			
		||||
    height: 60vh;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (max-width: 575px) {
 | 
			
		||||
  .convert_to_popup {
 | 
			
		||||
    width: 80vw !important;
 | 
			
		||||
    height: 75vh;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (max-height: 1000px) {
 | 
			
		||||
  .convert_to_popup {
 | 
			
		||||
    height: 40vh;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@media (max-height: 650px) {
 | 
			
		||||
  .convert_to_popup {
 | 
			
		||||
    height: 30vh;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (max-height: 500px) {
 | 
			
		||||
  .convert_to_popup {
 | 
			
		||||
    height: 25vh;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (max-height: 400px) {
 | 
			
		||||
  .convert_to_popup {
 | 
			
		||||
    height: 15vh;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										47
									
								
								src/theme/theme.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,47 @@
 | 
			
		||||
:root {
 | 
			
		||||
  /* Light mode */
 | 
			
		||||
  --contrast: oklch(100% 0 0);
 | 
			
		||||
  /* Neutral colors - Gray */
 | 
			
		||||
  --neutral-950: oklch(98.5% 0.002 247.839);
 | 
			
		||||
  --neutral-900: oklch(96.7% 0.003 264.542);
 | 
			
		||||
  --neutral-800: oklch(92.8% 0.006 264.531);
 | 
			
		||||
  --neutral-700: oklch(87.2% 0.01 258.338);
 | 
			
		||||
  --neutral-600: oklch(70.7% 0.022 261.325);
 | 
			
		||||
  --neutral-500: oklch(55.1% 0.027 264.364);
 | 
			
		||||
  --neutral-400: oklch(44.6% 0.03 256.802);
 | 
			
		||||
  --neutral-300: oklch(37.3% 0.034 259.733);
 | 
			
		||||
  --neutral-200: oklch(26.9% 0 0);
 | 
			
		||||
  --neutral-100: oklch(21% 0.034 264.665);
 | 
			
		||||
  --neutral-50: oklch(13% 0.028 261.692);
 | 
			
		||||
  /* lime-700 */
 | 
			
		||||
  --accent-600: oklch(53.2% 0.157 131.589);
 | 
			
		||||
  /* lime-600 */
 | 
			
		||||
  --accent-500: oklch(64.8% 0.2 131.684);
 | 
			
		||||
  /* lime-500 */
 | 
			
		||||
  --accent-400: oklch(76.8% 0.233 130.85);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (prefers-color-scheme: dark) {
 | 
			
		||||
  /* Dark mode */
 | 
			
		||||
  :root {
 | 
			
		||||
    --contrast: oklch(0% 0 0);
 | 
			
		||||
    /* Neutral colors - Gray */
 | 
			
		||||
    --neutral-950: oklch(13% 0.028 261.692);
 | 
			
		||||
    --neutral-900: oklch(21% 0.034 264.665);
 | 
			
		||||
    --neutral-800: oklch(27.8% 0.033 256.848);
 | 
			
		||||
    --neutral-700: oklch(37.3% 0.034 259.733);
 | 
			
		||||
    --neutral-600: oklch(44.6% 0.03 256.802);
 | 
			
		||||
    --neutral-500: oklch(55.1% 0.027 264.364);
 | 
			
		||||
    --neutral-400: oklch(70.7% 0.022 261.325);
 | 
			
		||||
    --neutral-300: oklch(87.2% 0.01 258.338);
 | 
			
		||||
    --neutral-200: oklch(92.8% 0.006 264.531);
 | 
			
		||||
    --neutral-100: oklch(96.7% 0.003 264.542);
 | 
			
		||||
    --neutral-50: oklch(98.5% 0.002 247.839);
 | 
			
		||||
    /* lime-600 */
 | 
			
		||||
    --accent-600: oklch(64.8% 0.2 131.684);
 | 
			
		||||
    /* lime-500 */
 | 
			
		||||
    --accent-500: oklch(76.8% 0.233 130.85);
 | 
			
		||||
    /* lime-400 */
 | 
			
		||||
    --accent-400: oklch(84.1% 0.238 128.85);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								tests/converters/assimp.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,7 @@
 | 
			
		||||
import { test } from "bun:test";
 | 
			
		||||
import { convert } from "../../src/converters/assimp";
 | 
			
		||||
import { runCommonTests } from "./helpers/commonTests";
 | 
			
		||||
 | 
			
		||||
runCommonTests(convert);
 | 
			
		||||
 | 
			
		||||
test.skip("dummy - required to trigger test detection", () => {});
 | 
			
		||||
							
								
								
									
										7
									
								
								tests/converters/calibre.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,7 @@
 | 
			
		||||
import { test } from "bun:test";
 | 
			
		||||
import { convert } from "../../src/converters/calibre";
 | 
			
		||||
import { runCommonTests } from "./helpers/commonTests";
 | 
			
		||||
 | 
			
		||||
runCommonTests(convert);
 | 
			
		||||
 | 
			
		||||
test.skip("dummy - required to trigger test detection", () => {});
 | 
			
		||||
							
								
								
									
										91
									
								
								tests/converters/dvisvgm.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,91 @@
 | 
			
		||||
import type { ExecFileException } from "node:child_process";
 | 
			
		||||
import { beforeEach, expect, test } from "bun:test";
 | 
			
		||||
import { convert } from "../../src/converters/dvisvgm";
 | 
			
		||||
import { ExecFileFn } from "../../src/converters/types";
 | 
			
		||||
import { runCommonTests } from "./helpers/commonTests";
 | 
			
		||||
 | 
			
		||||
let calls: string[][] = [];
 | 
			
		||||
 | 
			
		||||
beforeEach(() => {
 | 
			
		||||
  calls = [];
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
runCommonTests(convert);
 | 
			
		||||
 | 
			
		||||
test("convert respects eps filetype", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const mockExecFile: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    calls.push(_args);
 | 
			
		||||
    callback(null, "Fake stdout", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const result = await convert("input.eps", "eps", "stl", "output.stl", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(result).toBe("Done");
 | 
			
		||||
  expect(calls[0]).toEqual(expect.arrayContaining(["--eps", "input.eps", "output.stl"]));
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("convert respects pdf filetype", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const mockExecFile: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    calls.push(_args);
 | 
			
		||||
    callback(null, "Fake stdout", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const result = await convert("input.pdf", "pdf", "stl", "output.stl", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(result).toBe("Done");
 | 
			
		||||
  expect(calls[0]).toEqual(expect.arrayContaining(["--pdf", "input.pdf", "output.stl"]));
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("convert respects svgz conversion target type", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const mockExecFile: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    calls.push(_args);
 | 
			
		||||
    callback(null, "Fake stdout", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const result = await convert("input.obj", "eps", "svgz", "output.svgz", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(result).toBe("Done");
 | 
			
		||||
  expect(calls[0]).toEqual(expect.arrayContaining(["-z", "input.obj", "output.svgz"]));
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										181
									
								
								tests/converters/ffmpeg.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,181 @@
 | 
			
		||||
import { beforeEach, expect, test } from "bun:test";
 | 
			
		||||
import { convert } from "../../src/converters/ffmpeg";
 | 
			
		||||
 | 
			
		||||
let calls: string[][] = [];
 | 
			
		||||
 | 
			
		||||
function mockExecFile(
 | 
			
		||||
  _cmd: string,
 | 
			
		||||
  args: string[],
 | 
			
		||||
  callback: (err: Error | null, stdout: string, stderr: string) => void,
 | 
			
		||||
) {
 | 
			
		||||
  calls.push(args);
 | 
			
		||||
  if (args.includes("fail.mov")) {
 | 
			
		||||
    callback(new Error("mock failure"), "", "Fake stderr: fail");
 | 
			
		||||
  } else {
 | 
			
		||||
    callback(null, "Fake stdout", "");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
beforeEach(() => {
 | 
			
		||||
  calls = [];
 | 
			
		||||
  delete process.env.FFMPEG_ARGS;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("converts a normal file", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const result = await convert("in.mp4", "mp4", "avi", "out.avi", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(result).toBe("Done");
 | 
			
		||||
  expect(calls[0]).toEqual(expect.arrayContaining(["-i", "in.mp4", "out.avi"]));
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("adds resize for ico output", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const result = await convert("in.png", "png", "ico", "out.ico", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(result).toBe("Done: resized to 256x256");
 | 
			
		||||
  expect(calls[0]).toEqual(
 | 
			
		||||
    expect.arrayContaining(["-filter:v", expect.stringContaining("scale=")]),
 | 
			
		||||
  );
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("uses libaom-av1 for av1.mp4", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  await convert("in.mkv", "mkv", "av1.mp4", "out.mp4", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(calls[0]).toEqual(expect.arrayContaining(["-c:v", "libaom-av1"]));
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("uses libx264 for h264.mp4", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  await convert("in.mkv", "mkv", "h264.mp4", "out.mp4", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(calls[0]).toEqual(expect.arrayContaining(["-c:v", "libx264"]));
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("uses libx265 for h265.mp4", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  await convert("in.mkv", "mkv", "h265.mp4", "out.mp4", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(calls[0]).toEqual(expect.arrayContaining(["-c:v", "libx265"]));
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("uses libx266 for h266.mp4", async () => {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  await convert("in.mkv", "mkv", "h266.mp4", "out.mp4", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(calls[0]).toEqual(expect.arrayContaining(["-c:v", "libx266"]));
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("respects FFMPEG_ARGS", async () => {
 | 
			
		||||
  process.env.FFMPEG_ARGS = "-hide_banner -y";
 | 
			
		||||
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  await convert("input.mov", "mov", "mp4", "output.mp4", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(calls[0]?.slice(0, 2)).toEqual(["-hide_banner", "-y"]);
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("fails on exec error", async () => {
 | 
			
		||||
  const originalConsoleError = console.error;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.error = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  expect(convert("fail.mov", "mov", "mp4", "output.mp4", undefined, mockExecFile)).rejects.toThrow(
 | 
			
		||||
    "mock failure",
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  console.error = originalConsoleError;
 | 
			
		||||
 | 
			
		||||
  expect(loggedMessage).toBe("stderr: Fake stderr: fail");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("logs stderr when execFile returns only stderr and no error", async () => {
 | 
			
		||||
  const originalConsoleError = console.error;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.error = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Mock execFile to call back with no error, no stdout, but with stderr
 | 
			
		||||
  const mockExecFileStderrOnly = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: Error | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    callback(null, "", "Only stderr output");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  await convert("input.mov", "mov", "mp4", "output.mp4", undefined, mockExecFileStderrOnly);
 | 
			
		||||
 | 
			
		||||
  console.error = originalConsoleError;
 | 
			
		||||
 | 
			
		||||
  expect(loggedMessage).toBe("stderr: Only stderr output");
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										7
									
								
								tests/converters/graphicsmagick.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,7 @@
 | 
			
		||||
import { test } from "bun:test";
 | 
			
		||||
import { convert } from "../../src/converters/graphicsmagick";
 | 
			
		||||
import { runCommonTests } from "./helpers/commonTests";
 | 
			
		||||
 | 
			
		||||
runCommonTests(convert);
 | 
			
		||||
 | 
			
		||||
test.skip("dummy - required to trigger test detection", () => {});
 | 
			
		||||
							
								
								
									
										26
									
								
								tests/converters/helpers/commonTests.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,26 @@
 | 
			
		||||
import { test } from "bun:test";
 | 
			
		||||
import { ConvertFnWithExecFile } from "../../../src/converters/types";
 | 
			
		||||
import {
 | 
			
		||||
  runConvertFailTest,
 | 
			
		||||
  runConvertLogsStderror,
 | 
			
		||||
  runConvertLogsStderrorAndStdout,
 | 
			
		||||
  runConvertSuccessTest,
 | 
			
		||||
} from "./converters";
 | 
			
		||||
 | 
			
		||||
export function runCommonTests(convert: ConvertFnWithExecFile) {
 | 
			
		||||
  test("convert resolves when execFile succeeds", async () => {
 | 
			
		||||
    await runConvertSuccessTest(convert);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test("convert rejects when execFile fails", async () => {
 | 
			
		||||
    await runConvertFailTest(convert);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test("convert logs stderr when present", async () => {
 | 
			
		||||
    await runConvertLogsStderror(convert);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test("convert logs both stderr and stdout when present", async () => {
 | 
			
		||||
    await runConvertLogsStderrorAndStdout(convert);
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										121
									
								
								tests/converters/helpers/converters.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,121 @@
 | 
			
		||||
import type { ExecFileException } from "node:child_process";
 | 
			
		||||
import { expect } from "bun:test";
 | 
			
		||||
import { ConvertFnWithExecFile, ExecFileFn } from "../../../src/converters/types";
 | 
			
		||||
 | 
			
		||||
export async function runConvertSuccessTest(convertFn: ConvertFnWithExecFile) {
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const mockExecFile: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    callback(null, "Fake stdout", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const result = await convertFn("input.obj", "obj", "stl", "output.stl", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(result).toBe("Done");
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function runConvertFailTest(convertFn: ConvertFnWithExecFile) {
 | 
			
		||||
  const mockExecFile: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    callback(new Error("Test error"), "", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  expect(
 | 
			
		||||
    convertFn("input.obj", "obj", "stl", "output.stl", undefined, mockExecFile),
 | 
			
		||||
  ).rejects.toMatch(/error: Error: Test error/);
 | 
			
		||||
 | 
			
		||||
  // Test with error object lacking 'message' property
 | 
			
		||||
  const mockExecFileNoMessage: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    // Simulate a non-standard error object
 | 
			
		||||
    callback({ notMessage: true } as unknown as ExecFileException, "", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  expect(
 | 
			
		||||
    convertFn("input.obj", "obj", "stl", "output.stl", undefined, mockExecFileNoMessage),
 | 
			
		||||
  ).rejects.toMatch(/error:/i);
 | 
			
		||||
 | 
			
		||||
  // Test with a non-object error (e.g., a string)
 | 
			
		||||
  const mockExecFileStringError: ExecFileFn = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    callback("string error" as unknown as ExecFileException, "", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  expect(
 | 
			
		||||
    convertFn("input.obj", "obj", "stl", "output.stl", undefined, mockExecFileStringError),
 | 
			
		||||
  ).rejects.toMatch(/error:/i);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function runConvertLogsStderror(convertFn: ConvertFnWithExecFile) {
 | 
			
		||||
  const originalConsoleError = console.error;
 | 
			
		||||
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.error = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const mockExecFile = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: Error | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    callback(null, "", "Fake stderr");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  await convertFn("file.obj", "obj", "stl", "out.stl", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.error = originalConsoleError;
 | 
			
		||||
 | 
			
		||||
  expect(loggedMessage).toBe("stderr: Fake stderr");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function runConvertLogsStderrorAndStdout(convertFn: ConvertFnWithExecFile) {
 | 
			
		||||
  const originalConsoleError = console.error;
 | 
			
		||||
  const originalConsoleLog = console.log;
 | 
			
		||||
 | 
			
		||||
  let loggedError = "";
 | 
			
		||||
  let loggedMessage = "";
 | 
			
		||||
  console.error = (msg) => {
 | 
			
		||||
    loggedError = msg;
 | 
			
		||||
  };
 | 
			
		||||
  console.log = (msg) => {
 | 
			
		||||
    loggedMessage = msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const mockExecFile = (
 | 
			
		||||
    _cmd: string,
 | 
			
		||||
    _args: string[],
 | 
			
		||||
    callback: (err: Error | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  ) => {
 | 
			
		||||
    callback(null, "Fake stdout", "Fake stderr");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  await convertFn("file.obj", "obj", "stl", "out.stl", undefined, mockExecFile);
 | 
			
		||||
 | 
			
		||||
  console.error = originalConsoleError;
 | 
			
		||||
  console.log = originalConsoleLog;
 | 
			
		||||
 | 
			
		||||
  expect(loggedError).toBe("stderr: Fake stderr");
 | 
			
		||||
  expect(loggedMessage).toBe("stdout: Fake stdout");
 | 
			
		||||
}
 | 
			
		||||