Compare commits
	
		
			627 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					9a8d7a36b9 | ||
| 
						 | 
					80b55657a4 | ||
| 
						 | 
					32d1c567b9 | ||
| 
						 | 
					152bbd1b78 | ||
| 
						 | 
					4d65cc7228 | ||
| 
						 | 
					1a4e2dec7c | ||
| 
						 | 
					061e63e705 | ||
| 
						 | 
					80142ff269 | ||
| 
						 | 
					8b015f9045 | ||
| 
						 | 
					0159ac0481 | ||
| 
						 | 
					a677a5eec1 | ||
| 
						 | 
					0ac585c9da | ||
| 
						 | 
					4f61577421 | ||
| 
						 | 
					3e7e95b53c | ||
| 
						 | 
					27e9b7eb35 | ||
| 
						 | 
					add3a6229e | ||
| 
						 | 
					2e406cac3d | ||
| 
						 | 
					6fa6978f30 | ||
| 
						 | 
					7691594b10 | ||
| 
						 | 
					17325a6e6d | ||
| 
						 | 
					d984891791 | ||
| 
						 | 
					78bac9c9ca | ||
| 
						 | 
					126e60fec7 | ||
| 
						 | 
					7a59070e91 | ||
| 
						 | 
					c9b65c7652 | ||
| 
						 | 
					13d9ce09a4 | ||
| 
						 | 
					f4fc053db3 | ||
| 
						 | 
					d3fc6a237f | ||
| 
						 | 
					8a888ccda6 | ||
| 
						 | 
					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 | ||
| 
						 | 
					905adc5e1c | ||
| 
						 | 
					52ed7274e9 | ||
| 
						 | 
					a29238c265 | ||
| 
						 | 
					48c6fb79fc | ||
| 
						 | 
					8358396656 | ||
| 
						 | 
					b30e5800c3 | ||
| 
						 | 
					21a1b50ed8 | ||
| 
						 | 
					e6a94fb21d | ||
| 
						 | 
					bef1710e33 | ||
| 
						 | 
					16b322d4e6 | ||
| 
						 | 
					9bf64e42d5 | ||
| 
						 | 
					5988fe8212 | ||
| 
						 | 
					5df9c0b751 | ||
| 
						 | 
					136a8b2d74 | ||
| 
						 | 
					ccfb574d5d | ||
| 
						 | 
					ad6eedea69 | ||
| 
						 | 
					c3082db8f7 | ||
| 
						 | 
					a1f8cbae66 | ||
| 
						 | 
					bb34bdee87 | ||
| 
						 | 
					11fcbc3f96 | ||
| 
						 | 
					f7344e4c65 | ||
| 
						 | 
					781310f3dc | ||
| 
						 | 
					3f063644f2 | ||
| 
						 | 
					081634b610 | ||
| 
						 | 
					cf3da08c73 | ||
| 
						 | 
					5f7234d6c1 | ||
| 
						 | 
					6597c1d7ca | ||
| 
						 | 
					ecb2c75008 | ||
| 
						 | 
					d5eeef9f68 | ||
| 
						 | 
					7456174022 | ||
| 
						 | 
					bc4ad49285 | ||
| 
						 | 
					f0d0e43929 | ||
| 
						 | 
					8ca4f1587d | ||
| 
						 | 
					1535377bfe | ||
| 
						 | 
					83bf78fd57 | ||
| 
						 | 
					4d9c4d64aa | ||
| 
						 | 
					53fff594fc | ||
| 
						 | 
					fe4aeaff03 | ||
| 
						 | 
					2078cb0ee0 | ||
| 
						 | 
					86a61d35d7 | ||
| 
						 | 
					96fa7e2f55 | ||
| 
						 | 
					7d2af46b0b | ||
| 
						 | 
					57e2999866 | ||
| 
						 | 
					6fb8ca4d82 | ||
| 
						 | 
					c295e546bd | ||
| 
						 | 
					f7abb9389c | ||
| 
						 | 
					d7de154eda | ||
| 
						 | 
					20bd111765 | ||
| 
						 | 
					eadd0da291 | ||
| 
						 | 
					52294465fb | ||
| 
						 | 
					049e9163ce | ||
| 
						 | 
					d466d2dbbc | ||
| 
						 | 
					3f79ccaa2a | ||
| 
						 | 
					1e9bde18c7 | ||
| 
						 | 
					9af23346bf | ||
| 
						 | 
					d310341fca | ||
| 
						 | 
					d88a755c13 | ||
| 
						 | 
					7c6085c685 | ||
| 
						 | 
					7ed1ad21f2 | ||
| 
						 | 
					8a2237fbd9 | ||
| 
						 | 
					0e363f0731 | ||
| 
						 | 
					4074647b67 | ||
| 
						 | 
					c84968be50 | ||
| 
						 | 
					0e53a99d43 | ||
| 
						 | 
					bdd0cf556f | ||
| 
						 | 
					2483274388 | ||
| 
						 | 
					4c5129910a | ||
| 
						 | 
					fe13a1b736 | ||
| 
						 | 
					f1ac71b397 | ||
| 
						 | 
					1b1067a03f | ||
| 
						 | 
					8674557e42 | ||
| 
						 | 
					87052ce105 | ||
| 
						 | 
					98ee26f6e2 | ||
| 
						 | 
					96e2c88465 | ||
| 
						 | 
					d55ba218ff | ||
| 
						 | 
					ae2455e73e | ||
| 
						 | 
					b9fe32053c | ||
| 
						 | 
					5cf3d74e03 | ||
| 
						 | 
					2b92778f37 | ||
| 
						 | 
					27d4da8941 | ||
| 
						 | 
					2384e22c22 | ||
| 
						 | 
					6690caeb1e | ||
| 
						 | 
					c714ade3e2 | ||
| 
						 | 
					e9e95c61e9 | ||
| 
						 | 
					b1e0e68d9c | ||
| 
						 | 
					5ce3706550 | ||
| 
						 | 
					57e47e95c0 | ||
| 
						 | 
					6d6bc6cfdd | ||
| 
						 | 
					b44eb22e77 | ||
| 
						 | 
					6edfbaa27d | ||
| 
						 | 
					d669baeff4 | ||
| 
						 | 
					ec1a7bc015 | ||
| 
						 | 
					0805241a19 | ||
| 
						 | 
					83f041daa2 | ||
| 
						 | 
					55331a4496 | ||
| 
						 | 
					b53f07e7a7 | ||
| 
						 | 
					0eb89ae712 | ||
| 
						 | 
					7dd153b02c | ||
| 
						 | 
					6ccafeb3b0 | ||
| 
						 | 
					b703903b22 | ||
| 
						 | 
					9e66eab0a2 | ||
| 
						 | 
					b272bf9504 | ||
| 
						 | 
					56632f3500 | ||
| 
						 | 
					2d9d8f8b4f | ||
| 
						 | 
					65d4e0fbbe | ||
| 
						 | 
					8182d12ea0 | ||
| 
						 | 
					1c241d4cad | ||
| 
						 | 
					874ff6ee00 | ||
| 
						 | 
					e9f1219ad9 | ||
| 
						 | 
					4811452aec | ||
| 
						 | 
					382ebad35a | ||
| 
						 | 
					85945256e7 | ||
| 
						 | 
					c504692569 | ||
| 
						 | 
					64a16036be | ||
| 
						 | 
					b9f038386f | ||
| 
						 | 
					945775e52b | ||
| 
						 | 
					e7f3466736 | ||
| 
						 | 
					ee80eeb18d | ||
| 
						 | 
					34c7e0bd25 | ||
| 
						 | 
					492dbd5617 | ||
| 
						 | 
					0935bf66ce | ||
| 
						 | 
					7389e0a059 | ||
| 
						 | 
					c512b45f91 | ||
| 
						 | 
					3ae2db5d9b | ||
| 
						 | 
					0945b40a9c | ||
| 
						 | 
					20b958e547 | ||
| 
						 | 
					e7e146c6c9 | ||
| 
						 | 
					005ad2d66b | ||
| 
						 | 
					e5c3a8acc4 | ||
| 
						 | 
					87ecbabd1f | ||
| 
						 | 
					991c4e4ba8 | ||
| 
						 | 
					87ccd8b44c | ||
| 
						 | 
					83e6699ca6 | ||
| 
						 | 
					c91523c038 | ||
| 
						 | 
					1f73f036b2 | ||
| 
						 | 
					1223fabfca | ||
| 
						 | 
					8a42a39e69 | ||
| 
						 | 
					22023bad25 | ||
| 
						 | 
					db2f2d8f0a | ||
| 
						 | 
					d0fa9ac408 | ||
| 
						 | 
					776a97289b | ||
| 
						 | 
					95340dd0eb | ||
| 
						 | 
					7dcd74cc5f | ||
| 
						 | 
					c5efac9423 | ||
| 
						 | 
					cceca9a924 | ||
| 
						 | 
					4d4c13a8d8 | ||
| 
						 | 
					f8f90ebd69 | ||
| 
						 | 
					648d5070e2 | ||
| 
						 | 
					801cf28d1e | ||
| 
						 | 
					fae2ba9c54 | ||
| 
						 | 
					10d20a8786 | ||
| 
						 | 
					c9bc1e237e | ||
| 
						 | 
					48a76a46b3 | ||
| 
						 | 
					4dcb796e1b | ||
| 
						 | 
					5952103bcd | ||
| 
						 | 
					fd05ee5cd5 | ||
| 
						 | 
					7e7d238c7a | ||
| 
						 | 
					755a4170f2 | ||
| 
						 | 
					263ba415f5 | ||
| 
						 | 
					317260098a | ||
| 
						 | 
					5304e94b4e | ||
| 
						 | 
					aab2b311cf | ||
| 
						 | 
					baa7ea40e6 | ||
| 
						 | 
					481a11b610 | ||
| 
						 | 
					c09fe296b1 | ||
| 
						 | 
					f023aae753 | ||
| 
						 | 
					5cd9544b55 | ||
| 
						 | 
					97c23ba65c | ||
| 
						 | 
					0ffda40ac8 | ||
| 
						 | 
					cb639907ee | ||
| 
						 | 
					0166842b78 | 
							
								
								
									
										1
									
								
								.bun-version
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
			
		||||
1.2.2
 | 
			
		||||
							
								
								
									
										69
									
								
								.devcontainer/Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,69 @@
 | 
			
		||||
FROM debian:trixie-slim
 | 
			
		||||
 | 
			
		||||
WORKDIR /app
 | 
			
		||||
 | 
			
		||||
RUN apt-get update && apt-get install -y \
 | 
			
		||||
  curl \
 | 
			
		||||
  unzip \
 | 
			
		||||
  git \
 | 
			
		||||
  ca-certificates \
 | 
			
		||||
  build-essential \
 | 
			
		||||
  assimp-utils \
 | 
			
		||||
  calibre \
 | 
			
		||||
  dasel \
 | 
			
		||||
  dcraw \
 | 
			
		||||
  dvisvgm \
 | 
			
		||||
  ffmpeg \
 | 
			
		||||
  ghostscript \
 | 
			
		||||
  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/*
 | 
			
		||||
 | 
			
		||||
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 && \
 | 
			
		||||
  unzip -j bun-linux-*.zip -d /usr/local/bin && \
 | 
			
		||||
  rm bun-linux-*.zip && \
 | 
			
		||||
  chmod +x /usr/local/bin/bun
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
RUN mkdir -p data
 | 
			
		||||
ENV NODE_ENV=development
 | 
			
		||||
ENV QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox"
 | 
			
		||||
EXPOSE 3000
 | 
			
		||||
CMD ["bun", "run", "dev"]
 | 
			
		||||
							
								
								
									
										53
									
								
								.devcontainer/devcontainer.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,53 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "ConvertX",
 | 
			
		||||
  "build": {
 | 
			
		||||
    "dockerfile": "Dockerfile"
 | 
			
		||||
  },
 | 
			
		||||
  "features": {
 | 
			
		||||
    "ghcr.io/devcontainers/features/git:1": {},
 | 
			
		||||
    "ghcr.io/devcontainers/features/github-cli:1": {}
 | 
			
		||||
  },
 | 
			
		||||
  "customizations": {
 | 
			
		||||
    "vscode": {
 | 
			
		||||
      "extensions": [
 | 
			
		||||
        "ms-vscode.vscode-typescript-next",
 | 
			
		||||
        "bradlc.vscode-tailwindcss",
 | 
			
		||||
        "esbenp.prettier-vscode",
 | 
			
		||||
        "dbaeumer.vscode-eslint",
 | 
			
		||||
        "ms-vscode.vscode-json",
 | 
			
		||||
        "ms-vscode.vscode-docker",
 | 
			
		||||
        "oven.bun-vscode"
 | 
			
		||||
      ],
 | 
			
		||||
      "settings": {
 | 
			
		||||
        "editor.defaultFormatter": "esbenp.prettier-vscode",
 | 
			
		||||
        "editor.formatOnSave": true,
 | 
			
		||||
        "editor.codeActionsOnSave": {
 | 
			
		||||
          "source.fixAll.eslint": "explicit"
 | 
			
		||||
        },
 | 
			
		||||
        "typescript.preferences.importModuleSpecifier": "relative",
 | 
			
		||||
        "typescript.suggest.autoImports": true,
 | 
			
		||||
        "tailwindCSS.includeLanguages": {
 | 
			
		||||
          "typescript": "javascript",
 | 
			
		||||
          "typescriptreact": "javascript"
 | 
			
		||||
        },
 | 
			
		||||
        "files.associations": {
 | 
			
		||||
          "*.css": "tailwindcss"
 | 
			
		||||
        },
 | 
			
		||||
        "terminal.integrated.defaultProfile.linux": "bash"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "forwardPorts": [3000],
 | 
			
		||||
  "portsAttributes": {
 | 
			
		||||
    "3000": {
 | 
			
		||||
      "label": "ConvertX Application",
 | 
			
		||||
      "onAutoForward": "notify"
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "postCreateCommand": "bun install",
 | 
			
		||||
  "remoteUser": "root",
 | 
			
		||||
  "mounts": ["source=${localWorkspaceFolder}/data,target=/app/data,type=bind"],
 | 
			
		||||
  "containerEnv": {
 | 
			
		||||
    "JWT_SECRET": "jwt_secret_only_used_in_testing_for_easier_hot_reloading"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,16 +1,25 @@
 | 
			
		||||
node_modules
 | 
			
		||||
Dockerfile*
 | 
			
		||||
docker-compose*
 | 
			
		||||
.dockerignore
 | 
			
		||||
.editorconfig
 | 
			
		||||
.env
 | 
			
		||||
.git
 | 
			
		||||
.gitignore
 | 
			
		||||
README.md
 | 
			
		||||
LICENSE
 | 
			
		||||
.vscode
 | 
			
		||||
Makefile
 | 
			
		||||
helm-charts
 | 
			
		||||
.env
 | 
			
		||||
.editorconfig
 | 
			
		||||
.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
 | 
			
		||||
 
 | 
			
		||||
@@ -1,55 +0,0 @@
 | 
			
		||||
/** @type {import("eslint").Linter.Config} */
 | 
			
		||||
const config = {
 | 
			
		||||
  root: true,
 | 
			
		||||
  parser: "@typescript-eslint/parser",
 | 
			
		||||
  plugins: ["isaacscript", "import"],
 | 
			
		||||
  extends: [
 | 
			
		||||
    "plugin:@typescript-eslint/recommended-type-checked",
 | 
			
		||||
    "plugin:@typescript-eslint/stylistic-type-checked",
 | 
			
		||||
    "plugin:prettier/recommended",
 | 
			
		||||
  ],
 | 
			
		||||
  parserOptions: {
 | 
			
		||||
    ecmaVersion: "latest",
 | 
			
		||||
    sourceType: "module",
 | 
			
		||||
    tsconfigRootDir: __dirname,
 | 
			
		||||
    project: [
 | 
			
		||||
      "./tsconfig.json",
 | 
			
		||||
      "./cli/tsconfig.eslint.json", // separate eslint config for the CLI since we want to lint and typecheck differently due to template files
 | 
			
		||||
      "./upgrade/tsconfig.json",
 | 
			
		||||
      "./www/tsconfig.json",
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  overrides: [
 | 
			
		||||
    // Template files don't have reliable type information
 | 
			
		||||
    {
 | 
			
		||||
      files: ["./cli/template/**/*.{ts,tsx}"],
 | 
			
		||||
      extends: ["plugin:@typescript-eslint/disable-type-checked"],
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
  rules: {
 | 
			
		||||
    // These off/not-configured-the-way-we-want lint rules we like & opt into
 | 
			
		||||
    "@typescript-eslint/no-explicit-any": "error",
 | 
			
		||||
    "@typescript-eslint/no-unused-vars": [
 | 
			
		||||
      "error",
 | 
			
		||||
      { argsIgnorePattern: "^_", destructuredArrayIgnorePattern: "^_" },
 | 
			
		||||
    ],
 | 
			
		||||
    "@typescript-eslint/consistent-type-imports": [
 | 
			
		||||
      "error",
 | 
			
		||||
      { prefer: "type-imports", fixStyle: "inline-type-imports" },
 | 
			
		||||
    ],
 | 
			
		||||
    "import/consistent-type-specifier-style": ["error", "prefer-inline"],
 | 
			
		||||
 | 
			
		||||
    // For educational purposes we format our comments/jsdoc nicely
 | 
			
		||||
    "isaacscript/complete-sentences-jsdoc": "warn",
 | 
			
		||||
    "isaacscript/format-jsdoc-comments": "warn",
 | 
			
		||||
 | 
			
		||||
    // These lint rules don't make sense for us but are enabled in the preset configs
 | 
			
		||||
    "@typescript-eslint/no-confusing-void-expression": "off",
 | 
			
		||||
    "@typescript-eslint/restrict-template-expressions": "off",
 | 
			
		||||
 | 
			
		||||
    // This rule doesn't seem to be working properly
 | 
			
		||||
    "@typescript-eslint/prefer-nullish-coalescing": "off",
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
module.exports = config;
 | 
			
		||||
							
								
								
									
										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.
 | 
			
		||||
							
								
								
									
										23
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1,23 +0,0 @@
 | 
			
		||||
# To get started with Dependabot version updates, you'll need to specify which
 | 
			
		||||
# package ecosystems to update and where the package manifests are located.
 | 
			
		||||
# Please see the documentation for all configuration options:
 | 
			
		||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
 | 
			
		||||
 | 
			
		||||
version: 2
 | 
			
		||||
updates:
 | 
			
		||||
- package-ecosystem: npm
 | 
			
		||||
  versioning-strategy: increase
 | 
			
		||||
  directory: "/"
 | 
			
		||||
  schedule:
 | 
			
		||||
    interval: daily
 | 
			
		||||
  commit-message:
 | 
			
		||||
    prefix: "build"
 | 
			
		||||
    include: "scope"
 | 
			
		||||
  open-pull-requests-limit: 10
 | 
			
		||||
- package-ecosystem: github-actions
 | 
			
		||||
  directory: "/"
 | 
			
		||||
  schedule:
 | 
			
		||||
    interval: weekly
 | 
			
		||||
  commit-message:
 | 
			
		||||
    prefix: "build"
 | 
			
		||||
    include: "scope"
 | 
			
		||||
							
								
								
									
										20
									
								
								.github/release.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,20 @@
 | 
			
		||||
changelog:
 | 
			
		||||
  exclude:
 | 
			
		||||
    authors:
 | 
			
		||||
      - renovate[bot]
 | 
			
		||||
    categories:
 | 
			
		||||
      - title: Features
 | 
			
		||||
        labels:
 | 
			
		||||
          - feature
 | 
			
		||||
      - title: Bug Fixes
 | 
			
		||||
        labels:
 | 
			
		||||
          - fix
 | 
			
		||||
      - title: Miscellaneous Chores
 | 
			
		||||
        labels:
 | 
			
		||||
          - chore
 | 
			
		||||
          - ci
 | 
			
		||||
          - docs
 | 
			
		||||
          - test
 | 
			
		||||
      - title: Other Changes
 | 
			
		||||
        labels:
 | 
			
		||||
          - "*"
 | 
			
		||||
							
								
								
									
										28
									
								
								.github/workflows/bun-dependabot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1,28 +0,0 @@
 | 
			
		||||
name: 'Dependabot: Update bun.lockb'
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  pull_request:
 | 
			
		||||
    paths:
 | 
			
		||||
      - "package.json"
 | 
			
		||||
 | 
			
		||||
permissions:
 | 
			
		||||
  contents: write
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  update-bun-lockb:
 | 
			
		||||
    name: "Update bun.lockb"
 | 
			
		||||
    if: github.actor == 'dependabot[bot]'
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: oven-sh/setup-bun@v1
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
        with:
 | 
			
		||||
          fetch-depth: 0
 | 
			
		||||
          ref: ${{ github.event.pull_request.head.ref }}
 | 
			
		||||
      - run: |
 | 
			
		||||
          bun install
 | 
			
		||||
          git add bun.lockb
 | 
			
		||||
          git config --global user.name 'dependabot[bot]'
 | 
			
		||||
          git config --global user.email 'dependabot[bot]@users.noreply.github.com'
 | 
			
		||||
          git commit --amend --no-edit 
 | 
			
		||||
          git push --force
 | 
			
		||||
							
								
								
									
										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
 | 
			
		||||
							
								
								
									
										20
									
								
								.github/workflows/conventional-label.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,20 @@
 | 
			
		||||
on:
 | 
			
		||||
  pull_request_target:
 | 
			
		||||
    types: [opened, edited]
 | 
			
		||||
name: conventional-release-labels
 | 
			
		||||
jobs:
 | 
			
		||||
  label:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: bcoe/conventional-release-labels@v1
 | 
			
		||||
        with:
 | 
			
		||||
          type_labels: |
 | 
			
		||||
            {
 | 
			
		||||
              "feat": "Feature",
 | 
			
		||||
              "fix": "Fix",
 | 
			
		||||
              "breaking": "Breaking",
 | 
			
		||||
              "chore": "Chore",
 | 
			
		||||
              "docs": "Docs",
 | 
			
		||||
              "test": "Test",
 | 
			
		||||
              "ci": "CI"
 | 
			
		||||
            }
 | 
			
		||||
							
								
								
									
										177
									
								
								.github/workflows/docker-publish.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1,68 +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_name != 'pull_request' || (github.event.pull_request.head.repo.full_name == github.repository && contains(fromJSON('["COLLABORATOR","MEMBER","OWNER"]'), github.event.pull_request.author_association)) }}
 | 
			
		||||
          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@v5
 | 
			
		||||
        with:
 | 
			
		||||
          name: digests-${{ env.PLATFORM_PAIR }}
 | 
			
		||||
          path: /tmp/digests/*
 | 
			
		||||
          if-no-files-found: error
 | 
			
		||||
          retention-days: 1
 | 
			
		||||
 | 
			
		||||
  merge:
 | 
			
		||||
    if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.head.repo.full_name == github.repository && contains(fromJSON('["COLLABORATOR","MEMBER","OWNER"]'), github.event.pull_request.author_association)) }}
 | 
			
		||||
    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@v6
 | 
			
		||||
        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@v5
 | 
			
		||||
      - name: Set up Docker Buildx
 | 
			
		||||
        uses: docker/setup-buildx-action@v3
 | 
			
		||||
 | 
			
		||||
      - name: Login to GitHub Container Registry
 | 
			
		||||
        uses: docker/login-action@v3
 | 
			
		||||
        with:
 | 
			
		||||
          context: .
 | 
			
		||||
          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@v5
 | 
			
		||||
        with:
 | 
			
		||||
          username: ${{ env.DOCKERHUB_USERNAME }}
 | 
			
		||||
          password: ${{ secrets.DOCKERHUB_TOKEN }}
 | 
			
		||||
          repository: ${{ env.IMAGE_NAME }}
 | 
			
		||||
          short-description: ${{ github.event.repository.description }}
 | 
			
		||||
          enable-url-completion: true
 | 
			
		||||
							
								
								
									
										25
									
								
								.github/workflows/release-please.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1,25 +0,0 @@
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches:
 | 
			
		||||
      - main
 | 
			
		||||
 | 
			
		||||
permissions:
 | 
			
		||||
  contents: write
 | 
			
		||||
  pull-requests: write
 | 
			
		||||
 | 
			
		||||
name: release-please
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  release-please:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: googleapis/release-please-action@v4
 | 
			
		||||
        with:
 | 
			
		||||
          # this assumes that you have created a personal access token
 | 
			
		||||
          # (PAT) and configured it as a GitHub action secret named
 | 
			
		||||
          # `MY_RELEASE_PLEASE_TOKEN` (this secret name is not important).
 | 
			
		||||
          # token: ${{ secrets.MY_RELEASE_PLEASE_TOKEN }}
 | 
			
		||||
          token: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
          # this is a built-in strategy in release-please, see "Action Inputs"
 | 
			
		||||
          # for more options
 | 
			
		||||
          release-type: node
 | 
			
		||||
							
								
								
									
										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
 | 
			
		||||
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -46,4 +46,7 @@ package-lock.json
 | 
			
		||||
/output
 | 
			
		||||
/db
 | 
			
		||||
/data
 | 
			
		||||
/dist
 | 
			
		||||
/Bruno
 | 
			
		||||
/tsconfig.tsbuildinfo
 | 
			
		||||
/public/generated.css
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,4 @@
 | 
			
		||||
{
 | 
			
		||||
  "typescript.tsdk": "node_modules/typescript/lib",
 | 
			
		||||
  "typescript.enablePromptUseWorkspaceTsdk": true
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										236
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						@@ -1,35 +1,249 @@
 | 
			
		||||
# Changelog
 | 
			
		||||
 | 
			
		||||
## [0.15.0](https://github.com/C4illin/ConvertX/compare/v0.14.1...v0.15.0) (2025-10-07)
 | 
			
		||||
 | 
			
		||||
### Features
 | 
			
		||||
 | 
			
		||||
- add download all file by file alongside the tar download ([#415](https://github.com/C4illin/ConvertX/issues/415)) ([3e7e95b](https://github.com/C4illin/ConvertX/commit/3e7e95b53c78469f4aada996e835fcc6b9fde134))
 | 
			
		||||
- 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)
 | 
			
		||||
- missing public files ([8a888cc](https://github.com/C4illin/ConvertX/commit/8a888ccda679a31f801855e37800f52f1a1fda6e)), closes [#314](https://github.com/C4illin/ConvertX/issues/314)
 | 
			
		||||
- 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))
 | 
			
		||||
 | 
			
		||||
## [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))
 | 
			
		||||
 | 
			
		||||
### 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))
 | 
			
		||||
 | 
			
		||||
## [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))
 | 
			
		||||
 | 
			
		||||
## [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)
 | 
			
		||||
 | 
			
		||||
## [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))
 | 
			
		||||
 | 
			
		||||
## [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))
 | 
			
		||||
 | 
			
		||||
## [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" ]
 | 
			
		||||
							
								
								
									
										105
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						@@ -1,53 +1,104 @@
 | 
			
		||||
FROM oven/bun:1-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
 | 
			
		||||
 | 
			
		||||
# 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 . .
 | 
			
		||||
FROM base AS prerelease
 | 
			
		||||
WORKDIR /app
 | 
			
		||||
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 \
 | 
			
		||||
  texmf-dist-latexextra \
 | 
			
		||||
RUN apt-get update && apt-get install -y \
 | 
			
		||||
  assimp-utils \
 | 
			
		||||
  calibre \
 | 
			
		||||
  dasel \
 | 
			
		||||
  dcraw \
 | 
			
		||||
  dvisvgm \
 | 
			
		||||
  ffmpeg \
 | 
			
		||||
  graphicsmagick \
 | 
			
		||||
  ghostscript \
 | 
			
		||||
  vips-tools \
 | 
			
		||||
  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=prerelease /app/src/index.tsx /app/src/
 | 
			
		||||
# COPY --from=prerelease /app/package.json .
 | 
			
		||||
COPY . .
 | 
			
		||||
COPY --from=prerelease /app/public/ /app/public/
 | 
			
		||||
COPY --from=prerelease /app/dist /app/dist
 | 
			
		||||
 | 
			
		||||
# COPY . .
 | 
			
		||||
RUN mkdir data
 | 
			
		||||
 | 
			
		||||
EXPOSE 3000/tcp
 | 
			
		||||
ENTRYPOINT [ "bun", "run", "./src/index.tsx" ]
 | 
			
		||||
# used for calibre
 | 
			
		||||
ENV QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox"
 | 
			
		||||
ENV NODE_ENV=production
 | 
			
		||||
ENTRYPOINT [ "bun", "run", "dist/src/index.js" ]
 | 
			
		||||
							
								
								
									
										131
									
								
								README.md
									
									
									
									
									
								
							
							
						
						@@ -1,79 +1,154 @@
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
# 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          |
 | 
			
		||||
| [Vips](https://github.com/libvips/libvips)                                   | Images        | 45            | 23          |
 | 
			
		||||
| [PDFLaTeX](https://www.math.rug.nl/~trentelman/jacob/pdflatex/pdflatex.html) | Documents     | 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 |
 | 
			
		||||
| --------------------------------------------------------------- | ---------------- | ------------- | ----------- |
 | 
			
		||||
| [Inkscape](https://inkscape.org/)                               | Vector images    | 7             | 17          |
 | 
			
		||||
| [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          |
 | 
			
		||||
| [LibreOffice](https://www.libreoffice.org/)                     | Documents        | 41            | 22          |
 | 
			
		||||
| [Dasel](https://github.com/TomWright/dasel)                     | Data Files       | 5             | 4           |
 | 
			
		||||
| [Pandoc](https://pandoc.org/)                                   | Documents        | 43            | 65          |
 | 
			
		||||
| [msgconvert](https://github.com/mvz/email-outlook-message-perl) | Outlook          | 1             | 1           |
 | 
			
		||||
| [dvisvgm](https://dvisvgm.de/)                                  | Vector images    | 4             | 2           |
 | 
			
		||||
| [ImageMagick](https://imagemagick.org/)                         | Images           | 245           | 183         |
 | 
			
		||||
| [GraphicsMagick](http://www.graphicsmagick.org/)                | Images           | 167           | 130         |
 | 
			
		||||
| [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           |
 | 
			
		||||
 | 
			
		||||
<!-- many ffmpeg fileformats are duplicates -->
 | 
			
		||||
 | 
			
		||||
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:
 | 
			
		||||
    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
 | 
			
		||||
    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
 | 
			
		||||
or
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
docker run ghcr.io/c4illin/convertx:master -p 3000:3000 -e ACCOUNT_REGISTRATION=false -v /path/you/want:/app/data
 | 
			
		||||
``` -->
 | 
			
		||||
docker run -p 3000:3000 -v ./data:/app/data ghcr.io/c4illin/convertx
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Then visit `http://localhost:3000` in your browser and create your account. Don't leave it unconfigured and open, as anyone can register the first account.
 | 
			
		||||
 | 
			
		||||
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                                                               |
 | 
			
		||||
| MAX_CONVERT_PROCESS          | 0                                                  | Maximum number of concurrent conversion processes allowed. Set to 0 for unlimited.                                        |
 | 
			
		||||
 | 
			
		||||
### 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.
 | 
			
		||||
 | 
			
		||||
## Todo
 | 
			
		||||
- [x] Add messages for errors in converters
 | 
			
		||||
- [ ] Add options for converters
 | 
			
		||||
- [ ] Add more converters
 | 
			
		||||
- [ ] Divide index.tsx into smaller components
 | 
			
		||||
- [ ] Add tests
 | 
			
		||||
- [ ] Add searchable list of formats
 | 
			
		||||
- [ ] Make the upload button nicer and more easy to drop files on. Support copy paste as well if possible.
 | 
			
		||||
Tutorial in french: <https://belginux.com/installer-convertx-avec-docker/>
 | 
			
		||||
 | 
			
		||||
Tutorial in chinese: <https://xzllll.com/24092901/>
 | 
			
		||||
 | 
			
		||||
## Screenshots
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
## Development
 | 
			
		||||
 | 
			
		||||
0. Install [Bun](https://bun.sh/) and Git
 | 
			
		||||
1. Clone the repository
 | 
			
		||||
2. `bun install`
 | 
			
		||||
3. `bun run dev`
 | 
			
		||||
 | 
			
		||||
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!
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
<a href="https://github.com/C4illin/ConvertX/stargazers">
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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.
 | 
			
		||||
							
								
								
									
										18
									
								
								biome.json
									
									
									
									
									
								
							
							
						
						@@ -1,5 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
  "$schema": "https://biomejs.dev/schemas/1.7.3/schema.json",
 | 
			
		||||
  "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
 | 
			
		||||
  "formatter": {
 | 
			
		||||
    "enabled": true,
 | 
			
		||||
    "formatWithErrors": true,
 | 
			
		||||
@@ -9,7 +9,12 @@
 | 
			
		||||
    "lineWidth": 80,
 | 
			
		||||
    "attributePosition": "auto"
 | 
			
		||||
  },
 | 
			
		||||
  "organizeImports": { "enabled": true },
 | 
			
		||||
  "files": {
 | 
			
		||||
    "ignore": ["**/node_modules/**", "**/pico.lime.min.css"]
 | 
			
		||||
  },
 | 
			
		||||
  "organizeImports": {
 | 
			
		||||
    "enabled": true
 | 
			
		||||
  },
 | 
			
		||||
  "linter": {
 | 
			
		||||
    "enabled": true,
 | 
			
		||||
    "rules": {
 | 
			
		||||
@@ -22,7 +27,11 @@
 | 
			
		||||
        "useLiteralKeys": "error",
 | 
			
		||||
        "useOptionalChain": "error"
 | 
			
		||||
      },
 | 
			
		||||
      "correctness": { "noPrecisionLoss": "error", "noUnusedVariables": "off" },
 | 
			
		||||
      "correctness": {
 | 
			
		||||
        "noPrecisionLoss": "error",
 | 
			
		||||
        "noUnusedVariables": "off",
 | 
			
		||||
        "useJsxKeyInIterable": "off"
 | 
			
		||||
      },
 | 
			
		||||
      "style": {
 | 
			
		||||
        "noInferrableTypes": "error",
 | 
			
		||||
        "noNamespace": "error",
 | 
			
		||||
@@ -42,6 +51,9 @@
 | 
			
		||||
        "noUnsafeDeclarationMerging": "error",
 | 
			
		||||
        "useAwait": "error",
 | 
			
		||||
        "useNamespaceKeyword": "error"
 | 
			
		||||
      },
 | 
			
		||||
      "nursery": {
 | 
			
		||||
        "useSortedClasses": "error"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										689
									
								
								bun.lock
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,689 @@
 | 
			
		||||
{
 | 
			
		||||
  "lockfileVersion": 1,
 | 
			
		||||
  "workspaces": {
 | 
			
		||||
    "": {
 | 
			
		||||
      "name": "convertx-frontend",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@elysiajs/html": "^1.4.0",
 | 
			
		||||
        "@elysiajs/jwt": "^1.4.0",
 | 
			
		||||
        "@elysiajs/static": "^1.4.4",
 | 
			
		||||
        "@kitajs/html": "^4.2.10",
 | 
			
		||||
        "elysia": "^1.4.13",
 | 
			
		||||
        "sanitize-filename": "^1.6.3",
 | 
			
		||||
        "tar": "^7.5.1",
 | 
			
		||||
      },
 | 
			
		||||
      "devDependencies": {
 | 
			
		||||
        "@eslint/js": "^9.38.0",
 | 
			
		||||
        "@kitajs/ts-html-plugin": "^4.1.3",
 | 
			
		||||
        "@tailwindcss/cli": "^4.1.16",
 | 
			
		||||
        "@tailwindcss/postcss": "^4.1.16",
 | 
			
		||||
        "@types/bun": "latest",
 | 
			
		||||
        "@types/node": "^24.9.2",
 | 
			
		||||
        "@typescript-eslint/parser": "^8.46.2",
 | 
			
		||||
        "eslint": "^9.38.0",
 | 
			
		||||
        "eslint-plugin-better-tailwindcss": "^3.7.10",
 | 
			
		||||
        "globals": "^16.4.0",
 | 
			
		||||
        "knip": "^5.66.4",
 | 
			
		||||
        "npm-run-all2": "^8.0.4",
 | 
			
		||||
        "postcss": "^8.5.6",
 | 
			
		||||
        "prettier": "^3.6.2",
 | 
			
		||||
        "tailwind-scrollbar": "^4.0.2",
 | 
			
		||||
        "tailwindcss": "^4.1.16",
 | 
			
		||||
        "typescript": "^5.9.3",
 | 
			
		||||
        "typescript-eslint": "^8.46.2",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  "trustedDependencies": [
 | 
			
		||||
    "@tailwindcss/oxide",
 | 
			
		||||
    "oxc-resolver",
 | 
			
		||||
    "@parcel/watcher",
 | 
			
		||||
  ],
 | 
			
		||||
  "packages": {
 | 
			
		||||
    "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
 | 
			
		||||
 | 
			
		||||
    "@borewit/text-codec": ["@borewit/text-codec@0.1.1", "", {}, "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA=="],
 | 
			
		||||
 | 
			
		||||
    "@elysiajs/html": ["@elysiajs/html@1.4.0", "", { "dependencies": { "@kitajs/html": "^4.1.0", "@kitajs/ts-html-plugin": "^4.0.1" }, "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-j4jFqGEkIC8Rg2XiTOujb9s0WLnz1dnY/4uqczyCdOVruDeJtGP+6+GvF0A76SxEvltn8UR1yCUnRdLqRi3vuw=="],
 | 
			
		||||
 | 
			
		||||
    "@elysiajs/jwt": ["@elysiajs/jwt@1.4.0", "", { "dependencies": { "jose": "^6.0.11" }, "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-Z0PvZhQxdDeKZ8HslXzDoXXD83NKExNPmoiAPki3nI2Xvh5wtUrBH+zWOD17yP14IbRo8fxGj3L25MRCAPsgPA=="],
 | 
			
		||||
 | 
			
		||||
    "@elysiajs/static": ["@elysiajs/static@1.4.4", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-PT/uGvBHQL5I+APAGiuRjhVfySe5YmrJdPtSc2QyM6CgNp4WDCmPfhPoVYkHNaH5QGWdP62hMq0HUnClNxR3zw=="],
 | 
			
		||||
 | 
			
		||||
    "@emnapi/core": ["@emnapi/core@1.5.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="],
 | 
			
		||||
 | 
			
		||||
    "@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/core": ["@eslint/core@0.16.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/css-tree": ["@eslint/css-tree@3.6.5", "", { "dependencies": { "mdn-data": "2.23.0", "source-map-js": "^1.0.1" } }, "sha512-bJgnXu0D0K1BbfPfHTmCaJe2ucBOjeg/tG37H2CSqYCw51VMmBtPfWrH8LKPLAVCOp0h94e1n8PfR3v9iRbtyA=="],
 | 
			
		||||
 | 
			
		||||
    "@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.38.0", "", {}, "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.0", "", { "dependencies": { "@eslint/core": "^0.16.0", "levn": "^0.4.1" } }, "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A=="],
 | 
			
		||||
 | 
			
		||||
    "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
 | 
			
		||||
 | 
			
		||||
    "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
 | 
			
		||||
 | 
			
		||||
    "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
 | 
			
		||||
 | 
			
		||||
    "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
 | 
			
		||||
 | 
			
		||||
    "@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.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
 | 
			
		||||
 | 
			
		||||
    "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
 | 
			
		||||
 | 
			
		||||
    "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
 | 
			
		||||
 | 
			
		||||
    "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
 | 
			
		||||
 | 
			
		||||
    "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
 | 
			
		||||
 | 
			
		||||
    "@kitajs/html": ["@kitajs/html@4.2.10", "", { "dependencies": { "csstype": "^3.1.3" } }, "sha512-q9n2Ig7GlAYOdL+CeWxsIIZFIKna+eCJah15eK8PBIFHW3UcWayAMs8QYGJNYgP3uMucDimIAUBH26xnE7GILw=="],
 | 
			
		||||
 | 
			
		||||
    "@kitajs/ts-html-plugin": ["@kitajs/ts-html-plugin@4.1.3", "", { "dependencies": { "chalk": "^5.6.2", "tslib": "^2.8.1", "yargs": "^18.0.0" }, "peerDependencies": { "@kitajs/html": "^4.2.10", "typescript": "^5.6.2" }, "bin": { "ts-html-plugin": "dist/cli.js", "xss-scan": "dist/cli.js" } }, "sha512-NlYrID5yMxfRKiO1eiiSC4MWveKe0ffoCJOZm4idNOqwimmLXr0g1NmvCcquOU2XLRrgzynxZqw6rhwR5CY5Nw=="],
 | 
			
		||||
 | 
			
		||||
    "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.7", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" } }, "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw=="],
 | 
			
		||||
 | 
			
		||||
    "@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-android-arm-eabi": ["@oxc-resolver/binding-android-arm-eabi@11.12.0", "", { "os": "android", "cpu": "arm" }, "sha512-/IfGWLNdmS1kVYM2g+Xw4qXNWtCPZ/i5YMprflA8FC3vAjT4N0VucQcDxUPHxatQwre4qnhbFFWqRa1mz6Cgkw=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-android-arm64": ["@oxc-resolver/binding-android-arm64@11.12.0", "", { "os": "android", "cpu": "arm64" }, "sha512-H3Ehyinfx2VO8F5TwdaD/WY686Ia6J1H3LP0tgpNjlPGH2TrTniPERiwjqtOm/xHEef0KJvb/yfmUKLbHudhCA=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-darwin-arm64": ["@oxc-resolver/binding-darwin-arm64@11.12.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-hmm+A/0WdEtIeBrPtUHoSTzJefrZkhGSrmv5pwELKiqNqd+/gctzmTlt6wWrU8BMIryDMT9fWqLSQ3+NYfqAEA=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-darwin-x64": ["@oxc-resolver/binding-darwin-x64@11.12.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-g1tVu53EMfuRKs67o0PZR0+y/WXl/Tfn3d2ggjK3Hj17pQPcb9x1+Y6W7n4EjIDttwLZbCPCEr06X+aC03I45A=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-freebsd-x64": ["@oxc-resolver/binding-freebsd-x64@11.12.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TiMatzvcVMSOiAx8sbnAw7UCfQpZDlm91ItywZrSHlQIJqDBipOmjIEYUMc2p823Y+fJ2ADL5UBjUB2kfqpedw=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-linux-arm-gnueabihf": ["@oxc-resolver/binding-linux-arm-gnueabihf@11.12.0", "", { "os": "linux", "cpu": "arm" }, "sha512-zU+9UgxPIvfReqmRr/dqZt3387HPgcH0hA4u0QGE+280EFjBYYL2rxGDxK0L+keO6vc2+ITWVDXm9KIj+alofg=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-linux-arm-musleabihf": ["@oxc-resolver/binding-linux-arm-musleabihf@11.12.0", "", { "os": "linux", "cpu": "arm" }, "sha512-dfO1rrOeELYWD/BewMCp81k1I3pOdtAi2VCKg/A1I8z0uI4OR6cThb5dV9fpHkj7zlb0Y5iZFPe+NTbI/u1MgQ=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-linux-arm64-gnu": ["@oxc-resolver/binding-linux-arm64-gnu@11.12.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-JJNyN1ueryETKTUsG57+u0GDbtHKVcwcUoC6YyJmDdWE0o/3twXtHuS+F/121a2sVK8PKlROqGAev+STx3AuuQ=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-linux-arm64-musl": ["@oxc-resolver/binding-linux-arm64-musl@11.12.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-rQHoxL0H0WwYUuukPUscLyzWwTl/hyogptYsY+Ye6AggJEOuvgJxMum2glY7etGIGOXxrfjareHnNO1tNY7WYg=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-linux-ppc64-gnu": ["@oxc-resolver/binding-linux-ppc64-gnu@11.12.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-XPUZSctO+FrC0314Tcth+GrTtzy2yaYqyl8weBMAbKFMwuV8VnR2SHg9dmtI9vkukmM3auOLj0Kqjpl3YXwXiw=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-linux-riscv64-gnu": ["@oxc-resolver/binding-linux-riscv64-gnu@11.12.0", "", { "os": "linux", "cpu": "none" }, "sha512-AmMjcP+6zHLF1JNq/p3yPEcXmZW/Xw5Xl19Zd0eBCSyGORJRuUOkcnyC8bwMO43b/G7PtausB83fclnFL5KZ3w=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-linux-riscv64-musl": ["@oxc-resolver/binding-linux-riscv64-musl@11.12.0", "", { "os": "linux", "cpu": "none" }, "sha512-K2/yFBqFQOKyVwQxYDAKqDtk2kS4g58aGyj/R1bvYPr2P7v7971aUG/5m2WD5u2zSqWBfu1o4PdhX0lsqvA3vQ=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-linux-s390x-gnu": ["@oxc-resolver/binding-linux-s390x-gnu@11.12.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-uSl4jo78tONGZtwsOA4ldT/OI7/hoHJhSMlGYE4Z/lzwMjkAaBdX4soAK5P/rL+U2yCJlRMnnoUckhXlZvDbSw=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-linux-x64-gnu": ["@oxc-resolver/binding-linux-x64-gnu@11.12.0", "", { "os": "linux", "cpu": "x64" }, "sha512-YjL8VAkbPyQ1kUuR6pOBk1O+EkxOoLROTa+ia1/AmFLuXYNltLGI1YxOY14i80cKpOf0Z59IXnlrY3coAI9NDQ=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-linux-x64-musl": ["@oxc-resolver/binding-linux-x64-musl@11.12.0", "", { "os": "linux", "cpu": "x64" }, "sha512-qpHPU0qqeJXh7cPzA+I+WWA6RxtRArfmSrhTXidbiQ08G5A1e55YQwExWkitB2rSqN6YFxnpfhHKo9hyhpyfSg=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-wasm32-wasi": ["@oxc-resolver/binding-wasm32-wasi@11.12.0", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.0.7" }, "cpu": "none" }, "sha512-oqg80bERZAagWLqYmngnesE0/2miv4lST7+wiiZniD6gyb1SoRckwEkbTsytGutkudFtw7O61Pon6pNlOvyFaA=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-win32-arm64-msvc": ["@oxc-resolver/binding-win32-arm64-msvc@11.12.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-qKH816ycEN9yR/TX91CP1/i6xyVNHKX9VEOYa3XzQROPVtcYG2F6A3ng/PhwpJvS1cmL/DlilhglZe9KWkhNjg=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-win32-ia32-msvc": ["@oxc-resolver/binding-win32-ia32-msvc@11.12.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-3bgxubTlhzF6BwBnhGz5BTboarl1upuanEr6i0dncjfEcU+Z9xAOgbtA7Ip3G3EKDjE1objRKK+ny8PKJZ3b7Q=="],
 | 
			
		||||
 | 
			
		||||
    "@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@11.12.0", "", { "os": "win32", "cpu": "x64" }, "sha512-rbiWYQWxwy+x7+KgNAoAGYIPB3xUclQlFVV3L5lwfsbp4PQPomJohHowlWgi3GRAEybM5+ZL9xny0YfpJOsthA=="],
 | 
			
		||||
 | 
			
		||||
    "@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.41", "", {}, "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/cli": ["@tailwindcss/cli@4.1.16", "", { "dependencies": { "@parcel/watcher": "^2.5.1", "@tailwindcss/node": "4.1.16", "@tailwindcss/oxide": "4.1.16", "enhanced-resolve": "^5.18.3", "mri": "^1.2.0", "picocolors": "^1.1.1", "tailwindcss": "4.1.16" }, "bin": { "tailwindcss": "dist/index.mjs" } }, "sha512-dsnANPrh2ZooHyZ/8uJhc9ecpcYtufToc21NY09NS9vF16rxPCjJ8dP7TUAtPqlUJTHSmRkN2hCdoYQIlgh4fw=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/node": ["@tailwindcss/node@4.1.16", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.19", "source-map-js": "^1.2.1", "tailwindcss": "4.1.16" } }, "sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.16", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.16", "@tailwindcss/oxide-darwin-arm64": "4.1.16", "@tailwindcss/oxide-darwin-x64": "4.1.16", "@tailwindcss/oxide-freebsd-x64": "4.1.16", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.16", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.16", "@tailwindcss/oxide-linux-arm64-musl": "4.1.16", "@tailwindcss/oxide-linux-x64-gnu": "4.1.16", "@tailwindcss/oxide-linux-x64-musl": "4.1.16", "@tailwindcss/oxide-wasm32-wasi": "4.1.16", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.16", "@tailwindcss/oxide-win32-x64-msvc": "4.1.16" } }, "sha512-2OSv52FRuhdlgyOQqgtQHuCgXnS8nFSYRp2tJ+4WZXKgTxqPy7SMSls8c3mPT5pkZ17SBToGM5LHEJBO7miEdg=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.16", "", { "os": "android", "cpu": "arm64" }, "sha512-8+ctzkjHgwDJ5caq9IqRSgsP70xhdhJvm+oueS/yhD5ixLhqTw9fSL1OurzMUhBwE5zK26FXLCz2f/RtkISqHA=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.16", "", { "os": "darwin", "cpu": "arm64" }, "sha512-C3oZy5042v2FOALBZtY0JTDnGNdS6w7DxL/odvSny17ORUnaRKhyTse8xYi3yKGyfnTUOdavRCdmc8QqJYwFKA=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.16", "", { "os": "darwin", "cpu": "x64" }, "sha512-vjrl/1Ub9+JwU6BP0emgipGjowzYZMjbWCDqwA2Z4vCa+HBSpP4v6U2ddejcHsolsYxwL5r4bPNoamlV0xDdLg=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.16", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TSMpPYpQLm+aR1wW5rKuUuEruc/oOX3C7H0BTnPDn7W/eMw8W+MRMpiypKMkXZfwH8wqPIRKppuZoedTtNj2tg=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.16", "", { "os": "linux", "cpu": "arm" }, "sha512-p0GGfRg/w0sdsFKBjMYvvKIiKy/LNWLWgV/plR4lUgrsxFAoQBFrXkZ4C0w8IOXfslB9vHK/JGASWD2IefIpvw=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-DoixyMmTNO19rwRPdqviTrG1rYzpxgyYJl8RgQvdAQUzxC1ToLRqtNJpU/ATURSKgIg6uerPw2feW0aS8SNr/w=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.16", "", { "os": "linux", "cpu": "x64" }, "sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.16", "", { "os": "linux", "cpu": "x64" }, "sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.16", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.0.7", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.16", "", { "os": "win32", "cpu": "arm64" }, "sha512-zX+Q8sSkGj6HKRTMJXuPvOcP8XfYON24zJBRPlszcH1Np7xuHXhWn8qfFjIujVzvH3BHU+16jBXwgpl20i+v9A=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.16", "", { "os": "win32", "cpu": "x64" }, "sha512-m5dDFJUEejbFqP+UXVstd4W/wnxA4F61q8SoL+mqTypId2T2ZpuxosNSgowiCnLp2+Z+rivdU0AqpfgiD7yCBg=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.16", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.16", "@tailwindcss/oxide": "4.1.16", "postcss": "^8.4.41", "tailwindcss": "4.1.16" } }, "sha512-Qn3SFGPXYQMKR/UtqS+dqvPrzEeBZHrFA92maT4zijCVggdsXnDBMsPFJo1eArX3J+O+Gi+8pV4PkqjLCNBk3A=="],
 | 
			
		||||
 | 
			
		||||
    "@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=="],
 | 
			
		||||
 | 
			
		||||
    "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
 | 
			
		||||
 | 
			
		||||
    "@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="],
 | 
			
		||||
 | 
			
		||||
    "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
 | 
			
		||||
 | 
			
		||||
    "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
 | 
			
		||||
 | 
			
		||||
    "@types/node": ["@types/node@24.9.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA=="],
 | 
			
		||||
 | 
			
		||||
    "@types/prismjs": ["@types/prismjs@1.26.5", "", {}, "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ=="],
 | 
			
		||||
 | 
			
		||||
    "@types/react": ["@types/react@19.1.15", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-+kLxJpaJzXybyDyFXYADyP1cznTO8HSuBpenGlnKOAkH4hyNINiywvXS/tGJhsrGGP/gM185RA3xpjY0Yg4erA=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.46.2", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/type-utils": "8.46.2", "@typescript-eslint/utils": "8.46.2", "@typescript-eslint/visitor-keys": "8.46.2", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.46.2", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/parser": ["@typescript-eslint/parser@8.46.2", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", "@typescript-eslint/typescript-estree": "8.46.2", "@typescript-eslint/visitor-keys": "8.46.2", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.46.2", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.46.2", "@typescript-eslint/types": "^8.46.2", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.46.2", "", { "dependencies": { "@typescript-eslint/types": "8.46.2", "@typescript-eslint/visitor-keys": "8.46.2" } }, "sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.46.2", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.46.2", "", { "dependencies": { "@typescript-eslint/types": "8.46.2", "@typescript-eslint/typescript-estree": "8.46.2", "@typescript-eslint/utils": "8.46.2", "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-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/types": ["@typescript-eslint/types@8.46.2", "", {}, "sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.46.2", "", { "dependencies": { "@typescript-eslint/project-service": "8.46.2", "@typescript-eslint/tsconfig-utils": "8.46.2", "@typescript-eslint/types": "8.46.2", "@typescript-eslint/visitor-keys": "8.46.2", "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-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/utils": ["@typescript-eslint/utils@8.46.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", "@typescript-eslint/typescript-estree": "8.46.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.46.2", "", { "dependencies": { "@typescript-eslint/types": "8.46.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w=="],
 | 
			
		||||
 | 
			
		||||
    "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@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
 | 
			
		||||
 | 
			
		||||
    "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
 | 
			
		||||
 | 
			
		||||
    "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.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
 | 
			
		||||
 | 
			
		||||
    "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
 | 
			
		||||
 | 
			
		||||
    "bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="],
 | 
			
		||||
 | 
			
		||||
    "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
 | 
			
		||||
 | 
			
		||||
    "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
 | 
			
		||||
 | 
			
		||||
    "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
 | 
			
		||||
 | 
			
		||||
    "cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="],
 | 
			
		||||
 | 
			
		||||
    "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.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
 | 
			
		||||
 | 
			
		||||
    "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.4.13", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.2.2", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-6QaWQEm7QN1UCo1TPpEjaRJPHUmnM7R29y6LY224frDGk5PrpAnWmdHkoZxkcv+JRWp1j2ROr2IHbxHbG/jRjw=="],
 | 
			
		||||
 | 
			
		||||
    "emoji-regex": ["emoji-regex@10.5.0", "", {}, "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg=="],
 | 
			
		||||
 | 
			
		||||
    "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.38.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.1", "@eslint/core": "^0.16.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.38.0", "@eslint/plugin-kit": "^0.4.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "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-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw=="],
 | 
			
		||||
 | 
			
		||||
    "eslint-plugin-better-tailwindcss": ["eslint-plugin-better-tailwindcss@3.7.10", "", { "dependencies": { "@eslint/css-tree": "^3.6.5", "enhanced-resolve": "^5.18.3", "jiti": "^2.5.1", "postcss": "^8.5.6", "postcss-import": "^16.1.1", "synckit": "^0.11.11", "tailwind-csstree": "^0.1.4", "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-a6Hif4A9lPJZwpoMir2WVs1ZL4IK6fzwbZvVhQQchoqqg3BycL+ZGLHGIuqKBqwnZgOC6olwmu0sDzM3uSkEFw=="],
 | 
			
		||||
 | 
			
		||||
    "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.2.2", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-CrGe+4QzHZlnrXZVlo/WbUZ4qQZq8C0uATQVGVgXIrNXgHDBBNFD1VRfssRA2C9t3RYvh3MadZSdg2Wy7HBoQA=="],
 | 
			
		||||
 | 
			
		||||
    "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.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
 | 
			
		||||
 | 
			
		||||
    "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@21.0.0", "", { "dependencies": { "@tokenizer/inflate": "^0.2.7", "strtok3": "^10.2.2", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg=="],
 | 
			
		||||
 | 
			
		||||
    "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.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
 | 
			
		||||
 | 
			
		||||
    "formatly": ["formatly@0.3.0", "", { "dependencies": { "fd-package-json": "^2.0.0" }, "bin": { "formatly": "bin/index.mjs" } }, "sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w=="],
 | 
			
		||||
 | 
			
		||||
    "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
 | 
			
		||||
 | 
			
		||||
    "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
 | 
			
		||||
 | 
			
		||||
    "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="],
 | 
			
		||||
 | 
			
		||||
    "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
 | 
			
		||||
 | 
			
		||||
    "globals": ["globals@16.4.0", "", {}, "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw=="],
 | 
			
		||||
 | 
			
		||||
    "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.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
 | 
			
		||||
 | 
			
		||||
    "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-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.6.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ=="],
 | 
			
		||||
 | 
			
		||||
    "jose": ["jose@6.1.0", "", {}, "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA=="],
 | 
			
		||||
 | 
			
		||||
    "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
 | 
			
		||||
 | 
			
		||||
    "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.66.4", "", { "dependencies": { "@nodelib/fs.walk": "^1.2.3", "fast-glob": "^3.3.3", "formatly": "^0.3.0", "jiti": "^2.6.0", "js-yaml": "^4.1.0", "minimist": "^1.2.8", "oxc-resolver": "^11.12.0", "picocolors": "^1.1.1", "picomatch": "^4.0.1", "smol-toml": "^1.4.1", "strip-json-comments": "5.0.2", "zod": "^4.1.11" }, "peerDependencies": { "@types/node": ">=18", "typescript": ">=5.0.4 <7" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-HmTnxdmoHAvwKmFktRGY1++tXRI8J36eVrOpfj/ybTVVT1QBKBlbBEN1s3cJBx9UL+hXTZDNQif+gs7fUKldbw=="],
 | 
			
		||||
 | 
			
		||||
    "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.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="],
 | 
			
		||||
 | 
			
		||||
    "lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="],
 | 
			
		||||
 | 
			
		||||
    "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="],
 | 
			
		||||
 | 
			
		||||
    "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="],
 | 
			
		||||
 | 
			
		||||
    "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="],
 | 
			
		||||
 | 
			
		||||
    "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="],
 | 
			
		||||
 | 
			
		||||
    "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="],
 | 
			
		||||
 | 
			
		||||
    "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="],
 | 
			
		||||
 | 
			
		||||
    "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="],
 | 
			
		||||
 | 
			
		||||
    "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="],
 | 
			
		||||
 | 
			
		||||
    "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="],
 | 
			
		||||
 | 
			
		||||
    "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="],
 | 
			
		||||
 | 
			
		||||
    "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.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="],
 | 
			
		||||
 | 
			
		||||
    "mdn-data": ["mdn-data@2.23.0", "", {}, "sha512-786vq1+4079JSeu2XdcDjrhi/Ry7BWtjDl9WtGPWLiIHb2T66GvIVflZTBoSNZ5JqTtJGYEVMuFA/lbQlMOyDQ=="],
 | 
			
		||||
 | 
			
		||||
    "memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="],
 | 
			
		||||
 | 
			
		||||
    "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.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="],
 | 
			
		||||
 | 
			
		||||
    "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=="],
 | 
			
		||||
 | 
			
		||||
    "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.12.0", "", { "optionalDependencies": { "@oxc-resolver/binding-android-arm-eabi": "11.12.0", "@oxc-resolver/binding-android-arm64": "11.12.0", "@oxc-resolver/binding-darwin-arm64": "11.12.0", "@oxc-resolver/binding-darwin-x64": "11.12.0", "@oxc-resolver/binding-freebsd-x64": "11.12.0", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.12.0", "@oxc-resolver/binding-linux-arm-musleabihf": "11.12.0", "@oxc-resolver/binding-linux-arm64-gnu": "11.12.0", "@oxc-resolver/binding-linux-arm64-musl": "11.12.0", "@oxc-resolver/binding-linux-ppc64-gnu": "11.12.0", "@oxc-resolver/binding-linux-riscv64-gnu": "11.12.0", "@oxc-resolver/binding-linux-riscv64-musl": "11.12.0", "@oxc-resolver/binding-linux-s390x-gnu": "11.12.0", "@oxc-resolver/binding-linux-x64-gnu": "11.12.0", "@oxc-resolver/binding-linux-x64-musl": "11.12.0", "@oxc-resolver/binding-wasm32-wasi": "11.12.0", "@oxc-resolver/binding-win32-arm64-msvc": "11.12.0", "@oxc-resolver/binding-win32-ia32-msvc": "11.12.0", "@oxc-resolver/binding-win32-x64-msvc": "11.12.0" } }, "sha512-zmS2q2txiB+hS2u0aiIwmvITIJN8c8ThlWoWB762Wx5nUw8WBlttp0rzt8nnuP1cGIq9YJ7sGxfsgokm+SQk5Q=="],
 | 
			
		||||
 | 
			
		||||
    "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=="],
 | 
			
		||||
 | 
			
		||||
    "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
 | 
			
		||||
 | 
			
		||||
    "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
 | 
			
		||||
 | 
			
		||||
    "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.1.1", "", {}, "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ=="],
 | 
			
		||||
 | 
			
		||||
    "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=="],
 | 
			
		||||
 | 
			
		||||
    "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.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
 | 
			
		||||
 | 
			
		||||
    "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.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
 | 
			
		||||
 | 
			
		||||
    "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.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
 | 
			
		||||
 | 
			
		||||
    "smol-toml": ["smol-toml@1.4.2", "", {}, "sha512-rInDH6lCNiEyn3+hH8KVGFdbjc099j47+OSgbMrfDYX1CmXLfdKd7qi6IfcWj2wFxvSVkuI46M+wPGYfEOEj6g=="],
 | 
			
		||||
 | 
			
		||||
    "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
 | 
			
		||||
 | 
			
		||||
    "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
 | 
			
		||||
 | 
			
		||||
    "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
 | 
			
		||||
 | 
			
		||||
    "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.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="],
 | 
			
		||||
 | 
			
		||||
    "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.4", "", {}, "sha512-FzD187HuFIZEyeR7Xy6sJbJll2d4SybS90satC8SKIuaNRC05CxMvdzN7BUsfDQffcnabckRM5OIcfArjsZ0mg=="],
 | 
			
		||||
 | 
			
		||||
    "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.16", "", {}, "sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA=="],
 | 
			
		||||
 | 
			
		||||
    "tapable": ["tapable@2.2.3", "", {}, "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg=="],
 | 
			
		||||
 | 
			
		||||
    "tar": ["tar@7.5.1", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g=="],
 | 
			
		||||
 | 
			
		||||
    "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.1.1", "", { "dependencies": { "@borewit/text-codec": "^0.1.0", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ=="],
 | 
			
		||||
 | 
			
		||||
    "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.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
 | 
			
		||||
 | 
			
		||||
    "typescript-eslint": ["typescript-eslint@8.46.2", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.46.2", "@typescript-eslint/parser": "8.46.2", "@typescript-eslint/typescript-estree": "8.46.2", "@typescript-eslint/utils": "8.46.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-vbw8bOmiuYNdzzV3lsiWv6sRwjyuKJMQqWulBOU7M0RrxedXledX8G8kBbQeiOYDnTfiXz0Y4081E1QMNB6iQg=="],
 | 
			
		||||
 | 
			
		||||
    "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
 | 
			
		||||
 | 
			
		||||
    "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
 | 
			
		||||
 | 
			
		||||
    "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@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
 | 
			
		||||
 | 
			
		||||
    "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
 | 
			
		||||
 | 
			
		||||
    "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
 | 
			
		||||
 | 
			
		||||
    "yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="],
 | 
			
		||||
 | 
			
		||||
    "yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="],
 | 
			
		||||
 | 
			
		||||
    "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
 | 
			
		||||
 | 
			
		||||
    "zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="],
 | 
			
		||||
 | 
			
		||||
    "@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
 | 
			
		||||
 | 
			
		||||
    "@emnapi/core/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@emnapi/runtime/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
 | 
			
		||||
 | 
			
		||||
    "@eslint/config-helpers/@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
 | 
			
		||||
 | 
			
		||||
    "@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=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/node/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.5.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.7", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@tybys/wasm-util/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
 | 
			
		||||
 | 
			
		||||
    "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
 | 
			
		||||
 | 
			
		||||
    "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
 | 
			
		||||
 | 
			
		||||
    "eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
 | 
			
		||||
 | 
			
		||||
    "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.1.1", "", {}, "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw=="],
 | 
			
		||||
 | 
			
		||||
    "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
 | 
			
		||||
 | 
			
		||||
    "tsconfig-paths-webpack-plugin/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
 | 
			
		||||
 | 
			
		||||
    "@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": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
 | 
			
		||||
 | 
			
		||||
    "@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/@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.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
 | 
			
		||||
 | 
			
		||||
    "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
 | 
			
		||||
 | 
			
		||||
    "eslint/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
 | 
			
		||||
 | 
			
		||||
    "tsconfig-paths-webpack-plugin/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
 | 
			
		||||
 | 
			
		||||
    "@tailwindcss/oxide-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										14
									
								
								compose.yaml
									
									
									
									
									
								
							
							
						
						@@ -5,8 +5,16 @@ services:
 | 
			
		||||
      # dockerfile: Debian.Dockerfile
 | 
			
		||||
    volumes:
 | 
			
		||||
      - ./data:/app/data
 | 
			
		||||
    environment:
 | 
			
		||||
      - ACCOUNT_REGISTRATION=true
 | 
			
		||||
      - JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234
 | 
			
		||||
    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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										70
									
								
								eslint.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,70 @@
 | 
			
		||||
import js from "@eslint/js";
 | 
			
		||||
import eslintParserTypeScript from "@typescript-eslint/parser";
 | 
			
		||||
import eslintPluginBetterTailwindcss from "eslint-plugin-better-tailwindcss";
 | 
			
		||||
import globals from "globals";
 | 
			
		||||
import tseslint from "typescript-eslint";
 | 
			
		||||
 | 
			
		||||
export default tseslint.config(
 | 
			
		||||
  js.configs.recommended,
 | 
			
		||||
  tseslint.configs.recommended,
 | 
			
		||||
  {
 | 
			
		||||
    plugins: {
 | 
			
		||||
      "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"]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										65
									
								
								package.json
									
									
									
									
									
								
							
							
						
						@@ -1,42 +1,57 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "convertx-frontend",
 | 
			
		||||
  "version": "0.2.0",
 | 
			
		||||
  "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": "npm-run-all '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": "npm-run-all 'lint:*'",
 | 
			
		||||
    "lint:tsc": "tsc --noEmit",
 | 
			
		||||
    "lint:knip": "knip",
 | 
			
		||||
    "lint:eslint": "eslint .",
 | 
			
		||||
    "lint:prettier": "prettier --check ."
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@elysiajs/cookie": "^0.8.0",
 | 
			
		||||
    "@elysiajs/html": "^1.0.2",
 | 
			
		||||
    "@elysiajs/jwt": "^1.0.2",
 | 
			
		||||
    "@elysiajs/static": "^1.0.3",
 | 
			
		||||
    "elysia": "^1.0.24"
 | 
			
		||||
    "@elysiajs/html": "^1.4.0",
 | 
			
		||||
    "@elysiajs/jwt": "^1.4.0",
 | 
			
		||||
    "@elysiajs/static": "^1.4.4",
 | 
			
		||||
    "@kitajs/html": "^4.2.10",
 | 
			
		||||
    "elysia": "^1.4.13",
 | 
			
		||||
    "sanitize-filename": "^1.6.3",
 | 
			
		||||
    "tar": "^7.5.1"
 | 
			
		||||
  },
 | 
			
		||||
  "module": "src/index.tsx",
 | 
			
		||||
  "type": "module",
 | 
			
		||||
  "bun-create": {
 | 
			
		||||
    "start": "bun run src/index.tsx"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@biomejs/biome": "1.8.1",
 | 
			
		||||
    "@ianvs/prettier-plugin-sort-imports": "^4.2.1",
 | 
			
		||||
    "@kitajs/ts-html-plugin": "^4.0.1",
 | 
			
		||||
    "@picocss/pico": "^2.0.6",
 | 
			
		||||
    "@total-typescript/ts-reset": "^0.5.1",
 | 
			
		||||
    "@types/bun": "^1.1.5",
 | 
			
		||||
    "@types/eslint": "^8.56.10",
 | 
			
		||||
    "@types/node": "^20.14.6",
 | 
			
		||||
    "@types/ws": "^8.5.10",
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": "^7.13.1",
 | 
			
		||||
    "@typescript-eslint/parser": "^7.13.1",
 | 
			
		||||
    "cpy-cli": "^5.0.0",
 | 
			
		||||
    "eslint-config-prettier": "^9.1.0",
 | 
			
		||||
    "eslint-plugin-prettier": "^5.1.3",
 | 
			
		||||
    "prettier": "^3.3.2",
 | 
			
		||||
    "typescript": "^5.5.2"
 | 
			
		||||
    "@eslint/js": "^9.38.0",
 | 
			
		||||
    "@kitajs/ts-html-plugin": "^4.1.3",
 | 
			
		||||
    "@tailwindcss/cli": "^4.1.16",
 | 
			
		||||
    "@tailwindcss/postcss": "^4.1.16",
 | 
			
		||||
    "@types/bun": "latest",
 | 
			
		||||
    "@types/node": "^24.9.2",
 | 
			
		||||
    "@typescript-eslint/parser": "^8.46.2",
 | 
			
		||||
    "eslint": "^9.38.0",
 | 
			
		||||
    "eslint-plugin-better-tailwindcss": "^3.7.10",
 | 
			
		||||
    "globals": "^16.4.0",
 | 
			
		||||
    "knip": "^5.66.4",
 | 
			
		||||
    "npm-run-all2": "^8.0.4",
 | 
			
		||||
    "postcss": "^8.5.6",
 | 
			
		||||
    "prettier": "^3.6.2",
 | 
			
		||||
    "tailwind-scrollbar": "^4.0.2",
 | 
			
		||||
    "tailwindcss": "^4.1.16",
 | 
			
		||||
    "typescript": "^5.9.3",
 | 
			
		||||
    "typescript-eslint": "^8.46.2"
 | 
			
		||||
  },
 | 
			
		||||
  "trustedDependencies": [
 | 
			
		||||
    "@biomejs/biome"
 | 
			
		||||
    "@parcel/watcher",
 | 
			
		||||
    "@tailwindcss/oxide",
 | 
			
		||||
    "oxc-resolver"
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5
									
								
								postcss.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,5 @@
 | 
			
		||||
export default {
 | 
			
		||||
  plugins: {
 | 
			
		||||
    "@tailwindcss/postcss": {},
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
@@ -1,13 +0,0 @@
 | 
			
		||||
/**
 | 
			
		||||
 * @type {import('prettier').Config & import("@ianvs/prettier-plugin-sort-imports").PluginConfig}
 | 
			
		||||
 */
 | 
			
		||||
const config = {
 | 
			
		||||
  arrowParens: "always",
 | 
			
		||||
  printWidth: 80,
 | 
			
		||||
  singleQuote: false,
 | 
			
		||||
  semi: true,
 | 
			
		||||
  tabWidth: 2,
 | 
			
		||||
  plugins: ["@ianvs/prettier-plugin-sort-imports"],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default config;
 | 
			
		||||
							
								
								
									
										12
									
								
								prettier.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,12 @@
 | 
			
		||||
/**
 | 
			
		||||
 * @type {import('prettier').Config}
 | 
			
		||||
 */
 | 
			
		||||
const config = {
 | 
			
		||||
  arrowParens: "always",
 | 
			
		||||
  printWidth: 100,
 | 
			
		||||
  singleQuote: false,
 | 
			
		||||
  semi: true,
 | 
			
		||||
  tabWidth: 2,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default config;
 | 
			
		||||
							
								
								
									
										
											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())
 | 
			
		||||
@@ -34,3 +22,17 @@ const refreshData = () => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
refreshData();
 | 
			
		||||
 | 
			
		||||
window.downloadAll = function () {
 | 
			
		||||
  // Get all download links
 | 
			
		||||
  const downloadLinks = document.querySelectorAll("tbody 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 * 300);
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										2
									
								
								public/robots.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,2 @@
 | 
			
		||||
User-agent: *
 | 
			
		||||
Disallow: /
 | 
			
		||||
							
								
								
									
										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();
 | 
			
		||||
							
								
								
									
										9
									
								
								renovate.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,9 @@
 | 
			
		||||
{
 | 
			
		||||
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
 | 
			
		||||
  "extends": ["config:recommended", ":disableDependencyDashboard"],
 | 
			
		||||
  "lockFileMaintenance": {
 | 
			
		||||
    "enabled": true,
 | 
			
		||||
    "automerge": true
 | 
			
		||||
  },
 | 
			
		||||
  "ignoreDeps": ["bun-types", "@types/bun", "bun", "Bun"]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								reset.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1 +0,0 @@
 | 
			
		||||
import "@total-typescript/ts-reset";
 | 
			
		||||
@@ -1,30 +1,43 @@
 | 
			
		||||
export const BaseHtml = ({ children, title = "ConvertX" }) => (
 | 
			
		||||
import { version } from "../../package.json";
 | 
			
		||||
 | 
			
		||||
export const BaseHtml = ({
 | 
			
		||||
  children,
 | 
			
		||||
  title = "ConvertX",
 | 
			
		||||
  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,99 @@
 | 
			
		||||
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 className="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,7 @@ export const properties = {
 | 
			
		||||
      "mpc",
 | 
			
		||||
      "mpc8",
 | 
			
		||||
      "mpeg",
 | 
			
		||||
      "mpegts",
 | 
			
		||||
      "mpegtsraw",
 | 
			
		||||
      "mpegvideo",
 | 
			
		||||
      "mpg",
 | 
			
		||||
      "mpjpeg",
 | 
			
		||||
      "mpl2",
 | 
			
		||||
      "mpo",
 | 
			
		||||
@@ -293,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",
 | 
			
		||||
@@ -322,7 +323,7 @@ export const properties = {
 | 
			
		||||
      "pvf",
 | 
			
		||||
      "qcif",
 | 
			
		||||
      "qcp",
 | 
			
		||||
      "qdraw_pipe",
 | 
			
		||||
      "qdraw",
 | 
			
		||||
      "r3d",
 | 
			
		||||
      "rawvideo",
 | 
			
		||||
      "rco",
 | 
			
		||||
@@ -334,6 +335,7 @@ export const properties = {
 | 
			
		||||
      "rm",
 | 
			
		||||
      "roq",
 | 
			
		||||
      "rpl",
 | 
			
		||||
      "rka",
 | 
			
		||||
      "rsd",
 | 
			
		||||
      "rso",
 | 
			
		||||
      "rt",
 | 
			
		||||
@@ -354,6 +356,7 @@ export const properties = {
 | 
			
		||||
      "sbc",
 | 
			
		||||
      "sbg",
 | 
			
		||||
      "scc",
 | 
			
		||||
      "sdns",
 | 
			
		||||
      "sdp",
 | 
			
		||||
      "sdr2",
 | 
			
		||||
      "sds",
 | 
			
		||||
@@ -363,10 +366,9 @@ export const properties = {
 | 
			
		||||
      "sfx",
 | 
			
		||||
      "sfx2",
 | 
			
		||||
      "sga",
 | 
			
		||||
      "sgi_pipe",
 | 
			
		||||
      "sgi",
 | 
			
		||||
      "shn",
 | 
			
		||||
      "siff",
 | 
			
		||||
      "simbiosis_imx",
 | 
			
		||||
      "sln",
 | 
			
		||||
      "smi",
 | 
			
		||||
      "smjpeg",
 | 
			
		||||
@@ -388,12 +390,9 @@ export const properties = {
 | 
			
		||||
      "stp",
 | 
			
		||||
      "str",
 | 
			
		||||
      "sub",
 | 
			
		||||
      "subviewer",
 | 
			
		||||
      "subviewer1",
 | 
			
		||||
      "sunrast_pipe",
 | 
			
		||||
      "sup",
 | 
			
		||||
      "svag",
 | 
			
		||||
      "svg_pipe",
 | 
			
		||||
      "svg",
 | 
			
		||||
      "svs",
 | 
			
		||||
      "sw",
 | 
			
		||||
      "swf",
 | 
			
		||||
@@ -403,7 +402,8 @@ export const properties = {
 | 
			
		||||
      "thd",
 | 
			
		||||
      "thp",
 | 
			
		||||
      "tiertexseq",
 | 
			
		||||
      "tiff_pipe",
 | 
			
		||||
      "tif",
 | 
			
		||||
      "tiff",
 | 
			
		||||
      "tmv",
 | 
			
		||||
      "truehd",
 | 
			
		||||
      "tta",
 | 
			
		||||
@@ -423,6 +423,7 @@ export const properties = {
 | 
			
		||||
      "ul",
 | 
			
		||||
      "ult",
 | 
			
		||||
      "umx",
 | 
			
		||||
      "usm",
 | 
			
		||||
      "uw",
 | 
			
		||||
      "v",
 | 
			
		||||
      "v210",
 | 
			
		||||
@@ -446,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",
 | 
			
		||||
@@ -463,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",
 | 
			
		||||
@@ -496,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",
 | 
			
		||||
@@ -541,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",
 | 
			
		||||
@@ -612,13 +586,9 @@ export const properties = {
 | 
			
		||||
      "m4a",
 | 
			
		||||
      "m4b",
 | 
			
		||||
      "m4v",
 | 
			
		||||
      "matroska",
 | 
			
		||||
      "md5",
 | 
			
		||||
      "microdvd",
 | 
			
		||||
      "mjpeg",
 | 
			
		||||
      "mjpg",
 | 
			
		||||
      "mkv",
 | 
			
		||||
      "mkvtimestamp_v2",
 | 
			
		||||
      "mlp",
 | 
			
		||||
      "mmf",
 | 
			
		||||
      "mov",
 | 
			
		||||
@@ -628,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",
 | 
			
		||||
@@ -655,14 +616,14 @@ export const properties = {
 | 
			
		||||
      "pfm",
 | 
			
		||||
      "pgm",
 | 
			
		||||
      "pgmyuv",
 | 
			
		||||
      "phm",
 | 
			
		||||
      "pix",
 | 
			
		||||
      "png",
 | 
			
		||||
      "ppm",
 | 
			
		||||
      "psp",
 | 
			
		||||
      "pulse",
 | 
			
		||||
      "qoi",
 | 
			
		||||
      "ra",
 | 
			
		||||
      "ras",
 | 
			
		||||
      "rawvideo",
 | 
			
		||||
      "rco",
 | 
			
		||||
      "rcv",
 | 
			
		||||
      "rgb",
 | 
			
		||||
@@ -670,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",
 | 
			
		||||
@@ -755,12 +679,10 @@ export const properties = {
 | 
			
		||||
      "xbm",
 | 
			
		||||
      "xface",
 | 
			
		||||
      "xml",
 | 
			
		||||
      "xv",
 | 
			
		||||
      "xwd",
 | 
			
		||||
      "y",
 | 
			
		||||
      "y4m",
 | 
			
		||||
      "yuv",
 | 
			
		||||
      "yuv4mpegpipe",
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
@@ -770,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", "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,107 +1,200 @@
 | 
			
		||||
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 convertPdflatex,
 | 
			
		||||
  properties as propertiesPdflatex,
 | 
			
		||||
} from "./pdflatex";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  convert as convertLibjxl,
 | 
			
		||||
  properties as propertiesLibjxl,
 | 
			
		||||
} from "./libjxl";
 | 
			
		||||
 | 
			
		||||
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,
 | 
			
		||||
  },
 | 
			
		||||
  resvg: {
 | 
			
		||||
    properties: propertiesresvg,
 | 
			
		||||
    converter: convertresvg,
 | 
			
		||||
  },
 | 
			
		||||
  vips: {
 | 
			
		||||
    properties: propertiesImage,
 | 
			
		||||
    converter: convertImage,
 | 
			
		||||
  },
 | 
			
		||||
  pdflatex: {
 | 
			
		||||
    properties: propertiesPdflatex,
 | 
			
		||||
    converter: convertPdflatex,
 | 
			
		||||
  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];
 | 
			
		||||
 | 
			
		||||
@@ -111,9 +204,8 @@ export async function mainConverter(
 | 
			
		||||
 | 
			
		||||
      for (const key in converterObj.properties.from) {
 | 
			
		||||
        if (
 | 
			
		||||
          // HOW??
 | 
			
		||||
          converterObj.properties.from[key].includes(fileType) &&
 | 
			
		||||
          converterObj.properties.to[key].includes(convertTo)
 | 
			
		||||
          converterObj?.properties?.from[key]?.includes(fileType) &&
 | 
			
		||||
          converterObj?.properties?.to[key]?.includes(convertTo)
 | 
			
		||||
        ) {
 | 
			
		||||
          converterFunc = converterObj.converter;
 | 
			
		||||
          break;
 | 
			
		||||
@@ -123,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(
 | 
			
		||||
@@ -151,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;
 | 
			
		||||
@@ -170,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] || {};
 | 
			
		||||
@@ -202,11 +289,12 @@ for (const converterName in properties) {
 | 
			
		||||
}
 | 
			
		||||
possibleInputs.sort();
 | 
			
		||||
 | 
			
		||||
export const getPossibleInputs = () => {
 | 
			
		||||
// 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;
 | 
			
		||||
@@ -217,9 +305,9 @@ for (const converterName in properties) {
 | 
			
		||||
 | 
			
		||||
  for (const key in converterProperties.to) {
 | 
			
		||||
    if (allTargets[converterName]) {
 | 
			
		||||
      allTargets[converterName].push(...converterProperties.to[key]);
 | 
			
		||||
      allTargets[converterName].push(...(converterProperties.to[key] || []));
 | 
			
		||||
    } else {
 | 
			
		||||
      allTargets[converterName] = converterProperties.to[key];
 | 
			
		||||
      allTargets[converterName] = converterProperties.to[key] || [];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -228,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;
 | 
			
		||||
 | 
			
		||||
@@ -238,9 +326,9 @@ for (const converterName in properties) {
 | 
			
		||||
 | 
			
		||||
  for (const key in converterProperties.from) {
 | 
			
		||||
    if (allInputs[converterName]) {
 | 
			
		||||
      allInputs[converterName].push(...converterProperties.from[key]);
 | 
			
		||||
      allInputs[converterName].push(...(converterProperties.from[key] || []));
 | 
			
		||||
    } else {
 | 
			
		||||
      allInputs[converterName] = converterProperties.from[key];
 | 
			
		||||
      allInputs[converterName] = converterProperties.from[key] || [];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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,119 +0,0 @@
 | 
			
		||||
import sharp from "sharp";
 | 
			
		||||
import type { FormatEnum } from "sharp";
 | 
			
		||||
 | 
			
		||||
// declare possible conversions
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
    images: [
 | 
			
		||||
      "avif",
 | 
			
		||||
      "bif",
 | 
			
		||||
      "csv",
 | 
			
		||||
      "exr",
 | 
			
		||||
      "fits",
 | 
			
		||||
      "gif",
 | 
			
		||||
      "hdr.gz",
 | 
			
		||||
      "hdr",
 | 
			
		||||
      "heic",
 | 
			
		||||
      "heif",
 | 
			
		||||
      "img.gz",
 | 
			
		||||
      "img",
 | 
			
		||||
      "j2c",
 | 
			
		||||
      "j2k",
 | 
			
		||||
      "jp2",
 | 
			
		||||
      "jpeg",
 | 
			
		||||
      "jpx",
 | 
			
		||||
      "jxl",
 | 
			
		||||
      "mat",
 | 
			
		||||
      "mrxs",
 | 
			
		||||
      "ndpi",
 | 
			
		||||
      "nia.gz",
 | 
			
		||||
      "nia",
 | 
			
		||||
      "nii.gz",
 | 
			
		||||
      "nii",
 | 
			
		||||
      "pdf",
 | 
			
		||||
      "pfm",
 | 
			
		||||
      "pgm",
 | 
			
		||||
      "pic",
 | 
			
		||||
      "png",
 | 
			
		||||
      "ppm",
 | 
			
		||||
      "raw",
 | 
			
		||||
      "scn",
 | 
			
		||||
      "svg",
 | 
			
		||||
      "svs",
 | 
			
		||||
      "svslide",
 | 
			
		||||
      "szi",
 | 
			
		||||
      "tif",
 | 
			
		||||
      "tiff",
 | 
			
		||||
      "v",
 | 
			
		||||
      "vips",
 | 
			
		||||
      "vms",
 | 
			
		||||
      "vmu",
 | 
			
		||||
      "webp",
 | 
			
		||||
      "zip",
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  to: {
 | 
			
		||||
    images: [
 | 
			
		||||
      "avif",
 | 
			
		||||
      "dzi",
 | 
			
		||||
      "fits",
 | 
			
		||||
      "gif",
 | 
			
		||||
      "hdr.gz",
 | 
			
		||||
      "heic",
 | 
			
		||||
      "heif",
 | 
			
		||||
      "img.gz",
 | 
			
		||||
      "j2c",
 | 
			
		||||
      "j2k",
 | 
			
		||||
      "jp2",
 | 
			
		||||
      "jpeg",
 | 
			
		||||
      "jpx",
 | 
			
		||||
      "jxl",
 | 
			
		||||
      "mat",
 | 
			
		||||
      "nia.gz",
 | 
			
		||||
      "nia",
 | 
			
		||||
      "nii.gz",
 | 
			
		||||
      "nii",
 | 
			
		||||
      "png",
 | 
			
		||||
      "tiff",
 | 
			
		||||
      "vips",
 | 
			
		||||
      "webp",
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  options: {
 | 
			
		||||
    svg: {
 | 
			
		||||
      scale: {
 | 
			
		||||
        description: "Scale the image up or down",
 | 
			
		||||
        type: "number",
 | 
			
		||||
        default: 1,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export async function convert(
 | 
			
		||||
  filePath: string,
 | 
			
		||||
  fileType: string,
 | 
			
		||||
  convertTo: keyof FormatEnum,
 | 
			
		||||
  targetPath: string,
 | 
			
		||||
  // biome-ignore lint/suspicious/noExplicitAny: <explanation>
 | 
			
		||||
  options?: any,
 | 
			
		||||
) {
 | 
			
		||||
  if (fileType === "svg") {
 | 
			
		||||
    const scale = options.scale || 1;
 | 
			
		||||
    const metadata = await sharp(filePath).metadata();
 | 
			
		||||
 | 
			
		||||
    if (!metadata || !metadata.width || !metadata.height) {
 | 
			
		||||
      throw new Error("Could not get metadata from image");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const newWidth = Math.round(metadata.width * scale);
 | 
			
		||||
    const newHeight = Math.round(metadata.height * scale);
 | 
			
		||||
 | 
			
		||||
    return await sharp(filePath)
 | 
			
		||||
      .resize(newWidth, newHeight)
 | 
			
		||||
      .toFormat(convertTo)
 | 
			
		||||
      .toFile(targetPath);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return await sharp(filePath).toFormat(convertTo).toFile(targetPath);
 | 
			
		||||
}
 | 
			
		||||
@@ -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,27 +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"];
 | 
			
		||||
 | 
			
		||||
  // Build arguments array
 | 
			
		||||
  const args: string[] = [];
 | 
			
		||||
 | 
			
		||||
  if (xelatex.includes(convertTo)) {
 | 
			
		||||
    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 "${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");
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										38
									
								
								src/converters/resvg.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,38 @@
 | 
			
		||||
import { execFile as execFileOriginal } from "node:child_process";
 | 
			
		||||
import { ExecFileFn } from "./types";
 | 
			
		||||
 | 
			
		||||
export const properties = {
 | 
			
		||||
  from: {
 | 
			
		||||
    images: ["svg"],
 | 
			
		||||
  },
 | 
			
		||||
  to: {
 | 
			
		||||
    images: ["png"],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
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("resvg", [filePath, targetPath], (error, stdout, stderr) => {
 | 
			
		||||
      if (error) {
 | 
			
		||||
        reject(`error: ${error}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (stdout) {
 | 
			
		||||
        console.log(`stdout: ${stdout}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (stderr) {
 | 
			
		||||
        console.error(`stderr: ${stderr}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      resolve("Done");
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								src/converters/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,17 @@
 | 
			
		||||
import { ExecFileOptions } from "child_process";
 | 
			
		||||
 | 
			
		||||
export type ExecFileFn = (
 | 
			
		||||
  cmd: string,
 | 
			
		||||
  args: string[],
 | 
			
		||||
  callback: (err: Error | null, stdout: string, stderr: string) => void,
 | 
			
		||||
  options?: 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;
 | 
			
		||||
@@ -113,9 +114,13 @@ export function convert(
 | 
			
		||||
  //     .toFormat(convertTo)
 | 
			
		||||
  //     .toFile(targetPath);
 | 
			
		||||
  // }
 | 
			
		||||
  let action = "copy";
 | 
			
		||||
  if (fileType === "pdf") {
 | 
			
		||||
    action = "pdfload";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    exec(`vips copy "${filePath}" "${targetPath}"`, (error, stdout, stderr) => {
 | 
			
		||||
    execFile("vips", [action, filePath, targetPath], (error, stdout, stderr) => {
 | 
			
		||||
      if (error) {
 | 
			
		||||
        reject(`error: ${error}`);
 | 
			
		||||
      }
 | 
			
		||||
@@ -128,7 +133,7 @@ export function convert(
 | 
			
		||||
        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(
 | 
			
		||||
      `pdflatex -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:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										186
									
								
								src/helpers/printVersions.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,186 @@
 | 
			
		||||
import { exec } from "node:child_process";
 | 
			
		||||
import { version } from "../../package.json";
 | 
			
		||||
 | 
			
		||||
console.log(`ConvertX v${version}`);
 | 
			
		||||
 | 
			
		||||
if (process.env.NODE_ENV === "production") {
 | 
			
		||||
  exec("cat /etc/os-release", (error, stdout) => {
 | 
			
		||||
    if (error) {
 | 
			
		||||
      console.error("Not running on docker, this is not supported.");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (stdout) {
 | 
			
		||||
      console.log(stdout.split('PRETTY_NAME="')[1]?.split('"')[0]);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  exec("pandoc -v", (error, stdout) => {
 | 
			
		||||
    if (error) {
 | 
			
		||||
      console.error("Pandoc is not installed.");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (stdout) {
 | 
			
		||||
      console.log(stdout.split("\n")[0]);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  exec("ffmpeg -version", (error, stdout) => {
 | 
			
		||||
    if (error) {
 | 
			
		||||
      console.error("FFmpeg is not installed.");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (stdout) {
 | 
			
		||||
      console.log(stdout.split("\n")[0]);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  exec("vips -v", (error, stdout) => {
 | 
			
		||||
    if (error) {
 | 
			
		||||
      console.error("Vips is not installed.");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (stdout) {
 | 
			
		||||
      console.log(stdout.split("\n")[0]);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  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.");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (stdout) {
 | 
			
		||||
      console.log(stdout.split("\n")[0]);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  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.");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (stdout) {
 | 
			
		||||
      console.log(stdout.split("\n")[0]);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  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.");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (stdout) {
 | 
			
		||||
      console.log(stdout.split("\n")[0]);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  exec("resvg -V", (error, stdout) => {
 | 
			
		||||
    if (error) {
 | 
			
		||||
      console.error("resvg is not installed");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (stdout) {
 | 
			
		||||
      console.log(`resvg v${stdout.split("\n")[0]}`);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  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");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (stdout) {
 | 
			
		||||
      console.log(`Bun v${stdout.split("\n")[0]}`);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										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;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										18
									
								
								src/icons/delete.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,18 @@
 | 
			
		||||
export function DeleteIcon() {
 | 
			
		||||
  return (
 | 
			
		||||
    <svg
 | 
			
		||||
      xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
      fill="none"
 | 
			
		||||
      viewBox="0 0 24 24"
 | 
			
		||||
      strokeWidth="1.5"
 | 
			
		||||
      stroke="currentColor"
 | 
			
		||||
      class={`size-6`}
 | 
			
		||||
    >
 | 
			
		||||
      <path
 | 
			
		||||
        strokeLinecap="round"
 | 
			
		||||
        strokeLinejoin="round"
 | 
			
		||||
        d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
 | 
			
		||||
      />
 | 
			
		||||
    </svg>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										18
									
								
								src/icons/download.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,18 @@
 | 
			
		||||
export function DownloadIcon() {
 | 
			
		||||
  return (
 | 
			
		||||
    <svg
 | 
			
		||||
      xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
      fill="none"
 | 
			
		||||
      viewBox="0 0 24 24"
 | 
			
		||||
      strokeWidth="1.5"
 | 
			
		||||
      stroke="currentColor"
 | 
			
		||||
      class={`size-6`}
 | 
			
		||||
    >
 | 
			
		||||
      <path
 | 
			
		||||
        strokeLinecap="round"
 | 
			
		||||
        strokeLinejoin="round"
 | 
			
		||||
        d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3"
 | 
			
		||||
      />
 | 
			
		||||
    </svg>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										19
									
								
								src/icons/eye.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,19 @@
 | 
			
		||||
export function EyeIcon() {
 | 
			
		||||
  return (
 | 
			
		||||
    <svg
 | 
			
		||||
      xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
      fill="none"
 | 
			
		||||
      viewBox="0 0 24 24"
 | 
			
		||||
      strokeWidth={1.5}
 | 
			
		||||
      stroke="currentColor"
 | 
			
		||||
      class={`size-6`}
 | 
			
		||||
    >
 | 
			
		||||
      <path
 | 
			
		||||
        strokeLinecap="round"
 | 
			
		||||
        strokeLinejoin="round"
 | 
			
		||||
        d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z"
 | 
			
		||||
      />
 | 
			
		||||
      <path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
 | 
			
		||||
    </svg>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1123
									
								
								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;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										66
									
								
								src/pages/chooseConverter.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,66 @@
 | 
			
		||||
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() }) },
 | 
			
		||||
);
 | 
			
		||||
							
								
								
									
										94
									
								
								src/pages/convert.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,94 @@
 | 
			
		||||
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(),
 | 
			
		||||
    }),
 | 
			
		||||
    auth: true,
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
							
								
								
									
										32
									
								
								src/pages/deleteFile.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,32 @@
 | 
			
		||||
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, cookie: { jobId }, user }) => {
 | 
			
		||||
    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() }), auth: true },
 | 
			
		||||
);
 | 
			
		||||
							
								
								
									
										38
									
								
								src/pages/deleteJob.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,38 @@
 | 
			
		||||
import { rmSync } from "node:fs";
 | 
			
		||||
import { Elysia } from "elysia";
 | 
			
		||||
import { outputDir, uploadsDir } from "..";
 | 
			
		||||
import db from "../db/db";
 | 
			
		||||
import { WEBROOT } from "../helpers/env";
 | 
			
		||||
import { userService } from "./user";
 | 
			
		||||
import { Jobs } from "../db/types";
 | 
			
		||||
 | 
			
		||||
export const deleteJob = new Elysia().use(userService).get(
 | 
			
		||||
  "/delete/:userId/:jobId",
 | 
			
		||||
  async ({ params, redirect, user }) => {
 | 
			
		||||
    const job = db
 | 
			
		||||
      .query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
 | 
			
		||||
      .as(Jobs)
 | 
			
		||||
      .get(user.id, params.jobId);
 | 
			
		||||
 | 
			
		||||
    if (!job) {
 | 
			
		||||
      return redirect(`${WEBROOT}/results`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // delete the directories
 | 
			
		||||
    rmSync(`${outputDir}${job.user_id}/${job.id}`, {
 | 
			
		||||
      recursive: true,
 | 
			
		||||
      force: true,
 | 
			
		||||
    });
 | 
			
		||||
    rmSync(`${uploadsDir}${job.user_id}/${job.id}`, {
 | 
			
		||||
      recursive: true,
 | 
			
		||||
      force: true,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // delete the job
 | 
			
		||||
    db.query("DELETE FROM jobs WHERE id = ?").run(job.id);
 | 
			
		||||
    return redirect(`${WEBROOT}/history`, 302);
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    auth: true,
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
							
								
								
									
										65
									
								
								src/pages/download.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,65 @@
 | 
			
		||||
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, redirect, user }) => {
 | 
			
		||||
      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);
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      auth: true,
 | 
			
		||||
    },
 | 
			
		||||
  )
 | 
			
		||||
  .get(
 | 
			
		||||
    "/archive/:userId/:jobId",
 | 
			
		||||
    async ({ params, redirect, user }) => {
 | 
			
		||||
      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);
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      auth: true,
 | 
			
		||||
    },
 | 
			
		||||
  );
 | 
			
		||||
							
								
								
									
										12
									
								
								src/pages/healthcheck.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,12 @@
 | 
			
		||||
import Elysia from "elysia";
 | 
			
		||||
import { userService } from "./user";
 | 
			
		||||
 | 
			
		||||
export const healthcheck = new Elysia().use(userService).get(
 | 
			
		||||
  "/healthcheck",
 | 
			
		||||
  () => {
 | 
			
		||||
    return { status: "ok" };
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    auth: false,
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
							
								
								
									
										214
									
								
								src/pages/history.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,214 @@
 | 
			
		||||
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 ({ redirect, user }) => {
 | 
			
		||||
    if (HIDE_HISTORY) {
 | 
			
		||||
      return redirect(`${WEBROOT}/`, 302);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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>
 | 
			
		||||
    );
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    auth: true,
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
							
								
								
									
										74
									
								
								src/pages/listConverters.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,74 @@
 | 
			
		||||
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 () => {
 | 
			
		||||
    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>
 | 
			
		||||
    );
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    auth: true,
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
							
								
								
									
										214
									
								
								src/pages/results.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,214 @@
 | 
			
		||||
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 { DownloadIcon } from "../icons/download";
 | 
			
		||||
import { DeleteIcon } from "../icons/delete";
 | 
			
		||||
import { EyeIcon } from "../icons/eye";
 | 
			
		||||
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 class="flex flex-row gap-4">
 | 
			
		||||
          <a
 | 
			
		||||
            style={files.length !== job.num_files ? "pointer-events: none;" : ""}
 | 
			
		||||
            href={`${WEBROOT}/archive/${user.id}/${job.id}`}
 | 
			
		||||
            download={`converted_files_${job.id}.tar`}
 | 
			
		||||
            class="flex btn-primary flex-row gap-2 text-contrast"
 | 
			
		||||
            {...(files.length !== job.num_files ? { disabled: true, "aria-busy": "true" } : "")}
 | 
			
		||||
          >
 | 
			
		||||
            <DownloadIcon /> <p>Tar</p>
 | 
			
		||||
          </a>
 | 
			
		||||
          <button class="flex btn-primary flex-row gap-2 text-contrast" onclick="downloadAll()">
 | 
			
		||||
            <DownloadIcon /> <p>All</p>
 | 
			
		||||
          </button>
 | 
			
		||||
          <a
 | 
			
		||||
            style={files.length !== job.num_files ? "pointer-events: none;" : ""}
 | 
			
		||||
            class="flex btn-primary flex-row gap-2 text-contrast"
 | 
			
		||||
            href={`${WEBROOT}/delete/${user.id}/${job.id}`}
 | 
			
		||||
            {...(files.length !== job.num_files ? { disabled: true, "aria-busy": "true" } : "")}
 | 
			
		||||
          >
 | 
			
		||||
            <DeleteIcon /> <p>Delete</p>
 | 
			
		||||
          </a>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <progress
 | 
			
		||||
        max={job.num_files}
 | 
			
		||||
        {...(files.length === 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
 | 
			
		||||
              `}
 | 
			
		||||
            >
 | 
			
		||||
              Actions
 | 
			
		||||
            </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 class="flex flex-row gap-4">
 | 
			
		||||
                <a
 | 
			
		||||
                  class={`
 | 
			
		||||
                    text-accent-500 underline
 | 
			
		||||
                    hover:text-accent-400
 | 
			
		||||
                  `}
 | 
			
		||||
                  href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
 | 
			
		||||
                >
 | 
			
		||||
                  <EyeIcon />
 | 
			
		||||
                </a>
 | 
			
		||||
                <a
 | 
			
		||||
                  class={`
 | 
			
		||||
                    text-accent-500 underline
 | 
			
		||||
                    hover:text-accent-400
 | 
			
		||||
                  `}
 | 
			
		||||
                  href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
 | 
			
		||||
                  download={file.output_file_name}
 | 
			
		||||
                >
 | 
			
		||||
                  <DownloadIcon />
 | 
			
		||||
                </a>
 | 
			
		||||
              </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          ))}
 | 
			
		||||
        </tbody>
 | 
			
		||||
      </table>
 | 
			
		||||
    </article>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const results = new Elysia()
 | 
			
		||||
  .use(userService)
 | 
			
		||||
  .get(
 | 
			
		||||
    "/results/:jobId",
 | 
			
		||||
    async ({ params, set, cookie: { job_id }, user }) => {
 | 
			
		||||
      if (job_id?.value) {
 | 
			
		||||
        // Clear the job_id cookie since we are viewing the results
 | 
			
		||||
        job_id.remove();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      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>
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    { auth: true },
 | 
			
		||||
  )
 | 
			
		||||
  .post(
 | 
			
		||||
    "/progress/:jobId",
 | 
			
		||||
    async ({ set, params, cookie: { job_id }, user }) => {
 | 
			
		||||
      if (job_id?.value) {
 | 
			
		||||
        // Clear the job_id cookie since we are viewing the results
 | 
			
		||||
        job_id.remove();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      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} />;
 | 
			
		||||
    },
 | 
			
		||||
    { auth: true },
 | 
			
		||||
  );
 | 
			
		||||
							
								
								
									
										249
									
								
								src/pages/root.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,249 @@
 | 
			
		||||
import { randomInt } from "node:crypto";
 | 
			
		||||
import { JWTPayloadSpec } from "@elysiajs/jwt";
 | 
			
		||||
import { Elysia, t } 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>
 | 
			
		||||
    );
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    cookie: t.Cookie({
 | 
			
		||||
      auth: t.Optional(t.String()),
 | 
			
		||||
      jobId: t.Optional(t.String()),
 | 
			
		||||
    }),
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
							
								
								
									
										39
									
								
								src/pages/upload.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,39 @@
 | 
			
		||||
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, user, cookie: { jobId } }) => {
 | 
			
		||||
    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() }), auth: true },
 | 
			
		||||
);
 | 
			
		||||
							
								
								
									
										522
									
								
								src/pages/user.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,522 @@
 | 
			
		||||
import { randomUUID } from "node:crypto";
 | 
			
		||||
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(),
 | 
			
		||||
    }),
 | 
			
		||||
    session: t.Cookie({
 | 
			
		||||
      auth: t.String(),
 | 
			
		||||
      jobId: t.Optional(t.String()),
 | 
			
		||||
    }),
 | 
			
		||||
    optionalSession: t.Cookie({
 | 
			
		||||
      auth: t.Optional(t.String()),
 | 
			
		||||
      jobId: t.Optional(t.String()),
 | 
			
		||||
    }),
 | 
			
		||||
  })
 | 
			
		||||
  .macro("auth", {
 | 
			
		||||
    cookie: "session",
 | 
			
		||||
    async resolve({ status, jwt, cookie: { auth } }) {
 | 
			
		||||
      if (!auth.value) {
 | 
			
		||||
        return status(401, {
 | 
			
		||||
          success: false,
 | 
			
		||||
          message: "Unauthorized",
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      const user = await jwt.verify(auth.value);
 | 
			
		||||
      if (!user) {
 | 
			
		||||
        return status(401, {
 | 
			
		||||
          success: false,
 | 
			
		||||
          message: "Unauthorized",
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      return {
 | 
			
		||||
        success: true,
 | 
			
		||||
        user,
 | 
			
		||||
      };
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
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>
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    { body: "signIn", cookie: "optionalSession" },
 | 
			
		||||
  )
 | 
			
		||||
  .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 ({ user, redirect }) => {
 | 
			
		||||
      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>
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      auth: true,
 | 
			
		||||
    },
 | 
			
		||||
  )
 | 
			
		||||
  .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(),
 | 
			
		||||
      }),
 | 
			
		||||
      cookie: "session",
 | 
			
		||||
    },
 | 
			
		||||
  );
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 8.1 KiB  | 
| 
		 Before Width: | Height: | Size: 23 KiB  | 
| 
		 Before Width: | Height: | Size: 7.3 KiB  |