128 Commits

Author SHA1 Message Date
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
39 changed files with 1327 additions and 703 deletions

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;

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"

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

1
.gitignore vendored
View File

@@ -48,3 +48,4 @@ package-lock.json
/data /data
/Bruno /Bruno
/tsconfig.tsbuildinfo /tsconfig.tsbuildinfo
/src/public/generated.css

View File

@@ -1,5 +1,88 @@
# Changelog # Changelog
## [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) ## [0.3.3](https://github.com/C4illin/ConvertX/compare/v0.3.2...v0.3.3) (2024-07-30)

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,4 +1,5 @@
FROM oven/bun:1.1.21-alpine as base FROM oven/bun:1.1.29-alpine AS base
LABEL org.opencontainers.image.source="https://github.com/C4illin/ConvertX"
WORKDIR /app WORKDIR /app
# install dependencies into temp directory # install dependencies into temp directory
@@ -13,16 +14,22 @@ RUN mkdir -p /temp/prod
COPY package.json bun.lockb /temp/prod/ COPY package.json bun.lockb /temp/prod/
RUN cd /temp/prod && bun install --frozen-lockfile --production RUN cd /temp/prod && bun install --frozen-lockfile --production
FROM base AS builder
RUN apk --no-cache add curl gcc
ENV PATH=/root/.cargo/bin:$PATH
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
RUN cargo install resvg
# copy node_modules from temp directory # copy node_modules from temp directory
# then copy all (non-ignored) project files into the image # then copy all (non-ignored) project files into the image
# FROM base AS prerelease FROM base AS prerelease
# COPY --from=install /temp/dev/node_modules node_modules COPY --from=install /temp/dev/node_modules node_modules
# COPY . . COPY . .
# # [optional] tests & build # # [optional] tests & build
# ENV NODE_ENV=production ENV NODE_ENV=production
# RUN bun test # RUN bun test
# RUN bun run build RUN bun run build
# copy production dependencies and source code into final image # copy production dependencies and source code into final image
FROM base AS release FROM base AS release
@@ -40,12 +47,17 @@ RUN apk --no-cache add \
graphicsmagick \ graphicsmagick \
ghostscript \ ghostscript \
vips-tools \ vips-tools \
libjxl-tools vips-poppler \
vips-jxl \
libjxl-tools \
assimp
# this might be needed for some latex use cases, will add it if needed. # this might be needed for some latex use cases, will add it if needed.
# texmf-dist-fontsextra \ # texmf-dist-fontsextra \
COPY --from=install /temp/prod/node_modules node_modules COPY --from=install /temp/prod/node_modules node_modules
COPY --from=builder /root/.cargo/bin/resvg /usr/local/bin/resvg
COPY --from=prerelease /app/src/public/generated.css /app/src/public/
# COPY --from=prerelease /app/src/index.tsx /app/src/ # COPY --from=prerelease /app/src/index.tsx /app/src/
# COPY --from=prerelease /app/package.json . # COPY --from=prerelease /app/package.json .
COPY . . COPY . .

View File

@@ -7,11 +7,12 @@
![Docker container size](https://ghcr-badge.egpl.dev/c4illin/convertx/size?color=%230375b6&tag=latest&label=image+size&trim=) ![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) ![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. A self-hosted online file converter. Supports over a thousand different formats. Written with TypeScript, Bun and Elysia.
## Features ## Features
- Convert files to different formats - Convert files to different formats
- Process multiple files at once
- Password protection - Password protection
- Multiple accounts - Multiple accounts
@@ -20,8 +21,10 @@ A self-hosted online file converter. Supports 831 different formats. Written wit
| Converter | Use case | Converts from | Converts to | | Converter | Use case | Converts from | Converts to |
|------------------------------------------------------------------------------|---------------|---------------|-------------| |------------------------------------------------------------------------------|---------------|---------------|-------------|
| [libjxl](https://github.com/libjxl/libjxl) | JPEG XL | 11 | 11 | | [libjxl](https://github.com/libjxl/libjxl) | JPEG XL | 11 | 11 |
| [resvg](https://github.com/RazrFalcon/resvg) | SVG | 1 | 1 |
| [Vips](https://github.com/libvips/libvips) | Images | 45 | 23 | | [Vips](https://github.com/libvips/libvips) | Images | 45 | 23 |
| [XeLaTeX](https://tug.org/xetex/) | Documents | 1 | 1 | | [Assimp](https://github.com/assimp/assimp) | 3D Assets | 70 | 24 |
| [XeLaTeX](https://tug.org/xetex/) | LaTeX | 1 | 1 |
| [Pandoc](https://pandoc.org/) | Documents | 43 | 65 | | [Pandoc](https://pandoc.org/) | Documents | 43 | 65 |
| [GraphicsMagick](http://www.graphicsmagick.org/) | Images | 166 | 133 | | [GraphicsMagick](http://www.graphicsmagick.org/) | Images | 166 | 133 |
| [FFmpeg](https://ffmpeg.org/) | Video | ~473 | ~280 | | [FFmpeg](https://ffmpeg.org/) | Video | ~473 | ~280 |
@@ -37,21 +40,25 @@ Any missing converter? Open an issue or pull request!
services: services:
convertx: convertx:
image: ghcr.io/c4illin/convertx image: ghcr.io/c4illin/convertx
container_name: convertx
restart: unless-stopped
ports: ports:
- "3000:3000" - "3000:3000"
environment: # Defaults are listed below. All are optional. 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) - 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 - JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234 # will use randomUUID() by default
- HTTP_ALLOWED=false # setting this to true is unsafe, only set this to true locally - 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=24 # checks every n hours for files older then n hours and deletes them, set to 0 to disable
volumes: volumes:
- convertx:/app/data - convertx:/app/data
``` ```
<!-- or or
```bash ```bash
docker run ghcr.io/c4illin/convertx:master -p 3000:3000 -e ACCOUNT_REGISTRATION=false -v /path/you/want:/app/data docker run -p 3000:3000 -v ./data:/app/data ghcr.io/c4illin/convertx
``` --> ```
Then visit `http://localhost:3000` in your browser and create your account. Don't leave it unconfigured and open, as anyone can register the first account. 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.
@@ -61,14 +68,31 @@ If you get unable to open database file run `chown -R $USER:$USER path` on the p
Tutorial in french: https://belginux.com/installer-convertx-avec-docker/ Tutorial in french: https://belginux.com/installer-convertx-avec-docker/
## Screenshots
![ConvertX Preview](images/preview.png)
## Development
0. Install [Bun](https://bun.sh/) and Git
1. Clone the repository
2. `bun install`
3. `bun run dev`
Pull requests are welcome! See below and open issues for the list of todos.
## Todo ## Todo
- [x] Add messages for errors in converters - [x] Add messages for errors in converters
- [x] Add searchable list of formats
- [ ] Add options for converters - [ ] Add options for converters
- [ ] Add more converters
- [ ] Divide index.tsx into smaller components - [ ] Divide index.tsx into smaller components
- [ ] Add tests - [ ] 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. - [ ] Make the upload button nicer and more easy to drop files on. Support copy paste as well if possible.
- [ ] Make errors logs visible from the web ui
- [ ] Add more converters:
- [ ] [deark](https://github.com/jsummers/deark)
- [ ] LibreOffice
- [ ] [dvisvgm](https://github.com/mgieseki/dvisvgm)
## Contributors ## Contributors

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": { "formatter": {
"enabled": true, "enabled": true,
"formatWithErrors": true, "formatWithErrors": true,
@@ -9,7 +9,15 @@
"lineWidth": 80, "lineWidth": 80,
"attributePosition": "auto" "attributePosition": "auto"
}, },
"organizeImports": { "enabled": true }, "files": {
"ignore": [
"**/node_modules/**",
"**/pico.lime.min.css"
]
},
"organizeImports": {
"enabled": true
},
"linter": { "linter": {
"enabled": true, "enabled": true,
"rules": { "rules": {
@@ -22,7 +30,11 @@
"useLiteralKeys": "error", "useLiteralKeys": "error",
"useOptionalChain": "error" "useOptionalChain": "error"
}, },
"correctness": { "noPrecisionLoss": "error", "noUnusedVariables": "off" }, "correctness": {
"noPrecisionLoss": "error",
"noUnusedVariables": "off",
"useJsxKeyInIterable": "off"
},
"style": { "style": {
"noInferrableTypes": "error", "noInferrableTypes": "error",
"noNamespace": "error", "noNamespace": "error",
@@ -42,6 +54,9 @@
"noUnsafeDeclarationMerging": "error", "noUnsafeDeclarationMerging": "error",
"useAwait": "error", "useAwait": "error",
"useNamespaceKeyword": "error" "useNamespaceKeyword": "error"
},
"nursery": {
"useSortedClasses": "error"
} }
} }
}, },

BIN
bun.lockb

Binary file not shown.

View File

@@ -5,8 +5,11 @@ services:
# dockerfile: Debian.Dockerfile # dockerfile: Debian.Dockerfile
volumes: volumes:
- ./data:/app/data - ./data:/app/data
environment: environment: # Defaults are listed below. All are optional.
- ACCOUNT_REGISTRATION=true - 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 - JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234 # will use randomUUID() by default
- HTTP_ALLOWED=true # setting this to true is unsafe, only set this to true locally
- ALLOW_UNAUTHENTICATED=true # 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
ports: ports:
- 3000:3000 - 3000:3000

59
eslint.config.js Normal file
View File

@@ -0,0 +1,59 @@
import { fixupPluginRules } from "@eslint/compat";
import eslint from "@eslint/js";
import deprecationPlugin from "eslint-plugin-deprecation";
import eslintPluginReadableTailwind from "eslint-plugin-readable-tailwind";
import simpleImportSortPlugin from "eslint-plugin-simple-import-sort";
import tailwind from "eslint-plugin-tailwindcss";
import globals from "globals";
import tseslint from "typescript-eslint";
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
...tailwind.configs["flat/recommended"],
{
plugins: {
deprecation: fixupPluginRules(deprecationPlugin),
"simple-import-sort": simpleImportSortPlugin,
"readable-tailwind": eslintPluginReadableTailwind,
},
ignores: ["**/node_modules/**"],
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
ecmaVersion: "latest",
sourceType: "module",
project: ["./tsconfig.json"],
},
globals: {
...globals.node,
...globals.browser,
},
},
files: ["**/*.{js,mjs,cjs,tsx,ts}"],
rules: {
...eslintPluginReadableTailwind.configs.warning.rules,
"tailwindcss/classnames-order": "off",
"readable-tailwind/multiline": [
"warn",
{
group: "newLine",
printWidth: 100,
},
],
"tailwindcss/no-custom-classname": [
"warn",
{
whitelist: [
"select_container",
"convert_to_popup",
"convert_to_group",
"target",
"convert_to_target",
],
},
],
},
},
);

BIN
images/preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -1,21 +1,22 @@
{ {
"name": "convertx-frontend", "name": "convertx-frontend",
"version": "0.3.3", "version": "0.8.1",
"scripts": { "scripts": {
"dev": "bun run --watch src/index.tsx", "dev": "bun run --watch src/index.tsx",
"hot": "bun run --hot src/index.tsx", "hot": "bun run --hot src/index.tsx",
"format": "biome format --write ./src", "format": "eslint --fix .",
"css": "cpy 'node_modules/@picocss/pico/css/pico.lime.min.css' 'src/public/' --flat", "build": "postcss ./src/main.css -o ./src/public/generated.css",
"lint": "run-p 'lint:*'", "lint": "run-p 'lint:*'",
"lint:tsc": "tsc --noEmit", "lint:tsc": "tsc --noEmit",
"lint:knip": "knip" "lint:knip": "knip",
"lint:eslint": "eslint ."
}, },
"dependencies": { "dependencies": {
"@elysiajs/cookie": "^0.8.0", "@elysiajs/cookie": "^0.8.0",
"@elysiajs/html": "1.0.2", "@elysiajs/html": "1.0.2",
"@elysiajs/jwt": "^1.1.0", "@elysiajs/jwt": "^1.1.1",
"@elysiajs/static": "1.0.3", "@elysiajs/static": "1.0.3",
"elysia": "^1.1.4" "elysia": "^1.1.17"
}, },
"module": "src/index.tsx", "module": "src/index.tsx",
"type": "module", "type": "module",
@@ -23,28 +24,38 @@
"start": "bun run src/index.tsx" "start": "bun run src/index.tsx"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.8.3", "@eslint/compat": "^1.1.1",
"@eslint/js": "^9.12.0",
"@ianvs/prettier-plugin-sort-imports": "^4.3.1", "@ianvs/prettier-plugin-sort-imports": "^4.3.1",
"@kitajs/ts-html-plugin": "^4.0.2", "@kitajs/ts-html-plugin": "^4.1.0",
"@picocss/pico": "^2.0.6", "@total-typescript/ts-reset": "^0.6.1",
"@total-typescript/ts-reset": "^0.5.1", "@types/bun": "^1.1.10",
"@types/bun": "^1.1.6", "@types/eslint": "^9.6.1",
"@types/eslint": "^9.6.0", "@types/eslint-plugin-tailwindcss": "^3.17.0",
"@types/node": "^22.0.0", "@types/eslint__js": "^8.42.3",
"@typescript-eslint/eslint-plugin": "^7.18.0", "@types/node": "^22.7.4",
"@typescript-eslint/parser": "^7.18.0", "@typescript-eslint/eslint-plugin": "^8.7.0",
"cpy-cli": "^5.0.0", "@typescript-eslint/parser": "^8.7.0",
"eslint": "^9.8.0", "autoprefixer": "^10.4.20",
"cssnano": "^7.0.6",
"eslint": "^9.12.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.29.1", "eslint-plugin-deprecation": "^3.0.0",
"eslint-plugin-isaacscript": "^3.12.2", "eslint-plugin-isaacscript": "^4.0.0",
"eslint-plugin-prettier": "^5.2.1", "eslint-plugin-prettier": "^5.2.1",
"knip": "^5.27.0", "eslint-plugin-readable-tailwind": "^1.8.1",
"npm-run-all": "^4.1.5", "eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-tailwindcss": "^3.17.4",
"globals": "^15.9.0",
"knip": "^5.30.6",
"npm-run-all2": "^6.2.3",
"postcss": "^8.4.47",
"postcss-cli": "^11.0.0",
"postcss-lightningcss": "^1.0.1",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"typescript": "^5.5.4" "tailwind-scrollbar": "^3.1.0",
}, "tailwindcss": "^3.4.13",
"trustedDependencies": [ "typescript": "^5.6.2",
"@biomejs/biome" "typescript-eslint": "^8.8.0"
] }
} }

9
postcss.config.cjs Normal file
View File

@@ -0,0 +1,9 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {})
}
}

7
renovate.json Normal file
View File

@@ -0,0 +1,7 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
":disableDependencyDashboard"
]
}

View File

@@ -7,8 +7,7 @@ export const BaseHtml = ({
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title safe>{title}</title> <title safe>{title}</title>
<link rel="stylesheet" href="/pico.lime.min.css" /> <link rel="stylesheet" href="/generated.css" />
<link rel="stylesheet" href="/style.css" />
<link <link
rel="apple-touch-icon" rel="apple-touch-icon"
sizes="180x180" sizes="180x180"
@@ -28,6 +27,6 @@ export const BaseHtml = ({
/> />
<link rel="manifest" href="/site.webmanifest" /> <link rel="manifest" href="/site.webmanifest" />
</head> </head>
<body>{children}</body> <body class="w-full bg-neutral-900 text-neutral-200">{children}</body>
</html> </html>
); );

View File

@@ -5,44 +5,65 @@ export const Header = ({
let rightNav: JSX.Element; let rightNav: JSX.Element;
if (loggedIn) { if (loggedIn) {
rightNav = ( rightNav = (
<ul> <ul class="flex gap-4">
<li> <li>
<a href="/history">History</a> <a
class={`
text-accent-600 transition-all
hover:text-accent-500 hover:underline
`}
href="/history">
History
</a>
</li> </li>
<li> <li>
<a href="/logoff">Logout</a> <a
class={`
text-accent-600 transition-all
hover:text-accent-500 hover:underline
`}
href="/logoff">
Logout
</a>
</li> </li>
</ul> </ul>
); );
} else { } else {
rightNav = ( rightNav = (
<ul> <ul class="flex gap-4">
<li> <li>
<a href="/login">Login</a> <a
class={`
text-accent-600 transition-all
hover:text-accent-500 hover:underline
`}
href="/login">
Login
</a>
</li> </li>
{accountRegistration && ( {accountRegistration ? (
<li> <li>
<a href="/register">Register</a> <a
class={`
text-accent-600 transition-all
hover:text-accent-500 hover:underline
`}
href="/register">
Register
</a>
</li> </li>
)} ) : null}
</ul> </ul>
); );
} }
return ( return (
<header class="container"> <header class="w-full p-4">
<nav> <nav class="mx-auto flex max-w-4xl justify-between rounded bg-neutral-900 p-4">
<ul> <ul>
<li> <li>
<strong> <strong>
<a <a href="/">ConvertX</a>
href="/"
style={{
textDecoration: "none",
color: "inherit",
}}>
ConvertX
</a>
</strong> </strong>
</li> </li>
</ul> </ul>

143
src/converters/assimp.ts Normal file
View File

@@ -0,0 +1,143 @@
import { exec } from "node:child_process";
// This could be done dynamically by running `ffmpeg -formats` and parsing the output
export const properties = {
from: {
muxer: [
"3d",
"3ds",
"3mf",
"ac",
"ac3d",
"acc",
"amf",
"ase",
"ask",
"assbin",
"b3d",
"blend",
"bsp",
"bvh",
"cob",
"csm",
"dae",
"dxf",
"enff",
"fbx",
"glb",
"gltf",
"hmp",
"ifc",
"ifczip",
"iqm",
"irr",
"irrmesh",
"lwo",
"lws",
"lxo",
"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: {
muxer: [
"3ds",
"3mf",
"assbin",
"assjson",
"assxml",
"collada",
"dae",
"fbx",
"fbxa",
"glb",
"glb2",
"gltf",
"gltf2",
"m3d",
"m3da",
"obj",
"objnomtl",
"pbrt",
"ply",
"plyb",
"stl",
"stlb",
"stp",
"x",
"x3d",
],
},
};
export async function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
options?: unknown,
): Promise<string> {
// let command = "ffmpeg";
const command = `assimp export "${filePath}" "${targetPath}"`;
return new Promise((resolve, reject) => {
exec(command, (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
});
});
}

View File

@@ -6,6 +6,7 @@ export const properties = {
muxer: [ muxer: [
"264", "264",
"265", "265",
"266",
"302", "302",
"3dostr", "3dostr",
"3g2", "3g2",
@@ -18,6 +19,7 @@ export const properties = {
"aac", "aac",
"aax", "aax",
"ac3", "ac3",
"ac4",
"ace", "ace",
"acm", "acm",
"act", "act",
@@ -48,7 +50,6 @@ export const properties = {
"apng", "apng",
"aptx", "aptx",
"aptxhd", "aptxhd",
"aptx_hd",
"aqt", "aqt",
"aqtitle", "aqtitle",
"argo_asf", "argo_asf",
@@ -63,10 +64,12 @@ export const properties = {
"av1", "av1",
"avc", "avc",
"avi", "avi",
"avif",
"avr", "avr",
"avs", "avs",
"avs2", "avs2",
"avs3", "avs3",
"awb",
"bcstm", "bcstm",
"bethsoftvid", "bethsoftvid",
"bfi", "bfi",
@@ -75,8 +78,10 @@ export const properties = {
"bink", "bink",
"binka", "binka",
"bit", "bit",
"bmp_pipe", "bitpacked",
"bmv", "bmv",
"bmp",
"bonk",
"boa", "boa",
"brender_pix", "brender_pix",
"brstm", "brstm",
@@ -93,7 +98,7 @@ export const properties = {
"codec2", "codec2",
"codec2raw", "codec2raw",
"concat", "concat",
"cri_pipe", "cri",
"dash", "dash",
"dat", "dat",
"data", "data",
@@ -101,8 +106,9 @@ export const properties = {
"dav", "dav",
"dbm", "dbm",
"dcstr", "dcstr",
"dds_pipe", "dds",
"derf", "derf",
"dfpwm",
"dfa", "dfa",
"dhav", "dhav",
"dif", "dif",
@@ -131,6 +137,8 @@ export const properties = {
"exr_pipe", "exr_pipe",
"f32be", "f32be",
"f32le", "f32le",
"ec3",
"evc",
"f4v", "f4v",
"f64be", "f64be",
"f64le", "f64le",
@@ -157,13 +165,13 @@ export const properties = {
"gdv", "gdv",
"genh", "genh",
"gif", "gif",
"gif_pipe",
"gsm", "gsm",
"gxf", "gxf",
"h261", "h261",
"h263", "h263",
"h264", "h264",
"h265", "h265",
"h266",
"h26l", "h26l",
"hca", "hca",
"hcom", "hcom",
@@ -180,7 +188,6 @@ export const properties = {
"ifv", "ifv",
"ilbc", "ilbc",
"image2", "image2",
"image2pipe",
"imf", "imf",
"imx", "imx",
"ingenient", "ingenient",
@@ -197,21 +204,17 @@ export const properties = {
"ivr", "ivr",
"j2b", "j2b",
"j2k", "j2k",
"j2k_pipe",
"jack", "jack",
"jacosub", "jacosub",
"jpegls_pipe",
"jpeg_pipe",
"jv", "jv",
"jpegls",
"jpeg",
"jxl",
"kmsgrab", "kmsgrab",
"kux", "kux",
"kvag", "kvag",
"lavfi", "lavfi",
"libcdio", "laf",
"libdc1394",
"libgme",
"libopenmpt",
"live_flv",
"lmlm4", "lmlm4",
"loas", "loas",
"lrc", "lrc",
@@ -224,16 +227,13 @@ export const properties = {
"m4b", "m4b",
"m4v", "m4v",
"mac", "mac",
"matroska",
"mca", "mca",
"mcc", "mcc",
"mdl", "mdl",
"med", "med",
"mgsts",
"microdvd", "microdvd",
"mj2", "mj2",
"mjpeg", "mjpeg",
"mjpeg_2000",
"mjpg", "mjpg",
"mk3d", "mk3d",
"mka", "mka",
@@ -257,9 +257,6 @@ export const properties = {
"mpc", "mpc",
"mpc8", "mpc8",
"mpeg", "mpeg",
"mpegts",
"mpegtsraw",
"mpegvideo",
"mpg", "mpg",
"mpjpeg", "mpjpeg",
"mpl2", "mpl2",
@@ -294,25 +291,27 @@ export const properties = {
"okt", "okt",
"oma", "oma",
"omg", "omg",
"opus",
"openal", "openal",
"oss", "oss",
"osq",
"paf", "paf",
"pam_pipe", "pdv",
"pbm_pipe", "pam",
"pcx_pipe", "pbm",
"pgmyuv_pipe", "pcx",
"pgm_pipe", "pgmyuv",
"pgx_pipe", "pgm",
"photocd_pipe", "pgx",
"pictor_pipe", "photocd",
"pictor",
"pjs", "pjs",
"plm", "plm",
"pmp", "pmp",
"png_pipe", "png",
"ppm", "ppm",
"ppm_pipe", "pp",
"pp_bnk", "psd",
"psd_pipe",
"psm", "psm",
"psp", "psp",
"psxstr", "psxstr",
@@ -323,7 +322,7 @@ export const properties = {
"pvf", "pvf",
"qcif", "qcif",
"qcp", "qcp",
"qdraw_pipe", "qdraw",
"r3d", "r3d",
"rawvideo", "rawvideo",
"rco", "rco",
@@ -335,6 +334,7 @@ export const properties = {
"rm", "rm",
"roq", "roq",
"rpl", "rpl",
"rka",
"rsd", "rsd",
"rso", "rso",
"rt", "rt",
@@ -355,6 +355,7 @@ export const properties = {
"sbc", "sbc",
"sbg", "sbg",
"scc", "scc",
"sdns",
"sdp", "sdp",
"sdr2", "sdr2",
"sds", "sds",
@@ -364,10 +365,9 @@ export const properties = {
"sfx", "sfx",
"sfx2", "sfx2",
"sga", "sga",
"sgi_pipe", "sgi",
"shn", "shn",
"siff", "siff",
"simbiosis_imx",
"sln", "sln",
"smi", "smi",
"smjpeg", "smjpeg",
@@ -389,12 +389,9 @@ export const properties = {
"stp", "stp",
"str", "str",
"sub", "sub",
"subviewer",
"subviewer1",
"sunrast_pipe",
"sup", "sup",
"svag", "svag",
"svg_pipe", "svg",
"svs", "svs",
"sw", "sw",
"swf", "swf",
@@ -404,7 +401,8 @@ export const properties = {
"thd", "thd",
"thp", "thp",
"tiertexseq", "tiertexseq",
"tiff_pipe", "tif",
"tiff",
"tmv", "tmv",
"truehd", "truehd",
"tta", "tta",
@@ -424,6 +422,7 @@ export const properties = {
"ul", "ul",
"ult", "ult",
"umx", "umx",
"usm",
"uw", "uw",
"v", "v",
"v210", "v210",
@@ -447,12 +446,14 @@ export const properties = {
"vql", "vql",
"vt", "vt",
"vtt", "vtt",
"vvc",
"w64", "w64",
"wa",
"wav", "wav",
"way",
"wc3movie", "wc3movie",
"webm", "webm",
"webm_dash_manifest", "webp",
"webp_pipe",
"webvtt", "webvtt",
"wow", "wow",
"wsaud", "wsaud",
@@ -464,32 +465,31 @@ export const properties = {
"x11grab", "x11grab",
"xa", "xa",
"xbin", "xbin",
"xbm_pipe",
"xl", "xl",
"xm", "xm",
"xmd",
"xmv", "xmv",
"xpk", "xpk",
"xpm_pipe",
"xvag", "xvag",
"xwd_pipe",
"xwma", "xwma",
"y4m", "y4m",
"yop", "yop",
"yuv", "yuv",
"yuv10", "yuv10",
"yuv4mpegpipe",
], ],
}, },
to: { to: {
muxer: [ muxer: [
"264", "264",
"265", "265",
"266",
"302", "302",
"3g2", "3g2",
"3gp", "3gp",
"a64", "a64",
"aac", "aac",
"ac3", "ac3",
"ac4",
"adts", "adts",
"adx", "adx",
"afc", "afc",
@@ -497,43 +497,32 @@ export const properties = {
"aifc", "aifc",
"aiff", "aiff",
"al", "al",
"alaw",
"alp",
"alsa",
"amr", "amr",
"amv", "amv",
"apm", "apm",
"apng", "apng",
"aptx", "aptx",
"aptxhd", "aptxhd",
"aptx_hd",
"argo_asf",
"asf", "asf",
"asf_stream",
"ass", "ass",
"ast", "ast",
"au", "au",
"aud",
"av1",
"avi", "avi",
"avm2", "avif",
"avs", "avs",
"avs2", "avs2",
"avs3",
"bit", "bit",
"bmp", "bmp",
"c2", "c2",
"caca",
"caf", "caf",
"cavs", "cavs",
"cavsvideo",
"chk", "chk",
"chromaprint",
"codec2",
"codec2raw",
"cpk", "cpk",
"crc", "cvg",
"dash", "dfpwm",
"data",
"daud",
"dirac",
"dnxhd", "dnxhd",
"dnxhr", "dnxhr",
"dpx", "dpx",
@@ -542,30 +531,16 @@ export const properties = {
"dv", "dv",
"dvd", "dvd",
"eac3", "eac3",
"ec3",
"evc",
"exr", "exr",
"f32be",
"f32le",
"f4v", "f4v",
"f64be",
"f64le",
"fbdev",
"ffmeta", "ffmeta",
"ffmetadata",
"fifo",
"fifo_test",
"filmstrip",
"film_cpk",
"fits", "fits",
"flac", "flac",
"flm", "flm",
"flv", "flv",
"framecrc",
"framehash",
"framemd5",
"g722", "g722",
"g723_1",
"g726",
"g726le",
"gif", "gif",
"gsm", "gsm",
"gxf", "gxf",
@@ -573,32 +548,26 @@ export const properties = {
"h263", "h263",
"h264", "h264",
"h265", "h265",
"hash", "h266",
"hds", "hdr",
"hevc", "hevc",
"hls",
"ico", "ico",
"ilbc",
"im1", "im1",
"im24", "im24",
"im8", "im8",
"image2",
"image2pipe",
"ipod",
"ircam", "ircam",
"isma", "isma",
"ismv", "ismv",
"ivf", "ivf",
"j2c", "j2c",
"j2k", "j2k",
"jacosub",
"jls", "jls",
"jp2", "jp2",
"jpeg", "jpeg",
"jpg", "jpg",
"js", "js",
"jss", "jss",
"kvag", "jxl",
"latm", "latm",
"lbc", "lbc",
"ljpg", "ljpg",
@@ -613,13 +582,9 @@ export const properties = {
"m4a", "m4a",
"m4b", "m4b",
"m4v", "m4v",
"matroska",
"md5",
"microdvd",
"mjpeg", "mjpeg",
"mjpg", "mjpg",
"mkv", "mkv",
"mkvtimestamp_v2",
"mlp", "mlp",
"mmf", "mmf",
"mov", "mov",
@@ -629,26 +594,17 @@ export const properties = {
"mpa", "mpa",
"mpd", "mpd",
"mpeg", "mpeg",
"mpeg1video",
"mpeg2video",
"mpegts",
"mpg", "mpg",
"mpjpeg",
"msbc", "msbc",
"mts", "mts",
"mulaw",
"mxf", "mxf",
"mxf_d10",
"mxf_opatom",
"null",
"nut", "nut",
"obu",
"oga", "oga",
"ogg", "ogg",
"ogv", "ogv",
"oma", "oma",
"opengl",
"opus", "opus",
"oss",
"pam", "pam",
"pbm", "pbm",
"pcm", "pcm",
@@ -656,14 +612,14 @@ export const properties = {
"pfm", "pfm",
"pgm", "pgm",
"pgmyuv", "pgmyuv",
"phm",
"pix", "pix",
"png", "png",
"ppm", "ppm",
"psp", "psp",
"pulse", "qoi",
"ra", "ra",
"ras", "ras",
"rawvideo",
"rco", "rco",
"rcv", "rcv",
"rgb", "rgb",
@@ -671,84 +627,47 @@ export const properties = {
"roq", "roq",
"rs", "rs",
"rso", "rso",
"rtp",
"rtp_mpegts",
"rtsp",
"s16be",
"s16le",
"s24be",
"s24le",
"s32be",
"s32le",
"s8",
"sap",
"sb", "sb",
"sbc", "sbc",
"scc", "scc",
"sdl",
"sdl2",
"segment",
"sf", "sf",
"sgi", "sgi",
"singlejpeg",
"smjpeg",
"smoothstreaming",
"sndio",
"sox", "sox",
"spdif", "spdif",
"spx", "spx",
"srt", "srt",
"ssa", "ssa",
"ssegment",
"streamhash",
"stream_segment",
"sub", "sub",
"sun", "sun",
"sunras", "sunras",
"sup", "sup",
"svcd",
"sw", "sw",
"swf", "swf",
"tco", "tco",
"tee",
"tga", "tga",
"thd", "thd",
"tif", "tif",
"tiff", "tiff",
"truehd",
"ts", "ts",
"tta", "tta",
"ttml", "ttml",
"tun", "tun",
"u16be",
"u16le",
"u24be",
"u24le",
"u32be",
"u32le",
"u8",
"ub", "ub",
"ul", "ul",
"uncodedframecrc",
"uw", "uw",
"v4l2",
"vag", "vag",
"vbn",
"vc1", "vc1",
"vc1test",
"vc2", "vc2",
"vcd",
"vidc",
"video4linux2",
"vob", "vob",
"voc", "voc",
"vtt", "vtt",
"vvc",
"w64", "w64",
"wav", "wav",
"wbmp",
"webm", "webm",
"webm_chunk",
"webm_dash_manifest",
"webp", "webp",
"webvtt",
"wma", "wma",
"wmv", "wmv",
"wtv", "wtv",
@@ -756,12 +675,10 @@ export const properties = {
"xbm", "xbm",
"xface", "xface",
"xml", "xml",
"xv",
"xwd", "xwd",
"y", "y",
"y4m", "y4m",
"yuv", "yuv",
"yuv4mpegpipe",
], ],
}, },
}; };
@@ -771,42 +688,19 @@ export async function convert(
fileType: string, fileType: string,
convertTo: string, convertTo: string,
targetPath: string, targetPath: string,
// biome-ignore lint/suspicious/noExplicitAny: <explanation> // eslint-disable-next-line @typescript-eslint/no-unused-vars
options?: any, options?: unknown,
): Promise<string> { ): Promise<string> {
// let command = "ffmpeg"; let extra = "";
let message = "Done";
// these are containers that can contain multiple formats if (convertTo === "ico") {
// const autoDetect = [ // make sure image is 256x256 or smaller
// "mp4", extra = `-filter:v "scale='min(256,iw)':min'(256,ih)':force_original_aspect_ratio=decrease"`;
// "mkv", message = "Done: resized to 256x256";
// "avi", }
// "mov",
// "m4a",
// "3gp",
// "3g2",
// "mj2",
// "psp",
// "m4b",
// "ism",
// "ismv",
// "isma",
// "f4v",
// ];
// if (!(fileType in autoDetect)) { const command = `ffmpeg -i "${filePath}" ${extra} "${targetPath}"`;
// command += ` -f "${fileType}"`;
// }
// command += ` -i "${filePath}"`;
// if (!(convertTo in autoDetect)) {
// command += ` -f "${convertTo}"`;
// }
// command += ` "${targetPath}"`;
const command = `ffmpeg -i "${filePath}" "${targetPath}"`;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
exec(command, (error, stdout, stderr) => { exec(command, (error, stdout, stderr) => {
@@ -822,7 +716,7 @@ export async function convert(
console.error(`stderr: ${stderr}`); console.error(`stderr: ${stderr}`);
} }
resolve("success"); resolve(message);
}); });
}); });
} }

View File

@@ -143,6 +143,7 @@ export const properties = {
"svgz", "svgz",
"text", "text",
"tga", "tga",
"tif",
"tiff", "tiff",
"tile", "tile",
"tim", "tim",
@@ -227,7 +228,6 @@ export const properties = {
"jbig", "jbig",
"jng", "jng",
"jpeg", "jpeg",
"jpg",
"k", "k",
"m", "m",
"m2v", "m2v",
@@ -313,8 +313,8 @@ export function convert(
fileType: string, fileType: string,
convertTo: string, convertTo: string,
targetPath: string, targetPath: string,
// biome-ignore lint/suspicious/noExplicitAny: <explanation> // eslint-disable-next-line @typescript-eslint/no-unused-vars
options?: any, options?: unknown,
): Promise<string> { ): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
exec( exec(
@@ -332,7 +332,7 @@ export function convert(
console.error(`stderr: ${stderr}`); console.error(`stderr: ${stderr}`);
} }
resolve("success"); resolve("Done");
}, },
); );
}); });

View File

@@ -39,8 +39,8 @@ export function convert(
fileType: string, fileType: string,
convertTo: string, convertTo: string,
targetPath: string, targetPath: string,
// biome-ignore lint/suspicious/noExplicitAny: <explanation> // eslint-disable-next-line @typescript-eslint/no-unused-vars
options?: any, options?: unknown,
): Promise<string> { ): Promise<string> {
let tool = ""; let tool = "";
if (fileType === "jxl") { if (fileType === "jxl") {
@@ -65,7 +65,7 @@ export function convert(
console.error(`stderr: ${stderr}`); console.error(`stderr: ${stderr}`);
} }
resolve("success"); resolve("Done");
}); });
}); });
} }

View File

@@ -1,64 +1,52 @@
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 {
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 { normalizeFiletype } from "../helpers/normalizeFiletype";
import { convert as convertassimp, properties as propertiesassimp } from "./assimp";
import { convert as convertFFmpeg, properties as propertiesFFmpeg } from "./ffmpeg";
import { convert as convertGraphicsmagick, properties as propertiesGraphicsmagick } from "./graphicsmagick";
import { convert as convertLibjxl, properties as propertiesLibjxl } from "./libjxl";
import { convert as convertPandoc, properties as propertiesPandoc } from "./pandoc";
import { convert as convertresvg, properties as propertiesresvg } from "./resvg";
import { convert as convertImage, properties as propertiesImage } from "./vips";
import { convert as convertxelatex, properties as propertiesxelatex } from "./xelatex";
// This should probably be reconstructed so that the functions are not imported instead the functions hook into this to make the converters more modular // 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: { const properties: Record<
[key: string]: { string,
{
properties: { properties: {
from: { [key: string]: string[] }; from: Record<string, string[]>;
to: { [key: string]: string[] }; to: Record<string, string[]>;
options?: { options?: Record<
[key: string]: { string,
[key: string]: { Record<
string,
{
description: string; description: string;
type: string; type: string;
default: number; default: number;
}; }
}; >
}; >;
}; };
converter: ( converter: (
filePath: string, filePath: string,
fileType: string, fileType: string,
convertTo: string, convertTo: string,
targetPath: string, targetPath: string,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
options?: any, options?: unknown,
// biome-ignore lint/suspicious/noExplicitAny: <explanation> ) => unknown;
) => any; }
}; > = {
} = {
libjxl: { libjxl: {
properties: propertiesLibjxl, properties: propertiesLibjxl,
converter: convertLibjxl, converter: convertLibjxl,
}, },
resvg: {
properties: propertiesresvg,
converter: convertresvg,
},
vips: { vips: {
properties: propertiesImage, properties: propertiesImage,
converter: convertImage, converter: convertImage,
@@ -75,6 +63,10 @@ const properties: {
properties: propertiesGraphicsmagick, properties: propertiesGraphicsmagick,
converter: convertGraphicsmagick, converter: convertGraphicsmagick,
}, },
assimp: {
properties: propertiesassimp,
converter: convertassimp,
},
ffmpeg: { ffmpeg: {
properties: propertiesFFmpeg, properties: propertiesFFmpeg,
converter: convertFFmpeg, converter: convertFFmpeg,
@@ -84,24 +76,19 @@ const properties: {
export async function mainConverter( export async function mainConverter(
inputFilePath: string, inputFilePath: string,
fileTypeOriginal: string, fileTypeOriginal: string,
// biome-ignore lint/suspicious/noExplicitAny: <explanation> convertTo: string,
convertTo: any,
targetPath: string, targetPath: string,
// biome-ignore lint/suspicious/noExplicitAny: <explanation> options?: unknown,
options?: any,
converterName?: string, converterName?: string,
) { ) {
const fileType = normalizeFiletype(fileTypeOriginal); const fileType = normalizeFiletype(fileTypeOriginal);
// biome-ignore lint/suspicious/noExplicitAny: <explanation> let converterFunc: typeof properties["libjxl"]["converter"] | undefined;
let converterFunc: any;
// let converterName = converterName;
if (converterName) { if (converterName) {
converterFunc = properties[converterName]?.converter; converterFunc = properties[converterName]?.converter;
} else { } else {
// Iterate over each converter in properties // Iterate over each converter in properties
// biome-ignore lint/style/noParameterAssign: <explanation>
for (converterName in properties) { for (converterName in properties) {
const converterObj = properties[converterName]; const converterObj = properties[converterName];
@@ -129,7 +116,7 @@ export async function mainConverter(
} }
try { try {
await converterFunc( const result = await converterFunc(
inputFilePath, inputFilePath,
fileType, fileType,
convertTo, convertTo,
@@ -139,7 +126,13 @@ export async function mainConverter(
console.log( console.log(
`Converted ${inputFilePath} from ${fileType} to ${convertTo} successfully using ${converterName}.`, `Converted ${inputFilePath} from ${fileType} to ${convertTo} successfully using ${converterName}.`,
result,
); );
if (typeof result === "string") {
return result;
}
return "Done"; return "Done";
} catch (error) { } catch (error) {
console.error( console.error(
@@ -150,7 +143,7 @@ export async function mainConverter(
} }
} }
const possibleTargets: { [key: string]: { [key: string]: string[] } } = {}; const possibleTargets: Record<string, Record<string, string[]>> = {};
for (const converterName in properties) { for (const converterName in properties) {
const converterProperties = properties[converterName]?.properties; const converterProperties = properties[converterName]?.properties;
@@ -175,9 +168,7 @@ for (const converterName in properties) {
} }
} }
export const getPossibleTargets = ( export const getPossibleTargets = (from: string): Record<string, string[]> => {
from: string,
): { [key: string]: string[] } => {
const fromClean = normalizeFiletype(from); const fromClean = normalizeFiletype(from);
return possibleTargets[fromClean] || {}; return possibleTargets[fromClean] || {};
@@ -201,11 +192,12 @@ for (const converterName in properties) {
} }
possibleInputs.sort(); possibleInputs.sort();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const getPossibleInputs = () => { const getPossibleInputs = () => {
return possibleInputs; return possibleInputs;
}; };
const allTargets: { [key: string]: string[] } = {}; const allTargets: Record<string, string[]> = {};
for (const converterName in properties) { for (const converterName in properties) {
const converterProperties = properties[converterName]?.properties; const converterProperties = properties[converterName]?.properties;
@@ -227,7 +219,7 @@ export const getAllTargets = () => {
return allTargets; return allTargets;
}; };
const allInputs: { [key: string]: string[] } = {}; const allInputs: Record<string, string[]> = {};
for (const converterName in properties) { for (const converterName in properties) {
const converterProperties = properties[converterName]?.properties; const converterProperties = properties[converterName]?.properties;

View File

@@ -124,8 +124,8 @@ export function convert(
fileType: string, fileType: string,
convertTo: string, convertTo: string,
targetPath: string, targetPath: string,
// biome-ignore lint/suspicious/noExplicitAny: <explanation> // eslint-disable-next-line @typescript-eslint/no-unused-vars
options?: any, options?: unknown,
): Promise<string> { ): Promise<string> {
// set xelatex here // set xelatex here
const xelatex = ["pdf", "latex"]; const xelatex = ["pdf", "latex"];
@@ -149,7 +149,7 @@ export function convert(
console.error(`stderr: ${stderr}`); console.error(`stderr: ${stderr}`);
} }
resolve("success"); resolve("Done");
}, },
); );
}); });

37
src/converters/resvg.ts Normal file
View File

@@ -0,0 +1,37 @@
import { exec } from "node:child_process";
export const properties = {
from: {
images: ["svg"],
},
to: {
images: ["png"],
},
};
export function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
options?: unknown,
): Promise<string> {
return new Promise((resolve, reject) => {
exec(`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");
});
});
}

View File

@@ -1,5 +1,6 @@
import { exec } from "node:child_process"; import { exec } from "node:child_process";
// declare possible conversions // declare possible conversions
export const properties = { export const properties = {
from: { from: {
@@ -94,8 +95,8 @@ export function convert(
fileType: string, fileType: string,
convertTo: string, convertTo: string,
targetPath: string, targetPath: string,
// biome-ignore lint/suspicious/noExplicitAny: <explanation> // eslint-disable-next-line @typescript-eslint/no-unused-vars
options?: any, options?: unknown,
): Promise<string> { ): Promise<string> {
// if (fileType === "svg") { // if (fileType === "svg") {
// const scale = options.scale || 1; // const scale = options.scale || 1;
@@ -113,9 +114,15 @@ export function convert(
// .toFormat(convertTo) // .toFormat(convertTo)
// .toFile(targetPath); // .toFile(targetPath);
// } // }
let action = "copy";
if (fileType === "pdf") {
action = "pdfload";
}
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
exec(`vips copy "${filePath}" "${targetPath}"`, (error, stdout, stderr) => { exec(
`vips ${action} "${filePath}" "${targetPath}"`,
(error, stdout, stderr) => {
if (error) { if (error) {
reject(`error: ${error}`); reject(`error: ${error}`);
} }
@@ -128,7 +135,8 @@ export function convert(
console.error(`stderr: ${stderr}`); console.error(`stderr: ${stderr}`);
} }
resolve("success"); resolve("Done");
}); },
);
}); });
} }

View File

@@ -14,8 +14,8 @@ export function convert(
fileType: string, fileType: string,
convertTo: string, convertTo: string,
targetPath: string, targetPath: string,
// biome-ignore lint/suspicious/noExplicitAny: <explanation> // eslint-disable-next-line @typescript-eslint/no-unused-vars
options?: any, options?: unknown,
): Promise<string> { ): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// const fileName: string = (targetPath.split("/").pop() as string).replace(".pdf", "") // const fileName: string = (targetPath.split("/").pop() as string).replace(".pdf", "")
@@ -39,7 +39,7 @@ export function convert(
console.error(`stderr: ${stderr}`); console.error(`stderr: ${stderr}`);
} }
resolve("success"); resolve("Done");
}, },
); );
}); });

View File

@@ -2,6 +2,7 @@ export const normalizeFiletype = (filetype: string): string => {
const lowercaseFiletype = filetype.toLowerCase(); const lowercaseFiletype = filetype.toLowerCase();
switch (lowercaseFiletype) { switch (lowercaseFiletype) {
case "jfif":
case "jpg": case "jpg":
return "jpeg"; return "jpeg";
case "htm": case "htm":
@@ -23,6 +24,9 @@ export const normalizeOutputFiletype = (filetype: string): string => {
return "jpg"; return "jpg";
case "latex": case "latex":
return "tex"; return "tex";
case "markdown_phpextra":
case "markdown_strict":
case "markdown_mmd":
case "markdown": case "markdown":
return "md"; return "md";
default: default:

View File

@@ -73,6 +73,26 @@ 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 v${stdout.split("\n")[5]}`);
}
});
exec("bun -v", (error, stdout) => { exec("bun -v", (error, stdout) => {
if (error) { if (error) {
console.error("Bun is not installed. wait what"); console.error("Bun is not installed. wait what");

17
src/helpers/tailwind.ts Normal file
View File

@@ -0,0 +1,17 @@
import tw from "tailwindcss";
import postcss from "postcss";
export const generateTailwind = async () => {
const result = await Bun.file("./src/main.css")
.text()
.then((sourceText) => {
const config = "./tailwind.config.js";
return postcss([tw(config)]).process(sourceText, {
from: "./src/main.css",
to: "./public/generated.css",
});
});
return result;
};

View File

@@ -1,11 +1,11 @@
import { Database } from "bun:sqlite"; import { randomInt, randomUUID } from "node:crypto";
import { randomUUID } from "node:crypto";
import { rmSync } from "node:fs"; import { rmSync } from "node:fs";
import { mkdir, unlink } from "node:fs/promises"; import { mkdir, unlink } from "node:fs/promises";
import cookie from "@elysiajs/cookie"; import cookie from "@elysiajs/cookie";
import { html } from "@elysiajs/html"; import { html } from "@elysiajs/html";
import { jwt } from "@elysiajs/jwt"; import { jwt, type JWTPayloadSpec } from "@elysiajs/jwt";
import { staticPlugin } from "@elysiajs/static"; import { staticPlugin } from "@elysiajs/static";
import { Database } from "bun:sqlite";
import { Elysia, t } from "elysia"; import { Elysia, t } from "elysia";
import { BaseHtml } from "./components/base"; import { BaseHtml } from "./components/base";
import { Header } from "./components/header"; import { Header } from "./components/header";
@@ -27,9 +27,15 @@ const uploadsDir = "./data/uploads/";
const outputDir = "./data/output/"; const outputDir = "./data/output/";
const ACCOUNT_REGISTRATION = const ACCOUNT_REGISTRATION =
process.env.ACCOUNT_REGISTRATION === "true" || false; process.env.ACCOUNT_REGISTRATION?.toLowerCase() === "true" || false;
const HTTP_ALLOWED = process.env.HTTP_ALLOWED === "true" || false; const HTTP_ALLOWED =
process.env.HTTP_ALLOWED?.toLowerCase() === "true" || false;
const ALLOW_UNAUTHENTICATED =
process.env.ALLOW_UNAUTHENTICATED?.toLowerCase() === "true" || false;
const AUTO_DELETE_EVERY_N_HOURS = process.env.AUTO_DELETE_EVERY_N_HOURS
? Number(process.env.AUTO_DELETE_EVERY_N_HOURS)
: 24;
// fileNames: fileNames, // fileNames: fileNames,
// filesToConvert: fileNames.length, // filesToConvert: fileNames.length,
@@ -115,7 +121,7 @@ const app = new Elysia({
schema: t.Object({ schema: t.Object({
id: t.String(), id: t.String(),
}), }),
secret: process.env.JWT_SECRET || randomUUID(), secret: process.env.JWT_SECRET ?? randomUUID(),
exp: "7d", exp: "7d",
}), }),
) )
@@ -132,36 +138,51 @@ const app = new Elysia({
return ( return (
<BaseHtml title="ConvertX | Setup"> <BaseHtml title="ConvertX | Setup">
<main class="container"> <main class="mx-auto w-full max-w-4xl px-4">
<h1>Welcome to ConvertX</h1> <h1 class="my-8 text-3xl">Welcome to ConvertX!</h1>
<article> <article class="article p-0">
<header>Create your account</header> <header class="w-full bg-neutral-800 p-4">
<form method="post" action="/register"> Create your account
<fieldset> </header>
<label> <form method="post" action="/register" class="p-4">
Email/Username <fieldset class="mb-4 flex flex-col gap-4">
<label class="flex flex-col gap-1">
Email
<input <input
type="email" type="email"
name="email" name="email"
class="rounded bg-neutral-800 p-3"
placeholder="Email" placeholder="Email"
autocomplete="email"
required required
/> />
</label> </label>
<label> <label class="flex flex-col gap-1">
Password Password
<input <input
type="password" type="password"
name="password" name="password"
class="rounded bg-neutral-800 p-3"
placeholder="Password" placeholder="Password"
autocomplete="current-password"
required required
/> />
</label> </label>
</fieldset> </fieldset>
<input type="submit" value="Create account" /> <input type="submit" value="Create account" class="btn-primary" />
</form> </form>
<footer> <footer class="p-4">
Report any issues on{" "} Report any issues on{" "}
<a href="https://github.com/C4illin/ConvertX">GitHub</a>. <a
class={`
text-accent-500 underline
hover:text-accent-400
`}
href="https://github.com/C4illin/ConvertX"
>
GitHub
</a>
.
</footer> </footer>
</article> </article>
</main> </main>
@@ -177,32 +198,38 @@ const app = new Elysia({
<BaseHtml title="ConvertX | Register"> <BaseHtml title="ConvertX | Register">
<> <>
<Header accountRegistration={ACCOUNT_REGISTRATION} /> <Header accountRegistration={ACCOUNT_REGISTRATION} />
<main class="container"> <main class="w-full px-4">
<article> <article class="article">
<form method="post"> <form method="post" class="flex flex-col gap-4">
<fieldset> <fieldset class="mb-4 flex flex-col gap-4">
<label> <label class="flex flex-col gap-1">
Email Email
<input <input
type="email" type="email"
name="email" name="email"
class="rounded bg-neutral-800 p-3"
placeholder="Email" placeholder="Email"
autocomplete="email" autocomplete="email"
required required
/> />
</label> </label>
<label> <label class="flex flex-col gap-1">
Password Password
<input <input
type="password" type="password"
name="password" name="password"
class="rounded bg-neutral-800 p-3"
placeholder="Password" placeholder="Password"
autocomplete="new-password" autocomplete="current-password"
required required
/> />
</label> </label>
</fieldset> </fieldset>
<input type="submit" value="Register" /> <input
type="submit"
value="Register"
class="btn-primary w-full"
/>
</form> </form>
</article> </article>
</main> </main>
@@ -293,25 +320,27 @@ const app = new Elysia({
<BaseHtml title="ConvertX | Login"> <BaseHtml title="ConvertX | Login">
<> <>
<Header accountRegistration={ACCOUNT_REGISTRATION} /> <Header accountRegistration={ACCOUNT_REGISTRATION} />
<main class="container"> <main class="w-full px-4">
<article> <article class="article">
<form method="post"> <form method="post" class="flex flex-col gap-4">
<fieldset> <fieldset class="mb-4 flex flex-col gap-4">
<label> <label class="flex flex-col gap-1">
Email Email
<input <input
type="email" type="email"
name="email" name="email"
class="rounded bg-neutral-800 p-3"
placeholder="Email" placeholder="Email"
autocomplete="email" autocomplete="email"
required required
/> />
</label> </label>
<label> <label class="flex flex-col gap-1">
Password Password
<input <input
type="password" type="password"
name="password" name="password"
class="rounded bg-neutral-800 p-3"
placeholder="Password" placeholder="Password"
autocomplete="current-password" autocomplete="current-password"
required required
@@ -319,12 +348,20 @@ const app = new Elysia({
</label> </label>
</fieldset> </fieldset>
<div role="group"> <div role="group">
{ACCOUNT_REGISTRATION && ( {ACCOUNT_REGISTRATION ? (
<a href="/register" role="button" class="secondary"> <a
href="/register"
role="button"
class="btn-primary w-full"
>
Register an account Register an account
</a> </a>
)} ) : null}
<input type="submit" value="Login" /> <input
type="submit"
value="Login"
class="btn-primary w-full"
/>
</div> </div>
</form> </form>
</article> </article>
@@ -336,7 +373,7 @@ const app = new Elysia({
.post( .post(
"/login", "/login",
async function handler({ body, set, redirect, jwt, cookie: { auth } }) { async function handler({ body, set, redirect, jwt, cookie: { auth } }) {
const existingUser = await db const existingUser = db
.query("SELECT * FROM users WHERE email = ?") .query("SELECT * FROM users WHERE email = ?")
.as(User) .as(User)
.get(body.email); .get(body.email);
@@ -403,17 +440,19 @@ const app = new Elysia({
return redirect("/setup", 302); return redirect("/setup", 302);
} }
if (!auth?.value) { if (!auth?.value && !ALLOW_UNAUTHENTICATED) {
return redirect("/login", 302);
}
// validate jwt
const user = await jwt.verify(auth.value);
if (!user) {
return redirect("/login", 302); return redirect("/login", 302);
} }
// validate jwt
let user: ({ id: string } & JWTPayloadSpec) | false = false;
if (auth?.value) {
user = await jwt.verify(auth.value);
if (user !== false && user.id) {
if (Number.parseInt(user.id) < 2 ** 24 || !ALLOW_UNAUTHENTICATED) {
// make sure user exists in db // make sure user exists in db
const existingUser = await db const existingUser = db
.query("SELECT * FROM users WHERE id = ?") .query("SELECT * FROM users WHERE id = ?")
.as(User) .as(User)
.get(user.id); .get(user.id);
@@ -424,6 +463,39 @@ const app = new Elysia({
} }
return redirect("/login", 302); return redirect("/login", 302);
} }
}
}
} else if (ALLOW_UNAUTHENTICATED) {
const newUserId = String(
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",
});
}
if (!user) {
return redirect("/login", 302);
}
// create a new job // create a new job
db.query("INSERT INTO jobs (user_id, date_created) VALUES (?, ?)").run( db.query("INSERT INTO jobs (user_id, date_created) VALUES (?, ?)").run(
@@ -455,37 +527,110 @@ const app = new Elysia({
<BaseHtml> <BaseHtml>
<> <>
<Header loggedIn /> <Header loggedIn />
<main class="container"> <main class="w-full px-4">
<article> <article class="article">
<h1>Convert</h1> <h1 class="mb-4 text-xl">Convert</h1>
<div style={{ maxHeight: "50vh", overflowY: "auto" }}> <div class="mb-4 max-h-[50vh] overflow-y-auto scrollbar-thin">
<table id="file-list" class="striped" /> <table
id="file-list"
class={`
w-full table-auto rounded bg-neutral-900
[&_td]:p-4
[&_tr]:rounded [&_tr]:border-b [&_tr]:border-neutral-800
`}
/>
</div>
<div
id="dropzone"
class={`
relative flex h-48 w-full items-center justify-center rounded border border-dashed
border-neutral-700 transition-all
[&.dragover]:border-4 [&.dragover]:border-neutral-500
hover:border-neutral-600
`}
>
<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> </div>
<input type="file" name="file" multiple />
{/* <label for="convert_from">Convert from</label> */}
{/* <select name="convert_from" aria-label="Convert from" required>
<option selected disabled value="">
Convert from
</option>
{getPossibleInputs().map((input) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<option>{input}</option>
))}
</select> */}
</article> </article>
<form method="post" action="/convert"> <form
method="post"
action="/convert"
class="relative mx-auto mb-[35vh] w-full max-w-4xl"
>
<input type="hidden" name="file_names" id="file_names" /> <input type="hidden" name="file_names" id="file_names" />
<article> <article class="article w-full">
<select name="convert_to" aria-label="Convert to" required> <input
type="search"
name="convert_to_search"
placeholder="Search for conversions"
autocomplete="off"
class="w-full rounded 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-y-auto overflow-x-hidden rounded bg-neutral-800
sm:h-[30vh]
`}
>
{Object.entries(getAllTargets()).map(
([converter, targets]) => (
<article
class={`
convert_to_group flex w-full flex-col border-b border-neutral-700 p-4
`}
data-converter={converter}
>
<header class="mb-2 w-full text-xl font-bold" safe>
{converter}
</header>
<ul class="convert_to_target flex flex-row flex-wrap gap-1">
{targets.map((target) => (
<button
// https://stackoverflow.com/questions/121499/when-a-blur-event-occurs-how-can-i-find-out-which-element-focus-went-to#comment82388679_33325953
tabindex={0}
class={`
target rounded bg-neutral-700 p-1 text-base
hover:bg-neutral-600
`}
data-value={`${target},${converter}`}
data-target={target}
data-converter={converter}
type="button"
safe
>
{target}
</button>
))}
</ul>
</article>
),
)}
</article>
{/* Hidden element which determines the format to convert the file too and the converter to use */}
<select
name="convert_to"
aria-label="Convert to"
required
hidden
>
<option selected disabled value=""> <option selected disabled value="">
Convert to Convert to
</option> </option>
{Object.entries(getAllTargets()).map( {Object.entries(getAllTargets()).map(
([converter, targets]) => ( ([converter, targets]) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<optgroup label={converter}> <optgroup label={converter}>
{targets.map((target) => ( {targets.map((target) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<option value={`${target},${converter}`} safe> <option value={`${target},${converter}`} safe>
{target} {target}
</option> </option>
@@ -494,8 +639,17 @@ const app = new Elysia({
), ),
)} )}
</select> </select>
</div>
</article> </article>
<input type="submit" value="Convert" /> <input
class={`
btn-primary w-full
disabled:cursor-not-allowed disabled:opacity-50
`}
type="submit"
value="Convert"
disabled
/>
</form> </form>
</main> </main>
<script src="script.js" defer /> <script src="script.js" defer />
@@ -507,16 +661,55 @@ const app = new Elysia({
"/conversions", "/conversions",
({ body }) => { ({ body }) => {
return ( return (
<select name="convert_to" aria-label="Convert to" required> <>
<article
class={`
convert_to_popup absolute z-[2] m-0 hidden h-[50vh] max-h-[50vh] w-full flex-col
overflow-y-auto overflow-x-hidden rounded bg-neutral-800
sm:h-[30vh]
`}
>
{Object.entries(getPossibleTargets(body.fileType)).map(
([converter, targets]) => (
<article
class="convert_to_group flex w-full flex-col border-b border-neutral-700 p-4"
data-converter={converter}
>
<header class="mb-2 w-full text-xl font-bold" safe>
{converter}
</header>
<ul class="convert_to_target flex flex-row flex-wrap gap-1">
{targets.map((target) => (
<button
// https://stackoverflow.com/questions/121499/when-a-blur-event-occurs-how-can-i-find-out-which-element-focus-went-to#comment82388679_33325953
tabindex={0}
class={`
target rounded bg-neutral-700 p-1 text-base
hover:bg-neutral-600
`}
data-value={`${target},${converter}`}
data-target={target}
data-converter={converter}
type="button"
safe
>
{target}
</button>
))}
</ul>
</article>
),
)}
</article>
<select name="convert_to" aria-label="Convert to" required hidden>
<option selected disabled value=""> <option selected disabled value="">
Convert to Convert to
</option> </option>
{Object.entries(getPossibleTargets(body.fileType)).map( {Object.entries(getPossibleTargets(body.fileType)).map(
([converter, targets]) => ( ([converter, targets]) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<optgroup label={converter}> <optgroup label={converter}>
{targets.map((target) => ( {targets.map((target) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<option value={`${target},${converter}`} safe> <option value={`${target},${converter}`} safe>
{target} {target}
</option> </option>
@@ -525,6 +718,7 @@ const app = new Elysia({
), ),
)} )}
</select> </select>
</>
); );
}, },
{ body: t.Object({ fileType: t.String() }) }, { body: t.Object({ fileType: t.String() }) },
@@ -561,13 +755,7 @@ const app = new Elysia({
await Bun.write(`${userUploadsDir}${file.name}`, file); await Bun.write(`${userUploadsDir}${file.name}`, file);
} }
} else { } else {
await Bun.write( await Bun.write(`${userUploadsDir}${body.file["name"]}`, body.file);
`${userUploadsDir}${
// biome-ignore lint/complexity/useLiteralKeys: ts bug
body.file["name"]
}`,
body.file,
);
} }
} }
@@ -623,7 +811,7 @@ const app = new Elysia({
return redirect("/", 302); return redirect("/", 302);
} }
const existingJob = await db const existingJob = db
.query("SELECT * FROM jobs WHERE id = ? AND user_id = ?") .query("SELECT * FROM jobs WHERE id = ? AND user_id = ?")
.as(Jobs) .as(Jobs)
.get(jobId.value, user.id); .get(jobId.value, user.id);
@@ -645,9 +833,7 @@ const app = new Elysia({
); );
} }
const convertTo = normalizeFiletype( const convertTo = normalizeFiletype(body.convert_to.split(",")[0] ?? "");
body.convert_to.split(",")[0] as string,
);
const converterName = body.convert_to.split(",")[1]; const converterName = body.convert_to.split(",")[1];
const fileNames = JSON.parse(body.file_names) as string[]; const fileNames = JSON.parse(body.file_names) as string[];
@@ -667,10 +853,13 @@ const app = new Elysia({
Promise.all( Promise.all(
fileNames.map(async (fileName) => { fileNames.map(async (fileName) => {
const filePath = `${userUploadsDir}${fileName}`; const filePath = `${userUploadsDir}${fileName}`;
const fileTypeOrig = fileName.split(".").pop() as string; const fileTypeOrig = fileName.split(".").pop() ?? "";
const fileType = normalizeFiletype(fileTypeOrig); const fileType = normalizeFiletype(fileTypeOrig);
const newFileExt = normalizeOutputFiletype(convertTo); const newFileExt = normalizeOutputFiletype(convertTo);
const newFileName = fileName.replace(fileTypeOrig, newFileExt); const newFileName = fileName.replace(
new RegExp(`${fileTypeOrig}(?!.*${fileTypeOrig})`),
newFileExt,
);
const targetPath = `${userOutputDir}${newFileName}`; const targetPath = `${userOutputDir}${newFileName}`;
const result = await mainConverter( const result = await mainConverter(
@@ -742,29 +931,42 @@ const app = new Elysia({
<BaseHtml title="ConvertX | Results"> <BaseHtml title="ConvertX | Results">
<> <>
<Header loggedIn /> <Header loggedIn />
<main class="container"> <main class="w-full px-4">
<article> <article class="article">
<h1>Results</h1> <h1 class="mb-4 text-xl">Results</h1>
<table> <table
class={`
w-full table-auto rounded bg-neutral-900 text-left
[&_td]:p-4
[&_tr]:rounded [&_tr]:border-b [&_tr]:border-neutral-800
`}
>
<thead> <thead>
<tr> <tr>
<th>Time</th> <th class="px-4 py-2">Time</th>
<th>Files</th> <th class="px-4 py-2">Files</th>
<th>Files Done</th> <th class="px-4 py-2">Files Done</th>
<th>Status</th> <th class="px-4 py-2">Status</th>
<th>View</th> <th class="px-4 py-2">View</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{userJobs.map((job) => ( {userJobs.map((job) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<tr> <tr>
<td safe>{job.date_created}</td> <td safe>{job.date_created}</td>
<td>{job.num_files}</td> <td>{job.num_files}</td>
<td>{job.finished_files}</td> <td>{job.finished_files}</td>
<td safe>{job.status}</td> <td safe>{job.status}</td>
<td> <td>
<a href={`/results/${job.id}`}>View</a> <a
class={`
text-accent-500 underline
hover:text-accent-400
`}
href={`/results/${job.id}`}
>
View
</a>
</td> </td>
</tr> </tr>
))} ))}
@@ -793,7 +995,7 @@ const app = new Elysia({
return redirect("/login", 302); return redirect("/login", 302);
} }
const job = await db const job = db
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?") .query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
.as(Jobs) .as(Jobs)
.get(user.id, params.jobId); .get(user.id, params.jobId);
@@ -816,50 +1018,77 @@ const app = new Elysia({
<BaseHtml title="ConvertX | Result"> <BaseHtml title="ConvertX | Result">
<> <>
<Header loggedIn /> <Header loggedIn />
<main class="container"> <main class="w-full px-4">
<article> <article class="article">
<div class="grid"> <div class="mb-4 flex items-center justify-between">
<h1>Results</h1> <h1 class="text-xl">Results</h1>
<div> <div>
<button <button
type="button" type="button"
style={{ width: "10rem", float: "right" }} class="btn-primary float-right w-40"
onclick="downloadAll()" onclick="downloadAll()"
{...(files.length !== job.num_files {...(files.length !== job.num_files
? { disabled: true, "aria-busy": "true" } ? { disabled: true, "aria-busy": "true" }
: "")}> : "")}
>
{files.length === job.num_files {files.length === job.num_files
? "Download All" ? "Download All"
: "Converting..."} : "Converting..."}
</button> </button>
</div> </div>
</div> </div>
<progress max={job.num_files} value={files.length} /> <progress
<table> max={job.num_files}
value={files.length}
class={`
mb-4 inline-block h-2 w-full appearance-none overflow-hidden rounded-full
border-0 bg-neutral-700 bg-none text-accent-500 accent-accent-500
[&::-moz-progress-bar]:bg-neutral-700 [&::-webkit-progress-value]:rounded-full
[&::-webkit-progress-value]:[background:none]
[&[value]::-webkit-progress-value]:bg-accent-500
[&[value]::-webkit-progress-value]:transition-[inline-size]
`}
/>
<table
class={`
w-full table-auto rounded bg-neutral-900 text-left
[&_td]:p-4
[&_tr]:rounded [&_tr]:border-b [&_tr]:border-neutral-800
`}
>
<thead> <thead>
<tr> <tr>
<th>Converted File Name</th> <th class="px-4 py-2">Converted File Name</th>
<th>Status</th> <th class="px-4 py-2">Status</th>
<th>View</th> <th class="px-4 py-2">View</th>
<th>Download</th> <th class="px-4 py-2">Download</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{files.map((file) => ( {files.map((file) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<tr> <tr>
<td safe>{file.output_file_name}</td> <td safe>{file.output_file_name}</td>
<td safe>{file.status}</td> <td safe>{file.status}</td>
<td> <td>
<a <a
href={`/download/${outputPath}${file.output_file_name}`}> class={`
text-accent-500 underline
hover:text-accent-400
`}
href={`/download/${outputPath}${file.output_file_name}`}
>
View View
</a> </a>
</td> </td>
<td> <td>
<a <a
class={`
text-accent-500 underline
hover:text-accent-400
`}
href={`/download/${outputPath}${file.output_file_name}`} href={`/download/${outputPath}${file.output_file_name}`}
download={file.output_file_name}> download={file.output_file_name}
>
Download Download
</a> </a>
</td> </td>
@@ -892,7 +1121,7 @@ const app = new Elysia({
return redirect("/login", 302); return redirect("/login", 302);
} }
const job = await db const job = db
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?") .query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
.as(Jobs) .as(Jobs)
.get(user.id, params.jobId); .get(user.id, params.jobId);
@@ -912,48 +1141,76 @@ const app = new Elysia({
.all(params.jobId); .all(params.jobId);
return ( return (
<article> <article class="article">
<div class="grid"> <div class="mb-4 flex items-center justify-between">
<h1>Results</h1> <h1 class="text-xl">Results</h1>
<div> <div>
<button <button
type="button" type="button"
style={{ width: "10rem", float: "right" }} class="btn-primary float-right w-40"
onclick="downloadAll()" onclick="downloadAll()"
{...(files.length !== job.num_files {...(files.length !== job.num_files
? { disabled: true, "aria-busy": "true" } ? { disabled: true, "aria-busy": "true" }
: "")}> : "")}
>
{files.length === job.num_files {files.length === job.num_files
? "Download All" ? "Download All"
: "Converting..."} : "Converting..."}
</button> </button>
</div> </div>
</div> </div>
<progress max={job.num_files} value={files.length} /> <progress
<table> max={job.num_files}
value={files.length}
class={`
mb-4 inline-block h-2 w-full appearance-none overflow-hidden rounded-full border-0
bg-neutral-700 bg-none text-accent-500 accent-accent-500
[&::-moz-progress-bar]:bg-neutral-700 [&::-webkit-progress-value]:rounded-full
[&::-webkit-progress-value]:[background:none]
[&[value]::-webkit-progress-value]:bg-accent-500
[&[value]::-webkit-progress-value]:transition-[inline-size]
`}
/>
<table
class={`
w-full table-auto rounded bg-neutral-900 text-left
[&_td]:p-4
[&_tr]:rounded [&_tr]:border-b [&_tr]:border-neutral-800
`}
>
<thead> <thead>
<tr> <tr>
<th>Converted File Name</th> <th class="px-4 py-2">Converted File Name</th>
<th>Status</th> <th class="px-4 py-2">Status</th>
<th>View</th> <th class="px-4 py-2">View</th>
<th>Download</th> <th class="px-4 py-2">Download</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{files.map((file) => ( {files.map((file) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<tr> <tr>
<td safe>{file.output_file_name}</td> <td safe>{file.output_file_name}</td>
<td safe>{file.status}</td> <td safe>{file.status}</td>
<td> <td>
<a href={`/download/${outputPath}${file.output_file_name}`}> <a
class={`
text-accent-500 underline
hover:text-accent-400
`}
href={`/download/${outputPath}${file.output_file_name}`}
>
View View
</a> </a>
</td> </td>
<td> <td>
<a <a
class={`
text-accent-500 underline
hover:text-accent-400
`}
href={`/download/${outputPath}${file.output_file_name}`} href={`/download/${outputPath}${file.output_file_name}`}
download={file.output_file_name}> download={file.output_file_name}
>
Download Download
</a> </a>
</td> </td>
@@ -1007,15 +1264,22 @@ const app = new Elysia({
<BaseHtml title="ConvertX | Converters"> <BaseHtml title="ConvertX | Converters">
<> <>
<Header loggedIn /> <Header loggedIn />
<main class="container"> <main class="w-full px-4">
<article> <article class="article">
<h1>Converters</h1> <h1 class="mb-4 text-xl">Converters</h1>
<table> <table
class={`
w-full table-auto rounded bg-neutral-900 text-left
[&_td]:p-4
[&_tr]:rounded [&_tr]:border-b [&_tr]:border-neutral-800
[&_ul]:list-inside [&_ul]:list-disc
`}
>
<thead> <thead>
<tr> <tr>
<th>Converter</th> <th class="mx-4 my-2">Converter</th>
<th>From (Count)</th> <th class="mx-4 my-2">From (Count)</th>
<th>To (Count)</th> <th class="mx-4 my-2">To (Count)</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -1023,14 +1287,12 @@ const app = new Elysia({
([converter, targets]) => { ([converter, targets]) => {
const inputs = getAllInputs(converter); const inputs = getAllInputs(converter);
return ( return (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<tr> <tr>
<td safe>{converter}</td> <td safe>{converter}</td>
<td> <td>
Count: {inputs.length} Count: {inputs.length}
<ul> <ul>
{inputs.map((input) => ( {inputs.map((input) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<li safe>{input}</li> <li safe>{input}</li>
))} ))}
</ul> </ul>
@@ -1039,7 +1301,6 @@ const app = new Elysia({
Count: {targets.length} Count: {targets.length}
<ul> <ul>
{targets.map((target) => ( {targets.map((target) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<li safe>{target}</li> <li safe>{target}</li>
))} ))}
</ul> </ul>
@@ -1077,30 +1338,44 @@ const app = new Elysia({
return redirect("/results", 302); return redirect("/results", 302);
} }
const userId = decodeURIComponent(params.userId); // const userId = decodeURIComponent(params.userId);
const jobId = decodeURIComponent(params.jobId); // const jobId = decodeURIComponent(params.jobId);
const outputPath = `${outputDir}${userId}/${jobId}/`; // const outputPath = `${outputDir}${userId}/${jobId}/`;
// return Bun.zip(outputPath); // return Bun.zip(outputPath);
}, },
) )
.onError(({ code, error, request }) => { .onError(({ error }) => {
// log.error(` ${request.method} ${request.url}`, code, error); // log.error(` ${request.method} ${request.url}`, code, error);
console.error(error); console.error(error);
}) });
.listen(3000);
if (process.env.NODE_ENV !== "production") {
await import("./helpers/tailwind").then(async ({ generateTailwind }) => {
const result = await generateTailwind();
app.get("/generated.css", ({ set }) => {
set.headers["content-type"] = "text/css";
return result;
});
});
}
app.listen(3000);
console.log( console.log(
`🦊 Elysia is running at http://${app.server?.hostname}:${app.server?.port}`, `🦊 Elysia is running at http://${app.server?.hostname}:${app.server?.port}`,
); );
const clearJobs = () => { const clearJobs = () => {
// clear all jobs older than 24 hours
// get all files older than 24 hours
const jobs = db const jobs = db
.query("SELECT * FROM jobs WHERE date_created < ?") .query("SELECT * FROM jobs WHERE date_created < ?")
.as(Jobs) .as(Jobs)
.all(new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString()); .all(
new Date(
Date.now() - AUTO_DELETE_EVERY_N_HOURS * 60 * 60 * 1000,
).toISOString(),
);
for (const job of jobs) { for (const job of jobs) {
// delete the directories // delete the directories
@@ -1111,7 +1386,9 @@ const clearJobs = () => {
db.query("DELETE FROM jobs WHERE id = ?").run(job.id); db.query("DELETE FROM jobs WHERE id = ?").run(job.id);
} }
// run every 24 hours setTimeout(clearJobs, AUTO_DELETE_EVERY_N_HOURS * 60 * 60 * 1000);
setTimeout(clearJobs, 24 * 60 * 60 * 1000);
}; };
if (AUTO_DELETE_EVERY_N_HOURS > 0) {
clearJobs(); clearJobs();
}

45
src/main.css Normal file
View File

@@ -0,0 +1,45 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
.article {
@apply p-4 mb-4 bg-neutral-800/40 w-full mx-auto max-w-4xl rounded;
}
.btn-primary {
@apply bg-accent-500 text-contrast rounded p-4 hover:bg-accent-400 cursor-pointer transition-colors;
}
}
:root {
--contrast: 255, 255, 255;
--neutral-900: 243, 244, 246;
--neutral-800: 229, 231, 235;
--neutral-700: 209, 213, 219;
--neutral-600: 156, 163, 175;
--neutral-500: 180, 180, 180;
--neutral-400: 75, 85, 99;
--neutral-300: 55, 65, 81;
--neutral-200: 31, 41, 55;
--neutral-100: 17, 24, 39;
--accent-400: 132, 204, 22;
--accent-500: 101, 163, 13;
--accent-600: 77, 124, 15;
}
@media (prefers-color-scheme: dark) {
:root {
--contrast: 0, 0, 0;
--neutral-900: 17, 24, 39;
--neutral-800: 31, 41, 55;
--neutral-700: 55, 65, 81;
--neutral-600: 75, 85, 99;
--neutral-500: 107, 114, 128;
--neutral-300: 209, 213, 219;
--neutral-400: 156, 163, 175;
--neutral-200: 229, 231, 235;
--accent-600: 101, 163, 13;
--accent-500: 132, 204, 22;
--accent-400: 163, 230, 53;
}
}

File diff suppressed because one or more lines are too long

2
src/public/robots.txt Normal file
View File

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

View File

@@ -1,9 +1,98 @@
// Select the file input element // Select the file input element
const fileInput = document.querySelector('input[type="file"]'); const fileInput = document.querySelector('input[type="file"]');
const dropZone = document.getElementById("dropzone");
const fileNames = []; const fileNames = [];
let fileType; let fileType;
const selectContainer = document.querySelector("form > article"); dropZone.addEventListener("dragover", () => {
dropZone.classList.add("dragover");
});
dropZone.addEventListener("dragleave", () => {
dropZone.classList.remove("dragover");
});
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 convertButton = document.querySelector("input[type='submit']");
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}`;
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;
});
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");
});
};
// const convertFromSelect = document.querySelector("select[name='convert_from']"); // const convertFromSelect = document.querySelector("select[name='convert_from']");
@@ -28,6 +117,7 @@ fileInput.addEventListener("change", (e) => {
if (!fileType) { if (!fileType) {
fileType = file.name.split(".").pop(); fileType = file.name.split(".").pop();
console.log("fileType", fileType);
fileInput.setAttribute("accept", `.${fileType}`); fileInput.setAttribute("accept", `.${fileType}`);
setTitle(); setTitle();
@@ -49,6 +139,7 @@ fileInput.addEventListener("change", (e) => {
.then((res) => res.text()) .then((res) => res.text())
.then((html) => { .then((html) => {
selectContainer.innerHTML = html; selectContainer.innerHTML = html;
updateSearchBar();
}) })
.catch((err) => console.log(err)); .catch((err) => console.log(err));
} }
@@ -69,6 +160,7 @@ const setTitle = () => {
}; };
// Add a onclick for the delete button // Add a onclick for the delete button
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const deleteRow = (target) => { const deleteRow = (target) => {
const filename = target.parentElement.parentElement.children[0].textContent; const filename = target.parentElement.parentElement.children[0].textContent;
const row = target.parentElement.parentElement; const row = target.parentElement.parentElement;
@@ -119,7 +211,9 @@ const uploadFiles = (files) => {
const formConvert = document.querySelector("form[action='/convert']"); const formConvert = document.querySelector("form[action='/convert']");
formConvert.addEventListener("submit", (e) => { formConvert.addEventListener("submit", () => {
const hiddenInput = document.querySelector("input[name='file_names']"); const hiddenInput = document.querySelector("input[name='file_names']");
hiddenInput.value = JSON.stringify(fileNames); hiddenInput.value = JSON.stringify(fileNames);
}); });
updateSearchBar();

View File

@@ -1,15 +0,0 @@
div.icon {
height: 100px;
width: 100px;
}
button[type="submit"] {
width: 50%
}
div.center {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}

26
tailwind.config.js Normal file
View File

@@ -0,0 +1,26 @@
/* eslint-disable @typescript-eslint/no-require-imports */
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{html,js,tsx,jsx,cjs,mjs}"],
theme: {
extend: {
colors: {
contrast: "rgba(var(--contrast))",
"neutral-900": "rgba(var(--neutral-900))",
"neutral-800": "rgba(var(--neutral-800))",
"neutral-700": "rgba(var(--neutral-700))",
"neutral-600": "rgba(var(--neutral-600))",
"neutral-500": "rgba(var(--neutral-500))",
"neutral-400": "rgba(var(--neutral-400))",
"neutral-300": "rgba(var(--neutral-300))",
"neutral-200": "rgba(var(--neutral-200))",
"neutral-100": "rgba(var(--neutral-100))",
"accent-600": "rgba(var(--accent-600))",
"accent-500": "rgba(var(--accent-500))",
"accent-400": "rgba(var(--accent-400))",
},
},
},
plugins: [require("tailwind-scrollbar")],
};

View File

@@ -1,8 +1,8 @@
{ {
"compilerOptions": { "compilerOptions": {
"lib": ["ESNext"], "lib": ["ESNext"],
"module": "esnext", "module": "ESNext",
"target": "esnext", "target": "ES2021",
"moduleResolution": "bundler", "moduleResolution": "bundler",
"moduleDetection": "force", "moduleDetection": "force",
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,