Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59310c095d | ||
|
|
c47f0c12fe | ||
|
|
ca71a40485 | ||
|
|
d4e8f376c1 | ||
|
|
14c6ea1e6b | ||
|
|
d6e4d8fbd6 | ||
|
|
460bda62d5 | ||
|
|
d2702ab673 | ||
|
|
e2581f42f5 | ||
|
|
f0fcfc159f | ||
|
|
538c5b60c9 | ||
|
|
2fabb7bbb2 | ||
|
|
e7c34a9c94 | ||
|
|
618f9fce5a | ||
|
|
95dbc9f678 | ||
|
|
aa87bc5c51 | ||
|
|
815de531ed | ||
|
|
cf2b026dc4 | ||
|
|
9ce46aefba | ||
|
|
98b2db7818 | ||
|
|
0229851bf9 | ||
|
|
9e15114fe8 | ||
|
|
7f66a76bb0 | ||
|
|
e9cc8392bb | ||
|
|
d0b89ce74f | ||
|
|
f537c81db7 | ||
|
|
03d3edfff6 | ||
|
|
447b4c5e5c | ||
|
|
cb143209ae | ||
|
|
9c24fe73b5 | ||
|
|
19ae85424b | ||
|
|
22fad99552 | ||
|
|
8144bbef74 | ||
|
|
aad6da0ae8 | ||
|
|
c5f8162a22 | ||
|
|
f0f30224b5 | ||
|
|
d0d888e356 | ||
|
|
2c64122224 | ||
|
|
3b2eee96a9 | ||
|
|
465aacbf9b | ||
|
|
d1a2a66170 | ||
|
|
4c05fd72bb | ||
|
|
f04fe760e3 | ||
|
|
834d19bcc6 | ||
|
|
6808c4642c | ||
|
|
d0ce307f94 | ||
|
|
f3740e9ded | ||
|
|
b485bc9445 | ||
|
|
2d14c1bb26 | ||
|
|
1a442d6e69 | ||
|
|
2386543e5c | ||
|
|
58e220e82d | ||
|
|
24bea6e4d2 | ||
|
|
43497ad8d1 | ||
|
|
f22b61fe4c | ||
|
|
5b08f4cd19 | ||
|
|
1589f8d24e | ||
|
|
7d1db72cf5 | ||
|
|
53a8f66414 | ||
|
|
36cb6cc589 | ||
|
|
f3a4aece46 | ||
|
|
580a6a869a | ||
|
|
008eaac493 | ||
|
|
b450623bb4 | ||
|
|
8ac2ecb673 | ||
|
|
0a10a56ae3 | ||
|
|
9378ba9208 | ||
|
|
0c586e324b | ||
|
|
91c4a64284 | ||
|
|
c599e98d9d | ||
|
|
d2cd6706c9 | ||
|
|
e8ed10dde8 | ||
|
|
5fe0b79802 | ||
|
|
34a6722a68 | ||
|
|
5b0d769c63 | ||
|
|
718401a28b | ||
|
|
3112cd57f6 | ||
|
|
410fc777a7 | ||
|
|
8eed99e732 |
@@ -1,16 +1,20 @@
|
||||
node_modules
|
||||
Dockerfile*
|
||||
docker-compose*
|
||||
.dockerignore
|
||||
.editorconfig
|
||||
.env
|
||||
.git
|
||||
.gitignore
|
||||
README.md
|
||||
LICENSE
|
||||
.vscode
|
||||
Makefile
|
||||
helm-charts
|
||||
.env
|
||||
.editorconfig
|
||||
.idea
|
||||
.vscode
|
||||
CHANGELOG.md
|
||||
coverage*
|
||||
data
|
||||
data
|
||||
docker-compose*
|
||||
Dockerfile*
|
||||
eslint.config.js
|
||||
helm-charts
|
||||
LICENSE
|
||||
Makefile
|
||||
node_modules
|
||||
prettier.config.js
|
||||
README.md
|
||||
renovate.json
|
||||
149
.github/workflows/docker-publish.yml
vendored
@@ -1,69 +1,80 @@
|
||||
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 }}
|
||||
|
||||
|
||||
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 }}
|
||||
|
||||
# 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 }}
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
27
.github/workflows/dockerhub-description.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: Update Docker Hub Description
|
||||
|
||||
env:
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
DOCKERHUB_USERNAME: c4illin
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- README.md
|
||||
- .github/workflows/dockerhub-description.yml
|
||||
jobs:
|
||||
dockerHubDescription:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Docker Hub Description
|
||||
uses: peter-evans/dockerhub-description@v4
|
||||
with:
|
||||
username: ${{ env.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
repository: ${{ env.IMAGE_NAME }}
|
||||
short-description: ${{ github.event.repository.description }}
|
||||
enable-url-completion: true
|
||||
2
.gitignore
vendored
@@ -48,4 +48,4 @@ package-lock.json
|
||||
/data
|
||||
/Bruno
|
||||
/tsconfig.tsbuildinfo
|
||||
/src/public/generated.css
|
||||
/public/generated.css
|
||||
|
||||
29
CHANGELOG.md
@@ -1,5 +1,34 @@
|
||||
# Changelog
|
||||
|
||||
## [0.10.0](https://github.com/C4illin/ConvertX/compare/v0.9.0...v0.10.0) (2025-01-18)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add calibre ([03d3edf](https://github.com/C4illin/ConvertX/commit/03d3edfff65c252dd4b8922fc98257c089c1ff74)), closes [#191](https://github.com/C4illin/ConvertX/issues/191)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add FFMPEG_ARGS env variable ([f537c81](https://github.com/C4illin/ConvertX/commit/f537c81db7815df8017f834e3162291197e1c40f)), closes [#190](https://github.com/C4illin/ConvertX/issues/190)
|
||||
* add qt6-qtbase-private-dev from community repo ([95dbc9f](https://github.com/C4illin/ConvertX/commit/95dbc9f678bec7e6e2c03587e1473fb8ff708ea3))
|
||||
* skip account setup when ALLOW_UNAUTHENTICATED is true ([538c5b6](https://github.com/C4illin/ConvertX/commit/538c5b60c9e27a8184740305475245da79bae143))
|
||||
|
||||
## [0.9.0](https://github.com/C4illin/ConvertX/compare/v0.8.1...v0.9.0) (2024-11-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add inkscape for vector images ([f3740e9](https://github.com/C4illin/ConvertX/commit/f3740e9ded100b8500f3613517960248bbd3c210))
|
||||
* Allow to chose webroot ([36cb6cc](https://github.com/C4illin/ConvertX/commit/36cb6cc589d80d0a87fa8dbe605db71a9a2570f9)), closes [#180](https://github.com/C4illin/ConvertX/issues/180)
|
||||
* disable convert when uploading ([58e220e](https://github.com/C4illin/ConvertX/commit/58e220e82d7f9c163d6ea4dc31092c08a3e254f4)), closes [#177](https://github.com/C4illin/ConvertX/issues/177)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* treat unknown as m4a ([1a442d6](https://github.com/C4illin/ConvertX/commit/1a442d6e69606afef63b1e7df36aa83d111fa23d)), closes [#178](https://github.com/C4illin/ConvertX/issues/178)
|
||||
* wait for both upload and selection ([4c05fd7](https://github.com/C4illin/ConvertX/commit/4c05fd72bbbf91ee02327f6fcbf749b78272376b)), closes [#177](https://github.com/C4illin/ConvertX/issues/177)
|
||||
|
||||
## [0.8.1](https://github.com/C4illin/ConvertX/compare/v0.8.0...v0.8.1) (2024-10-05)
|
||||
|
||||
|
||||
|
||||
12
Dockerfile
@@ -1,4 +1,4 @@
|
||||
FROM oven/bun:1.1.29-alpine AS base
|
||||
FROM oven/bun:1.1.45-alpine AS base
|
||||
LABEL org.opencontainers.image.source="https://github.com/C4illin/ConvertX"
|
||||
WORKDIR /app
|
||||
|
||||
@@ -50,14 +50,20 @@ RUN apk --no-cache add \
|
||||
vips-poppler \
|
||||
vips-jxl \
|
||||
libjxl-tools \
|
||||
assimp
|
||||
assimp \
|
||||
inkscape \
|
||||
poppler-utils
|
||||
|
||||
RUN apk --no-cache add qt6-qtbase-private-dev --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community/
|
||||
|
||||
RUN apk --no-cache add calibre --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing/
|
||||
|
||||
# this might be needed for some latex use cases, will add it if needed.
|
||||
# texmf-dist-fontsextra \
|
||||
|
||||
COPY --from=install /temp/prod/node_modules node_modules
|
||||
COPY --from=builder /root/.cargo/bin/resvg /usr/local/bin/resvg
|
||||
COPY --from=prerelease /app/src/public/generated.css /app/src/public/
|
||||
COPY --from=prerelease /app/public/generated.css /app/public/
|
||||
# COPY --from=prerelease /app/src/index.tsx /app/src/
|
||||
# COPY --from=prerelease /app/package.json .
|
||||
COPY . .
|
||||
|
||||
259
README.md
@@ -1,111 +1,148 @@
|
||||

|
||||
# ConvertX
|
||||
[](https://github.com/C4illin/ConvertX/actions/workflows/docker-publish.yml)
|
||||
[](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 |
|
||||
| [Assimp](https://github.com/assimp/assimp) | 3D Assets | 70 | 24 |
|
||||
| [XeLaTeX](https://tug.org/xetex/) | LaTeX | 1 | 1 |
|
||||
| [Pandoc](https://pandoc.org/) | Documents | 43 | 65 |
|
||||
| [GraphicsMagick](http://www.graphicsmagick.org/) | Images | 166 | 133 |
|
||||
| [FFmpeg](https://ffmpeg.org/) | Video | ~473 | ~280 |
|
||||
|
||||
<!-- many ffmpeg fileformats are duplicates -->
|
||||
|
||||
Any missing converter? Open an issue or pull request!
|
||||
|
||||
## Deployment
|
||||
|
||||
```yml
|
||||
# docker-compose.yml
|
||||
services:
|
||||
convertx:
|
||||
image: ghcr.io/c4illin/convertx
|
||||
container_name: convertx
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment: # Defaults are listed below. All are optional.
|
||||
- ACCOUNT_REGISTRATION=false # true or false, doesn't matter for the first account (e.g. keep this to false if you only want one account)
|
||||
- JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234 # will use randomUUID() by default
|
||||
- HTTP_ALLOWED=false # setting this to true is unsafe, only set this to true locally
|
||||
- ALLOW_UNAUTHENTICATED=false # allows anyone to use the service without logging in, only set this to true locally
|
||||
- AUTO_DELETE_EVERY_N_HOURS=24 # checks every n hours for files older then n hours and deletes them, set to 0 to disable
|
||||
volumes:
|
||||
- convertx:/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.
|
||||
|
||||
### Tutorial
|
||||
|
||||
Tutorial in french: https://belginux.com/installer-convertx-avec-docker/
|
||||
|
||||
## 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" />
|
||||
</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 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 |
|
||||
| [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
|
||||
|
||||
```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` |
|
||||
|
||||
> [!WARNING]
|
||||
> If you can't login, make sure you are accessing the service over https or set HTTP_ALLOWED=true
|
||||
|
||||
### Docker images
|
||||
|
||||
There is a `:latest` tag that is updated with every release and a `:main` tag that is updated with every push to the main branch. `:latest` is recommended for normal use.
|
||||
|
||||
The image is available on [GitHub Container Registry](https://github.com/C4illin/ConvertX/pkgs/container/ConvertX) and [Docker Hub](https://hub.docker.com/r/c4illin/convertx).
|
||||
|
||||
| Image | What it is |
|
||||
|-------|------------|
|
||||
| `image: ghcr.io/c4illin/convertx` | The latest release on ghcr |
|
||||
| `image: ghcr.io/c4illin/convertx:main` | The latest commit on ghcr |
|
||||
| `image: c4illin/convertx` | The latest release on docker hub |
|
||||
| `image: c4illin/convertx:main` | The latest commit on docker hub |
|
||||
|
||||
<!-- Dockerhub was introduced in 0.9.0 and older releases -->
|
||||
|
||||
### Tutorial
|
||||
|
||||
Tutorial in french: <https://belginux.com/installer-convertx-avec-docker/>
|
||||
|
||||
Tutorial in 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>
|
||||
|
||||
32
compose.yaml
@@ -1,15 +1,17 @@
|
||||
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
|
||||
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=true # setting this to true is unsafe, only set this to true locally
|
||||
- ALLOW_UNAUTHENTICATED=true # allows anyone to use the service without logging in, only set this to true locally
|
||||
- AUTO_DELETE_EVERY_N_HOURS=1 # checks every n hours for files older then n hours and deletes them, set to 0 to disable
|
||||
# - FFMPEG_ARGS=-hwaccel vulkan # additional arguments to pass to ffmpeg
|
||||
# - WEBROOT=/convertx # the root path of the web interface, leave empty to disable
|
||||
ports:
|
||||
- 3000:3000
|
||||
|
||||
54
package.json
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "convertx-frontend",
|
||||
"version": "0.8.1",
|
||||
"version": "0.10.0",
|
||||
"scripts": {
|
||||
"dev": "bun run --watch src/index.tsx",
|
||||
"hot": "bun run --hot src/index.tsx",
|
||||
"format": "eslint --fix .",
|
||||
"build": "postcss ./src/main.css -o ./src/public/generated.css",
|
||||
"build": "postcss ./src/main.css -o ./public/generated.css",
|
||||
"lint": "run-p 'lint:*'",
|
||||
"lint:tsc": "tsc --noEmit",
|
||||
"lint:knip": "knip",
|
||||
@@ -13,10 +13,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@elysiajs/cookie": "^0.8.0",
|
||||
"@elysiajs/html": "1.0.2",
|
||||
"@elysiajs/jwt": "^1.1.1",
|
||||
"@elysiajs/static": "1.0.3",
|
||||
"elysia": "^1.1.17"
|
||||
"@elysiajs/html": "^1.2.0",
|
||||
"@elysiajs/jwt": "^1.2.0",
|
||||
"@elysiajs/static": "^1.2.0",
|
||||
"@kitajs/html": "^4.2.7",
|
||||
"elysia": "^1.2.10"
|
||||
},
|
||||
"module": "src/index.tsx",
|
||||
"type": "module",
|
||||
@@ -24,38 +25,31 @@
|
||||
"start": "bun run src/index.tsx"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.1.1",
|
||||
"@eslint/js": "^9.12.0",
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.3.1",
|
||||
"@kitajs/ts-html-plugin": "^4.1.0",
|
||||
"@eslint/compat": "^1.2.5",
|
||||
"@eslint/js": "^9.18.0",
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.4.1",
|
||||
"@kitajs/ts-html-plugin": "^4.1.1",
|
||||
"@total-typescript/ts-reset": "^0.6.1",
|
||||
"@types/bun": "^1.1.10",
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@types/bun": "^1.1.16",
|
||||
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
||||
"@types/eslint__js": "^8.42.3",
|
||||
"@types/node": "^22.7.4",
|
||||
"@typescript-eslint/eslint-plugin": "^8.7.0",
|
||||
"@typescript-eslint/parser": "^8.7.0",
|
||||
"@types/node": "^22.10.7",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"cssnano": "^7.0.6",
|
||||
"eslint": "^9.12.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-plugin-deprecation": "^3.0.0",
|
||||
"eslint-plugin-isaacscript": "^4.0.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-readable-tailwind": "^1.8.1",
|
||||
"eslint-plugin-readable-tailwind": "^1.8.2",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"eslint-plugin-tailwindcss": "^3.17.4",
|
||||
"globals": "^15.9.0",
|
||||
"knip": "^5.30.6",
|
||||
"npm-run-all2": "^6.2.3",
|
||||
"postcss": "^8.4.47",
|
||||
"eslint-plugin-tailwindcss": "^3.17.5",
|
||||
"globals": "^15.14.0",
|
||||
"knip": "^5.42.1",
|
||||
"npm-run-all2": "^7.0.2",
|
||||
"postcss": "^8.5.1",
|
||||
"postcss-cli": "^11.0.0",
|
||||
"postcss-lightningcss": "^1.0.1",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier": "^3.4.2",
|
||||
"tailwind-scrollbar": "^3.1.0",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"typescript": "^5.6.2",
|
||||
"typescript-eslint": "^8.8.0"
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.20.0"
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
|
||||
...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {})
|
||||
}
|
||||
}
|
||||
8
postcss.config.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import autoprefixer from "autoprefixer";
|
||||
import cssnano from "cssnano";
|
||||
import tailwind from "tailwindcss";
|
||||
import tailwindConfig from "./tailwind.config.js";
|
||||
|
||||
export default {
|
||||
plugins: [autoprefixer, tailwind(tailwindConfig), cssnano],
|
||||
};
|
||||
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 476 B After Width: | Height: | Size: 476 B |
|
Before Width: | Height: | Size: 960 B After Width: | Height: | Size: 960 B |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -1,3 +1,5 @@
|
||||
const webroot = document.querySelector("meta[name='webroot']").content;
|
||||
|
||||
window.downloadAll = function () {
|
||||
// Get all download links
|
||||
const downloadLinks = document.querySelectorAll("a[download]");
|
||||
@@ -18,7 +20,7 @@ let progressElem = document.querySelector("progress");
|
||||
const refreshData = () => {
|
||||
// console.log("Refreshing data...", progressElem.value, progressElem.max);
|
||||
if (progressElem.value !== progressElem.max) {
|
||||
fetch(`/progress/${jobId}`, {
|
||||
fetch(`${webroot}/progress/${jobId}`, {
|
||||
method: "POST",
|
||||
})
|
||||
.then((res) => res.text())
|
||||
@@ -1,219 +1,236 @@
|
||||
// Select the file input element
|
||||
const fileInput = document.querySelector('input[type="file"]');
|
||||
const dropZone = document.getElementById("dropzone");
|
||||
const fileNames = [];
|
||||
let fileType;
|
||||
|
||||
dropZone.addEventListener("dragover", () => {
|
||||
dropZone.classList.add("dragover");
|
||||
});
|
||||
|
||||
dropZone.addEventListener("dragleave", () => {
|
||||
dropZone.classList.remove("dragover");
|
||||
});
|
||||
|
||||
const selectContainer = document.querySelector("form .select_container");
|
||||
|
||||
const updateSearchBar = () => {
|
||||
const convertToInput = document.querySelector(
|
||||
"input[name='convert_to_search']",
|
||||
);
|
||||
const convertToPopup = document.querySelector(".convert_to_popup");
|
||||
const convertToGroupElements = document.querySelectorAll(".convert_to_group");
|
||||
const convertToGroups = {};
|
||||
const convertToElement = document.querySelector("select[name='convert_to']");
|
||||
const convertButton = document.querySelector("input[type='submit']");
|
||||
|
||||
const showMatching = (search) => {
|
||||
for (const [targets, groupElement] of Object.values(convertToGroups)) {
|
||||
let matchingTargetsFound = 0;
|
||||
for (const target of targets) {
|
||||
if (target.dataset.target.includes(search)) {
|
||||
matchingTargetsFound++;
|
||||
target.classList.remove("hidden");
|
||||
target.classList.add("flex");
|
||||
} else {
|
||||
target.classList.add("hidden");
|
||||
target.classList.remove("flex");
|
||||
}
|
||||
}
|
||||
|
||||
if (matchingTargetsFound === 0) {
|
||||
groupElement.classList.add("hidden");
|
||||
groupElement.classList.remove("flex");
|
||||
} else {
|
||||
groupElement.classList.remove("hidden");
|
||||
groupElement.classList.add("flex");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (const groupElement of convertToGroupElements) {
|
||||
const groupName = groupElement.dataset.converter;
|
||||
|
||||
const targetElements = groupElement.querySelectorAll(".target");
|
||||
const targets = Array.from(targetElements);
|
||||
|
||||
for (const target of targets) {
|
||||
target.onmousedown = () => {
|
||||
convertToElement.value = target.dataset.value;
|
||||
convertToInput.value = `${target.dataset.target} using ${target.dataset.converter}`;
|
||||
convertButton.disabled = false;
|
||||
showMatching("");
|
||||
};
|
||||
}
|
||||
|
||||
convertToGroups[groupName] = [targets, groupElement];
|
||||
}
|
||||
|
||||
convertToInput.addEventListener("input", (e) => {
|
||||
showMatching(e.target.value.toLowerCase());
|
||||
});
|
||||
|
||||
convertToInput.addEventListener("search", () => {
|
||||
// when the user clears the search bar using the 'x' button
|
||||
convertButton.disabled = true;
|
||||
});
|
||||
|
||||
convertToInput.addEventListener("blur", (e) => {
|
||||
// Keep the popup open even when clicking on a target button
|
||||
// for a split second to allow the click to go through
|
||||
if (e?.relatedTarget?.classList?.contains("target")) {
|
||||
convertToPopup.classList.add("hidden");
|
||||
convertToPopup.classList.remove("flex");
|
||||
return;
|
||||
}
|
||||
|
||||
convertToPopup.classList.add("hidden");
|
||||
convertToPopup.classList.remove("flex");
|
||||
});
|
||||
|
||||
convertToInput.addEventListener("focus", () => {
|
||||
convertToPopup.classList.remove("hidden");
|
||||
convertToPopup.classList.add("flex");
|
||||
});
|
||||
};
|
||||
|
||||
// const convertFromSelect = document.querySelector("select[name='convert_from']");
|
||||
|
||||
// Add a 'change' event listener to the file input element
|
||||
fileInput.addEventListener("change", (e) => {
|
||||
// console.log(e.target.files);
|
||||
// Get the selected files from the event target
|
||||
const files = e.target.files;
|
||||
|
||||
// Select the file-list table
|
||||
const fileList = document.querySelector("#file-list");
|
||||
|
||||
// Loop through the selected files
|
||||
for (const file of files) {
|
||||
// Create a new table row for each file
|
||||
const row = document.createElement("tr");
|
||||
row.innerHTML = `
|
||||
<td>${file.name}</td>
|
||||
<td>${(file.size / 1024).toFixed(2)} kB</td>
|
||||
<td><a class="secondary" onclick="deleteRow(this)" style="cursor: pointer">Remove</a></td>
|
||||
`;
|
||||
|
||||
if (!fileType) {
|
||||
fileType = file.name.split(".").pop();
|
||||
console.log("fileType", fileType);
|
||||
fileInput.setAttribute("accept", `.${fileType}`);
|
||||
setTitle();
|
||||
|
||||
// choose the option that matches the file type
|
||||
// for (const option of convertFromSelect.children) {
|
||||
// console.log(option.value);
|
||||
// if (option.value === fileType) {
|
||||
// option.selected = true;
|
||||
// }
|
||||
// }
|
||||
|
||||
fetch("/conversions", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ fileType: fileType }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((res) => res.text())
|
||||
.then((html) => {
|
||||
selectContainer.innerHTML = html;
|
||||
updateSearchBar();
|
||||
})
|
||||
.catch((err) => console.log(err));
|
||||
}
|
||||
|
||||
// Append the row to the file-list table
|
||||
fileList.appendChild(row);
|
||||
|
||||
// Append the file to the hidden input
|
||||
fileNames.push(file.name);
|
||||
}
|
||||
|
||||
uploadFiles(files);
|
||||
});
|
||||
|
||||
const setTitle = () => {
|
||||
const title = document.querySelector("h1");
|
||||
title.textContent = `Convert ${fileType ? `.${fileType}` : ""}`;
|
||||
};
|
||||
|
||||
// Add a onclick for the delete button
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const deleteRow = (target) => {
|
||||
const filename = target.parentElement.parentElement.children[0].textContent;
|
||||
const row = target.parentElement.parentElement;
|
||||
row.remove();
|
||||
|
||||
// remove from fileNames
|
||||
const index = fileNames.indexOf(filename);
|
||||
fileNames.splice(index, 1);
|
||||
|
||||
// if fileNames is empty, reset fileType
|
||||
if (fileNames.length === 0) {
|
||||
fileType = null;
|
||||
fileInput.removeAttribute("accept");
|
||||
setTitle();
|
||||
}
|
||||
|
||||
fetch("/delete", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ filename: filename }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
console.log(data);
|
||||
})
|
||||
.catch((err) => console.log(err));
|
||||
};
|
||||
|
||||
const uploadFiles = (files) => {
|
||||
const formData = new FormData();
|
||||
|
||||
for (const file of files) {
|
||||
formData.append("file", file, file.name);
|
||||
}
|
||||
|
||||
fetch("/upload", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
console.log(data);
|
||||
})
|
||||
.catch((err) => console.log(err));
|
||||
};
|
||||
|
||||
const formConvert = document.querySelector("form[action='/convert']");
|
||||
|
||||
formConvert.addEventListener("submit", () => {
|
||||
const hiddenInput = document.querySelector("input[name='file_names']");
|
||||
hiddenInput.value = JSON.stringify(fileNames);
|
||||
});
|
||||
|
||||
updateSearchBar();
|
||||
const webroot = document.querySelector("meta[name='webroot']").content;
|
||||
const fileInput = document.querySelector('input[type="file"]');
|
||||
const dropZone = document.getElementById("dropzone");
|
||||
const convertButton = document.querySelector("input[type='submit']");
|
||||
const fileNames = [];
|
||||
let fileType;
|
||||
let pendingFiles = 0;
|
||||
let formatSelected = false;
|
||||
|
||||
dropZone.addEventListener("dragover", () => {
|
||||
dropZone.classList.add("dragover");
|
||||
});
|
||||
|
||||
dropZone.addEventListener("dragleave", () => {
|
||||
dropZone.classList.remove("dragover");
|
||||
});
|
||||
|
||||
dropZone.addEventListener("drop", () => {
|
||||
dropZone.classList.remove("dragover");
|
||||
});
|
||||
|
||||
const selectContainer = document.querySelector("form .select_container");
|
||||
|
||||
const updateSearchBar = () => {
|
||||
const convertToInput = document.querySelector(
|
||||
"input[name='convert_to_search']",
|
||||
);
|
||||
const convertToPopup = document.querySelector(".convert_to_popup");
|
||||
const convertToGroupElements = document.querySelectorAll(".convert_to_group");
|
||||
const convertToGroups = {};
|
||||
const convertToElement = document.querySelector("select[name='convert_to']");
|
||||
|
||||
const showMatching = (search) => {
|
||||
for (const [targets, groupElement] of Object.values(convertToGroups)) {
|
||||
let matchingTargetsFound = 0;
|
||||
for (const target of targets) {
|
||||
if (target.dataset.target.includes(search)) {
|
||||
matchingTargetsFound++;
|
||||
target.classList.remove("hidden");
|
||||
target.classList.add("flex");
|
||||
} else {
|
||||
target.classList.add("hidden");
|
||||
target.classList.remove("flex");
|
||||
}
|
||||
}
|
||||
|
||||
if (matchingTargetsFound === 0) {
|
||||
groupElement.classList.add("hidden");
|
||||
groupElement.classList.remove("flex");
|
||||
} else {
|
||||
groupElement.classList.remove("hidden");
|
||||
groupElement.classList.add("flex");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (const groupElement of convertToGroupElements) {
|
||||
const groupName = groupElement.dataset.converter;
|
||||
|
||||
const targetElements = groupElement.querySelectorAll(".target");
|
||||
const targets = Array.from(targetElements);
|
||||
|
||||
for (const target of targets) {
|
||||
target.onmousedown = () => {
|
||||
convertToElement.value = target.dataset.value;
|
||||
convertToInput.value = `${target.dataset.target} using ${target.dataset.converter}`;
|
||||
formatSelected = true;
|
||||
if (pendingFiles === 0 && fileNames.length > 0) {
|
||||
convertButton.disabled = false;
|
||||
}
|
||||
showMatching("");
|
||||
};
|
||||
}
|
||||
|
||||
convertToGroups[groupName] = [targets, groupElement];
|
||||
}
|
||||
|
||||
convertToInput.addEventListener("input", (e) => {
|
||||
showMatching(e.target.value.toLowerCase());
|
||||
});
|
||||
|
||||
convertToInput.addEventListener("search", () => {
|
||||
// when the user clears the search bar using the 'x' button
|
||||
convertButton.disabled = true;
|
||||
formatSelected = false;
|
||||
});
|
||||
|
||||
convertToInput.addEventListener("blur", (e) => {
|
||||
// Keep the popup open even when clicking on a target button
|
||||
// for a split second to allow the click to go through
|
||||
if (e?.relatedTarget?.classList?.contains("target")) {
|
||||
convertToPopup.classList.add("hidden");
|
||||
convertToPopup.classList.remove("flex");
|
||||
return;
|
||||
}
|
||||
|
||||
convertToPopup.classList.add("hidden");
|
||||
convertToPopup.classList.remove("flex");
|
||||
});
|
||||
|
||||
convertToInput.addEventListener("focus", () => {
|
||||
convertToPopup.classList.remove("hidden");
|
||||
convertToPopup.classList.add("flex");
|
||||
});
|
||||
};
|
||||
|
||||
// Add a 'change' event listener to the file input element
|
||||
fileInput.addEventListener("change", (e) => {
|
||||
// Get the selected files from the event target
|
||||
const files = e.target.files;
|
||||
|
||||
// Select the file-list table
|
||||
const fileList = document.querySelector("#file-list");
|
||||
|
||||
// Loop through the selected files
|
||||
for (const file of files) {
|
||||
// Create a new table row for each file
|
||||
const row = document.createElement("tr");
|
||||
row.innerHTML = `
|
||||
<td>${file.name}</td>
|
||||
<td>${(file.size / 1024).toFixed(2)} kB</td>
|
||||
<td><a onclick="deleteRow(this)">Remove</a></td>
|
||||
`;
|
||||
|
||||
if (!fileType) {
|
||||
fileType = file.name.split(".").pop();
|
||||
fileInput.setAttribute("accept", `.${fileType}`);
|
||||
setTitle();
|
||||
|
||||
// choose the option that matches the file type
|
||||
// for (const option of convertFromSelect.children) {
|
||||
// console.log(option.value);
|
||||
// if (option.value === fileType) {
|
||||
// option.selected = true;
|
||||
// }
|
||||
// }
|
||||
|
||||
fetch(`${webroot}/conversions`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ fileType: fileType }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((res) => res.text())
|
||||
.then((html) => {
|
||||
selectContainer.innerHTML = html;
|
||||
updateSearchBar();
|
||||
})
|
||||
.catch((err) => console.log(err));
|
||||
}
|
||||
|
||||
// Append the row to the file-list table
|
||||
fileList.appendChild(row);
|
||||
|
||||
// Append the file to the hidden input
|
||||
fileNames.push(file.name);
|
||||
}
|
||||
|
||||
uploadFiles(files);
|
||||
});
|
||||
|
||||
const setTitle = () => {
|
||||
const title = document.querySelector("h1");
|
||||
title.textContent = `Convert ${fileType ? `.${fileType}` : ""}`;
|
||||
};
|
||||
|
||||
// Add a onclick for the delete button
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const deleteRow = (target) => {
|
||||
const filename = target.parentElement.parentElement.children[0].textContent;
|
||||
const row = target.parentElement.parentElement;
|
||||
row.remove();
|
||||
|
||||
// remove from fileNames
|
||||
const index = fileNames.indexOf(filename);
|
||||
fileNames.splice(index, 1);
|
||||
|
||||
// reset fileInput
|
||||
fileInput.value = "";
|
||||
|
||||
// if fileNames is empty, reset fileType
|
||||
if (fileNames.length === 0) {
|
||||
fileType = null;
|
||||
fileInput.removeAttribute("accept");
|
||||
convertButton.disabled = true;
|
||||
setTitle();
|
||||
}
|
||||
|
||||
fetch(`${webroot}/delete`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ filename: filename }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.catch((err) => console.log(err));
|
||||
};
|
||||
|
||||
const uploadFiles = (files) => {
|
||||
convertButton.disabled = true;
|
||||
convertButton.textContent = "Uploading...";
|
||||
pendingFiles += 1;
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
for (const file of files) {
|
||||
formData.append("file", file, file.name);
|
||||
}
|
||||
|
||||
fetch(`${webroot}/upload`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
pendingFiles -= 1;
|
||||
if (pendingFiles === 0) {
|
||||
if (formatSelected) {
|
||||
convertButton.disabled = false;
|
||||
}
|
||||
convertButton.textContent = "Convert";
|
||||
}
|
||||
console.log(data);
|
||||
})
|
||||
.catch((err) => console.log(err));
|
||||
};
|
||||
|
||||
const formConvert = document.querySelector(`form[action='${webroot}/convert']`);
|
||||
|
||||
formConvert.addEventListener("submit", () => {
|
||||
const hiddenInput = document.querySelector("input[name='file_names']");
|
||||
hiddenInput.value = JSON.stringify(fileNames);
|
||||
});
|
||||
|
||||
updateSearchBar();
|
||||
@@ -3,5 +3,9 @@
|
||||
"extends": [
|
||||
"config:recommended",
|
||||
":disableDependencyDashboard"
|
||||
]
|
||||
],
|
||||
"lockFileMaintenance": {
|
||||
"enabled": true,
|
||||
"automerge": true
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,39 @@
|
||||
import { Html } from "@elysiajs/html";
|
||||
|
||||
export const BaseHtml = ({
|
||||
children,
|
||||
title = "ConvertX",
|
||||
}: { children: JSX.Element; title?: string }) => (
|
||||
webroot = "",
|
||||
}: {
|
||||
children: JSX.Element;
|
||||
title?: string;
|
||||
webroot?: string;
|
||||
}) => (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="webroot" content={webroot} />
|
||||
<title safe>{title}</title>
|
||||
<link rel="stylesheet" href="/generated.css" />
|
||||
<link rel="stylesheet" href={`${webroot}/generated.css`} />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="/apple-touch-icon.png"
|
||||
href={`${webroot}/apple-touch-icon.png`}
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="/favicon-32x32.png"
|
||||
href={`${webroot}/favicon-32x32.png`}
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="/favicon-16x16.png"
|
||||
href={`${webroot}/favicon-16x16.png`}
|
||||
/>
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<link rel="manifest" href={`${webroot}/site.webmanifest`} />
|
||||
</head>
|
||||
<body class="w-full bg-neutral-900 text-neutral-200">{children}</body>
|
||||
</html>
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import { Html } from "@kitajs/html";
|
||||
|
||||
export const Header = ({
|
||||
loggedIn,
|
||||
accountRegistration,
|
||||
}: { loggedIn?: boolean; accountRegistration?: boolean }) => {
|
||||
allowUnauthenticated,
|
||||
webroot = "",
|
||||
}: {
|
||||
loggedIn?: boolean;
|
||||
accountRegistration?: boolean;
|
||||
allowUnauthenticated?: boolean;
|
||||
webroot?: string;
|
||||
}) => {
|
||||
let rightNav: JSX.Element;
|
||||
if (loggedIn) {
|
||||
rightNav = (
|
||||
@@ -12,20 +21,24 @@ export const Header = ({
|
||||
text-accent-600 transition-all
|
||||
hover:text-accent-500 hover:underline
|
||||
`}
|
||||
href="/history">
|
||||
href={`${webroot}/history`}
|
||||
>
|
||||
History
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
class={`
|
||||
text-accent-600 transition-all
|
||||
hover:text-accent-500 hover:underline
|
||||
`}
|
||||
href="/logoff">
|
||||
Logout
|
||||
</a>
|
||||
</li>
|
||||
{!allowUnauthenticated ? (
|
||||
<li>
|
||||
<a
|
||||
class={`
|
||||
text-accent-600 transition-all
|
||||
hover:text-accent-500 hover:underline
|
||||
`}
|
||||
href={`${webroot}/logoff`}
|
||||
>
|
||||
Logout
|
||||
</a>
|
||||
</li>
|
||||
) : null}
|
||||
</ul>
|
||||
);
|
||||
} else {
|
||||
@@ -37,7 +50,8 @@ export const Header = ({
|
||||
text-accent-600 transition-all
|
||||
hover:text-accent-500 hover:underline
|
||||
`}
|
||||
href="/login">
|
||||
href={`${webroot}/login`}
|
||||
>
|
||||
Login
|
||||
</a>
|
||||
</li>
|
||||
@@ -48,7 +62,8 @@ export const Header = ({
|
||||
text-accent-600 transition-all
|
||||
hover:text-accent-500 hover:underline
|
||||
`}
|
||||
href="/register">
|
||||
href={`${webroot}/register`}
|
||||
>
|
||||
Register
|
||||
</a>
|
||||
</li>
|
||||
@@ -63,7 +78,7 @@ export const Header = ({
|
||||
<ul>
|
||||
<li>
|
||||
<strong>
|
||||
<a href="/">ConvertX</a>
|
||||
<a href={`${webroot}/`}>ConvertX</a>
|
||||
</strong>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1,143 +1,141 @@
|
||||
import { exec } from "node:child_process";
|
||||
|
||||
// This could be done dynamically by running `ffmpeg -formats` and parsing the output
|
||||
export const properties = {
|
||||
from: {
|
||||
muxer: [
|
||||
"3d",
|
||||
"3ds",
|
||||
"3mf",
|
||||
"ac",
|
||||
"ac3d",
|
||||
"acc",
|
||||
"amf",
|
||||
"ase",
|
||||
"ask",
|
||||
"assbin",
|
||||
"b3d",
|
||||
"blend",
|
||||
"bsp",
|
||||
"bvh",
|
||||
"cob",
|
||||
"csm",
|
||||
"dae",
|
||||
"dxf",
|
||||
"enff",
|
||||
"fbx",
|
||||
"glb",
|
||||
"gltf",
|
||||
"hmp",
|
||||
"ifc",
|
||||
"ifczip",
|
||||
"iqm",
|
||||
"irr",
|
||||
"irrmesh",
|
||||
"lwo",
|
||||
"lws",
|
||||
"lxo",
|
||||
"md2",
|
||||
"md3",
|
||||
"md5anim",
|
||||
"md5camera",
|
||||
"md5mesh",
|
||||
"mdc",
|
||||
"mdl",
|
||||
"mesh.xml",
|
||||
"mesh",
|
||||
"mot",
|
||||
"ms3d",
|
||||
"ndo",
|
||||
"nff",
|
||||
"obj",
|
||||
"off",
|
||||
"ogex",
|
||||
"pk3",
|
||||
"ply",
|
||||
"pmx",
|
||||
"prj",
|
||||
"q3o",
|
||||
"q3s",
|
||||
"raw",
|
||||
"scn",
|
||||
"sib",
|
||||
"smd",
|
||||
"step",
|
||||
"stl",
|
||||
"stp",
|
||||
"ter",
|
||||
"uc",
|
||||
"usd",
|
||||
"usda",
|
||||
"usdc",
|
||||
"usdz",
|
||||
"vta",
|
||||
"x",
|
||||
"x3d",
|
||||
"x3db",
|
||||
"xgl",
|
||||
"xml",
|
||||
"zae",
|
||||
"zgl",
|
||||
],
|
||||
},
|
||||
to: {
|
||||
muxer: [
|
||||
"3ds",
|
||||
"3mf",
|
||||
"assbin",
|
||||
"assjson",
|
||||
"assxml",
|
||||
"collada",
|
||||
"dae",
|
||||
"fbx",
|
||||
"fbxa",
|
||||
"glb",
|
||||
"glb2",
|
||||
"gltf",
|
||||
"gltf2",
|
||||
"m3d",
|
||||
"m3da",
|
||||
"obj",
|
||||
"objnomtl",
|
||||
"pbrt",
|
||||
"ply",
|
||||
"plyb",
|
||||
"stl",
|
||||
"stlb",
|
||||
"stp",
|
||||
"x",
|
||||
"x3d",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export async function convert(
|
||||
filePath: string,
|
||||
fileType: string,
|
||||
convertTo: string,
|
||||
targetPath: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
options?: unknown,
|
||||
): Promise<string> {
|
||||
// let command = "ffmpeg";
|
||||
|
||||
const command = `assimp export "${filePath}" "${targetPath}"`;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(command, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(`error: ${error}`);
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(`stdout: ${stdout}`);
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
console.error(`stderr: ${stderr}`);
|
||||
}
|
||||
|
||||
resolve("Done");
|
||||
});
|
||||
});
|
||||
}
|
||||
import { exec } 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> {
|
||||
const command = `assimp export "${filePath}" "${targetPath}"`;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(command, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(`error: ${error}`);
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(`stdout: ${stdout}`);
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
console.error(`stderr: ${stderr}`);
|
||||
}
|
||||
|
||||
resolve("Done");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
86
src/converters/calibre.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { exec } from "node:child_process";
|
||||
|
||||
export const properties = {
|
||||
from: {
|
||||
document: [
|
||||
"azw4",
|
||||
"chm",
|
||||
"cbr",
|
||||
"cbz",
|
||||
"cbt",
|
||||
"cba",
|
||||
"cb7",
|
||||
"djvu",
|
||||
"docx",
|
||||
"epub",
|
||||
"fb2",
|
||||
"htlz",
|
||||
"html",
|
||||
"lit",
|
||||
"lrf",
|
||||
"mobi",
|
||||
"odt",
|
||||
"pdb",
|
||||
"pdf",
|
||||
"pml",
|
||||
"rb",
|
||||
"rtf",
|
||||
"recipe",
|
||||
"snb",
|
||||
"tcr",
|
||||
"txt",
|
||||
],
|
||||
},
|
||||
to: {
|
||||
document: [
|
||||
"azw3",
|
||||
"docx",
|
||||
"epub",
|
||||
"fb2",
|
||||
"html",
|
||||
"htmlz",
|
||||
"lit",
|
||||
"lrf",
|
||||
"mobi",
|
||||
"oeb",
|
||||
"pdb",
|
||||
"pdf",
|
||||
"pml",
|
||||
"rb",
|
||||
"rtf",
|
||||
"snb",
|
||||
"tcr",
|
||||
"txt",
|
||||
"txtz",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export async function convert(
|
||||
filePath: string,
|
||||
fileType: string,
|
||||
convertTo: string,
|
||||
targetPath: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
options?: unknown,
|
||||
): Promise<string> {
|
||||
const command = `ebook-convert "${filePath}" "${targetPath}"`;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(command, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(`error: ${error}`);
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(`stdout: ${stdout}`);
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
console.error(`stderr: ${stderr}`);
|
||||
}
|
||||
|
||||
resolve("Done");
|
||||
});
|
||||
});
|
||||
}
|
||||
64
src/converters/inkscape.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { exec } from "node:child_process";
|
||||
|
||||
export const properties = {
|
||||
from: {
|
||||
images: [
|
||||
"svg",
|
||||
"pdf",
|
||||
"eps",
|
||||
"ps",
|
||||
"wmf",
|
||||
"emf",
|
||||
"png"
|
||||
]
|
||||
},
|
||||
to: {
|
||||
images: [
|
||||
"dxf",
|
||||
"emf",
|
||||
"eps",
|
||||
"fxg",
|
||||
"gpl",
|
||||
"hpgl",
|
||||
"html",
|
||||
"odg",
|
||||
"pdf",
|
||||
"png",
|
||||
"pov",
|
||||
"ps",
|
||||
"sif",
|
||||
"svg",
|
||||
"svgz",
|
||||
"tex",
|
||||
"wmf",
|
||||
]
|
||||
},
|
||||
};
|
||||
|
||||
export function convert(
|
||||
filePath: string,
|
||||
fileType: string,
|
||||
convertTo: string,
|
||||
targetPath: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
options?: unknown,
|
||||
): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(`inkscape "${filePath}" -o "${targetPath}"`, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(`error: ${error}`);
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(`stdout: ${stdout}`);
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
console.error(`stderr: ${stderr}`);
|
||||
}
|
||||
|
||||
resolve("Done");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,11 +2,13 @@ import { normalizeFiletype } from "../helpers/normalizeFiletype";
|
||||
import { convert as convertassimp, properties as propertiesassimp } from "./assimp";
|
||||
import { convert as convertFFmpeg, properties as propertiesFFmpeg } from "./ffmpeg";
|
||||
import { convert as convertGraphicsmagick, properties as propertiesGraphicsmagick } from "./graphicsmagick";
|
||||
import { convert as convertInkscape, properties as propertiesInkscape } from "./inkscape";
|
||||
import { convert as convertLibjxl, properties as propertiesLibjxl } from "./libjxl";
|
||||
import { convert as convertPandoc, properties as propertiesPandoc } from "./pandoc";
|
||||
import { convert as convertresvg, properties as propertiesresvg } from "./resvg";
|
||||
import { convert as convertImage, properties as propertiesImage } from "./vips";
|
||||
import { convert as convertxelatex, properties as propertiesxelatex } from "./xelatex";
|
||||
import { convert as convertCalibre, properties as propertiesCalibre } from "./calibre";
|
||||
|
||||
|
||||
// This should probably be reconstructed so that the functions are not imported instead the functions hook into this to make the converters more modular
|
||||
@@ -55,6 +57,10 @@ const properties: Record<
|
||||
properties: propertiesxelatex,
|
||||
converter: convertxelatex,
|
||||
},
|
||||
calibre: {
|
||||
properties: propertiesCalibre,
|
||||
converter: convertCalibre,
|
||||
},
|
||||
pandoc: {
|
||||
properties: propertiesPandoc,
|
||||
converter: convertPandoc,
|
||||
@@ -63,6 +69,10 @@ const properties: Record<
|
||||
properties: propertiesGraphicsmagick,
|
||||
converter: convertGraphicsmagick,
|
||||
},
|
||||
inkscape: {
|
||||
properties: propertiesInkscape,
|
||||
converter: convertInkscape,
|
||||
},
|
||||
assimp: {
|
||||
properties: propertiesassimp,
|
||||
converter: convertassimp,
|
||||
|
||||
@@ -1,35 +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";
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -53,6 +53,16 @@ if (process.env.NODE_ENV === "production") {
|
||||
}
|
||||
});
|
||||
|
||||
exec("inkscape --version", (error, stdout) => {
|
||||
if (error) {
|
||||
console.error("Inkscape is not installed.");
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(stdout.split("\n")[0]);
|
||||
}
|
||||
});
|
||||
|
||||
exec("djxl --version", (error, stdout) => {
|
||||
if (error) {
|
||||
console.error("libjxl-tools is not installed.");
|
||||
@@ -93,6 +103,16 @@ if (process.env.NODE_ENV === "production") {
|
||||
}
|
||||
});
|
||||
|
||||
exec("ebook-convert --version", (error, stdout) => {
|
||||
if (error) {
|
||||
console.error("ebook-convert (calibre) is not installed");
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(stdout.split("\n")[0]);
|
||||
}
|
||||
});
|
||||
|
||||
exec("bun -v", (error, stdout) => {
|
||||
if (error) {
|
||||
console.error("Bun is not installed. wait what");
|
||||
|
||||
221
src/index.tsx
@@ -2,7 +2,7 @@ import { randomInt, randomUUID } from "node:crypto";
|
||||
import { rmSync } from "node:fs";
|
||||
import { mkdir, unlink } from "node:fs/promises";
|
||||
import cookie from "@elysiajs/cookie";
|
||||
import { html } from "@elysiajs/html";
|
||||
import { html, Html } from "@elysiajs/html";
|
||||
import { jwt, type JWTPayloadSpec } from "@elysiajs/jwt";
|
||||
import { staticPlugin } from "@elysiajs/static";
|
||||
import { Database } from "bun:sqlite";
|
||||
@@ -37,6 +37,8 @@ const AUTO_DELETE_EVERY_N_HOURS = process.env.AUTO_DELETE_EVERY_N_HOURS
|
||||
? Number(process.env.AUTO_DELETE_EVERY_N_HOURS)
|
||||
: 24;
|
||||
|
||||
const WEBROOT = process.env.WEBROOT ?? "";
|
||||
|
||||
// fileNames: fileNames,
|
||||
// filesToConvert: fileNames.length,
|
||||
// convertedFiles : 0,
|
||||
@@ -112,6 +114,7 @@ const app = new Elysia({
|
||||
serve: {
|
||||
maxRequestBodySize: Number.MAX_SAFE_INTEGER,
|
||||
},
|
||||
prefix: WEBROOT,
|
||||
})
|
||||
.use(cookie())
|
||||
.use(html())
|
||||
@@ -127,24 +130,36 @@ const app = new Elysia({
|
||||
)
|
||||
.use(
|
||||
staticPlugin({
|
||||
assets: "src/public/",
|
||||
prefix: "/",
|
||||
assets: "public",
|
||||
prefix: "",
|
||||
}),
|
||||
)
|
||||
.get("/test", () => {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Hello World</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello</h1>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
})
|
||||
.get("/setup", ({ redirect }) => {
|
||||
if (!FIRST_RUN) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseHtml title="ConvertX | Setup">
|
||||
<BaseHtml title="ConvertX | Setup" webroot={WEBROOT}>
|
||||
<main class="mx-auto w-full max-w-4xl px-4">
|
||||
<h1 class="my-8 text-3xl">Welcome to ConvertX!</h1>
|
||||
<article class="article p-0">
|
||||
<header class="w-full bg-neutral-800 p-4">
|
||||
Create your account
|
||||
</header>
|
||||
<form method="post" action="/register" class="p-4">
|
||||
<form method="post" action={`${WEBROOT}/register`} class="p-4">
|
||||
<fieldset class="mb-4 flex flex-col gap-4">
|
||||
<label class="flex flex-col gap-1">
|
||||
Email
|
||||
@@ -191,13 +206,17 @@ const app = new Elysia({
|
||||
})
|
||||
.get("/register", ({ redirect }) => {
|
||||
if (!ACCOUNT_REGISTRATION) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseHtml title="ConvertX | Register">
|
||||
<BaseHtml webroot={WEBROOT} title="ConvertX | Register">
|
||||
<>
|
||||
<Header accountRegistration={ACCOUNT_REGISTRATION} />
|
||||
<Header
|
||||
webroot={WEBROOT}
|
||||
accountRegistration={ACCOUNT_REGISTRATION}
|
||||
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
|
||||
/>
|
||||
<main class="w-full px-4">
|
||||
<article class="article">
|
||||
<form method="post" class="flex flex-col gap-4">
|
||||
@@ -241,7 +260,7 @@ const app = new Elysia({
|
||||
"/register",
|
||||
async ({ body, set, redirect, jwt, cookie: { auth } }) => {
|
||||
if (!ACCOUNT_REGISTRATION && !FIRST_RUN) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
if (FIRST_RUN) {
|
||||
@@ -296,13 +315,13 @@ const app = new Elysia({
|
||||
sameSite: "strict",
|
||||
});
|
||||
|
||||
return redirect("/", 302);
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
},
|
||||
{ body: t.Object({ email: t.String(), password: t.String() }) },
|
||||
)
|
||||
.get("/login", async ({ jwt, redirect, cookie: { auth } }) => {
|
||||
if (FIRST_RUN) {
|
||||
return redirect("/setup", 302);
|
||||
return redirect(`${WEBROOT}/setup`, 302);
|
||||
}
|
||||
|
||||
// if already logged in, redirect to home
|
||||
@@ -310,16 +329,20 @@ const app = new Elysia({
|
||||
const user = await jwt.verify(auth.value);
|
||||
|
||||
if (user) {
|
||||
return redirect("/", 302);
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
}
|
||||
|
||||
auth.remove();
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseHtml title="ConvertX | Login">
|
||||
<BaseHtml webroot={WEBROOT} title="ConvertX | Login">
|
||||
<>
|
||||
<Header accountRegistration={ACCOUNT_REGISTRATION} />
|
||||
<Header
|
||||
webroot={WEBROOT}
|
||||
accountRegistration={ACCOUNT_REGISTRATION}
|
||||
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
|
||||
/>
|
||||
<main class="w-full px-4">
|
||||
<article class="article">
|
||||
<form method="post" class="flex flex-col gap-4">
|
||||
@@ -350,7 +373,7 @@ const app = new Elysia({
|
||||
<div role="group">
|
||||
{ACCOUNT_REGISTRATION ? (
|
||||
<a
|
||||
href="/register"
|
||||
href={`${WEBROOT}/register`}
|
||||
role="button"
|
||||
class="btn-primary w-full"
|
||||
>
|
||||
@@ -417,7 +440,7 @@ const app = new Elysia({
|
||||
sameSite: "strict",
|
||||
});
|
||||
|
||||
return redirect("/", 302);
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
},
|
||||
{ body: t.Object({ email: t.String(), password: t.String() }) },
|
||||
)
|
||||
@@ -426,46 +449,29 @@ const app = new Elysia({
|
||||
auth.remove();
|
||||
}
|
||||
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
})
|
||||
.post("/logoff", ({ redirect, cookie: { auth } }) => {
|
||||
if (auth?.value) {
|
||||
auth.remove();
|
||||
}
|
||||
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
})
|
||||
.get("/", async ({ jwt, redirect, cookie: { auth, jobId } }) => {
|
||||
if (FIRST_RUN) {
|
||||
return redirect("/setup", 302);
|
||||
}
|
||||
if (!ALLOW_UNAUTHENTICATED) {
|
||||
if (FIRST_RUN) {
|
||||
return redirect(`${WEBROOT}/setup`, 302);
|
||||
}
|
||||
|
||||
if (!auth?.value && !ALLOW_UNAUTHENTICATED) {
|
||||
return redirect("/login", 302);
|
||||
if (!auth?.value) {
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
}
|
||||
|
||||
// validate jwt
|
||||
let user: ({ id: string } & JWTPayloadSpec) | false = false;
|
||||
if (auth?.value) {
|
||||
user = await jwt.verify(auth.value);
|
||||
|
||||
if (user !== false && user.id) {
|
||||
if (Number.parseInt(user.id) < 2 ** 24 || !ALLOW_UNAUTHENTICATED) {
|
||||
// make sure user exists in db
|
||||
const existingUser = db
|
||||
.query("SELECT * FROM users WHERE id = ?")
|
||||
.as(User)
|
||||
.get(user.id);
|
||||
|
||||
if (!existingUser) {
|
||||
if (auth?.value) {
|
||||
auth.remove();
|
||||
}
|
||||
return redirect("/login", 302);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (ALLOW_UNAUTHENTICATED) {
|
||||
if (ALLOW_UNAUTHENTICATED) {
|
||||
const newUserId = String(
|
||||
randomInt(
|
||||
2 ** 24,
|
||||
@@ -491,10 +497,29 @@ const app = new Elysia({
|
||||
maxAge: 24 * 60 * 60,
|
||||
sameSite: "strict",
|
||||
});
|
||||
} else if (auth?.value) {
|
||||
user = await jwt.verify(auth.value);
|
||||
|
||||
if (user !== false && user.id) {
|
||||
if (Number.parseInt(user.id) < 2 ** 24 || !ALLOW_UNAUTHENTICATED) {
|
||||
// make sure user exists in db
|
||||
const existingUser = db
|
||||
.query("SELECT * FROM users WHERE id = ?")
|
||||
.as(User)
|
||||
.get(user.id);
|
||||
|
||||
if (!existingUser) {
|
||||
if (auth?.value) {
|
||||
auth.remove();
|
||||
}
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
// create a new job
|
||||
@@ -524,9 +549,13 @@ const app = new Elysia({
|
||||
console.log("jobId set to:", id);
|
||||
|
||||
return (
|
||||
<BaseHtml>
|
||||
<BaseHtml webroot={WEBROOT}>
|
||||
<>
|
||||
<Header loggedIn />
|
||||
<Header
|
||||
webroot={WEBROOT}
|
||||
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
|
||||
loggedIn
|
||||
/>
|
||||
<main class="w-full px-4">
|
||||
<article class="article">
|
||||
<h1 class="mb-4 text-xl">Convert</h1>
|
||||
@@ -562,7 +591,7 @@ const app = new Elysia({
|
||||
</article>
|
||||
<form
|
||||
method="post"
|
||||
action="/convert"
|
||||
action={`${WEBROOT}/convert`}
|
||||
class="relative mx-auto mb-[35vh] w-full max-w-4xl"
|
||||
>
|
||||
<input type="hidden" name="file_names" id="file_names" />
|
||||
@@ -727,16 +756,16 @@ const app = new Elysia({
|
||||
"/upload",
|
||||
async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
|
||||
if (!auth?.value) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
const user = await jwt.verify(auth.value);
|
||||
if (!user) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
if (!jobId?.value) {
|
||||
return redirect("/", 302);
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
}
|
||||
|
||||
const existingJob = await db
|
||||
@@ -744,7 +773,7 @@ const app = new Elysia({
|
||||
.get(jobId.value, user.id);
|
||||
|
||||
if (!existingJob) {
|
||||
return redirect("/", 302);
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
}
|
||||
|
||||
const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
|
||||
@@ -769,16 +798,16 @@ const app = new Elysia({
|
||||
"/delete",
|
||||
async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
|
||||
if (!auth?.value) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
const user = await jwt.verify(auth.value);
|
||||
if (!user) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
if (!jobId?.value) {
|
||||
return redirect("/", 302);
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
}
|
||||
|
||||
const existingJob = await db
|
||||
@@ -786,7 +815,7 @@ const app = new Elysia({
|
||||
.get(jobId.value, user.id);
|
||||
|
||||
if (!existingJob) {
|
||||
return redirect("/", 302);
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
}
|
||||
|
||||
const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
|
||||
@@ -799,16 +828,16 @@ const app = new Elysia({
|
||||
"/convert",
|
||||
async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
|
||||
if (!auth?.value) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
const user = await jwt.verify(auth.value);
|
||||
if (!user) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
if (!jobId?.value) {
|
||||
return redirect("/", 302);
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
}
|
||||
|
||||
const existingJob = db
|
||||
@@ -817,7 +846,7 @@ const app = new Elysia({
|
||||
.get(jobId.value, user.id);
|
||||
|
||||
if (!existingJob) {
|
||||
return redirect("/", 302);
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
}
|
||||
|
||||
const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
|
||||
@@ -838,7 +867,7 @@ const app = new Elysia({
|
||||
const fileNames = JSON.parse(body.file_names) as string[];
|
||||
|
||||
if (!Array.isArray(fileNames) || fileNames.length === 0) {
|
||||
return redirect("/", 302);
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
}
|
||||
|
||||
db.query(
|
||||
@@ -891,7 +920,7 @@ const app = new Elysia({
|
||||
});
|
||||
|
||||
// Redirect the client immediately
|
||||
return redirect(`/results/${jobId.value}`, 302);
|
||||
return redirect(`${WEBROOT}/results/${jobId.value}`, 302);
|
||||
},
|
||||
{
|
||||
body: t.Object({
|
||||
@@ -902,12 +931,12 @@ const app = new Elysia({
|
||||
)
|
||||
.get("/history", async ({ jwt, redirect, cookie: { auth } }) => {
|
||||
if (!auth?.value) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
const user = await jwt.verify(auth.value);
|
||||
|
||||
if (!user) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
let userJobs = db
|
||||
@@ -928,9 +957,13 @@ const app = new Elysia({
|
||||
userJobs = userJobs.filter((job) => job.num_files > 0);
|
||||
|
||||
return (
|
||||
<BaseHtml title="ConvertX | Results">
|
||||
<BaseHtml webroot={WEBROOT} title="ConvertX | Results">
|
||||
<>
|
||||
<Header loggedIn />
|
||||
<Header
|
||||
webroot={WEBROOT}
|
||||
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
|
||||
loggedIn
|
||||
/>
|
||||
<main class="w-full px-4">
|
||||
<article class="article">
|
||||
<h1 class="mb-4 text-xl">Results</h1>
|
||||
@@ -963,7 +996,7 @@ const app = new Elysia({
|
||||
text-accent-500 underline
|
||||
hover:text-accent-400
|
||||
`}
|
||||
href={`/results/${job.id}`}
|
||||
href={`${WEBROOT}/results/${job.id}`}
|
||||
>
|
||||
View
|
||||
</a>
|
||||
@@ -982,7 +1015,7 @@ const app = new Elysia({
|
||||
"/results/:jobId",
|
||||
async ({ params, jwt, set, redirect, cookie: { auth, job_id } }) => {
|
||||
if (!auth?.value) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
if (job_id?.value) {
|
||||
@@ -992,7 +1025,7 @@ const app = new Elysia({
|
||||
|
||||
const user = await jwt.verify(auth.value);
|
||||
if (!user) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
const job = db
|
||||
@@ -1015,9 +1048,13 @@ const app = new Elysia({
|
||||
.all(params.jobId);
|
||||
|
||||
return (
|
||||
<BaseHtml title="ConvertX | Result">
|
||||
<BaseHtml webroot={WEBROOT} title="ConvertX | Result">
|
||||
<>
|
||||
<Header loggedIn />
|
||||
<Header
|
||||
webroot={WEBROOT}
|
||||
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
|
||||
loggedIn
|
||||
/>
|
||||
<main class="w-full px-4">
|
||||
<article class="article">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
@@ -1075,7 +1112,7 @@ const app = new Elysia({
|
||||
text-accent-500 underline
|
||||
hover:text-accent-400
|
||||
`}
|
||||
href={`/download/${outputPath}${file.output_file_name}`}
|
||||
href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
|
||||
>
|
||||
View
|
||||
</a>
|
||||
@@ -1086,7 +1123,7 @@ const app = new Elysia({
|
||||
text-accent-500 underline
|
||||
hover:text-accent-400
|
||||
`}
|
||||
href={`/download/${outputPath}${file.output_file_name}`}
|
||||
href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
|
||||
download={file.output_file_name}
|
||||
>
|
||||
Download
|
||||
@@ -1098,7 +1135,7 @@ const app = new Elysia({
|
||||
</table>
|
||||
</article>
|
||||
</main>
|
||||
<script src="/results.js" defer />
|
||||
<script src={`${WEBROOT}/results.js`} defer />
|
||||
</>
|
||||
</BaseHtml>
|
||||
);
|
||||
@@ -1108,7 +1145,7 @@ const app = new Elysia({
|
||||
"/progress/:jobId",
|
||||
async ({ jwt, set, params, redirect, cookie: { auth, job_id } }) => {
|
||||
if (!auth?.value) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
if (job_id?.value) {
|
||||
@@ -1118,7 +1155,7 @@ const app = new Elysia({
|
||||
|
||||
const user = await jwt.verify(auth.value);
|
||||
if (!user) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
const job = db
|
||||
@@ -1197,7 +1234,7 @@ const app = new Elysia({
|
||||
text-accent-500 underline
|
||||
hover:text-accent-400
|
||||
`}
|
||||
href={`/download/${outputPath}${file.output_file_name}`}
|
||||
href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
|
||||
>
|
||||
View
|
||||
</a>
|
||||
@@ -1208,7 +1245,7 @@ const app = new Elysia({
|
||||
text-accent-500 underline
|
||||
hover:text-accent-400
|
||||
`}
|
||||
href={`/download/${outputPath}${file.output_file_name}`}
|
||||
href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
|
||||
download={file.output_file_name}
|
||||
>
|
||||
Download
|
||||
@@ -1226,12 +1263,12 @@ const app = new Elysia({
|
||||
"/download/:userId/:jobId/:fileName",
|
||||
async ({ params, jwt, redirect, cookie: { auth } }) => {
|
||||
if (!auth?.value) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
const user = await jwt.verify(auth.value);
|
||||
if (!user) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
const job = await db
|
||||
@@ -1239,7 +1276,7 @@ const app = new Elysia({
|
||||
.get(user.id, params.jobId);
|
||||
|
||||
if (!job) {
|
||||
return redirect("/results", 302);
|
||||
return redirect(`${WEBROOT}/results`, 302);
|
||||
}
|
||||
// parse from url encoded string
|
||||
const userId = decodeURIComponent(params.userId);
|
||||
@@ -1252,18 +1289,22 @@ const app = new Elysia({
|
||||
)
|
||||
.get("/converters", async ({ jwt, redirect, cookie: { auth } }) => {
|
||||
if (!auth?.value) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
const user = await jwt.verify(auth.value);
|
||||
if (!user) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseHtml title="ConvertX | Converters">
|
||||
<BaseHtml webroot={WEBROOT} title="ConvertX | Converters">
|
||||
<>
|
||||
<Header loggedIn />
|
||||
<Header
|
||||
webroot={WEBROOT}
|
||||
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
|
||||
loggedIn
|
||||
/>
|
||||
<main class="w-full px-4">
|
||||
<article class="article">
|
||||
<h1 class="mb-4 text-xl">Converters</h1>
|
||||
@@ -1322,12 +1363,12 @@ const app = new Elysia({
|
||||
async ({ params, jwt, redirect, cookie: { auth } }) => {
|
||||
// TODO: Implement zip download
|
||||
if (!auth?.value) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
const user = await jwt.verify(auth.value);
|
||||
if (!user) {
|
||||
return redirect("/login", 302);
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
|
||||
const job = await db
|
||||
@@ -1335,12 +1376,12 @@ const app = new Elysia({
|
||||
.get(user.id, params.jobId);
|
||||
|
||||
if (!job) {
|
||||
return redirect("/results", 302);
|
||||
return redirect(`${WEBROOT}/results`, 302);
|
||||
}
|
||||
|
||||
// const userId = decodeURIComponent(params.userId);
|
||||
// const jobId = decodeURIComponent(params.jobId);
|
||||
// const outputPath = `${outputDir}${userId}/${jobId}/`;
|
||||
// const outputPath = `${outputDir}${userId}/`{jobId}/);
|
||||
|
||||
// return Bun.zip(outputPath);
|
||||
},
|
||||
@@ -1364,7 +1405,7 @@ if (process.env.NODE_ENV !== "production") {
|
||||
app.listen(3000);
|
||||
|
||||
console.log(
|
||||
`🦊 Elysia is running at http://${app.server?.hostname}:${app.server?.port}`,
|
||||
`🦊 Elysia is running at http://${app.server?.hostname}:${app.server?.port}${WEBROOT}`,
|
||||
);
|
||||
|
||||
const clearJobs = () => {
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
|
||||
module.exports = {
|
||||
content: ["./src/**/*.{html,js,tsx,jsx,cjs,mjs}"],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
contrast: "rgba(var(--contrast))",
|
||||
"neutral-900": "rgba(var(--neutral-900))",
|
||||
"neutral-800": "rgba(var(--neutral-800))",
|
||||
"neutral-700": "rgba(var(--neutral-700))",
|
||||
"neutral-600": "rgba(var(--neutral-600))",
|
||||
"neutral-500": "rgba(var(--neutral-500))",
|
||||
"neutral-400": "rgba(var(--neutral-400))",
|
||||
"neutral-300": "rgba(var(--neutral-300))",
|
||||
"neutral-200": "rgba(var(--neutral-200))",
|
||||
"neutral-100": "rgba(var(--neutral-100))",
|
||||
"accent-600": "rgba(var(--accent-600))",
|
||||
"accent-500": "rgba(var(--accent-500))",
|
||||
"accent-400": "rgba(var(--accent-400))",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwind-scrollbar")],
|
||||
};
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
|
||||
import tailwindScrollbar from "tailwind-scrollbar";
|
||||
|
||||
export default {
|
||||
content: ["./src/**/*.{html,js,tsx,jsx,cjs,mjs}"],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
contrast: "rgba(var(--contrast))",
|
||||
"neutral-900": "rgba(var(--neutral-900))",
|
||||
"neutral-800": "rgba(var(--neutral-800))",
|
||||
"neutral-700": "rgba(var(--neutral-700))",
|
||||
"neutral-600": "rgba(var(--neutral-600))",
|
||||
"neutral-500": "rgba(var(--neutral-500))",
|
||||
"neutral-400": "rgba(var(--neutral-400))",
|
||||
"neutral-300": "rgba(var(--neutral-300))",
|
||||
"neutral-200": "rgba(var(--neutral-200))",
|
||||
"neutral-100": "rgba(var(--neutral-100))",
|
||||
"accent-600": "rgba(var(--accent-600))",
|
||||
"accent-500": "rgba(var(--accent-500))",
|
||||
"accent-400": "rgba(var(--accent-400))",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [tailwindScrollbar],
|
||||
};
|
||||
|
||||
@@ -27,4 +27,4 @@
|
||||
"noImplicitOverride": true
|
||||
// "noImplicitReturns": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||