Compare commits
	
		
			83 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 961a55cbe5 | ||
|  | cdf9bad903 | ||
|  | 6769fa2f83 | ||
|  | 3b7ea88b73 | ||
|  | 59310c095d | ||
|  | c47f0c12fe | ||
|  | ca71a40485 | ||
|  | d4e8f376c1 | ||
|  | 14c6ea1e6b | ||
|  | d6e4d8fbd6 | ||
|  | 460bda62d5 | ||
|  | d2702ab673 | ||
|  | e2581f42f5 | ||
|  | f0fcfc159f | ||
|  | 538c5b60c9 | ||
|  | 2fabb7bbb2 | ||
|  | e7c34a9c94 | ||
|  | 618f9fce5a | ||
|  | 95dbc9f678 | ||
|  | aa87bc5c51 | ||
|  | 815de531ed | ||
|  | cf2b026dc4 | ||
|  | 9ce46aefba | ||
|  | 98b2db7818 | ||
|  | 0229851bf9 | ||
|  | 9e15114fe8 | ||
|  | 7f66a76bb0 | ||
|  | e9cc8392bb | ||
|  | d0b89ce74f | ||
|  | f537c81db7 | ||
|  | 03d3edfff6 | ||
|  | 447b4c5e5c | ||
|  | cb143209ae | ||
|  | 9c24fe73b5 | ||
|  | 19ae85424b | ||
|  | 22fad99552 | ||
|  | 8144bbef74 | ||
|  | aad6da0ae8 | ||
|  | c5f8162a22 | ||
|  | f0f30224b5 | ||
|  | d0d888e356 | ||
|  | 2c64122224 | ||
|  | 3b2eee96a9 | ||
|  | 465aacbf9b | ||
|  | d1a2a66170 | ||
|  | 4c05fd72bb | ||
|  | f04fe760e3 | ||
|  | 834d19bcc6 | ||
|  | 6808c4642c | ||
|  | d0ce307f94 | ||
|  | f3740e9ded | ||
|  | b485bc9445 | ||
|  | 2d14c1bb26 | ||
|  | 1a442d6e69 | ||
|  | 2386543e5c | ||
|  | 58e220e82d | ||
|  | 24bea6e4d2 | ||
|  | 43497ad8d1 | ||
|  | f22b61fe4c | ||
|  | 5b08f4cd19 | ||
|  | 1589f8d24e | ||
|  | 7d1db72cf5 | ||
|  | 53a8f66414 | ||
|  | 36cb6cc589 | ||
|  | f3a4aece46 | ||
|  | 580a6a869a | ||
|  | 008eaac493 | ||
|  | b450623bb4 | ||
|  | 8ac2ecb673 | ||
|  | 0a10a56ae3 | ||
|  | 9378ba9208 | ||
|  | 0c586e324b | ||
|  | 91c4a64284 | ||
|  | c599e98d9d | ||
|  | d2cd6706c9 | ||
|  | e8ed10dde8 | ||
|  | 5fe0b79802 | ||
|  | 34a6722a68 | ||
|  | 5b0d769c63 | ||
|  | 718401a28b | ||
|  | 3112cd57f6 | ||
|  | 410fc777a7 | ||
|  | 8eed99e732 | 
| @@ -1,16 +1,20 @@ | ||||
| node_modules | ||||
| Dockerfile* | ||||
| docker-compose* | ||||
| .dockerignore | ||||
| .editorconfig | ||||
| .env | ||||
| .git | ||||
| .gitignore | ||||
| README.md | ||||
| LICENSE | ||||
| .vscode | ||||
| Makefile | ||||
| helm-charts | ||||
| .env | ||||
| .editorconfig | ||||
| .idea | ||||
| .vscode | ||||
| CHANGELOG.md | ||||
| coverage* | ||||
| data | ||||
| docker-compose* | ||||
| Dockerfile* | ||||
| eslint.config.js | ||||
| helm-charts | ||||
| LICENSE | ||||
| Makefile | ||||
| node_modules | ||||
| prettier.config.js | ||||
| README.md | ||||
| renovate.json | ||||
							
								
								
									
										13
									
								
								.github/workflows/docker-publish.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -19,6 +19,7 @@ env: | ||||
|   REGISTRY: ghcr.io | ||||
|   # github.repository as <account>/<repo> | ||||
|   IMAGE_NAME: ${{ github.repository }} | ||||
|   DOCKERHUB_USERNAME: c4illin | ||||
|  | ||||
|  | ||||
| jobs: | ||||
| @@ -46,13 +47,22 @@ jobs: | ||||
|           username: ${{ github.actor }} | ||||
|           password: ${{ secrets.GITHUB_TOKEN }} | ||||
|        | ||||
|       - name: Login to Docker Hub | ||||
|         if: github.event_name != 'pull_request' | ||||
|         uses: docker/login-action@v3 | ||||
|         with: | ||||
|           username: ${{ env.DOCKERHUB_USERNAME }} | ||||
|           password: ${{ secrets.DOCKERHUB_TOKEN }} | ||||
|  | ||||
|       # Extract metadata (tags, labels) for Docker | ||||
|       # https://github.com/docker/metadata-action | ||||
|       - name: Extract Docker metadata | ||||
|         id: meta | ||||
|         uses: docker/metadata-action@v5 | ||||
|         with: | ||||
|           images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | ||||
|           images: | | ||||
|             ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | ||||
|             ${{ env.IMAGE_NAME }} | ||||
|  | ||||
|       # Build and push Docker image with Buildx (don't push on PR) | ||||
|       # https://github.com/docker/build-push-action | ||||
| @@ -67,3 +77,4 @@ jobs: | ||||
|           labels: ${{ steps.meta.outputs.labels }} | ||||
|           cache-from: type=gha | ||||
|           cache-to: type=gha,mode=max | ||||
|        | ||||
							
								
								
									
										27
									
								
								.github/workflows/dockerhub-description.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,27 @@ | ||||
| name: Update Docker Hub Description | ||||
|  | ||||
| env: | ||||
|   IMAGE_NAME: ${{ github.repository }} | ||||
|   DOCKERHUB_USERNAME: c4illin | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: | ||||
|       - main | ||||
|     paths: | ||||
|       - README.md | ||||
|       - .github/workflows/dockerhub-description.yml | ||||
| jobs: | ||||
|   dockerHubDescription: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|     - uses: actions/checkout@v4 | ||||
|  | ||||
|     - name: Docker Hub Description | ||||
|       uses: peter-evans/dockerhub-description@v4 | ||||
|       with: | ||||
|         username: ${{ env.DOCKERHUB_USERNAME }} | ||||
|         password: ${{ secrets.DOCKERHUB_TOKEN }} | ||||
|         repository: ${{ env.IMAGE_NAME }} | ||||
|         short-description: ${{ github.event.repository.description }} | ||||
|         enable-url-completion: true | ||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -48,4 +48,4 @@ package-lock.json | ||||
| /data | ||||
| /Bruno | ||||
| /tsconfig.tsbuildinfo | ||||
| /src/public/generated.css | ||||
| /public/generated.css | ||||
|   | ||||
							
								
								
									
										36
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						| @@ -1,5 +1,41 @@ | ||||
| # Changelog | ||||
|  | ||||
| ## [0.10.1](https://github.com/C4illin/ConvertX/compare/v0.10.0...v0.10.1) (2025-01-21) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * ffmpeg works without ffmpeg_args ([3b7ea88](https://github.com/C4illin/ConvertX/commit/3b7ea88b7382f7c21b120bdc9bda5bb10547f55d)), closes [#212](https://github.com/C4illin/ConvertX/issues/212) | ||||
|  | ||||
| ## [0.10.0](https://github.com/C4illin/ConvertX/compare/v0.9.0...v0.10.0) (2025-01-18) | ||||
|  | ||||
|  | ||||
| ### Features | ||||
|  | ||||
| * add calibre ([03d3edf](https://github.com/C4illin/ConvertX/commit/03d3edfff65c252dd4b8922fc98257c089c1ff74)), closes [#191](https://github.com/C4illin/ConvertX/issues/191) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * add FFMPEG_ARGS env variable ([f537c81](https://github.com/C4illin/ConvertX/commit/f537c81db7815df8017f834e3162291197e1c40f)), closes [#190](https://github.com/C4illin/ConvertX/issues/190) | ||||
| * add qt6-qtbase-private-dev from community repo ([95dbc9f](https://github.com/C4illin/ConvertX/commit/95dbc9f678bec7e6e2c03587e1473fb8ff708ea3)) | ||||
| * skip account setup when ALLOW_UNAUTHENTICATED is true ([538c5b6](https://github.com/C4illin/ConvertX/commit/538c5b60c9e27a8184740305475245da79bae143)) | ||||
|  | ||||
| ## [0.9.0](https://github.com/C4illin/ConvertX/compare/v0.8.1...v0.9.0) (2024-11-21) | ||||
|  | ||||
|  | ||||
| ### Features | ||||
|  | ||||
| * add inkscape for vector images ([f3740e9](https://github.com/C4illin/ConvertX/commit/f3740e9ded100b8500f3613517960248bbd3c210)) | ||||
| * Allow to chose webroot ([36cb6cc](https://github.com/C4illin/ConvertX/commit/36cb6cc589d80d0a87fa8dbe605db71a9a2570f9)), closes [#180](https://github.com/C4illin/ConvertX/issues/180) | ||||
| * disable convert when uploading ([58e220e](https://github.com/C4illin/ConvertX/commit/58e220e82d7f9c163d6ea4dc31092c08a3e254f4)), closes [#177](https://github.com/C4illin/ConvertX/issues/177) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * treat unknown as m4a ([1a442d6](https://github.com/C4illin/ConvertX/commit/1a442d6e69606afef63b1e7df36aa83d111fa23d)), closes [#178](https://github.com/C4illin/ConvertX/issues/178) | ||||
| * wait for both upload and selection ([4c05fd7](https://github.com/C4illin/ConvertX/commit/4c05fd72bbbf91ee02327f6fcbf749b78272376b)), closes [#177](https://github.com/C4illin/ConvertX/issues/177) | ||||
|  | ||||
| ## [0.8.1](https://github.com/C4illin/ConvertX/compare/v0.8.0...v0.8.1) (2024-10-05) | ||||
|  | ||||
|  | ||||
|   | ||||
							
								
								
									
										12
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						| @@ -1,4 +1,4 @@ | ||||
| FROM oven/bun:1.1.29-alpine AS base | ||||
| FROM oven/bun:1.1.45-alpine AS base | ||||
| LABEL org.opencontainers.image.source="https://github.com/C4illin/ConvertX" | ||||
| WORKDIR /app | ||||
|  | ||||
| @@ -50,14 +50,20 @@ RUN apk --no-cache add  \ | ||||
|   vips-poppler \ | ||||
|   vips-jxl \ | ||||
|   libjxl-tools \ | ||||
|   assimp | ||||
|   assimp \ | ||||
|   inkscape \ | ||||
|   poppler-utils | ||||
|  | ||||
| RUN apk --no-cache add qt6-qtbase-private-dev --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community/ | ||||
|  | ||||
| RUN apk --no-cache add calibre --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing/ | ||||
|  | ||||
| # this might be needed for some latex use cases, will add it if needed. | ||||
| #   texmf-dist-fontsextra \ | ||||
|  | ||||
| COPY --from=install /temp/prod/node_modules node_modules | ||||
| COPY --from=builder /root/.cargo/bin/resvg /usr/local/bin/resvg | ||||
| COPY --from=prerelease /app/src/public/generated.css /app/src/public/ | ||||
| COPY --from=prerelease /app/public/generated.css /app/public/ | ||||
| # COPY --from=prerelease /app/src/index.tsx /app/src/ | ||||
| # COPY --from=prerelease /app/package.json . | ||||
| COPY . . | ||||
|   | ||||
							
								
								
									
										63
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @@ -1,11 +1,15 @@ | ||||
|  | ||||
|  | ||||
| # ConvertX | ||||
|  | ||||
| [](https://github.com/C4illin/ConvertX/actions/workflows/docker-publish.yml) | ||||
| [](https://github.com/C4illin/ConvertX/pkgs/container/ConvertX) | ||||
| [](https://hub.docker.com/r/c4illin/convertx) | ||||
| [](https://github.com/C4illin/ConvertX/pkgs/container/convertx) | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| <!--  --> | ||||
|  | ||||
| A self-hosted online file converter. Supports over a thousand different formats. Written with TypeScript, Bun and Elysia. | ||||
|  | ||||
| @@ -23,11 +27,13 @@ A self-hosted online file converter. Supports over a thousand different formats. | ||||
| | [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          | | ||||
| | [Assimp](https://github.com/assimp/assimp)                                   | 3D Assets     | 70            | 24          | | ||||
| | [XeLaTeX](https://tug.org/xetex/)                                            | LaTeX         | 1             | 1           | | ||||
| | [Calibre](https://calibre-ebook.com/)                                        | E-books       | 26            | 19          | | ||||
| | [Pandoc](https://pandoc.org/)                                                | Documents     | 43            | 65          | | ||||
| | [GraphicsMagick](http://www.graphicsmagick.org/)                             | Images        | 166           | 133         | | ||||
| | [FFmpeg](https://ffmpeg.org/)                                                | Video         | ~473          | ~280        | | ||||
| | [GraphicsMagick](http://www.graphicsmagick.org/)                             | Images        | 167           | 130         | | ||||
| | [Inkscape](https://inkscape.org/)                                            | Vector images | 7             | 17          | | ||||
| | [Assimp](https://github.com/assimp/assimp)                                   | 3D Assets     | 77            | 23          | | ||||
| | [FFmpeg](https://ffmpeg.org/)                                                | Video         | ~472          | ~199        | | ||||
|  | ||||
| <!-- many ffmpeg fileformats are duplicates --> | ||||
|  | ||||
| @@ -44,14 +50,10 @@ services: | ||||
|     restart: unless-stopped | ||||
|     ports: | ||||
|       - "3000:3000" | ||||
|     environment: # Defaults are listed below. All are optional. | ||||
|       - ACCOUNT_REGISTRATION=false # true or false, doesn't matter for the first account (e.g. keep this to false if you only want one account) | ||||
|       - JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234 # will use randomUUID() by default | ||||
|       - HTTP_ALLOWED=false # setting this to true is unsafe, only set this to true locally | ||||
|       - ALLOW_UNAUTHENTICATED=false # allows anyone to use the service without logging in, only set this to true locally | ||||
|       - AUTO_DELETE_EVERY_N_HOURS=24 # checks every n hours for files older then n hours and deletes them, set to 0 to disable | ||||
|     environment: | ||||
|       - JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234 # will use randomUUID() if unset | ||||
|     volumes: | ||||
|       - convertx:/app/data | ||||
|       - ./data:/app/data | ||||
| ``` | ||||
|  | ||||
| or | ||||
| @@ -64,9 +66,43 @@ Then visit `http://localhost:3000` in your browser and create your account. Don' | ||||
|  | ||||
| If you get unable to open database file run `chown -R $USER:$USER path` on the path you choose. | ||||
|  | ||||
| ### Environment variables | ||||
|  | ||||
| All are optional, JWT_SECRET is recommended to be set. | ||||
|  | ||||
| | Name                      | Default | Description | | ||||
| |---------------------------|---------|-------------| | ||||
| | JWT_SECRET                | when unset it will use the value from randomUUID() | A long and secret string used to sign the JSON Web Token | | ||||
| | ACCOUNT_REGISTRATION      | false | Allow users to register accounts | | ||||
| | HTTP_ALLOWED              | false | Allow HTTP connections, only set this to true locally | | ||||
| | ALLOW_UNAUTHENTICATED     | false | Allow unauthenticated users to use the service, only set this to true locally | | ||||
| | AUTO_DELETE_EVERY_N_HOURS | 24 | Checks every n hours for files older then n hours and deletes them, set to 0 to disable | | ||||
| | WEBROOT                   |  | The address to the root path setting this to "/convert" will serve the website on "example.com/convert/" | | ||||
| | FFMPEG_ARGS               |  | Arguments to pass to ffmpeg, e.g. `-preset veryfast` | | ||||
|  | ||||
| > [!WARNING] | ||||
| > If you can't login, make sure you are accessing the service over https or set HTTP_ALLOWED=true | ||||
|  | ||||
| ### Docker images | ||||
|  | ||||
| There is a `:latest` tag that is updated with every release and a `:main` tag that is updated with every push to the main branch. `:latest` is recommended for normal use. | ||||
|  | ||||
| The image is available on [GitHub Container Registry](https://github.com/C4illin/ConvertX/pkgs/container/ConvertX) and [Docker Hub](https://hub.docker.com/r/c4illin/convertx). | ||||
|  | ||||
| | Image | What it is | | ||||
| |-------|------------| | ||||
| | `image: ghcr.io/c4illin/convertx` | The latest release on ghcr | | ||||
| | `image: ghcr.io/c4illin/convertx:main` | The latest commit on ghcr | | ||||
| | `image: c4illin/convertx` | The latest release on docker hub | | ||||
| | `image: c4illin/convertx:main` | The latest commit on docker hub | | ||||
|  | ||||
| <!-- Dockerhub was introduced in 0.9.0 and older releases --> | ||||
|  | ||||
| ### Tutorial | ||||
|  | ||||
| Tutorial in french: https://belginux.com/installer-convertx-avec-docker/ | ||||
| Tutorial in french: <https://belginux.com/installer-convertx-avec-docker/> | ||||
|  | ||||
| Tutorial in chinese: <https://xzllll.com/24092901/> | ||||
|  | ||||
| ## Screenshots | ||||
|  | ||||
| @@ -82,6 +118,7 @@ Tutorial in french: https://belginux.com/installer-convertx-avec-docker/ | ||||
| Pull requests are welcome! See below and open issues for the list of todos. | ||||
|  | ||||
| ## Todo | ||||
|  | ||||
| - [x] Add messages for errors in converters | ||||
| - [x] Add searchable list of formats | ||||
| - [ ] Add options for converters | ||||
| @@ -97,7 +134,7 @@ Pull requests are welcome! See below and open issues for the list of todos. | ||||
| ## Contributors | ||||
|  | ||||
| <a href="https://github.com/C4illin/ConvertX/graphs/contributors"> | ||||
|   <img src="https://contrib.rocks/image?repo=C4illin/ConvertX" /> | ||||
|   <img src="https://contrib.rocks/image?repo=C4illin/ConvertX" alt="Image with all contributors"/> | ||||
| </a> | ||||
|  | ||||
| ## Star History | ||||
|   | ||||
| @@ -11,5 +11,7 @@ services: | ||||
|       - 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 | ||||
|       # - FFMPEG_ARGS=-hwaccel vulkan # additional arguments to pass to ffmpeg | ||||
|       # - WEBROOT=/convertx # the root path of the web interface, leave empty to disable | ||||
|     ports: | ||||
|       - 3000:3000 | ||||
|   | ||||
							
								
								
									
										54
									
								
								package.json
									
									
									
									
									
								
							
							
						
						| @@ -1,11 +1,11 @@ | ||||
| { | ||||
|   "name": "convertx-frontend", | ||||
|   "version": "0.8.1", | ||||
|   "version": "0.10.1", | ||||
|   "scripts": { | ||||
|     "dev": "bun run --watch src/index.tsx", | ||||
|     "hot": "bun run --hot src/index.tsx", | ||||
|     "format": "eslint --fix .", | ||||
|     "build": "postcss ./src/main.css -o ./src/public/generated.css", | ||||
|     "build": "postcss ./src/main.css -o ./public/generated.css", | ||||
|     "lint": "run-p 'lint:*'", | ||||
|     "lint:tsc": "tsc --noEmit", | ||||
|     "lint:knip": "knip", | ||||
| @@ -13,10 +13,11 @@ | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@elysiajs/cookie": "^0.8.0", | ||||
|     "@elysiajs/html": "1.0.2", | ||||
|     "@elysiajs/jwt": "^1.1.1", | ||||
|     "@elysiajs/static": "1.0.3", | ||||
|     "elysia": "^1.1.17" | ||||
|     "@elysiajs/html": "^1.2.0", | ||||
|     "@elysiajs/jwt": "^1.2.0", | ||||
|     "@elysiajs/static": "^1.2.0", | ||||
|     "@kitajs/html": "^4.2.7", | ||||
|     "elysia": "^1.2.10" | ||||
|   }, | ||||
|   "module": "src/index.tsx", | ||||
|   "type": "module", | ||||
| @@ -24,38 +25,31 @@ | ||||
|     "start": "bun run src/index.tsx" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@eslint/compat": "^1.1.1", | ||||
|     "@eslint/js": "^9.12.0", | ||||
|     "@ianvs/prettier-plugin-sort-imports": "^4.3.1", | ||||
|     "@kitajs/ts-html-plugin": "^4.1.0", | ||||
|     "@eslint/compat": "^1.2.5", | ||||
|     "@eslint/js": "^9.18.0", | ||||
|     "@ianvs/prettier-plugin-sort-imports": "^4.4.1", | ||||
|     "@kitajs/ts-html-plugin": "^4.1.1", | ||||
|     "@total-typescript/ts-reset": "^0.6.1", | ||||
|     "@types/bun": "^1.1.10", | ||||
|     "@types/eslint": "^9.6.1", | ||||
|     "@types/bun": "^1.1.16", | ||||
|     "@types/eslint-plugin-tailwindcss": "^3.17.0", | ||||
|     "@types/eslint__js": "^8.42.3", | ||||
|     "@types/node": "^22.7.4", | ||||
|     "@typescript-eslint/eslint-plugin": "^8.7.0", | ||||
|     "@typescript-eslint/parser": "^8.7.0", | ||||
|     "@types/node": "^22.10.7", | ||||
|     "autoprefixer": "^10.4.20", | ||||
|     "cssnano": "^7.0.6", | ||||
|     "eslint": "^9.12.0", | ||||
|     "eslint-config-prettier": "^9.1.0", | ||||
|     "eslint": "^9.18.0", | ||||
|     "eslint-plugin-deprecation": "^3.0.0", | ||||
|     "eslint-plugin-isaacscript": "^4.0.0", | ||||
|     "eslint-plugin-prettier": "^5.2.1", | ||||
|     "eslint-plugin-readable-tailwind": "^1.8.1", | ||||
|     "eslint-plugin-readable-tailwind": "^1.8.2", | ||||
|     "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", | ||||
|     "eslint-plugin-tailwindcss": "^3.17.5", | ||||
|     "globals": "^15.14.0", | ||||
|     "knip": "^5.42.1", | ||||
|     "npm-run-all2": "^7.0.2", | ||||
|     "postcss": "^8.5.1", | ||||
|     "postcss-cli": "^11.0.0", | ||||
|     "postcss-lightningcss": "^1.0.1", | ||||
|     "prettier": "^3.3.3", | ||||
|     "prettier": "^3.4.2", | ||||
|     "tailwind-scrollbar": "^3.1.0", | ||||
|     "tailwindcss": "^3.4.13", | ||||
|     "typescript": "^5.6.2", | ||||
|     "typescript-eslint": "^8.8.0" | ||||
|     "tailwindcss": "^3.4.17", | ||||
|     "typescript": "^5.7.3", | ||||
|     "typescript-eslint": "^8.20.0" | ||||
|   } | ||||
| } | ||||
| @@ -1,9 +0,0 @@ | ||||
|   | ||||
| module.exports = { | ||||
|   plugins: { | ||||
|     tailwindcss: {}, | ||||
|     autoprefixer: {}, | ||||
|       | ||||
|     ...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {}) | ||||
|   } | ||||
| } | ||||
							
								
								
									
										8
									
								
								postcss.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,8 @@ | ||||
| import autoprefixer from "autoprefixer"; | ||||
| import cssnano from "cssnano"; | ||||
| import tailwind from "tailwindcss"; | ||||
| import tailwindConfig from "./tailwind.config.js"; | ||||
|  | ||||
| export default { | ||||
|   plugins: [autoprefixer, tailwind(tailwindConfig), cssnano], | ||||
| }; | ||||
| Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.1 KiB | 
| Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB | 
| Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB | 
| Before Width: | Height: | Size: 476 B After Width: | Height: | Size: 476 B | 
| Before Width: | Height: | Size: 960 B After Width: | Height: | Size: 960 B | 
| Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB | 
| @@ -1,3 +1,5 @@ | ||||
| const webroot = document.querySelector("meta[name='webroot']").content; | ||||
| 
 | ||||
| window.downloadAll = function () { | ||||
|   // Get all download links
 | ||||
|   const downloadLinks = document.querySelectorAll("a[download]"); | ||||
| @@ -18,7 +20,7 @@ let progressElem = document.querySelector("progress"); | ||||
| const refreshData = () => { | ||||
|   // console.log("Refreshing data...", progressElem.value, progressElem.max);
 | ||||
|   if (progressElem.value !== progressElem.max) { | ||||
|     fetch(`/progress/${jobId}`, { | ||||
|     fetch(`${webroot}/progress/${jobId}`, { | ||||
|       method: "POST", | ||||
|     }) | ||||
|       .then((res) => res.text()) | ||||
| @@ -1,8 +1,11 @@ | ||||
| // Select the file input element
 | ||||
| const webroot = document.querySelector("meta[name='webroot']").content; | ||||
| const fileInput = document.querySelector('input[type="file"]'); | ||||
| const dropZone = document.getElementById("dropzone"); | ||||
| const convertButton = document.querySelector("input[type='submit']"); | ||||
| const fileNames = []; | ||||
| let fileType; | ||||
| let pendingFiles = 0; | ||||
| let formatSelected = false; | ||||
| 
 | ||||
| dropZone.addEventListener("dragover", () => { | ||||
|   dropZone.classList.add("dragover"); | ||||
| @@ -12,6 +15,10 @@ dropZone.addEventListener("dragleave", () => { | ||||
|   dropZone.classList.remove("dragover"); | ||||
| }); | ||||
| 
 | ||||
| dropZone.addEventListener("drop", () => { | ||||
|   dropZone.classList.remove("dragover"); | ||||
| }); | ||||
| 
 | ||||
| const selectContainer = document.querySelector("form .select_container"); | ||||
| 
 | ||||
| const updateSearchBar = () => { | ||||
| @@ -22,7 +29,6 @@ const updateSearchBar = () => { | ||||
|   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)) { | ||||
| @@ -58,7 +64,10 @@ const updateSearchBar = () => { | ||||
|       target.onmousedown = () => { | ||||
|         convertToElement.value = target.dataset.value; | ||||
|         convertToInput.value = `${target.dataset.target} using ${target.dataset.converter}`; | ||||
|         formatSelected = true; | ||||
|         if (pendingFiles === 0 && fileNames.length > 0) { | ||||
|           convertButton.disabled = false; | ||||
|         } | ||||
|         showMatching(""); | ||||
|       }; | ||||
|     } | ||||
| @@ -73,6 +82,7 @@ const updateSearchBar = () => { | ||||
|   convertToInput.addEventListener("search", () => { | ||||
|     // when the user clears the search bar using the 'x' button
 | ||||
|     convertButton.disabled = true; | ||||
|     formatSelected = false; | ||||
|   }); | ||||
| 
 | ||||
|   convertToInput.addEventListener("blur", (e) => { | ||||
| @@ -94,11 +104,8 @@ const updateSearchBar = () => { | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| // const convertFromSelect = document.querySelector("select[name='convert_from']");
 | ||||
| 
 | ||||
| // Add a 'change' event listener to the file input element
 | ||||
| fileInput.addEventListener("change", (e) => { | ||||
|   // console.log(e.target.files);
 | ||||
|   // Get the selected files from the event target
 | ||||
|   const files = e.target.files; | ||||
| 
 | ||||
| @@ -112,12 +119,11 @@ fileInput.addEventListener("change", (e) => { | ||||
|     row.innerHTML = ` | ||||
|       <td>${file.name}</td> | ||||
|       <td>${(file.size / 1024).toFixed(2)} kB</td> | ||||
|       <td><a class="secondary" onclick="deleteRow(this)" style="cursor: pointer">Remove</a></td> | ||||
|       <td><a onclick="deleteRow(this)">Remove</a></td> | ||||
|     `;
 | ||||
| 
 | ||||
|     if (!fileType) { | ||||
|       fileType = file.name.split(".").pop(); | ||||
|       console.log("fileType", fileType); | ||||
|       fileInput.setAttribute("accept", `.${fileType}`); | ||||
|       setTitle(); | ||||
| 
 | ||||
| @@ -129,7 +135,7 @@ fileInput.addEventListener("change", (e) => { | ||||
|       //   }
 | ||||
|       // }
 | ||||
| 
 | ||||
|       fetch("/conversions", { | ||||
|       fetch(`${webroot}/conversions`, { | ||||
|         method: "POST", | ||||
|         body: JSON.stringify({ fileType: fileType }), | ||||
|         headers: { | ||||
| @@ -170,46 +176,57 @@ const deleteRow = (target) => { | ||||
|   const index = fileNames.indexOf(filename); | ||||
|   fileNames.splice(index, 1); | ||||
| 
 | ||||
|   // reset fileInput
 | ||||
|   fileInput.value = ""; | ||||
| 
 | ||||
|   // if fileNames is empty, reset fileType
 | ||||
|   if (fileNames.length === 0) { | ||||
|     fileType = null; | ||||
|     fileInput.removeAttribute("accept"); | ||||
|     convertButton.disabled = true; | ||||
|     setTitle(); | ||||
|   } | ||||
| 
 | ||||
|   fetch("/delete", { | ||||
|   fetch(`${webroot}/delete`, { | ||||
|     method: "POST", | ||||
|     body: JSON.stringify({ filename: filename }), | ||||
|     headers: { | ||||
|       "Content-Type": "application/json", | ||||
|     }, | ||||
|   }) | ||||
|     .then((res) => res.json()) | ||||
|     .then((data) => { | ||||
|       console.log(data); | ||||
|   }) | ||||
|     .catch((err) => console.log(err)); | ||||
| }; | ||||
| 
 | ||||
| const uploadFiles = (files) => { | ||||
|   convertButton.disabled = true; | ||||
|   convertButton.textContent = "Uploading..."; | ||||
|   pendingFiles += 1; | ||||
| 
 | ||||
|   const formData = new FormData(); | ||||
| 
 | ||||
|   for (const file of files) { | ||||
|     formData.append("file", file, file.name); | ||||
|   } | ||||
| 
 | ||||
|   fetch("/upload", { | ||||
|   fetch(`${webroot}/upload`, { | ||||
|     method: "POST", | ||||
|     body: formData, | ||||
|   }) | ||||
|     .then((res) => res.json()) | ||||
|     .then((data) => { | ||||
|       pendingFiles -= 1; | ||||
|       if (pendingFiles === 0) { | ||||
|         if (formatSelected) { | ||||
|           convertButton.disabled = false; | ||||
|         } | ||||
|         convertButton.textContent = "Convert"; | ||||
|       } | ||||
|       console.log(data); | ||||
|     }) | ||||
|     .catch((err) => console.log(err)); | ||||
| }; | ||||
| 
 | ||||
| const formConvert = document.querySelector("form[action='/convert']"); | ||||
| const formConvert = document.querySelector(`form[action='${webroot}/convert']`); | ||||
| 
 | ||||
| formConvert.addEventListener("submit", () => { | ||||
|   const hiddenInput = document.querySelector("input[name='file_names']"); | ||||
| @@ -3,5 +3,9 @@ | ||||
|   "extends": [ | ||||
|     "config:recommended", | ||||
|     ":disableDependencyDashboard" | ||||
|   ] | ||||
|   ], | ||||
|   "lockFileMaintenance": { | ||||
|     "enabled": true, | ||||
|     "automerge": true | ||||
|   } | ||||
| } | ||||
| @@ -1,31 +1,39 @@ | ||||
| import { Html } from "@elysiajs/html"; | ||||
|  | ||||
| export const BaseHtml = ({ | ||||
|   children, | ||||
|   title = "ConvertX", | ||||
| }: { children: JSX.Element; title?: string }) => ( | ||||
|   webroot = "", | ||||
| }: { | ||||
|   children: JSX.Element; | ||||
|   title?: string; | ||||
|   webroot?: string; | ||||
| }) => ( | ||||
|   <html lang="en"> | ||||
|     <head> | ||||
|       <meta charset="UTF-8" /> | ||||
|       <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||
|       <meta name="webroot" content={webroot} /> | ||||
|       <title safe>{title}</title> | ||||
|       <link rel="stylesheet" href="/generated.css" /> | ||||
|       <link rel="stylesheet" href={`${webroot}/generated.css`} /> | ||||
|       <link | ||||
|         rel="apple-touch-icon" | ||||
|         sizes="180x180" | ||||
|         href="/apple-touch-icon.png" | ||||
|         href={`${webroot}/apple-touch-icon.png`} | ||||
|       /> | ||||
|       <link | ||||
|         rel="icon" | ||||
|         type="image/png" | ||||
|         sizes="32x32" | ||||
|         href="/favicon-32x32.png" | ||||
|         href={`${webroot}/favicon-32x32.png`} | ||||
|       /> | ||||
|       <link | ||||
|         rel="icon" | ||||
|         type="image/png" | ||||
|         sizes="16x16" | ||||
|         href="/favicon-16x16.png" | ||||
|         href={`${webroot}/favicon-16x16.png`} | ||||
|       /> | ||||
|       <link rel="manifest" href="/site.webmanifest" /> | ||||
|       <link rel="manifest" href={`${webroot}/site.webmanifest`} /> | ||||
|     </head> | ||||
|     <body class="w-full bg-neutral-900 text-neutral-200">{children}</body> | ||||
|   </html> | ||||
|   | ||||
| @@ -1,7 +1,16 @@ | ||||
| import { Html } from "@kitajs/html"; | ||||
|  | ||||
| export const Header = ({ | ||||
|   loggedIn, | ||||
|   accountRegistration, | ||||
| }: { loggedIn?: boolean; accountRegistration?: boolean }) => { | ||||
|   allowUnauthenticated, | ||||
|   webroot = "", | ||||
| }: { | ||||
|   loggedIn?: boolean; | ||||
|   accountRegistration?: boolean; | ||||
|   allowUnauthenticated?: boolean; | ||||
|   webroot?: string; | ||||
| }) => { | ||||
|   let rightNav: JSX.Element; | ||||
|   if (loggedIn) { | ||||
|     rightNav = ( | ||||
| @@ -12,20 +21,24 @@ export const Header = ({ | ||||
|               text-accent-600 transition-all | ||||
|               hover:text-accent-500 hover:underline | ||||
|             `} | ||||
|             href="/history"> | ||||
|             href={`${webroot}/history`} | ||||
|           > | ||||
|             History | ||||
|           </a> | ||||
|         </li> | ||||
|         {!allowUnauthenticated ? ( | ||||
|           <li> | ||||
|             <a | ||||
|               class={` | ||||
|                 text-accent-600 transition-all | ||||
|                 hover:text-accent-500 hover:underline | ||||
|               `} | ||||
|             href="/logoff"> | ||||
|               href={`${webroot}/logoff`} | ||||
|             > | ||||
|               Logout | ||||
|             </a> | ||||
|           </li> | ||||
|         ) : null} | ||||
|       </ul> | ||||
|     ); | ||||
|   } else { | ||||
| @@ -37,7 +50,8 @@ export const Header = ({ | ||||
|               text-accent-600 transition-all | ||||
|               hover:text-accent-500 hover:underline | ||||
|             `} | ||||
|             href="/login"> | ||||
|             href={`${webroot}/login`} | ||||
|           > | ||||
|             Login | ||||
|           </a> | ||||
|         </li> | ||||
| @@ -48,7 +62,8 @@ export const Header = ({ | ||||
|                 text-accent-600 transition-all | ||||
|                 hover:text-accent-500 hover:underline | ||||
|               `} | ||||
|               href="/register"> | ||||
|               href={`${webroot}/register`} | ||||
|             > | ||||
|               Register | ||||
|             </a> | ||||
|           </li> | ||||
| @@ -63,7 +78,7 @@ export const Header = ({ | ||||
|         <ul> | ||||
|           <li> | ||||
|             <strong> | ||||
|               <a href="/">ConvertX</a> | ||||
|               <a href={`${webroot}/`}>ConvertX</a> | ||||
|             </strong> | ||||
|           </li> | ||||
|         </ul> | ||||
|   | ||||
| @@ -1,9 +1,8 @@ | ||||
| import { exec } from "node:child_process"; | ||||
|  | ||||
| // This could be done dynamically by running `ffmpeg -formats` and parsing the output | ||||
| export const properties = { | ||||
|   from: { | ||||
|     muxer: [ | ||||
|     object: [ | ||||
|       "3d", | ||||
|       "3ds", | ||||
|       "3mf", | ||||
| @@ -11,6 +10,7 @@ export const properties = { | ||||
|       "ac3d", | ||||
|       "acc", | ||||
|       "amf", | ||||
|       "amj", | ||||
|       "ase", | ||||
|       "ask", | ||||
|       "assbin", | ||||
| @@ -26,6 +26,7 @@ export const properties = { | ||||
|       "fbx", | ||||
|       "glb", | ||||
|       "gltf", | ||||
|       "hmb", | ||||
|       "hmp", | ||||
|       "ifc", | ||||
|       "ifczip", | ||||
| @@ -35,6 +36,7 @@ export const properties = { | ||||
|       "lwo", | ||||
|       "lws", | ||||
|       "lxo", | ||||
|       "m3d", | ||||
|       "md2", | ||||
|       "md3", | ||||
|       "md5anim", | ||||
| @@ -81,7 +83,7 @@ export const properties = { | ||||
|     ], | ||||
|   }, | ||||
|   to: { | ||||
|     muxer: [ | ||||
|     object: [ | ||||
|       "3ds", | ||||
|       "3mf", | ||||
|       "assbin", | ||||
| @@ -95,8 +97,7 @@ export const properties = { | ||||
|       "glb2", | ||||
|       "gltf", | ||||
|       "gltf2", | ||||
|       "m3d", | ||||
|       "m3da", | ||||
|       "json", | ||||
|       "obj", | ||||
|       "objnomtl", | ||||
|       "pbrt", | ||||
| @@ -106,7 +107,6 @@ export const properties = { | ||||
|       "stlb", | ||||
|       "stp", | ||||
|       "x", | ||||
|       "x3d", | ||||
|     ], | ||||
|   }, | ||||
| }; | ||||
| @@ -119,8 +119,6 @@ export async function convert( | ||||
|   // 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) => { | ||||
|   | ||||
							
								
								
									
										86
									
								
								src/converters/calibre.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,86 @@ | ||||
| import { exec } from "node:child_process"; | ||||
|  | ||||
| export const properties = { | ||||
|   from: { | ||||
|     document: [ | ||||
|       "azw4", | ||||
|       "chm", | ||||
|       "cbr", | ||||
|       "cbz", | ||||
|       "cbt", | ||||
|       "cba", | ||||
|       "cb7", | ||||
|       "djvu", | ||||
|       "docx", | ||||
|       "epub", | ||||
|       "fb2", | ||||
|       "htlz", | ||||
|       "html", | ||||
|       "lit", | ||||
|       "lrf", | ||||
|       "mobi", | ||||
|       "odt", | ||||
|       "pdb", | ||||
|       "pdf", | ||||
|       "pml", | ||||
|       "rb", | ||||
|       "rtf", | ||||
|       "recipe", | ||||
|       "snb", | ||||
|       "tcr", | ||||
|       "txt", | ||||
|     ], | ||||
|   }, | ||||
|   to: { | ||||
|     document: [ | ||||
|       "azw3", | ||||
|       "docx", | ||||
|       "epub", | ||||
|       "fb2", | ||||
|       "html", | ||||
|       "htmlz", | ||||
|       "lit", | ||||
|       "lrf", | ||||
|       "mobi", | ||||
|       "oeb", | ||||
|       "pdb", | ||||
|       "pdf", | ||||
|       "pml", | ||||
|       "rb", | ||||
|       "rtf", | ||||
|       "snb", | ||||
|       "tcr", | ||||
|       "txt", | ||||
|       "txtz", | ||||
|     ], | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| export async function convert( | ||||
|   filePath: string, | ||||
|   fileType: string, | ||||
|   convertTo: string, | ||||
|   targetPath: string, | ||||
|   // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||||
|   options?: unknown, | ||||
| ): Promise<string> { | ||||
|   const command = `ebook-convert "${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"); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
| @@ -700,7 +700,7 @@ export async function convert( | ||||
|     message = "Done: resized to 256x256"; | ||||
|   } | ||||
|  | ||||
|   const command = `ffmpeg -i "${filePath}" ${extra} "${targetPath}"`; | ||||
|   const command = `ffmpeg ${process.env.FFMPEG_ARGS || ""} -i "${filePath}" ${extra} "${targetPath}"`; | ||||
|  | ||||
|   return new Promise((resolve, reject) => { | ||||
|     exec(command, (error, stdout, stderr) => { | ||||
|   | ||||
							
								
								
									
										64
									
								
								src/converters/inkscape.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,64 @@ | ||||
| import { exec } from "node:child_process"; | ||||
|  | ||||
| export const properties = { | ||||
|     from: { | ||||
|       images: [ | ||||
|         "svg", | ||||
|         "pdf", | ||||
|         "eps", | ||||
|         "ps", | ||||
|         "wmf", | ||||
|         "emf", | ||||
|         "png" | ||||
|       ] | ||||
|     }, | ||||
|     to: { | ||||
|       images: [ | ||||
|         "dxf", | ||||
|         "emf", | ||||
|         "eps", | ||||
|         "fxg", | ||||
|         "gpl", | ||||
|         "hpgl", | ||||
|         "html", | ||||
|         "odg", | ||||
|         "pdf", | ||||
|         "png", | ||||
|         "pov", | ||||
|         "ps", | ||||
|         "sif", | ||||
|         "svg", | ||||
|         "svgz", | ||||
|         "tex", | ||||
|         "wmf", | ||||
|       ] | ||||
|     }, | ||||
|   }; | ||||
|  | ||||
|   export function convert( | ||||
|     filePath: string, | ||||
|     fileType: string, | ||||
|     convertTo: string, | ||||
|     targetPath: string, | ||||
|     // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||||
|     options?: unknown, | ||||
|   ): Promise<string> { | ||||
|     return new Promise((resolve, reject) => { | ||||
|       exec(`inkscape "${filePath}" -o "${targetPath}"`, (error, stdout, stderr) => { | ||||
|         if (error) { | ||||
|           reject(`error: ${error}`); | ||||
|         } | ||||
|    | ||||
|         if (stdout) { | ||||
|           console.log(`stdout: ${stdout}`); | ||||
|         } | ||||
|    | ||||
|         if (stderr) { | ||||
|           console.error(`stderr: ${stderr}`); | ||||
|         } | ||||
|    | ||||
|         resolve("Done"); | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
|    | ||||
| @@ -2,11 +2,13 @@ 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 convertInkscape, properties as propertiesInkscape } from "./inkscape"; | ||||
| 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"; | ||||
| import { convert as convertCalibre, properties as propertiesCalibre } from "./calibre"; | ||||
|  | ||||
|  | ||||
| // This should probably be reconstructed so that the functions are not imported instead the functions hook into this to make the converters more modular | ||||
| @@ -55,6 +57,10 @@ const properties: Record< | ||||
|     properties: propertiesxelatex, | ||||
|     converter: convertxelatex, | ||||
|   }, | ||||
|   calibre: { | ||||
|     properties: propertiesCalibre, | ||||
|     converter: convertCalibre, | ||||
|   }, | ||||
|   pandoc: { | ||||
|     properties: propertiesPandoc, | ||||
|     converter: convertPandoc, | ||||
| @@ -63,6 +69,10 @@ const properties: Record< | ||||
|     properties: propertiesGraphicsmagick, | ||||
|     converter: convertGraphicsmagick, | ||||
|   }, | ||||
|   inkscape: { | ||||
|     properties: propertiesInkscape, | ||||
|     converter: convertInkscape, | ||||
|   }, | ||||
|   assimp: { | ||||
|     properties: propertiesassimp, | ||||
|     converter: convertassimp, | ||||
|   | ||||
| @@ -11,6 +11,8 @@ export const normalizeFiletype = (filetype: string): string => { | ||||
|       return "latex"; | ||||
|     case "md": | ||||
|       return "markdown"; | ||||
|     case "unknown": | ||||
|       return "m4a"; | ||||
|     default: | ||||
|       return lowercaseFiletype; | ||||
|   } | ||||
|   | ||||
| @@ -53,6 +53,16 @@ if (process.env.NODE_ENV === "production") { | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   exec("inkscape --version", (error, stdout) => { | ||||
|     if (error) { | ||||
|       console.error("Inkscape is not installed."); | ||||
|     } | ||||
|  | ||||
|     if (stdout) { | ||||
|       console.log(stdout.split("\n")[0]); | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   exec("djxl --version", (error, stdout) => { | ||||
|     if (error) { | ||||
|       console.error("libjxl-tools is not installed."); | ||||
| @@ -93,6 +103,16 @@ if (process.env.NODE_ENV === "production") { | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   exec("ebook-convert --version", (error, stdout) => { | ||||
|     if (error) { | ||||
|       console.error("ebook-convert (calibre) is not installed"); | ||||
|     } | ||||
|  | ||||
|     if (stdout) { | ||||
|       console.log(stdout.split("\n")[0]); | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   exec("bun -v", (error, stdout) => { | ||||
|     if (error) { | ||||
|       console.error("Bun is not installed. wait what"); | ||||
|   | ||||
							
								
								
									
										217
									
								
								src/index.tsx
									
									
									
									
									
								
							
							
						
						| @@ -2,7 +2,7 @@ import { randomInt, randomUUID } from "node:crypto"; | ||||
| import { rmSync } from "node:fs"; | ||||
| import { mkdir, unlink } from "node:fs/promises"; | ||||
| import cookie from "@elysiajs/cookie"; | ||||
| import { html } from "@elysiajs/html"; | ||||
| import { html, Html } from "@elysiajs/html"; | ||||
| import { jwt, type JWTPayloadSpec } from "@elysiajs/jwt"; | ||||
| import { staticPlugin } from "@elysiajs/static"; | ||||
| import { Database } from "bun:sqlite"; | ||||
| @@ -37,6 +37,8 @@ const AUTO_DELETE_EVERY_N_HOURS = process.env.AUTO_DELETE_EVERY_N_HOURS | ||||
|   ? Number(process.env.AUTO_DELETE_EVERY_N_HOURS) | ||||
|   : 24; | ||||
|  | ||||
| const WEBROOT = process.env.WEBROOT ?? ""; | ||||
|  | ||||
| // fileNames: fileNames, | ||||
| // filesToConvert: fileNames.length, | ||||
| // convertedFiles : 0, | ||||
| @@ -112,6 +114,7 @@ const app = new Elysia({ | ||||
|   serve: { | ||||
|     maxRequestBodySize: Number.MAX_SAFE_INTEGER, | ||||
|   }, | ||||
|   prefix: WEBROOT, | ||||
| }) | ||||
|   .use(cookie()) | ||||
|   .use(html()) | ||||
| @@ -127,24 +130,36 @@ const app = new Elysia({ | ||||
|   ) | ||||
|   .use( | ||||
|     staticPlugin({ | ||||
|       assets: "src/public/", | ||||
|       prefix: "/", | ||||
|       assets: "public", | ||||
|       prefix: "", | ||||
|     }), | ||||
|   ) | ||||
|   .get("/test", () => { | ||||
|     return ( | ||||
|       <html lang="en"> | ||||
|         <head> | ||||
|           <title>Hello World</title> | ||||
|         </head> | ||||
|         <body> | ||||
|           <h1>Hello</h1> | ||||
|         </body> | ||||
|       </html> | ||||
|     ); | ||||
|   }) | ||||
|   .get("/setup", ({ redirect }) => { | ||||
|     if (!FIRST_RUN) { | ||||
|       return redirect("/login", 302); | ||||
|       return redirect(`${WEBROOT}/login`, 302); | ||||
|     } | ||||
|  | ||||
|     return ( | ||||
|       <BaseHtml title="ConvertX | Setup"> | ||||
|       <BaseHtml title="ConvertX | Setup" webroot={WEBROOT}> | ||||
|         <main class="mx-auto w-full max-w-4xl px-4"> | ||||
|           <h1 class="my-8 text-3xl">Welcome to ConvertX!</h1> | ||||
|           <article class="article p-0"> | ||||
|             <header class="w-full bg-neutral-800 p-4"> | ||||
|               Create your account | ||||
|             </header> | ||||
|             <form method="post" action="/register" class="p-4"> | ||||
|             <form method="post" action={`${WEBROOT}/register`} class="p-4"> | ||||
|               <fieldset class="mb-4 flex flex-col gap-4"> | ||||
|                 <label class="flex flex-col gap-1"> | ||||
|                   Email | ||||
| @@ -191,13 +206,17 @@ const app = new Elysia({ | ||||
|   }) | ||||
|   .get("/register", ({ redirect }) => { | ||||
|     if (!ACCOUNT_REGISTRATION) { | ||||
|       return redirect("/login", 302); | ||||
|       return redirect(`${WEBROOT}/login`, 302); | ||||
|     } | ||||
|  | ||||
|     return ( | ||||
|       <BaseHtml title="ConvertX | Register"> | ||||
|       <BaseHtml webroot={WEBROOT} title="ConvertX | Register"> | ||||
|         <> | ||||
|           <Header accountRegistration={ACCOUNT_REGISTRATION} /> | ||||
|           <Header | ||||
|             webroot={WEBROOT} | ||||
|             accountRegistration={ACCOUNT_REGISTRATION} | ||||
|             allowUnauthenticated={ALLOW_UNAUTHENTICATED} | ||||
|           /> | ||||
|           <main class="w-full px-4"> | ||||
|             <article class="article"> | ||||
|               <form method="post" class="flex flex-col gap-4"> | ||||
| @@ -241,7 +260,7 @@ const app = new Elysia({ | ||||
|     "/register", | ||||
|     async ({ body, set, redirect, jwt, cookie: { auth } }) => { | ||||
|       if (!ACCOUNT_REGISTRATION && !FIRST_RUN) { | ||||
|         return redirect("/login", 302); | ||||
|         return redirect(`${WEBROOT}/login`, 302); | ||||
|       } | ||||
|  | ||||
|       if (FIRST_RUN) { | ||||
| @@ -296,13 +315,13 @@ const app = new Elysia({ | ||||
|         sameSite: "strict", | ||||
|       }); | ||||
|  | ||||
|       return redirect("/", 302); | ||||
|       return redirect(`${WEBROOT}/`, 302); | ||||
|     }, | ||||
|     { body: t.Object({ email: t.String(), password: t.String() }) }, | ||||
|   ) | ||||
|   .get("/login", async ({ jwt, redirect, cookie: { auth } }) => { | ||||
|     if (FIRST_RUN) { | ||||
|       return redirect("/setup", 302); | ||||
|       return redirect(`${WEBROOT}/setup`, 302); | ||||
|     } | ||||
|  | ||||
|     // if already logged in, redirect to home | ||||
| @@ -310,16 +329,20 @@ const app = new Elysia({ | ||||
|       const user = await jwt.verify(auth.value); | ||||
|  | ||||
|       if (user) { | ||||
|         return redirect("/", 302); | ||||
|         return redirect(`${WEBROOT}/`, 302); | ||||
|       } | ||||
|  | ||||
|       auth.remove(); | ||||
|     } | ||||
|  | ||||
|     return ( | ||||
|       <BaseHtml title="ConvertX | Login"> | ||||
|       <BaseHtml webroot={WEBROOT} title="ConvertX | Login"> | ||||
|         <> | ||||
|           <Header accountRegistration={ACCOUNT_REGISTRATION} /> | ||||
|           <Header | ||||
|             webroot={WEBROOT} | ||||
|             accountRegistration={ACCOUNT_REGISTRATION} | ||||
|             allowUnauthenticated={ALLOW_UNAUTHENTICATED} | ||||
|           /> | ||||
|           <main class="w-full px-4"> | ||||
|             <article class="article"> | ||||
|               <form method="post" class="flex flex-col gap-4"> | ||||
| @@ -350,7 +373,7 @@ const app = new Elysia({ | ||||
|                 <div role="group"> | ||||
|                   {ACCOUNT_REGISTRATION ? ( | ||||
|                     <a | ||||
|                       href="/register" | ||||
|                       href={`${WEBROOT}/register`} | ||||
|                       role="button" | ||||
|                       class="btn-primary w-full" | ||||
|                     > | ||||
| @@ -417,7 +440,7 @@ const app = new Elysia({ | ||||
|         sameSite: "strict", | ||||
|       }); | ||||
|  | ||||
|       return redirect("/", 302); | ||||
|       return redirect(`${WEBROOT}/`, 302); | ||||
|     }, | ||||
|     { body: t.Object({ email: t.String(), password: t.String() }) }, | ||||
|   ) | ||||
| @@ -426,46 +449,29 @@ const app = new Elysia({ | ||||
|       auth.remove(); | ||||
|     } | ||||
|  | ||||
|     return redirect("/login", 302); | ||||
|     return redirect(`${WEBROOT}/login`, 302); | ||||
|   }) | ||||
|   .post("/logoff", ({ redirect, cookie: { auth } }) => { | ||||
|     if (auth?.value) { | ||||
|       auth.remove(); | ||||
|     } | ||||
|  | ||||
|     return redirect("/login", 302); | ||||
|     return redirect(`${WEBROOT}/login`, 302); | ||||
|   }) | ||||
|   .get("/", async ({ jwt, redirect, cookie: { auth, jobId } }) => { | ||||
|     if (!ALLOW_UNAUTHENTICATED) { | ||||
|       if (FIRST_RUN) { | ||||
|       return redirect("/setup", 302); | ||||
|         return redirect(`${WEBROOT}/setup`, 302); | ||||
|       } | ||||
|  | ||||
|     if (!auth?.value && !ALLOW_UNAUTHENTICATED) { | ||||
|       return redirect("/login", 302); | ||||
|       if (!auth?.value) { | ||||
|         return redirect(`${WEBROOT}/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 | ||||
|           const existingUser = db | ||||
|             .query("SELECT * FROM users WHERE id = ?") | ||||
|             .as(User) | ||||
|             .get(user.id); | ||||
|  | ||||
|           if (!existingUser) { | ||||
|             if (auth?.value) { | ||||
|               auth.remove(); | ||||
|             } | ||||
|             return redirect("/login", 302); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } else if (ALLOW_UNAUTHENTICATED) { | ||||
|     if (ALLOW_UNAUTHENTICATED) { | ||||
|       const newUserId = String( | ||||
|         randomInt( | ||||
|           2 ** 24, | ||||
| @@ -491,10 +497,29 @@ const app = new Elysia({ | ||||
|         maxAge: 24 * 60 * 60, | ||||
|         sameSite: "strict", | ||||
|       }); | ||||
|     } else 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 | ||||
|           const existingUser = db | ||||
|             .query("SELECT * FROM users WHERE id = ?") | ||||
|             .as(User) | ||||
|             .get(user.id); | ||||
|  | ||||
|           if (!existingUser) { | ||||
|             if (auth?.value) { | ||||
|               auth.remove(); | ||||
|             } | ||||
|             return redirect(`${WEBROOT}/login`, 302); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     if (!user) { | ||||
|       return redirect("/login", 302); | ||||
|       return redirect(`${WEBROOT}/login`, 302); | ||||
|     } | ||||
|  | ||||
|     // create a new job | ||||
| @@ -524,9 +549,13 @@ const app = new Elysia({ | ||||
|     console.log("jobId set to:", id); | ||||
|  | ||||
|     return ( | ||||
|       <BaseHtml> | ||||
|       <BaseHtml webroot={WEBROOT}> | ||||
|         <> | ||||
|           <Header loggedIn /> | ||||
|           <Header | ||||
|             webroot={WEBROOT} | ||||
|             allowUnauthenticated={ALLOW_UNAUTHENTICATED} | ||||
|             loggedIn | ||||
|           /> | ||||
|           <main class="w-full px-4"> | ||||
|             <article class="article"> | ||||
|               <h1 class="mb-4 text-xl">Convert</h1> | ||||
| @@ -562,7 +591,7 @@ const app = new Elysia({ | ||||
|             </article> | ||||
|             <form | ||||
|               method="post" | ||||
|               action="/convert" | ||||
|               action={`${WEBROOT}/convert`} | ||||
|               class="relative mx-auto mb-[35vh] w-full max-w-4xl" | ||||
|             > | ||||
|               <input type="hidden" name="file_names" id="file_names" /> | ||||
| @@ -727,16 +756,16 @@ const app = new Elysia({ | ||||
|     "/upload", | ||||
|     async ({ body, redirect, jwt, cookie: { auth, jobId } }) => { | ||||
|       if (!auth?.value) { | ||||
|         return redirect("/login", 302); | ||||
|         return redirect(`${WEBROOT}/login`, 302); | ||||
|       } | ||||
|  | ||||
|       const user = await jwt.verify(auth.value); | ||||
|       if (!user) { | ||||
|         return redirect("/login", 302); | ||||
|         return redirect(`${WEBROOT}/login`, 302); | ||||
|       } | ||||
|  | ||||
|       if (!jobId?.value) { | ||||
|         return redirect("/", 302); | ||||
|         return redirect(`${WEBROOT}/`, 302); | ||||
|       } | ||||
|  | ||||
|       const existingJob = await db | ||||
| @@ -744,7 +773,7 @@ const app = new Elysia({ | ||||
|         .get(jobId.value, user.id); | ||||
|  | ||||
|       if (!existingJob) { | ||||
|         return redirect("/", 302); | ||||
|         return redirect(`${WEBROOT}/`, 302); | ||||
|       } | ||||
|  | ||||
|       const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`; | ||||
| @@ -769,16 +798,16 @@ const app = new Elysia({ | ||||
|     "/delete", | ||||
|     async ({ body, redirect, jwt, cookie: { auth, jobId } }) => { | ||||
|       if (!auth?.value) { | ||||
|         return redirect("/login", 302); | ||||
|         return redirect(`${WEBROOT}/login`, 302); | ||||
|       } | ||||
|  | ||||
|       const user = await jwt.verify(auth.value); | ||||
|       if (!user) { | ||||
|         return redirect("/login", 302); | ||||
|         return redirect(`${WEBROOT}/login`, 302); | ||||
|       } | ||||
|  | ||||
|       if (!jobId?.value) { | ||||
|         return redirect("/", 302); | ||||
|         return redirect(`${WEBROOT}/`, 302); | ||||
|       } | ||||
|  | ||||
|       const existingJob = await db | ||||
| @@ -786,7 +815,7 @@ const app = new Elysia({ | ||||
|         .get(jobId.value, user.id); | ||||
|  | ||||
|       if (!existingJob) { | ||||
|         return redirect("/", 302); | ||||
|         return redirect(`${WEBROOT}/`, 302); | ||||
|       } | ||||
|  | ||||
|       const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`; | ||||
| @@ -799,16 +828,16 @@ const app = new Elysia({ | ||||
|     "/convert", | ||||
|     async ({ body, redirect, jwt, cookie: { auth, jobId } }) => { | ||||
|       if (!auth?.value) { | ||||
|         return redirect("/login", 302); | ||||
|         return redirect(`${WEBROOT}/login`, 302); | ||||
|       } | ||||
|  | ||||
|       const user = await jwt.verify(auth.value); | ||||
|       if (!user) { | ||||
|         return redirect("/login", 302); | ||||
|         return redirect(`${WEBROOT}/login`, 302); | ||||
|       } | ||||
|  | ||||
|       if (!jobId?.value) { | ||||
|         return redirect("/", 302); | ||||
|         return redirect(`${WEBROOT}/`, 302); | ||||
|       } | ||||
|  | ||||
|       const existingJob = db | ||||
| @@ -817,7 +846,7 @@ const app = new Elysia({ | ||||
|         .get(jobId.value, user.id); | ||||
|  | ||||
|       if (!existingJob) { | ||||
|         return redirect("/", 302); | ||||
|         return redirect(`${WEBROOT}/`, 302); | ||||
|       } | ||||
|  | ||||
|       const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`; | ||||
| @@ -838,7 +867,7 @@ const app = new Elysia({ | ||||
|       const fileNames = JSON.parse(body.file_names) as string[]; | ||||
|  | ||||
|       if (!Array.isArray(fileNames) || fileNames.length === 0) { | ||||
|         return redirect("/", 302); | ||||
|         return redirect(`${WEBROOT}/`, 302); | ||||
|       } | ||||
|  | ||||
|       db.query( | ||||
| @@ -891,7 +920,7 @@ const app = new Elysia({ | ||||
|         }); | ||||
|  | ||||
|       // Redirect the client immediately | ||||
|       return redirect(`/results/${jobId.value}`, 302); | ||||
|       return redirect(`${WEBROOT}/results/${jobId.value}`, 302); | ||||
|     }, | ||||
|     { | ||||
|       body: t.Object({ | ||||
| @@ -902,12 +931,12 @@ const app = new Elysia({ | ||||
|   ) | ||||
|   .get("/history", async ({ jwt, redirect, cookie: { auth } }) => { | ||||
|     if (!auth?.value) { | ||||
|       return redirect("/login", 302); | ||||
|       return redirect(`${WEBROOT}/login`, 302); | ||||
|     } | ||||
|     const user = await jwt.verify(auth.value); | ||||
|  | ||||
|     if (!user) { | ||||
|       return redirect("/login", 302); | ||||
|       return redirect(`${WEBROOT}/login`, 302); | ||||
|     } | ||||
|  | ||||
|     let userJobs = db | ||||
| @@ -928,9 +957,13 @@ const app = new Elysia({ | ||||
|     userJobs = userJobs.filter((job) => job.num_files > 0); | ||||
|  | ||||
|     return ( | ||||
|       <BaseHtml title="ConvertX | Results"> | ||||
|       <BaseHtml webroot={WEBROOT} title="ConvertX | Results"> | ||||
|         <> | ||||
|           <Header loggedIn /> | ||||
|           <Header | ||||
|             webroot={WEBROOT} | ||||
|             allowUnauthenticated={ALLOW_UNAUTHENTICATED} | ||||
|             loggedIn | ||||
|           /> | ||||
|           <main class="w-full px-4"> | ||||
|             <article class="article"> | ||||
|               <h1 class="mb-4 text-xl">Results</h1> | ||||
| @@ -963,7 +996,7 @@ const app = new Elysia({ | ||||
|                             text-accent-500 underline | ||||
|                             hover:text-accent-400 | ||||
|                           `} | ||||
|                           href={`/results/${job.id}`} | ||||
|                           href={`${WEBROOT}/results/${job.id}`} | ||||
|                         > | ||||
|                           View | ||||
|                         </a> | ||||
| @@ -982,7 +1015,7 @@ const app = new Elysia({ | ||||
|     "/results/:jobId", | ||||
|     async ({ params, jwt, set, redirect, cookie: { auth, job_id } }) => { | ||||
|       if (!auth?.value) { | ||||
|         return redirect("/login", 302); | ||||
|         return redirect(`${WEBROOT}/login`, 302); | ||||
|       } | ||||
|  | ||||
|       if (job_id?.value) { | ||||
| @@ -992,7 +1025,7 @@ const app = new Elysia({ | ||||
|  | ||||
|       const user = await jwt.verify(auth.value); | ||||
|       if (!user) { | ||||
|         return redirect("/login", 302); | ||||
|         return redirect(`${WEBROOT}/login`, 302); | ||||
|       } | ||||
|  | ||||
|       const job = db | ||||
| @@ -1015,9 +1048,13 @@ const app = new Elysia({ | ||||
|         .all(params.jobId); | ||||
|  | ||||
|       return ( | ||||
|         <BaseHtml title="ConvertX | Result"> | ||||
|         <BaseHtml webroot={WEBROOT} title="ConvertX | Result"> | ||||
|           <> | ||||
|             <Header loggedIn /> | ||||
|             <Header | ||||
|               webroot={WEBROOT} | ||||
|               allowUnauthenticated={ALLOW_UNAUTHENTICATED} | ||||
|               loggedIn | ||||
|             /> | ||||
|             <main class="w-full px-4"> | ||||
|               <article class="article"> | ||||
|                 <div class="mb-4 flex items-center justify-between"> | ||||
| @@ -1075,7 +1112,7 @@ const app = new Elysia({ | ||||
|                               text-accent-500 underline | ||||
|                               hover:text-accent-400 | ||||
|                             `} | ||||
|                             href={`/download/${outputPath}${file.output_file_name}`} | ||||
|                             href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`} | ||||
|                           > | ||||
|                             View | ||||
|                           </a> | ||||
| @@ -1086,7 +1123,7 @@ const app = new Elysia({ | ||||
|                               text-accent-500 underline | ||||
|                               hover:text-accent-400 | ||||
|                             `} | ||||
|                             href={`/download/${outputPath}${file.output_file_name}`} | ||||
|                             href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`} | ||||
|                             download={file.output_file_name} | ||||
|                           > | ||||
|                             Download | ||||
| @@ -1098,7 +1135,7 @@ const app = new Elysia({ | ||||
|                 </table> | ||||
|               </article> | ||||
|             </main> | ||||
|             <script src="/results.js" defer /> | ||||
|             <script src={`${WEBROOT}/results.js`} defer /> | ||||
|           </> | ||||
|         </BaseHtml> | ||||
|       ); | ||||
| @@ -1108,7 +1145,7 @@ const app = new Elysia({ | ||||
|     "/progress/:jobId", | ||||
|     async ({ jwt, set, params, redirect, cookie: { auth, job_id } }) => { | ||||
|       if (!auth?.value) { | ||||
|         return redirect("/login", 302); | ||||
|         return redirect(`${WEBROOT}/login`, 302); | ||||
|       } | ||||
|  | ||||
|       if (job_id?.value) { | ||||
| @@ -1118,7 +1155,7 @@ const app = new Elysia({ | ||||
|  | ||||
|       const user = await jwt.verify(auth.value); | ||||
|       if (!user) { | ||||
|         return redirect("/login", 302); | ||||
|         return redirect(`${WEBROOT}/login`, 302); | ||||
|       } | ||||
|  | ||||
|       const job = db | ||||
| @@ -1197,7 +1234,7 @@ const app = new Elysia({ | ||||
|                         text-accent-500 underline | ||||
|                         hover:text-accent-400 | ||||
|                       `} | ||||
|                       href={`/download/${outputPath}${file.output_file_name}`} | ||||
|                       href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`} | ||||
|                     > | ||||
|                       View | ||||
|                     </a> | ||||
| @@ -1208,7 +1245,7 @@ const app = new Elysia({ | ||||
|                         text-accent-500 underline | ||||
|                         hover:text-accent-400 | ||||
|                       `} | ||||
|                       href={`/download/${outputPath}${file.output_file_name}`} | ||||
|                       href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`} | ||||
|                       download={file.output_file_name} | ||||
|                     > | ||||
|                       Download | ||||
| @@ -1226,12 +1263,12 @@ const app = new Elysia({ | ||||
|     "/download/:userId/:jobId/:fileName", | ||||
|     async ({ params, jwt, redirect, cookie: { auth } }) => { | ||||
|       if (!auth?.value) { | ||||
|         return redirect("/login", 302); | ||||
|         return redirect(`${WEBROOT}/login`, 302); | ||||
|       } | ||||
|  | ||||
|       const user = await jwt.verify(auth.value); | ||||
|       if (!user) { | ||||
|         return redirect("/login", 302); | ||||
|         return redirect(`${WEBROOT}/login`, 302); | ||||
|       } | ||||
|  | ||||
|       const job = await db | ||||
| @@ -1239,7 +1276,7 @@ const app = new Elysia({ | ||||
|         .get(user.id, params.jobId); | ||||
|  | ||||
|       if (!job) { | ||||
|         return redirect("/results", 302); | ||||
|         return redirect(`${WEBROOT}/results`, 302); | ||||
|       } | ||||
|       // parse from url encoded string | ||||
|       const userId = decodeURIComponent(params.userId); | ||||
| @@ -1252,18 +1289,22 @@ const app = new Elysia({ | ||||
|   ) | ||||
|   .get("/converters", async ({ jwt, redirect, cookie: { auth } }) => { | ||||
|     if (!auth?.value) { | ||||
|       return redirect("/login", 302); | ||||
|       return redirect(`${WEBROOT}/login`, 302); | ||||
|     } | ||||
|  | ||||
|     const user = await jwt.verify(auth.value); | ||||
|     if (!user) { | ||||
|       return redirect("/login", 302); | ||||
|       return redirect(`${WEBROOT}/login`, 302); | ||||
|     } | ||||
|  | ||||
|     return ( | ||||
|       <BaseHtml title="ConvertX | Converters"> | ||||
|       <BaseHtml webroot={WEBROOT} title="ConvertX | Converters"> | ||||
|         <> | ||||
|           <Header loggedIn /> | ||||
|           <Header | ||||
|             webroot={WEBROOT} | ||||
|             allowUnauthenticated={ALLOW_UNAUTHENTICATED} | ||||
|             loggedIn | ||||
|           /> | ||||
|           <main class="w-full px-4"> | ||||
|             <article class="article"> | ||||
|               <h1 class="mb-4 text-xl">Converters</h1> | ||||
| @@ -1322,12 +1363,12 @@ const app = new Elysia({ | ||||
|     async ({ params, jwt, redirect, cookie: { auth } }) => { | ||||
|       // TODO: Implement zip download | ||||
|       if (!auth?.value) { | ||||
|         return redirect("/login", 302); | ||||
|         return redirect(`${WEBROOT}/login`, 302); | ||||
|       } | ||||
|  | ||||
|       const user = await jwt.verify(auth.value); | ||||
|       if (!user) { | ||||
|         return redirect("/login", 302); | ||||
|         return redirect(`${WEBROOT}/login`, 302); | ||||
|       } | ||||
|  | ||||
|       const job = await db | ||||
| @@ -1335,12 +1376,12 @@ const app = new Elysia({ | ||||
|         .get(user.id, params.jobId); | ||||
|  | ||||
|       if (!job) { | ||||
|         return redirect("/results", 302); | ||||
|         return redirect(`${WEBROOT}/results`, 302); | ||||
|       } | ||||
|  | ||||
|       // const userId = decodeURIComponent(params.userId); | ||||
|       // const jobId = decodeURIComponent(params.jobId); | ||||
|       // const outputPath = `${outputDir}${userId}/${jobId}/`; | ||||
|       // const outputPath = `${outputDir}${userId}/`{jobId}/); | ||||
|  | ||||
|       // return Bun.zip(outputPath); | ||||
|     }, | ||||
| @@ -1364,7 +1405,7 @@ if (process.env.NODE_ENV !== "production") { | ||||
| app.listen(3000); | ||||
|  | ||||
| console.log( | ||||
|   `🦊 Elysia is running at http://${app.server?.hostname}:${app.server?.port}`, | ||||
|   `🦊 Elysia is running at http://${app.server?.hostname}:${app.server?.port}${WEBROOT}`, | ||||
| ); | ||||
|  | ||||
| const clearJobs = () => { | ||||
|   | ||||
| @@ -1,7 +1,8 @@ | ||||
| /* eslint-disable @typescript-eslint/no-require-imports */ | ||||
| /** @type {import('tailwindcss').Config} */ | ||||
|  | ||||
| module.exports = { | ||||
| import tailwindScrollbar from "tailwind-scrollbar"; | ||||
|  | ||||
| export default { | ||||
|   content: ["./src/**/*.{html,js,tsx,jsx,cjs,mjs}"], | ||||
|   theme: { | ||||
|     extend: { | ||||
| @@ -22,5 +23,5 @@ module.exports = { | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
|   plugins: [require("tailwind-scrollbar")], | ||||
|   plugins: [tailwindScrollbar], | ||||
| }; | ||||
|   | ||||