Compare commits

...

600 commits
v0.3.3 ... main

Author SHA1 Message Date
43e829309a Update README.md
Some checks failed
Check Lint / Run linting checks (push) Failing after 6s
Update Docker Hub Description / dockerHubDescription (push) Failing after 1s
Check Tests / Run tests (push) Failing after 2s
Docker / Build Docker image for linux/amd64 (push) Has been cancelled
Docker / Build Docker image for linux/arm64 (push) Has been cancelled
Docker / Merge Docker manifests (push) Has been cancelled
2026-07-05 20:13:28 -07:00
Emrik Östling
393441faa1 chore: fix release config 2026-06-21 22:40:20 +02:00
Emrik Östling
cc4de7625d chore: refresh lockfile 2026-06-21 22:40:09 +02:00
Emrik Östling
174877cd88 ci: remove bump version action 2026-06-21 22:39:58 +02:00
Emrik Östling
eceb29c22d chore: prepare for 0.18.0 2026-06-21 22:29:15 +02:00
Emrik Östling
4bdb643ed4 chore(deps): bump deps 2026-06-21 22:26:09 +02:00
Emrik Östling
e125326720
Merge commit from fork
Co-authored-by: Ayushi Rathore <ayushirathore@Ayushis-MacBook-Air.local>
2026-06-21 21:04:29 +02:00
Fluxmux
0965928949
security: fix path traversal vulnerability in conversion API (#532) 2026-04-27 21:43:47 +02:00
Dante Barbieri
1ba82cf1b2
fix(assimp): pass -f<format> so non-extension targets work (#557)
* fix(assimp): pass -f<format> so non-extension targets work

The assimp converter invokes `assimp export <in> <out>` and relies on
assimp's extension-based format inference to pick the output format.
That works for targets whose id happens to match a real file extension
(glb, gltf, obj, stl, ply, etc.) but silently fails for every target
whose assimp id is not a conventional filename extension:

  glb2, gltf2, objnomtl, stlb, plyb, fbxa, assbin, assxml, pbrt, assjson

Picking any of those in the UI aborts the job with:

  assimp export: no output format specified and I failed to guess it

assimp_cmd's `-f<h>` flag overrides extension inference, and the values
in `properties.to.object` already correspond 1:1 to assimp's format ids
(verified against `assimp listexport`). Passing `-f${convertTo}` makes
every advertised target work without changing anything else.

Reproduction: upload any .stl, convert to glb2. Before: ENOENT on the
nonexistent output file. After: valid binary glTF 2.0.

* fix(assimp): write real file extensions for non-extension format ids

Follow-up to the previous commit. With `-f<format>` now passed to
assimp, every advertised target exports successfully — but seven of
them still produce files with assimp's internal format id as the
extension, which no third-party viewer recognises:

  glb2 → .glb2   (really binary glTF 2.0, should be .glb)
  gltf2 → .gltf2 (text glTF 2.0, should be .gltf)
  objnomtl → .objnomtl   (OBJ without .mtl, should be .obj)
  stlb → .stlb   (binary STL, should be .stl)
  plyb → .plyb   (binary PLY, should be .ply)
  fbxa → .fbxa   (ASCII FBX, should be .fbx)
  assjson → .assjson   (JSON dump, should be .json)

Map these in `normalizeOutputFiletype`, the same helper that already
handles jpeg→jpg, latex→tex, markdown→md. The format id on `convertTo`
is untouched, so `assimp -f<id>` still selects the correct
encoding/variant — only the output filename changes.

assbin, assxml, and pbrt are both the format id and the canonical
extension, so they need no mapping.

* style(assimp): fix prettier formatting
2026-04-22 22:28:52 +02:00
Emrik Östling
70fcc8413f
chore: downgrade elysia to 1.4.22 (#551)
* chore: downgrade elysia to 1.4.22

* chore: add  ca-certificates
2026-03-28 12:25:14 +01:00
renovate[bot]
bee3f6302b
chore(deps): update docker/login-action action to v4 (#545)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-04 15:41:11 +01:00
renovate[bot]
0d201261c8
chore(deps): lock file maintenance (#542)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 08:38:02 +01:00
renovate[bot]
f1439faf12
chore(deps): update github artifact actions (#540)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-28 21:39:32 +01:00
renovate[bot]
9cbd233dd6
chore(deps): lock file maintenance (#539)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-23 08:44:58 +01:00
renovate[bot]
7c93846afa
chore(deps): lock file maintenance (#537)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-17 23:09:12 +01:00
Emrik Östling
b013b4d0bc
feat: update to debian testing (forky) (#534) 2026-02-15 12:49:11 +01:00
Giulio Librando
d291f049b8
fix: allow dynamic server port via environment variable (#530) 2026-02-11 18:06:23 +01:00
Emrik Östling
8aa23c75aa
Potential fix for code scanning alert no. 4: Workflow does not contain permissions (#527) 2026-02-11 17:35:23 +01:00
renovate[bot]
0483392f7e
chore(deps): lock file maintenance (#533)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-09 08:17:45 +01:00
renovate[bot]
debbe5d1d4
chore(deps): update eslint monorepo to v10 (#531)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-07 12:09:41 +01:00
renovate[bot]
cd32144bce
chore(deps): lock file maintenance (#528)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-02 08:54:23 +01:00
Emrik Östling
f747a5b94b
chore: update eslint config (#521) 2026-01-30 19:10:03 +01:00
Toni Ros
52af8d5824
PDF to DOCX using LibreOffice, fixes #425 (#510)
* Fix to issue #425

* Fix to Fix error in previous fix, and adapt tests

* Fix to Fix error in previous fix, and adapt tests plus prettier

* Update tests/converters/libreoffice.test.ts

Thanks

Co-authored-by: Emrik Östling <emrik.ostling@gmail.com>

* Update src/converters/libreoffice.ts

Thanks

Co-authored-by: Emrik Östling <emrik.ostling@gmail.com>

* Update src/converters/libreoffice.ts

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: Emrik Östling <emrik.ostling@gmail.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-01-29 17:20:12 +01:00
Emrik Östling
8129ecdc39
chore: update deps (#522) 2026-01-26 20:48:26 +01:00
renovate[bot]
18f2aa8562
chore(deps): update dependency eslint-plugin-better-tailwindcss to v4 (#506)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Emrik Östling <emrik.ostling@gmail.com>
2026-01-26 19:44:19 +01:00
renovate[bot]
081f62de95
chore(deps): lock file maintenance (#519)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-26 07:52:27 +01:00
renovate[bot]
1032311299
chore(deps): lock file maintenance (#512)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-19 09:24:59 +01:00
Emrik Östling
6c812b44bb chore: prepare for 0.17.0 2026-01-13 20:38:35 +01:00
Emrik Östling
7a936bdc04
Merge commit from fork 2026-01-13 20:34:21 +01:00
renovate[bot]
7ec6b66da8
chore(deps): lock file maintenance (#501)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-12 17:22:22 +01:00
Emrik Östling
bb751163ba chore: add comment to dev compose file 2026-01-11 17:46:56 +01:00
kunal763
c3f17cc5a7
feat: add VCF to CSV converter (#497) 2026-01-11 16:38:32 +01:00
Mehul Anshumali
60f6f5b77f
fix(docker): add python3-tinycss2 for Inkscape DXF export (#498)
Co-authored-by: Mehul Anshumali <mehul.a@phonepe.com>
2026-01-11 16:18:46 +01:00
Emrik Östling
bbf6f6ba52
Fix Polish tutorial link in README.md 2026-01-08 21:53:34 +01:00
Emrik Östling
9802519e45
Add Polish tutorial link to README 2026-01-07 20:41:04 +01:00
renovate[bot]
2b40443885
chore(deps): lock file maintenance (#493)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-05 10:41:35 +01:00
renovate[bot]
28e5ed4c03
chore(deps): update dependency globals to v17 (#490)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-01 22:52:29 +00:00
renovate[bot]
c1ca9c770a
chore(deps): lock file maintenance (#489)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-31 13:43:44 +01:00
Sahil sharma
f2a92aaf39
feat: markitdown implementation (#486)
* feat: markitdown implementation

* fix: code review and docker file:

* fix: add markitdown PATH in container

* fix: feedback changes

* en: readme changed
2025-12-27 12:59:17 +05:30
Namit
8af8e59b4f
feat: bulk job deletion with multi-select UI (#481)
Closes #445
2025-12-27 00:24:39 +01:00
lif
9ac5e7569b
Fix timezone display in history page (#479) 2025-12-23 02:29:34 +01:00
renovate[bot]
955ea5c014
chore(deps): lock file maintenance (#484)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-23 02:12:09 +01:00
Geek Squirrel
5d70df424e
add .wma file as input (#485) 2025-12-23 02:11:07 +01:00
renovate[bot]
911587ea68
chore(deps): lock file maintenance (#477)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-15 08:37:32 +01:00
Emrik Östling
3d84925e1b
Potential fix for code scanning alert no. 2: Workflow does not contain permissions (#476)
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-12-14 20:46:01 +01:00
Emrik Östling
ab352987d5
Potential fix for code scanning alert no. 1: Workflow does not contain permissions (#474)
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-12-14 20:33:27 +01:00
Emrik Östling
689e66e3f9
Potential fix for code scanning alert no. 3: Workflow does not contain permissions (#475)
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-12-14 20:33:13 +01:00
Emrik Östling
4598745956
Potential fix for code scanning alert no. 6: Unnecessary use of cat process (#473)
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-12-14 20:32:56 +01:00
Emrik Östling
af199ab8bb
Potential fix for code scanning alert no. 5: Workflow does not contain permissions (#472)
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-12-14 20:30:31 +01:00
Emrik Östling
20b321c308 chore: update readme with hw accel link 2025-12-14 19:32:08 +01:00
Laertes87
8f898be20e
Merge pull request #462 from Laertes87/AddMoreUnitTests
test: add more unit tests
2025-12-14 14:38:14 +01:00
Emrik Östling
df3330fdc2
fix: add FFMPEG_OUTPUT_ARGS (#470) 2025-12-14 14:12:21 +01:00
Emrik Östling
12a5580694
fix: ffmpeg args (#469) 2025-12-14 13:43:20 +01:00
Emrik Östling
eabd6e6a7f
chore: update version number (#468) 2025-12-14 11:43:38 +01:00
Jörg Krzeslak
0521af0d52 test: fix code style issue 2025-12-14 02:57:00 +01:00
Jörg Krzeslak
826dc1062a test: add awaits to rejects and resolves 2025-12-14 02:57:00 +01:00
Jörg Krzeslak
84d7da0195 test: add tests for dasel, pandoc and vtracer 2025-12-14 02:57:00 +01:00
Jörg Krzeslak
ad000f35ba test: add libreoffice.test.ts 2025-12-14 02:57:00 +01:00
renovate[bot]
2f965035b6
chore(deps): update github artifact actions (#467)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-13 12:26:28 +01:00
renovate[bot]
e3dc013b20
chore(deps): lock file maintenance (#461)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-08 15:51:01 +01:00
renovate[bot]
a59a9fdeec
chore(deps): update dependency node to v24 (#458)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-03 21:29:56 +01:00
renovate[bot]
f5bc47b532
chore(deps): update ramonpaolo/bump-version action to v2.4.0 (#457)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-03 21:29:32 +01:00
Emrik Östling
bef07a787e
fix: Version wasn't bumped for v0.16.0 (#456) 2025-12-03 21:16:05 +01:00
Emrik Östling
5268838975 chore: fix format 2025-12-01 22:31:10 +01:00
Emrik Östling
550f472451
Merge commit from fork 2025-12-01 22:26:56 +01:00
renovate[bot]
d165f11798
chore(deps): lock file maintenance (#453)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 16:58:33 +01:00
renovate[bot]
4ae2aab66a
chore(deps): update actions/checkout action to v6 (#446)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 21:54:32 +01:00
renovate[bot]
2be0b13d42
chore(deps): lock file maintenance (#448)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 21:54:05 +01:00
Emrik Östling
74ebf5e4dc
fix: getPossibleTargets select correct targets (#441) 2025-11-23 19:56:57 +01:00
renovate[bot]
128b118382
chore(deps): lock file maintenance (#443)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-17 09:14:30 +01:00
Emrik Östling
64264a41d9
feat: add delete button in history (#440) 2025-11-16 00:34:49 +01:00
Emrik Östling
0096a6f197
chore: fix typescript-eslint (#439) 2025-11-15 20:15:02 +01:00
Emrik Östling
53c1a54df5
workaround for #435 (#438) 2025-11-14 19:28:40 +01:00
Rdeisenroth
b5c20778f9
fix: ensure data dir exists before loading db (#433)
* fix: ensure data dir exists before loading db

* Update src/db/db.ts

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

---------

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-11-07 09:26:19 +01:00
Emrik Östling
9a8d7a36b9
chore: add repobeats
Add Repobeats analytics image to README
2025-11-02 14:14:13 +01:00
Emrik Östling
80b55657a4
feat: add healthcheck endpoint (#431) 2025-11-02 13:58:57 +01:00
C4illin
32d1c567b9 chore: update lockfile 2025-11-02 13:44:28 +01:00
C4illin
152bbd1b78 chore: update packages 2025-10-29 21:12:57 +01:00
Param Siddharth
4d65cc7228
feat: add button and API endpoint to delete job (#423) 2025-10-29 20:35:43 +01:00
renovate[bot]
1a4e2dec7c
chore(deps): update github artifact actions (#426)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-29 18:13:57 +01:00
C4illin
061e63e705 chore: add bun to renovate ignore 2025-10-07 20:56:55 +00:00
C4illin
80142ff269 chore: fix lint 2025-10-07 20:34:55 +00:00
Emrik Östling
8b015f9045
chore(main): update changelog 2025-10-07 22:33:00 +02:00
C4illin
0159ac0481 chore: change version number 2025-10-07 20:27:38 +00:00
Emrik Östling
a677a5eec1
ci: add exception for collaborators (#418) 2025-10-07 22:26:15 +02:00
C4illin
0ac585c9da chore: correct spacing 2025-10-07 20:11:44 +00:00
Emrik Östling
4f61577421
ci: remove release please (#416) 2025-10-07 21:58:11 +02:00
Emrik Östling
3e7e95b53c
feature: add download all file by file alongside the tar download (#415) 2025-10-07 21:27:31 +02:00
C4illin
27e9b7eb35 chore: update with new converters 2025-10-05 20:46:07 +00:00
Emrik Östling
add3a6229e
Merge pull request #413 from C4illin/devcontainer 2025-10-05 17:14:42 +02:00
C4illin
2e406cac3d chore: add bun extension 2025-10-05 15:11:41 +00:00
C4illin
6fa6978f30 chore: fix lint 2025-10-05 15:09:51 +00:00
C4illin
7691594b10 chore: fix formatting 2025-10-05 14:54:19 +00:00
C4illin
17325a6e6d chore: fix lint 2025-10-05 14:19:13 +00:00
C4illin
d984891791 chore: add devcontainer 2025-10-05 13:57:54 +00:00
C4illin
78bac9c9ca chore: update packages 2025-10-05 13:56:47 +00:00
C4illin
126e60fec7 chore: remove gif from vips 2025-10-05 14:48:57 +02:00
C4illin
7a59070e91 Merge branch 'main' of https://github.com/C4illin/ConvertX 2025-10-02 17:30:02 +02:00
C4illin
c9b65c7652 chore: use auth macro instead of checking it on every path 2025-10-02 17:29:58 +02:00
C4illin
13d9ce09a4 chore: update packages 2025-10-02 17:29:07 +02:00
Emrik Östling
f4fc053db3
Merge pull request #412 from C4illin/renovate/peter-evans-dockerhub-description-5.x
chore(deps): update peter-evans/dockerhub-description action to v5
2025-10-01 23:16:07 +02:00
renovate[bot]
d3fc6a237f
chore(deps): update peter-evans/dockerhub-description action to v5 2025-10-01 20:30:22 +00:00
C4illin
8a888ccda6 fix: missing public files
issue: #314
2025-09-28 16:16:38 +02:00
Emrik Östling
a4e20aa62a
Merge pull request #402 from ben-burwood/dasel 2025-09-09 20:10:00 +02:00
Ben Burwood
1cc4862d51 Change require to import for FS and Remove Test 2025-09-09 18:54:29 +01:00
Ben Burwood
c6b64ced91 Formatting 2025-09-09 09:17:39 +01:00
Ben Burwood
38cbf093e5 Update README.md 2025-09-09 00:32:47 +01:00
Ben Burwood
d8ddfa31e3 Update dasel.test.ts 2025-09-09 00:32:09 +01:00
Ben Burwood
c3e4f676fc Add Dasel Converter 2025-09-09 00:26:40 +01:00
Ben Burwood
e668b828ea Add Dasel VersionCheck 2025-09-09 00:18:26 +01:00
Ben Burwood
eaeac2cb78 Add Fake Dasel Test 2025-09-09 00:12:29 +01:00
Ben Burwood
5d74ec59c1 Install Dasel in Dockerfile 2025-09-08 23:52:13 +01:00
Ben Burwood
645b29e5d1 Add Dasel to Readme 2025-09-08 23:51:50 +01:00
Emrik Östling
aee9677029
Merge pull request #394 from r0gueSch0lar/main
Adding latexmk to dockerfile install packages, convertx errors otherw…
2025-08-25 16:13:29 +02:00
foo
da982dc831 Adding latexmk to dockerfile install packages, convertx errors otherwise complaining about latexmk not found in path when converting latex files with xetex 2025-08-25 15:32:05 +10:00
Emrik Östling
a64eaa3fc0
Merge pull request #391 from C4illin/copilot/fix-68d2749d-16f6-4f00-8ba6-58b411cc0c98 2025-08-21 15:56:07 +02:00
copilot-swe-agent[bot]
a63651f715 Fix linting issues: configure knip for test files and fix prettier formatting
Co-authored-by: C4illin <20753603+C4illin@users.noreply.github.com>
2025-08-21 13:34:04 +00:00
copilot-swe-agent[bot]
bb67b708e7 Initial plan 2025-08-21 13:25:41 +00:00
C4illin
dc0d37c71e chore: upgrade bun-types due to bug 2025-08-13 23:48:05 +02:00
C4illin
0287c4d458 chore: include tests in knip 2025-08-13 23:44:35 +02:00
C4illin
47be1061b7 chore: add bun version files 2025-08-13 23:35:07 +02:00
C4illin
1c79de2f37 ci: add bun test workflow 2025-08-13 20:52:55 +02:00
C4illin
9696cc7188 chore: format files 2025-08-13 20:51:41 +02:00
Emrik Östling
082dd8c1f2
Merge pull request #388 from SAHIL-Sharma21/feat/Vtracer_implementation
Feat: add VTracer converter for raster to vector conversion
2025-08-13 18:37:35 +02:00
Sahil
43524dcdb1 Refactor and fix: clean up dockerfile and format done with fix in types 2025-08-13 18:26:17 +05:30
Sahil
45a0540edf vtracer: bug fix in vtracer.ts file and code refactor 2025-08-13 17:55:45 +05:30
Sahil
2b784d1edc dockerfile: removing line spaces and adding original comments 2025-08-13 17:14:07 +05:30
Sahil
8650cf9a63 docs: add Vtracer to converter table 2025-08-13 17:07:35 +05:30
Sahil
76c840dbaa feat: vtracer implemented and added docker file binaries install 2025-08-12 22:26:39 +05:30
Sahil
e78de6f6de vtracer implementation in converter directory 2025-08-12 20:53:23 +05:30
Emrik Östling
554edf5a27
Merge pull request #373 from Laertes87/AddUnitTests
test: add unit tests for converters
2025-08-11 22:35:57 +02:00
Emrik Östling
6fca398a12
Merge pull request #387 from C4illin/renovate/actions-checkout-5.x
chore(deps): update actions/checkout action to v5
2025-08-11 22:22:14 +02:00
renovate[bot]
5bf3fbc10e
chore(deps): update actions/checkout action to v5 2025-08-11 13:42:58 +00:00
Jörg Krzeslak
d994c38219 test: fix imports after eslint config changes 2025-08-11 15:09:20 +02:00
Jörg Krzeslak
3dccbfc797 test: add tests directory to tsconfig 2025-08-11 14:24:28 +02:00
Emrik Östling
c3d461f102
Merge branch 'main' into AddUnitTests 2025-08-11 14:10:42 +02:00
Jörg Krzeslak
c0105889ab test: extract duplicate code into another helper 2025-08-11 13:16:31 +02:00
Jörg Krzeslak
c6006b58d2 test: add test case to libjxl.test.ts when input and output are not jxl 2025-08-11 13:16:23 +02:00
Jörg Krzeslak
178f009458 test: extend fail test with different error messages 2025-08-11 13:16:17 +02:00
Jörg Krzeslak
9c24cf4aba test: add test case to ffmpeg.test.ts 2025-08-11 13:16:11 +02:00
Jörg Krzeslak
af68498494 test: change order of parameters in ExecFileFn type 2025-08-11 13:16:05 +02:00
Jörg Krzeslak
eac22d53d3 test: prettify test for msgconvert.ts 2025-08-11 13:15:59 +02:00
Jörg Krzeslak
e5ac60c187 test: add unit test for msgconvert.ts 2025-08-11 13:15:52 +02:00
Jörg Krzeslak
4b42a5fbda test: add test if stderror and stdout get logged if both are present 2025-08-11 13:15:46 +02:00
Jörg Krzeslak
08a833f1cf test: add parameter options to usage of type ExecFileFn 2025-08-11 13:15:39 +02:00
Jörg Krzeslak
c0f0dc5192 test: add optional options parameter to ExecFileFn type 2025-08-11 13:15:33 +02:00
Jörg Krzeslak
6452d0b357 test: add unit test for xelatex.ts 2025-08-11 13:15:23 +02:00
Jörg Krzeslak
9f6b815197 test: add unit test for vips.ts 2025-08-11 13:15:16 +02:00
Jörg Krzeslak
d8cbc0aaee test: add unit test for resvg.ts 2025-08-11 13:15:08 +02:00
Jörg Krzeslak
b1f70ec36c test: add unit test for potrace.ts 2025-08-11 13:15:02 +02:00
Jörg Krzeslak
311d2516ce test: add unit test for libjxl.ts 2025-08-11 13:14:55 +02:00
Jörg Krzeslak
5957873534 test: add unit test for libheif.ts 2025-08-11 13:14:48 +02:00
Jörg Krzeslak
7524f304fe test: add unit test for inkscape.ts 2025-08-11 13:14:39 +02:00
Jörg Krzeslak
f1730ede97 test: add unit test for imagemagick.ts 2025-08-11 13:14:30 +02:00
Jörg Krzeslak
7f9c8868fd test: add unit test for graphicsmagick.ts 2025-08-11 13:14:23 +02:00
Jörg Krzeslak
72b484a480 test: add unit test for ffmpeg.ts 2025-08-11 13:14:14 +02:00
Jörg Krzeslak
c47c1dd4f4 test: add unit test for dvisvgm.ts 2025-08-11 13:14:06 +02:00
Jörg Krzeslak
3975c70de7 test: move converters test helper to avoid confusion 2025-08-11 13:13:58 +02:00
Jörg Krzeslak
fd4e73e76c test: add unit test for calibre.ts 2025-08-11 13:13:49 +02:00
Jörg Krzeslak
301fab5c17 test: fix import in test 2025-08-11 13:13:30 +02:00
Jörg Krzeslak
2c90454244 test: extract ExecFileFn type to separate file to make it reusable 2025-08-11 13:13:21 +02:00
Jörg Krzeslak
2db99edeaf test: move test file into separate subfolder 2025-08-11 13:13:07 +02:00
Jörg Krzeslak
4fa471263f test: export type to be usable in test 2025-08-11 13:12:55 +02:00
Jörg Krzeslak
435f654cbe test: add unit test for assimp.ts 2025-08-11 13:12:39 +02:00
C4illin
15b03d7561 chore: fix tseslint config 2025-08-08 01:02:20 +02:00
Emrik Östling
219e6a29e4
Merge pull request #355 from VictorBravo9er/changes 2025-08-08 00:23:15 +02:00
Emrik Östling
a85edb6cee
Merge branch 'main' into changes 2025-08-08 00:19:50 +02:00
C4illin
43081c5179 chore: attempt to restrict push when allowed 2025-08-08 00:16:25 +02:00
C4illin
d390dce843 chore: test without push 2025-08-08 00:16:25 +02:00
C4illin
e5939aaa5d chore: test with id-token permissions 2025-08-07 23:44:45 +02:00
Emrik Östling
1db0c0f531 Update .github/ISSUE_TEMPLATE/converter_request.md
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
2025-08-07 23:38:25 +02:00
Emrik Östling
eefd33ac88 Update .github/ISSUE_TEMPLATE/converter_request.md
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
2025-08-07 23:38:25 +02:00
C4illin
de10436c1a chore: remove bun-types 2025-08-07 23:38:25 +02:00
C4illin
81f109f830 chore: format files 2025-08-07 23:38:25 +02:00
C4illin
858ee28ef2 ci: check formatting 2025-08-07 23:38:25 +02:00
C4illin
b89afe1b0c chore: add ignored deps 2025-08-07 23:19:31 +02:00
C4illin
b4fedc1c1d chore: downgrade bun-types to correct version 2025-08-07 23:18:30 +02:00
renovate[bot]
d24e8b4027 chore(deps): update actions/download-artifact action to v5 2025-08-06 17:07:48 +02:00
C4illin
93fbdbe0f3 chore: test global permissions 2025-08-03 21:15:04 +02:00
C4illin
068d9b8716 chore: hardcode ghcr token 2025-08-03 20:45:37 +02:00
Emrik Östling
2295f23725
chore: update README.md 2025-08-03 20:09:05 +02:00
C4illin
fed587b4a4 chore: fix template checklist 2025-08-03 20:03:26 +02:00
C4illin
8f73f9c365 Merge branch 'main' of https://github.com/C4illin/ConvertX 2025-08-03 19:55:18 +02:00
C4illin
363efc2e7f chore: add converter request template 2025-08-03 19:55:15 +02:00
Emrik Östling
cf93fed64b
Merge branch 'main' into changes 2025-08-03 19:42:54 +02:00
C4illin
99c689657f ci: remove hardcoded ghcr image name 2025-08-03 19:42:08 +02:00
Emrik Östling
394c98c65a
chore: add commented http allowed variable
closes #346
2025-07-27 11:00:41 +02:00
radhakrishnan
8f93ac29dd Improve msgconvert error handling and security
- Remove unnecessary stdout logging to reduce output clutter
- Sanitize stderr logging to protect sensitive path information
- Return targetPath instead of generic 'Done' message for better caller context
- Use proper Error objects instead of string rejections
- Address Sourcery AI feedback from PR #370
2025-07-24 18:08:19 +02:00
radhakrishnan
5ffb7f4a01 Add MSG to EML email conversion support (#367)
- Add new msgconvert converter using libemail-outlook-message-perl
- Support conversion from Outlook MSG files to standard EML format
- Add msgconvert to Docker dependencies and version checking
- Register msgconvert converter in main converter registry

Implements feature request #367 for email format conversion
2025-07-24 18:08:19 +02:00
Nick J
f5f718a84a Use a new env variable to determine whether the user ID should be set to 0 for unauthenticated users 2025-07-24 18:03:55 +02:00
Nick J
4c36a950a7 Set user ID to 0 if not using accounts 2025-07-24 18:03:55 +02:00
Emrik Östling
a9bc9d7e8d
Merge pull request #342 from Netzz0/FEAT/better-handling-of-multiples-files
Better handling of multiples files (Added Archive downloads and env var to set maximum concurrent processes)
2025-07-22 18:15:01 +02:00
Emrik Östling
18fed70ddf
Merge pull request #354 from fasonju/main
Text files support using libreoffice
2025-07-22 18:14:01 +02:00
radhakrishnan
dd9d117ab8 Fix EMF argument handling to preserve existing args
- Change from assignment to append for inputArgs and outputArgs in EMF handling
- Addresses Sourcery AI feedback about potential argument override
- Ensures compatibility with other file type conversions (e.g., SVG)
2025-07-22 18:12:50 +02:00
radhakrishnan
0e94fe354f Fix EMF to PNG conversion issue #362
- Add EMF-specific handling in ImageMagick converter to avoid LibreOffice delegate issues
- Disable EMF delegate and set proper conversion parameters (density: 300, background: white, alpha: remove)
- Prioritize Inkscape over ImageMagick for EMF files as it handles them natively
- Resolves LibreOffice delegate command failures when converting EMF files

Fixes #362
2025-07-22 18:12:50 +02:00
Vikramjit Borah
3b99c79495 - Update ENTRYPOINT to run dist/src/index.js instead of dist/index.js
- Ensure /data directory exists for SQLite database creation
- Remove unnecessary config files from final image
2025-07-10 21:43:56 +05:30
Vikramjit Borah
20e914c85b change to ignore list 2025-07-10 21:13:16 +05:30
Jason Fu
efc4b3f84c add libreoffice 2025-07-09 02:59:05 +02:00
Jason Fu
2bc6b52e99 idk what happened with these files tbh 2025-07-09 02:42:13 +02:00
Jason Fu
feb59e560b Merge branch 'main' of github.com:fasonju/ConvertX 2025-07-09 02:38:21 +02:00
Jason Fu
17be8f3601 handle no slashes case 2025-07-09 02:37:03 +02:00
Ren Jason Fu
3b053e8222
Update src/converters/libreoffice.ts
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
2025-07-09 02:34:43 +02:00
Jason Fu
9d5050d3ee Merge branch 'main' of github.com:fasonju/ConvertX 2025-07-09 02:22:31 +02:00
Jason Fu
1bd56e1d0e wpd removed 2025-07-09 02:13:30 +02:00
Ren Jason Fu
3f46abf261
Merge branch 'C4illin:main' into main 2025-07-09 02:11:48 +02:00
jasonfu
6a248e17be fix filters to have different filters per category 2025-07-08 13:05:40 +02:00
Jason Fu
8273a2a6a0 addedsperate filters per file category 2025-06-29 02:34:53 +02:00
Jason Fu
78f52c769d draft implementation 2025-06-27 01:33:55 +02:00
Emrik Östling
fe22b2f8fb
Merge pull request #344 from C4illin/fix/158/update-favicon 2025-06-26 10:29:18 +02:00
Jason Fu
482421f10e qsimple text files 2025-06-26 04:53:37 +02:00
Jason Fu
dcb15aee0e setup 2025-06-26 04:01:47 +02:00
C4illin
827f22e2fc fix: update favicon
issue: #158

Co-authored-by: clockwinder <54368516+clockwinder@users.noreply.github.com>
Co-authored-by: Denny Subke <155585746+dennysubke@users.noreply.github.com>
2025-06-26 00:38:59 +02:00
C4illin
bd36314f00 chore: change to major version only 2025-06-26 00:23:53 +02:00
Maxime
4ad7892eab fix promise.All location 2025-06-25 14:52:58 +02:00
Maxime
31b7e62983 removed redundant boolean eval 2025-06-25 14:52:38 +02:00
Maxime
0f5ef2f49c added setting number of concurrent convert processes 2025-06-25 14:35:30 +02:00
Maxime
2ce3fee70b added tar creation when using Download All 2025-06-25 14:34:27 +02:00
Emrik Östling
33e5bee9fb
Merge pull request #336 from C4illin/renovate/bun-types-1.x
chore(deps): update dependency bun-types to v1.2.17
2025-06-21 11:37:52 +02:00
renovate[bot]
b32f7dba5d
chore(deps): update dependency bun-types to v1.2.17 2025-06-21 09:00:27 +00:00
Emrik Östling
2661acbadb
Merge pull request #335 from C4illin/renovate/docker-setup-buildx-action-3.x
chore(deps): update docker/setup-buildx-action action to v3.11.1
2025-06-18 14:58:06 +02:00
renovate[bot]
d28c079a57
chore(deps): update docker/setup-buildx-action action to v3.11.1 2025-06-18 12:48:01 +00:00
Emrik Östling
29a159c094
Merge pull request #334 from C4illin/renovate/docker-setup-buildx-action-3.x
chore(deps): update docker/setup-buildx-action action to v3.11.0
2025-06-17 10:01:28 +02:00
renovate[bot]
68dad51948
chore(deps): update docker/setup-buildx-action action to v3.11.0 2025-06-16 16:54:22 +00:00
C4illin
3bf82b5b86 fix: move color variables to seperate directory
issue: #53
2025-06-10 22:39:11 +02:00
C4illin
e52e8c12cf chore: downgrade bun types to 1.2.2 2025-06-10 22:25:30 +02:00
C4illin
8f7a7faa91 chore: change to oklch colors 2025-06-10 22:05:10 +02:00
Emrik Östling
ff0edec652
Merge pull request #325 from C4illin/renovate/node-24.x
chore(deps): update dependency @types/node to v24
2025-06-10 10:36:04 +02:00
renovate[bot]
bbcecf274f
chore(deps): update dependency @types/node to v24 2025-06-10 03:37:07 +00:00
C4illin
cc41be6856 Merge branch 'main' of https://github.com/C4illin/ConvertX 2025-06-08 22:12:56 +02:00
C4illin
761f56b869 fix: add lmodern
issue: #320
2025-06-08 22:12:26 +02:00
Emrik Östling
d4e8eaadd7
Merge pull request #322 from C4illin/fix/qtwebengine-no-sandbox 2025-06-08 21:38:46 +02:00
C4illin
9f2bdadde7 fix: run qtwebengine without sandbox
issue: #318, issue: #320
2025-06-08 21:29:58 +02:00
C4illin
f789d9dfe3 fix: add language env 2025-06-05 19:56:22 +02:00
Emrik Östling
ce41ee2387
Merge pull request #313 from C4illin/release-please--branches--main--components--convertx-frontend 2025-06-04 10:57:03 +02:00
Emrik Östling
01c8fad012 chore(main): release 0.14.1 2025-06-04 10:48:18 +02:00
Emrik Östling
908e91cb91
Merge pull request #312 from C4illin/fix/311/use-baseline-build 2025-06-04 10:47:46 +02:00
Emrik Östling
f1c5cd9f6b
Merge pull request #309 from C4illin/fix/301/add-support-for-kepub 2025-06-04 10:28:13 +02:00
C4illin
6ea3058e66 fix: change to baseline build
issue: #311
2025-06-04 10:26:28 +02:00
C4illin
a4e741cc0a chore: add links to changelog 2025-06-03 20:09:37 +02:00
Emrik Östling
0f2172d61f
Merge pull request #296 from C4illin/release-please--branches--main--components--convertx-frontend 2025-06-03 20:07:34 +02:00
Emrik Östling
2baa69ca17
Update issue templates 2025-06-03 19:34:54 +02:00
Emrik Östling
3bbfa9186e
Update issue templates 2025-06-03 19:31:27 +02:00
C4illin
c1428f5c2b chore: fix knip 2025-06-03 19:27:36 +02:00
C4illin
1be11708c4 chore: format all files 2025-06-03 19:19:28 +02:00
Emrik Östling
8c04b318fd chore(main): release 0.14.0 2025-06-03 18:26:42 +02:00
C4illin
ff2c0057e8 fix: progress bars on firefox 2025-06-03 18:23:26 +02:00
C4illin
c830721e02 chore: update deps 2025-06-03 18:08:12 +02:00
C4illin
625e1a51f6 feat: add dvisvgm 2025-06-03 17:58:43 +02:00
C4illin
6af1e8f326 refactor: create db types 2025-06-03 17:13:20 +02:00
Emrik Östling
82f0e14abf
Merge pull request #310 from C4illin/refactor-elysia-router 2025-06-03 15:11:19 +02:00
C4illin
9e759a75de refactor: split main file to pages 2025-06-03 15:04:18 +02:00
C4illin
33388cf209 fix: add support for kepub
issue: #301
2025-06-02 15:51:35 +02:00
C4illin
2490c3a7e7 chore: update dependencies 2025-06-02 15:50:24 +02:00
C4illin
7f86c352e3 chore: format 2025-06-02 15:50:17 +02:00
Emrik Östling
2a3b08487e
Merge pull request #304 from xxeisenberg/main
feat: improve job details interaction and accessibility
2025-05-30 09:38:52 +02:00
xeisenberg
29ba229bc2 feat: improve job details interaction and accessibility
- Enhanced job details toggle functionality by adding event listeners to job detail elements.
- Updated job detail rows to use data attributes for better accessibility and maintainability.
- Improved the rendering of file information with unique keys for each file entry.
2025-05-29 23:43:24 +02:00
xeisenberg
50725edd02 feat: enhance job details display with file information
- Added `files_detailed` property to the `Jobs` class to store detailed file information.
- Updated job listing to include a toggle for displaying detailed file information.
- Implemented a toggle function for showing/hiding detailed file rows in the UI.
2025-05-29 23:43:24 +02:00
Emrik Östling
40d1d8a191
Merge pull request #306 from C4illin/renovate/docker-build-push-action-6.x 2025-05-28 11:04:27 +02:00
renovate[bot]
3417564278
chore(deps): update docker/build-push-action action to v6.18.0 2025-05-27 23:08:35 +00:00
C4illin
9a49dedaca feat: show version in footer
Co-authored-by: thejjw <72130076+thejjw@users.noreply.github.com>
2025-05-23 23:11:54 +02:00
C4illin
d9076bf42a chore: update dependencies 2025-05-23 23:10:52 +02:00
C4illin
b9bbf7792f fix: register button style 2025-05-23 21:36:26 +02:00
C4illin
5cc6678ceb chore: add imagemagick 2025-05-23 21:22:12 +02:00
C4illin
b47e5755f6 feat: add ImageMagick
issue: #295, #269
2025-05-23 21:18:47 +02:00
C4illin
af5c768dc7 fix: add av1 and h26X with containers
issue: #287, #293
2025-05-23 21:15:36 +02:00
Emrik Östling
3b573cccae
Merge pull request #300 from C4illin/renovate/docker-build-push-action-6.x
chore(deps): update docker/build-push-action action to v6.17.0
2025-05-23 19:35:54 +02:00
renovate[bot]
0c6f6d6904
chore(deps): update docker/build-push-action action to v6.17.0 2025-05-23 17:13:16 +00:00
Emrik Östling
6e2fe27f31
Merge pull request #298 from bennett-sh/fix-account-page-title 2025-05-23 19:13:15 +02:00
Emrik Östling
b6cdd3741a
Merge pull request #297 from C4illin/ci-merge-images 2025-05-23 19:12:46 +02:00
Emrik Östling
00f95b6daa ci: merge images 2025-05-23 19:09:16 +02:00
Bennett
2d05bbf86b fix account page title 2025-05-23 12:34:11 +02:00
Emrik Östling
2c87a6c8c2
Merge pull request #291 from C4illin/trixie-test 2025-05-23 11:03:18 +02:00
C4illin
254509db5e chore: restore calibre 2025-05-23 10:06:57 +02:00
C4illin
4e4c029cb8 fix: switch from alpine to debian trixie
issue: #234, #199
2025-05-23 10:06:57 +02:00
Emrik Östling
6dc60679bb
Merge pull request #288 from bennett-sh/account-settings 2025-05-23 10:04:27 +02:00
Emrik Östling
6e5d5d9de0
chore: adjust new lines 2025-05-22 16:07:04 +02:00
Emrik Östling
6289c033c8
chore: add github trending badge 2025-05-22 16:06:35 +02:00
Emrik Östling
b200049a81
Merge pull request #292 from C4illin/native-arm64-builds 2025-05-22 14:21:11 +02:00
C4illin
5646f79f99 ci: add native arm64 build 2025-05-22 14:17:31 +02:00
Emrik Östling
5083968b80
chore: add conventional commits 2025-05-22 12:39:07 +02:00
Bennett
2eb9b8fe96
fix updating logic 2025-05-19 20:12:29 +02:00
Bennett
8dc60b41ff Fixes email update validation logic 2025-05-19 18:51:32 +02:00
Bennett
b4be479d02
Update src/index.tsx
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
2025-05-19 18:20:06 +02:00
Bennett
f56a93a1b2
Update src/index.tsx
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
2025-05-19 18:19:55 +02:00
Bennett
ff8b9fca67 add account management page 2025-05-19 17:46:38 +02:00
Emrik Östling
0579f1852b
chore: add potrace to readme 2025-05-14 13:14:55 +02:00
Emrik Östling
52d4cc0d03
chore: remove calibre from README.md 2025-05-14 13:10:57 +02:00
Emrik Östling
2c68016ca6
chore: remove duplicates 2025-05-14 13:09:06 +02:00
Emrik Östling
7914194856
Merge pull request #255 from C4illin/release-please--branches--main--components--convertx-frontend 2025-05-14 13:07:23 +02:00
Emrik Östling
2dac7f1362 chore: downgrade bun to 1.2.2 2025-05-14 12:19:40 +02:00
Emrik Östling
a17e5fd614 chore(main): release 0.13.0 2025-05-14 08:55:39 +02:00
Emrik Östling
21994fb6a2
Merge pull request #282 from aidanjacobson/main
Added support for drag/drop of images
2025-05-14 08:54:10 +02:00
Emrik Östling
a5eaaa422a
Merge pull request #284 from frederickjansen/hif
feat: Add support for .HIF files
2025-05-14 08:02:28 +02:00
aidanjacobson
ff2ef74135 feat: add support for drag/drop of images 2025-05-13 19:19:57 -07:00
Frederick Jansen
70705c1850 feat: Add support for .HIF files 2025-05-13 12:22:08 -04:00
C4illin
fd9c151e01 chore: update deps 2025-05-12 09:24:36 +02:00
Emrik Östling
4f0573963f
Merge pull request #283 from C4illin/renovate/oven-bun-1.x
chore(deps): update oven/bun docker tag to v1.2.13
2025-05-10 18:11:06 +02:00
renovate[bot]
6bb6bce8a4
chore(deps): update oven/bun docker tag to v1.2.13 2025-05-10 14:45:25 +00:00
Emrik Östling
448557bece
Merge pull request #278 from atfox98/main 2025-05-07 10:11:44 +02:00
A Fox
bdbd4a122c feat: add potrace converter 2025-05-06 12:46:05 -05:00
Emrik Östling
cb9d0ec680
Merge pull request #275 from C4illin/renovate/oven-bun-1.x 2025-05-04 23:04:38 +02:00
renovate[bot]
fb60ef66f5
chore(deps): update oven/bun docker tag to v1.2.12 2025-05-04 10:08:16 +00:00
Emrik Östling
c1ae43075f
Merge pull request #274 from C4illin/renovate/npm-run-all2-8.x
chore(deps): update dependency npm-run-all2 to v8
2025-05-02 21:30:55 +02:00
renovate[bot]
377f69ae8d
chore(deps): update dependency npm-run-all2 to v8 2025-05-02 18:38:15 +00:00
Emrik Östling
cb131cd0a0
Merge pull request #270 from C4illin/renovate/oven-bun-1.x 2025-04-29 10:46:38 +02:00
renovate[bot]
fcc83c5ea8
chore(deps): update oven/bun docker tag to v1.2.11 2025-04-29 08:11:10 +00:00
Emrik Östling
96d4717d13
chore: create FUNDING.yml 2025-04-24 18:17:24 +02:00
Emrik Östling
4d73bf9760
chore: add tutorial disclaimer 2025-04-24 18:06:57 +02:00
Emrik Östling
725a94bc95
chore: move http warning 2025-04-24 18:03:21 +02:00
Emrik Östling
0a366b447a
chore: add dev image size 2025-04-24 18:01:47 +02:00
Emrik Östling
4a27a7bc03
Merge pull request #264 from C4illin/renovate/oven-bun-1.x 2025-04-17 13:45:47 +02:00
renovate[bot]
3ca5803bda
chore(deps): update oven/bun docker tag to v1.2.10 2025-04-17 10:54:10 +00:00
Emrik Östling
239041294c
Merge pull request #260 from C4illin/renovate/oven-bun-1.x
chore(deps): update oven/bun docker tag to v1.2.9
2025-04-16 12:46:11 +02:00
renovate[bot]
31fdd8f214
chore(deps): update oven/bun docker tag to v1.2.9 2025-04-16 10:08:31 +00:00
C4illin
c3319c09eb chore: remove calibre dependency 2025-04-16 11:23:44 +02:00
C4illin
d460e94d52 chore: disable calibre due to conflict with other packages 2025-04-16 11:21:40 +02:00
C4illin
4b5c732380 fix: add timezone support
issue #258
2025-04-12 10:24:08 +02:00
C4illin
f42665ca40 chore: update deps 2025-04-12 10:18:44 +02:00
Emrik Östling
bed52cef17
Merge pull request #254 from kek-Sec/feat/hide-history
feat: add HIDE_HISTORY option to control visibility of history page
2025-04-02 14:26:35 +02:00
g.petrakis
9d1c93155c feat: add HIDE_HISTORY option to control visibility of history page 2025-04-02 15:02:56 +03:00
C4illin
794cc7c474 chore: update deps 2025-04-01 18:09:56 +02:00
C4illin
d7d584e497 chore: format 2025-04-01 14:50:15 +02:00
Emrik Östling
f5320df86e
Merge pull request #241 from C4illin/renovate/oven-bun-1.x
chore(deps): update oven/bun docker tag to v1.2.8
2025-04-01 14:19:48 +02:00
C4illin
056fd4ba93 change to bun 1.2.2 2025-04-01 14:19:00 +02:00
Emrik Östling
5b6e70eb3a
Merge pull request #246 from C4illin/release-please--branches--main--components--convertx-frontend
chore(main): release 0.12.1
2025-04-01 14:15:28 +02:00
renovate[bot]
f437a8e7e2
chore(deps): update oven/bun docker tag to v1.2.8 2025-03-31 19:37:32 +00:00
Emrik Östling
cdae798fcf chore: rollback to 1.2.2
issue: #235
2025-03-20 11:05:24 +01:00
Emrik Östling
bcc827a81b chore(main): release 0.12.1 2025-03-20 09:40:15 +01:00
Emrik Östling
84274b9c55 chore: revert to bun 1.2.3
issue: #235
2025-03-20 09:39:36 +01:00
Emrik Östling
20c6f8249e
Merge pull request #245 from C4illin/fix/#235/change-to-canary-bun
fix: change to canary bun
2025-03-19 21:19:45 +01:00
C4illin
8f0ea2a592 fix: change to canary bun
issue: #235
2025-03-19 20:30:50 +01:00
Emrik Östling
a29e4a930a
Merge pull request #242 from C4illin/downgrade-bun-to-1.2.2
chore: downgrade bun to 1.2.2
2025-03-10 13:12:31 +01:00
Emrik Östling
4549c96ae3 chore: remove old labels 2025-03-10 12:38:17 +01:00
Emrik Östling
bc64094c04 chore: downgrade bun to 1.2.2
issue: #235
2025-03-10 12:34:47 +01:00
Emrik Östling
fa58827ad5
Merge pull request #240 from C4illin/donwgrade-bun-to-1.2.3
chore: downgrade bun to 1.2.3
2025-03-09 22:43:02 +01:00
C4illin
8f27be0e3d chore: downgrade bun 2025-03-09 21:10:59 +01:00
C4illin
df43df1178 Merge branch 'main' of https://github.com/C4illin/ConvertX 2025-03-09 21:09:58 +01:00
C4illin
c2beb4a227 chore: add default full opacity 2025-03-09 21:09:53 +01:00
Emrik Östling
9277c27a50
chore: change security url 2025-03-09 21:07:04 +01:00
Emrik Östling
171ecd6884
chore: Create SECURITY.md 2025-03-09 21:04:18 +01:00
Emrik Östling
dca29f7e5a
Merge pull request #239 from C4illin/remove-slim
build: remove slim for tailwind
2025-03-09 16:40:20 +01:00
C4illin
318acc20bd build: remove slim for tailwind 2025-03-08 01:08:00 +01:00
C4illin
f433493d57 chore: remove @elysiajs/cookie 2025-03-08 00:28:27 +01:00
C4illin
19970fc132 chore: fix lint 2025-03-06 21:09:02 +01:00
Emrik Östling
24394ca3c5
Merge pull request #226 from C4illin/release-please--branches--main--components--convertx-frontend
chore(main): release 0.12.0
2025-03-06 18:47:25 +01:00
Emrik Östling
10ff0b464a chore(main): release 0.12.0 2025-03-06 18:17:28 +01:00
C4illin
9263d17609 feat: replace exec with execFile 2025-03-06 18:16:51 +01:00
Emrik Östling
c1b75a13fd chore: sanitize filename 2025-03-04 09:23:06 +01:00
Emrik Östling
a8ed60d48f
Merge pull request #233 from Lacni135/feature-progress
Added progress bar for file upload
2025-02-28 09:57:39 +01:00
lacni
dc82a438d4 fix: refactored uploadFile to only accept a single file instead of multiple 2025-02-27 21:11:52 -05:00
lacni
cc54bdcbe7 feat: made every upload file independent 2025-02-27 19:18:13 -05:00
lacni
ae4bbc8baa fix: added onerror log 2025-02-27 19:15:58 -05:00
C4illin
ad98499da0 chore: move libheif below vips 2025-02-27 22:17:02 +01:00
lacni
db60f355b2 feat: added progress bar for file upload 2025-02-26 23:31:31 -05:00
Emrik Östling
eb91d8b298
Merge pull request #232 from C4illin/renovate/oven-bun-1.x
chore(deps): update oven/bun docker tag to v1.2.4
2025-02-26 16:07:27 +01:00
renovate[bot]
b8312be4b7
chore(deps): update oven/bun docker tag to v1.2.4 2025-02-26 14:32:53 +00:00
Emrik Östling
326a8e3404
Merge pull request #230 from C4illin/renovate/oven-bun-1.x 2025-02-23 12:45:09 +01:00
renovate[bot]
f017e13ac1
chore(deps): update oven/bun docker tag to v1.2.3 2025-02-23 01:15:18 +00:00
Emrik Östling
67a5fe353e
Merge pull request #229 from C4illin/renovate/oven-bun-1.x
chore(deps): update oven/bun docker tag to v1.2.3
2025-02-22 11:47:50 +01:00
renovate[bot]
51d49d7ff3
chore(deps): update oven/bun docker tag to v1.2.3 2025-02-22 10:05:09 +00:00
Emrik Östling
d42b820b36
Merge pull request #227 from C4illin/renovate/eslint__js-9.x
chore(deps): update dependency @types/eslint__js to v9
2025-02-22 11:04:42 +01:00
renovate[bot]
07d32776d3
chore(deps): update dependency @types/eslint__js to v9 2025-02-21 23:05:04 +00:00
Emrik Östling
ef027e81b5
Merge pull request #228 from C4illin/renovate/globals-16.x
chore(deps): update dependency globals to v16
2025-02-22 00:04:37 +01:00
renovate[bot]
a75e4b495d
chore(deps): update dependency globals to v16 2025-02-21 20:05:07 +00:00
C4illin
fba5e212e8 fix: update libheif to 1.19.5
issue: #202
2025-02-18 21:24:54 +01:00
C4illin
62f44fb052 chore: print libheif version 2025-02-18 20:05:46 +01:00
Emrik Östling
6b9254047c
Merge pull request #225 from C4illin/fix/#202/add-libheif
fix: add libheif
2025-02-16 23:04:35 +01:00
C4illin
decfea5dc9 fix: add libheif
issue #202
2025-02-16 21:18:33 +01:00
Emrik Östling
eacded6848
Merge pull request #224 from C4illin/release-please--branches--main--components--convertx-frontend
chore(main): release 0.11.1
2025-02-07 22:52:29 +01:00
Emrik Östling
279ca72c64 chore(main): release 0.11.1 2025-02-07 16:15:21 +01:00
C4illin
b8fc9383ca chore: update deps 2025-02-07 16:14:46 +01:00
C4illin
bec58ac59f fix: mobile view overflow 2025-02-06 19:57:07 +01:00
Emrik Östling
83d7126820
Merge pull request #216 from C4illin/release-please--branches--main--components--convertx-frontend
chore(main): release 0.11.0
2025-02-05 15:23:43 +01:00
Emrik Östling
f0e9c6d794 chore(main): release 0.11.0 2025-02-05 14:18:31 +01:00
Emrik Östling
0e61051fc6 fix: install numpy for inkscape 2025-02-05 14:17:55 +01:00
C4illin
480ba77ebe chore: fix eslint config 2025-02-01 21:46:15 +01:00
C4illin
16f27c13bb fix: don't crash if file is not found 2025-02-01 21:09:48 +01:00
C4illin
afe5c50d66 chore: fix tailwind v4 docker 2025-02-01 21:09:12 +01:00
C4illin
72ea859ebb chore: switch to new lockfile format 2025-02-01 21:08:38 +01:00
C4illin
8edf3834c4 chore: print correct version 2025-02-01 21:08:04 +01:00
C4illin
e595014fcd refactor: update Tailwind to v4 2025-02-01 20:15:50 +01:00
Emrik Östling
8bebf7e569
Merge pull request #220 from C4illin/renovate/oven-bun-1.x
chore(deps): update oven/bun docker tag to v1.2.2
2025-02-01 10:05:04 +01:00
renovate[bot]
c825ec06e2
chore(deps): update oven/bun docker tag to v1.2.2 2025-02-01 08:31:38 +00:00
Emrik Östling
8c75f273fb
Merge pull request #217 from C4illin/renovate/oven-bun-1.x
chore(deps): update oven/bun docker tag to v1.2.1
2025-01-28 07:13:26 +01:00
renovate[bot]
0ba776c129
chore(deps): update oven/bun docker tag to v1.2.1 2025-01-28 01:48:23 +00:00
C4illin
2bbbd03554 feat: add deps for vaapi
issue #192
2025-01-23 22:00:19 +01:00
C4illin
0a5d0487b1 chore: update deps 2025-01-23 21:59:24 +01:00
Emrik Östling
583cd2dd3b
Merge pull request #214 from C4illin/renovate/oven-bun-1.x
chore(deps): update oven/bun docker tag to v1.2.0
2025-01-22 23:11:45 +01:00
renovate[bot]
e1f7fc1ecb
chore(deps): update oven/bun docker tag to v1.2.0 2025-01-22 18:46:19 +00:00
Emrik Östling
961a55cbe5
Merge pull request #213 from C4illin/release-please--branches--main--components--convertx-frontend 2025-01-21 23:28:20 +01:00
Emrik Östling
cdf9bad903 chore(main): release 0.10.1 2025-01-21 23:25:50 +01:00
C4illin
6769fa2f83 chore: update deps 2025-01-21 23:25:11 +01:00
C4illin
3b7ea88b73 fix: ffmpeg works without ffmpeg_args
issue #212
2025-01-21 23:24:53 +01:00
Emrik Östling
59310c095d
Merge pull request #192 from C4illin/release-please--branches--main--components--convertx-frontend
chore(main): release 0.10.0
2025-01-18 12:46:01 +01:00
Emrik Östling
c47f0c12fe chore(main): release 0.10.0 2025-01-18 12:01:01 +01:00
Emrik Östling
ca71a40485
Merge pull request #211 from C4illin/renovate/oven-bun-1.x
chore(deps): update oven/bun docker tag to v1.1.45
2025-01-18 12:00:28 +01:00
renovate[bot]
d4e8f376c1
chore(deps): update oven/bun docker tag to v1.1.45 2025-01-17 17:41:37 +00:00
Emrik Östling
14c6ea1e6b
Merge pull request #210 from C4illin/renovate/oven-bun-1.x
chore(deps): update oven/bun docker tag to v1.1.44
2025-01-17 11:14:27 +01:00
renovate[bot]
d6e4d8fbd6
chore(deps): update oven/bun docker tag to v1.1.44 2025-01-17 09:03:11 +00:00
C4illin
460bda62d5 chore: update deps 2025-01-16 21:30:10 +01:00
Emrik Östling
d2702ab673
Merge pull request #208 from C4illin/renovate/oven-bun-1.x
chore(deps): update oven/bun docker tag to v1.1.43
2025-01-08 23:35:11 +01:00
renovate[bot]
e2581f42f5
chore(deps): update oven/bun docker tag to v1.1.43 2025-01-08 19:03:18 +00:00
Emrik Östling
f0fcfc159f
Merge pull request #203 from tejashah88/fix-unauthed-users-1
Fix: skip account setup when ALLOW_UNAUTHENTICATED is true
2025-01-04 17:36:04 +01:00
Tejas Shah
538c5b60c9 fix: skip account setup when ALLOW_UNAUTHENTICATED is true 2025-01-04 17:05:03 +01:00
Emrik Östling
2fabb7bbb2
Merge pull request #197 from C4illin/renovate/oven-bun-1.x
chore(deps): update oven/bun docker tag to v1.1.42
2025-01-04 14:27:59 +01:00
renovate[bot]
e7c34a9c94
chore(deps): update oven/bun docker tag to v1.1.42 2025-01-04 12:43:57 +00:00
Emrik Östling
618f9fce5a
Merge pull request #204 from C4illin/fix-calibre-build-error
fix: add qt6-qtbase-private-dev from community repo
2025-01-04 13:43:30 +01:00
C4illin
95dbc9f678 fix: add qt6-qtbase-private-dev from community repo 2025-01-04 12:54:22 +01:00
Emrik Östling
aa87bc5c51
Merge pull request #200 from tejashah88/main
Fixed example docker-compose.yml file
2024-12-27 03:34:01 +01:00
Tejas Shah
815de531ed
Fixed example docker-compose.yml file 2024-12-26 07:04:26 -08:00
Emrik Östling
cf2b026dc4
Merge pull request #196 from C4illin/renovate/oven-bun-1.x
chore(deps): update oven/bun docker tag to v1.1.41
2024-12-20 16:44:02 +01:00
renovate[bot]
9ce46aefba
chore(deps): update oven/bun docker tag to v1.1.41 2024-12-20 15:33:31 +00:00
Emrik Östling
98b2db7818
Merge pull request #195 from C4illin/renovate/oven-bun-1.x
chore(deps): update oven/bun docker tag to v1.1.40
2024-12-18 13:20:35 +01:00
renovate[bot]
0229851bf9
chore(deps): update oven/bun docker tag to v1.1.40 2024-12-18 09:07:21 +00:00
C4illin
9e15114fe8 chore: update deps 2024-12-17 21:56:24 +01:00
Emrik Östling
7f66a76bb0
Merge pull request #194 from C4illin/renovate/oven-bun-1.x
chore(deps): update oven/bun docker tag to v1.1.39
2024-12-17 18:04:50 +01:00
renovate[bot]
e9cc8392bb
chore(deps): update oven/bun docker tag to v1.1.39 2024-12-17 16:08:25 +00:00
C4illin
d0b89ce74f chore: add ffmpeg args and calibre to readme 2024-12-11 11:09:03 +01:00
C4illin
f537c81db7 fix: add FFMPEG_ARGS env variable
issue #190
2024-12-11 11:01:39 +01:00
C4illin
03d3edfff6 feat: add calibre
issue #191
2024-12-07 02:38:30 +01:00
Emrik Östling
447b4c5e5c
Merge pull request #189 from C4illin/renovate/oven-bun-1.x 2024-12-01 12:22:47 +01:00
renovate[bot]
cb143209ae
chore(deps): update oven/bun docker tag to v1.1.38 2024-11-29 14:10:18 +00:00
C4illin
9c24fe73b5 chore: add table overview for images 2024-11-27 17:06:35 +01:00
C4illin
19ae85424b chore: add dockerhub pulls badge 2024-11-27 16:51:31 +01:00
C4illin
22fad99552 chore: update converter count 2024-11-26 17:18:48 +01:00
Emrik Östling
8144bbef74
Merge pull request #188 from C4illin/renovate/oven-bun-1.x
chore(deps): update oven/bun docker tag to v1.1.37
2024-11-26 09:31:12 +01:00
renovate[bot]
aad6da0ae8
chore(deps): update oven/bun docker tag to v1.1.37 2024-11-26 04:13:39 +00:00
C4illin
c5f8162a22 chore: update deps 2024-11-25 23:22:18 +01:00
Emrik Östling
f0f30224b5
Merge pull request #183 from C4illin/release-please--branches--main--components--convertx-frontend
chore(main): release 0.9.0
2024-11-24 14:47:34 +01:00
C4illin
d0d888e356 chore: update postcss to ejs 2024-11-21 23:08:30 +01:00
C4illin
2c64122224 chore: update deps 2024-11-21 23:08:16 +01:00
C4illin
3b2eee96a9 chore: update deps 2024-11-21 22:50:45 +01:00
C4illin
465aacbf9b chore: update formats 2024-11-21 22:50:38 +01:00
Emrik Östling
d1a2a66170 chore(main): release 0.9.0 2024-11-21 22:44:51 +01:00
C4illin
4c05fd72bb fix: wait for both upload and selection
issue #177
2024-11-21 22:44:13 +01:00
Emrik Östling
f04fe760e3
Merge pull request #187 from C4illin/renovate/oven-bun-1.x
chore(deps): update oven/bun docker tag to v1.1.36
2024-11-20 10:12:40 +01:00
renovate[bot]
834d19bcc6
chore(deps): update oven/bun docker tag to v1.1.36 2024-11-20 02:03:06 +00:00
Emrik Östling
6808c4642c
Update README.md 2024-11-18 17:00:04 +01:00
Emrik Östling
d0ce307f94
Merge pull request #186 from C10udburst/main
feat: add inkscape for vector images
2024-11-16 14:13:43 +01:00
Cloudburst
f3740e9ded feat: add inkscape for vector images 2024-11-16 11:34:47 +01:00
C4illin
b485bc9445 chore: add download stats 2024-11-14 11:26:24 +01:00
Emrik Östling
2d14c1bb26
Merge pull request #184 from C4illin/feature/#177/disable-convert-when-uploading 2024-11-13 14:59:31 +01:00
C4illin
1a442d6e69 fix: treat unknown as m4a
issue #178
2024-11-13 13:08:40 +01:00
C4illin
2386543e5c chore: update deps 2024-11-13 13:08:00 +01:00
C4illin
58e220e82d feat: disable convert when uploading
issue #177
2024-11-12 22:30:16 +01:00
C4illin
24bea6e4d2 chore: add tutorial link 2024-11-12 22:20:32 +01:00
C4illin
43497ad8d1 ci: split docker hub description to separate workflow 2024-11-12 12:35:05 +01:00
C4illin
f22b61fe4c ci: sync docker hub description with readme 2024-11-12 12:27:39 +01:00
C4illin
5b08f4cd19 ci: support dockerhub 2024-11-12 12:17:45 +01:00
Emrik Östling
1589f8d24e
Merge pull request #182 from C4illin/feature/#180/add-webroot-env-variable 2024-11-06 22:56:00 +01:00
C4illin
7d1db72cf5 chore: fix default value for webroot 2024-11-06 14:14:11 +01:00
C4illin
53a8f66414 chore: update deps 2024-11-06 14:04:57 +01:00
C4illin
36cb6cc589 feat: Allow to chose webroot
issue #180
2024-11-06 08:49:53 +01:00
Emrik Östling
f3a4aece46
Merge pull request #181 from C4illin/renovate/oven-bun-1.x
chore(deps): update oven/bun docker tag to v1.1.34
2024-11-04 11:21:48 +01:00
renovate[bot]
580a6a869a
chore(deps): update oven/bun docker tag to v1.1.34 2024-11-02 06:59:17 +00:00
Emrik Östling
008eaac493
Merge pull request #176 from C4illin/renovate/oven-bun-1.x
chore(deps): update oven/bun docker tag to v1.1.33
2024-10-28 22:56:14 +01:00
renovate[bot]
b450623bb4
chore(deps): update oven/bun docker tag to v1.1.33 2024-10-24 10:42:28 +00:00
Emrik Östling
8ac2ecb673
Merge pull request #175 from C4illin/renovate/npm-run-all2-7.x
chore(deps): update dependency npm-run-all2 to v7
2024-10-22 09:04:39 +02:00
Emrik Östling
0a10a56ae3
Merge pull request #174 from C4illin/renovate/oven-bun-1.x
chore(deps): update oven/bun docker tag to v1.1.32
2024-10-22 09:04:14 +02:00
renovate[bot]
9378ba9208
chore(deps): update dependency npm-run-all2 to v7 2024-10-22 01:09:18 +00:00
renovate[bot]
0c586e324b
chore(deps): update oven/bun docker tag to v1.1.32 2024-10-21 23:14:26 +00:00
Emrik Östling
91c4a64284
chore: add dev image size 2024-10-18 20:07:44 +02:00
C4illin
c599e98d9d chore: ignore more files 2024-10-18 20:03:19 +02:00
C4illin
d2cd6706c9 chore: update @elysiajs/static 2024-10-18 19:32:42 +02:00
C4illin
e8ed10dde8 chore(deps): update @elysiajs/html to version 1.1.1 2024-10-18 18:53:45 +02:00
C4illin
5fe0b79802 chore(deps): update dependencies to latest versions 2024-10-18 18:43:51 +02:00
Emrik Östling
34a6722a68
Merge pull request #172 from C4illin/renovate/oven-bun-1.x 2024-10-18 11:52:36 +02:00
renovate[bot]
5b0d769c63
chore(deps): update oven/bun docker tag to v1.1.31 2024-10-18 07:56:43 +00:00
Emrik Östling
718401a28b
Merge pull request #169 from C4illin/renovate/oven-bun-1.x
chore(deps): update oven/bun docker tag to v1.1.30
2024-10-08 18:09:13 +02:00
renovate[bot]
3112cd57f6
chore(deps): update oven/bun docker tag to v1.1.30 2024-10-08 14:13:13 +00:00
C4illin
410fc777a7 Merge branch 'main' of https://github.com/C4illin/ConvertX 2024-10-07 10:47:40 +02:00
C4illin
8eed99e732 chore: fix drop done style 2024-10-07 10:47:37 +02:00
Emrik Östling
663b1d4171
Merge pull request #167 from C4illin/release-please--branches--main--components--convertx-frontend 2024-10-06 23:36:13 +02:00
Emrik Östling
c3067ca12d chore(main): release 0.8.1 2024-10-06 00:49:06 +02:00
Emrik Östling
4561ca3760
Merge pull request #164 from C4illin/fix/#163/add-jfif-support 2024-10-06 00:48:46 +02:00
Emrik Östling
698cce58ce
Merge pull request #165 from C4illin/fix/#157/resize-when-converting-to-ico 2024-10-06 00:46:22 +02:00
C4illin
339b79f786 fix: treat jfif as jpeg
issue #163
2024-10-06 00:45:08 +02:00
C4illin
4f98f778f0 chore: add message when resizing image 2024-10-06 00:40:34 +02:00
Emrik Östling
8479b33a47
Merge pull request #166 from C4illin/fix/#151/disable-convert-button 2024-10-05 23:29:39 +02:00
C4illin
78844d7bd5 fix: disable convert button when input is empty
issue #151
2024-10-05 01:20:23 +02:00
Emrik Östling
64e4a271e1
Merge branch 'main' into fix/#157/resize-when-converting-to-ico 2024-10-05 01:02:48 +02:00
C4illin
5fb8c3575b chore: add eslint-plugin-readable-tailwind 2024-10-05 01:01:00 +02:00
C4illin
a6b8bcecae chore: disable dependency dashboard in Renovate configuration 2024-10-05 00:47:34 +02:00
C4illin
bc9c820820 chore: remove DeepSource configuration file 2024-10-05 00:45:34 +02:00
C4illin
ee9207a7f4 chore: fix eslint rules 2024-10-05 00:43:24 +02:00
C4illin
a34e215202 chore: remove biome 2024-10-05 00:01:39 +02:00
C4illin
b4e53dbb8e fix: resize to fit for ico
issue #157
2024-10-04 23:55:39 +02:00
C4illin
b5e8d82bfa chore(eslint): add browser globals to ESLint configuration 2024-10-04 23:44:18 +02:00
Emrik Östling
5d9000bb33
Merge pull request #162 from C4illin/renovate/biomejs-biome-1.x
chore(deps): update dependency @biomejs/biome to v1.9.3
2024-10-01 22:01:56 +02:00
renovate[bot]
ccb065ef0f
chore(deps): update dependency @biomejs/biome to v1.9.3 2024-10-01 15:11:07 +00:00
Emrik Östling
883fad806b
Merge pull request #155 from C4illin/release-please--branches--main--components--convertx-frontend 2024-10-01 14:17:30 +02:00
Emrik Östling
feacd1b816 chore(main): release 0.8.0 2024-09-30 22:01:44 +02:00
Emrik Östling
094e7a0d1c
Merge pull request #160 from C4illin/feature-light-theme
feat: add light theme
2024-09-30 22:01:19 +02:00
C4illin
72636c5059 feat: add light theme, fixes #156 2024-09-30 17:14:44 +02:00
C4illin
291cfc80c6 chore: update deps 2024-09-30 15:56:33 +02:00
C4illin
ae1dfafc9d fix: cleanup formats and add opus, fixes #159 2024-09-30 15:51:11 +02:00
Emrik Östling
6caa583c35
Merge pull request #154 from C4illin/fix/#153/clean-up-ffmpeg-formats
fix: support .awb and clean up, fixes #153, #92
2024-09-28 13:36:46 +02:00
C4illin
2057167576 fix: add support for usd for assimp, #144 2024-09-28 13:13:48 +02:00
C4illin
1c9e67fc32 fix: support .awb and clean up, fixes #153, #92 2024-09-28 13:02:17 +02:00
Emrik Östling
d3af9688c6
Merge pull request #149 from C4illin/release-please--branches--main--components--convertx-frontend
chore(main): release 0.7.0
2024-09-28 11:11:11 +02:00
Emrik Östling
7d0cbb9844 chore(main): release 0.7.0 2024-09-26 23:37:45 +02:00
C4illin
88173891ba fix: wrong layout on search with few options 2024-09-26 23:37:19 +02:00
Emrik Östling
2b4b8f9551
Merge pull request #148 from Aymendje/feat-assimp 2024-09-26 23:01:33 +02:00
Aymen Djellal
63a4328d4a feat: Add support for 3d assets through assimp converter
This is a start for #144
It does not support all the 3d formats, but its a good few
2024-09-26 22:55:42 +02:00
Emrik Östling
413f5dc7b4
Merge pull request #138 from C4illin/release-please--branches--main--components--convertx-frontend 2024-09-25 23:56:29 +02:00
Emrik Östling
ebccdf9169 chore(main): release 0.6.0 2024-09-25 23:47:47 +02:00
C4illin
47139a550b fix: rename css file to force update cache, fixes #141 2024-09-25 23:47:18 +02:00
C4illin
fa5446c446 chore: fix eslint config 2024-09-25 23:46:21 +02:00
Emrik Östling
8772e582b0
Merge pull request #142 from Aymendje/patch-2 2024-09-25 23:31:23 +02:00
Aymen Djellal
45922ed3a3
[FIX] Fix broken CSS
This is a fix for #141 
The CSS was broken due to the import in the tailwind.config.js, it should be inside the module.exports and not outside. Thats why bun was giving a warning before :

```
# bun run dev
warn - The `content` option in your Tailwind CSS configuration is missing or empty.
warn - Configure your content sources or your generated CSS will be missing styles.
warn - https://tailwindcss.com/docs/content-configuration
```

it is now fixed
2024-09-25 09:12:54 -04:00
C4illin
4c747e8908 chore: format and update deps 2024-09-24 23:49:14 +02:00
Emrik Östling
e573997aa9
Merge pull request #140 from Aymendje/patch-1
Fix UNAUTHENTICATED mode
2024-09-24 09:12:28 +02:00
Aymen Djellal
c57b69991c
Fix UNAUTHENTICATED mode
the function used here, randmInt(Min, Max) has an issue.
When running the code, I get a 500 error, with the error being 

```
 |       const newUserId = String(randomInt(2 ** 24,  Number.MAX_SAFE_INTEGER));
      ^
RangeError: The "max - min" is out of range. It must be <= 281474976710655. Received 9007199237963775
 code: "ERR_OUT_OF_RANGE"

      at randomInt (native:1:1)
      at /.../ConvertX/src/index.tsx:460:32
      at /.../ConvertX/src/index.tsx:594:29
      at file:///.../ConvertX/node_modules/elysia/dist/bun/index.js:76:22
```

When digging deeper in the implementation, it seems that the official node doc says : 
> The range (max - min) must be less than 2**48. min and max must be safe integers.

See : https://nodejs.org/api/crypto.html#cryptorandomintmin-max-callback

Feel free to close this PR and do the fix another way (it: by using a uuid instead of randomInt, etc.)
2024-09-23 11:44:37 -04:00
Emrik Östling
eee983a56a
Merge pull request #137 from C4illin/feature-tailwind
feat: ui remake with tailwind
2024-09-23 10:20:01 +02:00
C4illin
22f823c535 feat: ui remake with tailwind 2024-09-23 03:58:29 +02:00
Emrik Östling
ed59cd7aa4
chore: clarify that batch processing is possible 2024-09-22 23:53:37 +02:00
Emrik Östling
b28977ffe2
Merge pull request #136 from C4illin/release-please--branches--main--components--convertx-frontend 2024-09-21 14:33:34 +02:00
deepsource-io[bot]
a47bb682a5
ci: add .deepsource.toml 2024-09-20 14:28:59 +00:00
C4illin
a17eca0a09 chore: format 2024-09-20 13:27:54 +02:00
Emrik Östling
ea9250543e chore(main): release 0.5.0 2024-09-20 13:24:55 +02:00
C4illin
317c932c2a feat: add option to customize how often files are automatically deleted 2024-09-20 13:24:18 +02:00
C4illin
5b1703db68 chore: add safe attribute to input element 2024-09-20 12:55:00 +02:00
C4illin
60ba7c93fb fix: improve file name replacement logic 2024-09-20 12:49:19 +02:00
C4illin
22227130dd chore: add screenshot, fixes #110 2024-09-20 12:42:58 +02:00
Emrik Östling
5daf66f5d0
Merge pull request #135 from C4illin/renovate/oven-bun-1.x 2024-09-20 12:30:55 +02:00
renovate[bot]
aee1962607
chore(deps): update oven/bun docker tag to v1.1.29 2024-09-20 09:09:55 +00:00
Emrik Östling
0d42762b36
Merge pull request #134 from C4illin/renovate/biomejs-biome-1.x 2024-09-19 17:51:12 +02:00
renovate[bot]
b97b12b449
chore(deps): update dependency @biomejs/biome to v1.9.2 2024-09-19 14:43:48 +00:00
Emrik Östling
bdf651df82
Merge pull request #133 from C4illin/renovate/oven-bun-1.x
chore(deps): update oven/bun docker tag to v1.1.28
2024-09-19 09:35:06 +02:00
renovate[bot]
267ef14789
chore(deps): update oven/bun docker tag to v1.1.28 2024-09-18 20:12:09 +00:00
Emrik Östling
905adc5e1c
Merge pull request #130 from C4illin/release-please--branches--main--components--convertx-frontend 2024-09-18 16:19:47 +02:00
Emrik Östling
52ed7274e9 chore(main): release 0.4.1 2024-09-15 23:24:06 +02:00
Emrik Östling
a29238c265
Merge pull request #132 from C4illin/renovate/biomejs-biome-1.x
chore(deps): update dependency @biomejs/biome to v1.9.1
2024-09-15 23:23:41 +02:00
renovate[bot]
48c6fb79fc
chore(deps): update dependency @biomejs/biome to v1.9.1 2024-09-15 20:09:54 +00:00
Emrik Östling
8358396656
Merge pull request #131 from C4illin/renovate/biomejs-biome-1.x 2024-09-12 17:40:23 +02:00
renovate[bot]
b30e5800c3
chore(deps): update dependency @biomejs/biome to v1.9.0 2024-09-12 15:03:57 +00:00
Emrik Östling
21a1b50ed8
Merge pull request #129 from C4illin/fix/#122/lowercase-env-variables 2024-09-12 13:02:45 +02:00
C4illin
e6a94fb21d chore: format 2024-09-12 12:59:59 +02:00
C4illin
bef1710e33 fix: allow non lowercase true and false values, fixes #122 2024-09-12 12:58:28 +02:00
Emrik Östling
16b322d4e6
Merge pull request #128 from C4illin/renovate/eslint-plugin-isaacscript-4.x
chore(deps): update dependency eslint-plugin-isaacscript to v4
2024-09-12 09:32:38 +02:00
renovate[bot]
9bf64e42d5
chore(deps): update dependency eslint-plugin-isaacscript to v4 2024-09-11 21:56:10 +00:00
C4illin
5988fe8212 chore: fix docker run command fixes #127 2024-09-09 13:17:37 +02:00
Emrik Östling
5df9c0b751
Merge pull request #126 from C4illin/renovate/oven-bun-1.x
chore(deps): update oven/bun docker tag to v1.1.27
2024-09-07 20:40:34 +02:00
renovate[bot]
136a8b2d74
chore(deps): update oven/bun docker tag to v1.1.27 2024-09-07 13:48:15 +00:00
C4illin
ccfb574d5d chore: Update dependencies 2024-09-05 12:22:27 +02:00
Emrik Östling
ad6eedea69
chore: Update README.md 2024-08-27 18:19:07 +02:00
Emrik Östling
c3082db8f7
Merge pull request #118 from C4illin/release-please--branches--main--components--convertx-frontend 2024-08-26 16:20:32 +02:00
Emrik Östling
a1f8cbae66 chore(main): release 0.4.0 2024-08-26 16:01:20 +02:00
C4illin
bb34bdee87 Merge branch 'main' of https://github.com/C4illin/ConvertX 2024-08-26 16:00:15 +02:00
C4illin
11fcbc3f96 t push origin main Merge branch 'luis-c465-searchable-formats' 2024-08-26 15:57:48 +02:00
Emrik Östling
f7344e4c65
Merge pull request #121 from C4illin/renovate/total-typescript-ts-reset-0.x 2024-08-25 14:42:31 +02:00
Emrik Östling
781310f3dc
Merge pull request #120 from C4illin/renovate/oven-bun-1.x
chore(deps): update oven/bun docker tag to v1.1.26
2024-08-24 19:35:33 +02:00
renovate[bot]
3f063644f2
chore(deps): update dependency @total-typescript/ts-reset to ^0.6.0 2024-08-24 12:11:04 +00:00
renovate[bot]
081634b610
chore(deps): update oven/bun docker tag to v1.1.26 2024-08-24 11:41:24 +00:00
C4illin
cf3da08c73 docs: add dev instructions 2024-08-23 22:07:11 +02:00
Luis Canada
5f7234d6c1 Merge remote-tracking branch 'upstream/main' into searchable-formats 2024-08-23 14:20:16 -04:00
C4illin
6597c1d7ca feat: add robots.txt 2024-08-23 20:14:34 +02:00
Emrik Östling
ecb2c75008
Merge pull request #119 from C4illin/feature-resvg 2024-08-23 19:58:14 +02:00
C4illin
d5eeef9f68 feat: add resvg converter 2024-08-23 19:56:43 +02:00
C4illin
7456174022 chore(deps): update dependencies 2024-08-23 19:16:25 +02:00
C4illin
bc4ad49285 fix: keep unauthenticated user logged in if allowed #114 2024-08-23 15:18:43 +02:00
C4illin
f0d0e43929 feat: add option for unauthenticated file conversions #114 2024-08-23 15:09:49 +02:00
C4illin
8ca4f1587d fix: pdf support in vips 2024-08-23 14:30:02 +02:00
Emrik Östling
1535377bfe
Merge pull request #117 from C4illin/renovate/oven-bun-1.x 2024-08-23 11:25:31 +02:00
renovate[bot]
83bf78fd57
chore(deps): update oven/bun docker tag to v1.1.25 2024-08-21 06:35:24 +00:00
Luis Canada
4d9c4d64aa
fix: Slow click on conversion popup does not work 2024-08-20 18:10:34 -04:00
Luis Canada
53fff594fc
feat: Add search bar for formats 2024-08-20 14:59:25 -04:00
Emrik Östling
fe4aeaff03
Merge pull request #115 from 101br03k/patch-1
added container name and restart policy to deployement example
2024-08-17 21:57:08 +02:00
A3
2078cb0ee0
added container name and restart policy 2024-08-17 21:31:21 +02:00
Emrik Östling
86a61d35d7
Merge pull request #112 from C4illin/renovate/oven-bun-1.x
chore(deps): update oven/bun docker tag to v1.1.24
2024-08-16 10:10:23 +02:00
renovate[bot]
96fa7e2f55
chore(deps): update oven/bun docker tag to v1.1.24 2024-08-14 12:53:14 +00:00
Emrik Östling
7d2af46b0b
Merge pull request #111 from C4illin/renovate/oven-bun-1.x
chore(deps): update oven/bun docker tag to v1.1.23
2024-08-14 09:57:08 +02:00
renovate[bot]
57e2999866
chore(deps): update oven/bun docker tag to v1.1.23 2024-08-14 01:52:09 +00:00
Emrik Östling
6fb8ca4d82
Merge pull request #109 from C4illin/renovate/oven-bun-1.x
chore(deps): update oven/bun docker tag to v1.1.22
2024-08-12 09:04:37 +02:00
renovate[bot]
c295e546bd
chore(deps): update oven/bun docker tag to v1.1.22 2024-08-08 01:44:24 +00:00
Emrik Östling
f7abb9389c
chore: move to renovate 2024-08-06 22:06:45 +02:00
Emrik Östling
d7de154eda
Merge pull request #108 from C4illin/dependabot/npm_and_yarn/npm-run-all2-tw-6.2.2
build(deps-dev): update npm-run-all2 requirement from ^6.0.0 to ^6.2.2
2024-08-06 16:52:37 +02:00
dependabot[bot]
20bd111765
build(deps-dev): update npm-run-all2 requirement from ^6.0.0 to ^6.2.2
Updates the requirements on [npm-run-all2](https://github.com/bcomnes/npm-run-all2) to permit the latest version.
- [Release notes](https://github.com/bcomnes/npm-run-all2/releases)
- [Changelog](https://github.com/bcomnes/npm-run-all2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/bcomnes/npm-run-all2/compare/v6.0.0...v6.2.2)

---
updated-dependencies:
- dependency-name: npm-run-all2
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-06 08:12:26 +00:00
Emrik Östling
eadd0da291
Merge pull request #107 from C4illin/renovate/npm-run-all2-6.x 2024-08-05 21:58:52 +02:00
C4illin
52294465fb chore: fix type errors 2024-08-05 21:57:40 +02:00
renovate[bot]
049e9163ce
chore(deps): update dependency npm-run-all2 to v6 2024-08-05 19:31:50 +00:00
C4illin
d466d2dbbc Merge branch 'main' of https://github.com/C4illin/ConvertX 2024-08-05 21:31:06 +02:00
C4illin
3f79ccaa2a chore: Update eslint configuration and dependencies 2024-08-05 21:25:35 +02:00
C4illin
1e9bde18c7 chore: use renovate instead 2024-08-05 21:25:18 +02:00
Emrik Östling
9af23346bf
Merge pull request #103 from C4illin/renovate/npm-run-all-replacement 2024-08-05 21:24:13 +02:00
renovate[bot]
d310341fca
chore(deps): replace dependency npm-run-all with npm-run-all2 ^5.0.0 2024-08-05 19:24:04 +00:00
Emrik Östling
d88a755c13
Merge pull request #101 from C4illin/dependabot/npm_and_yarn/elysia-tw-1.1.5 2024-08-05 21:23:12 +02:00
Emrik Östling
7c6085c685
Merge pull request #102 from C4illin/renovate/configure 2024-08-05 20:35:48 +02:00
renovate[bot]
7ed1ad21f2
Add renovate.json 2024-08-05 18:34:31 +00:00
dependabot[bot]
8a2237fbd9 build(deps): update elysia requirement from ^1.1.4 to ^1.1.5
---
updated-dependencies:
- dependency-name: elysia
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-05 14:50:25 +00:00
Emrik Östling
0e363f0731
Merge pull request #100 from C4illin/dependabot/npm_and_yarn/types/node-tw-22.1.0
build(deps-dev): update @types/node requirement from ^22.0.3 to ^22.1.0
2024-08-05 16:49:04 +02:00
dependabot[bot]
4074647b67 build(deps-dev): update @types/node requirement from ^22.0.3 to ^22.1.0
---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-05 08:55:13 +00:00
Emrik Östling
c84968be50
Merge pull request #98 from C4illin/dependabot/npm_and_yarn/types/node-tw-22.0.3
build(deps-dev): update @types/node requirement from ^22.0.2 to ^22.0.3
2024-08-02 16:45:25 +02:00
dependabot[bot]
0e53a99d43 build(deps-dev): update @types/node requirement from ^22.0.2 to ^22.0.3
---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-02 10:43:50 +00:00
Emrik Östling
bdd0cf556f
Merge pull request #99 from C4illin/dependabot/npm_and_yarn/typescript-eslint/parser-8.0.0
build(deps-dev): bump @typescript-eslint/parser from 7.18.0 to 8.0.0
2024-08-02 12:42:23 +02:00
dependabot[bot]
2483274388 build(deps-dev): bump @typescript-eslint/parser from 7.18.0 to 8.0.0
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 7.18.0 to 8.0.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.0.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-02 08:58:10 +00:00
Emrik Östling
4c5129910a
Merge pull request #96 from C4illin/dependabot/npm_and_yarn/typescript-eslint/eslint-plugin-8.0.0
build(deps-dev): bump @typescript-eslint/eslint-plugin from 7.18.0 to 8.0.0
2024-08-01 12:30:43 +02:00
dependabot[bot]
fe13a1b736 build(deps-dev): bump @typescript-eslint/eslint-plugin
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 7.18.0 to 8.0.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.0.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-01 09:12:49 +00:00
Emrik Östling
f1ac71b397
Merge pull request #97 from C4illin/dependabot/npm_and_yarn/types/node-tw-22.0.2
build(deps-dev): update @types/node requirement from ^22.0.0 to ^22.0.2
2024-08-01 11:11:16 +02:00
dependabot[bot]
1b1067a03f build(deps-dev): update @types/node requirement from ^22.0.0 to ^22.0.2
---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-01 08:44:36 +00:00
132 changed files with 8196 additions and 2853 deletions

1
.bun-version Normal file
View file

@ -0,0 +1 @@
1.2.2

70
.devcontainer/Dockerfile Normal file
View file

@ -0,0 +1,70 @@
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 \
python3-tinycss2 \
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"]

View 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"
}
}

View file

@ -1,16 +1,25 @@
node_modules
Dockerfile*
docker-compose*
.dockerignore
.git
.gitignore
README.md
LICENSE
.vscode
Makefile
helm-charts
.env
.editorconfig
.idea
coverage*
data
.dockerignore
.editorconfig
.env
.git
.gitignore
.github
.idea
.vscode
biome.json
CHANGELOG.md
compose.yaml
coverage*
data
docker-compose*
Dockerfile*
eslint.config.js
helm-charts
images
LICENSE
Makefile
node_modules
prettier.config.js
README.md
renovate.json
SECURITY.md

View file

@ -1,46 +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"],
},
overrides: [
// Template files don't have reliable type information
{
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",
},
};
module.exports = config;

15
.github/FUNDING.yml vendored Normal file
View 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
View 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`

View 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. -->

View 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.

View file

@ -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
View 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:
- "*"

View file

@ -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@v2
- 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

33
.github/workflows/check-lint.yml vendored Normal file
View file

@ -0,0 +1,33 @@
name: Check Lint
permissions:
contents: read
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@v6
- 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

View file

@ -0,0 +1,23 @@
on:
pull_request_target:
types: [opened, edited]
name: conventional-release-labels
permissions:
contents: read
pull-requests: write
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"
}

View file

@ -1,69 +1,173 @@
name: Docker
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# thanks to https://github.com/sredevopsorg/multi-arch-docker-github-workflow
on:
push:
branches: [ "main" ]
# Publish semver tags as releases.
tags: [ 'v*.*.*' ]
branches: ["main"]
tags: ["v*.*.*"]
pull_request:
branches: [ "main" ]
branches: ["main"]
workflow_dispatch:
env:
# Use docker.io for Docker Hub if empty
REGISTRY: ghcr.io
# github.repository as <account>/<repo>
IMAGE_NAME: ${{ github.repository }}
DOCKERHUB_USERNAME: c4illin
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
# The build job builds the Docker image for each platform specified in the matrix.
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
- linux/arm64
permissions:
contents: read
contents: write
packages: write
attestations: write
id-token: write
runs-on: ${{ matrix.platform == 'linux/amd64' && 'ubuntu-24.04' || matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' }}
name: Build Docker image for ${{ matrix.platform }}
steps:
- name: Prepare environment for current platform
# This step sets up the environment for the current platform being built.
# It replaces the '/' character in the platform name with '-' and sets it as an environment variable.
# This is useful for naming artifacts and other resources that cannot contain '/'.
# The environment variable PLATFORMS_PAIR will be used later in the workflow.
id: prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
# Workaround: https://github.com/docker/build-push-action/issues/461
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v3
- name: downcase REPO
run: |
echo "REPO=${GITHUB_REPOSITORY@L}" >> "${GITHUB_ENV}"
# 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'
uses: docker/login-action@v3
- name: Docker meta default
id: meta
uses: docker/metadata-action@v5
with:
registry: ${{ env.REGISTRY }}
images: ghcr.io/${{ env.REPO }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
platforms: ${{ matrix.platform }}
- name: Login to GitHub Container Registry
# here we only login to ghcr.io since the this only pushes internal images
uses: docker/login-action@v4
with:
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@v7
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@v8
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- name: downcase REPO
run: |
echo "REPO=${GITHUB_REPOSITORY@L}" >> "${GITHUB_ENV}"
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
images: |
ghcr.io/${{ env.REPO }}
${{ env.IMAGE_NAME }}
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
with:
context: .
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@v4
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 }}'

View file

@ -0,0 +1,29 @@
name: Update Docker Hub Description
permissions:
contents: read
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@v6
- 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

View file

@ -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

View file

@ -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 }}

33
.github/workflows/run-bun-test.yml vendored Normal file
View file

@ -0,0 +1,33 @@
name: Check Tests
permissions:
contents: read
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@v6
- 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

102
.gitignore vendored
View file

@ -1,50 +1,52 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
# vercel
.vercel
**/*.trace
**/*.zip
**/*.tar.gz
**/*.tgz
**/*.log
package-lock.json
**/*.bun
/src/uploads
/uploads
/mydb.sqlite
/output
/db
/data
/Bruno
/tsconfig.tsbuildinfo
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
# vercel
.vercel
**/*.trace
**/*.zip
**/*.tar.gz
**/*.tgz
**/*.log
package-lock.json
**/*.bun
/src/uploads
/uploads
/mydb.sqlite
/output
/db
/data
/dist
/Bruno
/tsconfig.tsbuildinfo
/public/generated.css

View file

@ -1,4 +1,4 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}
}

View file

@ -1,65 +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))
- downgrade @elysiajs/html dependency to version 1.0.2 ([c714ade](https://github.com/C4illin/ConvertX/commit/c714ade3e23865ba6cfaf76c9e7259df1cda222c))
## [0.3.2](https://github.com/C4illin/ConvertX/compare/v0.3.1...v0.3.2) (2024-07-09)
### Bug Fixes
* increase max request body to support large uploads ([3ae2db5](https://github.com/C4illin/ConvertX/commit/3ae2db5d9b36fe3dcd4372ddcd32aa573ea59aa6)), closes [#64](https://github.com/C4illin/ConvertX/issues/64)
- increase max request body to support large uploads ([3ae2db5](https://github.com/C4illin/ConvertX/commit/3ae2db5d9b36fe3dcd4372ddcd32aa573ea59aa6)), closes [#64](https://github.com/C4illin/ConvertX/issues/64)
## [0.3.1](https://github.com/C4illin/ConvertX/compare/v0.3.0...v0.3.1) (2024-06-27)
### Bug Fixes
* release releases ([4d4c13a](https://github.com/C4illin/ConvertX/commit/4d4c13a8d85ec7c9209ad41cdbea7d4380b0edbf))
- release releases ([4d4c13a](https://github.com/C4illin/ConvertX/commit/4d4c13a8d85ec7c9209ad41cdbea7d4380b0edbf))
## [0.3.0](https://github.com/C4illin/ConvertX/compare/v0.2.0...v0.3.0) (2024-06-27)
### Features
* add version number to log ([4dcb796](https://github.com/C4illin/ConvertX/commit/4dcb796e1bd27badc078d0638076cd9f1e81c4a4)), closes [#44](https://github.com/C4illin/ConvertX/issues/44)
* change to xelatex ([fae2ba9](https://github.com/C4illin/ConvertX/commit/fae2ba9c54461dccdccd1bfb5e76398540d11d0b))
* print version of installed converters to log ([801cf28](https://github.com/C4illin/ConvertX/commit/801cf28d1e5edac9353b0b16be75a4fb48470b8a))
- add version number to log ([4dcb796](https://github.com/C4illin/ConvertX/commit/4dcb796e1bd27badc078d0638076cd9f1e81c4a4)), closes [#44](https://github.com/C4illin/ConvertX/issues/44)
- change to xelatex ([fae2ba9](https://github.com/C4illin/ConvertX/commit/fae2ba9c54461dccdccd1bfb5e76398540d11d0b))
- print version of installed converters to log ([801cf28](https://github.com/C4illin/ConvertX/commit/801cf28d1e5edac9353b0b16be75a4fb48470b8a))
## [0.2.0](https://github.com/C4illin/ConvertX/compare/v0.1.2...v0.2.0) (2024-06-20)
### Features
* add libjxl for jpegxl conversion ([ff680cb](https://github.com/C4illin/ConvertX/commit/ff680cb29534a25c3148a90fd064bb86c71fb482))
* change from debian to alpine ([1316957](https://github.com/C4illin/ConvertX/commit/13169574f0134ae236f8d41287bb73930b575e82)), closes [#34](https://github.com/C4illin/ConvertX/issues/34)
- add libjxl for jpegxl conversion ([ff680cb](https://github.com/C4illin/ConvertX/commit/ff680cb29534a25c3148a90fd064bb86c71fb482))
- change from debian to alpine ([1316957](https://github.com/C4illin/ConvertX/commit/13169574f0134ae236f8d41287bb73930b575e82)), closes [#34](https://github.com/C4illin/ConvertX/issues/34)
## [0.1.2](https://github.com/C4illin/ConvertX/compare/v0.1.1...v0.1.2) (2024-06-10)
### Bug Fixes
* fix incorrect redirect ([25df58b](https://github.com/C4illin/ConvertX/commit/25df58ba82321aaa6617811a6995cb96c2a00a40)), closes [#23](https://github.com/C4illin/ConvertX/issues/23)
- fix incorrect redirect ([25df58b](https://github.com/C4illin/ConvertX/commit/25df58ba82321aaa6617811a6995cb96c2a00a40)), closes [#23](https://github.com/C4illin/ConvertX/issues/23)
## [0.1.1](https://github.com/C4illin/ConvertX/compare/v0.1.0...v0.1.1) (2024-05-30)
### Bug Fixes
* :bug: make sure all redirects are 302 ([9970fd3](https://github.com/C4illin/ConvertX/commit/9970fd3f89190af96f8762edc3817d1e03082b3a)), closes [#12](https://github.com/C4illin/ConvertX/issues/12)
- :bug: make sure all redirects are 302 ([9970fd3](https://github.com/C4illin/ConvertX/commit/9970fd3f89190af96f8762edc3817d1e03082b3a)), closes [#12](https://github.com/C4illin/ConvertX/issues/12)
## 0.1.0 (2024-05-30)
### Features
* remove file from file list in index.html ([787ff97](https://github.com/C4illin/ConvertX/commit/787ff9741ecbbf4fb4c02b43bd22a214a173fd7b))
- remove file from file list in index.html ([787ff97](https://github.com/C4illin/ConvertX/commit/787ff9741ecbbf4fb4c02b43bd22a214a173fd7b))
### Miscellaneous Chores
* release 0.1.0 ([54d9aec](https://github.com/C4illin/ConvertX/commit/54d9aecbf949689b12aa7e5e8e9be7b9032f4431))
- release 0.1.0 ([54d9aec](https://github.com/C4illin/ConvertX/commit/54d9aecbf949689b12aa7e5e8e9be7b9032f4431))

View file

@ -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" ]

View file

@ -1,55 +1,113 @@
FROM oven/bun:1.1.21-alpine 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
# 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 apk --no-cache add \
pandoc \
texlive \
texlive-xetex \
texmf-dist-latexextra \
ffmpeg \
graphicsmagick \
ghostscript \
vips-tools \
libjxl-tools
# this might be needed for some latex use cases, will add it if needed.
# texmf-dist-fontsextra \
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
ENV NODE_ENV=production
ENTRYPOINT [ "bun", "run", "./src/index.tsx" ]
FROM debian:testing-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 \
ca-certificates \
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.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.lock /temp/prod/
RUN cd /temp/prod && bun install --frozen-lockfile --production
FROM base AS prerelease
WORKDIR /app
COPY --from=install /temp/dev/node_modules node_modules
COPY . .
# ENV NODE_ENV=production
RUN bun run build
# copy production dependencies and source code into final image
FROM base AS release
# install additional dependencies
RUN apt-get update && apt-get install -y \
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 \
python3-tinycss2 \
resvg \
texlive \
texlive-fonts-recommended \
texlive-latex-extra \
texlive-latex-recommended \
texlive-xetex \
python3 \
python3-pip \
pipx \
--no-install-recommends \
&& pipx install "markitdown[all]" \
&& rm -rf /var/lib/apt/lists/*
# Add pipx bin directory to PATH
ENV PATH="/root/.local/bin:${PATH}"
# 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/public/ /app/public/
COPY --from=prerelease /app/dist /app/dist
# COPY . .
RUN mkdir data
EXPOSE 3000/tcp
# used for calibre
ENV QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox"
ENV NODE_ENV=production
ENTRYPOINT [ "bun", "run", "dist/src/index.js" ]

1322
LICENSE

File diff suppressed because it is too large Load diff

View file

@ -1,87 +0,0 @@
![ConvertX](images/logo.png)
# ConvertX
[![Docker](https://github.com/C4illin/ConvertX/actions/workflows/docker-publish.yml/badge.svg?branch=main)](https://github.com/C4illin/ConvertX/actions/workflows/docker-publish.yml)
[![GitHub Release](https://img.shields.io/github/v/release/C4illin/ConvertX)](https://github.com/C4illin/ConvertX/pkgs/container/convertx)
![GitHub commits since latest release](https://img.shields.io/github/commits-since/C4illin/ConvertX/latest)
![GitHub repo size](https://img.shields.io/github/repo-size/C4illin/ConvertX)
![Docker container size](https://ghcr-badge.egpl.dev/c4illin/convertx/size?color=%230375b6&tag=latest&label=image+size&trim=)
![GitHub top language](https://img.shields.io/github/languages/top/C4illin/ConvertX)
A self-hosted online file converter. Supports 831 different formats. Written with TypeScript, Bun and Elysia.
## Features
- Convert files to different formats
- 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 |
| [XeLaTeX](https://tug.org/xetex/) | 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 |
<!-- many ffmpeg fileformats are duplicates -->
Any missing converter? Open an issue or pull request!
## Deployment
```yml
# docker-compose.yml
services:
convertx:
image: ghcr.io/c4illin/convertx
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
volumes:
- convertx:/app/data
```
<!-- or
```bash
docker run ghcr.io/c4illin/convertx:master -p 3000:3000 -e ACCOUNT_REGISTRATION=false -v /path/you/want:/app/data
``` -->
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.
### Tutorial
Tutorial in french: https://belginux.com/installer-convertx-avec-docker/
## 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.
## Contributors
<a href="https://github.com/C4illin/ConvertX/graphs/contributors">
<img src="https://contrib.rocks/image?repo=C4illin/ConvertX" />
</a>
## Star History
<a href="https://github.com/C4illin/ConvertX/stargazers">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=C4illin/ConvertX&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=C4illin/ConvertX&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=C4illin/ConvertX&type=Date" />
</picture>
</a>

9
SECURITY.md Normal file
View 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.

View file

@ -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"
}
}
},

668
bun.lock Normal file
View file

@ -0,0 +1,668 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "convertx-frontend",
"dependencies": {
"@elysiajs/html": "^1.4.2",
"@elysiajs/jwt": "^1.4.2",
"@elysiajs/static": "^1.4.10",
"@kitajs/html": "^4.2.13",
"elysia": "1.4.22",
"sanitize-filename": "^1.6.4",
"tar": "^7.5.16",
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@kitajs/ts-html-plugin": "^4.1.4",
"@tailwindcss/cli": "^4.3.1",
"@tailwindcss/postcss": "^4.3.1",
"@types/bun": "latest",
"@types/node": "^24.13.2",
"eslint": "^10.5.0",
"eslint-plugin-better-tailwindcss": "^4.6.0",
"globals": "^17.6.0",
"knip": "^5.88.1",
"npm-run-all2": "^8.0.4",
"postcss": "^8.5.15",
"prettier": "^3.8.4",
"tailwind-scrollbar": "^4.0.2",
"tailwindcss": "^4.3.1",
"typescript": "^5.9.3",
"typescript-eslint": "^8.61.1",
},
},
},
"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.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="],
"@elysiajs/html": ["@elysiajs/html@1.4.2", "", { "dependencies": { "@kitajs/html": "^4.1.0", "@kitajs/ts-html-plugin": "^4.0.1" }, "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-Db7dmbkN7gptckMpU0/Fq9Qi3QuhQr/CH60A+8rs+RT+74NUC8sONs5nkfMm5oL+6kCUWCv19uUwOjBP7zsjYQ=="],
"@elysiajs/jwt": ["@elysiajs/jwt@1.4.2", "", { "dependencies": { "jose": "^6.0.11" }, "peerDependencies": { "elysia": ">= 1.4.27" } }, "sha512-gSPjF8KoL7cQIxL3kb+Ui9g5jpBdO4iUTe2+F52w66JVFct2K8cTLum8PEArxRZNg4PBrg100h6i1bJ2zfcqMg=="],
"@elysiajs/static": ["@elysiajs/static@1.4.10", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-BN/3PZVWjqkL/6TM7RH69Pae89CfU0OqfEYPeBp2hB/GR5Euds39Wy+0ug9Vbvu++GG4tRRANf2QjaLnriJveg=="],
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
"@eslint/config-array": ["@eslint/config-array@0.23.5", "", { "dependencies": { "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA=="],
"@eslint/config-helpers": ["@eslint/config-helpers@0.6.0", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA=="],
"@eslint/core": ["@eslint/core@1.2.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ=="],
"@eslint/css-tree": ["@eslint/css-tree@4.0.4", "", { "dependencies": { "mdn-data": "2.28.1", "source-map-js": "^1.2.1" } }, "sha512-nxMparyhqVWQvadx9x8dIfubfIPOE+X2b2waua8fzdnM9vdp9rgVtwEZlG0TmCwEUz/d/f40fzvO/eqBwdxz0A=="],
"@eslint/js": ["@eslint/js@10.0.1", "", { "peerDependencies": { "eslint": "^10.0.0" }, "optionalPeers": ["eslint"] }, "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA=="],
"@eslint/object-schema": ["@eslint/object-schema@3.0.5", "", {}, "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw=="],
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.2", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A=="],
"@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.13", "", { "dependencies": { "csstype": "^3.1.3" } }, "sha512-o+8e61EsoLDPTP7rsPkYolca1YFybHuxU2Lr5fWDZCUkYT/6uBlVkvnZUdCXMQKentJL9dxwpR8/xK2Q+U4LhA=="],
"@kitajs/ts-html-plugin": ["@kitajs/ts-html-plugin@4.1.4", "", { "dependencies": { "chalk": "^5.6.2", "tslib": "^2.8.1", "yargs": "^18.0.0" }, "peerDependencies": { "@kitajs/html": "^4.2.10", "typescript": "^5.9.3" }, "bin": { "ts-html-plugin": "dist/cli.js", "xss-scan": "dist/cli.js" } }, "sha512-xK5mNrhnIy73kJFKx5yVGChJyWFRGmIaE0sjlVxVYllk5dyaEYVCrIh1N8AfnseEHka8gAqzPGW95HlkhDvnJA=="],
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
"@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.19.1", "", { "os": "android", "cpu": "arm" }, "sha512-aUs47y+xyXHUKlbhqHUjBABjvycq6YSD7bpxSW7vplUmdzAlJ93yXY6ZR0c1o1x5A/QKbENCvs3+NlY8IpIVzg=="],
"@oxc-resolver/binding-android-arm64": ["@oxc-resolver/binding-android-arm64@11.19.1", "", { "os": "android", "cpu": "arm64" }, "sha512-oolbkRX+m7Pq2LNjr/kKgYeC7bRDMVTWPgxBGMjSpZi/+UskVo4jsMU3MLheZV55jL6c3rNelPl4oD60ggYmqA=="],
"@oxc-resolver/binding-darwin-arm64": ["@oxc-resolver/binding-darwin-arm64@11.19.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-nUC6d2i3R5B12sUW4O646qD5cnMXf2oBGPLIIeaRfU9doJRORAbE2SGv4eW6rMqhD+G7nf2Y8TTJTLiiO3Q/dQ=="],
"@oxc-resolver/binding-darwin-x64": ["@oxc-resolver/binding-darwin-x64@11.19.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-cV50vE5+uAgNcFa3QY1JOeKDSkM/9ReIcc/9wn4TavhW/itkDGrXhw9jaKnkQnGbjJ198Yh5nbX/Gr2mr4Z5jQ=="],
"@oxc-resolver/binding-freebsd-x64": ["@oxc-resolver/binding-freebsd-x64@11.19.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-xZOQiYGFxtk48PBKff+Zwoym7ScPAIVp4c14lfLxizO2LTTTJe5sx9vQNGrBymrf/vatSPNMD4FgsaaRigPkqw=="],
"@oxc-resolver/binding-linux-arm-gnueabihf": ["@oxc-resolver/binding-linux-arm-gnueabihf@11.19.1", "", { "os": "linux", "cpu": "arm" }, "sha512-lXZYWAC6kaGe/ky2su94e9jN9t6M0/6c+GrSlCqL//XO1cxi5lpAhnJYdyrKfm0ZEr/c7RNyAx3P7FSBcBd5+A=="],
"@oxc-resolver/binding-linux-arm-musleabihf": ["@oxc-resolver/binding-linux-arm-musleabihf@11.19.1", "", { "os": "linux", "cpu": "arm" }, "sha512-veG1kKsuK5+t2IsO9q0DErYVSw2azvCVvWHnfTOS73WE0STdLLB7Q1bB9WR+yHPQM76ASkFyRbogWo1GR1+WbQ=="],
"@oxc-resolver/binding-linux-arm64-gnu": ["@oxc-resolver/binding-linux-arm64-gnu@11.19.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-heV2+jmXyYnUrpUXSPugqWDRpnsQcDm2AX4wzTuvgdlZfoNYO0O3W2AVpJYaDn9AG4JdM6Kxom8+foE7/BcSig=="],
"@oxc-resolver/binding-linux-arm64-musl": ["@oxc-resolver/binding-linux-arm64-musl@11.19.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jvo2Pjs1c9KPxMuMPIeQsgu0mOJF9rEb3y3TdpsrqwxRM+AN6/nDDwv45n5ZrUnQMsdBy5gIabioMKnQfWo9ew=="],
"@oxc-resolver/binding-linux-ppc64-gnu": ["@oxc-resolver/binding-linux-ppc64-gnu@11.19.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-vLmdNxWCdN7Uo5suays6A/+ywBby2PWBBPXctWPg5V0+eVuzsJxgAn6MMB4mPlshskYbppjpN2Zg83ArHze9gQ=="],
"@oxc-resolver/binding-linux-riscv64-gnu": ["@oxc-resolver/binding-linux-riscv64-gnu@11.19.1", "", { "os": "linux", "cpu": "none" }, "sha512-/b+WgR+VTSBxzgOhDO7TlMXC1ufPIMR6Vj1zN+/x+MnyXGW7prTLzU9eW85Aj7Th7CCEG9ArCbTeqxCzFWdg2w=="],
"@oxc-resolver/binding-linux-riscv64-musl": ["@oxc-resolver/binding-linux-riscv64-musl@11.19.1", "", { "os": "linux", "cpu": "none" }, "sha512-YlRdeWb9j42p29ROh+h4eg/OQ3dTJlpHSa+84pUM9+p6i3djtPz1q55yLJhgW9XfDch7FN1pQ/Vd6YP+xfRIuw=="],
"@oxc-resolver/binding-linux-s390x-gnu": ["@oxc-resolver/binding-linux-s390x-gnu@11.19.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-EDpafVOQWF8/MJynsjOGFThcqhRHy417sRyLfQmeiamJ8qVhSKAn2Dn2VVKUGCjVB9C46VGjhNo7nOPUi1x6uA=="],
"@oxc-resolver/binding-linux-x64-gnu": ["@oxc-resolver/binding-linux-x64-gnu@11.19.1", "", { "os": "linux", "cpu": "x64" }, "sha512-NxjZe+rqWhr+RT8/Ik+5ptA3oz7tUw361Wa5RWQXKnfqwSSHdHyrw6IdcTfYuml9dM856AlKWZIUXDmA9kkiBQ=="],
"@oxc-resolver/binding-linux-x64-musl": ["@oxc-resolver/binding-linux-x64-musl@11.19.1", "", { "os": "linux", "cpu": "x64" }, "sha512-cM/hQwsO3ReJg5kR+SpI69DMfvNCp+A/eVR4b4YClE5bVZwz8rh2Nh05InhwI5HR/9cArbEkzMjcKgTHS6UaNw=="],
"@oxc-resolver/binding-openharmony-arm64": ["@oxc-resolver/binding-openharmony-arm64@11.19.1", "", { "os": "none", "cpu": "arm64" }, "sha512-QF080IowFB0+9Rh6RcD19bdgh49BpQHUW5TajG1qvWHvmrQznTZZjYlgE2ltLXyKY+qs4F/v5xuX1XS7Is+3qA=="],
"@oxc-resolver/binding-wasm32-wasi": ["@oxc-resolver/binding-wasm32-wasi@11.19.1", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-w8UCKhX826cP/ZLokXDS6+milN8y4X7zidsAttEdWlVoamTNf6lhBJldaWr3ukTDiye7s4HRcuPEPOXNC432Vg=="],
"@oxc-resolver/binding-win32-arm64-msvc": ["@oxc-resolver/binding-win32-arm64-msvc@11.19.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-nJ4AsUVZrVKwnU/QRdzPCCrO0TrabBqgJ8pJhXITdZGYOV28TIYystV1VFLbQ7DtAcaBHpocT5/ZJnF78YJPtQ=="],
"@oxc-resolver/binding-win32-ia32-msvc": ["@oxc-resolver/binding-win32-ia32-msvc@11.19.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-EW+ND5q2Tl+a3pH81l1QbfgbF3HmqgwLfDfVithRFheac8OTcnbXt/JxqD2GbDkb7xYEqy1zNaVFRr3oeG8npA=="],
"@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@11.19.1", "", { "os": "win32", "cpu": "x64" }, "sha512-6hIU3RQu45B+VNTY4Ru8ppFwjVS/S5qwYyGhBotmjxfEKk41I2DlGtRfGJndZ5+6lneE2pwloqunlOyZuX/XAw=="],
"@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.48", "", {}, "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA=="],
"@tailwindcss/cli": ["@tailwindcss/cli@4.3.1", "", { "dependencies": { "@parcel/watcher": "2.5.1", "@tailwindcss/node": "4.3.1", "@tailwindcss/oxide": "4.3.1", "enhanced-resolve": "5.21.6", "mri": "^1.2.0", "picocolors": "^1.1.1", "tailwindcss": "4.3.1" }, "bin": { "tailwindcss": "dist/index.mjs" } }, "sha512-ZWPy20rF+TBfTImxDMG3Wr75Y3RpaPlo9lc+oJbInlMyjT+XPkTVKVIL5RZ7JirXuIahcfHoLNFRmDorKi+JQQ=="],
"@tailwindcss/node": ["@tailwindcss/node@4.3.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "5.21.6", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.1" } }, "sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.1", "@tailwindcss/oxide-darwin-arm64": "4.3.1", "@tailwindcss/oxide-darwin-x64": "4.3.1", "@tailwindcss/oxide-freebsd-x64": "4.3.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.1", "@tailwindcss/oxide-linux-arm64-musl": "4.3.1", "@tailwindcss/oxide-linux-x64-gnu": "4.3.1", "@tailwindcss/oxide-linux-x64-musl": "4.3.1", "@tailwindcss/oxide-wasm32-wasi": "4.3.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.1", "@tailwindcss/oxide-win32-x64-msvc": "4.3.1" } }, "sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.1", "", { "os": "android", "cpu": "arm64" }, "sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.1", "", { "os": "linux", "cpu": "arm" }, "sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.1", "", { "dependencies": { "@emnapi/core": "^1.10.0", "@emnapi/runtime": "^1.10.0", "@emnapi/wasi-threads": "^1.2.1", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.1", "", { "os": "win32", "cpu": "x64" }, "sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA=="],
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.3.1", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.3.1", "@tailwindcss/oxide": "4.3.1", "postcss": "8.5.15", "tailwindcss": "4.3.1" } }, "sha512-dNJuNbdEJT/SWRuXTYP1WSamelsz3ztkUsdtWQPjrexysrTpaEPM40P/71knXiXLYEojqPOEGitVLLpPMS5T6A=="],
"@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="],
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
"@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="],
"@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.13.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA=="],
"@types/prismjs": ["@types/prismjs@1.26.6", "", {}, "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.61.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.61.1", "@typescript-eslint/type-utils": "8.61.1", "@typescript-eslint/utils": "8.61.1", "@typescript-eslint/visitor-keys": "8.61.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.61.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.61.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.61.1", "@typescript-eslint/types": "8.61.1", "@typescript-eslint/typescript-estree": "8.61.1", "@typescript-eslint/visitor-keys": "8.61.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg=="],
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.61.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.61.1", "@typescript-eslint/types": "^8.61.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA=="],
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.61.1", "", { "dependencies": { "@typescript-eslint/types": "8.61.1", "@typescript-eslint/visitor-keys": "8.61.1" } }, "sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w=="],
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.61.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.61.1", "", { "dependencies": { "@typescript-eslint/types": "8.61.1", "@typescript-eslint/typescript-estree": "8.61.1", "@typescript-eslint/utils": "8.61.1", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.61.1", "", {}, "sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.61.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.61.1", "@typescript-eslint/tsconfig-utils": "8.61.1", "@typescript-eslint/types": "8.61.1", "@typescript-eslint/visitor-keys": "8.61.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.61.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.61.1", "@typescript-eslint/types": "8.61.1", "@typescript-eslint/typescript-estree": "8.61.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.61.1", "", { "dependencies": { "@typescript-eslint/types": "8.61.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w=="],
"@valibot/to-json-schema": ["@valibot/to-json-schema@1.7.1", "", { "peerDependencies": { "valibot": "^1.4.0" } }, "sha512-3qkmU6KXWh8GIThEAW3kuRHPQBMjWkKy+Ppz3WkUucx53DTpOa6siMn4xDGSOhlVyMrDaJTCTMLYPZVAIk1P0A=="],
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
"ajv": ["ajv@6.14.0", "", { "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-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
"brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
"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=="],
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
"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.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"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.22", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.6", "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-Q90VCb1RVFxnFaRV0FDoSylESQQLWgLHFmWciQJdX9h3b2cSasji9KWEUvaJuy/L9ciAGg4RAhUVfsXHg5K2RQ=="],
"emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
"enhanced-resolve": ["enhanced-resolve@5.21.6", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"eslint": ["eslint@10.5.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.6.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.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", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-1y+7C+vi12bUK1IpZeaV3gsH9fHLBmPvYmPx42pvT/E9yG0IC8g3PUZZgp0+JLJl7ZDK0flc2gc+Aw9dpCvIsQ=="],
"eslint-plugin-better-tailwindcss": ["eslint-plugin-better-tailwindcss@4.6.0", "", { "dependencies": { "@eslint/css-tree": "^4.0.1", "@valibot/to-json-schema": "^1.6.0", "enhanced-resolve": "^5.20.1", "jiti": "^2.6.1", "synckit": "^0.11.12", "tailwind-csstree": "^0.3.2", "tsconfig-paths-webpack-plugin": "^4.2.0", "valibot": "^1.3.1" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0", "oxlint": "^1.35.0", "tailwindcss": "^3.3.0 || ^4.1.17" }, "optionalPeers": ["eslint", "oxlint"] }, "sha512-Xeh/6KzLeays6jXSqTKx6iCdJL1qo5ant/mAabbz7dJqADYebKUUPR2zXp1xuiqqJ3HIm+MN+Ql2m4Jp0BxkMQ=="],
"eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="],
"eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
"espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="],
"esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
"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.7", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-+MeEmDcLA4o/vjK2zujgk+1VTxPR4hdp23qLqkWfStbECtAq9gmsvQa3LW6z/0GXZyHJobrCnmy1cdeE7BjsYg=="],
"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.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
"fd-package-json": ["fd-package-json@2.0.0", "", { "dependencies": { "walk-up-path": "^4.0.0" } }, "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
"file-type": ["file-type@21.3.0", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA=="],
"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=="],
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
"get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="],
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
"globals": ["globals@17.6.0", "", {}, "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
"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.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
"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.88.1", "", { "dependencies": { "@nodelib/fs.walk": "^1.2.3", "fast-glob": "^3.3.3", "formatly": "^0.3.0", "jiti": "^2.6.0", "minimist": "^1.2.8", "oxc-resolver": "^11.19.1", "picocolors": "^1.1.1", "picomatch": "^4.0.1", "smol-toml": "^1.5.2", "strip-json-comments": "5.0.3", "unbash": "^2.2.0", "yaml": "^2.8.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-tpy5o7zu1MjawVkLPuahymVJekYY3kYjvzcoInhIchgePxTlo+api90tBv2KfhAIe5uXh+mez1tAfmbv8/TiZg=="],
"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.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"mdn-data": ["mdn-data@2.28.1", "", {}, "sha512-U9w+PzSZ00Z5m9rZ5ARVFL5xOfuCHdKYi/1RRwDCJsboFgJDNT3zT6PIPD7mZQYaQLhsZM3GfDRgSMRHhSmVng=="],
"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@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
"minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
"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.14", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-U9kYi5bpVMEI31yC8iw4bJJp0avcHXA0W8/wNfLfnvJYzihQo2ZRPYPvpAAd570HAcCBjCTN7vnr+v4StKl1IQ=="],
"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.19.1", "", { "optionalDependencies": { "@oxc-resolver/binding-android-arm-eabi": "11.19.1", "@oxc-resolver/binding-android-arm64": "11.19.1", "@oxc-resolver/binding-darwin-arm64": "11.19.1", "@oxc-resolver/binding-darwin-x64": "11.19.1", "@oxc-resolver/binding-freebsd-x64": "11.19.1", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.19.1", "@oxc-resolver/binding-linux-arm-musleabihf": "11.19.1", "@oxc-resolver/binding-linux-arm64-gnu": "11.19.1", "@oxc-resolver/binding-linux-arm64-musl": "11.19.1", "@oxc-resolver/binding-linux-ppc64-gnu": "11.19.1", "@oxc-resolver/binding-linux-riscv64-gnu": "11.19.1", "@oxc-resolver/binding-linux-riscv64-musl": "11.19.1", "@oxc-resolver/binding-linux-s390x-gnu": "11.19.1", "@oxc-resolver/binding-linux-x64-gnu": "11.19.1", "@oxc-resolver/binding-linux-x64-musl": "11.19.1", "@oxc-resolver/binding-openharmony-arm64": "11.19.1", "@oxc-resolver/binding-wasm32-wasi": "11.19.1", "@oxc-resolver/binding-win32-arm64-msvc": "11.19.1", "@oxc-resolver/binding-win32-ia32-msvc": "11.19.1", "@oxc-resolver/binding-win32-x64-msvc": "11.19.1" } }, "sha512-qE/CIg/spwrTBFt5aKmwe3ifeDdLfA2NESN30E42X/lII5ClF8V7Wt6WIJhcGZjp0/Q+nQ+9vgxGk//xZNX2hg=="],
"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=="],
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"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=="],
"postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="],
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
"prettier": ["prettier@3.8.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q=="],
"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.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
"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=="],
"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.4", "", { "dependencies": { "truncate-utf8-bytes": "^1.0.0" } }, "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg=="],
"semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"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.6.0", "", {}, "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw=="],
"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.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
"strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
"strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="],
"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=="],
"synckit": ["synckit@0.11.12", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ=="],
"tailwind-csstree": ["tailwind-csstree@0.3.3", "", { "peerDependencies": { "@eslint/css": ">=1.0.0" }, "optionalPeers": ["@eslint/css"] }, "sha512-je9J5UYRsTJqAjYrIBMMlge8T/rreRd44pJxgG5Zx/zeo4kAC/liUKqzztRZrGlYRJLvIf2Cb1DVJMTXSzEShA=="],
"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.3.1", "", {}, "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q=="],
"tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
"tar": ["tar@7.5.16", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"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.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="],
"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.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="],
"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.61.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.61.1", "@typescript-eslint/parser": "8.61.1", "@typescript-eslint/typescript-estree": "8.61.1", "@typescript-eslint/utils": "8.61.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-V7PayAfJokV3pEHgN7/v03D1SpujhRfQtYLbLIiBfDDncdg4PAiRBfoS4cnCANK4jmAPncczi59QO3afiXUlNw=="],
"uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
"unbash": ["unbash@2.2.0", "", {}, "sha512-X2wH19RAPZE3+ldGicOkoj/SIA83OIxcJ6Cuaw23hf8Xc6fQpvZXY0SftE2JgS0QhYLUG4uwodSI3R53keyh7w=="],
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"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=="],
"valibot": ["valibot@1.4.1", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-klCmFTz2jeDluy9RwX+F884TCiogtdBJ/YaxSx1EOBYXa3NXNWj8kR1jjN8rzluwojJVWWaHJ4r1U5LfICnM3g=="],
"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=="],
"yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="],
"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.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"@napi-rs/wasm-runtime/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
"@napi-rs/wasm-runtime/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
"@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@tailwindcss/node/jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.5", "", { "dependencies": { "@tybys/wasm-util": "^0.10.2" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q=="],
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"eslint-plugin-better-tailwindcss/enhanced-resolve": ["enhanced-resolve@5.24.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-SkE2t82KlkkxQRVMVLAGKxLfORGQfrkx5dkj+vlgXRVNEdPc4eZcR+J/Fvj8C+yKSFH5L0q3NFlyufOVQnCcYQ=="],
"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.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"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=="],
"tsconfig-paths-webpack-plugin/enhanced-resolve": ["enhanced-resolve@5.20.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ=="],
"tsconfig-paths-webpack-plugin/tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
"@napi-rs/wasm-runtime/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
"@napi-rs/wasm-runtime/@emnapi/core/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@napi-rs/wasm-runtime/@emnapi/runtime/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@napi-rs/wasm-runtime/@tybys/wasm-util/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"tsconfig-paths-webpack-plugin/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"@napi-rs/wasm-runtime/@emnapi/core/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/runtime/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
}
}

BIN
bun.lockb

Binary file not shown.

View file

@ -1,3 +1,5 @@
# This compose file is for development and testing purposes only. See README for production deployment instructions.
services:
convertx:
build:
@ -5,8 +7,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

77
eslint.config.ts Normal file
View file

@ -0,0 +1,77 @@
import js from "@eslint/js";
import eslintPluginBetterTailwindcss from "eslint-plugin-better-tailwindcss";
import { defineConfig } from "eslint/config";
import globals from "globals";
import tseslint, { parser as eslintParserTypeScript } from "typescript-eslint";
export default defineConfig(
{
ignores: ["**/node_modules/**", "dist/**"],
},
js.configs.recommended,
tseslint.configs.recommended,
{
files: ["**/*.{ts,tsx,cts,mts}"],
extends: [
eslintPluginBetterTailwindcss.configs.recommended,
eslintPluginBetterTailwindcss.configs.stylistic,
],
languageOptions: {
parser: eslintParserTypeScript,
parserOptions: {
project: "./tsconfig.eslint.json",
},
globals: {
...globals.node,
},
},
settings: {
"better-tailwindcss": {
entryPoint: "src/main.css",
},
},
rules: {
"better-tailwindcss/enforce-consistent-line-wrapping": [
"warn",
{
group: "newLine",
printWidth: 100,
},
],
"better-tailwindcss/no-unknown-classes": [
"warn",
{
ignore: [
"^group(?:\\/(\\S*))?$",
"^peer(?:\\/(\\S*))?$",
"select_container",
"convert_to_popup",
"convert_to_group",
"target",
"convert_to_target",
"job-details-toggle",
],
},
],
},
},
{
files: ["**/*.{jsx,tsx}"],
languageOptions: {
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
},
},
{
files: ["**/*.{js,cjs,mjs,jsx}"],
extends: [tseslint.configs.disableTypeChecked],
languageOptions: {
globals: {
...globals.browser,
},
},
},
);

BIN
images/preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

8
knip.json Normal file
View file

@ -0,0 +1,8 @@
{
"$schema": "https://unpkg.com/knip@5/schema.json",
"entry": ["tests/**/*.test.ts"],
"project": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts"],
"tailwind": {
"entry": ["src/main.css"]
}
}

6
mise.toml Normal file
View file

@ -0,0 +1,6 @@
[tools]
bun = "1.2.2"
[env]
JWT_SECRET = "aLongAndSecretStringUsedToSignTheJSONWebToken1234"
FFMPEG_OUTPUT_ARGS = "-preset veryfast -threads 2"

View file

@ -1,21 +1,28 @@
{
"name": "convertx-frontend",
"version": "0.3.3",
"version": "0.18.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",
"lint": "run-p 'lint:*'",
"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:knip": "knip",
"lint:eslint": "eslint .",
"lint:prettier": "prettier --check ."
},
"dependencies": {
"@elysiajs/cookie": "^0.8.0",
"@elysiajs/html": "1.0.2",
"@elysiajs/jwt": "^1.1.0",
"@elysiajs/static": "1.0.3",
"elysia": "^1.1.4"
"@elysiajs/html": "^1.4.2",
"@elysiajs/jwt": "^1.4.2",
"@elysiajs/static": "^1.4.10",
"@kitajs/html": "^4.2.13",
"elysia": "1.4.22",
"sanitize-filename": "^1.6.4",
"tar": "^7.5.16"
},
"module": "src/index.tsx",
"type": "module",
@ -23,28 +30,27 @@
"start": "bun run src/index.tsx"
},
"devDependencies": {
"@biomejs/biome": "1.8.3",
"@ianvs/prettier-plugin-sort-imports": "^4.3.1",
"@kitajs/ts-html-plugin": "^4.0.2",
"@picocss/pico": "^2.0.6",
"@total-typescript/ts-reset": "^0.5.1",
"@types/bun": "^1.1.6",
"@types/eslint": "^9.6.0",
"@types/node": "^22.0.0",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"cpy-cli": "^5.0.0",
"eslint": "^9.8.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-isaacscript": "^3.12.2",
"eslint-plugin-prettier": "^5.2.1",
"knip": "^5.27.0",
"npm-run-all": "^4.1.5",
"prettier": "^3.3.3",
"typescript": "^5.5.4"
"@eslint/js": "^10.0.1",
"@kitajs/ts-html-plugin": "^4.1.4",
"@tailwindcss/cli": "^4.3.1",
"@tailwindcss/postcss": "^4.3.1",
"@types/bun": "latest",
"@types/node": "^24.13.2",
"eslint": "^10.5.0",
"eslint-plugin-better-tailwindcss": "^4.6.0",
"globals": "^17.6.0",
"knip": "^5.88.1",
"npm-run-all2": "^8.0.4",
"postcss": "^8.5.15",
"prettier": "^3.8.4",
"tailwind-scrollbar": "^4.0.2",
"tailwindcss": "^4.3.1",
"typescript": "^5.9.3",
"typescript-eslint": "^8.61.1"
},
"trustedDependencies": [
"@biomejs/biome"
"@parcel/watcher",
"@tailwindcss/oxide",
"oxc-resolver"
]
}

5
postcss.config.js Normal file
View file

@ -0,0 +1,5 @@
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};

View file

@ -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
View file

@ -0,0 +1,12 @@
/**
* @type {import('prettier').Config}
*/
const config = {
arrowParens: "always",
printWidth: 100,
singleQuote: false,
semi: true,
tabWidth: 2,
};
export default config;

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

BIN
public/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 B

BIN
public/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 831 B

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

1
public/favicon.svg Normal file
View 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

View file

@ -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
View file

@ -0,0 +1,2 @@
User-agent: *
Disallow: /

251
public/script.js Normal file
View 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><button type="button" class="text-accent-500 hover:underline" onclick="deleteRow(this)">Remove</button></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
View 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
View file

@ -1 +0,0 @@
import "@total-typescript/ts-reset";

View file

@ -1,33 +1,43 @@
import { version } from "../../package.json";
export const BaseHtml = ({
children,
title = "ConvertX",
}: { children: JSX.Element; title?: string }) => (
webroot = "",
}: {
children: JSX.Element;
title?: string;
webroot?: string;
}) => (
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="webroot" content={webroot} />
<title safe>{title}</title>
<link rel="stylesheet" href="/pico.lime.min.css" />
<link rel="stylesheet" href="/style.css" />
<link
rel="apple-touch-icon"
sizes="180x180"
href="/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/favicon-16x16.png"
/>
<link rel="manifest" href="/site.webmanifest" />
<link rel="stylesheet" href={`${webroot}/generated.css`} />
<link rel="apple-touch-icon" sizes="180x180" href={`${webroot}/apple-touch-icon.png`} />
<link rel="icon" type="image/png" sizes="32x32" href={`${webroot}/favicon-32x32.png`} />
<link rel="icon" type="image/png" sizes="16x16" href={`${webroot}/favicon-16x16.png`} />
<link rel="manifest" href={`${webroot}/site.webmanifest`} />
</head>
<body>{children}</body>
<body class={`flex min-h-screen w-full flex-col bg-neutral-900 text-neutral-200`}>
{children}
<footer class="w-full">
<div class="p-4 text-center text-sm text-neutral-500">
<span>Powered by </span>
<a
href="https://github.com/C4illin/ConvertX"
class={`
text-neutral-400
hover:text-accent-500
`}
>
ConvertX{" "}
</a>
<span safe>v{version || ""}</span>
</div>
</footer>
</body>
</html>
);

View file

@ -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 class="container">
<nav>
<header class="w-full p-4">
<nav class={`mx-auto flex max-w-4xl justify-between rounded-sm bg-neutral-900 p-4`}>
<ul>
<li>
<strong>
<a
href="/"
style={{
textDecoration: "none",
color: "inherit",
}}>
ConvertX
</a>
<a href={`${webroot}/`}>ConvertX</a>
</strong>
</li>
</ul>

144
src/converters/assimp.ts Normal file
View file

@ -0,0 +1,144 @@
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, `-f${convertTo}`],
(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
View 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
View 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
View 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");
});
});
}

View file

@ -1,4 +1,5 @@
import { exec } from "node:child_process";
import { execFile as execFileOriginal } from "node:child_process";
import { ExecFileFn } from "./types";
// This could be done dynamically by running `ffmpeg -formats` and parsing the output
export const properties = {
@ -6,6 +7,7 @@ export const properties = {
muxer: [
"264",
"265",
"266",
"302",
"3dostr",
"3g2",
@ -18,6 +20,7 @@ export const properties = {
"aac",
"aax",
"ac3",
"ac4",
"ace",
"acm",
"act",
@ -48,7 +51,6 @@ export const properties = {
"apng",
"aptx",
"aptxhd",
"aptx_hd",
"aqt",
"aqtitle",
"argo_asf",
@ -63,10 +65,12 @@ export const properties = {
"av1",
"avc",
"avi",
"avif",
"avr",
"avs",
"avs2",
"avs3",
"awb",
"bcstm",
"bethsoftvid",
"bfi",
@ -75,8 +79,10 @@ export const properties = {
"bink",
"binka",
"bit",
"bmp_pipe",
"bitpacked",
"bmv",
"bmp",
"bonk",
"boa",
"brender_pix",
"brstm",
@ -93,7 +99,7 @@ export const properties = {
"codec2",
"codec2raw",
"concat",
"cri_pipe",
"cri",
"dash",
"dat",
"data",
@ -101,8 +107,9 @@ export const properties = {
"dav",
"dbm",
"dcstr",
"dds_pipe",
"dds",
"derf",
"dfpwm",
"dfa",
"dhav",
"dif",
@ -131,6 +138,8 @@ export const properties = {
"exr_pipe",
"f32be",
"f32le",
"ec3",
"evc",
"f4v",
"f64be",
"f64le",
@ -157,13 +166,13 @@ export const properties = {
"gdv",
"genh",
"gif",
"gif_pipe",
"gsm",
"gxf",
"h261",
"h263",
"h264",
"h265",
"h266",
"h26l",
"hca",
"hcom",
@ -180,7 +189,6 @@ export const properties = {
"ifv",
"ilbc",
"image2",
"image2pipe",
"imf",
"imx",
"ingenient",
@ -197,21 +205,17 @@ export const properties = {
"ivr",
"j2b",
"j2k",
"j2k_pipe",
"jack",
"jacosub",
"jpegls_pipe",
"jpeg_pipe",
"jv",
"jpegls",
"jpeg",
"jxl",
"kmsgrab",
"kux",
"kvag",
"lavfi",
"libcdio",
"libdc1394",
"libgme",
"libopenmpt",
"live_flv",
"laf",
"lmlm4",
"loas",
"lrc",
@ -224,16 +228,13 @@ export const properties = {
"m4b",
"m4v",
"mac",
"matroska",
"mca",
"mcc",
"mdl",
"med",
"mgsts",
"microdvd",
"mj2",
"mjpeg",
"mjpeg_2000",
"mjpg",
"mk3d",
"mka",
@ -257,9 +258,6 @@ export const properties = {
"mpc",
"mpc8",
"mpeg",
"mpegts",
"mpegtsraw",
"mpegvideo",
"mpg",
"mpjpeg",
"mpl2",
@ -294,25 +292,27 @@ export const properties = {
"okt",
"oma",
"omg",
"opus",
"openal",
"oss",
"osq",
"paf",
"pam_pipe",
"pbm_pipe",
"pcx_pipe",
"pgmyuv_pipe",
"pgm_pipe",
"pgx_pipe",
"photocd_pipe",
"pictor_pipe",
"pdv",
"pam",
"pbm",
"pcx",
"pgmyuv",
"pgm",
"pgx",
"photocd",
"pictor",
"pjs",
"plm",
"pmp",
"png_pipe",
"png",
"ppm",
"ppm_pipe",
"pp_bnk",
"psd_pipe",
"pp",
"psd",
"psm",
"psp",
"psxstr",
@ -323,7 +323,7 @@ export const properties = {
"pvf",
"qcif",
"qcp",
"qdraw_pipe",
"qdraw",
"r3d",
"rawvideo",
"rco",
@ -335,6 +335,7 @@ export const properties = {
"rm",
"roq",
"rpl",
"rka",
"rsd",
"rso",
"rt",
@ -355,6 +356,7 @@ export const properties = {
"sbc",
"sbg",
"scc",
"sdns",
"sdp",
"sdr2",
"sds",
@ -364,10 +366,9 @@ export const properties = {
"sfx",
"sfx2",
"sga",
"sgi_pipe",
"sgi",
"shn",
"siff",
"simbiosis_imx",
"sln",
"smi",
"smjpeg",
@ -389,12 +390,9 @@ export const properties = {
"stp",
"str",
"sub",
"subviewer",
"subviewer1",
"sunrast_pipe",
"sup",
"svag",
"svg_pipe",
"svg",
"svs",
"sw",
"swf",
@ -404,7 +402,8 @@ export const properties = {
"thd",
"thp",
"tiertexseq",
"tiff_pipe",
"tif",
"tiff",
"tmv",
"truehd",
"tta",
@ -424,6 +423,7 @@ export const properties = {
"ul",
"ult",
"umx",
"usm",
"uw",
"v",
"v210",
@ -447,13 +447,16 @@ export const properties = {
"vql",
"vt",
"vtt",
"vvc",
"w64",
"wa",
"wav",
"way",
"wc3movie",
"webm",
"webm_dash_manifest",
"webp_pipe",
"webp",
"webvtt",
"wma",
"wow",
"wsaud",
"wsd",
@ -464,32 +467,31 @@ export const properties = {
"x11grab",
"xa",
"xbin",
"xbm_pipe",
"xl",
"xm",
"xmd",
"xmv",
"xpk",
"xpm_pipe",
"xvag",
"xwd_pipe",
"xwma",
"y4m",
"yop",
"yuv",
"yuv10",
"yuv4mpegpipe",
],
},
to: {
muxer: [
"264",
"265",
"266",
"302",
"3g2",
"3gp",
"a64",
"aac",
"ac3",
"ac4",
"adts",
"adx",
"afc",
@ -497,43 +499,33 @@ export const properties = {
"aifc",
"aiff",
"al",
"alaw",
"alp",
"alsa",
"amr",
"amv",
"apm",
"apng",
"aptx",
"aptxhd",
"aptx_hd",
"argo_asf",
"asf",
"asf_stream",
"ass",
"ast",
"au",
"aud",
"av1.mkv",
"av1.mp4",
"avi",
"avm2",
"avif",
"avs",
"avs2",
"avs3",
"bit",
"bmp",
"c2",
"caca",
"caf",
"cavs",
"cavsvideo",
"chk",
"chromaprint",
"codec2",
"codec2raw",
"cpk",
"crc",
"dash",
"data",
"daud",
"dirac",
"cvg",
"dfpwm",
"dnxhd",
"dnxhr",
"dpx",
@ -542,63 +534,45 @@ export const properties = {
"dv",
"dvd",
"eac3",
"ec3",
"evc",
"exr",
"f32be",
"f32le",
"f4v",
"f64be",
"f64le",
"fbdev",
"ffmeta",
"ffmetadata",
"fifo",
"fifo_test",
"filmstrip",
"film_cpk",
"fits",
"flac",
"flm",
"flv",
"framecrc",
"framehash",
"framemd5",
"g722",
"g723_1",
"g726",
"g726le",
"gif",
"gsm",
"gxf",
"h261",
"h263",
"h264",
"h265",
"hash",
"hds",
"h264.mkv",
"h264.mp4",
"h265.mkv",
"h265.mp4",
"h266.mkv",
"hdr",
"hevc",
"hls",
"ico",
"ilbc",
"im1",
"im24",
"im8",
"image2",
"image2pipe",
"ipod",
"ircam",
"isma",
"ismv",
"ivf",
"j2c",
"j2k",
"jacosub",
"jls",
"jp2",
"jpeg",
"jpg",
"js",
"jss",
"kvag",
"jxl",
"latm",
"lbc",
"ljpg",
@ -613,13 +587,9 @@ export const properties = {
"m4a",
"m4b",
"m4v",
"matroska",
"md5",
"microdvd",
"mjpeg",
"mjpg",
"mkv",
"mkvtimestamp_v2",
"mlp",
"mmf",
"mov",
@ -629,26 +599,17 @@ export const properties = {
"mpa",
"mpd",
"mpeg",
"mpeg1video",
"mpeg2video",
"mpegts",
"mpg",
"mpjpeg",
"msbc",
"mts",
"mulaw",
"mxf",
"mxf_d10",
"mxf_opatom",
"null",
"nut",
"obu",
"oga",
"ogg",
"ogv",
"oma",
"opengl",
"opus",
"oss",
"pam",
"pbm",
"pcm",
@ -656,14 +617,14 @@ export const properties = {
"pfm",
"pgm",
"pgmyuv",
"phm",
"pix",
"png",
"ppm",
"psp",
"pulse",
"qoi",
"ra",
"ras",
"rawvideo",
"rco",
"rcv",
"rgb",
@ -671,84 +632,47 @@ export const properties = {
"roq",
"rs",
"rso",
"rtp",
"rtp_mpegts",
"rtsp",
"s16be",
"s16le",
"s24be",
"s24le",
"s32be",
"s32le",
"s8",
"sap",
"sb",
"sbc",
"scc",
"sdl",
"sdl2",
"segment",
"sf",
"sgi",
"singlejpeg",
"smjpeg",
"smoothstreaming",
"sndio",
"sox",
"spdif",
"spx",
"srt",
"ssa",
"ssegment",
"streamhash",
"stream_segment",
"sub",
"sun",
"sunras",
"sup",
"svcd",
"sw",
"swf",
"tco",
"tee",
"tga",
"thd",
"tif",
"tiff",
"truehd",
"ts",
"tta",
"ttml",
"tun",
"u16be",
"u16le",
"u24be",
"u24le",
"u32be",
"u32le",
"u8",
"ub",
"ul",
"uncodedframecrc",
"uw",
"v4l2",
"vag",
"vbn",
"vc1",
"vc1test",
"vc2",
"vcd",
"vidc",
"video4linux2",
"vob",
"voc",
"vtt",
"vvc",
"w64",
"wav",
"wbmp",
"webm",
"webm_chunk",
"webm_dash_manifest",
"webp",
"webvtt",
"wma",
"wmv",
"wtv",
@ -756,12 +680,10 @@ export const properties = {
"xbm",
"xface",
"xml",
"xv",
"xwd",
"y",
"y4m",
"yuv",
"yuv4mpegpipe",
],
},
};
@ -771,58 +693,67 @@ 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+/) : [];
const ffmpegOutputArgs = process.env.FFMPEG_OUTPUT_ARGS
? process.env.FFMPEG_OUTPUT_ARGS.split(/\s+/)
: [];
return new Promise((resolve, reject) => {
exec(command, (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
execFile(
"ffmpeg",
[...ffmpegArgs, "-i", filePath, ...ffmpegOutputArgs, ...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);
},
);
});
}

View file

@ -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");
});
});
}

View 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");
},
);
});
}

View 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
View 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");
});
});
}

View file

@ -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");
});
});
}

View file

@ -0,0 +1,179 @@
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 (converto === "pdf") {
return [null, null];
} else 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");
});
});
}

View file

@ -1,107 +1,215 @@
import { convert as convertImage, properties as propertiesImage } from "./vips";
import {
convert as convertPandoc,
properties as propertiesPandoc,
} from "./pandoc";
import {
convert as convertFFmpeg,
properties as propertiesFFmpeg,
} from "./ffmpeg";
import { Cookie } from "elysia";
import db from "../db/db";
import { MAX_CONVERT_PROCESS } from "../helpers/env";
import { normalizeFiletype, normalizeOutputFiletype } from "../helpers/normalizeFiletype";
import { convert as convertassimp, properties as propertiesassimp } from "./assimp";
import { convert as convertCalibre, properties as propertiesCalibre } from "./calibre";
import { convert as convertDasel, properties as propertiesDasel } from "./dasel";
import { convert as convertDvisvgm, properties as propertiesDvisvgm } from "./dvisvgm";
import { convert as convertFFmpeg, properties as propertiesFFmpeg } from "./ffmpeg";
import {
convert as convertGraphicsmagick,
properties as propertiesGraphicsmagick,
} from "./graphicsmagick";
import {
convert as convertxelatex,
properties as propertiesxelatex,
} from "./xelatex";
import {
convert as convertLibjxl,
properties as propertiesLibjxl,
} from "./libjxl";
import { 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 convertVcf, properties as propertiesVcf } from "./vcf";
import { convert as convertxelatex, properties as propertiesxelatex } from "./xelatex";
import { convert as convertMarkitdown, properties as propertiesMarkitdown } from "./markitdown";
// 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,
},
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,
},
vcf: {
properties: propertiesVcf,
converter: convertVcf,
},
markitDown: {
properties: propertiesMarkitdown,
converter: convertMarkitdown,
},
};
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.includes(".") ? (fileName.split(".").pop() ?? "") : "";
const fileType = normalizeFiletype(fileTypeOrig);
const newFileExt = normalizeOutputFiletype(convertTo);
let newFileName: string;
if (fileTypeOrig === "") {
newFileName = `${fileName}.${newFileExt}`;
} else {
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];
@ -122,24 +230,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(
@ -150,34 +256,27 @@ 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;
if (!converterProperties) {
continue;
}
if (!converterProperties) continue;
for (const key in converterProperties.from) {
if (converterProperties.from[key] === undefined) {
continue;
}
const fromList = converterProperties.from[key];
const toList = converterProperties.to[key];
for (const extension of converterProperties.from[key] ?? []) {
if (!possibleTargets[extension]) {
possibleTargets[extension] = {};
}
if (!fromList || !toList) continue;
possibleTargets[extension][converterName] =
converterProperties.to[key] || [];
for (const ext of fromList) {
if (!possibleTargets[ext]) possibleTargets[ext] = {};
possibleTargets[ext][converterName] = toList;
}
}
}
export const getPossibleTargets = (
from: string,
): { [key: string]: string[] } => {
export const getPossibleTargets = (from: string): Record<string, string[]> => {
const fromClean = normalizeFiletype(from);
return possibleTargets[fromClean] || {};
@ -201,11 +300,7 @@ for (const converterName in properties) {
}
possibleInputs.sort();
const getPossibleInputs = () => {
return possibleInputs;
};
const allTargets: { [key: string]: string[] } = {};
const allTargets: Record<string, string[]> = {};
for (const converterName in properties) {
const converterProperties = properties[converterName]?.properties;
@ -227,7 +322,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;
@ -247,29 +342,3 @@ for (const converterName in properties) {
export const getAllInputs = (converter: string) => {
return allInputs[converter] || [];
};
// // count the number of unique formats
// const uniqueFormats = new Set();
// for (const converterName in properties) {
// const converterProperties = properties[converterName]?.properties;
// if (!converterProperties) {
// continue;
// }
// for (const key in converterProperties.from) {
// for (const extension of converterProperties.from[key] ?? []) {
// uniqueFormats.add(extension);
// }
// }
// for (const key in converterProperties.to) {
// for (const extension of converterProperties.to[key] ?? []) {
// uniqueFormats.add(extension);
// }
// }
// }
// // print the number of unique Inputs and Outputs
// console.log(`Unique Formats: ${uniqueFormats.size}`);

View file

@ -0,0 +1,39 @@
import { execFile as execFileOriginal } from "node:child_process";
import { ExecFileFn } from "./types";
export const properties = {
from: {
document: ["pdf", "powerpoint", "excel", "docx", "pptx", "html"],
},
to: {
document: ["md"],
},
};
export async function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
options?: unknown,
execFile: ExecFileFn = execFileOriginal,
): Promise<string> {
return new Promise((resolve, reject) => {
execFile("markitdown", [filePath, "-o", targetPath], (err, stdout, stderr) => {
if (err) {
reject(`markitdown error: ${err}`);
return;
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
});
});
}

View 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.`,
),
);
}
});
}

View file

@ -1,4 +1,5 @@
import { exec } from "node:child_process";
import { execFile as execFileOriginal } from "node:child_process";
import { ExecFileFn } from "./types";
export const properties = {
from: {
@ -124,33 +125,39 @@ export function convert(
fileType: string,
convertTo: string,
targetPath: string,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
options?: any,
options?: unknown,
execFile: ExecFileFn = execFileOriginal,
): Promise<string> {
// set xelatex here
const xelatex = ["pdf", "latex"];
let option = "";
// Build arguments array
const args: string[] = [];
if (xelatex.includes(convertTo)) {
option = "--pdf-engine=xelatex";
args.push("--pdf-engine=xelatex");
}
args.push(filePath);
args.push("-f", fileType);
args.push("-t", convertTo);
args.push("-o", targetPath);
return new Promise((resolve, reject) => {
exec(
`pandoc ${option} "${filePath}" -f ${fileType} -t ${convertTo} -o "${targetPath}"`,
(error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
execFile("pandoc", args, (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("success");
},
);
resolve("Done");
});
});
}

50
src/converters/potrace.ts Normal file
View 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
View 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
View 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>;

69
src/converters/vcf.ts Normal file
View file

@ -0,0 +1,69 @@
import { readFile, writeFile } from "fs/promises";
export const properties = {
from: {
contacts: ["vcf"],
},
to: {
contacts: ["csv"],
},
};
export function parseVCF(data: string): Record<string, string>[] {
const cards = data
.split(/BEGIN:VCARD/)
.slice(1)
.map((card) => card.split(/END:VCARD/)[0])
.filter((card) => card);
return cards
.map((card) => {
if (!card) return {};
const lines = card.split("\n").filter((line) => line.trim());
const contact: Record<string, string> = {};
for (const line of lines) {
const colonIndex = line.indexOf(":");
if (colonIndex === -1) continue;
const key = line.slice(0, colonIndex).trim();
const value = line.slice(colonIndex + 1).trim();
if (key === "FN") {
contact["Full Name"] = value;
} else if (key === "N") {
const parts = value.split(";");
contact["Last Name"] = parts[0] || "";
contact["First Name"] = parts[1] || "";
} else if (key.startsWith("TEL")) {
contact["Phone"] = value;
} else if (key.startsWith("EMAIL")) {
contact["Email"] = value;
} else if (key === "ORG") {
contact["Organization"] = value.split(";")[0] || "";
}
}
return contact;
})
.filter((contact) => Object.keys(contact).length > 0);
}
export function toCSV(data: Record<string, string>[]): string {
if (!data.length) return "";
const first = data[0];
if (!first) return "";
const headers = Object.keys(first);
const escape = (str: string) => `"${str.replace(/"/g, '""')}"`;
const rows = data.map((row) => headers.map((h) => escape(row[h] || "")).join(","));
return [headers.join(","), ...rows].join("\n");
}
export async function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
options?: unknown, // eslint-disable-line @typescript-eslint/no-unused-vars
): Promise<string> {
const vcfData = await readFile(filePath, "utf-8");
const contacts = parseVCF(vcfData);
const csvData = toCSV(contacts);
await writeFile(targetPath, csvData, "utf-8");
return "Done";
}

View file

@ -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
View 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");
});
});
}

View file

@ -1,4 +1,5 @@
import { exec } from "node:child_process";
import { execFile as execFileOriginal } from "node:child_process";
import { ExecFileFn } from "./types";
export const properties = {
from: {
@ -14,18 +15,16 @@ export function convert(
fileType: string,
convertTo: string,
targetPath: string,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
options?: any,
options?: unknown,
execFile: ExecFileFn = execFileOriginal,
): Promise<string> {
return new Promise((resolve, reject) => {
// const fileName: string = (targetPath.split("/").pop() as string).replace(".pdf", "")
const outputPath = targetPath
.split("/")
.slice(0, -1)
.join("/")
.replace("./", "");
exec(
`latexmk -xelatex -interaction=nonstopmode -output-directory="${outputPath}" "${filePath}"`,
const outputPath = targetPath.split("/").slice(0, -1).join("/").replace("./", "");
execFile(
"latexmk",
["-xelatex", "-interaction=nonstopmode", `-output-directory=${outputPath}`, filePath],
(error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
@ -39,7 +38,7 @@ export function convert(
console.error(`stderr: ${stderr}`);
}
resolve("success");
resolve("Done");
},
);
});

43
src/db/db.ts Normal file
View file

@ -0,0 +1,43 @@
import { mkdirSync } from "node:fs";
import { Database } from "bun:sqlite";
mkdirSync("./data", { recursive: true });
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
View 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;
}

27
src/helpers/env.ts Normal file
View file

@ -0,0 +1,27 @@
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;
export const TIMEZONE = process.env.TZ || undefined;

View file

@ -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,8 +26,29 @@ 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";
// assimp format ids that aren't real file extensions — map to the
// canonical extension for the underlying format so the output file
// opens in third-party viewers. The format id is still passed to
// `assimp export -f<id>` so the right encoding/variant is produced.
case "glb2":
return "glb";
case "gltf2":
return "gltf";
case "objnomtl":
return "obj";
case "stlb":
return "stl";
case "plyb":
return "ply";
case "fbxa":
return "fbx";
case "assjson":
return "json";
default:
return lowercaseFiletype;
}

View file

@ -1,9 +1,11 @@
import { exec } from "node:child_process";
import { readFile } from "node:fs";
import { version } from "../../package.json";
console.log(`ConvertX v${version}`);
if (process.env.NODE_ENV === "production") {
exec("cat /etc/os-release", (error, stdout) => {
readFile("/etc/os-release", "utf8", (error, stdout) => {
if (error) {
console.error("Not running on docker, this is not supported.");
}
@ -43,6 +45,16 @@ if (process.env.NODE_ENV === "production") {
}
});
exec("magick --version", (error, stdout) => {
if (error) {
console.error("ImageMagick is not installed.");
}
if (stdout) {
console.log(stdout.split("\n")[0]?.replace("Version: ", ""));
}
});
exec("gm version", (error, stdout) => {
if (error) {
console.error("GraphicsMagick is not installed.");
@ -53,6 +65,16 @@ if (process.env.NODE_ENV === "production") {
}
});
exec("inkscape --version", (error, stdout) => {
if (error) {
console.error("Inkscape is not installed.");
}
if (stdout) {
console.log(stdout.split("\n")[0]);
}
});
exec("djxl --version", (error, stdout) => {
if (error) {
console.error("libjxl-tools is not installed.");
@ -63,6 +85,16 @@ if (process.env.NODE_ENV === "production") {
}
});
exec("dasel --version", (error, stdout) => {
if (error) {
console.error("dasel is not installed.");
}
if (stdout) {
console.log(stdout.split("\n")[0]);
}
});
exec("xelatex -version", (error, stdout) => {
if (error) {
console.error("Tex Live with XeTeX is not installed.");
@ -73,6 +105,76 @@ if (process.env.NODE_ENV === "production") {
}
});
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");

15
src/helpers/tailwind.ts Normal file
View 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
View 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
View 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
View 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>
);
}

File diff suppressed because it is too large Load diff

32
src/main.css Normal file
View 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;
}

View 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-sm 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-sm 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() }) },
);

99
src/pages/convert.tsx Normal file
View file

@ -0,0 +1,99 @@
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 ||
convertTo.includes("/") ||
convertTo.includes("\\") ||
convertTo.includes("..")
) {
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,
},
);

37
src/pages/deleteFile.tsx Normal file
View file

@ -0,0 +1,37 @@
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";
import sanitize from "sanitize-filename";
import path from "node:path";
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 = path.join(uploadsDir, user.id, jobId.value);
const sanitized = sanitize(body.filename);
const targetPath = path.join(userUploadsDir, sanitized);
await unlink(targetPath);
return {
message: "File deleted successfully.",
};
},
{ body: t.Object({ filename: t.String() }), auth: true },
);

117
src/pages/deleteJob.tsx Normal file
View file

@ -0,0 +1,117 @@
import { rmSync } from "node:fs";
import { Elysia, t } 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)
// SECURE: Use POST instead of GET for state-changing actions to prevent CSRF.
// Browsers automatically follow GET links, which can lead to silent data deletion.
.post(
"/delete/: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,
},
)
.post(
"/delete-multiple",
async ({ body, user, set }) => {
const { jobIds } = body;
if (!Array.isArray(jobIds) || jobIds.length === 0) {
set.status = 400;
return { success: false, message: "Invalid job IDs provided" };
}
const results = {
success: [] as string[],
failed: [] as { jobId: string; error: string }[],
};
// Process deletions sequentially for safety
for (const jobId of jobIds) {
try {
const job = db
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
.as(Jobs)
.get(user.id, jobId);
if (!job) {
results.failed.push({
jobId,
error: "Job not found or unauthorized",
});
continue;
}
// Delete the directories
try {
rmSync(`${outputDir}${job.user_id}/${job.id}`, {
recursive: true,
force: true,
});
} catch (error) {
console.error(`Failed to delete output directory for job ${jobId}:`, error);
}
try {
rmSync(`${uploadsDir}${job.user_id}/${job.id}`, {
recursive: true,
force: true,
});
} catch (error) {
console.error(`Failed to delete uploads directory for job ${jobId}:`, error);
}
// Delete the job from database
db.query("DELETE FROM jobs WHERE id = ?").run(job.id);
results.success.push(jobId);
} catch (error) {
results.failed.push({
jobId,
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
return {
success: results.failed.length === 0,
deleted: results.success.length,
failed: results.failed.length,
details: results,
};
},
{
auth: true,
body: t.Object({
jobIds: t.Array(t.String(), { maxItems: 100 }),
}),
},
);

65
src/pages/download.tsx Normal file
View 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 userId = user.id;
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 jobId = decodeURIComponent(params.jobId);
const fileName = sanitize(decodeURIComponent(params.fileName));
const filePath = `${outputDir}${userId}/${jobId}/${fileName}`;
return Bun.file(filePath);
},
{
auth: true,
},
)
.get(
"/archive/:jobId",
async ({ params, redirect, user }) => {
const userId = user.id;
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 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
View 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,
},
);

340
src/pages/history.tsx Normal file
View file

@ -0,0 +1,340 @@
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, TIMEZONE, WEBROOT } from "../helpers/env";
import { userService } from "./user";
import { EyeIcon } from "../icons/eye";
import { DeleteIcon } from "../icons/delete";
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">
<div class="mb-4 flex items-center justify-between">
<h1 class="text-xl">Results</h1>
<div id="delete-selected-container">
<button
id="delete-selected-btn"
class={`
flex btn-secondary flex-row gap-2 text-contrast
disabled:cursor-not-allowed disabled:opacity-50
`}
disabled
>
<DeleteIcon />{" "}
<span>
Delete Selected (<span id="selected-count">0</span>)
</span>
</button>
</div>
</div>
<table
class={`
w-full table-auto overflow-y-auto rounded-sm bg-neutral-900 text-left
[&_td]:p-4
[&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
`}
>
<thead>
<tr>
<th
class={`
p-2
sm:px-4
`}
>
<input
type="checkbox"
id="select-all"
class="size-4 cursor-pointer"
title="Select all"
/>
</th>
<th
class={`
p-2
sm:px-4
`}
>
<span class="sr-only">Expand details</span>
</th>
<th
class={`
p-2
sm:px-4
`}
>
Time
</th>
<th
class={`
p-2
sm:px-4
`}
>
Files
</th>
<th
class={`
p-2
max-sm:hidden
sm:px-4
`}
>
Files Done
</th>
<th
class={`
p-2
sm:px-4
`}
>
Status
</th>
<th
class={`
p-2
sm:px-4
`}
>
Actions
</th>
</tr>
</thead>
<tbody>
{userJobs.map((job) => (
<>
<tr id={`job-row-${job.id}`}>
<td>
<input
type="checkbox"
class="size-4 cursor-pointer"
data-checkbox-type="job"
data-job-id={job.id}
/>
</td>
<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 size-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, {
timeZone: TIMEZONE,
})}
</td>
<td>{job.num_files}</td>
<td class="max-sm:hidden">{job.finished_files}</td>
<td safe>{job.status}</td>
<td class="flex flex-row gap-4">
<a
class={`
text-accent-500 underline
hover:text-accent-400
`}
href={`${WEBROOT}/results/${job.id}`}
>
<EyeIcon />
</a>
<form action={`${WEBROOT}/delete/${job.id}`} method="POST" class="inline">
<button
type="submit"
class={`
text-accent-500 underline
hover:text-accent-400
`}
>
<DeleteIcon />
</button>
</form>
</td>
</tr>
<tr id={`details-${job.id}`} class="hidden">
<td colspan="7">
<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 size-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', () => {
// Expand/collapse job details
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}\`);
const arrow = document.getElementById(\`arrow-\${jobId}\`);
if (detailsRow && arrow) {
detailsRow.classList.toggle("hidden");
if (detailsRow.classList.contains("hidden")) {
arrow.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />';
} else {
arrow.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />';
}
}
});
});
// Checkbox management
const selectAllCheckbox = document.getElementById('select-all');
const jobCheckboxes = document.querySelectorAll('[data-checkbox-type="job"]');
const deleteSelectedBtn = document.getElementById('delete-selected-btn');
const deleteSelectedContainer = document.getElementById('delete-selected-container');
const selectedCountSpan = document.getElementById('selected-count');
function updateDeleteButton() {
const checkedBoxes = Array.from(jobCheckboxes).filter(cb => cb.checked);
if (checkedBoxes.length > 0) {
deleteSelectedBtn.disabled = false;
selectedCountSpan.textContent = checkedBoxes.length;
} else {
deleteSelectedBtn.disabled = true;
selectedCountSpan.textContent = '0';
}
}
selectAllCheckbox?.addEventListener('change', function() {
jobCheckboxes.forEach(checkbox => {
checkbox.checked = this.checked;
});
updateDeleteButton();
});
jobCheckboxes.forEach(checkbox => {
checkbox.addEventListener('change', function() {
const allChecked = Array.from(jobCheckboxes).every(cb => cb.checked);
const someChecked = Array.from(jobCheckboxes).some(cb => cb.checked);
if (selectAllCheckbox) {
selectAllCheckbox.checked = allChecked;
selectAllCheckbox.indeterminate = someChecked && !allChecked;
}
updateDeleteButton();
});
});
deleteSelectedBtn?.addEventListener('click', async function() {
const checkedBoxes = Array.from(jobCheckboxes).filter(cb => cb.checked);
const jobIds = checkedBoxes.map(cb => cb.dataset.jobId);
if (jobIds.length === 0) return;
const confirmed = confirm(\`Are you sure you want to delete \${jobIds.length} job(s)? This action cannot be undone.\`);
if (!confirmed) return;
try {
const response = await fetch('${WEBROOT}/delete-multiple', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ jobIds }),
});
if (!response.ok) {
throw new Error(\`HTTP error! status: \${response.status}\`);
}
const result = await response.json();
if (result.success || result.deleted > 0) {
alert(\`Successfully deleted \${result.deleted} job(s).\${result.failed > 0 ? \` Failed to delete \${result.failed} job(s).\` : ''}\`);
window.location.reload();
} else {
alert('Failed to delete jobs. Please try again.');
}
} catch (error) {
console.error('Error deleting jobs:', error);
alert('An error occurred while deleting jobs. Please try again.');
}
});
});
`}
</script>
</>
</BaseHtml>
);
},
{
auth: true,
},
);

View 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-sm 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,
},
);

211
src/pages/results.tsx Normal file
View file

@ -0,0 +1,211 @@
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({
job,
files,
outputPath,
}: {
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">
<form action={`${WEBROOT}/delete/${job.id}`} method="POST">
<button
type="submit"
style={files.length !== job.num_files ? "pointer-events: none;" : ""}
class="flex btn-secondary flex-row gap-2 text-contrast"
{...(files.length !== job.num_files ? { disabled: true, "aria-busy": "true" } : "")}
>
<DeleteIcon /> <p>Delete</p>
</button>
</form>
<a
style={files.length !== job.num_files ? "pointer-events: none;" : ""}
href={`${WEBROOT}/archive/${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>
</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-sm bg-neutral-900 text-left
[&_td]:p-4
[&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
`}
>
<thead>
<tr>
<th
class={`
p-2
sm:px-4
`}
>
Converted File Name
</th>
<th
class={`
p-2
sm:px-4
`}
>
Status
</th>
<th
class={`
p-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 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 job={job} files={files} outputPath={outputPath} />;
},
{ auth: true },
);

250
src/pages/root.tsx Normal file
View file

@ -0,0 +1,250 @@
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-sm 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-sm 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-sm 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-sm 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()),
}),
},
);

42
src/pages/upload.tsx Normal file
View file

@ -0,0 +1,42 @@
import { Elysia, t } from "elysia";
import db from "../db/db";
import { WEBROOT } from "../helpers/env";
import { uploadsDir } from "../index";
import { userService } from "./user";
import sanitize from "sanitize-filename";
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) {
const santizedFileName = sanitize(file.name);
await Bun.write(`${userUploadsDir}${santizedFileName}`, file);
}
} else {
const santizedFileName = sanitize(body.file["name"]);
await Bun.write(`${userUploadsDir}${santizedFileName}`, body.file);
}
}
return {
message: "Files uploaded successfully.",
};
},
{ body: t.Object({ file: t.Files() }), auth: true },
);

522
src/pages/user.tsx Normal file
View 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",
},
);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Some files were not shown because too many files have changed in this diff Show more