mirror of
https://github.com/C4illin/ConvertX.git
synced 2025-10-28 02:23:31 +00:00
Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce41ee2387 | ||
|
|
01c8fad012 | ||
|
|
908e91cb91 | ||
|
|
f1c5cd9f6b | ||
|
|
6ea3058e66 | ||
|
|
a4e741cc0a | ||
|
|
0f2172d61f | ||
|
|
2baa69ca17 | ||
|
|
3bbfa9186e | ||
|
|
c1428f5c2b | ||
|
|
1be11708c4 | ||
|
|
8c04b318fd | ||
|
|
ff2c0057e8 | ||
|
|
c830721e02 | ||
|
|
625e1a51f6 | ||
|
|
6af1e8f326 | ||
|
|
82f0e14abf | ||
|
|
9e759a75de | ||
|
|
33388cf209 | ||
|
|
2490c3a7e7 | ||
|
|
7f86c352e3 | ||
|
|
2a3b08487e | ||
|
|
29ba229bc2 | ||
|
|
50725edd02 | ||
|
|
40d1d8a191 | ||
|
|
3417564278 | ||
|
|
9a49dedaca | ||
|
|
d9076bf42a | ||
|
|
b9bbf7792f | ||
|
|
5cc6678ceb | ||
|
|
b47e5755f6 | ||
|
|
af5c768dc7 | ||
|
|
3b573cccae | ||
|
|
0c6f6d6904 | ||
|
|
6e2fe27f31 | ||
|
|
b6cdd3741a | ||
|
|
00f95b6daa | ||
|
|
2d05bbf86b | ||
|
|
2c87a6c8c2 | ||
|
|
254509db5e | ||
|
|
4e4c029cb8 | ||
|
|
6dc60679bb | ||
|
|
6e5d5d9de0 | ||
|
|
6289c033c8 | ||
|
|
b200049a81 | ||
|
|
5646f79f99 | ||
|
|
5083968b80 | ||
|
|
2eb9b8fe96 | ||
|
|
8dc60b41ff | ||
|
|
b4be479d02 | ||
|
|
f56a93a1b2 | ||
|
|
ff8b9fca67 | ||
|
|
0579f1852b | ||
|
|
52d4cc0d03 | ||
|
|
2c68016ca6 |
@@ -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
21
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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`
|
||||
14
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
14
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal 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.
|
||||
244
.github/workflows/docker-publish.yml
vendored
244
.github/workflows/docker-publish.yml
vendored
@@ -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 }}'
|
||||
|
||||
18
.github/workflows/dockerhub-description.yml
vendored
18
.github/workflows/dockerhub-description.yml
vendored
@@ -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
|
||||
|
||||
10
.github/workflows/remove-docker-tag.yml
vendored
10
.github/workflows/remove-docker-tag.yml
vendored
@@ -14,8 +14,8 @@ jobs:
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Remove Docker Tag
|
||||
uses: ArchieAtkinson/remove-dockertag-action@v0.0
|
||||
with:
|
||||
tag_name: master
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Remove Docker Tag
|
||||
uses: ArchieAtkinson/remove-dockertag-action@v0.0
|
||||
with:
|
||||
tag_name: master
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true
|
||||
}
|
||||
}
|
||||
|
||||
182
CHANGELOG.md
182
CHANGELOG.md
@@ -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))
|
||||
|
||||
72
Dockerfile
72
Dockerfile
@@ -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
314
README.md
@@ -1,155 +1,159 @@
|
||||

|
||||
|
||||
# ConvertX
|
||||
|
||||
[](https://github.com/C4illin/ConvertX/actions/workflows/docker-publish.yml)
|
||||
[](https://github.com/C4illin/ConvertX/pkgs/container/ConvertX)
|
||||
[](https://hub.docker.com/r/c4illin/convertx)
|
||||
[](https://github.com/C4illin/ConvertX/pkgs/container/convertx)
|
||||

|
||||

|
||||

|
||||
<!--  -->
|
||||
|
||||
A self-hosted online file converter. Supports over a thousand different formats. Written with TypeScript, Bun and Elysia.
|
||||
|
||||
## 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 |
|
||||
|
||||

|
||||

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

|
||||
|
||||
## 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
|
||||
|
||||
[](https://github.com/C4illin/ConvertX/actions/workflows/docker-publish.yml)
|
||||
[](https://github.com/C4illin/ConvertX/pkgs/container/ConvertX)
|
||||
[](https://hub.docker.com/r/c4illin/convertx)
|
||||
[](https://github.com/C4illin/ConvertX/pkgs/container/convertx)
|
||||

|
||||

|
||||

|
||||
|
||||
<a href="https://trendshift.io/repositories/13818" target="_blank"><img src="https://trendshift.io/api/badge/repositories/13818" alt="C4illin%2FConvertX | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
|
||||
<!--  -->
|
||||
|
||||
A self-hosted online file converter. Supports over a thousand different formats. Written with TypeScript, Bun and Elysia.
|
||||
|
||||
## Features
|
||||
|
||||
- Convert files to different formats
|
||||
- Process multiple files at once
|
||||
- Password protection
|
||||
- Multiple accounts
|
||||
|
||||
## Converters supported
|
||||
|
||||
| Converter | Use case | Converts from | Converts to |
|
||||
| ------------------------------------------------ | ---------------- | ------------- | ----------- |
|
||||
| [libjxl](https://github.com/libjxl/libjxl) | JPEG XL | 11 | 11 |
|
||||
| [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 |
|
||||
|
||||

|
||||

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

|
||||
|
||||
## 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>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
37
compose.yaml
37
compose.yaml
@@ -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
|
||||
|
||||
123
eslint.config.ts
123
eslint.config.ts
@@ -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
9
knip.json
Normal 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"]
|
||||
}
|
||||
57
package.json
57
package.json
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export default {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
export default {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
const config = {
|
||||
arrowParens: "always",
|
||||
printWidth: 80,
|
||||
printWidth: 100,
|
||||
singleQuote: false,
|
||||
semi: true,
|
||||
tabWidth: 2,
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
2
reset.d.ts
vendored
@@ -1 +1 @@
|
||||
import "@total-typescript/ts-reset";
|
||||
import "@total-typescript/ts-reset";
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
48
src/converters/dvisvgm.ts
Normal 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
@@ -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");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
484
src/converters/imagemagick.ts
Normal file
484
src/converters/imagemagick.ts
Normal 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");
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
41
src/db/db.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Database } from "bun:sqlite";
|
||||
|
||||
const db = new Database("./data/mydb.sqlite", { create: true });
|
||||
|
||||
if (!db.query("SELECT * FROM sqlite_master WHERE type='table'").get()) {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT NOT NULL,
|
||||
password TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS file_names (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
job_id INTEGER NOT NULL,
|
||||
file_name TEXT NOT NULL,
|
||||
output_file_name TEXT NOT NULL,
|
||||
status TEXT DEFAULT 'not started',
|
||||
FOREIGN KEY (job_id) REFERENCES jobs(id)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS jobs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
date_created TEXT NOT NULL,
|
||||
status TEXT DEFAULT 'not started',
|
||||
num_files INTEGER DEFAULT 0,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
PRAGMA user_version = 1;`);
|
||||
}
|
||||
|
||||
const dbVersion = (db.query("PRAGMA user_version").get() as { user_version?: number }).user_version;
|
||||
if (dbVersion === 0) {
|
||||
db.exec("ALTER TABLE file_names ADD COLUMN status TEXT DEFAULT 'not started';");
|
||||
db.exec("PRAGMA user_version = 1;");
|
||||
console.log("Updated database to version 1.");
|
||||
}
|
||||
|
||||
// enable WAL mode
|
||||
db.exec("PRAGMA journal_mode = WAL;");
|
||||
|
||||
export default db;
|
||||
23
src/db/types.ts
Normal file
23
src/db/types.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export class Filename {
|
||||
id!: number;
|
||||
job_id!: number;
|
||||
file_name!: string;
|
||||
output_file_name!: string;
|
||||
status!: string;
|
||||
}
|
||||
|
||||
export class Jobs {
|
||||
finished_files!: number;
|
||||
id!: number;
|
||||
user_id!: number;
|
||||
date_created!: string;
|
||||
status!: string;
|
||||
num_files!: number;
|
||||
files_detailed!: Filename[];
|
||||
}
|
||||
|
||||
export class User {
|
||||
id!: number;
|
||||
email!: string;
|
||||
password!: string;
|
||||
}
|
||||
15
src/helpers/env.ts
Normal file
15
src/helpers/env.ts
Normal 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 ?? "";
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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.");
|
||||
|
||||
1552
src/index.tsx
1552
src/index.tsx
File diff suppressed because it is too large
Load Diff
22
src/main.css
22
src/main.css
@@ -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;
|
||||
|
||||
67
src/pages/chooseConverter.tsx
Normal file
67
src/pages/chooseConverter.tsx
Normal 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
116
src/pages/convert.tsx
Normal 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
41
src/pages/deleteFile.tsx
Normal 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
62
src/pages/download.tsx
Normal 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
216
src/pages/history.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
80
src/pages/listConverters.tsx
Normal file
80
src/pages/listConverters.tsx
Normal 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
215
src/pages/results.tsx
Normal 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
240
src/pages/root.tsx
Normal 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
48
src/pages/upload.tsx
Normal 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
509
src/pages/user.tsx
Normal 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(),
|
||||
}),
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user