Compare commits

55 Commits

Author SHA1 Message Date
Emrik Östling
ce41ee2387 Merge pull request #313 from C4illin/release-please--branches--main--components--convertx-frontend 2025-06-04 10:57:03 +02:00
Emrik Östling
01c8fad012 chore(main): release 0.14.1 2025-06-04 10:48:18 +02:00
Emrik Östling
908e91cb91 Merge pull request #312 from C4illin/fix/311/use-baseline-build 2025-06-04 10:47:46 +02:00
Emrik Östling
f1c5cd9f6b Merge pull request #309 from C4illin/fix/301/add-support-for-kepub 2025-06-04 10:28:13 +02:00
C4illin
6ea3058e66 fix: change to baseline build
issue: #311
2025-06-04 10:26:28 +02:00
C4illin
a4e741cc0a chore: add links to changelog 2025-06-03 20:09:37 +02:00
Emrik Östling
0f2172d61f Merge pull request #296 from C4illin/release-please--branches--main--components--convertx-frontend 2025-06-03 20:07:34 +02:00
Emrik Östling
2baa69ca17 Update issue templates 2025-06-03 19:34:54 +02:00
Emrik Östling
3bbfa9186e Update issue templates 2025-06-03 19:31:27 +02:00
C4illin
c1428f5c2b chore: fix knip 2025-06-03 19:27:36 +02:00
C4illin
1be11708c4 chore: format all files 2025-06-03 19:19:28 +02:00
Emrik Östling
8c04b318fd chore(main): release 0.14.0 2025-06-03 18:26:42 +02:00
C4illin
ff2c0057e8 fix: progress bars on firefox 2025-06-03 18:23:26 +02:00
C4illin
c830721e02 chore: update deps 2025-06-03 18:08:12 +02:00
C4illin
625e1a51f6 feat: add dvisvgm 2025-06-03 17:58:43 +02:00
C4illin
6af1e8f326 refactor: create db types 2025-06-03 17:13:20 +02:00
Emrik Östling
82f0e14abf Merge pull request #310 from C4illin/refactor-elysia-router 2025-06-03 15:11:19 +02:00
C4illin
9e759a75de refactor: split main file to pages 2025-06-03 15:04:18 +02:00
C4illin
33388cf209 fix: add support for kepub
issue: #301
2025-06-02 15:51:35 +02:00
C4illin
2490c3a7e7 chore: update dependencies 2025-06-02 15:50:24 +02:00
C4illin
7f86c352e3 chore: format 2025-06-02 15:50:17 +02:00
Emrik Östling
2a3b08487e Merge pull request #304 from xxeisenberg/main
feat: improve job details interaction and accessibility
2025-05-30 09:38:52 +02:00
xeisenberg
29ba229bc2 feat: improve job details interaction and accessibility
- Enhanced job details toggle functionality by adding event listeners to job detail elements.
- Updated job detail rows to use data attributes for better accessibility and maintainability.
- Improved the rendering of file information with unique keys for each file entry.
2025-05-29 23:43:24 +02:00
xeisenberg
50725edd02 feat: enhance job details display with file information
- Added `files_detailed` property to the `Jobs` class to store detailed file information.
- Updated job listing to include a toggle for displaying detailed file information.
- Implemented a toggle function for showing/hiding detailed file rows in the UI.
2025-05-29 23:43:24 +02:00
Emrik Östling
40d1d8a191 Merge pull request #306 from C4illin/renovate/docker-build-push-action-6.x 2025-05-28 11:04:27 +02:00
renovate[bot]
3417564278 chore(deps): update docker/build-push-action action to v6.18.0 2025-05-27 23:08:35 +00:00
C4illin
9a49dedaca feat: show version in footer
Co-authored-by: thejjw <72130076+thejjw@users.noreply.github.com>
2025-05-23 23:11:54 +02:00
C4illin
d9076bf42a chore: update dependencies 2025-05-23 23:10:52 +02:00
C4illin
b9bbf7792f fix: register button style 2025-05-23 21:36:26 +02:00
C4illin
5cc6678ceb chore: add imagemagick 2025-05-23 21:22:12 +02:00
C4illin
b47e5755f6 feat: add ImageMagick
issue: #295, #269
2025-05-23 21:18:47 +02:00
C4illin
af5c768dc7 fix: add av1 and h26X with containers
issue: #287, #293
2025-05-23 21:15:36 +02:00
Emrik Östling
3b573cccae Merge pull request #300 from C4illin/renovate/docker-build-push-action-6.x
chore(deps): update docker/build-push-action action to v6.17.0
2025-05-23 19:35:54 +02:00
renovate[bot]
0c6f6d6904 chore(deps): update docker/build-push-action action to v6.17.0 2025-05-23 17:13:16 +00:00
Emrik Östling
6e2fe27f31 Merge pull request #298 from bennett-sh/fix-account-page-title 2025-05-23 19:13:15 +02:00
Emrik Östling
b6cdd3741a Merge pull request #297 from C4illin/ci-merge-images 2025-05-23 19:12:46 +02:00
Emrik Östling
00f95b6daa ci: merge images 2025-05-23 19:09:16 +02:00
Bennett
2d05bbf86b fix account page title 2025-05-23 12:34:11 +02:00
Emrik Östling
2c87a6c8c2 Merge pull request #291 from C4illin/trixie-test 2025-05-23 11:03:18 +02:00
C4illin
254509db5e chore: restore calibre 2025-05-23 10:06:57 +02:00
C4illin
4e4c029cb8 fix: switch from alpine to debian trixie
issue: #234, #199
2025-05-23 10:06:57 +02:00
Emrik Östling
6dc60679bb Merge pull request #288 from bennett-sh/account-settings 2025-05-23 10:04:27 +02:00
Emrik Östling
6e5d5d9de0 chore: adjust new lines 2025-05-22 16:07:04 +02:00
Emrik Östling
6289c033c8 chore: add github trending badge 2025-05-22 16:06:35 +02:00
Emrik Östling
b200049a81 Merge pull request #292 from C4illin/native-arm64-builds 2025-05-22 14:21:11 +02:00
C4illin
5646f79f99 ci: add native arm64 build 2025-05-22 14:17:31 +02:00
Emrik Östling
5083968b80 chore: add conventional commits 2025-05-22 12:39:07 +02:00
Bennett
2eb9b8fe96 fix updating logic 2025-05-19 20:12:29 +02:00
Bennett
8dc60b41ff Fixes email update validation logic 2025-05-19 18:51:32 +02:00
Bennett
b4be479d02 Update src/index.tsx
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
2025-05-19 18:20:06 +02:00
Bennett
f56a93a1b2 Update src/index.tsx
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
2025-05-19 18:19:55 +02:00
Bennett
ff8b9fca67 add account management page 2025-05-19 17:46:38 +02:00
Emrik Östling
0579f1852b chore: add potrace to readme 2025-05-14 13:14:55 +02:00
Emrik Östling
52d4cc0d03 chore: remove calibre from README.md 2025-05-14 13:10:57 +02:00
Emrik Östling
2c68016ca6 chore: remove duplicates 2025-05-14 13:09:06 +02:00
55 changed files with 4880 additions and 4522 deletions

View File

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

21
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,21 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Checklist:**
- [ ] I am accessing ConvertX over HTTPS or have `HTTP_ALLOWED=true`

View File

@@ -0,0 +1,14 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[Feature Request]"
labels: enhancement
assignees: ''
---
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -1,80 +1,164 @@
name: Docker
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
on:
push:
branches: [ "main" ]
# Publish semver tags as releases.
tags: [ 'v*.*.*' ]
pull_request:
branches: [ "main" ]
workflow_dispatch:
env:
# Use docker.io for Docker Hub if empty
REGISTRY: ghcr.io
# github.repository as <account>/<repo>
IMAGE_NAME: ${{ github.repository }}
DOCKERHUB_USERNAME: c4illin
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Workaround: https://github.com/docker/build-push-action/issues/461
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v3
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
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 }}
${{ env.IMAGE_NAME }}
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
name: Docker
# thanks to https://github.com/sredevopsorg/multi-arch-docker-github-workflow
on:
push:
branches: ["main"]
tags: ["v*.*.*"]
pull_request:
branches: ["main"]
workflow_dispatch:
env:
GHCR_IMAGE: ghcr.io/c4illin/convertx
IMAGE_NAME: ${{ github.repository }}
DOCKERHUB_USERNAME: c4illin
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
# The build job builds the Docker image for each platform specified in the matrix.
build:
strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
- linux/arm64
permissions:
contents: write
packages: write
attestations: write
checks: write
actions: read
runs-on: ${{ matrix.platform == 'linux/amd64' && 'ubuntu-24.04' || matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' }}
name: Build Docker image for ${{ matrix.platform }}
steps:
- name: Prepare environment for current platform
# This step sets up the environment for the current platform being built.
# It replaces the '/' character in the platform name with '-' and sets it as an environment variable.
# This is useful for naming artifacts and other resources that cannot contain '/'.
# The environment variable PLATFORMS_PAIR will be used later in the workflow.
id: prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Checkout repository
uses: actions/checkout@v4
- name: Docker meta default
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.GHCR_IMAGE }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.10.0
with:
platforms: ${{ matrix.platform }}
- name: Login to GitHub Container Registry
# here we only login to ghcr.io since the this only pushes internal images
uses: docker/login-action@v3.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push by digest
id: build
uses: docker/build-push-action@v6.18.0
env:
DOCKER_BUILDKIT: 1
with:
context: .
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
annotations: ${{ steps.meta.outputs.annotations }}
outputs: type=image,name=${{ env.GHCR_IMAGE }},push-by-digest=true,name-canonical=true,push=true,oci-mediatypes=true
cache-from: type=gha,scope=${{ matrix.platform }}
cache-to: type=gha,mode=max,scope=${{ matrix.platform }}
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
merge:
name: Merge Docker manifests
runs-on: ubuntu-latest
permissions:
attestations: write
contents: read
packages: write
needs:
- build
steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.GHCR_IMAGE }}
${{ env.IMAGE_NAME }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ env.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Get execution timestamp with RFC3339 format
id: timestamp
run: |
echo "timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> $GITHUB_OUTPUT
- name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create \
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
--annotation='index:org.opencontainers.image.description=${{ github.event.repository.description }}' \
--annotation='index:org.opencontainers.image.created=${{ steps.timestamp.outputs.timestamp }}' \
--annotation='index:org.opencontainers.image.url=${{ github.event.repository.url }}' \
--annotation='index:org.opencontainers.image.source=${{ github.event.repository.url }}' \
$(printf '${{ env.GHCR_IMAGE }}@sha256:%s ' *)
- name: Inspect image
run: |
docker buildx imagetools inspect '${{ env.GHCR_IMAGE }}:${{ steps.meta.outputs.version }}'

View File

@@ -15,13 +15,13 @@ jobs:
dockerHubDescription:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- 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
- 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

View File

@@ -14,8 +14,8 @@ jobs:
packages: write
steps:
- name: Remove Docker Tag
uses: ArchieAtkinson/remove-dockertag-action@v0.0
with:
tag_name: master
github_token: ${{ secrets.GITHUB_TOKEN }}
- name: Remove Docker Tag
uses: ArchieAtkinson/remove-dockertag-action@v0.0
with:
tag_name: master
github_token: ${{ secrets.GITHUB_TOKEN }}

View File

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

View File

@@ -1,246 +1,234 @@
# Changelog
## [0.14.1](https://github.com/C4illin/ConvertX/compare/v0.14.0...v0.14.1) (2025-06-04)
### Bug Fixes
* change to baseline build ([6ea3058](https://github.com/C4illin/ConvertX/commit/6ea3058e66262f7a14633bddcecd5573948f524a)), closes [#311](https://github.com/C4illin/ConvertX/issues/311)
## [0.14.0](https://github.com/C4illin/ConvertX/compare/v0.13.0...v0.14.0) (2025-06-03)
### Features
- add dvisvgm ([625e1a5](https://github.com/C4illin/ConvertX/commit/625e1a51f620fe9da79d0127eb6c95f468d9ea2b))
- add ImageMagick ([b47e575](https://github.com/C4illin/ConvertX/commit/b47e5755f677056e8acecad54c0c2e28a5e137f3)), closes [#295](https://github.com/C4illin/ConvertX/issues/295), closes [#269](https://github.com/C4illin/ConvertX/issues/269)
- enhance job details display with file information ([50725ed](https://github.com/C4illin/ConvertX/commit/50725edd021bb9a7f58c85b79c1eab355ad22ced)), closes [#251](https://github.com/C4illin/ConvertX/issues/251)
- improve job details interaction and accessibility ([29ba229](https://github.com/C4illin/ConvertX/commit/29ba229bc23d2019d2ee9829da7852f884ffa611))
- show version in footer ([9a49ded](https://github.com/C4illin/ConvertX/commit/9a49dedacac7e67a432b6da0daf1967038d97d26))
### Bug Fixes
- add av1 and h26X with containers ([af5c768](https://github.com/C4illin/ConvertX/commit/af5c768dc74b3124fd7ef4b29e27c83a5d19ad49)), closes [#287](https://github.com/C4illin/ConvertX/issues/287), closes [#293](https://github.com/C4illin/ConvertX/issues/293)
- progress bars on firefox ([ff2c005](https://github.com/C4illin/ConvertX/commit/ff2c0057e890b9ecb552df30914333349ea20eb7))
- register button style ([b9bbf77](https://github.com/C4illin/ConvertX/commit/b9bbf7792f01fcaa77e3520925de107e856926f1))
- switch from alpine to debian trixie ([4e4c029](https://github.com/C4illin/ConvertX/commit/4e4c029cb800df86affb99c3a82dda9e6708bdde)), closes [#234](https://github.com/C4illin/ConvertX/issues/234), closes [#199](https://github.com/C4illin/ConvertX/issues/199)
## [0.13.0](https://github.com/C4illin/ConvertX/compare/v0.12.1...v0.13.0) (2025-05-14)
### Features
* add HIDE_HISTORY option to control visibility of history page ([bed52ce](https://github.com/C4illin/ConvertX/commit/bed52cef17ff68ec5e8770705a1fdf038e02e607))
* add HIDE_HISTORY option to control visibility of history page ([9d1c931](https://github.com/C4illin/ConvertX/commit/9d1c93155cc33ed6c83f9e5122afff8f28d0e4bf))
* add potrace converter ([bdbd4a1](https://github.com/C4illin/ConvertX/commit/bdbd4a122c09559b089b985ea12c5f3e085107da))
* Add support for .HIF files ([a5eaaa4](https://github.com/C4illin/ConvertX/commit/a5eaaa422a64506dd16d90d48a240556de33bc93))
* Add support for .HIF files ([70705c1](https://github.com/C4illin/ConvertX/commit/70705c1850d470296df85958c02a01fb5bc3a25f))
* add support for drag/drop of images ([ff2ef74](https://github.com/C4illin/ConvertX/commit/ff2ef7413542cf10ba7a6e246763bcecd6829ec1))
- add HIDE_HISTORY option to control visibility of history page ([9d1c931](https://github.com/C4illin/ConvertX/commit/9d1c93155cc33ed6c83f9e5122afff8f28d0e4bf))
- add potrace converter ([bdbd4a1](https://github.com/C4illin/ConvertX/commit/bdbd4a122c09559b089b985ea12c5f3e085107da))
- Add support for .HIF files ([70705c1](https://github.com/C4illin/ConvertX/commit/70705c1850d470296df85958c02a01fb5bc3a25f))
- add support for drag/drop of images ([ff2ef74](https://github.com/C4illin/ConvertX/commit/ff2ef7413542cf10ba7a6e246763bcecd6829ec1))
### Bug Fixes
* add timezone support ([4b5c732](https://github.com/C4illin/ConvertX/commit/4b5c732380bc844dccf340ea1eb4f8bfe3bb44a5)), closes [#258](https://github.com/C4illin/ConvertX/issues/258)
- add timezone support ([4b5c732](https://github.com/C4illin/ConvertX/commit/4b5c732380bc844dccf340ea1eb4f8bfe3bb44a5)), closes [#258](https://github.com/C4illin/ConvertX/issues/258)
## [0.12.1](https://github.com/C4illin/ConvertX/compare/v0.12.0...v0.12.1) (2025-03-20)
### Bug Fixes
* rollback to bun 1.2.2 ([cdae798](https://github.com/C4illin/ConvertX/commit/cdae798fcf5879e4adea87386a38748b9a1e1ddc))
- rollback to bun 1.2.2 ([cdae798](https://github.com/C4illin/ConvertX/commit/cdae798fcf5879e4adea87386a38748b9a1e1ddc))
## [0.12.0](https://github.com/C4illin/ConvertX/compare/v0.11.1...v0.12.0) (2025-03-06)
### Features
* added progress bar for file upload ([db60f35](https://github.com/C4illin/ConvertX/commit/db60f355b2973f43f8e5990e6fe4e351b959b659))
* made every upload file independent ([cc54bdc](https://github.com/C4illin/ConvertX/commit/cc54bdcbe764c41cc3273485d072fd3178ad2dca))
* replace exec with execFile ([9263d17](https://github.com/C4illin/ConvertX/commit/9263d17609dc4b2b367eb7fee67b3182e283b3a3))
- added progress bar for file upload ([db60f35](https://github.com/C4illin/ConvertX/commit/db60f355b2973f43f8e5990e6fe4e351b959b659))
- made every upload file independent ([cc54bdc](https://github.com/C4illin/ConvertX/commit/cc54bdcbe764c41cc3273485d072fd3178ad2dca))
- replace exec with execFile ([9263d17](https://github.com/C4illin/ConvertX/commit/9263d17609dc4b2b367eb7fee67b3182e283b3a3))
### Bug Fixes
* add libheif ([6b92540](https://github.com/C4illin/ConvertX/commit/6b9254047c0598963aee1d99e20ba1650a0368bf))
* add libheif ([decfea5](https://github.com/C4illin/ConvertX/commit/decfea5dc9627b216bb276a9e1578c32cfa1deb6)), closes [#202](https://github.com/C4illin/ConvertX/issues/202)
* added onerror log ([ae4bbc8](https://github.com/C4illin/ConvertX/commit/ae4bbc8baacbaf67763c62ea44140bb21cc17230))
* refactored uploadFile to only accept a single file instead of multiple ([dc82a43](https://github.com/C4illin/ConvertX/commit/dc82a438d4104b79ff423d502a6779a43928968a))
* update libheif to 1.19.5 ([fba5e21](https://github.com/C4illin/ConvertX/commit/fba5e212e8d0eaba8971e239e35aeb521f3cd813)), closes [#202](https://github.com/C4illin/ConvertX/issues/202)
- add libheif ([6b92540](https://github.com/C4illin/ConvertX/commit/6b9254047c0598963aee1d99e20ba1650a0368bf))
- add libheif ([decfea5](https://github.com/C4illin/ConvertX/commit/decfea5dc9627b216bb276a9e1578c32cfa1deb6)), closes [#202](https://github.com/C4illin/ConvertX/issues/202)
- added onerror log ([ae4bbc8](https://github.com/C4illin/ConvertX/commit/ae4bbc8baacbaf67763c62ea44140bb21cc17230))
- refactored uploadFile to only accept a single file instead of multiple ([dc82a43](https://github.com/C4illin/ConvertX/commit/dc82a438d4104b79ff423d502a6779a43928968a))
- update libheif to 1.19.5 ([fba5e21](https://github.com/C4illin/ConvertX/commit/fba5e212e8d0eaba8971e239e35aeb521f3cd813)), closes [#202](https://github.com/C4illin/ConvertX/issues/202)
## [0.11.1](https://github.com/C4illin/ConvertX/compare/v0.11.0...v0.11.1) (2025-02-07)
### Bug Fixes
* mobile view overflow ([bec58ac](https://github.com/C4illin/ConvertX/commit/bec58ac59f9600e35385b9e21d174f3ab1b42b1d))
- mobile view overflow ([bec58ac](https://github.com/C4illin/ConvertX/commit/bec58ac59f9600e35385b9e21d174f3ab1b42b1d))
## [0.11.0](https://github.com/C4illin/ConvertX/compare/v0.10.1...v0.11.0) (2025-02-05)
### Features
* add deps for vaapi ([2bbbd03](https://github.com/C4illin/ConvertX/commit/2bbbd03554d384a4488143f29e5fc863cfdf333b)), closes [#192](https://github.com/C4illin/ConvertX/issues/192)
- add deps for vaapi ([2bbbd03](https://github.com/C4illin/ConvertX/commit/2bbbd03554d384a4488143f29e5fc863cfdf333b)), closes [#192](https://github.com/C4illin/ConvertX/issues/192)
### Bug Fixes
* don't crash if file is not found ([16f27c1](https://github.com/C4illin/ConvertX/commit/16f27c13bbc1c0e5fa2316f3db11d0918524053b))
* install numpy for inkscape ([0e61051](https://github.com/C4illin/ConvertX/commit/0e61051fc6be188164c3865b4fb579c140859fdc))
- don't crash if file is not found ([16f27c1](https://github.com/C4illin/ConvertX/commit/16f27c13bbc1c0e5fa2316f3db11d0918524053b))
- install numpy for inkscape ([0e61051](https://github.com/C4illin/ConvertX/commit/0e61051fc6be188164c3865b4fb579c140859fdc))
## [0.10.1](https://github.com/C4illin/ConvertX/compare/v0.10.0...v0.10.1) (2025-01-21)
### Bug Fixes
* ffmpeg works without ffmpeg_args ([3b7ea88](https://github.com/C4illin/ConvertX/commit/3b7ea88b7382f7c21b120bdc9bda5bb10547f55d)), closes [#212](https://github.com/C4illin/ConvertX/issues/212)
- 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)
- 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))
- 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)
- 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)
- treat unknown as m4a ([1a442d6](https://github.com/C4illin/ConvertX/commit/1a442d6e69606afef63b1e7df36aa83d111fa23d)), closes [#178](https://github.com/C4illin/ConvertX/issues/178)
- wait for both upload and selection ([4c05fd7](https://github.com/C4illin/ConvertX/commit/4c05fd72bbbf91ee02327f6fcbf749b78272376b)), closes [#177](https://github.com/C4illin/ConvertX/issues/177)
## [0.8.1](https://github.com/C4illin/ConvertX/compare/v0.8.0...v0.8.1) (2024-10-05)
### Bug Fixes
* disable convert button when input is empty ([78844d7](https://github.com/C4illin/ConvertX/commit/78844d7bd55990789ed07c81e49043e688cbe656)), closes [#151](https://github.com/C4illin/ConvertX/issues/151)
* resize to fit for ico ([b4e53db](https://github.com/C4illin/ConvertX/commit/b4e53dbb8e70b3a95b44e5b756759d16117a87e1)), closes [#157](https://github.com/C4illin/ConvertX/issues/157)
* treat jfif as jpeg ([339b79f](https://github.com/C4illin/ConvertX/commit/339b79f786131deb93f0d5683e03178fdcab1ef5)), closes [#163](https://github.com/C4illin/ConvertX/issues/163)
- 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))
- 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))
- 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))
- 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))
- 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))
- 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))
- 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))
- 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))
- improve file name replacement logic ([60ba7c9](https://github.com/C4illin/ConvertX/commit/60ba7c93fbdc961f3569882fade7cc13dee7a7a5))
## [0.4.1](https://github.com/C4illin/ConvertX/compare/v0.4.0...v0.4.1) (2024-09-15)
### Bug Fixes
* allow non lowercase true and false values, fixes [#122](https://github.com/C4illin/ConvertX/issues/122) ([bef1710](https://github.com/C4illin/ConvertX/commit/bef1710e3376baa7e25c107ded20a40d18b8c6b0))
- allow non lowercase true and false values, fixes [#122](https://github.com/C4illin/ConvertX/issues/122) ([bef1710](https://github.com/C4illin/ConvertX/commit/bef1710e3376baa7e25c107ded20a40d18b8c6b0))
## [0.4.0](https://github.com/C4illin/ConvertX/compare/v0.3.3...v0.4.0) (2024-08-26)
### Features
* add option for unauthenticated file conversions [#114](https://github.com/C4illin/ConvertX/issues/114) ([f0d0e43](https://github.com/C4illin/ConvertX/commit/f0d0e4392983c3e4c530304ea88e023fda9bcac0))
* add resvg converter ([d5eeef9](https://github.com/C4illin/ConvertX/commit/d5eeef9f6884b2bb878508bed97ea9ceaa662995))
* add robots.txt ([6597c1d](https://github.com/C4illin/ConvertX/commit/6597c1d7caeb4dfb6bc47b442e4dfc9840ad12b7))
* Add search bar for formats ([53fff59](https://github.com/C4illin/ConvertX/commit/53fff594fc4d69306abcb2a5cad890fcd0953a58))
- add option for unauthenticated file conversions [#114](https://github.com/C4illin/ConvertX/issues/114) ([f0d0e43](https://github.com/C4illin/ConvertX/commit/f0d0e4392983c3e4c530304ea88e023fda9bcac0))
- add resvg converter ([d5eeef9](https://github.com/C4illin/ConvertX/commit/d5eeef9f6884b2bb878508bed97ea9ceaa662995))
- add robots.txt ([6597c1d](https://github.com/C4illin/ConvertX/commit/6597c1d7caeb4dfb6bc47b442e4dfc9840ad12b7))
- Add search bar for formats ([53fff59](https://github.com/C4illin/ConvertX/commit/53fff594fc4d69306abcb2a5cad890fcd0953a58))
### Bug Fixes
* keep unauthenticated user logged in if allowed [#114](https://github.com/C4illin/ConvertX/issues/114) ([bc4ad49](https://github.com/C4illin/ConvertX/commit/bc4ad492852fad8cb832a0c03485cccdd7f7b117))
* pdf support in vips ([8ca4f15](https://github.com/C4illin/ConvertX/commit/8ca4f1587df7f358893941c656d78d75f04dac93))
* Slow click on conversion popup does not work ([4d9c4d6](https://github.com/C4illin/ConvertX/commit/4d9c4d64aa0266f3928935ada68d91ac81f638aa))
- keep unauthenticated user logged in if allowed [#114](https://github.com/C4illin/ConvertX/issues/114) ([bc4ad49](https://github.com/C4illin/ConvertX/commit/bc4ad492852fad8cb832a0c03485cccdd7f7b117))
- pdf support in vips ([8ca4f15](https://github.com/C4illin/ConvertX/commit/8ca4f1587df7f358893941c656d78d75f04dac93))
- Slow click on conversion popup does not work ([4d9c4d6](https://github.com/C4illin/ConvertX/commit/4d9c4d64aa0266f3928935ada68d91ac81f638aa))
## [0.3.3](https://github.com/C4illin/ConvertX/compare/v0.3.2...v0.3.3) (2024-07-30)
### Bug Fixes
* downgrade @elysiajs/html dependency to version 1.0.2 ([c714ade](https://github.com/C4illin/ConvertX/commit/c714ade3e23865ba6cfaf76c9e7259df1cda222c))
- downgrade @elysiajs/html dependency to version 1.0.2 ([c714ade](https://github.com/C4illin/ConvertX/commit/c714ade3e23865ba6cfaf76c9e7259df1cda222c))
## [0.3.2](https://github.com/C4illin/ConvertX/compare/v0.3.1...v0.3.2) (2024-07-09)
### Bug Fixes
* increase max request body to support large uploads ([3ae2db5](https://github.com/C4illin/ConvertX/commit/3ae2db5d9b36fe3dcd4372ddcd32aa573ea59aa6)), closes [#64](https://github.com/C4illin/ConvertX/issues/64)
- increase max request body to support large uploads ([3ae2db5](https://github.com/C4illin/ConvertX/commit/3ae2db5d9b36fe3dcd4372ddcd32aa573ea59aa6)), closes [#64](https://github.com/C4illin/ConvertX/issues/64)
## [0.3.1](https://github.com/C4illin/ConvertX/compare/v0.3.0...v0.3.1) (2024-06-27)
### Bug Fixes
* release releases ([4d4c13a](https://github.com/C4illin/ConvertX/commit/4d4c13a8d85ec7c9209ad41cdbea7d4380b0edbf))
- release releases ([4d4c13a](https://github.com/C4illin/ConvertX/commit/4d4c13a8d85ec7c9209ad41cdbea7d4380b0edbf))
## [0.3.0](https://github.com/C4illin/ConvertX/compare/v0.2.0...v0.3.0) (2024-06-27)
### Features
* add version number to log ([4dcb796](https://github.com/C4illin/ConvertX/commit/4dcb796e1bd27badc078d0638076cd9f1e81c4a4)), closes [#44](https://github.com/C4illin/ConvertX/issues/44)
* change to xelatex ([fae2ba9](https://github.com/C4illin/ConvertX/commit/fae2ba9c54461dccdccd1bfb5e76398540d11d0b))
* print version of installed converters to log ([801cf28](https://github.com/C4illin/ConvertX/commit/801cf28d1e5edac9353b0b16be75a4fb48470b8a))
- add version number to log ([4dcb796](https://github.com/C4illin/ConvertX/commit/4dcb796e1bd27badc078d0638076cd9f1e81c4a4)), closes [#44](https://github.com/C4illin/ConvertX/issues/44)
- change to xelatex ([fae2ba9](https://github.com/C4illin/ConvertX/commit/fae2ba9c54461dccdccd1bfb5e76398540d11d0b))
- print version of installed converters to log ([801cf28](https://github.com/C4illin/ConvertX/commit/801cf28d1e5edac9353b0b16be75a4fb48470b8a))
## [0.2.0](https://github.com/C4illin/ConvertX/compare/v0.1.2...v0.2.0) (2024-06-20)
### Features
* add libjxl for jpegxl conversion ([ff680cb](https://github.com/C4illin/ConvertX/commit/ff680cb29534a25c3148a90fd064bb86c71fb482))
* change from debian to alpine ([1316957](https://github.com/C4illin/ConvertX/commit/13169574f0134ae236f8d41287bb73930b575e82)), closes [#34](https://github.com/C4illin/ConvertX/issues/34)
- add libjxl for jpegxl conversion ([ff680cb](https://github.com/C4illin/ConvertX/commit/ff680cb29534a25c3148a90fd064bb86c71fb482))
- change from debian to alpine ([1316957](https://github.com/C4illin/ConvertX/commit/13169574f0134ae236f8d41287bb73930b575e82)), closes [#34](https://github.com/C4illin/ConvertX/issues/34)
## [0.1.2](https://github.com/C4illin/ConvertX/compare/v0.1.1...v0.1.2) (2024-06-10)
### Bug Fixes
* fix incorrect redirect ([25df58b](https://github.com/C4illin/ConvertX/commit/25df58ba82321aaa6617811a6995cb96c2a00a40)), closes [#23](https://github.com/C4illin/ConvertX/issues/23)
- fix incorrect redirect ([25df58b](https://github.com/C4illin/ConvertX/commit/25df58ba82321aaa6617811a6995cb96c2a00a40)), closes [#23](https://github.com/C4illin/ConvertX/issues/23)
## [0.1.1](https://github.com/C4illin/ConvertX/compare/v0.1.0...v0.1.1) (2024-05-30)
### Bug Fixes
* :bug: make sure all redirects are 302 ([9970fd3](https://github.com/C4illin/ConvertX/commit/9970fd3f89190af96f8762edc3817d1e03082b3a)), closes [#12](https://github.com/C4illin/ConvertX/issues/12)
- :bug: make sure all redirects are 302 ([9970fd3](https://github.com/C4illin/ConvertX/commit/9970fd3f89190af96f8762edc3817d1e03082b3a)), closes [#12](https://github.com/C4illin/ConvertX/issues/12)
## 0.1.0 (2024-05-30)
### Features
* remove file from file list in index.html ([787ff97](https://github.com/C4illin/ConvertX/commit/787ff9741ecbbf4fb4c02b43bd22a214a173fd7b))
- remove file from file list in index.html ([787ff97](https://github.com/C4illin/ConvertX/commit/787ff9741ecbbf4fb4c02b43bd22a214a173fd7b))
### Miscellaneous Chores
* release 0.1.0 ([54d9aec](https://github.com/C4illin/ConvertX/commit/54d9aecbf949689b12aa7e5e8e9be7b9032f4431))
- release 0.1.0 ([54d9aec](https://github.com/C4illin/ConvertX/commit/54d9aecbf949689b12aa7e5e8e9be7b9032f4431))

View File

@@ -1,7 +1,25 @@
FROM oven/bun:1.2.2-alpine AS base
FROM debian:trixie-slim AS base
LABEL org.opencontainers.image.source="https://github.com/C4illin/ConvertX"
WORKDIR /app
# install bun
RUN apt-get update && apt-get install -y \
curl \
unzip \
&& rm -rf /var/lib/apt/lists/*
# if architecture is arm64, use the arm64 version of bun
RUN ARCH=$(uname -m) && \
if [ "$ARCH" = "aarch64" ]; then \
curl -fsSL -o bun-linux-aarch64.zip https://github.com/oven-sh/bun/releases/download/bun-v1.2.2/bun-linux-aarch64.zip; \
else \
curl -fsSL -o bun-linux-x64-baseline.zip https://github.com/oven-sh/bun/releases/download/bun-v1.2.2/bun-linux-x64-baseline.zip; \
fi
RUN unzip -j bun-linux-*.zip -d /usr/local/bin && \
rm bun-linux-*.zip && \
chmod +x /usr/local/bin/bun
# install dependencies into temp directory
# this will cache them and speed up future builds
FROM base AS install
@@ -14,12 +32,6 @@ RUN mkdir -p /temp/prod
COPY package.json bun.lock /temp/prod/
RUN cd /temp/prod && bun install --frozen-lockfile --production
FROM base AS builder
RUN apk --no-cache add curl gcc
ENV PATH=/root/.cargo/bin:$PATH
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
RUN cargo install resvg
FROM base AS prerelease
WORKDIR /app
COPY --from=install /temp/dev/node_modules node_modules
@@ -31,38 +43,34 @@ RUN bun run build
# copy production dependencies and source code into final image
FROM base AS release
RUN apk --no-cache add libheif-tools --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community/
# install additional dependencies
RUN apk --no-cache add \
pandoc \
texlive \
texlive-xetex \
texmf-dist-latexextra \
RUN apt-get update && apt-get install -y \
assimp-utils \
calibre \
dcraw \
dvisvgm \
ffmpeg \
graphicsmagick \
ghostscript \
vips-tools \
vips-poppler \
vips-jxl \
vips-heif \
vips-magick \
libjxl-tools \
assimp \
graphicsmagick \
imagemagick-7.q16 \
inkscape \
libheif-examples \
libjxl-tools \
libva2 \
libvips-tools \
mupdf-tools \
pandoc \
poppler-utils \
gcompat \
libva-utils \
py3-numpy \
potrace
# RUN apk --no-cache add calibre@testing --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main/
# this might be needed for some latex use cases, will add it if needed.
# texmf-dist-fontsextra \
potrace \
python3-numpy \
resvg \
texlive \
texlive-latex-extra \
texlive-xetex \
--no-install-recommends \
&& rm -rf /var/lib/apt/lists/*
COPY --from=install /temp/prod/node_modules node_modules
COPY --from=builder /root/.cargo/bin/resvg /usr/local/bin/resvg
COPY --from=prerelease /app/public/generated.css /app/public/
COPY . .

314
README.md
View File

@@ -1,155 +1,159 @@
![ConvertX](images/logo.png)
# ConvertX
[![Docker](https://github.com/C4illin/ConvertX/actions/workflows/docker-publish.yml/badge.svg?branch=main)](https://github.com/C4illin/ConvertX/actions/workflows/docker-publish.yml)
[![ghcr.io Pulls](https://img.shields.io/badge/dynamic/json?logo=github&url=https%3A%2F%2Fipitio.github.io%2Fbackage%2FC4illin%2FConvertX%2Fconvertx.json&query=%24.downloads&label=ghcr.io%20pulls&cacheSeconds=14400)](https://github.com/C4illin/ConvertX/pkgs/container/ConvertX)
[![Docker Pulls](https://img.shields.io/docker/pulls/c4illin/convertx?style=flat&logo=docker&label=dockerhub%20pulls&link=https%3A%2F%2Fhub.docker.com%2Frepository%2Fdocker%2Fc4illin%2Fconvertx%2Fgeneral)](https://hub.docker.com/r/c4illin/convertx)
[![GitHub Release](https://img.shields.io/github/v/release/C4illin/ConvertX)](https://github.com/C4illin/ConvertX/pkgs/container/convertx)
![GitHub commits since latest release](https://img.shields.io/github/commits-since/C4illin/ConvertX/latest)
![GitHub repo size](https://img.shields.io/github/repo-size/C4illin/ConvertX)
![Docker container size](https://ghcr-badge.egpl.dev/c4illin/convertx/size?color=%230375b6&tag=latest&label=image+size&trim=)
<!-- ![Dev image size](https://ghcr-badge.egpl.dev/c4illin/convertx/size?color=%230375b6&tag=main&label=dev+image&trim=) -->
A self-hosted online file converter. Supports over a thousand different formats. Written with TypeScript, Bun and Elysia.
## Features
- Convert files to different formats
- Process multiple files at once
- Password protection
- Multiple accounts
## Converters supported
| Converter | Use case | Converts from | Converts to |
|------------------------------------------------------------------------------|---------------|---------------|-------------|
| [libjxl](https://github.com/libjxl/libjxl) | JPEG XL | 11 | 11 |
| [resvg](https://github.com/RazrFalcon/resvg) | SVG | 1 | 1 |
| [Vips](https://github.com/libvips/libvips) | Images | 45 | 23 |
| [libheif](https://github.com/strukturag/libheif) | HEIF | 2 | 4 |
| [XeLaTeX](https://tug.org/xetex/) | LaTeX | 1 | 1 |
| [Calibre](https://calibre-ebook.com/) | E-books | 26 | 19 |
| [Pandoc](https://pandoc.org/) | Documents | 43 | 65 |
| [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 -->
Any missing converter? Open an issue or pull request!
## Deployment
> [!WARNING]
> If you can't login, make sure you are accessing the service over localhost or https otherwise set HTTP_ALLOWED=true
```yml
# docker-compose.yml
services:
convertx:
image: ghcr.io/c4illin/convertx
container_name: convertx
restart: unless-stopped
ports:
- "3000:3000"
environment:
- JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234 # will use randomUUID() if unset
volumes:
- ./data:/app/data
```
or
```bash
docker run -p 3000:3000 -v ./data:/app/data ghcr.io/c4illin/convertx
```
Then visit `http://localhost:3000` in your browser and create your account. Don't leave it unconfigured and open, as anyone can register the first account.
If you get unable to open database file run `chown -R $USER:$USER path` on the path you choose.
### Environment variables
All are optional, JWT_SECRET is recommended to be set.
| Name | Default | Description |
|---------------------------|---------|-------------|
| JWT_SECRET | when unset it will use the value from randomUUID() | A long and secret string used to sign the JSON Web Token |
| ACCOUNT_REGISTRATION | false | Allow users to register accounts |
| HTTP_ALLOWED | false | Allow HTTP connections, only set this to true locally |
| ALLOW_UNAUTHENTICATED | false | Allow unauthenticated users to use the service, only set this to true locally |
| AUTO_DELETE_EVERY_N_HOURS | 24 | Checks every n hours for files older then n hours and deletes them, set to 0 to disable |
| WEBROOT | | The address to the root path setting this to "/convert" will serve the website on "example.com/convert/" |
| FFMPEG_ARGS | | Arguments to pass to ffmpeg, e.g. `-preset veryfast` |
| HIDE_HISTORY | false | Hide the history page |
### 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 |
![Release image size](https://ghcr-badge.egpl.dev/c4illin/convertx/size?color=%230375b6&tag=latest&label=release+image&trim=)
![Dev image size](https://ghcr-badge.egpl.dev/c4illin/convertx/size?color=%230375b6&tag=main&label=dev+image&trim=)
<!-- Dockerhub was introduced in 0.9.0 and older releases -->
### Tutorial
> [!NOTE]
> These are written by other people, and may be outdated, incorrect or wrong.
Tutorial in french: <https://belginux.com/installer-convertx-avec-docker/>
Tutorial in chinese: <https://xzllll.com/24092901/>
## Screenshots
![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
- [x] Add messages for errors in converters
- [x] Add searchable list of formats
- [ ] Add options for converters
- [ ] Divide index.tsx into smaller components
- [ ] Add tests
- [ ] Make the upload button nicer and more easy to drop files on. Support copy paste as well if possible.
- [ ] Make errors logs visible from the web ui
- [ ] Add more converters:
- [ ] [deark](https://github.com/jsummers/deark)
- [ ] LibreOffice
- [ ] [dvisvgm](https://github.com/mgieseki/dvisvgm)
## Contributors
<a href="https://github.com/C4illin/ConvertX/graphs/contributors">
<img src="https://contrib.rocks/image?repo=C4illin/ConvertX" alt="Image with all contributors"/>
</a>
## Star History
<a href="https://github.com/C4illin/ConvertX/stargazers">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=C4illin/ConvertX&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=C4illin/ConvertX&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=C4illin/ConvertX&type=Date" />
</picture>
</a>
![ConvertX](images/logo.png)
# ConvertX
[![Docker](https://github.com/C4illin/ConvertX/actions/workflows/docker-publish.yml/badge.svg?branch=main)](https://github.com/C4illin/ConvertX/actions/workflows/docker-publish.yml)
[![ghcr.io Pulls](https://img.shields.io/badge/dynamic/json?logo=github&url=https%3A%2F%2Fipitio.github.io%2Fbackage%2FC4illin%2FConvertX%2Fconvertx.json&query=%24.downloads&label=ghcr.io%20pulls&cacheSeconds=14400)](https://github.com/C4illin/ConvertX/pkgs/container/ConvertX)
[![Docker Pulls](https://img.shields.io/docker/pulls/c4illin/convertx?style=flat&logo=docker&label=dockerhub%20pulls&link=https%3A%2F%2Fhub.docker.com%2Frepository%2Fdocker%2Fc4illin%2Fconvertx%2Fgeneral)](https://hub.docker.com/r/c4illin/convertx)
[![GitHub Release](https://img.shields.io/github/v/release/C4illin/ConvertX)](https://github.com/C4illin/ConvertX/pkgs/container/convertx)
![GitHub commits since latest release](https://img.shields.io/github/commits-since/C4illin/ConvertX/latest)
![GitHub repo size](https://img.shields.io/github/repo-size/C4illin/ConvertX)
![Docker container size](https://ghcr-badge.egpl.dev/c4illin/convertx/size?color=%230375b6&tag=latest&label=image+size&trim=)
<a href="https://trendshift.io/repositories/13818" target="_blank"><img src="https://trendshift.io/api/badge/repositories/13818" alt="C4illin%2FConvertX | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<!-- ![Dev image size](https://ghcr-badge.egpl.dev/c4illin/convertx/size?color=%230375b6&tag=main&label=dev+image&trim=) -->
A self-hosted online file converter. Supports over a thousand different formats. Written with TypeScript, Bun and Elysia.
## Features
- Convert files to different formats
- Process multiple files at once
- Password protection
- Multiple accounts
## Converters supported
| Converter | Use case | Converts from | Converts to |
| ------------------------------------------------ | ---------------- | ------------- | ----------- |
| [libjxl](https://github.com/libjxl/libjxl) | JPEG XL | 11 | 11 |
| [resvg](https://github.com/RazrFalcon/resvg) | SVG | 1 | 1 |
| [Vips](https://github.com/libvips/libvips) | Images | 45 | 23 |
| [libheif](https://github.com/strukturag/libheif) | HEIF | 2 | 4 |
| [XeLaTeX](https://tug.org/xetex/) | LaTeX | 1 | 1 |
| [Calibre](https://calibre-ebook.com/) | E-books | 26 | 19 |
| [Pandoc](https://pandoc.org/) | Documents | 43 | 65 |
| [dvisvgm](https://dvisvgm.de/) | Vector images | 4 | 2 |
| [ImageMagick](https://imagemagick.org/) | Images | 245 | 183 |
| [GraphicsMagick](http://www.graphicsmagick.org/) | Images | 167 | 130 |
| [Inkscape](https://inkscape.org/) | Vector images | 7 | 17 |
| [Assimp](https://github.com/assimp/assimp) | 3D Assets | 77 | 23 |
| [FFmpeg](https://ffmpeg.org/) | Video | ~472 | ~199 |
| [Potrace](https://potrace.sourceforge.net/) | Raster to vector | 4 | 11 |
<!-- many ffmpeg fileformats are duplicates -->
Any missing converter? Open an issue or pull request!
## Deployment
> [!WARNING]
> If you can't login, make sure you are accessing the service over localhost or https otherwise set HTTP_ALLOWED=true
```yml
# docker-compose.yml
services:
convertx:
image: ghcr.io/c4illin/convertx
container_name: convertx
restart: unless-stopped
ports:
- "3000:3000"
environment:
- JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234 # will use randomUUID() if unset
volumes:
- ./data:/app/data
```
or
```bash
docker run -p 3000:3000 -v ./data:/app/data ghcr.io/c4illin/convertx
```
Then visit `http://localhost:3000` in your browser and create your account. Don't leave it unconfigured and open, as anyone can register the first account.
If you get unable to open database file run `chown -R $USER:$USER path` on the path you choose.
### Environment variables
All are optional, JWT_SECRET is recommended to be set.
| Name | Default | Description |
| ------------------------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| JWT_SECRET | when unset it will use the value from randomUUID() | A long and secret string used to sign the JSON Web Token |
| ACCOUNT_REGISTRATION | false | Allow users to register accounts |
| HTTP_ALLOWED | false | Allow HTTP connections, only set this to true locally |
| ALLOW_UNAUTHENTICATED | false | Allow unauthenticated users to use the service, only set this to true locally |
| AUTO_DELETE_EVERY_N_HOURS | 24 | Checks every n hours for files older then n hours and deletes them, set to 0 to disable |
| WEBROOT | | The address to the root path setting this to "/convert" will serve the website on "example.com/convert/" |
| FFMPEG_ARGS | | Arguments to pass to ffmpeg, e.g. `-preset veryfast` |
| HIDE_HISTORY | false | Hide the history page |
### 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 |
![Release image size](https://ghcr-badge.egpl.dev/c4illin/convertx/size?color=%230375b6&tag=latest&label=release+image&trim=)
![Dev image size](https://ghcr-badge.egpl.dev/c4illin/convertx/size?color=%230375b6&tag=main&label=dev+image&trim=)
<!-- Dockerhub was introduced in 0.9.0 and older releases -->
### Tutorial
> [!NOTE]
> These are written by other people, and may be outdated, incorrect or wrong.
Tutorial in french: <https://belginux.com/installer-convertx-avec-docker/>
Tutorial in chinese: <https://xzllll.com/24092901/>
## Screenshots
![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.
Use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) for commit messages.
## Todo
- [ ] Add options for converters
- [ ] Add tests
- [ ] Make errors logs visible from the web ui
- [ ] Add more converters:
- [ ] [deark](https://github.com/jsummers/deark)
- [ ] LibreOffice
## Contributors
<a href="https://github.com/C4illin/ConvertX/graphs/contributors">
<img src="https://contrib.rocks/image?repo=C4illin/ConvertX" alt="Image with all contributors"/>
</a>
## Star History
<a href="https://github.com/C4illin/ConvertX/stargazers">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=C4illin/ConvertX&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=C4illin/ConvertX&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=C4illin/ConvertX&type=Date" />
</picture>
</a>

View File

@@ -10,10 +10,7 @@
"attributePosition": "auto"
},
"files": {
"ignore": [
"**/node_modules/**",
"**/pico.lime.min.css"
]
"ignore": ["**/node_modules/**", "**/pico.lime.min.css"]
},
"organizeImports": {
"enabled": true
@@ -72,4 +69,4 @@
"attributePosition": "auto"
}
}
}
}

714
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,19 @@
services:
convertx:
build:
context: .
# dockerfile: Debian.Dockerfile
volumes:
- ./data:/app/data
environment: # Defaults are listed below. All are optional.
- ACCOUNT_REGISTRATION=true # true or false, doesn't matter for the first account (e.g. keep this to false if you only want one account)
- JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234 # will use randomUUID() by default
- HTTP_ALLOWED=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
# - HIDE_HISTORY=true # hides the history tab in the web interface, defaults to false
ports:
- 3000:3000
services:
convertx:
build:
context: .
# dockerfile: Debian.Dockerfile
volumes:
- ./data:/app/data
environment: # Defaults are listed below. All are optional.
- ACCOUNT_REGISTRATION=true # true or false, doesn't matter for the first account (e.g. keep this to false if you only want one account)
- JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234 # will use randomUUID() by default
- HTTP_ALLOWED=false # setting this to true is unsafe, only set this to true locally
- ALLOW_UNAUTHENTICATED=false # allows anyone to use the service without logging in, only set this to true locally
- AUTO_DELETE_EVERY_N_HOURS=1 # checks every n hours for files older then n hours and deletes them, set to 0 to disable
# - FFMPEG_ARGS=-hwaccel vulkan # additional arguments to pass to ffmpeg
# - WEBROOT=/convertx # the root path of the web interface, leave empty to disable
# - HIDE_HISTORY=true # hides the history tab in the web interface, defaults to false
- TZ=Europe/Stockholm # set your timezone, defaults to UTC
ports:
- 3000:3000

View File

@@ -1,57 +1,66 @@
import js from "@eslint/js";
import eslintParserTypeScript from "@typescript-eslint/parser";
import type { Linter } from "eslint";
import eslintPluginReadableTailwind from "eslint-plugin-readable-tailwind";
import simpleImportSortPlugin from "eslint-plugin-simple-import-sort";
import globals from "globals";
import tseslint from "typescript-eslint";
export default [
js.configs.recommended,
...tseslint.configs.recommended,
// ...tailwind.configs["flat/recommended"],
{
plugins: {
"simple-import-sort": simpleImportSortPlugin,
"readable-tailwind": eslintPluginReadableTailwind,
},
ignores: ["**/node_modules/**"],
languageOptions: {
parser: eslintParserTypeScript,
parserOptions: {
project: true,
ecmaFeatures: {
jsx: true,
},
},
globals: {
...globals.node,
...globals.browser,
},
},
files: ["**/*.{js,mjs,cjs,jsx,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",
// ],
// },
// ],
},
},
] as Linter.Config[];
import js from "@eslint/js";
import eslintParserTypeScript from "@typescript-eslint/parser";
import type { Linter } from "eslint";
import eslintPluginBetterTailwindcss from "eslint-plugin-better-tailwindcss";
import simpleImportSortPlugin from "eslint-plugin-simple-import-sort";
import globals from "globals";
import tseslint from "typescript-eslint";
export default [
js.configs.recommended,
...tseslint.configs.recommended,
// ...tailwind.configs["flat/recommended"],
{
plugins: {
"simple-import-sort": simpleImportSortPlugin,
"better-tailwindcss": eslintPluginBetterTailwindcss,
},
ignores: ["**/node_modules/**"],
languageOptions: {
parser: eslintParserTypeScript,
parserOptions: {
project: true,
ecmaFeatures: {
jsx: true,
},
},
globals: {
...globals.node,
...globals.browser,
},
},
files: ["**/*.{js,mjs,cjs,jsx,tsx,ts}"],
settings: {
"better-tailwindcss": {
entryPoint: "src/main.css",
},
},
rules: {
...(eslintPluginBetterTailwindcss.configs["recommended-warn"] ?? {}).rules,
...(eslintPluginBetterTailwindcss.configs["stylistic-warn"] ?? {}).rules,
// "tailwindcss/classnames-order": "off",
"better-tailwindcss/multiline": [
"warn",
{
group: "newLine",
printWidth: 100,
},
],
"better-tailwindcss/no-unregistered-classes": [
"warn",
{
ignore: [
"^group(?:\\/(\\S*))?$",
"^peer(?:\\/(\\S*))?$",
"select_container",
"convert_to_popup",
"convert_to_group",
"target",
"convert_to_target",
"job-details-toggle",
],
},
],
},
},
] as Linter.Config[];

9
knip.json Normal file
View File

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

View File

@@ -1,22 +1,25 @@
{
"name": "convertx-frontend",
"version": "0.13.0",
"version": "0.14.1",
"scripts": {
"dev": "bun run --watch src/index.tsx",
"hot": "bun run --hot src/index.tsx",
"format": "eslint --fix .",
"build": "bunx @tailwindcss/cli -i ./src/main.css -o ./public/generated.css",
"format": "run-p 'format:*'",
"format:eslint": "eslint --fix .",
"format:prettier": "prettier --write .",
"build": "bun x @tailwindcss/cli -i ./src/main.css -o ./public/generated.css",
"lint": "run-p 'lint:*'",
"lint:tsc": "tsc --noEmit",
"lint:knip": "knip",
"lint:eslint": "eslint ."
"lint:eslint": "eslint .",
"lint:prettier": "prettier --check ."
},
"dependencies": {
"@elysiajs/html": "^1.3.0",
"@elysiajs/jwt": "^1.3.0",
"@elysiajs/jwt": "^1.3.1",
"@elysiajs/static": "^1.3.0",
"@kitajs/html": "^4.2.9",
"elysia": "^1.3.1",
"elysia": "^1.3.4",
"sanitize-filename": "^1.6.3"
},
"module": "src/index.tsx",
@@ -25,30 +28,30 @@
"start": "bun run src/index.tsx"
},
"devDependencies": {
"@eslint/js": "^9.26.0",
"@ianvs/prettier-plugin-sort-imports": "^4.4.1",
"@eslint/js": "^9.28.0",
"@ianvs/prettier-plugin-sort-imports": "^4.4.2",
"@kitajs/ts-html-plugin": "^4.1.1",
"@tailwindcss/cli": "^4.1.6",
"@tailwindcss/postcss": "^4.1.6",
"@tailwindcss/cli": "^4.1.8",
"@tailwindcss/postcss": "^4.1.8",
"@total-typescript/ts-reset": "^0.6.1",
"@types/bun": "^1.2.13",
"@types/eslint-plugin-tailwindcss": "^3.17.0",
"@types/node": "^22.15.17",
"autoprefixer": "^10.4.21",
"cssnano": "^7.0.7",
"eslint": "^9.26.0",
"eslint-plugin-readable-tailwind": "^2.1.1",
"@types/bun": "^1.2.15",
"@types/node": "^22.15.29",
"@typescript-eslint/parser": "^8.33.1",
"eslint": "^9.28.0",
"eslint-plugin-better-tailwindcss": "^3.1.0",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-tailwindcss": "4.0.0-alpha.0",
"globals": "^16.1.0",
"knip": "^5.55.1",
"npm-run-all2": "^8.0.1",
"postcss": "^8.5.3",
"postcss-cli": "^11.0.1",
"globals": "^16.2.0",
"knip": "^5.59.1",
"npm-run-all2": "^8.0.4",
"postcss": "^8.5.4",
"prettier": "^3.5.3",
"tailwind-scrollbar": "^4.0.2",
"tailwindcss": "^4.1.6",
"tailwindcss": "^4.1.8",
"typescript": "^5.8.3",
"typescript-eslint": "^8.32.0"
}
}
"typescript-eslint": "^8.33.1"
},
"trustedDependencies": [
"@parcel/watcher",
"@tailwindcss/oxide"
]
}

View File

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

View File

@@ -3,7 +3,7 @@
*/
const config = {
arrowParens: "always",
printWidth: 80,
printWidth: 100,
singleQuote: false,
semi: true,
tabWidth: 2,

View File

@@ -17,20 +17,20 @@ dropZone.addEventListener("dragleave", () => {
});
dropZone.addEventListener("drop", (e) => {
e.preventDefault();
dropZone.classList.remove("dragover");
const files = e.dataTransfer.files;
if (files.length === 0) {
console.warn("No files dropped — likely a URL or unsupported source.");
return;
}
for (const file of files) {
console.log("Handling dropped file:", file.name);
handleFile(file);
}
e.preventDefault();
dropZone.classList.remove("dragover");
const files = e.dataTransfer.files;
if (files.length === 0) {
console.warn("No files dropped — likely a URL or unsupported source.");
return;
}
for (const file of files) {
console.log("Handling dropped file:", file.name);
handleFile(file);
}
});
// Extracted handleFile function for reusability in drag-and-drop and file input
@@ -40,7 +40,7 @@ function handleFile(file) {
const row = document.createElement("tr");
row.innerHTML = `
<td>${file.name}</td>
<td><progress max="100"></progress></td>
<td><progress max="100" class="inline-block h-2 appearance-none overflow-hidden rounded-full border-0 bg-neutral-700 bg-none text-accent-500 accent-accent-500 [&::-moz-progress-bar]:bg-accent-500 [&::-webkit-progress-value]:rounded-full [&::-webkit-progress-value]:[background:none] [&[value]::-webkit-progress-value]:bg-accent-500 [&[value]::-webkit-progress-value]:transition-[inline-size]"></progress></td>
<td>${(file.size / 1024).toFixed(2)} kB</td>
<td><a onclick="deleteRow(this)">Remove</a></td>
`;
@@ -72,9 +72,7 @@ function handleFile(file) {
const selectContainer = document.querySelector("form .select_container");
const updateSearchBar = () => {
const convertToInput = document.querySelector(
"input[name='convert_to_search']",
);
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 = {};
@@ -195,8 +193,7 @@ const deleteRow = (target) => {
headers: {
"Content-Type": "application/json",
},
})
.catch((err) => console.log(err));
}).catch((err) => console.log(err));
};
const uploadFile = (file) => {
@@ -207,7 +204,7 @@ const uploadFile = (file) => {
const formData = new FormData();
formData.append("file", file, file.name);
let xhr = new XMLHttpRequest();
let xhr = new XMLHttpRequest();
xhr.open("POST", `${webroot}/upload`, true);
@@ -234,7 +231,7 @@ const uploadFile = (file) => {
console.log(`upload progress (${file.name}):`, (100 * sent) / total);
let progressbar = file.htmlRow.getElementsByTagName("progress");
progressbar[0].value = ((100 * sent) / total);
progressbar[0].value = (100 * sent) / total;
};
xhr.onerror = (e) => {

View File

@@ -1,11 +1,8 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
":disableDependencyDashboard"
],
"extends": ["config:recommended", ":disableDependencyDashboard"],
"lockFileMaintenance": {
"enabled": true,
"automerge": true
}
}
}

2
reset.d.ts vendored
View File

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

View File

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

View File

@@ -30,6 +30,19 @@ export const Header = ({
</a>
</li>
)}
{!allowUnauthenticated ? (
<li>
<a
class={`
text-accent-600 transition-all
hover:text-accent-500 hover:underline
`}
href={`${webroot}/account`}
>
Account
</a>
</li>
) : null}
{!allowUnauthenticated ? (
<li>
<a

View File

@@ -1,139 +1,139 @@
import { execFile } from "node:child_process";
export const properties = {
from: {
object: [
"3d",
"3ds",
"3mf",
"ac",
"ac3d",
"acc",
"amf",
"amj",
"ase",
"ask",
"assbin",
"b3d",
"blend",
"bsp",
"bvh",
"cob",
"csm",
"dae",
"dxf",
"enff",
"fbx",
"glb",
"gltf",
"hmb",
"hmp",
"ifc",
"ifczip",
"iqm",
"irr",
"irrmesh",
"lwo",
"lws",
"lxo",
"m3d",
"md2",
"md3",
"md5anim",
"md5camera",
"md5mesh",
"mdc",
"mdl",
"mesh.xml",
"mesh",
"mot",
"ms3d",
"ndo",
"nff",
"obj",
"off",
"ogex",
"pk3",
"ply",
"pmx",
"prj",
"q3o",
"q3s",
"raw",
"scn",
"sib",
"smd",
"step",
"stl",
"stp",
"ter",
"uc",
"usd",
"usda",
"usdc",
"usdz",
"vta",
"x",
"x3d",
"x3db",
"xgl",
"xml",
"zae",
"zgl",
],
},
to: {
object: [
"3ds",
"3mf",
"assbin",
"assjson",
"assxml",
"collada",
"dae",
"fbx",
"fbxa",
"glb",
"glb2",
"gltf",
"gltf2",
"json",
"obj",
"objnomtl",
"pbrt",
"ply",
"plyb",
"stl",
"stlb",
"stp",
"x",
],
},
};
export async function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
options?: unknown,
): Promise<string> {
return new Promise((resolve, reject) => {
execFile("assimp", ["export", filePath, targetPath], (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
});
});
}
import { execFile } from "node:child_process";
export const properties = {
from: {
object: [
"3d",
"3ds",
"3mf",
"ac",
"ac3d",
"acc",
"amf",
"amj",
"ase",
"ask",
"assbin",
"b3d",
"blend",
"bsp",
"bvh",
"cob",
"csm",
"dae",
"dxf",
"enff",
"fbx",
"glb",
"gltf",
"hmb",
"hmp",
"ifc",
"ifczip",
"iqm",
"irr",
"irrmesh",
"lwo",
"lws",
"lxo",
"m3d",
"md2",
"md3",
"md5anim",
"md5camera",
"md5mesh",
"mdc",
"mdl",
"mesh.xml",
"mesh",
"mot",
"ms3d",
"ndo",
"nff",
"obj",
"off",
"ogex",
"pk3",
"ply",
"pmx",
"prj",
"q3o",
"q3s",
"raw",
"scn",
"sib",
"smd",
"step",
"stl",
"stp",
"ter",
"uc",
"usd",
"usda",
"usdc",
"usdz",
"vta",
"x",
"x3d",
"x3db",
"xgl",
"xml",
"zae",
"zgl",
],
},
to: {
object: [
"3ds",
"3mf",
"assbin",
"assjson",
"assxml",
"collada",
"dae",
"fbx",
"fbxa",
"glb",
"glb2",
"gltf",
"gltf2",
"json",
"obj",
"objnomtl",
"pbrt",
"ply",
"plyb",
"stl",
"stlb",
"stp",
"x",
],
},
};
export async function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
options?: unknown,
): Promise<string> {
return new Promise((resolve, reject) => {
execFile("assimp", ["export", filePath, targetPath], (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
});
});
}

View File

@@ -39,6 +39,7 @@ export const properties = {
"fb2",
"html",
"htmlz",
"kepub.epub",
"lit",
"lrf",
"mobi",
@@ -65,20 +66,24 @@ export async function convert(
options?: unknown,
): Promise<string> {
return new Promise((resolve, reject) => {
execFile("ebook-convert", [filePath, targetPath], (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
execFile(
"ebook-convert",
[filePath, targetPath],
(error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
});
resolve("Done");
},
);
});
}

48
src/converters/dvisvgm.ts Normal file
View File

@@ -0,0 +1,48 @@
import { execFile } from "node:child_process";
export const properties = {
from: {
images: ["dvi", "xdv", "pdf", "eps"],
},
to: {
images: ["svg", "svgz"],
},
};
export function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
options?: unknown,
): Promise<string> {
const inputArgs: string[] = [];
if (fileType === "eps") {
inputArgs.push("--eps");
}
if (fileType === "pdf") {
inputArgs.push("--pdf");
}
if (convertTo === "svgz") {
inputArgs.push("-z");
}
return new Promise((resolve, reject) => {
execFile("dvisvgm", [...inputArgs, filePath, "-o", targetPath], (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
});
});
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,340 +1,336 @@
import { execFile } from "node:child_process";
export const properties = {
from: {
image: [
"3fr",
"8bim",
"8bimtext",
"8bimwtext",
"app1",
"app1jpeg",
"art",
"arw",
"avs",
"b",
"bie",
"bigtiff",
"bmp",
"c",
"cals",
"caption",
"cin",
"cmyk",
"cmyka",
"cr2",
"crw",
"cur",
"cut",
"dcm",
"dcr",
"dcx",
"dng",
"dpx",
"epdf",
"epi",
"eps",
"epsf",
"epsi",
"ept",
"ept2",
"ept3",
"erf",
"exif",
"fax",
"file",
"fits",
"fractal",
"ftp",
"g",
"gif",
"gif87",
"gradient",
"gray",
"graya",
"heic",
"heif",
"hrz",
"http",
"icb",
"icc",
"icm",
"ico",
"icon",
"identity",
"image",
"iptc",
"iptctext",
"iptcwtext",
"jbg",
"jbig",
"jng",
"jnx",
"jpeg",
"jpg",
"k",
"k25",
"kdc",
"label",
"m",
"mac",
"map",
"mat",
"mef",
"miff",
"mng",
"mono",
"mpc",
"mrw",
"msl",
"mtv",
"mvg",
"nef",
"null",
"o",
"orf",
"otb",
"p7",
"pal",
"palm",
"pam",
"pbm",
"pcd",
"pcds",
"pct",
"pcx",
"pdb",
"pdf",
"pef",
"pfa",
"pfb",
"pgm",
"picon",
"pict",
"pix",
"plasma",
"png",
"png00",
"png24",
"png32",
"png48",
"png64",
"png8",
"pnm",
"ppm",
"ps",
"ptif",
"pwp",
"r",
"raf",
"ras",
"rgb",
"rgba",
"rla",
"rle",
"sct",
"sfw",
"sgi",
"sr2",
"srf",
"stegano",
"sun",
"svg",
"svgz",
"text",
"tga",
"tif",
"tiff",
"tile",
"tim",
"topol",
"ttf",
"txt",
"uyvy",
"vda",
"vicar",
"vid",
"viff",
"vst",
"wbmp",
"webp",
"wmf",
"wpg",
"x3f",
"xbm",
"xc",
"xcf",
"xmp",
"xpm",
"xv",
"xwd",
"y",
"yuv",
],
},
to: {
image: [
"8bim",
"8bimtext",
"8bimwtext",
"app1",
"app1jpeg",
"art",
"avs",
"b",
"bie",
"bigtiff",
"bmp",
"bmp2",
"bmp3",
"brf",
"c",
"cals",
"cin",
"cmyk",
"cmyka",
"dcx",
"dpx",
"epdf",
"epi",
"eps",
"eps2",
"eps3",
"epsf",
"epsi",
"ept",
"ept2",
"ept3",
"exif",
"fax",
"fits",
"g",
"gif",
"gif87",
"gray",
"graya",
"histogram",
"html",
"icb",
"icc",
"icm",
"info",
"iptc",
"iptctext",
"iptcwtext",
"isobrl",
"isobrl6",
"jbg",
"jbig",
"jng",
"jpeg",
"k",
"m",
"m2v",
"map",
"mat",
"matte",
"miff",
"mng",
"mono",
"mpc",
"mpeg",
"mpg",
"msl",
"mtv",
"mvg",
"null",
"o",
"otb",
"p7",
"pal",
"pam",
"pbm",
"pcd",
"pcds",
"pcl",
"pct",
"pcx",
"pdb",
"pdf",
"pgm",
"picon",
"pict",
"png",
"png00",
"png24",
"png32",
"png48",
"png64",
"png8",
"pnm",
"ppm",
"preview",
"ps",
"ps2",
"ps3",
"ptif",
"r",
"ras",
"rgb",
"rgba",
"sgi",
"shtml",
"sun",
"text",
"tga",
"tiff",
"txt",
"ubrl",
"ubrl6",
"uil",
"uyvy",
"vda",
"vicar",
"vid",
"viff",
"vst",
"wbmp",
"webp",
"x",
"xbm",
"xmp",
"xpm",
"xv",
"xwd",
"y",
"yuv",
],
},
};
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) => {
execFile(
"gm",
["convert", filePath, targetPath],
(error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
},
);
});
}
import { execFile } from "node:child_process";
export const properties = {
from: {
image: [
"3fr",
"8bim",
"8bimtext",
"8bimwtext",
"app1",
"app1jpeg",
"art",
"arw",
"avs",
"b",
"bie",
"bigtiff",
"bmp",
"c",
"cals",
"caption",
"cin",
"cmyk",
"cmyka",
"cr2",
"crw",
"cur",
"cut",
"dcm",
"dcr",
"dcx",
"dng",
"dpx",
"epdf",
"epi",
"eps",
"epsf",
"epsi",
"ept",
"ept2",
"ept3",
"erf",
"exif",
"fax",
"file",
"fits",
"fractal",
"ftp",
"g",
"gif",
"gif87",
"gradient",
"gray",
"graya",
"heic",
"heif",
"hrz",
"http",
"icb",
"icc",
"icm",
"ico",
"icon",
"identity",
"image",
"iptc",
"iptctext",
"iptcwtext",
"jbg",
"jbig",
"jng",
"jnx",
"jpeg",
"jpg",
"k",
"k25",
"kdc",
"label",
"m",
"mac",
"map",
"mat",
"mef",
"miff",
"mng",
"mono",
"mpc",
"mrw",
"msl",
"mtv",
"mvg",
"nef",
"null",
"o",
"orf",
"otb",
"p7",
"pal",
"palm",
"pam",
"pbm",
"pcd",
"pcds",
"pct",
"pcx",
"pdb",
"pdf",
"pef",
"pfa",
"pfb",
"pgm",
"picon",
"pict",
"pix",
"plasma",
"png",
"png00",
"png24",
"png32",
"png48",
"png64",
"png8",
"pnm",
"ppm",
"ps",
"ptif",
"pwp",
"r",
"raf",
"ras",
"rgb",
"rgba",
"rla",
"rle",
"sct",
"sfw",
"sgi",
"sr2",
"srf",
"stegano",
"sun",
"svg",
"svgz",
"text",
"tga",
"tif",
"tiff",
"tile",
"tim",
"topol",
"ttf",
"txt",
"uyvy",
"vda",
"vicar",
"vid",
"viff",
"vst",
"wbmp",
"webp",
"wmf",
"wpg",
"x3f",
"xbm",
"xc",
"xcf",
"xmp",
"xpm",
"xv",
"xwd",
"y",
"yuv",
],
},
to: {
image: [
"8bim",
"8bimtext",
"8bimwtext",
"app1",
"app1jpeg",
"art",
"avs",
"b",
"bie",
"bigtiff",
"bmp",
"bmp2",
"bmp3",
"brf",
"c",
"cals",
"cin",
"cmyk",
"cmyka",
"dcx",
"dpx",
"epdf",
"epi",
"eps",
"eps2",
"eps3",
"epsf",
"epsi",
"ept",
"ept2",
"ept3",
"exif",
"fax",
"fits",
"g",
"gif",
"gif87",
"gray",
"graya",
"histogram",
"html",
"icb",
"icc",
"icm",
"info",
"iptc",
"iptctext",
"iptcwtext",
"isobrl",
"isobrl6",
"jbg",
"jbig",
"jng",
"jpeg",
"k",
"m",
"m2v",
"map",
"mat",
"matte",
"miff",
"mng",
"mono",
"mpc",
"mpeg",
"mpg",
"msl",
"mtv",
"mvg",
"null",
"o",
"otb",
"p7",
"pal",
"pam",
"pbm",
"pcd",
"pcds",
"pcl",
"pct",
"pcx",
"pdb",
"pdf",
"pgm",
"picon",
"pict",
"png",
"png00",
"png24",
"png32",
"png48",
"png64",
"png8",
"pnm",
"ppm",
"preview",
"ps",
"ps2",
"ps3",
"ptif",
"r",
"ras",
"rgb",
"rgba",
"sgi",
"shtml",
"sun",
"text",
"tga",
"tiff",
"txt",
"ubrl",
"ubrl6",
"uil",
"uyvy",
"vda",
"vicar",
"vid",
"viff",
"vst",
"wbmp",
"webp",
"x",
"xbm",
"xmp",
"xpm",
"xv",
"xwd",
"y",
"yuv",
],
},
};
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) => {
execFile("gm", ["convert", filePath, targetPath], (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
});
});
}

View File

@@ -0,0 +1,484 @@
import { execFile } from "node:child_process";
// declare possible conversions
export const properties = {
from: {
images: [
"3fr",
"3g2",
"3gp",
"aai",
"ai",
"apng",
"art",
"arw",
"avci",
"avi",
"avif",
"avs",
"bayer",
"bayera",
"bgr",
"bgra",
"bgro",
"bmp",
"bmp2",
"bmp3",
"cal",
"cals",
"canvas",
"caption",
"cin",
"clip",
"clipboard",
"cmyk",
"cmyka",
"cr2",
"cr3",
"crw",
"cube",
"cur",
"cut",
"data",
"dcm",
"dcr",
"dcraw",
"dcx",
"dds",
"dfont",
"dng",
"dpx",
"dxt1",
"dxt5",
"emf",
"epdf",
"epi",
"eps",
"epsf",
"epsi",
"ept",
"ept2",
"ept3",
"erf",
"exr",
"farbfeld",
"fax",
"ff",
"fff",
"file",
"fits",
"fl32",
"flif",
"flv",
"fractal",
"ftp",
"fts",
"ftxt",
"g3",
"g4",
"gif",
"gif87",
"gradient",
"gray",
"graya",
"group4",
"hald",
"hdr",
"heic",
"heif",
"hrz",
"http",
"https",
"icb",
"ico",
"icon",
"iiq",
"inline",
"ipl",
"j2c",
"j2k",
"jng",
"jnx",
"jp2",
"jpc",
"jpe",
"jpeg",
"jpg",
"jpm",
"jps",
"jpt",
"jxl",
"k25",
"kdc",
"label",
"m2v",
"m4v",
"mac",
"map",
"mask",
"mat",
"mdc",
"mef",
"miff",
"mkv",
"mng",
"mono",
"mos",
"mov",
"mp4",
"mpc",
"mpeg",
"mpg",
"mpo",
"mrw",
"msl",
"msvg",
"mtv",
"mvg",
"nef",
"nrw",
"null",
"ora",
"orf",
"otb",
"otf",
"pal",
"palm",
"pam",
"pango",
"pattern",
"pbm",
"pcd",
"pcds",
"pcl",
"pct",
"pcx",
"pdb",
"pdf",
"pdfa",
"pef",
"pes",
"pfa",
"pfb",
"pfm",
"pgm",
"pgx",
"phm",
"picon",
"pict",
"pix",
"pjpeg",
"plasma",
"png",
"png00",
"png24",
"png32",
"png48",
"png64",
"png8",
"pnm",
"pocketmod",
"ppm",
"ps",
"psb",
"psd",
"ptif",
"pwp",
"qoi",
"radial",
"raf",
"ras",
"raw",
"rgb",
"rgb565",
"rgba",
"rgbo",
"rgf",
"rla",
"rle",
"rmf",
"rsvg",
"rw2",
"rwl",
"scr",
"screenshot",
"sct",
"sfw",
"sgi",
"six",
"sixel",
"sr2",
"srf",
"srw",
"stegano",
"sti",
"strimg",
"sun",
"svg",
"svgz",
"text",
"tga",
"tiff",
"tiff64",
"tile",
"tim",
"tm2",
"ttc",
"ttf",
"txt",
"uyvy",
"vda",
"vicar",
"vid",
"viff",
"vips",
"vst",
"wbmp",
"webm",
"webp",
"wmf",
"wmv",
"wpg",
"x3f",
"xbm",
"xc",
"xcf",
"xpm",
"xps",
"xv",
"ycbcr",
"ycbcra",
"yuv",
],
},
to: {
images: [
"aai",
"ai",
"apng",
"art",
"ashlar",
"avif",
"avs",
"bayer",
"bayera",
"bgr",
"bgra",
"bgro",
"bmp",
"bmp2",
"bmp3",
"brf",
"cal",
"cals",
"cin",
"cip",
"clip",
"clipboard",
"cmyk",
"cmyka",
"cur",
"data",
"dcx",
"dds",
"dpx",
"dxt1",
"dxt5",
"epdf",
"epi",
"eps",
"eps2",
"eps3",
"epsf",
"epsi",
"ept",
"ept2",
"ept3",
"exr",
"farbfeld",
"fax",
"ff",
"fits",
"fl32",
"flif",
"flv",
"fts",
"ftxt",
"g3",
"g4",
"gif",
"gif87",
"gray",
"graya",
"group4",
"hdr",
"histogram",
"hrz",
"htm",
"html",
"icb",
"ico",
"icon",
"info",
"inline",
"ipl",
"isobrl",
"isobrl6",
"j2c",
"j2k",
"jng",
"jp2",
"jpc",
"jpe",
"jpeg",
"jpg",
"jpm",
"jps",
"jpt",
"json",
"jxl",
"m2v",
"m4v",
"map",
"mask",
"mat",
"matte",
"miff",
"mkv",
"mng",
"mono",
"mov",
"mp4",
"mpc",
"mpeg",
"mpg",
"msl",
"msvg",
"mtv",
"mvg",
"null",
"otb",
"pal",
"palm",
"pam",
"pbm",
"pcd",
"pcds",
"pcl",
"pct",
"pcx",
"pdb",
"pdf",
"pdfa",
"pfm",
"pgm",
"pgx",
"phm",
"picon",
"pict",
"pjpeg",
"png",
"png00",
"png24",
"png32",
"png48",
"png64",
"png8",
"pnm",
"pocketmod",
"ppm",
"ps",
"ps2",
"ps3",
"psb",
"psd",
"ptif",
"qoi",
"ras",
"rgb",
"rgba",
"rgbo",
"rgf",
"rsvg",
"sgi",
"shtml",
"six",
"sixel",
"sparse",
"strimg",
"sun",
"svg",
"svgz",
"tga",
"thumbnail",
"tiff",
"tiff64",
"txt",
"ubrl",
"ubrl6",
"uil",
"uyvy",
"vda",
"vicar",
"vid",
"viff",
"vips",
"vst",
"wbmp",
"webm",
"webp",
"wmv",
"wpg",
"xbm",
"xpm",
"xv",
"yaml",
"ycbcr",
"ycbcra",
"yuv",
],
},
};
export function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
options?: unknown,
): Promise<string> {
let outputArgs: string[] = [];
let inputArgs: string[] = [];
if (convertTo === "ico") {
outputArgs = ["-define", "icon:auto-resize=256,128,64,48,32,16", "-background", "none"];
if (fileType === "svg") {
// this might be a bit too much, but it works
inputArgs = ["-background", "none", "-density", "512"];
}
}
return new Promise((resolve, reject) => {
execFile(
"magick",
[...inputArgs, filePath, ...outputArgs, targetPath],
(error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
},
);
});
}

View File

@@ -36,24 +36,20 @@ export function convert(
options?: unknown,
): Promise<string> {
return new Promise((resolve, reject) => {
execFile(
"inkscape",
[filePath, "-o", targetPath],
(error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
execFile("inkscape", [filePath, "-o", targetPath], (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
},
);
resolve("Done");
});
});
}

View File

@@ -2,19 +2,7 @@ import { execFile } from "child_process";
export const properties = {
from: {
images: [
"avci",
"avcs",
"avif",
"h264",
"heic",
"heics",
"heif",
"heifs",
"hif",
"mkv",
"mp4",
],
images: ["avci", "avcs", "avif", "h264", "heic", "heics", "heif", "heifs", "hif", "mkv", "mp4"],
},
to: {
images: ["jpeg", "png", "y4m"],
@@ -30,24 +18,20 @@ export function convert(
options?: unknown,
): Promise<string> {
return new Promise((resolve, reject) => {
execFile(
"heif-convert",
[filePath, targetPath],
(error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
execFile("heif-convert", [filePath, targetPath], (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
},
);
resolve("Done");
});
});
}

View File

@@ -1,71 +1,49 @@
import { execFile } from "node:child_process";
// declare possible conversions
export const properties = {
from: {
jxl: ["jxl"],
images: [
"apng",
"exr",
"gif",
"jpeg",
"pam",
"pfm",
"pgm",
"pgx",
"png",
"ppm",
],
},
to: {
jxl: [
"apng",
"exr",
"gif",
"jpeg",
"pam",
"pfm",
"pgm",
"pgx",
"png",
"ppm",
],
images: ["jxl"],
},
};
export function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
options?: unknown,
): Promise<string> {
let tool = "";
if (fileType === "jxl") {
tool = "djxl";
}
if (convertTo === "jxl") {
tool = "cjxl";
}
return new Promise((resolve, reject) => {
execFile(tool, [filePath, targetPath], (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
});
});
}
import { execFile } from "node:child_process";
// declare possible conversions
export const properties = {
from: {
jxl: ["jxl"],
images: ["apng", "exr", "gif", "jpeg", "pam", "pfm", "pgm", "pgx", "png", "ppm"],
},
to: {
jxl: ["apng", "exr", "gif", "jpeg", "pam", "pfm", "pgm", "pgx", "png", "ppm"],
images: ["jxl"],
},
};
export function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
options?: unknown,
): Promise<string> {
let tool = "";
if (fileType === "jxl") {
tool = "djxl";
}
if (convertTo === "jxl") {
tool = "cjxl";
}
return new Promise((resolve, reject) => {
execFile(tool, [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,17 +1,21 @@
import { normalizeFiletype } from "../helpers/normalizeFiletype";
import { convert as convertassimp, properties as propertiesassimp } from "./assimp";
import { convert as convertCalibre, properties as propertiesCalibre } from "./calibre";
import { convert as convertDvisvgm, properties as propertiesDvisvgm } from "./dvisvgm";
import { convert as convertFFmpeg, properties as propertiesFFmpeg } from "./ffmpeg";
import { convert as convertGraphicsmagick, properties as propertiesGraphicsmagick } from "./graphicsmagick";
import {
convert as convertGraphicsmagick,
properties as propertiesGraphicsmagick,
} from "./graphicsmagick";
import { convert as convertImagemagick, properties as propertiesImagemagick } from "./imagemagick";
import { convert as convertInkscape, properties as propertiesInkscape } from "./inkscape";
import { convert as convertLibheif, properties as propertiesLibheif } from "./libheif";
import { convert as convertLibjxl, properties as propertiesLibjxl } from "./libjxl";
import { convert as convertPandoc, properties as propertiesPandoc } from "./pandoc";
import { convert as convertPotrace, properties as propertiesPotrace } from "./potrace";
import { convert as convertresvg, properties as propertiesresvg } from "./resvg";
import { convert as convertImage, properties as propertiesImage } from "./vips";
import { convert as convertxelatex, properties as propertiesxelatex } from "./xelatex";
// import { convert as convertCalibre, properties as propertiesCalibre } from "./calibre";
import { convert as convertLibheif, properties as propertiesLibheif } from "./libheif";
import { convert as convertpotrace, properties as propertiespotrace } from "./potrace";
// This should probably be reconstructed so that the functions are not imported instead the functions hook into this to make the converters more modular
@@ -63,14 +67,22 @@ const properties: Record<
properties: propertiesxelatex,
converter: convertxelatex,
},
// calibre: {
// properties: propertiesCalibre,
// converter: convertCalibre,
// },
calibre: {
properties: propertiesCalibre,
converter: convertCalibre,
},
pandoc: {
properties: propertiesPandoc,
converter: convertPandoc,
},
dvisvgm: {
properties: propertiesDvisvgm,
converter: convertDvisvgm,
},
imagemagick: {
properties: propertiesImagemagick,
converter: convertImagemagick,
},
graphicsmagick: {
properties: propertiesGraphicsmagick,
converter: convertGraphicsmagick,
@@ -88,8 +100,8 @@ const properties: Record<
converter: convertFFmpeg,
},
potrace: {
properties: propertiespotrace,
converter: convertpotrace,
properties: propertiesPotrace,
converter: convertPotrace,
},
};
@@ -103,7 +115,7 @@ export async function mainConverter(
) {
const fileType = normalizeFiletype(fileTypeOriginal);
let converterFunc: typeof properties["libjxl"]["converter"] | undefined;
let converterFunc: (typeof properties)["libjxl"]["converter"] | undefined;
if (converterName) {
converterFunc = properties[converterName]?.converter;
@@ -129,20 +141,12 @@ export async function mainConverter(
}
if (!converterFunc) {
console.log(
`No available converter supports converting from ${fileType} to ${convertTo}.`,
);
console.log(`No available converter supports converting from ${fileType} to ${convertTo}.`);
return "File type not supported";
}
try {
const result = await converterFunc(
inputFilePath,
fileType,
convertTo,
targetPath,
options,
);
const result = await converterFunc(inputFilePath, fileType, convertTo, targetPath, options);
console.log(
`Converted ${inputFilePath} from ${fileType} to ${convertTo} successfully using ${converterName}.`,
@@ -182,8 +186,7 @@ for (const converterName in properties) {
possibleTargets[extension] = {};
}
possibleTargets[extension][converterName] =
converterProperties.to[key] || [];
possibleTargets[extension][converterName] = converterProperties.to[key] || [];
}
}
}
@@ -284,4 +287,4 @@ export const getAllInputs = (converter: string) => {
// }
// // print the number of unique Inputs and Outputs
// console.log(`Unique Formats: ${uniqueFormats.size}`);
// console.log(`Unique Formats: ${uniqueFormats.size}`);

View File

@@ -1,162 +1,162 @@
import { execFile } from "node:child_process";
export const properties = {
from: {
text: [
"textile",
"tikiwiki",
"tsv",
"twiki",
"typst",
"vimwiki",
"biblatex",
"bibtex",
"bits",
"commonmark",
"commonmark_x",
"creole",
"csljson",
"csv",
"djot",
"docbook",
"docx",
"dokuwiki",
"endnotexml",
"epub",
"fb2",
"gfm",
"haddock",
"html",
"ipynb",
"jats",
"jira",
"json",
"latex",
"man",
"markdown",
"markdown_mmd",
"markdown_phpextra",
"markdown_strict",
"mediawiki",
"muse",
"pandoc native",
"opml",
"org",
"ris",
"rst",
"rtf",
"t2t",
],
},
to: {
text: [
"tei",
"texinfo",
"textile",
"typst",
"xwiki",
"zimwiki",
"asciidoc",
"asciidoc_legacy",
"asciidoctor",
"beamer",
"biblatex",
"bibtex",
"chunkedhtml",
"commonmark",
"commonmark_x",
"context",
"csljson",
"djot",
"docbook",
"docbook4",
"docbook5",
"docx",
"dokuwiki",
"dzslides",
"epub",
"epub2",
"epub3",
"fb2",
"gfm",
"haddock",
"html",
"html4",
"html5",
"icml",
"ipynb",
"jats",
"jats_archiving",
"jats_articleauthoring",
"jats_publishing",
"jira",
"json",
"latex",
"man",
"markdown",
"markdown_mmd",
"markdown_phpextra",
"markdown_strict",
"markua",
"mediawiki",
"ms",
"muse",
"pandoc native",
"odt",
"opendocument",
"opml",
"org",
"pdf",
"plain",
"pptx",
"revealjs",
"rst",
"rtf",
"s5",
"slideous",
"slidy",
],
},
};
export function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
options?: unknown,
): Promise<string> {
// set xelatex here
const xelatex = ["pdf", "latex"];
// Build arguments array
const args: string[] = [];
if (xelatex.includes(convertTo)) {
args.push("--pdf-engine=xelatex");
}
args.push(filePath);
args.push("-f", fileType);
args.push("-t", convertTo);
args.push("-o", targetPath);
return new Promise((resolve, reject) => {
execFile("pandoc", args, (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
});
});
}
import { execFile } from "node:child_process";
export const properties = {
from: {
text: [
"textile",
"tikiwiki",
"tsv",
"twiki",
"typst",
"vimwiki",
"biblatex",
"bibtex",
"bits",
"commonmark",
"commonmark_x",
"creole",
"csljson",
"csv",
"djot",
"docbook",
"docx",
"dokuwiki",
"endnotexml",
"epub",
"fb2",
"gfm",
"haddock",
"html",
"ipynb",
"jats",
"jira",
"json",
"latex",
"man",
"markdown",
"markdown_mmd",
"markdown_phpextra",
"markdown_strict",
"mediawiki",
"muse",
"pandoc native",
"opml",
"org",
"ris",
"rst",
"rtf",
"t2t",
],
},
to: {
text: [
"tei",
"texinfo",
"textile",
"typst",
"xwiki",
"zimwiki",
"asciidoc",
"asciidoc_legacy",
"asciidoctor",
"beamer",
"biblatex",
"bibtex",
"chunkedhtml",
"commonmark",
"commonmark_x",
"context",
"csljson",
"djot",
"docbook",
"docbook4",
"docbook5",
"docx",
"dokuwiki",
"dzslides",
"epub",
"epub2",
"epub3",
"fb2",
"gfm",
"haddock",
"html",
"html4",
"html5",
"icml",
"ipynb",
"jats",
"jats_archiving",
"jats_articleauthoring",
"jats_publishing",
"jira",
"json",
"latex",
"man",
"markdown",
"markdown_mmd",
"markdown_phpextra",
"markdown_strict",
"markua",
"mediawiki",
"ms",
"muse",
"pandoc native",
"odt",
"opendocument",
"opml",
"org",
"pdf",
"plain",
"pptx",
"revealjs",
"rst",
"rtf",
"s5",
"slideous",
"slidy",
],
},
};
export function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
options?: unknown,
): Promise<string> {
// set xelatex here
const xelatex = ["pdf", "latex"];
// Build arguments array
const args: string[] = [];
if (xelatex.includes(convertTo)) {
args.push("--pdf-engine=xelatex");
}
args.push(filePath);
args.push("-f", fileType);
args.push("-t", convertTo);
args.push("-o", targetPath);
return new Promise((resolve, reject) => {
execFile("pandoc", args, (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
});
});
}

View File

@@ -5,33 +5,45 @@ export const properties = {
images: ["pnm", "pbm", "pgm", "bmp"],
},
to: {
images: ["svg", "pdf", "pdfpage", "eps", "postscript", "ps", "dxf", "geojson", "pgm", "gimppath", "xfig"],
images: [
"svg",
"pdf",
"pdfpage",
"eps",
"postscript",
"ps",
"dxf",
"geojson",
"pgm",
"gimppath",
"xfig",
],
},
};
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) => {
execFile("potrace", [filePath, "-o", targetPath, "-b", convertTo], (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
});
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) => {
execFile("potrace", [filePath, "-o", targetPath, "-b", convertTo], (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
});
}
});
}

View File

@@ -1,37 +1,37 @@
import { execFile } 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) => {
execFile("resvg", [filePath, targetPath], (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
});
});
}
import { execFile } 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) => {
execFile("resvg", [filePath, targetPath], (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
});
});
}

View File

@@ -1,142 +1,138 @@
import { execFile } from "node:child_process";
// declare possible conversions
export const properties = {
from: {
images: [
"avif",
"bif",
"csv",
"exr",
"fits",
"gif",
"hdr.gz",
"hdr",
"heic",
"heif",
"img.gz",
"img",
"j2c",
"j2k",
"jp2",
"jpeg",
"jpx",
"jxl",
"mat",
"mrxs",
"ndpi",
"nia.gz",
"nia",
"nii.gz",
"nii",
"pdf",
"pfm",
"pgm",
"pic",
"png",
"ppm",
"raw",
"scn",
"svg",
"svs",
"svslide",
"szi",
"tif",
"tiff",
"v",
"vips",
"vms",
"vmu",
"webp",
"zip",
],
},
to: {
images: [
"avif",
"dzi",
"fits",
"gif",
"hdr.gz",
"heic",
"heif",
"img.gz",
"j2c",
"j2k",
"jp2",
"jpeg",
"jpx",
"jxl",
"mat",
"nia.gz",
"nia",
"nii.gz",
"nii",
"png",
"tiff",
"vips",
"webp",
],
},
options: {
svg: {
scale: {
description: "Scale the image up or down",
type: "number",
default: 1,
},
},
},
};
export function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
options?: unknown,
): Promise<string> {
// if (fileType === "svg") {
// const scale = options.scale || 1;
// const metadata = await sharp(filePath).metadata();
// if (!metadata || !metadata.width || !metadata.height) {
// throw new Error("Could not get metadata from image");
// }
// const newWidth = Math.round(metadata.width * scale);
// const newHeight = Math.round(metadata.height * scale);
// return await sharp(filePath)
// .resize(newWidth, newHeight)
// .toFormat(convertTo)
// .toFile(targetPath);
// }
let action = "copy";
if (fileType === "pdf") {
action = "pdfload";
}
return new Promise((resolve, reject) => {
execFile(
"vips",
[action, filePath, targetPath],
(error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
},
);
});
}
import { execFile } from "node:child_process";
// declare possible conversions
export const properties = {
from: {
images: [
"avif",
"bif",
"csv",
"exr",
"fits",
"gif",
"hdr.gz",
"hdr",
"heic",
"heif",
"img.gz",
"img",
"j2c",
"j2k",
"jp2",
"jpeg",
"jpx",
"jxl",
"mat",
"mrxs",
"ndpi",
"nia.gz",
"nia",
"nii.gz",
"nii",
"pdf",
"pfm",
"pgm",
"pic",
"png",
"ppm",
"raw",
"scn",
"svg",
"svs",
"svslide",
"szi",
"tif",
"tiff",
"v",
"vips",
"vms",
"vmu",
"webp",
"zip",
],
},
to: {
images: [
"avif",
"dzi",
"fits",
"gif",
"hdr.gz",
"heic",
"heif",
"img.gz",
"j2c",
"j2k",
"jp2",
"jpeg",
"jpx",
"jxl",
"mat",
"nia.gz",
"nia",
"nii.gz",
"nii",
"png",
"tiff",
"vips",
"webp",
],
},
options: {
svg: {
scale: {
description: "Scale the image up or down",
type: "number",
default: 1,
},
},
},
};
export function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
options?: unknown,
): Promise<string> {
// if (fileType === "svg") {
// const scale = options.scale || 1;
// const metadata = await sharp(filePath).metadata();
// if (!metadata || !metadata.width || !metadata.height) {
// throw new Error("Could not get metadata from image");
// }
// const newWidth = Math.round(metadata.width * scale);
// const newHeight = Math.round(metadata.height * scale);
// return await sharp(filePath)
// .resize(newWidth, newHeight)
// .toFormat(convertTo)
// .toFile(targetPath);
// }
let action = "copy";
if (fileType === "pdf") {
action = "pdfload";
}
return new Promise((resolve, reject) => {
execFile("vips", [action, 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,53 +1,44 @@
import { execFile } from "node:child_process";
export const properties = {
from: {
text: ["tex", "latex"],
},
to: {
text: ["pdf"],
},
};
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) => {
// const fileName: string = (targetPath.split("/").pop() as string).replace(".pdf", "")
const outputPath = targetPath
.split("/")
.slice(0, -1)
.join("/")
.replace("./", "");
execFile(
"latexmk",
[
"-xelatex",
"-interaction=nonstopmode",
`-output-directory=${outputPath}`,
filePath,
],
(error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
},
);
});
}
import { execFile } from "node:child_process";
export const properties = {
from: {
text: ["tex", "latex"],
},
to: {
text: ["pdf"],
},
};
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) => {
// const fileName: string = (targetPath.split("/").pop() as string).replace(".pdf", "")
const outputPath = targetPath.split("/").slice(0, -1).join("/").replace("./", "");
execFile(
"latexmk",
["-xelatex", "-interaction=nonstopmode", `-output-directory=${outputPath}`, filePath],
(error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
},
);
});
}

41
src/db/db.ts Normal file
View File

@@ -0,0 +1,41 @@
import { Database } from "bun:sqlite";
const db = new Database("./data/mydb.sqlite", { create: true });
if (!db.query("SELECT * FROM sqlite_master WHERE type='table'").get()) {
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL,
password TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS file_names (
id INTEGER PRIMARY KEY AUTOINCREMENT,
job_id INTEGER NOT NULL,
file_name TEXT NOT NULL,
output_file_name TEXT NOT NULL,
status TEXT DEFAULT 'not started',
FOREIGN KEY (job_id) REFERENCES jobs(id)
);
CREATE TABLE IF NOT EXISTS jobs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
date_created TEXT NOT NULL,
status TEXT DEFAULT 'not started',
num_files INTEGER DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES users(id)
);
PRAGMA user_version = 1;`);
}
const dbVersion = (db.query("PRAGMA user_version").get() as { user_version?: number }).user_version;
if (dbVersion === 0) {
db.exec("ALTER TABLE file_names ADD COLUMN status TEXT DEFAULT 'not started';");
db.exec("PRAGMA user_version = 1;");
console.log("Updated database to version 1.");
}
// enable WAL mode
db.exec("PRAGMA journal_mode = WAL;");
export default db;

23
src/db/types.ts Normal file
View File

@@ -0,0 +1,23 @@
export class Filename {
id!: number;
job_id!: number;
file_name!: string;
output_file_name!: string;
status!: string;
}
export class Jobs {
finished_files!: number;
id!: number;
user_id!: number;
date_created!: string;
status!: string;
num_files!: number;
files_detailed!: Filename[];
}
export class User {
id!: number;
email!: string;
password!: string;
}

15
src/helpers/env.ts Normal file
View File

@@ -0,0 +1,15 @@
export const ACCOUNT_REGISTRATION =
process.env.ACCOUNT_REGISTRATION?.toLowerCase() === "true" || false;
export const HTTP_ALLOWED = process.env.HTTP_ALLOWED?.toLowerCase() === "true" || false;
export const ALLOW_UNAUTHENTICATED =
process.env.ALLOW_UNAUTHENTICATED?.toLowerCase() === "true" || false;
export const AUTO_DELETE_EVERY_N_HOURS = process.env.AUTO_DELETE_EVERY_N_HOURS
? Number(process.env.AUTO_DELETE_EVERY_N_HOURS)
: 24;
export const HIDE_HISTORY = process.env.HIDE_HISTORY?.toLowerCase() === "true" || false;
export const WEBROOT = process.env.WEBROOT ?? "";

View File

@@ -1,37 +1,37 @@
export const normalizeFiletype = (filetype: string): string => {
const lowercaseFiletype = filetype.toLowerCase();
switch (lowercaseFiletype) {
case "jfif":
case "jpg":
return "jpeg";
case "htm":
return "html";
case "tex":
return "latex";
case "md":
return "markdown";
case "unknown":
return "m4a";
default:
return lowercaseFiletype;
}
};
export const normalizeOutputFiletype = (filetype: string): string => {
const lowercaseFiletype = filetype.toLowerCase();
switch (lowercaseFiletype) {
case "jpeg":
return "jpg";
case "latex":
return "tex";
case "markdown_phpextra":
case "markdown_strict":
case "markdown_mmd":
case "markdown":
return "md";
default:
return lowercaseFiletype;
}
};
export const normalizeFiletype = (filetype: string): string => {
const lowercaseFiletype = filetype.toLowerCase();
switch (lowercaseFiletype) {
case "jfif":
case "jpg":
return "jpeg";
case "htm":
return "html";
case "tex":
return "latex";
case "md":
return "markdown";
case "unknown":
return "m4a";
default:
return lowercaseFiletype;
}
};
export const normalizeOutputFiletype = (filetype: string): string => {
const lowercaseFiletype = filetype.toLowerCase();
switch (lowercaseFiletype) {
case "jpeg":
return "jpg";
case "latex":
return "tex";
case "markdown_phpextra":
case "markdown_strict":
case "markdown_mmd":
case "markdown":
return "md";
default:
return lowercaseFiletype;
}
};

View File

@@ -44,6 +44,16 @@ if (process.env.NODE_ENV === "production") {
}
});
exec("magick --version", (error, stdout) => {
if (error) {
console.error("ImageMagick is not installed.");
}
if (stdout) {
console.log(stdout.split("\n")[0]?.replace("Version: ", ""));
}
});
exec("gm version", (error, stdout) => {
if (error) {
console.error("GraphicsMagick is not installed.");

File diff suppressed because it is too large Load Diff

View File

@@ -18,24 +18,6 @@
--color-accent-400: rgba(var(--accent-400));
}
/*
The default border color has changed to `currentColor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentColor);
}
}
@utility article {
@apply px-2 sm:px-4 py-4 mb-4 bg-neutral-800/40 w-full mx-auto max-w-4xl rounded-sm;
}
@@ -44,6 +26,10 @@
@apply bg-accent-500 text-contrast rounded-sm p-2 sm:p-4 hover:bg-accent-400 cursor-pointer transition-colors;
}
@utility btn-secondary {
@apply bg-neutral-400 text-contrast rounded-sm p-2 sm:p-4 hover:bg-neutral-300 cursor-pointer transition-colors;
}
:root {
--contrast: 255, 255, 255;
--neutral-900: 243, 244, 246;

View File

@@ -0,0 +1,67 @@
import { Html } from "@elysiajs/html";
import Elysia, { t } from "elysia";
import { getPossibleTargets } from "../converters/main";
import { userService } from "./user";
export const chooseConverter = new Elysia().use(userService).post(
"/conversions",
({ body }) => {
return (
<>
<article
class={`
convert_to_popup absolute z-2 m-0 hidden h-[50vh] max-h-[50vh] w-full flex-col
overflow-x-hidden overflow-y-auto rounded bg-neutral-800
sm:h-[30vh]
`}
>
{Object.entries(getPossibleTargets(body.fileType)).map(([converter, targets]) => (
<article
class="convert_to_group flex w-full flex-col border-b border-neutral-700 p-4"
data-converter={converter}
>
<header class="mb-2 w-full text-xl font-bold" safe>
{converter}
</header>
<ul class="convert_to_target flex flex-row flex-wrap gap-1">
{targets.map((target) => (
<button
// https://stackoverflow.com/questions/121499/when-a-blur-event-occurs-how-can-i-find-out-which-element-focus-went-to#comment82388679_33325953
tabindex={0}
class={`
target rounded bg-neutral-700 p-1 text-base
hover:bg-neutral-600
`}
data-value={`${target},${converter}`}
data-target={target}
data-converter={converter}
type="button"
safe
>
{target}
</button>
))}
</ul>
</article>
))}
</article>
<select name="convert_to" aria-label="Convert to" required hidden>
<option selected disabled value="">
Convert to
</option>
{Object.entries(getPossibleTargets(body.fileType)).map(([converter, targets]) => (
<optgroup label={converter}>
{targets.map((target) => (
<option value={`${target},${converter}`} safe>
{target}
</option>
))}
</optgroup>
))}
</select>
</>
);
},
{ body: t.Object({ fileType: t.String() }) },
);

116
src/pages/convert.tsx Normal file
View File

@@ -0,0 +1,116 @@
import { mkdir } from "node:fs/promises";
import { Elysia, t } from "elysia";
import sanitize from "sanitize-filename";
import { outputDir, uploadsDir } from "..";
import { mainConverter } from "../converters/main";
import db from "../db/db";
import { Jobs } from "../db/types";
import { WEBROOT } from "../helpers/env";
import { normalizeFiletype, normalizeOutputFiletype } from "../helpers/normalizeFiletype";
import { userService } from "./user";
export const convert = new Elysia().use(userService).post(
"/convert",
async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
if (!auth?.value) {
return redirect(`${WEBROOT}/login`, 302);
}
const user = await jwt.verify(auth.value);
if (!user) {
return redirect(`${WEBROOT}/login`, 302);
}
if (!jobId?.value) {
return redirect(`${WEBROOT}/`, 302);
}
const existingJob = db
.query("SELECT * FROM jobs WHERE id = ? AND user_id = ?")
.as(Jobs)
.get(jobId.value, user.id);
if (!existingJob) {
return redirect(`${WEBROOT}/`, 302);
}
const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
const userOutputDir = `${outputDir}${user.id}/${jobId.value}/`;
// create the output directory
try {
await mkdir(userOutputDir, { recursive: true });
} catch (error) {
console.error(`Failed to create the output directory: ${userOutputDir}.`, error);
}
const convertTo = normalizeFiletype(body.convert_to.split(",")[0] ?? "");
const converterName = body.convert_to.split(",")[1];
const fileNames = JSON.parse(body.file_names) as string[];
for (let i = 0; i < fileNames.length; i++) {
fileNames[i] = sanitize(fileNames[i] || "");
}
if (!Array.isArray(fileNames) || fileNames.length === 0) {
return redirect(`${WEBROOT}/`, 302);
}
db.query("UPDATE jobs SET num_files = ?1, status = 'pending' WHERE id = ?2").run(
fileNames.length,
jobId.value,
);
const query = db.query(
"INSERT INTO file_names (job_id, file_name, output_file_name, status) VALUES (?1, ?2, ?3, ?4)",
);
// Start the conversion process in the background
Promise.all(
fileNames.map(async (fileName) => {
const filePath = `${userUploadsDir}${fileName}`;
const fileTypeOrig = fileName.split(".").pop() ?? "";
const fileType = normalizeFiletype(fileTypeOrig);
const newFileExt = normalizeOutputFiletype(convertTo);
const newFileName = fileName.replace(
new RegExp(`${fileTypeOrig}(?!.*${fileTypeOrig})`),
newFileExt,
);
const targetPath = `${userOutputDir}${newFileName}`;
const result = await mainConverter(
filePath,
fileType,
convertTo,
targetPath,
{},
converterName,
);
if (jobId.value) {
query.run(jobId.value, fileName, newFileName, result);
}
}),
)
.then(() => {
// All conversions are done, update the job status to 'completed'
if (jobId.value) {
db.query("UPDATE jobs SET status = 'completed' WHERE id = ?1").run(jobId.value);
}
// delete all uploaded files in userUploadsDir
// rmSync(userUploadsDir, { recursive: true, force: true });
})
.catch((error) => {
console.error("Error in conversion process:", error);
});
// Redirect the client immediately
return redirect(`${WEBROOT}/results/${jobId.value}`, 302);
},
{
body: t.Object({
convert_to: t.String(),
file_names: t.String(),
}),
},
);

41
src/pages/deleteFile.tsx Normal file
View File

@@ -0,0 +1,41 @@
import { unlink } from "node:fs/promises";
import { Elysia, t } from "elysia";
import { uploadsDir } from "..";
import db from "../db/db";
import { WEBROOT } from "../helpers/env";
import { userService } from "./user";
export const deleteFile = new Elysia().use(userService).post(
"/delete",
async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
if (!auth?.value) {
return redirect(`${WEBROOT}/login`, 302);
}
const user = await jwt.verify(auth.value);
if (!user) {
return redirect(`${WEBROOT}/login`, 302);
}
if (!jobId?.value) {
return redirect(`${WEBROOT}/`, 302);
}
const existingJob = await db
.query("SELECT * FROM jobs WHERE id = ? AND user_id = ?")
.get(jobId.value, user.id);
if (!existingJob) {
return redirect(`${WEBROOT}/`, 302);
}
const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
await unlink(`${userUploadsDir}${body.filename}`);
return {
message: "File deleted successfully.",
};
},
{ body: t.Object({ filename: t.String() }) },
);

62
src/pages/download.tsx Normal file
View File

@@ -0,0 +1,62 @@
import { Elysia } from "elysia";
import sanitize from "sanitize-filename";
import { outputDir } from "..";
import db from "../db/db";
import { WEBROOT } from "../helpers/env";
import { userService } from "./user";
export const download = new Elysia()
.use(userService)
.get(
"/download/:userId/:jobId/:fileName",
async ({ params, jwt, redirect, cookie: { auth } }) => {
if (!auth?.value) {
return redirect(`${WEBROOT}/login`, 302);
}
const user = await jwt.verify(auth.value);
if (!user) {
return redirect(`${WEBROOT}/login`, 302);
}
const job = await db
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
.get(user.id, params.jobId);
if (!job) {
return redirect(`${WEBROOT}/results`, 302);
}
// parse from url encoded string
const userId = decodeURIComponent(params.userId);
const jobId = decodeURIComponent(params.jobId);
const fileName = sanitize(decodeURIComponent(params.fileName));
const filePath = `${outputDir}${userId}/${jobId}/${fileName}`;
return Bun.file(filePath);
},
)
.get("/zip/:userId/:jobId", async ({ params, jwt, redirect, cookie: { auth } }) => {
// TODO: Implement zip download
if (!auth?.value) {
return redirect(`${WEBROOT}/login`, 302);
}
const user = await jwt.verify(auth.value);
if (!user) {
return redirect(`${WEBROOT}/login`, 302);
}
const job = await db
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
.get(user.id, params.jobId);
if (!job) {
return redirect(`${WEBROOT}/results`, 302);
}
// const userId = decodeURIComponent(params.userId);
// const jobId = decodeURIComponent(params.jobId);
// const outputPath = `${outputDir}${userId}/`{jobId}/);
// return Bun.zip(outputPath);
});

216
src/pages/history.tsx Normal file
View File

@@ -0,0 +1,216 @@
import { Html } from "@elysiajs/html";
import { Elysia } from "elysia";
import { BaseHtml } from "../components/base";
import { Header } from "../components/header";
import db from "../db/db";
import { Filename, Jobs } from "../db/types";
import { ALLOW_UNAUTHENTICATED, HIDE_HISTORY, WEBROOT } from "../helpers/env";
import { userService } from "./user";
export const history = new Elysia()
.use(userService)
.get("/history", async ({ jwt, redirect, cookie: { auth } }) => {
if (HIDE_HISTORY) {
return redirect(`${WEBROOT}/`, 302);
}
if (!auth?.value) {
return redirect(`${WEBROOT}/login`, 302);
}
const user = await jwt.verify(auth.value);
if (!user) {
return redirect(`${WEBROOT}/login`, 302);
}
let userJobs = db.query("SELECT * FROM jobs WHERE user_id = ?").as(Jobs).all(user.id).reverse();
for (const job of userJobs) {
const files = db.query("SELECT * FROM file_names WHERE job_id = ?").as(Filename).all(job.id);
job.finished_files = files.length;
job.files_detailed = files;
}
// filter out jobs with no files
userJobs = userJobs.filter((job) => job.num_files > 0);
return (
<BaseHtml webroot={WEBROOT} title="ConvertX | Results">
<>
<Header
webroot={WEBROOT}
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
hideHistory={HIDE_HISTORY}
loggedIn
/>
<main
class={`
w-full flex-1 px-2
sm:px-4
`}
>
<article class="article">
<h1 class="mb-4 text-xl">Results</h1>
<table
class={`
w-full table-auto overflow-y-auto rounded bg-neutral-900 text-left
[&_td]:p-4
[&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
`}
>
<thead>
<tr>
<th
class={`
px-2 py-2
sm:px-4
`}
>
<span class="sr-only">Expand details</span>
</th>
<th
class={`
px-2 py-2
sm:px-4
`}
>
Time
</th>
<th
class={`
px-2 py-2
sm:px-4
`}
>
Files
</th>
<th
class={`
px-2 py-2
max-sm:hidden
sm:px-4
`}
>
Files Done
</th>
<th
class={`
px-2 py-2
sm:px-4
`}
>
Status
</th>
<th
class={`
px-2 py-2
sm:px-4
`}
>
View
</th>
</tr>
</thead>
<tbody>
{userJobs.map((job) => (
<>
<tr id={`job-row-${job.id}`}>
<td class="job-details-toggle cursor-pointer" data-job-id={job.id}>
<svg
id={`arrow-${job.id}`}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="inline-block h-4 w-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M8.25 4.5l7.5 7.5-7.5 7.5"
/>
</svg>
</td>
<td safe>{new Date(job.date_created).toLocaleTimeString()}</td>
<td>{job.num_files}</td>
<td class="max-sm:hidden">{job.finished_files}</td>
<td safe>{job.status}</td>
<td>
<a
class={`
text-accent-500 underline
hover:text-accent-400
`}
href={`${WEBROOT}/results/${job.id}`}
>
View
</a>
</td>
</tr>
<tr id={`details-${job.id}`} class="hidden">
<td colspan="6">
<div class="p-2 text-sm text-neutral-500">
<div class="mb-1 font-semibold">Detailed File Information:</div>
{job.files_detailed.map((file: Filename) => (
<div class="flex items-center">
<span class="w-5/12 truncate" title={file.file_name} safe>
{file.file_name}
</span>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="mx-2 inline-block h-4 w-4 text-neutral-500"
>
<path
fill-rule="evenodd"
d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z"
clip-rule="evenodd"
/>
</svg>
<span class="w-5/12 truncate" title={file.output_file_name} safe>
{file.output_file_name}
</span>
</div>
))}
</div>
</td>
</tr>
</>
))}
</tbody>
</table>
</article>
</main>
<script>
{`
document.addEventListener('DOMContentLoaded', () => {
const toggles = document.querySelectorAll('.job-details-toggle');
toggles.forEach(toggle => {
toggle.addEventListener('click', function() {
const jobId = this.dataset.jobId;
const detailsRow = document.getElementById(\`details-\${jobId}\`);
// The arrow SVG itself has the ID arrow-\${jobId}
const arrow = document.getElementById(\`arrow-\${jobId}\`);
if (detailsRow && arrow) {
detailsRow.classList.toggle("hidden");
if (detailsRow.classList.contains("hidden")) {
// Right-facing arrow (collapsed)
arrow.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />';
} else {
// Down-facing arrow (expanded)
arrow.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />';
}
}
});
});
});
`}
</script>
</>
</BaseHtml>
);
});

View File

@@ -0,0 +1,80 @@
import { Html } from "@elysiajs/html";
import Elysia from "elysia";
import { BaseHtml } from "../components/base";
import { Header } from "../components/header";
import { getAllInputs, getAllTargets } from "../converters/main";
import { ALLOW_UNAUTHENTICATED, WEBROOT } from "../helpers/env";
import { userService } from "./user";
export const listConverters = new Elysia()
.use(userService)
.get("/converters", async ({ jwt, redirect, cookie: { auth } }) => {
if (!auth?.value) {
return redirect(`${WEBROOT}/login`, 302);
}
const user = await jwt.verify(auth.value);
if (!user) {
return redirect(`${WEBROOT}/login`, 302);
}
return (
<BaseHtml webroot={WEBROOT} title="ConvertX | Converters">
<>
<Header webroot={WEBROOT} allowUnauthenticated={ALLOW_UNAUTHENTICATED} loggedIn />
<main
class={`
w-full flex-1 px-2
sm:px-4
`}
>
<article class="article">
<h1 class="mb-4 text-xl">Converters</h1>
<table
class={`
w-full table-auto rounded bg-neutral-900 text-left
[&_td]:p-4
[&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
[&_ul]:list-inside [&_ul]:list-disc
`}
>
<thead>
<tr>
<th class="mx-4 my-2">Converter</th>
<th class="mx-4 my-2">From (Count)</th>
<th class="mx-4 my-2">To (Count)</th>
</tr>
</thead>
<tbody>
{Object.entries(getAllTargets()).map(([converter, targets]) => {
const inputs = getAllInputs(converter);
return (
<tr>
<td safe>{converter}</td>
<td>
Count: {inputs.length}
<ul>
{inputs.map((input) => (
<li safe>{input}</li>
))}
</ul>
</td>
<td>
Count: {targets.length}
<ul>
{targets.map((target) => (
<li safe>{target}</li>
))}
</ul>
</td>
</tr>
);
})}
</tbody>
</table>
</article>
</main>
</>
</BaseHtml>
);
});

215
src/pages/results.tsx Normal file
View File

@@ -0,0 +1,215 @@
import { Html } from "@elysiajs/html";
import { Elysia } from "elysia";
import { BaseHtml } from "../components/base";
import { Header } from "../components/header";
import db from "../db/db";
import { Filename, Jobs } from "../db/types";
import { ALLOW_UNAUTHENTICATED, WEBROOT } from "../helpers/env";
import { userService } from "./user";
function ResultsArticle({
job,
files,
outputPath,
}: {
job: Jobs;
files: Filename[];
outputPath: string;
}) {
return (
<article class="article">
<div class="mb-4 flex items-center justify-between">
<h1 class="text-xl">Results</h1>
<div>
<button
type="button"
class="float-right w-40 btn-primary"
onclick="downloadAll()"
{...(files.length !== job.num_files ? { disabled: true, "aria-busy": "true" } : "")}
>
{files.length === job.num_files ? "Download All" : "Converting..."}
</button>
</div>
</div>
<progress
max={job.num_files}
value={files.length}
class={`
mb-4 inline-block h-2 w-full appearance-none overflow-hidden rounded-full border-0
bg-neutral-700 bg-none text-accent-500 accent-accent-500
[&::-moz-progress-bar]:bg-accent-500 [&::-webkit-progress-value]:rounded-full
[&::-webkit-progress-value]:[background:none]
[&[value]::-webkit-progress-value]:bg-accent-500
[&[value]::-webkit-progress-value]:transition-[inline-size]
`}
/>
<table
class={`
w-full table-auto rounded bg-neutral-900 text-left
[&_td]:p-4
[&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
`}
>
<thead>
<tr>
<th
class={`
px-2 py-2
sm:px-4
`}
>
Converted File Name
</th>
<th
class={`
px-2 py-2
sm:px-4
`}
>
Status
</th>
<th
class={`
px-2 py-2
sm:px-4
`}
>
View
</th>
<th
class={`
px-2 py-2
sm:px-4
`}
>
Download
</th>
</tr>
</thead>
<tbody>
{files.map((file) => (
<tr>
<td safe class="max-w-[20vw] truncate">
{file.output_file_name}
</td>
<td safe>{file.status}</td>
<td>
<a
class={`
text-accent-500 underline
hover:text-accent-400
`}
href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
>
View
</a>
</td>
<td>
<a
class={`
text-accent-500 underline
hover:text-accent-400
`}
href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
download={file.output_file_name}
>
Download
</a>
</td>
</tr>
))}
</tbody>
</table>
</article>
);
}
export const results = new Elysia()
.use(userService)
.get("/results/:jobId", async ({ params, jwt, set, redirect, cookie: { auth, job_id } }) => {
if (!auth?.value) {
return redirect(`${WEBROOT}/login`, 302);
}
if (job_id?.value) {
// clear the job_id cookie since we are viewing the results
job_id.remove();
}
const user = await jwt.verify(auth.value);
if (!user) {
return redirect(`${WEBROOT}/login`, 302);
}
const job = db
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
.as(Jobs)
.get(user.id, params.jobId);
if (!job) {
set.status = 404;
return {
message: "Job not found.",
};
}
const outputPath = `${user.id}/${params.jobId}/`;
const files = db
.query("SELECT * FROM file_names WHERE job_id = ?")
.as(Filename)
.all(params.jobId);
return (
<BaseHtml webroot={WEBROOT} title="ConvertX | Result">
<>
<Header webroot={WEBROOT} allowUnauthenticated={ALLOW_UNAUTHENTICATED} loggedIn />
<main
class={`
w-full flex-1 px-2
sm:px-4
`}
>
<ResultsArticle job={job} files={files} outputPath={outputPath} />
</main>
<script src={`${WEBROOT}/results.js`} defer />
</>
</BaseHtml>
);
})
.post("/progress/:jobId", async ({ jwt, set, params, redirect, cookie: { auth, job_id } }) => {
if (!auth?.value) {
return redirect(`${WEBROOT}/login`, 302);
}
if (job_id?.value) {
// clear the job_id cookie since we are viewing the results
job_id.remove();
}
const user = await jwt.verify(auth.value);
if (!user) {
return redirect(`${WEBROOT}/login`, 302);
}
const job = db
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
.as(Jobs)
.get(user.id, params.jobId);
if (!job) {
set.status = 404;
return {
message: "Job not found.",
};
}
const outputPath = `${user.id}/${params.jobId}/`;
const files = db
.query("SELECT * FROM file_names WHERE job_id = ?")
.as(Filename)
.all(params.jobId);
return <ResultsArticle job={job} files={files} outputPath={outputPath} />;
});

240
src/pages/root.tsx Normal file
View File

@@ -0,0 +1,240 @@
import { randomInt } from "node:crypto";
import { Html } from "@elysiajs/html";
import { JWTPayloadSpec } from "@elysiajs/jwt";
import { Elysia } from "elysia";
import { BaseHtml } from "../components/base";
import { Header } from "../components/header";
import { getAllTargets } from "../converters/main";
import db from "../db/db";
import { User } from "../db/types";
import {
ACCOUNT_REGISTRATION,
ALLOW_UNAUTHENTICATED,
HIDE_HISTORY,
HTTP_ALLOWED,
WEBROOT,
} from "../helpers/env";
import { FIRST_RUN, userService } from "./user";
export const root = new Elysia()
.use(userService)
.get("/", async ({ jwt, redirect, cookie: { auth, jobId } }) => {
if (!ALLOW_UNAUTHENTICATED) {
if (FIRST_RUN) {
return redirect(`${WEBROOT}/setup`, 302);
}
if (!auth?.value) {
return redirect(`${WEBROOT}/login`, 302);
}
}
// validate jwt
let user: ({ id: string } & JWTPayloadSpec) | false = false;
if (ALLOW_UNAUTHENTICATED) {
const newUserId = String(
randomInt(2 ** 24, Math.min(2 ** 48 + 2 ** 24 - 1, Number.MAX_SAFE_INTEGER)),
);
const accessToken = await jwt.sign({
id: newUserId,
});
user = { id: newUserId };
if (!auth) {
return {
message: "No auth cookie, perhaps your browser is blocking cookies.",
};
}
// set cookie
auth.set({
value: accessToken,
httpOnly: true,
secure: !HTTP_ALLOWED,
maxAge: 24 * 60 * 60,
sameSite: "strict",
});
} else if (auth?.value) {
user = await jwt.verify(auth.value);
if (
user !== false &&
user.id &&
(Number.parseInt(user.id) < 2 ** 24 || !ALLOW_UNAUTHENTICATED)
) {
// make sure user exists in db
const existingUser = db.query("SELECT * FROM users WHERE id = ?").as(User).get(user.id);
if (!existingUser) {
if (auth?.value) {
auth.remove();
}
return redirect(`${WEBROOT}/login`, 302);
}
}
}
if (!user) {
return redirect(`${WEBROOT}/login`, 302);
}
// create a new job
db.query("INSERT INTO jobs (user_id, date_created) VALUES (?, ?)").run(
user.id,
new Date().toISOString(),
);
const { id } = db
.query("SELECT id FROM jobs WHERE user_id = ? ORDER BY id DESC")
.get(user.id) as { id: number };
if (!jobId) {
return { message: "Cookies should be enabled to use this app." };
}
jobId.set({
value: id,
httpOnly: true,
secure: !HTTP_ALLOWED,
maxAge: 24 * 60 * 60,
sameSite: "strict",
});
console.log("jobId set to:", id);
return (
<BaseHtml webroot={WEBROOT}>
<>
<Header
webroot={WEBROOT}
accountRegistration={ACCOUNT_REGISTRATION}
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
hideHistory={HIDE_HISTORY}
loggedIn
/>
<main
class={`
w-full flex-1 px-2
sm:px-4
`}
>
<article class="article">
<h1 class="mb-4 text-xl">Convert</h1>
<div class="mb-4 scrollbar-thin max-h-[50vh] overflow-y-auto">
<table
id="file-list"
class={`
w-full table-auto rounded bg-neutral-900
[&_td]:p-4 [&_td]:first:max-w-[30vw] [&_td]:first:truncate
[&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
`}
/>
</div>
<div
id="dropzone"
class={`
relative flex h-48 w-full items-center justify-center rounded border border-dashed
border-neutral-700 transition-all
hover:border-neutral-600
[&.dragover]:border-4 [&.dragover]:border-neutral-500
`}
>
<span>
<b>Choose a file</b> or drag it here
</span>
<input
type="file"
name="file"
multiple
class="absolute inset-0 size-full cursor-pointer opacity-0"
/>
</div>
</article>
<form
method="post"
action={`${WEBROOT}/convert`}
class="relative mx-auto mb-[35vh] w-full max-w-4xl"
>
<input type="hidden" name="file_names" id="file_names" />
<article class="article w-full">
<input
type="search"
name="convert_to_search"
placeholder="Search for conversions"
autocomplete="off"
class="w-full rounded-sm bg-neutral-800 p-4"
/>
<div class="select_container relative">
<article
class={`
convert_to_popup absolute z-2 m-0 hidden h-[30vh] max-h-[50vh] w-full flex-col
overflow-x-hidden overflow-y-auto rounded bg-neutral-800
sm:h-[30vh]
`}
>
{Object.entries(getAllTargets()).map(([converter, targets]) => (
<article
class={`
convert_to_group flex w-full flex-col border-b border-neutral-700 p-4
`}
data-converter={converter}
>
<header class="mb-2 w-full text-xl font-bold" safe>
{converter}
</header>
<ul class="convert_to_target flex flex-row flex-wrap gap-1">
{targets.map((target) => (
<button
// https://stackoverflow.com/questions/121499/when-a-blur-event-occurs-how-can-i-find-out-which-element-focus-went-to#comment82388679_33325953
tabindex={0}
class={`
target rounded bg-neutral-700 p-1 text-base
hover:bg-neutral-600
`}
data-value={`${target},${converter}`}
data-target={target}
data-converter={converter}
type="button"
safe
>
{target}
</button>
))}
</ul>
</article>
))}
</article>
{/* Hidden element which determines the format to convert the file too and the converter to use */}
<select name="convert_to" aria-label="Convert to" required hidden>
<option selected disabled value="">
Convert to
</option>
{Object.entries(getAllTargets()).map(([converter, targets]) => (
<optgroup label={converter}>
{targets.map((target) => (
<option value={`${target},${converter}`} safe>
{target}
</option>
))}
</optgroup>
))}
</select>
</div>
</article>
<input
class={`
w-full btn-primary opacity-100
disabled:cursor-not-allowed disabled:opacity-50
`}
type="submit"
value="Convert"
disabled
/>
</form>
</main>
<script src="script.js" defer />
</>
</BaseHtml>
);
});

48
src/pages/upload.tsx Normal file
View File

@@ -0,0 +1,48 @@
import { Elysia, t } from "elysia";
import db from "../db/db";
import { WEBROOT } from "../helpers/env";
import { uploadsDir } from "../index";
import { userService } from "./user";
export const upload = new Elysia().use(userService).post(
"/upload",
async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
if (!auth?.value) {
return redirect(`${WEBROOT}/login`, 302);
}
const user = await jwt.verify(auth.value);
if (!user) {
return redirect(`${WEBROOT}/login`, 302);
}
if (!jobId?.value) {
return redirect(`${WEBROOT}/`, 302);
}
const existingJob = await db
.query("SELECT * FROM jobs WHERE id = ? AND user_id = ?")
.get(jobId.value, user.id);
if (!existingJob) {
return redirect(`${WEBROOT}/`, 302);
}
const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
if (body?.file) {
if (Array.isArray(body.file)) {
for (const file of body.file) {
await Bun.write(`${userUploadsDir}${file.name}`, file);
}
} else {
await Bun.write(`${userUploadsDir}${body.file["name"]}`, body.file);
}
}
return {
message: "Files uploaded successfully.",
};
},
{ body: t.Object({ file: t.Files() }) },
);

509
src/pages/user.tsx Normal file
View File

@@ -0,0 +1,509 @@
import { randomUUID } from "node:crypto";
import { Html } from "@elysiajs/html";
import { jwt } from "@elysiajs/jwt";
import { Elysia, t } from "elysia";
import { BaseHtml } from "../components/base";
import { Header } from "../components/header";
import db from "../db/db";
import { User } from "../db/types";
import {
ACCOUNT_REGISTRATION,
ALLOW_UNAUTHENTICATED,
HIDE_HISTORY,
HTTP_ALLOWED,
WEBROOT,
} from "../helpers/env";
export let FIRST_RUN = db.query("SELECT * FROM users").get() === null || false;
export const userService = new Elysia({ name: "user/service" })
.use(
jwt({
name: "jwt",
schema: t.Object({
id: t.String(),
}),
secret: process.env.JWT_SECRET ?? randomUUID(),
exp: "7d",
}),
)
.model({
signIn: t.Object({
email: t.String(),
password: t.String(),
}),
})
.macro({
isSignIn(enabled: boolean) {
if (!enabled) return;
return {
async beforeHandle({ status, jwt, cookie: { auth } }) {
if (auth?.value) {
const user = await jwt.verify(auth.value);
return {
success: true,
user,
};
}
return status(401, {
success: false,
message: "Unauthorized",
});
},
};
},
});
export const user = new Elysia()
.use(userService)
.get("/setup", ({ redirect }) => {
if (!FIRST_RUN) {
return redirect(`${WEBROOT}/login`, 302);
}
return (
<BaseHtml title="ConvertX | Setup" webroot={WEBROOT}>
<main
class={`
mx-auto w-full max-w-4xl flex-1 px-2
sm:px-4
`}
>
<h1 class="my-8 text-3xl">Welcome to ConvertX!</h1>
<article class="article p-0">
<header class="w-full bg-neutral-800 p-4">Create your account</header>
<form method="post" action={`${WEBROOT}/register`} class="p-4">
<fieldset class="mb-4 flex flex-col gap-4">
<label class="flex flex-col gap-1">
Email
<input
type="email"
name="email"
class="rounded-sm bg-neutral-800 p-3"
placeholder="Email"
autocomplete="email"
required
/>
</label>
<label class="flex flex-col gap-1">
Password
<input
type="password"
name="password"
class="rounded-sm bg-neutral-800 p-3"
placeholder="Password"
autocomplete="current-password"
required
/>
</label>
</fieldset>
<input type="submit" value="Create account" class="btn-primary" />
</form>
<footer class="p-4">
Report any issues on{" "}
<a
class={`
text-accent-500 underline
hover:text-accent-400
`}
href="https://github.com/C4illin/ConvertX"
>
GitHub
</a>
.
</footer>
</article>
</main>
</BaseHtml>
);
})
.get("/register", ({ redirect }) => {
if (!ACCOUNT_REGISTRATION) {
return redirect(`${WEBROOT}/login`, 302);
}
return (
<BaseHtml webroot={WEBROOT} title="ConvertX | Register">
<>
<Header
webroot={WEBROOT}
accountRegistration={ACCOUNT_REGISTRATION}
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
hideHistory={HIDE_HISTORY}
/>
<main
class={`
w-full flex-1 px-2
sm:px-4
`}
>
<article class="article">
<form method="post" class="flex flex-col gap-4">
<fieldset class="mb-4 flex flex-col gap-4">
<label class="flex flex-col gap-1">
Email
<input
type="email"
name="email"
class="rounded-sm bg-neutral-800 p-3"
placeholder="Email"
autocomplete="email"
required
/>
</label>
<label class="flex flex-col gap-1">
Password
<input
type="password"
name="password"
class="rounded-sm bg-neutral-800 p-3"
placeholder="Password"
autocomplete="current-password"
required
/>
</label>
</fieldset>
<input type="submit" value="Register" class="w-full btn-primary" />
</form>
</article>
</main>
</>
</BaseHtml>
);
})
.post(
"/register",
async ({ body: { email, password }, set, redirect, jwt, cookie: { auth } }) => {
if (!ACCOUNT_REGISTRATION && !FIRST_RUN) {
return redirect(`${WEBROOT}/login`, 302);
}
if (FIRST_RUN) {
FIRST_RUN = false;
}
const existingUser = await db.query("SELECT * FROM users WHERE email = ?").get(email);
if (existingUser) {
set.status = 400;
return {
message: "Email already in use.",
};
}
const savedPassword = await Bun.password.hash(password);
db.query("INSERT INTO users (email, password) VALUES (?, ?)").run(email, savedPassword);
const user = db.query("SELECT * FROM users WHERE email = ?").as(User).get(email);
if (!user) {
set.status = 500;
return {
message: "Failed to create user.",
};
}
const accessToken = await jwt.sign({
id: String(user.id),
});
if (!auth) {
set.status = 500;
return {
message: "No auth cookie, perhaps your browser is blocking cookies.",
};
}
// set cookie
auth.set({
value: accessToken,
httpOnly: true,
secure: !HTTP_ALLOWED,
maxAge: 60 * 60 * 24 * 7,
sameSite: "strict",
});
return redirect(`${WEBROOT}/`, 302);
},
{ body: "signIn" },
)
.get("/login", async ({ jwt, redirect, cookie: { auth } }) => {
if (FIRST_RUN) {
return redirect(`${WEBROOT}/setup`, 302);
}
// if already logged in, redirect to home
if (auth?.value) {
const user = await jwt.verify(auth.value);
if (user) {
return redirect(`${WEBROOT}/`, 302);
}
auth.remove();
}
return (
<BaseHtml webroot={WEBROOT} title="ConvertX | Login">
<>
<Header
webroot={WEBROOT}
accountRegistration={ACCOUNT_REGISTRATION}
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
hideHistory={HIDE_HISTORY}
/>
<main
class={`
w-full flex-1 px-2
sm:px-4
`}
>
<article class="article">
<form method="post" class="flex flex-col gap-4">
<fieldset class="mb-4 flex flex-col gap-4">
<label class="flex flex-col gap-1">
Email
<input
type="email"
name="email"
class="rounded-sm bg-neutral-800 p-3"
placeholder="Email"
autocomplete="email"
required
/>
</label>
<label class="flex flex-col gap-1">
Password
<input
type="password"
name="password"
class="rounded-sm bg-neutral-800 p-3"
placeholder="Password"
autocomplete="current-password"
required
/>
</label>
</fieldset>
<div class="flex flex-row gap-4">
{ACCOUNT_REGISTRATION ? (
<a
href={`${WEBROOT}/register`}
role="button"
class="w-full btn-secondary text-center"
>
Register
</a>
) : null}
<input type="submit" value="Login" class="w-full btn-primary" />
</div>
</form>
</article>
</main>
</>
</BaseHtml>
);
})
.post(
"/login",
async function handler({ body, set, redirect, jwt, cookie: { auth } }) {
const existingUser = db.query("SELECT * FROM users WHERE email = ?").as(User).get(body.email);
if (!existingUser) {
set.status = 403;
return {
message: "Invalid credentials.",
};
}
const validPassword = await Bun.password.verify(body.password, existingUser.password);
if (!validPassword) {
set.status = 403;
return {
message: "Invalid credentials.",
};
}
const accessToken = await jwt.sign({
id: String(existingUser.id),
});
if (!auth) {
set.status = 500;
return {
message: "No auth cookie, perhaps your browser is blocking cookies.",
};
}
// set cookie
auth.set({
value: accessToken,
httpOnly: true,
secure: !HTTP_ALLOWED,
maxAge: 60 * 60 * 24 * 7,
sameSite: "strict",
});
return redirect(`${WEBROOT}/`, 302);
},
{ body: "signIn" },
)
.get("/logoff", ({ redirect, cookie: { auth } }) => {
if (auth?.value) {
auth.remove();
}
return redirect(`${WEBROOT}/login`, 302);
})
.post("/logoff", ({ redirect, cookie: { auth } }) => {
if (auth?.value) {
auth.remove();
}
return redirect(`${WEBROOT}/login`, 302);
})
.get("/account", async ({ jwt, redirect, cookie: { auth } }) => {
if (!auth?.value) {
return redirect(`${WEBROOT}/`);
}
const user = await jwt.verify(auth.value);
if (!user) {
return redirect(`${WEBROOT}/`, 302);
}
const userData = db.query("SELECT * FROM users WHERE id = ?").as(User).get(user.id);
if (!userData) {
return redirect(`${WEBROOT}/`, 302);
}
return (
<BaseHtml webroot={WEBROOT} title="ConvertX | Account">
<>
<Header
webroot={WEBROOT}
accountRegistration={ACCOUNT_REGISTRATION}
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
hideHistory={HIDE_HISTORY}
loggedIn
/>
<main
class={`
w-full flex-1 px-2
sm:px-4
`}
>
<article class="article">
<form method="post" class="flex flex-col gap-4">
<fieldset class="mb-4 flex flex-col gap-4">
<label class="flex flex-col gap-1">
Email
<input
type="email"
name="email"
class="rounded-sm bg-neutral-800 p-3"
placeholder="Email"
autocomplete="email"
value={userData.email}
required
/>
</label>
<label class="flex flex-col gap-1">
Password (leave blank for unchanged)
<input
type="password"
name="newPassword"
class="rounded-sm bg-neutral-800 p-3"
placeholder="Password"
autocomplete="new-password"
/>
</label>
<label class="flex flex-col gap-1">
Current Password
<input
type="password"
name="password"
class="rounded-sm bg-neutral-800 p-3"
placeholder="Password"
autocomplete="current-password"
required
/>
</label>
</fieldset>
<div role="group">
<input type="submit" value="Update" class="w-full btn-primary" />
</div>
</form>
</article>
</main>
</>
</BaseHtml>
);
})
.post(
"/account",
async function handler({ body, set, redirect, jwt, cookie: { auth } }) {
if (!auth?.value) {
return redirect(`${WEBROOT}/login`, 302);
}
const user = await jwt.verify(auth.value);
if (!user) {
return redirect(`${WEBROOT}/login`, 302);
}
const existingUser = db.query("SELECT * FROM users WHERE id = ?").as(User).get(user.id);
if (!existingUser) {
if (auth?.value) {
auth.remove();
}
return redirect(`${WEBROOT}/login`, 302);
}
const validPassword = await Bun.password.verify(body.password, existingUser.password);
if (!validPassword) {
set.status = 403;
return {
message: "Invalid credentials.",
};
}
const fields = [];
const values = [];
if (body.email) {
const existingUser = await db
.query("SELECT id FROM users WHERE email = ?")
.as(User)
.get(body.email);
if (existingUser && existingUser.id.toString() !== user.id) {
set.status = 409;
return { message: "Email already in use." };
}
fields.push("email");
values.push(body.email);
}
if (body.newPassword) {
fields.push("password");
values.push(await Bun.password.hash(body.newPassword));
}
if (fields.length > 0) {
db.query(
`UPDATE users SET ${fields.map((field) => `${field}=?`).join(", ")} WHERE id=?`,
).run(...values, user.id);
}
return redirect(`${WEBROOT}/`, 302);
},
{
body: t.Object({
email: t.MaybeEmpty(t.String()),
newPassword: t.MaybeEmpty(t.String()),
password: t.String(),
}),
},
);