76 Commits

Author SHA1 Message Date
Emrik Östling
f0f30224b5 Merge pull request #183 from C4illin/release-please--branches--main--components--convertx-frontend
chore(main): release 0.9.0
2024-11-24 14:47:34 +01:00
C4illin
d0d888e356 chore: update postcss to ejs 2024-11-21 23:08:30 +01:00
C4illin
2c64122224 chore: update deps 2024-11-21 23:08:16 +01:00
C4illin
3b2eee96a9 chore: update deps 2024-11-21 22:50:45 +01:00
C4illin
465aacbf9b chore: update formats 2024-11-21 22:50:38 +01:00
Emrik Östling
d1a2a66170 chore(main): release 0.9.0 2024-11-21 22:44:51 +01:00
C4illin
4c05fd72bb fix: wait for both upload and selection
issue #177
2024-11-21 22:44:13 +01:00
Emrik Östling
f04fe760e3 Merge pull request #187 from C4illin/renovate/oven-bun-1.x
chore(deps): update oven/bun docker tag to v1.1.36
2024-11-20 10:12:40 +01:00
renovate[bot]
834d19bcc6 chore(deps): update oven/bun docker tag to v1.1.36 2024-11-20 02:03:06 +00:00
Emrik Östling
6808c4642c Update README.md 2024-11-18 17:00:04 +01:00
Emrik Östling
d0ce307f94 Merge pull request #186 from C10udburst/main
feat: add inkscape for vector images
2024-11-16 14:13:43 +01:00
Cloudburst
f3740e9ded feat: add inkscape for vector images 2024-11-16 11:34:47 +01:00
C4illin
b485bc9445 chore: add download stats 2024-11-14 11:26:24 +01:00
Emrik Östling
2d14c1bb26 Merge pull request #184 from C4illin/feature/#177/disable-convert-when-uploading 2024-11-13 14:59:31 +01:00
C4illin
1a442d6e69 fix: treat unknown as m4a
issue #178
2024-11-13 13:08:40 +01:00
C4illin
2386543e5c chore: update deps 2024-11-13 13:08:00 +01:00
C4illin
58e220e82d feat: disable convert when uploading
issue #177
2024-11-12 22:30:16 +01:00
C4illin
24bea6e4d2 chore: add tutorial link 2024-11-12 22:20:32 +01:00
C4illin
43497ad8d1 ci: split docker hub description to separate workflow 2024-11-12 12:35:05 +01:00
C4illin
f22b61fe4c ci: sync docker hub description with readme 2024-11-12 12:27:39 +01:00
C4illin
5b08f4cd19 ci: support dockerhub 2024-11-12 12:17:45 +01:00
Emrik Östling
1589f8d24e Merge pull request #182 from C4illin/feature/#180/add-webroot-env-variable 2024-11-06 22:56:00 +01:00
C4illin
7d1db72cf5 chore: fix default value for webroot 2024-11-06 14:14:11 +01:00
C4illin
53a8f66414 chore: update deps 2024-11-06 14:04:57 +01:00
C4illin
36cb6cc589 feat: Allow to chose webroot
issue #180
2024-11-06 08:49:53 +01:00
Emrik Östling
f3a4aece46 Merge pull request #181 from C4illin/renovate/oven-bun-1.x
chore(deps): update oven/bun docker tag to v1.1.34
2024-11-04 11:21:48 +01:00
renovate[bot]
580a6a869a chore(deps): update oven/bun docker tag to v1.1.34 2024-11-02 06:59:17 +00:00
Emrik Östling
008eaac493 Merge pull request #176 from C4illin/renovate/oven-bun-1.x
chore(deps): update oven/bun docker tag to v1.1.33
2024-10-28 22:56:14 +01:00
renovate[bot]
b450623bb4 chore(deps): update oven/bun docker tag to v1.1.33 2024-10-24 10:42:28 +00:00
Emrik Östling
8ac2ecb673 Merge pull request #175 from C4illin/renovate/npm-run-all2-7.x
chore(deps): update dependency npm-run-all2 to v7
2024-10-22 09:04:39 +02:00
Emrik Östling
0a10a56ae3 Merge pull request #174 from C4illin/renovate/oven-bun-1.x
chore(deps): update oven/bun docker tag to v1.1.32
2024-10-22 09:04:14 +02:00
renovate[bot]
9378ba9208 chore(deps): update dependency npm-run-all2 to v7 2024-10-22 01:09:18 +00:00
renovate[bot]
0c586e324b chore(deps): update oven/bun docker tag to v1.1.32 2024-10-21 23:14:26 +00:00
Emrik Östling
91c4a64284 chore: add dev image size 2024-10-18 20:07:44 +02:00
C4illin
c599e98d9d chore: ignore more files 2024-10-18 20:03:19 +02:00
C4illin
d2cd6706c9 chore: update @elysiajs/static 2024-10-18 19:32:42 +02:00
C4illin
e8ed10dde8 chore(deps): update @elysiajs/html to version 1.1.1 2024-10-18 18:53:45 +02:00
C4illin
5fe0b79802 chore(deps): update dependencies to latest versions 2024-10-18 18:43:51 +02:00
Emrik Östling
34a6722a68 Merge pull request #172 from C4illin/renovate/oven-bun-1.x 2024-10-18 11:52:36 +02:00
renovate[bot]
5b0d769c63 chore(deps): update oven/bun docker tag to v1.1.31 2024-10-18 07:56:43 +00:00
Emrik Östling
718401a28b Merge pull request #169 from C4illin/renovate/oven-bun-1.x
chore(deps): update oven/bun docker tag to v1.1.30
2024-10-08 18:09:13 +02:00
renovate[bot]
3112cd57f6 chore(deps): update oven/bun docker tag to v1.1.30 2024-10-08 14:13:13 +00:00
C4illin
410fc777a7 Merge branch 'main' of https://github.com/C4illin/ConvertX 2024-10-07 10:47:40 +02:00
C4illin
8eed99e732 chore: fix drop done style 2024-10-07 10:47:37 +02:00
Emrik Östling
663b1d4171 Merge pull request #167 from C4illin/release-please--branches--main--components--convertx-frontend 2024-10-06 23:36:13 +02:00
Emrik Östling
c3067ca12d chore(main): release 0.8.1 2024-10-06 00:49:06 +02:00
Emrik Östling
4561ca3760 Merge pull request #164 from C4illin/fix/#163/add-jfif-support 2024-10-06 00:48:46 +02:00
Emrik Östling
698cce58ce Merge pull request #165 from C4illin/fix/#157/resize-when-converting-to-ico 2024-10-06 00:46:22 +02:00
C4illin
339b79f786 fix: treat jfif as jpeg
issue #163
2024-10-06 00:45:08 +02:00
C4illin
4f98f778f0 chore: add message when resizing image 2024-10-06 00:40:34 +02:00
Emrik Östling
8479b33a47 Merge pull request #166 from C4illin/fix/#151/disable-convert-button 2024-10-05 23:29:39 +02:00
C4illin
78844d7bd5 fix: disable convert button when input is empty
issue #151
2024-10-05 01:20:23 +02:00
Emrik Östling
64e4a271e1 Merge branch 'main' into fix/#157/resize-when-converting-to-ico 2024-10-05 01:02:48 +02:00
C4illin
5fb8c3575b chore: add eslint-plugin-readable-tailwind 2024-10-05 01:01:00 +02:00
C4illin
a6b8bcecae chore: disable dependency dashboard in Renovate configuration 2024-10-05 00:47:34 +02:00
C4illin
bc9c820820 chore: remove DeepSource configuration file 2024-10-05 00:45:34 +02:00
C4illin
ee9207a7f4 chore: fix eslint rules 2024-10-05 00:43:24 +02:00
C4illin
a34e215202 chore: remove biome 2024-10-05 00:01:39 +02:00
C4illin
b4e53dbb8e fix: resize to fit for ico
issue #157
2024-10-04 23:55:39 +02:00
C4illin
b5e8d82bfa chore(eslint): add browser globals to ESLint configuration 2024-10-04 23:44:18 +02:00
Emrik Östling
5d9000bb33 Merge pull request #162 from C4illin/renovate/biomejs-biome-1.x
chore(deps): update dependency @biomejs/biome to v1.9.3
2024-10-01 22:01:56 +02:00
renovate[bot]
ccb065ef0f chore(deps): update dependency @biomejs/biome to v1.9.3 2024-10-01 15:11:07 +00:00
Emrik Östling
883fad806b Merge pull request #155 from C4illin/release-please--branches--main--components--convertx-frontend 2024-10-01 14:17:30 +02:00
Emrik Östling
feacd1b816 chore(main): release 0.8.0 2024-09-30 22:01:44 +02:00
Emrik Östling
094e7a0d1c Merge pull request #160 from C4illin/feature-light-theme
feat: add light theme
2024-09-30 22:01:19 +02:00
C4illin
72636c5059 feat: add light theme, fixes #156 2024-09-30 17:14:44 +02:00
C4illin
291cfc80c6 chore: update deps 2024-09-30 15:56:33 +02:00
C4illin
ae1dfafc9d fix: cleanup formats and add opus, fixes #159 2024-09-30 15:51:11 +02:00
Emrik Östling
6caa583c35 Merge pull request #154 from C4illin/fix/#153/clean-up-ffmpeg-formats
fix: support .awb and clean up, fixes #153, #92
2024-09-28 13:36:46 +02:00
C4illin
2057167576 fix: add support for usd for assimp, #144 2024-09-28 13:13:48 +02:00
C4illin
1c9e67fc32 fix: support .awb and clean up, fixes #153, #92 2024-09-28 13:02:17 +02:00
Emrik Östling
d3af9688c6 Merge pull request #149 from C4illin/release-please--branches--main--components--convertx-frontend
chore(main): release 0.7.0
2024-09-28 11:11:11 +02:00
Emrik Östling
7d0cbb9844 chore(main): release 0.7.0 2024-09-26 23:37:45 +02:00
C4illin
88173891ba fix: wrong layout on search with few options 2024-09-26 23:37:19 +02:00
Emrik Östling
2b4b8f9551 Merge pull request #148 from Aymendje/feat-assimp 2024-09-26 23:01:33 +02:00
Aymen Djellal
63a4328d4a feat: Add support for 3d assets through assimp converter
This is a start for #144
It does not support all the 3d formats, but its a good few
2024-09-26 22:55:42 +02:00
44 changed files with 1340 additions and 887 deletions

View File

@@ -1,7 +0,0 @@
version = 1
[[analyzers]]
name = "javascript"
[analyzers.meta]
environment = ["nodejs"]

View File

@@ -1,16 +1,20 @@
node_modules
Dockerfile*
docker-compose*
.dockerignore .dockerignore
.editorconfig
.env
.git .git
.gitignore .gitignore
README.md
LICENSE
.vscode
Makefile
helm-charts
.env
.editorconfig
.idea .idea
.vscode
CHANGELOG.md
coverage* coverage*
data data
docker-compose*
Dockerfile*
eslint.config.js
helm-charts
LICENSE
Makefile
node_modules
prettier.config.js
README.md
renovate.json

View File

@@ -1,69 +1,80 @@
name: Docker name: Docker
# This workflow uses actions that are not certified by GitHub. # This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by # They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support # separate terms of service, privacy policy, and support
# documentation. # documentation.
on: on:
push: push:
branches: [ "main" ] branches: [ "main" ]
# Publish semver tags as releases. # Publish semver tags as releases.
tags: [ 'v*.*.*' ] tags: [ 'v*.*.*' ]
pull_request: pull_request:
branches: [ "main" ] branches: [ "main" ]
workflow_dispatch: workflow_dispatch:
env: env:
# Use docker.io for Docker Hub if empty # Use docker.io for Docker Hub if empty
REGISTRY: ghcr.io REGISTRY: ghcr.io
# github.repository as <account>/<repo> # github.repository as <account>/<repo>
IMAGE_NAME: ${{ github.repository }} IMAGE_NAME: ${{ github.repository }}
DOCKERHUB_USERNAME: c4illin
jobs:
build: jobs:
runs-on: ubuntu-latest build:
permissions: runs-on: ubuntu-latest
contents: read permissions:
packages: write contents: read
packages: write
steps:
- name: Checkout repository steps:
uses: actions/checkout@v4 - name: Checkout repository
uses: actions/checkout@v4
# Workaround: https://github.com/docker/build-push-action/issues/461
- name: Setup Docker buildx # Workaround: https://github.com/docker/build-push-action/issues/461
uses: docker/setup-buildx-action@v3 - name: Setup Docker buildx
uses: docker/setup-buildx-action@v3
# Login against a Docker registry except on PR
# https://github.com/docker/login-action # Login against a Docker registry except on PR
- name: Log into registry ${{ env.REGISTRY }} # https://github.com/docker/login-action
if: github.event_name != 'pull_request' - name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v3 if: github.event_name != 'pull_request'
with: uses: docker/login-action@v3
registry: ${{ env.REGISTRY }} with:
username: ${{ github.actor }} registry: ${{ env.REGISTRY }}
password: ${{ secrets.GITHUB_TOKEN }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action - name: Login to Docker Hub
- name: Extract Docker metadata if: github.event_name != 'pull_request'
id: meta uses: docker/login-action@v3
uses: docker/metadata-action@v5 with:
with: username: ${{ env.DOCKERHUB_USERNAME }}
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} password: ${{ secrets.DOCKERHUB_TOKEN }}
# Build and push Docker image with Buildx (don't push on PR) # Extract metadata (tags, labels) for Docker
# https://github.com/docker/build-push-action # https://github.com/docker/metadata-action
- name: Build and push Docker image - name: Extract Docker metadata
id: build-and-push id: meta
uses: docker/build-push-action@v6 uses: docker/metadata-action@v5
with: with:
context: . images: |
platforms: linux/amd64,linux/arm64 ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
push: ${{ github.event_name != 'pull_request' }} ${{ env.IMAGE_NAME }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} # Build and push Docker image with Buildx (don't push on PR)
cache-from: type=gha # https://github.com/docker/build-push-action
cache-to: type=gha,mode=max - 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

View 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
View File

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

View File

@@ -1,5 +1,55 @@
# Changelog # Changelog
## [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)
### Bug Fixes
* disable convert button when input is empty ([78844d7](https://github.com/C4illin/ConvertX/commit/78844d7bd55990789ed07c81e49043e688cbe656)), closes [#151](https://github.com/C4illin/ConvertX/issues/151)
* resize to fit for ico ([b4e53db](https://github.com/C4illin/ConvertX/commit/b4e53dbb8e70b3a95b44e5b756759d16117a87e1)), closes [#157](https://github.com/C4illin/ConvertX/issues/157)
* treat jfif as jpeg ([339b79f](https://github.com/C4illin/ConvertX/commit/339b79f786131deb93f0d5683e03178fdcab1ef5)), closes [#163](https://github.com/C4illin/ConvertX/issues/163)
## [0.8.0](https://github.com/C4illin/ConvertX/compare/v0.7.0...v0.8.0) (2024-09-30)
### Features
* add light theme, fixes [#156](https://github.com/C4illin/ConvertX/issues/156) ([72636c5](https://github.com/C4illin/ConvertX/commit/72636c5059ebf09c8fece2e268293650b2f8ccf6))
### Bug Fixes
* add support for usd for assimp, [#144](https://github.com/C4illin/ConvertX/issues/144) ([2057167](https://github.com/C4illin/ConvertX/commit/20571675766209ad1251f07e687d29a6791afc8b))
* cleanup formats and add opus, fixes [#159](https://github.com/C4illin/ConvertX/issues/159) ([ae1dfaf](https://github.com/C4illin/ConvertX/commit/ae1dfafc9d9116a57b08c2f7fc326990e00824b0))
* support .awb and clean up, fixes [#153](https://github.com/C4illin/ConvertX/issues/153), [#92](https://github.com/C4illin/ConvertX/issues/92) ([1c9e67f](https://github.com/C4illin/ConvertX/commit/1c9e67fc3201e0e5dee91e8981adf34daaabf33a))
## [0.7.0](https://github.com/C4illin/ConvertX/compare/v0.6.0...v0.7.0) (2024-09-26)
### Features
* Add support for 3d assets through assimp converter ([63a4328](https://github.com/C4illin/ConvertX/commit/63a4328d4a1e01df3e0ec4a877bad8c8ffe71129))
### Bug Fixes
* wrong layout on search with few options ([8817389](https://github.com/C4illin/ConvertX/commit/88173891ba2d69da46eda46f3f598a9b54f26f96))
## [0.6.0](https://github.com/C4illin/ConvertX/compare/v0.5.0...v0.6.0) (2024-09-25) ## [0.6.0](https://github.com/C4illin/ConvertX/compare/v0.5.0...v0.6.0) (2024-09-25)

View File

@@ -1,4 +1,4 @@
FROM oven/bun:1.1.29-alpine AS base FROM oven/bun:1.1.36-alpine AS base
LABEL org.opencontainers.image.source="https://github.com/C4illin/ConvertX" LABEL org.opencontainers.image.source="https://github.com/C4illin/ConvertX"
WORKDIR /app WORKDIR /app
@@ -49,14 +49,16 @@ RUN apk --no-cache add \
vips-tools \ vips-tools \
vips-poppler \ vips-poppler \
vips-jxl \ vips-jxl \
libjxl-tools libjxl-tools \
assimp \
inkscape
# this might be needed for some latex use cases, will add it if needed. # this might be needed for some latex use cases, will add it if needed.
# texmf-dist-fontsextra \ # texmf-dist-fontsextra \
COPY --from=install /temp/prod/node_modules node_modules COPY --from=install /temp/prod/node_modules node_modules
COPY --from=builder /root/.cargo/bin/resvg /usr/local/bin/resvg COPY --from=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/src/index.tsx /app/src/
# COPY --from=prerelease /app/package.json . # COPY --from=prerelease /app/package.json .
COPY . . COPY . .

240
README.md
View File

@@ -1,110 +1,130 @@
![ConvertX](images/logo.png) ![ConvertX](images/logo.png)
# ConvertX
[![Docker](https://github.com/C4illin/ConvertX/actions/workflows/docker-publish.yml/badge.svg?branch=main)](https://github.com/C4illin/ConvertX/actions/workflows/docker-publish.yml) # ConvertX
[![GitHub Release](https://img.shields.io/github/v/release/C4illin/ConvertX)](https://github.com/C4illin/ConvertX/pkgs/container/convertx)
![GitHub commits since latest release](https://img.shields.io/github/commits-since/C4illin/ConvertX/latest) [![Docker](https://github.com/C4illin/ConvertX/actions/workflows/docker-publish.yml/badge.svg?branch=main)](https://github.com/C4illin/ConvertX/actions/workflows/docker-publish.yml)
![GitHub repo size](https://img.shields.io/github/repo-size/C4illin/ConvertX) [![ghcr.io Pulls](https://img.shields.io/badge/dynamic/json?logo=github&url=https%3A%2F%2Fipitio.github.io%2Fbackage%2FC4illin%2FConvertX%2Fconvertx.json&query=%24.downloads&label=ghcr.io%20pulls&cacheSeconds=14400)](https://github.com/C4illin/ConvertX/pkgs/container/ConvertX)
![Docker container size](https://ghcr-badge.egpl.dev/c4illin/convertx/size?color=%230375b6&tag=latest&label=image+size&trim=) [![GitHub Release](https://img.shields.io/github/v/release/C4illin/ConvertX)](https://github.com/C4illin/ConvertX/pkgs/container/convertx)
![GitHub top language](https://img.shields.io/github/languages/top/C4illin/ConvertX) ![GitHub commits since latest release](https://img.shields.io/github/commits-since/C4illin/ConvertX/latest)
![GitHub repo size](https://img.shields.io/github/repo-size/C4illin/ConvertX)
A self-hosted online file converter. Supports 831 different formats. Written with TypeScript, Bun and Elysia. ![Docker container size](https://ghcr-badge.egpl.dev/c4illin/convertx/size?color=%230375b6&tag=latest&label=image+size&trim=)
<!-- ![Dev image size](https://ghcr-badge.egpl.dev/c4illin/convertx/size?color=%230375b6&tag=main&label=dev+image&trim=) -->
## Features
A self-hosted online file converter. Supports over a thousand different formats. Written with TypeScript, Bun and Elysia.
- Convert files to different formats
- Process multiple files at once ## Features
- Password protection
- Multiple accounts - Convert files to different formats
- Process multiple files at once
## Converters supported - Password protection
- Multiple accounts
| Converter | Use case | Converts from | Converts to |
|------------------------------------------------------------------------------|---------------|---------------|-------------| ## Converters supported
| [libjxl](https://github.com/libjxl/libjxl) | JPEG XL | 11 | 11 |
| [resvg](https://github.com/RazrFalcon/resvg) | SVG | 1 | 1 | | Converter | Use case | Converts from | Converts to |
| [Vips](https://github.com/libvips/libvips) | Images | 45 | 23 | |------------------------------------------------------------------------------|---------------|---------------|-------------|
| [XeLaTeX](https://tug.org/xetex/) | LaTeX | 1 | 1 | | [libjxl](https://github.com/libjxl/libjxl) | JPEG XL | 11 | 11 |
| [Pandoc](https://pandoc.org/) | Documents | 43 | 65 | | [resvg](https://github.com/RazrFalcon/resvg) | SVG | 1 | 1 |
| [GraphicsMagick](http://www.graphicsmagick.org/) | Images | 166 | 133 | | [Vips](https://github.com/libvips/libvips) | Images | 45 | 23 |
| [FFmpeg](https://ffmpeg.org/) | Video | ~473 | ~280 | | [Assimp](https://github.com/assimp/assimp) | 3D Assets | 70 | 24 |
| [XeLaTeX](https://tug.org/xetex/) | LaTeX | 1 | 1 |
<!-- many ffmpeg fileformats are duplicates --> | [Pandoc](https://pandoc.org/) | Documents | 43 | 65 |
| [GraphicsMagick](http://www.graphicsmagick.org/) | Images | 166 | 133 |
Any missing converter? Open an issue or pull request! | [Inkscape](https://inkscape.org/) | Vector images | 7 | 17 |
| [FFmpeg](https://ffmpeg.org/) | Video | ~473 | ~280 |
## Deployment
<!-- many ffmpeg fileformats are duplicates -->
```yml
# docker-compose.yml Any missing converter? Open an issue or pull request!
services:
convertx: ## Deployment
image: ghcr.io/c4illin/convertx
container_name: convertx ```yml
restart: unless-stopped # docker-compose.yml
ports: services:
- "3000:3000" convertx:
environment: # Defaults are listed below. All are optional. image: ghcr.io/c4illin/convertx
- 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) container_name: convertx
- JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234 # will use randomUUID() by default restart: unless-stopped
- HTTP_ALLOWED=false # setting this to true is unsafe, only set this to true locally ports:
- ALLOW_UNAUTHENTICATED=false # allows anyone to use the service without logging in, only set this to true locally - "3000:3000"
- AUTO_DELETE_EVERY_N_HOURS=24 # checks every n hours for files older then n hours and deletes them, set to 0 to disable environment:
volumes: - JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234 # will use randomUUID() if unset
- convertx:/app/data volumes:
``` - convertx:/app/data
```
or
or
```bash
docker run -p 3000:3000 -v ./data:/app/data ghcr.io/c4illin/convertx ```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.
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.
If you get unable to open database file run `chown -R $USER:$USER path` on the path you choose.
### Tutorial
### Environment variables
Tutorial in french: https://belginux.com/installer-convertx-avec-docker/
All are optional, JWT_SECRET is recommended to be set.
## Screenshots
| Name | Default | Description |
![ConvertX Preview](images/preview.png) |---------------------------|---------|-------------|
| JWT_SECRET | when unset it will use the value from randomUUID() | A long and secret string used to sign the JSON Web Token |
## Development | ACCOUNT_REGISTRATION | false | Allow users to register accounts |
| HTTP_ALLOWED | false | Allow HTTP connections, only set this to true locally |
0. Install [Bun](https://bun.sh/) and Git | ALLOW_UNAUTHENTICATED | false | Allow unauthenticated users to use the service, only set this to true locally |
1. Clone the repository | AUTO_DELETE_EVERY_N_HOURS | 24 | Checks every n hours for files older then n hours and deletes them, set to 0 to disable |
2. `bun install` | WEBROOT | "" | The address to the root path setting this to "/convert" will serve the website on "example.com/convert/" |
3. `bun run dev`
> [!WARNING]
Pull requests are welcome! See below and open issues for the list of todos. > If you can't login, make sure you are accessing the service over https or set HTTP_ALLOWED=true
## Todo ### Tutorial
- [x] Add messages for errors in converters
- [x] Add searchable list of formats Tutorial in french: <https://belginux.com/installer-convertx-avec-docker/>
- [ ] Add options for converters
- [ ] Divide index.tsx into smaller components Tutorial in chinese: <https://xzllll.com/24092901/>
- [ ] Add tests
- [ ] Make the upload button nicer and more easy to drop files on. Support copy paste as well if possible. ## Screenshots
- [ ] Make errors logs visible from the web ui
- [ ] Add more converters: ![ConvertX Preview](images/preview.png)
- [ ] [deark](https://github.com/jsummers/deark)
- [ ] LibreOffice ## Development
- [ ] [dvisvgm](https://github.com/mgieseki/dvisvgm)
0. Install [Bun](https://bun.sh/) and Git
## Contributors 1. Clone the repository
2. `bun install`
<a href="https://github.com/C4illin/ConvertX/graphs/contributors"> 3. `bun run dev`
<img src="https://contrib.rocks/image?repo=C4illin/ConvertX" />
</a> Pull requests are welcome! See below and open issues for the list of todos.
## Star History ## Todo
<a href="https://github.com/C4illin/ConvertX/stargazers"> - [x] Add messages for errors in converters
<picture> - [x] Add searchable list of formats
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=C4illin/ConvertX&type=Date&theme=dark" /> - [ ] Add options for converters
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=C4illin/ConvertX&type=Date" /> - [ ] Divide index.tsx into smaller components
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=C4illin/ConvertX&type=Date" /> - [ ] Add tests
</picture> - [ ] Make the upload button nicer and more easy to drop files on. Support copy paste as well if possible.
</a> - [ ] 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>

BIN
bun.lockb

Binary file not shown.

View File

@@ -11,5 +11,6 @@ services:
- HTTP_ALLOWED=true # setting this to true is unsafe, only set this to true locally - 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 - 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 - AUTO_DELETE_EVERY_N_HOURS=1 # checks every n hours for files older then n hours and deletes them, set to 0 to disable
- WEBROOT=/convertx # the root path of the web interface, leave empty to disable
ports: ports:
- 3000:3000 - 3000:3000

View File

@@ -1,27 +1,23 @@
import comments from "@eslint-community/eslint-plugin-eslint-comments/configs";
import { fixupPluginRules } from "@eslint/compat"; import { fixupPluginRules } from "@eslint/compat";
import js from "@eslint/js"; import eslint from "@eslint/js";
import deprecationPlugin from "eslint-plugin-deprecation"; import deprecationPlugin from "eslint-plugin-deprecation";
import importPlugin from "eslint-plugin-import"; import eslintPluginReadableTailwind from "eslint-plugin-readable-tailwind";
import simpleImportSortPlugin from "eslint-plugin-simple-import-sort"; import simpleImportSortPlugin from "eslint-plugin-simple-import-sort";
import tailwind from "eslint-plugin-tailwindcss"; import tailwind from "eslint-plugin-tailwindcss";
import globals from "globals"; import globals from "globals";
import tseslint from "typescript-eslint"; import tseslint from "typescript-eslint";
export default tseslint.config( export default tseslint.config(
js.configs.recommended, eslint.configs.recommended,
importPlugin.flatConfigs.recommended,
comments.recommended,
...tseslint.configs.recommended, ...tseslint.configs.recommended,
...tailwind.configs["flat/recommended"], ...tailwind.configs["flat/recommended"],
{ {
plugins: { plugins: {
"@typescript-eslint": tseslint.plugin,
deprecation: fixupPluginRules(deprecationPlugin), deprecation: fixupPluginRules(deprecationPlugin),
import: fixupPluginRules(importPlugin),
"simple-import-sort": simpleImportSortPlugin, "simple-import-sort": simpleImportSortPlugin,
"readable-tailwind": eslintPluginReadableTailwind,
}, },
ignores: ["**/node_modules/**", "**/public/**"], ignores: ["**/node_modules/**"],
languageOptions: { languageOptions: {
parserOptions: { parserOptions: {
projectService: true, projectService: true,
@@ -32,14 +28,23 @@ export default tseslint.config(
}, },
globals: { globals: {
...globals.node, ...globals.node,
...globals.browser,
}, },
}, },
files: ["**/*.{js,mjs,cjs}"], files: ["**/*.{js,mjs,cjs,tsx,ts}"],
rules: { rules: {
"tailwindcss/no-custom-classname": [ ...eslintPluginReadableTailwind.configs.warning.rules,
"error", "tailwindcss/classnames-order": "off",
"readable-tailwind/multiline": [
"warn",
{
group: "newLine",
printWidth: 100,
},
],
"tailwindcss/no-custom-classname": [
"warn",
{ {
config: "./tailwind.config.js",
whitelist: [ whitelist: [
"select_container", "select_container",
"convert_to_popup", "convert_to_popup",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -1,22 +1,23 @@
{ {
"name": "convertx-frontend", "name": "convertx-frontend",
"version": "0.6.0", "version": "0.9.0",
"scripts": { "scripts": {
"dev": "bun run --watch src/index.tsx", "dev": "bun run --watch src/index.tsx",
"hot": "bun run --hot src/index.tsx", "hot": "bun run --hot src/index.tsx",
"format": "biome format --write ./src", "format": "eslint --fix .",
"build": "postcss ./src/main.css -o ./src/public/generated.css", "build": "postcss ./src/main.css -o ./public/generated.css",
"lint": "run-p 'lint:*'", "lint": "run-p 'lint:*'",
"lint:tsc": "tsc --noEmit", "lint:tsc": "tsc --noEmit",
"lint:knip": "knip", "lint:knip": "knip",
"lint:biome": "biome lint --error-on-warnings ./src" "lint:eslint": "eslint ."
}, },
"dependencies": { "dependencies": {
"@elysiajs/cookie": "^0.8.0", "@elysiajs/cookie": "^0.8.0",
"@elysiajs/html": "1.0.2", "@elysiajs/html": "^1.1.1",
"@elysiajs/jwt": "^1.1.1", "@elysiajs/jwt": "^1.1.1",
"@elysiajs/static": "1.0.3", "@elysiajs/static": "^1.1.1",
"elysia": "^1.1.16" "@kitajs/html": "^4.2.4",
"elysia": "^1.1.25"
}, },
"module": "src/index.tsx", "module": "src/index.tsx",
"type": "module", "type": "module",
@@ -24,44 +25,31 @@
"start": "bun run src/index.tsx" "start": "bun run src/index.tsx"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.9.2", "@eslint/compat": "^1.2.3",
"@eslint-community/eslint-plugin-eslint-comments": "^4.4.0", "@eslint/js": "^9.15.0",
"@eslint/compat": "^1.1.1", "@ianvs/prettier-plugin-sort-imports": "^4.4.0",
"@eslint/js": "^9.11.1",
"@ianvs/prettier-plugin-sort-imports": "^4.3.1",
"@kitajs/ts-html-plugin": "^4.1.0", "@kitajs/ts-html-plugin": "^4.1.0",
"@picocss/pico": "^2.0.6",
"@total-typescript/ts-reset": "^0.6.1", "@total-typescript/ts-reset": "^0.6.1",
"@types/bun": "^1.1.10", "@types/bun": "^1.1.13",
"@types/eslint": "^9.6.1",
"@types/eslint-plugin-tailwindcss": "^3.17.0", "@types/eslint-plugin-tailwindcss": "^3.17.0",
"@types/eslint__js": "^8.42.3", "@types/eslint__js": "^8.42.3",
"@types/node": "^22.6.1", "@types/node": "^22.9.1",
"@typescript-eslint/eslint-plugin": "^8.7.0",
"@typescript-eslint/parser": "^8.7.0",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"cssnano": "^7.0.6", "cssnano": "^7.0.6",
"eslint": "^9.11.1", "eslint": "^9.15.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-deprecation": "^3.0.0", "eslint-plugin-deprecation": "^3.0.0",
"eslint-plugin-import": "^2.30.0", "eslint-plugin-readable-tailwind": "^1.8.2",
"eslint-plugin-isaacscript": "^4.0.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-tailwindcss": "^3.17.4", "eslint-plugin-tailwindcss": "^3.17.5",
"globals": "^15.9.0", "globals": "^15.12.0",
"knip": "^5.30.5", "knip": "^5.37.1",
"npm-run-all2": "^6.2.3", "npm-run-all2": "^7.0.1",
"postcss": "^8.4.47", "postcss": "^8.4.49",
"postcss-cli": "^11.0.0", "postcss-cli": "^11.0.0",
"postcss-lightningcss": "^1.0.1",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"tailwind-scrollbar": "^3.1.0", "tailwind-scrollbar": "^3.1.0",
"tailwindcss": "^3.4.13", "tailwindcss": "^3.4.15",
"typescript": "^5.6.2", "typescript": "^5.6.3",
"typescript-eslint": "^8.7.0" "typescript-eslint": "^8.15.0"
}, }
"trustedDependencies": [
"@biomejs/biome"
]
} }

View File

@@ -1,9 +0,0 @@
// eslint-disable-next-line no-undef
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
// eslint-disable-next-line no-undef
...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {})
}
}

8
postcss.config.js Normal file
View 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],
};

View File

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

Before

Width:  |  Height:  |  Size: 476 B

After

Width:  |  Height:  |  Size: 476 B

View File

Before

Width:  |  Height:  |  Size: 960 B

After

Width:  |  Height:  |  Size: 960 B

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,3 +1,5 @@
const webroot = document.querySelector("meta[name='webroot']").content;
window.downloadAll = function () { window.downloadAll = function () {
// Get all download links // Get all download links
const downloadLinks = document.querySelectorAll("a[download]"); const downloadLinks = document.querySelectorAll("a[download]");
@@ -18,7 +20,7 @@ let progressElem = document.querySelector("progress");
const refreshData = () => { const refreshData = () => {
// console.log("Refreshing data...", progressElem.value, progressElem.max); // console.log("Refreshing data...", progressElem.value, progressElem.max);
if (progressElem.value !== progressElem.max) { if (progressElem.value !== progressElem.max) {
fetch(`/progress/${jobId}`, { fetch(`${webroot}/progress/${jobId}`, {
method: "POST", method: "POST",
}) })
.then((res) => res.text()) .then((res) => res.text())

View File

@@ -1,211 +1,236 @@
// Select the file input element const webroot = document.querySelector("meta[name='webroot']").content;
const fileInput = document.querySelector('input[type="file"]'); const fileInput = document.querySelector('input[type="file"]');
const dropZone = document.getElementById("dropzone"); const dropZone = document.getElementById("dropzone");
const fileNames = []; const convertButton = document.querySelector("input[type='submit']");
let fileType; const fileNames = [];
let fileType;
dropZone.addEventListener("dragover", (e) => { let pendingFiles = 0;
dropZone.classList.add("dragover"); let formatSelected = false;
});
dropZone.addEventListener("dragover", () => {
dropZone.addEventListener("dragleave", (e) => { dropZone.classList.add("dragover");
dropZone.classList.remove("dragover"); });
});
dropZone.addEventListener("dragleave", () => {
const selectContainer = document.querySelector("form .select_container"); dropZone.classList.remove("dragover");
});
const updateSearchBar = () => {
const convertToInput = document.querySelector( dropZone.addEventListener("drop", () => {
"input[name='convert_to_search']", dropZone.classList.remove("dragover");
); });
const convertToPopup = document.querySelector(".convert_to_popup");
const convertToGroupElements = document.querySelectorAll(".convert_to_group"); const selectContainer = document.querySelector("form .select_container");
const convertToGroups = {};
const convertToElement = document.querySelector("select[name='convert_to']"); const updateSearchBar = () => {
const convertToInput = document.querySelector(
const showMatching = (search) => { "input[name='convert_to_search']",
for (const [targets, groupElement] of Object.values(convertToGroups)) { );
let matchingTargetsFound = 0; const convertToPopup = document.querySelector(".convert_to_popup");
for (const target of targets) { const convertToGroupElements = document.querySelectorAll(".convert_to_group");
if (target.dataset.target.includes(search)) { const convertToGroups = {};
matchingTargetsFound++; const convertToElement = document.querySelector("select[name='convert_to']");
target.classList.remove("hidden");
target.classList.add("flex"); const showMatching = (search) => {
} else { for (const [targets, groupElement] of Object.values(convertToGroups)) {
target.classList.add("hidden"); let matchingTargetsFound = 0;
target.classList.remove("flex"); for (const target of targets) {
} if (target.dataset.target.includes(search)) {
} matchingTargetsFound++;
target.classList.remove("hidden");
if (matchingTargetsFound === 0) { target.classList.add("flex");
groupElement.classList.add("hidden"); } else {
groupElement.classList.remove("flex"); target.classList.add("hidden");
} else { target.classList.remove("flex");
groupElement.classList.remove("hidden"); }
groupElement.classList.add("flex"); }
}
} if (matchingTargetsFound === 0) {
}; groupElement.classList.add("hidden");
groupElement.classList.remove("flex");
for (const groupElement of convertToGroupElements) { } else {
const groupName = groupElement.dataset.converter; groupElement.classList.remove("hidden");
groupElement.classList.add("flex");
const targetElements = groupElement.querySelectorAll(".target"); }
const targets = Array.from(targetElements); }
};
for (const target of targets) {
target.onmousedown = () => { for (const groupElement of convertToGroupElements) {
convertToElement.value = target.dataset.value; const groupName = groupElement.dataset.converter;
convertToInput.value = `${target.dataset.target} using ${target.dataset.converter}`;
showMatching(""); const targetElements = groupElement.querySelectorAll(".target");
}; const targets = Array.from(targetElements);
}
for (const target of targets) {
convertToGroups[groupName] = [targets, groupElement]; target.onmousedown = () => {
} convertToElement.value = target.dataset.value;
convertToInput.value = `${target.dataset.target} using ${target.dataset.converter}`;
convertToInput.addEventListener("input", (e) => { formatSelected = true;
showMatching(e.target.value.toLowerCase()); if (pendingFiles === 0 && fileNames.length > 0) {
}); convertButton.disabled = false;
}
convertToInput.addEventListener("blur", (e) => { showMatching("");
// 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"); convertToGroups[groupName] = [targets, groupElement];
convertToPopup.classList.remove("flex"); }
return;
} convertToInput.addEventListener("input", (e) => {
showMatching(e.target.value.toLowerCase());
convertToPopup.classList.add("hidden"); });
convertToPopup.classList.remove("flex");
}); convertToInput.addEventListener("search", () => {
// when the user clears the search bar using the 'x' button
convertToInput.addEventListener("focus", () => { convertButton.disabled = true;
convertToPopup.classList.remove("hidden"); formatSelected = false;
convertToPopup.classList.add("flex"); });
});
}; convertToInput.addEventListener("blur", (e) => {
// Keep the popup open even when clicking on a target button
// const convertFromSelect = document.querySelector("select[name='convert_from']"); // for a split second to allow the click to go through
if (e?.relatedTarget?.classList?.contains("target")) {
// Add a 'change' event listener to the file input element convertToPopup.classList.add("hidden");
fileInput.addEventListener("change", (e) => { convertToPopup.classList.remove("flex");
// console.log(e.target.files); return;
// Get the selected files from the event target }
const files = e.target.files;
convertToPopup.classList.add("hidden");
// Select the file-list table convertToPopup.classList.remove("flex");
const fileList = document.querySelector("#file-list"); });
// Loop through the selected files convertToInput.addEventListener("focus", () => {
for (const file of files) { convertToPopup.classList.remove("hidden");
// Create a new table row for each file convertToPopup.classList.add("flex");
const row = document.createElement("tr"); });
row.innerHTML = ` };
<td>${file.name}</td>
<td>${(file.size / 1024).toFixed(2)} kB</td> // Add a 'change' event listener to the file input element
<td><a class="secondary" onclick="deleteRow(this)" style="cursor: pointer">Remove</a></td> fileInput.addEventListener("change", (e) => {
`; // Get the selected files from the event target
const files = e.target.files;
if (!fileType) {
fileType = file.name.split(".").pop(); // Select the file-list table
console.log("fileType", fileType); const fileList = document.querySelector("#file-list");
fileInput.setAttribute("accept", `.${fileType}`);
setTitle(); // Loop through the selected files
for (const file of files) {
// choose the option that matches the file type // Create a new table row for each file
// for (const option of convertFromSelect.children) { const row = document.createElement("tr");
// console.log(option.value); row.innerHTML = `
// if (option.value === fileType) { <td>${file.name}</td>
// option.selected = true; <td>${(file.size / 1024).toFixed(2)} kB</td>
// } <td><a onclick="deleteRow(this)">Remove</a></td>
// } `;
fetch("/conversions", { if (!fileType) {
method: "POST", fileType = file.name.split(".").pop();
body: JSON.stringify({ fileType: fileType }), fileInput.setAttribute("accept", `.${fileType}`);
headers: { setTitle();
"Content-Type": "application/json",
}, // choose the option that matches the file type
}) // for (const option of convertFromSelect.children) {
.then((res) => res.text()) // console.log(option.value);
.then((html) => { // if (option.value === fileType) {
selectContainer.innerHTML = html; // option.selected = true;
updateSearchBar(); // }
}) // }
.catch((err) => console.log(err));
} fetch(`${webroot}/conversions`, {
method: "POST",
// Append the row to the file-list table body: JSON.stringify({ fileType: fileType }),
fileList.appendChild(row); headers: {
"Content-Type": "application/json",
// Append the file to the hidden input },
fileNames.push(file.name); })
} .then((res) => res.text())
.then((html) => {
uploadFiles(files); selectContainer.innerHTML = html;
}); updateSearchBar();
})
const setTitle = () => { .catch((err) => console.log(err));
const title = document.querySelector("h1"); }
title.textContent = `Convert ${fileType ? `.${fileType}` : ""}`;
}; // Append the row to the file-list table
fileList.appendChild(row);
// Add a onclick for the delete button
const deleteRow = (target) => { // Append the file to the hidden input
const filename = target.parentElement.parentElement.children[0].textContent; fileNames.push(file.name);
const row = target.parentElement.parentElement; }
row.remove();
uploadFiles(files);
// remove from fileNames });
const index = fileNames.indexOf(filename);
fileNames.splice(index, 1); const setTitle = () => {
const title = document.querySelector("h1");
// if fileNames is empty, reset fileType title.textContent = `Convert ${fileType ? `.${fileType}` : ""}`;
if (fileNames.length === 0) { };
fileType = null;
fileInput.removeAttribute("accept"); // Add a onclick for the delete button
setTitle(); // eslint-disable-next-line @typescript-eslint/no-unused-vars
} const deleteRow = (target) => {
const filename = target.parentElement.parentElement.children[0].textContent;
fetch("/delete", { const row = target.parentElement.parentElement;
method: "POST", row.remove();
body: JSON.stringify({ filename: filename }),
headers: { // remove from fileNames
"Content-Type": "application/json", const index = fileNames.indexOf(filename);
}, fileNames.splice(index, 1);
})
.then((res) => res.json()) // reset fileInput
.then((data) => { fileInput.value = "";
console.log(data);
}) // if fileNames is empty, reset fileType
.catch((err) => console.log(err)); if (fileNames.length === 0) {
}; fileType = null;
fileInput.removeAttribute("accept");
const uploadFiles = (files) => { convertButton.disabled = true;
const formData = new FormData(); setTitle();
}
for (const file of files) {
formData.append("file", file, file.name); fetch(`${webroot}/delete`, {
} method: "POST",
body: JSON.stringify({ filename: filename }),
fetch("/upload", { headers: {
method: "POST", "Content-Type": "application/json",
body: formData, },
}) })
.then((res) => res.json()) .catch((err) => console.log(err));
.then((data) => { };
console.log(data);
}) const uploadFiles = (files) => {
.catch((err) => console.log(err)); convertButton.disabled = true;
}; convertButton.textContent = "Uploading...";
pendingFiles += 1;
const formConvert = document.querySelector("form[action='/convert']");
const formData = new FormData();
formConvert.addEventListener("submit", (e) => {
const hiddenInput = document.querySelector("input[name='file_names']"); for (const file of files) {
hiddenInput.value = JSON.stringify(fileNames); formData.append("file", file, file.name);
}); }
updateSearchBar(); 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();

View File

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

View File

@@ -1,32 +1,40 @@
import { Html } from "@elysiajs/html";
export const BaseHtml = ({ export const BaseHtml = ({
children, children,
title = "ConvertX", title = "ConvertX",
}: { children: JSX.Element; title?: string }) => ( webroot = "",
}: {
children: JSX.Element;
title?: string;
webroot?: string;
}) => (
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="webroot" content={webroot} />
<title safe>{title}</title> <title safe>{title}</title>
<link rel="stylesheet" href="/generated.css" /> <link rel="stylesheet" href={`${webroot}/generated.css`} />
<link <link
rel="apple-touch-icon" rel="apple-touch-icon"
sizes="180x180" sizes="180x180"
href="/apple-touch-icon.png" href={`${webroot}/apple-touch-icon.png`}
/> />
<link <link
rel="icon" rel="icon"
type="image/png" type="image/png"
sizes="32x32" sizes="32x32"
href="/favicon-32x32.png" href={`${webroot}/favicon-32x32.png`}
/> />
<link <link
rel="icon" rel="icon"
type="image/png" type="image/png"
sizes="16x16" 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> </head>
<body class="w-full bg-gray-900 text-gray-200">{children}</body> <body class="w-full bg-neutral-900 text-neutral-200">{children}</body>
</html> </html>
); );

View File

@@ -1,22 +1,37 @@
import { Html } from "@kitajs/html";
export const Header = ({ export const Header = ({
loggedIn, loggedIn,
accountRegistration, accountRegistration,
}: { loggedIn?: boolean; accountRegistration?: boolean }) => { webroot = "",
}: {
loggedIn?: boolean;
accountRegistration?: boolean;
webroot?: string;
}) => {
let rightNav: JSX.Element; let rightNav: JSX.Element;
if (loggedIn) { if (loggedIn) {
rightNav = ( rightNav = (
<ul class="flex gap-4 "> <ul class="flex gap-4">
<li> <li>
<a <a
class="text-lime-600 transition-all hover:text-lime-500 hover:underline" class={`
href="/history"> text-accent-600 transition-all
hover:text-accent-500 hover:underline
`}
href={`${webroot}/history`}
>
History History
</a> </a>
</li> </li>
<li> <li>
<a <a
class="text-lime-600 transition-all hover:text-lime-500 hover:underline" class={`
href="/logoff"> text-accent-600 transition-all
hover:text-accent-500 hover:underline
`}
href={`${webroot}/logoff`}
>
Logout Logout
</a> </a>
</li> </li>
@@ -27,16 +42,24 @@ export const Header = ({
<ul class="flex gap-4"> <ul class="flex gap-4">
<li> <li>
<a <a
class="text-lime-600 transition-all hover:text-lime-500 hover:underline" class={`
href="/login"> text-accent-600 transition-all
hover:text-accent-500 hover:underline
`}
href={`${webroot}/login`}
>
Login Login
</a> </a>
</li> </li>
{accountRegistration ? ( {accountRegistration ? (
<li> <li>
<a <a
class="text-lime-600 transition-all hover:text-lime-500 hover:underline" class={`
href="/register"> text-accent-600 transition-all
hover:text-accent-500 hover:underline
`}
href={`${webroot}/register`}
>
Register Register
</a> </a>
</li> </li>
@@ -47,11 +70,11 @@ export const Header = ({
return ( return (
<header class="w-full p-4"> <header class="w-full p-4">
<nav class="mx-auto flex max-w-4xl justify-between rounded bg-gray-900 p-4"> <nav class="mx-auto flex max-w-4xl justify-between rounded bg-neutral-900 p-4">
<ul> <ul>
<li> <li>
<strong> <strong>
<a href="/">ConvertX</a> <a href={`${webroot}/`}>ConvertX</a>
</strong> </strong>
</li> </li>
</ul> </ul>

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

@@ -0,0 +1,144 @@
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",
"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: {
muxer: [
"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> {
// let command = "ffmpeg";
const command = `assimp export "${filePath}" "${targetPath}"`;
return new Promise((resolve, reject) => {
exec(command, (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("Done");
});
});
}

View File

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

View File

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

View File

@@ -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");
});
});
}

View File

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

View File

@@ -1,59 +1,45 @@
import { convert as convertImage, properties as propertiesImage } from "./vips";
import {
convert as convertPandoc,
properties as propertiesPandoc,
} from "./pandoc";
import {
convert as convertFFmpeg,
properties as propertiesFFmpeg,
} from "./ffmpeg";
import {
convert as convertGraphicsmagick,
properties as propertiesGraphicsmagick,
} from "./graphicsmagick";
import {
convert as convertxelatex,
properties as propertiesxelatex,
} from "./xelatex";
import {
convert as convertLibjxl,
properties as propertiesLibjxl,
} from "./libjxl";
import {
convert as convertresvg,
properties as propertiesresvg,
} from "./resvg";
import { normalizeFiletype } from "../helpers/normalizeFiletype"; import { normalizeFiletype } from "../helpers/normalizeFiletype";
import { convert as convertassimp, properties as propertiesassimp } from "./assimp";
import { convert as convertFFmpeg, properties as propertiesFFmpeg } from "./ffmpeg";
import { convert as convertGraphicsmagick, properties as propertiesGraphicsmagick } from "./graphicsmagick";
import { convert as 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";
// This should probably be reconstructed so that the functions are not imported instead the functions hook into this to make the converters more modular // This should probably be reconstructed so that the functions are not imported instead the functions hook into this to make the converters more modular
const properties: Record<string, { const properties: Record<
string,
{
properties: { properties: {
from: Record<string, string[]>; from: Record<string, string[]>;
to: Record<string, string[]>; to: Record<string, string[]>;
options?: Record<string, Record<string, { options?: Record<
string,
Record<
string,
{
description: string; description: string;
type: string; type: string;
default: number; default: number;
}>>; }
>
>;
}; };
converter: ( converter: (
filePath: string, filePath: string,
fileType: string, fileType: string,
convertTo: string, convertTo: string,
targetPath: string, targetPath: string,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
options?: any, options?: unknown,
// biome-ignore lint/suspicious/noExplicitAny: <explanation> ) => unknown;
) => any; }
}> = { > = {
libjxl: { libjxl: {
properties: propertiesLibjxl, properties: propertiesLibjxl,
converter: convertLibjxl, converter: convertLibjxl,
@@ -78,6 +64,14 @@ const properties: Record<string, {
properties: propertiesGraphicsmagick, properties: propertiesGraphicsmagick,
converter: convertGraphicsmagick, converter: convertGraphicsmagick,
}, },
inkscape: {
properties: propertiesInkscape,
converter: convertInkscape,
},
assimp: {
properties: propertiesassimp,
converter: convertassimp,
},
ffmpeg: { ffmpeg: {
properties: propertiesFFmpeg, properties: propertiesFFmpeg,
converter: convertFFmpeg, converter: convertFFmpeg,
@@ -87,24 +81,19 @@ const properties: Record<string, {
export async function mainConverter( export async function mainConverter(
inputFilePath: string, inputFilePath: string,
fileTypeOriginal: string, fileTypeOriginal: string,
// biome-ignore lint/suspicious/noExplicitAny: <explanation> convertTo: string,
convertTo: any,
targetPath: string, targetPath: string,
// biome-ignore lint/suspicious/noExplicitAny: <explanation> options?: unknown,
options?: any,
converterName?: string, converterName?: string,
) { ) {
const fileType = normalizeFiletype(fileTypeOriginal); const fileType = normalizeFiletype(fileTypeOriginal);
// biome-ignore lint/suspicious/noExplicitAny: <explanation> let converterFunc: typeof properties["libjxl"]["converter"] | undefined;
let converterFunc: any;
// let converterName = converterName;
if (converterName) { if (converterName) {
converterFunc = properties[converterName]?.converter; converterFunc = properties[converterName]?.converter;
} else { } else {
// Iterate over each converter in properties // Iterate over each converter in properties
// biome-ignore lint/style/noParameterAssign: <explanation>
for (converterName in properties) { for (converterName in properties) {
const converterObj = properties[converterName]; const converterObj = properties[converterName];
@@ -132,7 +121,7 @@ export async function mainConverter(
} }
try { try {
await converterFunc( const result = await converterFunc(
inputFilePath, inputFilePath,
fileType, fileType,
convertTo, convertTo,
@@ -142,7 +131,13 @@ export async function mainConverter(
console.log( console.log(
`Converted ${inputFilePath} from ${fileType} to ${convertTo} successfully using ${converterName}.`, `Converted ${inputFilePath} from ${fileType} to ${convertTo} successfully using ${converterName}.`,
result,
); );
if (typeof result === "string") {
return result;
}
return "Done"; return "Done";
} catch (error) { } catch (error) {
console.error( console.error(
@@ -178,9 +173,7 @@ for (const converterName in properties) {
} }
} }
export const getPossibleTargets = ( export const getPossibleTargets = (from: string): Record<string, string[]> => {
from: string,
): Record<string, string[]> => {
const fromClean = normalizeFiletype(from); const fromClean = normalizeFiletype(from);
return possibleTargets[fromClean] || {}; return possibleTargets[fromClean] || {};
@@ -204,6 +197,7 @@ for (const converterName in properties) {
} }
possibleInputs.sort(); possibleInputs.sort();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const getPossibleInputs = () => { const getPossibleInputs = () => {
return possibleInputs; return possibleInputs;
}; };
@@ -275,4 +269,4 @@ export const getAllInputs = (converter: string) => {
// } // }
// // print the number of unique Inputs and Outputs // // print the number of unique Inputs and Outputs
// console.log(`Unique Formats: ${uniqueFormats.size}`); // console.log(`Unique Formats: ${uniqueFormats.size}`);

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
import { exec } from "node:child_process"; import { exec } from "node:child_process";
// declare possible conversions // declare possible conversions
export const properties = { export const properties = {
from: { from: {
@@ -94,8 +95,8 @@ export function convert(
fileType: string, fileType: string,
convertTo: string, convertTo: string,
targetPath: string, targetPath: string,
// biome-ignore lint/suspicious/noExplicitAny: <explanation> // eslint-disable-next-line @typescript-eslint/no-unused-vars
options?: any, options?: unknown,
): Promise<string> { ): Promise<string> {
// if (fileType === "svg") { // if (fileType === "svg") {
// const scale = options.scale || 1; // const scale = options.scale || 1;
@@ -134,8 +135,8 @@ export function convert(
console.error(`stderr: ${stderr}`); console.error(`stderr: ${stderr}`);
} }
resolve("success"); resolve("Done");
}, },
); );
}); });
} }

View File

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

View File

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

View File

@@ -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) => { exec("djxl --version", (error, stdout) => {
if (error) { if (error) {
console.error("libjxl-tools is not installed."); console.error("libjxl-tools is not installed.");
@@ -83,6 +93,16 @@ if (process.env.NODE_ENV === "production") {
} }
}); });
exec("assimp version", (error, stdout) => {
if (error) {
console.error("assimp is not installed");
}
if (stdout) {
console.log(`assimp v${stdout.split("\n")[5]}`);
}
});
exec("bun -v", (error, stdout) => { exec("bun -v", (error, stdout) => {
if (error) { if (error) {
console.error("Bun is not installed. wait what"); console.error("Bun is not installed. wait what");

View File

@@ -2,7 +2,7 @@ import { randomInt, randomUUID } from "node:crypto";
import { rmSync } from "node:fs"; import { rmSync } from "node:fs";
import { mkdir, unlink } from "node:fs/promises"; import { mkdir, unlink } from "node:fs/promises";
import cookie from "@elysiajs/cookie"; import cookie from "@elysiajs/cookie";
import { html } from "@elysiajs/html"; import { html, Html } from "@elysiajs/html";
import { jwt, type JWTPayloadSpec } from "@elysiajs/jwt"; import { jwt, type JWTPayloadSpec } from "@elysiajs/jwt";
import { staticPlugin } from "@elysiajs/static"; import { staticPlugin } from "@elysiajs/static";
import { Database } from "bun:sqlite"; import { 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) ? Number(process.env.AUTO_DELETE_EVERY_N_HOURS)
: 24; : 24;
const WEBROOT = process.env.WEBROOT ?? "";
// fileNames: fileNames, // fileNames: fileNames,
// filesToConvert: fileNames.length, // filesToConvert: fileNames.length,
// convertedFiles : 0, // convertedFiles : 0,
@@ -112,6 +114,7 @@ const app = new Elysia({
serve: { serve: {
maxRequestBodySize: Number.MAX_SAFE_INTEGER, maxRequestBodySize: Number.MAX_SAFE_INTEGER,
}, },
prefix: WEBROOT,
}) })
.use(cookie()) .use(cookie())
.use(html()) .use(html())
@@ -127,29 +130,43 @@ const app = new Elysia({
) )
.use( .use(
staticPlugin({ staticPlugin({
assets: "src/public/", assets: "public",
prefix: "/", prefix: "",
}), }),
) )
.get("/test", () => {
return (
<html lang="en">
<head>
<title>Hello World</title>
</head>
<body>
<h1>Hello</h1>
</body>
</html>
);
})
.get("/setup", ({ redirect }) => { .get("/setup", ({ redirect }) => {
if (!FIRST_RUN) { if (!FIRST_RUN) {
return redirect("/login", 302); return redirect(`${WEBROOT}/login`, 302);
} }
return ( return (
<BaseHtml title="ConvertX | Setup"> <BaseHtml title="ConvertX | Setup" webroot={WEBROOT}>
<main class="mx-auto w-full max-w-4xl px-4"> <main class="mx-auto w-full max-w-4xl px-4">
<h1 class="my-8 text-3xl">Welcome to ConvertX!</h1> <h1 class="my-8 text-3xl">Welcome to ConvertX!</h1>
<article class="article p-0"> <article class="article p-0">
<header class="w-full bg-gray-800 p-4">Create your account</header> <header class="w-full bg-neutral-800 p-4">
<form method="post" action="/register" class="p-4"> Create your account
</header>
<form method="post" action={`${WEBROOT}/register`} class="p-4">
<fieldset class="mb-4 flex flex-col gap-4"> <fieldset class="mb-4 flex flex-col gap-4">
<label class="flex flex-col gap-1"> <label class="flex flex-col gap-1">
Email Email
<input <input
type="email" type="email"
name="email" name="email"
class="rounded bg-gray-800 p-3" class="rounded bg-neutral-800 p-3"
placeholder="Email" placeholder="Email"
autocomplete="email" autocomplete="email"
required required
@@ -160,7 +177,7 @@ const app = new Elysia({
<input <input
type="password" type="password"
name="password" name="password"
class="rounded bg-gray-800 p-3" class="rounded bg-neutral-800 p-3"
placeholder="Password" placeholder="Password"
autocomplete="current-password" autocomplete="current-password"
required required
@@ -172,7 +189,10 @@ const app = new Elysia({
<footer class="p-4"> <footer class="p-4">
Report any issues on{" "} Report any issues on{" "}
<a <a
class="text-lime-500 underline hover:text-lime-400" class={`
text-accent-500 underline
hover:text-accent-400
`}
href="https://github.com/C4illin/ConvertX" href="https://github.com/C4illin/ConvertX"
> >
GitHub GitHub
@@ -186,13 +206,16 @@ const app = new Elysia({
}) })
.get("/register", ({ redirect }) => { .get("/register", ({ redirect }) => {
if (!ACCOUNT_REGISTRATION) { if (!ACCOUNT_REGISTRATION) {
return redirect("/login", 302); return redirect(`${WEBROOT}/login`, 302);
} }
return ( return (
<BaseHtml title="ConvertX | Register"> <BaseHtml webroot={WEBROOT} title="ConvertX | Register">
<> <>
<Header accountRegistration={ACCOUNT_REGISTRATION} /> <Header
webroot={WEBROOT}
accountRegistration={ACCOUNT_REGISTRATION}
/>
<main class="w-full px-4"> <main class="w-full px-4">
<article class="article"> <article class="article">
<form method="post" class="flex flex-col gap-4"> <form method="post" class="flex flex-col gap-4">
@@ -202,7 +225,7 @@ const app = new Elysia({
<input <input
type="email" type="email"
name="email" name="email"
class="rounded bg-gray-800 p-3" class="rounded bg-neutral-800 p-3"
placeholder="Email" placeholder="Email"
autocomplete="email" autocomplete="email"
required required
@@ -213,7 +236,7 @@ const app = new Elysia({
<input <input
type="password" type="password"
name="password" name="password"
class="rounded bg-gray-800 p-3" class="rounded bg-neutral-800 p-3"
placeholder="Password" placeholder="Password"
autocomplete="current-password" autocomplete="current-password"
required required
@@ -236,7 +259,7 @@ const app = new Elysia({
"/register", "/register",
async ({ body, set, redirect, jwt, cookie: { auth } }) => { async ({ body, set, redirect, jwt, cookie: { auth } }) => {
if (!ACCOUNT_REGISTRATION && !FIRST_RUN) { if (!ACCOUNT_REGISTRATION && !FIRST_RUN) {
return redirect("/login", 302); return redirect(`${WEBROOT}/login`, 302);
} }
if (FIRST_RUN) { if (FIRST_RUN) {
@@ -291,13 +314,13 @@ const app = new Elysia({
sameSite: "strict", sameSite: "strict",
}); });
return redirect("/", 302); return redirect(`${WEBROOT}/`, 302);
}, },
{ body: t.Object({ email: t.String(), password: t.String() }) }, { body: t.Object({ email: t.String(), password: t.String() }) },
) )
.get("/login", async ({ jwt, redirect, cookie: { auth } }) => { .get("/login", async ({ jwt, redirect, cookie: { auth } }) => {
if (FIRST_RUN) { if (FIRST_RUN) {
return redirect("/setup", 302); return redirect(`${WEBROOT}/setup`, 302);
} }
// if already logged in, redirect to home // if already logged in, redirect to home
@@ -305,16 +328,19 @@ const app = new Elysia({
const user = await jwt.verify(auth.value); const user = await jwt.verify(auth.value);
if (user) { if (user) {
return redirect("/", 302); return redirect(`${WEBROOT}/`, 302);
} }
auth.remove(); auth.remove();
} }
return ( return (
<BaseHtml title="ConvertX | Login"> <BaseHtml webroot={WEBROOT} title="ConvertX | Login">
<> <>
<Header accountRegistration={ACCOUNT_REGISTRATION} /> <Header
webroot={WEBROOT}
accountRegistration={ACCOUNT_REGISTRATION}
/>
<main class="w-full px-4"> <main class="w-full px-4">
<article class="article"> <article class="article">
<form method="post" class="flex flex-col gap-4"> <form method="post" class="flex flex-col gap-4">
@@ -324,7 +350,7 @@ const app = new Elysia({
<input <input
type="email" type="email"
name="email" name="email"
class="rounded bg-gray-800 p-3" class="rounded bg-neutral-800 p-3"
placeholder="Email" placeholder="Email"
autocomplete="email" autocomplete="email"
required required
@@ -335,7 +361,7 @@ const app = new Elysia({
<input <input
type="password" type="password"
name="password" name="password"
class="rounded bg-gray-800 p-3" class="rounded bg-neutral-800 p-3"
placeholder="Password" placeholder="Password"
autocomplete="current-password" autocomplete="current-password"
required required
@@ -345,7 +371,7 @@ const app = new Elysia({
<div role="group"> <div role="group">
{ACCOUNT_REGISTRATION ? ( {ACCOUNT_REGISTRATION ? (
<a <a
href="/register" href={`${WEBROOT}/register`}
role="button" role="button"
class="btn-primary w-full" class="btn-primary w-full"
> >
@@ -412,7 +438,7 @@ const app = new Elysia({
sameSite: "strict", sameSite: "strict",
}); });
return redirect("/", 302); return redirect(`${WEBROOT}/`, 302);
}, },
{ body: t.Object({ email: t.String(), password: t.String() }) }, { body: t.Object({ email: t.String(), password: t.String() }) },
) )
@@ -421,22 +447,22 @@ const app = new Elysia({
auth.remove(); auth.remove();
} }
return redirect("/login", 302); return redirect(`${WEBROOT}/login`, 302);
}) })
.post("/logoff", ({ redirect, cookie: { auth } }) => { .post("/logoff", ({ redirect, cookie: { auth } }) => {
if (auth?.value) { if (auth?.value) {
auth.remove(); auth.remove();
} }
return redirect("/login", 302); return redirect(`${WEBROOT}/login`, 302);
}) })
.get("/", async ({ jwt, redirect, cookie: { auth, jobId } }) => { .get("/", async ({ jwt, redirect, cookie: { auth, jobId } }) => {
if (FIRST_RUN) { if (FIRST_RUN) {
return redirect("/setup", 302); return redirect(`${WEBROOT}/setup`, 302);
} }
if (!auth?.value && !ALLOW_UNAUTHENTICATED) { if (!auth?.value && !ALLOW_UNAUTHENTICATED) {
return redirect("/login", 302); return redirect(`${WEBROOT}/login`, 302);
} }
// validate jwt // validate jwt
@@ -456,7 +482,7 @@ const app = new Elysia({
if (auth?.value) { if (auth?.value) {
auth.remove(); auth.remove();
} }
return redirect("/login", 302); return redirect(`${WEBROOT}/login`, 302);
} }
} }
} }
@@ -489,7 +515,7 @@ const app = new Elysia({
} }
if (!user) { if (!user) {
return redirect("/login", 302); return redirect(`${WEBROOT}/login`, 302);
} }
// create a new job // create a new job
@@ -519,21 +545,30 @@ const app = new Elysia({
console.log("jobId set to:", id); console.log("jobId set to:", id);
return ( return (
<BaseHtml> <BaseHtml webroot={WEBROOT}>
<> <>
<Header loggedIn /> <Header webroot={WEBROOT} loggedIn />
<main class="w-full px-4"> <main class="w-full px-4">
<article class="article"> <article class="article">
<h1 class="mb-4 text-xl">Convert</h1> <h1 class="mb-4 text-xl">Convert</h1>
<div class="mb-4 max-h-[50vh] overflow-y-auto scrollbar-thin"> <div class="mb-4 max-h-[50vh] overflow-y-auto scrollbar-thin">
<table <table
id="file-list" id="file-list"
class="w-full table-auto rounded bg-gray-900 [&_td]:p-4 [&_tr]:rounded [&_tr]:border-b [&_tr]:border-gray-800" class={`
w-full table-auto rounded bg-neutral-900
[&_td]:p-4
[&_tr]:rounded [&_tr]:border-b [&_tr]:border-neutral-800
`}
/> />
</div> </div>
<div <div
id="dropzone" id="dropzone"
class="relative flex h-48 w-full items-center justify-center rounded border border-dashed border-gray-700 transition-all hover:border-gray-600 [&.dragover]:border-4 [&.dragover]:border-gray-500" class={`
relative flex h-48 w-full items-center justify-center rounded border border-dashed
border-neutral-700 transition-all
[&.dragover]:border-4 [&.dragover]:border-neutral-500
hover:border-neutral-600
`}
> >
<span> <span>
<b>Choose a file</b> or drag it here <b>Choose a file</b> or drag it here
@@ -548,7 +583,7 @@ const app = new Elysia({
</article> </article>
<form <form
method="post" method="post"
action="/convert" action={`${WEBROOT}/convert`}
class="relative mx-auto mb-[35vh] w-full max-w-4xl" class="relative mx-auto mb-[35vh] w-full max-w-4xl"
> >
<input type="hidden" name="file_names" id="file_names" /> <input type="hidden" name="file_names" id="file_names" />
@@ -558,14 +593,22 @@ const app = new Elysia({
name="convert_to_search" name="convert_to_search"
placeholder="Search for conversions" placeholder="Search for conversions"
autocomplete="off" autocomplete="off"
class="w-full rounded bg-gray-800 p-4" class="w-full rounded bg-neutral-800 p-4"
/> />
<div class="select_container relative"> <div class="select_container relative">
<article class="convert_to_popup absolute z-[2] m-0 hidden h-[30vh] max-h-[50vh] w-full flex-col overflow-y-auto overflow-x-hidden rounded bg-gray-800 sm:h-[30vh]"> <article
class={`
convert_to_popup absolute z-[2] m-0 hidden h-[30vh] max-h-[50vh] w-full
flex-col overflow-y-auto overflow-x-hidden rounded bg-neutral-800
sm:h-[30vh]
`}
>
{Object.entries(getAllTargets()).map( {Object.entries(getAllTargets()).map(
([converter, targets]) => ( ([converter, targets]) => (
<article <article
class="convert_to_group w-full border-b border-gray-700 p-4" class={`
convert_to_group flex w-full flex-col border-b border-neutral-700 p-4
`}
data-converter={converter} data-converter={converter}
> >
<header class="mb-2 w-full text-xl font-bold" safe> <header class="mb-2 w-full text-xl font-bold" safe>
@@ -576,7 +619,10 @@ const app = new Elysia({
<button <button
// https://stackoverflow.com/questions/121499/when-a-blur-event-occurs-how-can-i-find-out-which-element-focus-went-to#comment82388679_33325953 // https://stackoverflow.com/questions/121499/when-a-blur-event-occurs-how-can-i-find-out-which-element-focus-went-to#comment82388679_33325953
tabindex={0} tabindex={0}
class="target rounded bg-gray-700 p-1 text-base hover:bg-gray-600" class={`
target rounded bg-neutral-700 p-1 text-base
hover:bg-neutral-600
`}
data-value={`${target},${converter}`} data-value={`${target},${converter}`}
data-target={target} data-target={target}
data-converter={converter} data-converter={converter}
@@ -616,7 +662,15 @@ const app = new Elysia({
</select> </select>
</div> </div>
</article> </article>
<input class="btn-primary w-full" type="submit" value="Convert" /> <input
class={`
btn-primary w-full
disabled:cursor-not-allowed disabled:opacity-50
`}
type="submit"
value="Convert"
disabled
/>
</form> </form>
</main> </main>
<script src="script.js" defer /> <script src="script.js" defer />
@@ -629,11 +683,17 @@ const app = new Elysia({
({ body }) => { ({ body }) => {
return ( return (
<> <>
<article class="convert_to_popup absolute z-[2] m-0 hidden h-[50vh] max-h-[50vh] w-full flex-col overflow-y-auto overflow-x-hidden rounded bg-gray-800 sm:h-[30vh]"> <article
class={`
convert_to_popup absolute z-[2] m-0 hidden h-[50vh] max-h-[50vh] w-full flex-col
overflow-y-auto overflow-x-hidden rounded bg-neutral-800
sm:h-[30vh]
`}
>
{Object.entries(getPossibleTargets(body.fileType)).map( {Object.entries(getPossibleTargets(body.fileType)).map(
([converter, targets]) => ( ([converter, targets]) => (
<article <article
class="convert_to_group w-full border-b border-gray-700 p-4" class="convert_to_group flex w-full flex-col border-b border-neutral-700 p-4"
data-converter={converter} data-converter={converter}
> >
<header class="mb-2 w-full text-xl font-bold" safe> <header class="mb-2 w-full text-xl font-bold" safe>
@@ -644,7 +704,10 @@ const app = new Elysia({
<button <button
// https://stackoverflow.com/questions/121499/when-a-blur-event-occurs-how-can-i-find-out-which-element-focus-went-to#comment82388679_33325953 // https://stackoverflow.com/questions/121499/when-a-blur-event-occurs-how-can-i-find-out-which-element-focus-went-to#comment82388679_33325953
tabindex={0} tabindex={0}
class="target rounded bg-gray-700 p-1 text-base hover:bg-gray-600" class={`
target rounded bg-neutral-700 p-1 text-base
hover:bg-neutral-600
`}
data-value={`${target},${converter}`} data-value={`${target},${converter}`}
data-target={target} data-target={target}
data-converter={converter} data-converter={converter}
@@ -685,16 +748,16 @@ const app = new Elysia({
"/upload", "/upload",
async ({ body, redirect, jwt, cookie: { auth, jobId } }) => { async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
if (!auth?.value) { if (!auth?.value) {
return redirect("/login", 302); return redirect(`${WEBROOT}/login`, 302);
} }
const user = await jwt.verify(auth.value); const user = await jwt.verify(auth.value);
if (!user) { if (!user) {
return redirect("/login", 302); return redirect(`${WEBROOT}/login`, 302);
} }
if (!jobId?.value) { if (!jobId?.value) {
return redirect("/", 302); return redirect(`${WEBROOT}/`, 302);
} }
const existingJob = await db const existingJob = await db
@@ -702,7 +765,7 @@ const app = new Elysia({
.get(jobId.value, user.id); .get(jobId.value, user.id);
if (!existingJob) { if (!existingJob) {
return redirect("/", 302); return redirect(`${WEBROOT}/`, 302);
} }
const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`; const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
@@ -713,7 +776,6 @@ const app = new Elysia({
await Bun.write(`${userUploadsDir}${file.name}`, file); await Bun.write(`${userUploadsDir}${file.name}`, file);
} }
} else { } else {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/dot-notation
await Bun.write(`${userUploadsDir}${body.file["name"]}`, body.file); await Bun.write(`${userUploadsDir}${body.file["name"]}`, body.file);
} }
} }
@@ -728,16 +790,16 @@ const app = new Elysia({
"/delete", "/delete",
async ({ body, redirect, jwt, cookie: { auth, jobId } }) => { async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
if (!auth?.value) { if (!auth?.value) {
return redirect("/login", 302); return redirect(`${WEBROOT}/login`, 302);
} }
const user = await jwt.verify(auth.value); const user = await jwt.verify(auth.value);
if (!user) { if (!user) {
return redirect("/login", 302); return redirect(`${WEBROOT}/login`, 302);
} }
if (!jobId?.value) { if (!jobId?.value) {
return redirect("/", 302); return redirect(`${WEBROOT}/`, 302);
} }
const existingJob = await db const existingJob = await db
@@ -745,7 +807,7 @@ const app = new Elysia({
.get(jobId.value, user.id); .get(jobId.value, user.id);
if (!existingJob) { if (!existingJob) {
return redirect("/", 302); return redirect(`${WEBROOT}/`, 302);
} }
const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`; const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
@@ -758,16 +820,16 @@ const app = new Elysia({
"/convert", "/convert",
async ({ body, redirect, jwt, cookie: { auth, jobId } }) => { async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
if (!auth?.value) { if (!auth?.value) {
return redirect("/login", 302); return redirect(`${WEBROOT}/login`, 302);
} }
const user = await jwt.verify(auth.value); const user = await jwt.verify(auth.value);
if (!user) { if (!user) {
return redirect("/login", 302); return redirect(`${WEBROOT}/login`, 302);
} }
if (!jobId?.value) { if (!jobId?.value) {
return redirect("/", 302); return redirect(`${WEBROOT}/`, 302);
} }
const existingJob = db const existingJob = db
@@ -776,7 +838,7 @@ const app = new Elysia({
.get(jobId.value, user.id); .get(jobId.value, user.id);
if (!existingJob) { if (!existingJob) {
return redirect("/", 302); return redirect(`${WEBROOT}/`, 302);
} }
const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`; const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
@@ -797,7 +859,7 @@ const app = new Elysia({
const fileNames = JSON.parse(body.file_names) as string[]; const fileNames = JSON.parse(body.file_names) as string[];
if (!Array.isArray(fileNames) || fileNames.length === 0) { if (!Array.isArray(fileNames) || fileNames.length === 0) {
return redirect("/", 302); return redirect(`${WEBROOT}/`, 302);
} }
db.query( db.query(
@@ -850,7 +912,7 @@ const app = new Elysia({
}); });
// Redirect the client immediately // Redirect the client immediately
return redirect(`/results/${jobId.value}`, 302); return redirect(`${WEBROOT}/results/${jobId.value}`, 302);
}, },
{ {
body: t.Object({ body: t.Object({
@@ -861,12 +923,12 @@ const app = new Elysia({
) )
.get("/history", async ({ jwt, redirect, cookie: { auth } }) => { .get("/history", async ({ jwt, redirect, cookie: { auth } }) => {
if (!auth?.value) { if (!auth?.value) {
return redirect("/login", 302); return redirect(`${WEBROOT}/login`, 302);
} }
const user = await jwt.verify(auth.value); const user = await jwt.verify(auth.value);
if (!user) { if (!user) {
return redirect("/login", 302); return redirect(`${WEBROOT}/login`, 302);
} }
let userJobs = db let userJobs = db
@@ -887,13 +949,19 @@ const app = new Elysia({
userJobs = userJobs.filter((job) => job.num_files > 0); userJobs = userJobs.filter((job) => job.num_files > 0);
return ( return (
<BaseHtml title="ConvertX | Results"> <BaseHtml webroot={WEBROOT} title="ConvertX | Results">
<> <>
<Header loggedIn /> <Header webroot={WEBROOT} loggedIn />
<main class="w-full px-4"> <main class="w-full px-4">
<article class="article"> <article class="article">
<h1 class="mb-4 text-xl">Results</h1> <h1 class="mb-4 text-xl">Results</h1>
<table class="w-full table-auto rounded bg-gray-900 text-left [&_td]:p-4 [&_tr]:rounded [&_tr]:border-b [&_tr]:border-gray-800"> <table
class={`
w-full table-auto rounded bg-neutral-900 text-left
[&_td]:p-4
[&_tr]:rounded [&_tr]:border-b [&_tr]:border-neutral-800
`}
>
<thead> <thead>
<tr> <tr>
<th class="px-4 py-2">Time</th> <th class="px-4 py-2">Time</th>
@@ -912,8 +980,11 @@ const app = new Elysia({
<td safe>{job.status}</td> <td safe>{job.status}</td>
<td> <td>
<a <a
class="text-lime-500 underline hover:text-lime-400" class={`
href={`/results/${job.id}`} text-accent-500 underline
hover:text-accent-400
`}
href={`${WEBROOT}/results/${job.id}`}
> >
View View
</a> </a>
@@ -932,7 +1003,7 @@ const app = new Elysia({
"/results/:jobId", "/results/:jobId",
async ({ params, jwt, set, redirect, cookie: { auth, job_id } }) => { async ({ params, jwt, set, redirect, cookie: { auth, job_id } }) => {
if (!auth?.value) { if (!auth?.value) {
return redirect("/login", 302); return redirect(`${WEBROOT}/login`, 302);
} }
if (job_id?.value) { if (job_id?.value) {
@@ -942,7 +1013,7 @@ const app = new Elysia({
const user = await jwt.verify(auth.value); const user = await jwt.verify(auth.value);
if (!user) { if (!user) {
return redirect("/login", 302); return redirect(`${WEBROOT}/login`, 302);
} }
const job = db const job = db
@@ -965,9 +1036,9 @@ const app = new Elysia({
.all(params.jobId); .all(params.jobId);
return ( return (
<BaseHtml title="ConvertX | Result"> <BaseHtml webroot={WEBROOT} title="ConvertX | Result">
<> <>
<Header loggedIn /> <Header webroot={WEBROOT} loggedIn />
<main class="w-full px-4"> <main class="w-full px-4">
<article class="article"> <article class="article">
<div class="mb-4 flex items-center justify-between"> <div class="mb-4 flex items-center justify-between">
@@ -990,9 +1061,22 @@ const app = new Elysia({
<progress <progress
max={job.num_files} max={job.num_files}
value={files.length} value={files.length}
class="mb-4 inline-block h-2 w-full appearance-none overflow-hidden rounded-full border-0 bg-gray-700 bg-none text-lime-500 accent-lime-500 [&::-moz-progress-bar]:bg-gray-700 [&::-webkit-progress-value]:rounded-full [&::-webkit-progress-value]:[background:none] [&[value]::-webkit-progress-value]:bg-lime-500 [&[value]::-webkit-progress-value]:transition-[inline-size]" class={`
mb-4 inline-block h-2 w-full appearance-none overflow-hidden rounded-full
border-0 bg-neutral-700 bg-none text-accent-500 accent-accent-500
[&::-moz-progress-bar]:bg-neutral-700 [&::-webkit-progress-value]:rounded-full
[&::-webkit-progress-value]:[background:none]
[&[value]::-webkit-progress-value]:bg-accent-500
[&[value]::-webkit-progress-value]:transition-[inline-size]
`}
/> />
<table class="w-full table-auto rounded bg-gray-900 text-left [&_td]:p-4 [&_tr]:rounded [&_tr]:border-b [&_tr]:border-gray-800"> <table
class={`
w-full table-auto rounded bg-neutral-900 text-left
[&_td]:p-4
[&_tr]:rounded [&_tr]:border-b [&_tr]:border-neutral-800
`}
>
<thead> <thead>
<tr> <tr>
<th class="px-4 py-2">Converted File Name</th> <th class="px-4 py-2">Converted File Name</th>
@@ -1008,16 +1092,22 @@ const app = new Elysia({
<td safe>{file.status}</td> <td safe>{file.status}</td>
<td> <td>
<a <a
class="text-lime-500 underline hover:text-lime-400" class={`
href={`/download/${outputPath}${file.output_file_name}`} text-accent-500 underline
hover:text-accent-400
`}
href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
> >
View View
</a> </a>
</td> </td>
<td> <td>
<a <a
class="text-lime-500 underline hover:text-lime-400" class={`
href={`/download/${outputPath}${file.output_file_name}`} text-accent-500 underline
hover:text-accent-400
`}
href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
download={file.output_file_name} download={file.output_file_name}
> >
Download Download
@@ -1029,7 +1119,7 @@ const app = new Elysia({
</table> </table>
</article> </article>
</main> </main>
<script src="/results.js" defer /> <script src={`${WEBROOT}/results.js`} defer />
</> </>
</BaseHtml> </BaseHtml>
); );
@@ -1039,7 +1129,7 @@ const app = new Elysia({
"/progress/:jobId", "/progress/:jobId",
async ({ jwt, set, params, redirect, cookie: { auth, job_id } }) => { async ({ jwt, set, params, redirect, cookie: { auth, job_id } }) => {
if (!auth?.value) { if (!auth?.value) {
return redirect("/login", 302); return redirect(`${WEBROOT}/login`, 302);
} }
if (job_id?.value) { if (job_id?.value) {
@@ -1049,7 +1139,7 @@ const app = new Elysia({
const user = await jwt.verify(auth.value); const user = await jwt.verify(auth.value);
if (!user) { if (!user) {
return redirect("/login", 302); return redirect(`${WEBROOT}/login`, 302);
} }
const job = db const job = db
@@ -1093,9 +1183,22 @@ const app = new Elysia({
<progress <progress
max={job.num_files} max={job.num_files}
value={files.length} value={files.length}
class="mb-4 inline-block h-2 w-full appearance-none overflow-hidden rounded-full border-0 bg-gray-700 bg-none text-lime-500 accent-lime-500 [&::-moz-progress-bar]:bg-gray-700 [&::-webkit-progress-value]:rounded-full [&::-webkit-progress-value]:[background:none] [&[value]::-webkit-progress-value]:bg-lime-500 [&[value]::-webkit-progress-value]:transition-[inline-size]" class={`
mb-4 inline-block h-2 w-full appearance-none overflow-hidden rounded-full border-0
bg-neutral-700 bg-none text-accent-500 accent-accent-500
[&::-moz-progress-bar]:bg-neutral-700 [&::-webkit-progress-value]:rounded-full
[&::-webkit-progress-value]:[background:none]
[&[value]::-webkit-progress-value]:bg-accent-500
[&[value]::-webkit-progress-value]:transition-[inline-size]
`}
/> />
<table class="w-full table-auto rounded bg-gray-900 text-left [&_td]:p-4 [&_tr]:rounded [&_tr]:border-b [&_tr]:border-gray-800"> <table
class={`
w-full table-auto rounded bg-neutral-900 text-left
[&_td]:p-4
[&_tr]:rounded [&_tr]:border-b [&_tr]:border-neutral-800
`}
>
<thead> <thead>
<tr> <tr>
<th class="px-4 py-2">Converted File Name</th> <th class="px-4 py-2">Converted File Name</th>
@@ -1111,16 +1214,22 @@ const app = new Elysia({
<td safe>{file.status}</td> <td safe>{file.status}</td>
<td> <td>
<a <a
class="text-lime-500 underline hover:text-lime-400" class={`
href={`/download/${outputPath}${file.output_file_name}`} text-accent-500 underline
hover:text-accent-400
`}
href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
> >
View View
</a> </a>
</td> </td>
<td> <td>
<a <a
class="text-lime-500 underline hover:text-lime-400" class={`
href={`/download/${outputPath}${file.output_file_name}`} text-accent-500 underline
hover:text-accent-400
`}
href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
download={file.output_file_name} download={file.output_file_name}
> >
Download Download
@@ -1138,12 +1247,12 @@ const app = new Elysia({
"/download/:userId/:jobId/:fileName", "/download/:userId/:jobId/:fileName",
async ({ params, jwt, redirect, cookie: { auth } }) => { async ({ params, jwt, redirect, cookie: { auth } }) => {
if (!auth?.value) { if (!auth?.value) {
return redirect("/login", 302); return redirect(`${WEBROOT}/login`, 302);
} }
const user = await jwt.verify(auth.value); const user = await jwt.verify(auth.value);
if (!user) { if (!user) {
return redirect("/login", 302); return redirect(`${WEBROOT}/login`, 302);
} }
const job = await db const job = await db
@@ -1151,7 +1260,7 @@ const app = new Elysia({
.get(user.id, params.jobId); .get(user.id, params.jobId);
if (!job) { if (!job) {
return redirect("/results", 302); return redirect(`${WEBROOT}/results`, 302);
} }
// parse from url encoded string // parse from url encoded string
const userId = decodeURIComponent(params.userId); const userId = decodeURIComponent(params.userId);
@@ -1164,22 +1273,29 @@ const app = new Elysia({
) )
.get("/converters", async ({ jwt, redirect, cookie: { auth } }) => { .get("/converters", async ({ jwt, redirect, cookie: { auth } }) => {
if (!auth?.value) { if (!auth?.value) {
return redirect("/login", 302); return redirect(`${WEBROOT}/login`, 302);
} }
const user = await jwt.verify(auth.value); const user = await jwt.verify(auth.value);
if (!user) { if (!user) {
return redirect("/login", 302); return redirect(`${WEBROOT}/login`, 302);
} }
return ( return (
<BaseHtml title="ConvertX | Converters"> <BaseHtml webroot={WEBROOT} title="ConvertX | Converters">
<> <>
<Header loggedIn /> <Header webroot={WEBROOT} loggedIn />
<main class="w-full px-4"> <main class="w-full px-4">
<article class="article"> <article class="article">
<h1 class="mb-4 text-xl">Converters</h1> <h1 class="mb-4 text-xl">Converters</h1>
<table class="w-full table-auto rounded bg-gray-900 text-left [&_td]:p-4 [&_tr]:rounded [&_tr]:border-b [&_tr]:border-gray-800 [&_ul]:list-inside [&_ul]:list-disc"> <table
class={`
w-full table-auto rounded bg-neutral-900 text-left
[&_td]:p-4
[&_tr]:rounded [&_tr]:border-b [&_tr]:border-neutral-800
[&_ul]:list-inside [&_ul]:list-disc
`}
>
<thead> <thead>
<tr> <tr>
<th class="mx-4 my-2">Converter</th> <th class="mx-4 my-2">Converter</th>
@@ -1227,12 +1343,12 @@ const app = new Elysia({
async ({ params, jwt, redirect, cookie: { auth } }) => { async ({ params, jwt, redirect, cookie: { auth } }) => {
// TODO: Implement zip download // TODO: Implement zip download
if (!auth?.value) { if (!auth?.value) {
return redirect("/login", 302); return redirect(`${WEBROOT}/login`, 302);
} }
const user = await jwt.verify(auth.value); const user = await jwt.verify(auth.value);
if (!user) { if (!user) {
return redirect("/login", 302); return redirect(`${WEBROOT}/login`, 302);
} }
const job = await db const job = await db
@@ -1240,12 +1356,12 @@ const app = new Elysia({
.get(user.id, params.jobId); .get(user.id, params.jobId);
if (!job) { if (!job) {
return redirect("/results", 302); return redirect(`${WEBROOT}/results`, 302);
} }
// const userId = decodeURIComponent(params.userId); // const userId = decodeURIComponent(params.userId);
// const jobId = decodeURIComponent(params.jobId); // const jobId = decodeURIComponent(params.jobId);
// const outputPath = `${outputDir}${userId}/${jobId}/`; // const outputPath = `${outputDir}${userId}/`{jobId}/);
// return Bun.zip(outputPath); // return Bun.zip(outputPath);
}, },
@@ -1269,7 +1385,7 @@ if (process.env.NODE_ENV !== "production") {
app.listen(3000); app.listen(3000);
console.log( console.log(
`🦊 Elysia is running at http://${app.server?.hostname}:${app.server?.port}`, `🦊 Elysia is running at http://${app.server?.hostname}:${app.server?.port}${WEBROOT}`,
); );
const clearJobs = () => { const clearJobs = () => {

View File

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

View File

@@ -1,9 +1,27 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
// eslint-disable-next-line no-undef
module.exports = { import tailwindScrollbar from "tailwind-scrollbar";
content: ['./src/**/*.{html,js,tsx,jsx,cjs,mjs}'],
theme: { export default {
extend: {}, content: ["./src/**/*.{html,js,tsx,jsx,cjs,mjs}"],
}, theme: {
plugins: [require('tailwind-scrollbar')], 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],
};

View File

@@ -27,4 +27,4 @@
"noImplicitOverride": true "noImplicitOverride": true
// "noImplicitReturns": true // "noImplicitReturns": true
} }
} }