Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
961a55cbe5 | ||
|
|
cdf9bad903 | ||
|
|
6769fa2f83 | ||
|
|
3b7ea88b73 | ||
|
|
59310c095d | ||
|
|
c47f0c12fe | ||
|
|
ca71a40485 | ||
|
|
d4e8f376c1 | ||
|
|
14c6ea1e6b | ||
|
|
d6e4d8fbd6 | ||
|
|
460bda62d5 | ||
|
|
d2702ab673 | ||
|
|
e2581f42f5 | ||
|
|
f0fcfc159f | ||
|
|
538c5b60c9 | ||
|
|
2fabb7bbb2 | ||
|
|
e7c34a9c94 | ||
|
|
618f9fce5a | ||
|
|
95dbc9f678 | ||
|
|
aa87bc5c51 | ||
|
|
815de531ed | ||
|
|
cf2b026dc4 | ||
|
|
9ce46aefba | ||
|
|
98b2db7818 | ||
|
|
0229851bf9 | ||
|
|
9e15114fe8 | ||
|
|
7f66a76bb0 | ||
|
|
e9cc8392bb | ||
|
|
d0b89ce74f | ||
|
|
f537c81db7 | ||
|
|
03d3edfff6 | ||
|
|
447b4c5e5c | ||
|
|
cb143209ae | ||
|
|
9c24fe73b5 | ||
|
|
19ae85424b | ||
|
|
22fad99552 | ||
|
|
8144bbef74 | ||
|
|
aad6da0ae8 | ||
|
|
c5f8162a22 | ||
|
|
f0f30224b5 | ||
|
|
d0d888e356 | ||
|
|
2c64122224 | ||
|
|
3b2eee96a9 | ||
|
|
465aacbf9b | ||
|
|
d1a2a66170 | ||
|
|
4c05fd72bb | ||
|
|
f04fe760e3 | ||
|
|
834d19bcc6 | ||
|
|
6808c4642c | ||
|
|
d0ce307f94 | ||
|
|
f3740e9ded | ||
|
|
b485bc9445 | ||
|
|
2d14c1bb26 | ||
|
|
1a442d6e69 | ||
|
|
2386543e5c | ||
|
|
58e220e82d | ||
|
|
24bea6e4d2 | ||
|
|
43497ad8d1 | ||
|
|
f22b61fe4c | ||
|
|
5b08f4cd19 | ||
|
|
1589f8d24e | ||
|
|
7d1db72cf5 | ||
|
|
53a8f66414 | ||
|
|
36cb6cc589 | ||
|
|
f3a4aece46 | ||
|
|
580a6a869a | ||
|
|
008eaac493 | ||
|
|
b450623bb4 | ||
|
|
8ac2ecb673 | ||
|
|
0a10a56ae3 | ||
|
|
9378ba9208 | ||
|
|
0c586e324b | ||
|
|
91c4a64284 | ||
|
|
c599e98d9d | ||
|
|
d2cd6706c9 | ||
|
|
e8ed10dde8 | ||
|
|
5fe0b79802 | ||
|
|
34a6722a68 | ||
|
|
5b0d769c63 | ||
|
|
718401a28b | ||
|
|
3112cd57f6 | ||
|
|
410fc777a7 | ||
|
|
8eed99e732 |
@@ -1,16 +1,20 @@
|
|||||||
node_modules
|
|
||||||
Dockerfile*
|
|
||||||
docker-compose*
|
|
||||||
.dockerignore
|
.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
|
||||||
13
.github/workflows/docker-publish.yml
vendored
@@ -19,6 +19,7 @@ env:
|
|||||||
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:
|
jobs:
|
||||||
@@ -46,13 +47,22 @@ jobs:
|
|||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ env.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
# Extract metadata (tags, labels) for Docker
|
# Extract metadata (tags, labels) for Docker
|
||||||
# https://github.com/docker/metadata-action
|
# https://github.com/docker/metadata-action
|
||||||
- name: Extract Docker metadata
|
- name: Extract Docker metadata
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
images: |
|
||||||
|
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
${{ env.IMAGE_NAME }}
|
||||||
|
|
||||||
# Build and push Docker image with Buildx (don't push on PR)
|
# Build and push Docker image with Buildx (don't push on PR)
|
||||||
# https://github.com/docker/build-push-action
|
# https://github.com/docker/build-push-action
|
||||||
@@ -67,3 +77,4 @@ jobs:
|
|||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
27
.github/workflows/dockerhub-description.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
name: Update Docker Hub Description
|
||||||
|
|
||||||
|
env:
|
||||||
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
DOCKERHUB_USERNAME: c4illin
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- README.md
|
||||||
|
- .github/workflows/dockerhub-description.yml
|
||||||
|
jobs:
|
||||||
|
dockerHubDescription:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Docker Hub Description
|
||||||
|
uses: peter-evans/dockerhub-description@v4
|
||||||
|
with:
|
||||||
|
username: ${{ env.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
repository: ${{ env.IMAGE_NAME }}
|
||||||
|
short-description: ${{ github.event.repository.description }}
|
||||||
|
enable-url-completion: true
|
||||||
2
.gitignore
vendored
@@ -48,4 +48,4 @@ package-lock.json
|
|||||||
/data
|
/data
|
||||||
/Bruno
|
/Bruno
|
||||||
/tsconfig.tsbuildinfo
|
/tsconfig.tsbuildinfo
|
||||||
/src/public/generated.css
|
/public/generated.css
|
||||||
|
|||||||
36
CHANGELOG.md
@@ -1,5 +1,41 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [0.10.1](https://github.com/C4illin/ConvertX/compare/v0.10.0...v0.10.1) (2025-01-21)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* ffmpeg works without ffmpeg_args ([3b7ea88](https://github.com/C4illin/ConvertX/commit/3b7ea88b7382f7c21b120bdc9bda5bb10547f55d)), closes [#212](https://github.com/C4illin/ConvertX/issues/212)
|
||||||
|
|
||||||
|
## [0.10.0](https://github.com/C4illin/ConvertX/compare/v0.9.0...v0.10.0) (2025-01-18)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add calibre ([03d3edf](https://github.com/C4illin/ConvertX/commit/03d3edfff65c252dd4b8922fc98257c089c1ff74)), closes [#191](https://github.com/C4illin/ConvertX/issues/191)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add FFMPEG_ARGS env variable ([f537c81](https://github.com/C4illin/ConvertX/commit/f537c81db7815df8017f834e3162291197e1c40f)), closes [#190](https://github.com/C4illin/ConvertX/issues/190)
|
||||||
|
* add qt6-qtbase-private-dev from community repo ([95dbc9f](https://github.com/C4illin/ConvertX/commit/95dbc9f678bec7e6e2c03587e1473fb8ff708ea3))
|
||||||
|
* skip account setup when ALLOW_UNAUTHENTICATED is true ([538c5b6](https://github.com/C4illin/ConvertX/commit/538c5b60c9e27a8184740305475245da79bae143))
|
||||||
|
|
||||||
|
## [0.9.0](https://github.com/C4illin/ConvertX/compare/v0.8.1...v0.9.0) (2024-11-21)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add inkscape for vector images ([f3740e9](https://github.com/C4illin/ConvertX/commit/f3740e9ded100b8500f3613517960248bbd3c210))
|
||||||
|
* Allow to chose webroot ([36cb6cc](https://github.com/C4illin/ConvertX/commit/36cb6cc589d80d0a87fa8dbe605db71a9a2570f9)), closes [#180](https://github.com/C4illin/ConvertX/issues/180)
|
||||||
|
* disable convert when uploading ([58e220e](https://github.com/C4illin/ConvertX/commit/58e220e82d7f9c163d6ea4dc31092c08a3e254f4)), closes [#177](https://github.com/C4illin/ConvertX/issues/177)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* treat unknown as m4a ([1a442d6](https://github.com/C4illin/ConvertX/commit/1a442d6e69606afef63b1e7df36aa83d111fa23d)), closes [#178](https://github.com/C4illin/ConvertX/issues/178)
|
||||||
|
* wait for both upload and selection ([4c05fd7](https://github.com/C4illin/ConvertX/commit/4c05fd72bbbf91ee02327f6fcbf749b78272376b)), closes [#177](https://github.com/C4illin/ConvertX/issues/177)
|
||||||
|
|
||||||
## [0.8.1](https://github.com/C4illin/ConvertX/compare/v0.8.0...v0.8.1) (2024-10-05)
|
## [0.8.1](https://github.com/C4illin/ConvertX/compare/v0.8.0...v0.8.1) (2024-10-05)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
12
Dockerfile
@@ -1,4 +1,4 @@
|
|||||||
FROM oven/bun:1.1.29-alpine AS base
|
FROM oven/bun:1.1.45-alpine AS base
|
||||||
LABEL org.opencontainers.image.source="https://github.com/C4illin/ConvertX"
|
LABEL org.opencontainers.image.source="https://github.com/C4illin/ConvertX"
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -50,14 +50,20 @@ RUN apk --no-cache add \
|
|||||||
vips-poppler \
|
vips-poppler \
|
||||||
vips-jxl \
|
vips-jxl \
|
||||||
libjxl-tools \
|
libjxl-tools \
|
||||||
assimp
|
assimp \
|
||||||
|
inkscape \
|
||||||
|
poppler-utils
|
||||||
|
|
||||||
|
RUN apk --no-cache add qt6-qtbase-private-dev --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community/
|
||||||
|
|
||||||
|
RUN apk --no-cache add calibre --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing/
|
||||||
|
|
||||||
# this might be needed for some latex use cases, will add it if needed.
|
# 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 . .
|
||||||
|
|||||||
63
README.md
@@ -1,11 +1,15 @@
|
|||||||

|

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

|

|
||||||

|

|
||||||

|

|
||||||

|
<!--  -->
|
||||||
|
|
||||||
A self-hosted online file converter. Supports over a thousand different formats. Written with TypeScript, Bun and Elysia.
|
A self-hosted online file converter. Supports over a thousand different formats. Written with TypeScript, Bun and Elysia.
|
||||||
|
|
||||||
@@ -23,11 +27,13 @@ A self-hosted online file converter. Supports over a thousand different formats.
|
|||||||
| [libjxl](https://github.com/libjxl/libjxl) | JPEG XL | 11 | 11 |
|
| [libjxl](https://github.com/libjxl/libjxl) | JPEG XL | 11 | 11 |
|
||||||
| [resvg](https://github.com/RazrFalcon/resvg) | SVG | 1 | 1 |
|
| [resvg](https://github.com/RazrFalcon/resvg) | SVG | 1 | 1 |
|
||||||
| [Vips](https://github.com/libvips/libvips) | Images | 45 | 23 |
|
| [Vips](https://github.com/libvips/libvips) | Images | 45 | 23 |
|
||||||
| [Assimp](https://github.com/assimp/assimp) | 3D Assets | 70 | 24 |
|
|
||||||
| [XeLaTeX](https://tug.org/xetex/) | LaTeX | 1 | 1 |
|
| [XeLaTeX](https://tug.org/xetex/) | LaTeX | 1 | 1 |
|
||||||
|
| [Calibre](https://calibre-ebook.com/) | E-books | 26 | 19 |
|
||||||
| [Pandoc](https://pandoc.org/) | Documents | 43 | 65 |
|
| [Pandoc](https://pandoc.org/) | Documents | 43 | 65 |
|
||||||
| [GraphicsMagick](http://www.graphicsmagick.org/) | Images | 166 | 133 |
|
| [GraphicsMagick](http://www.graphicsmagick.org/) | Images | 167 | 130 |
|
||||||
| [FFmpeg](https://ffmpeg.org/) | Video | ~473 | ~280 |
|
| [Inkscape](https://inkscape.org/) | Vector images | 7 | 17 |
|
||||||
|
| [Assimp](https://github.com/assimp/assimp) | 3D Assets | 77 | 23 |
|
||||||
|
| [FFmpeg](https://ffmpeg.org/) | Video | ~472 | ~199 |
|
||||||
|
|
||||||
<!-- many ffmpeg fileformats are duplicates -->
|
<!-- many ffmpeg fileformats are duplicates -->
|
||||||
|
|
||||||
@@ -44,14 +50,10 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
environment: # Defaults are listed below. All are optional.
|
environment:
|
||||||
- ACCOUNT_REGISTRATION=false # true or false, doesn't matter for the first account (e.g. keep this to false if you only want one account)
|
- JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234 # will use randomUUID() if unset
|
||||||
- JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234 # will use randomUUID() by default
|
|
||||||
- HTTP_ALLOWED=false # setting this to true is unsafe, only set this to true locally
|
|
||||||
- ALLOW_UNAUTHENTICATED=false # allows anyone to use the service without logging in, only set this to true locally
|
|
||||||
- AUTO_DELETE_EVERY_N_HOURS=24 # checks every n hours for files older then n hours and deletes them, set to 0 to disable
|
|
||||||
volumes:
|
volumes:
|
||||||
- convertx:/app/data
|
- ./data:/app/data
|
||||||
```
|
```
|
||||||
|
|
||||||
or
|
or
|
||||||
@@ -64,9 +66,43 @@ Then visit `http://localhost:3000` in your browser and create your account. Don'
|
|||||||
|
|
||||||
If you get unable to open database file run `chown -R $USER:$USER path` on the path you choose.
|
If you get unable to open database file run `chown -R $USER:$USER path` on the path you choose.
|
||||||
|
|
||||||
|
### Environment variables
|
||||||
|
|
||||||
|
All are optional, JWT_SECRET is recommended to be set.
|
||||||
|
|
||||||
|
| Name | Default | Description |
|
||||||
|
|---------------------------|---------|-------------|
|
||||||
|
| JWT_SECRET | when unset it will use the value from randomUUID() | A long and secret string used to sign the JSON Web Token |
|
||||||
|
| ACCOUNT_REGISTRATION | false | Allow users to register accounts |
|
||||||
|
| HTTP_ALLOWED | false | Allow HTTP connections, only set this to true locally |
|
||||||
|
| ALLOW_UNAUTHENTICATED | false | Allow unauthenticated users to use the service, only set this to true locally |
|
||||||
|
| AUTO_DELETE_EVERY_N_HOURS | 24 | Checks every n hours for files older then n hours and deletes them, set to 0 to disable |
|
||||||
|
| WEBROOT | | The address to the root path setting this to "/convert" will serve the website on "example.com/convert/" |
|
||||||
|
| FFMPEG_ARGS | | Arguments to pass to ffmpeg, e.g. `-preset veryfast` |
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> If you can't login, make sure you are accessing the service over https or set HTTP_ALLOWED=true
|
||||||
|
|
||||||
|
### Docker images
|
||||||
|
|
||||||
|
There is a `:latest` tag that is updated with every release and a `:main` tag that is updated with every push to the main branch. `:latest` is recommended for normal use.
|
||||||
|
|
||||||
|
The image is available on [GitHub Container Registry](https://github.com/C4illin/ConvertX/pkgs/container/ConvertX) and [Docker Hub](https://hub.docker.com/r/c4illin/convertx).
|
||||||
|
|
||||||
|
| Image | What it is |
|
||||||
|
|-------|------------|
|
||||||
|
| `image: ghcr.io/c4illin/convertx` | The latest release on ghcr |
|
||||||
|
| `image: ghcr.io/c4illin/convertx:main` | The latest commit on ghcr |
|
||||||
|
| `image: c4illin/convertx` | The latest release on docker hub |
|
||||||
|
| `image: c4illin/convertx:main` | The latest commit on docker hub |
|
||||||
|
|
||||||
|
<!-- Dockerhub was introduced in 0.9.0 and older releases -->
|
||||||
|
|
||||||
### Tutorial
|
### Tutorial
|
||||||
|
|
||||||
Tutorial in french: https://belginux.com/installer-convertx-avec-docker/
|
Tutorial in french: <https://belginux.com/installer-convertx-avec-docker/>
|
||||||
|
|
||||||
|
Tutorial in chinese: <https://xzllll.com/24092901/>
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
@@ -82,6 +118,7 @@ Tutorial in french: https://belginux.com/installer-convertx-avec-docker/
|
|||||||
Pull requests are welcome! See below and open issues for the list of todos.
|
Pull requests are welcome! See below and open issues for the list of todos.
|
||||||
|
|
||||||
## Todo
|
## Todo
|
||||||
|
|
||||||
- [x] Add messages for errors in converters
|
- [x] Add messages for errors in converters
|
||||||
- [x] Add searchable list of formats
|
- [x] Add searchable list of formats
|
||||||
- [ ] Add options for converters
|
- [ ] Add options for converters
|
||||||
@@ -97,7 +134,7 @@ Pull requests are welcome! See below and open issues for the list of todos.
|
|||||||
## Contributors
|
## Contributors
|
||||||
|
|
||||||
<a href="https://github.com/C4illin/ConvertX/graphs/contributors">
|
<a href="https://github.com/C4illin/ConvertX/graphs/contributors">
|
||||||
<img src="https://contrib.rocks/image?repo=C4illin/ConvertX" />
|
<img src="https://contrib.rocks/image?repo=C4illin/ConvertX" alt="Image with all contributors"/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
## Star History
|
## Star History
|
||||||
|
|||||||
@@ -11,5 +11,7 @@ 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
|
||||||
|
# - FFMPEG_ARGS=-hwaccel vulkan # additional arguments to pass to ffmpeg
|
||||||
|
# - WEBROOT=/convertx # the root path of the web interface, leave empty to disable
|
||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 3000:3000
|
||||||
|
|||||||
54
package.json
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "convertx-frontend",
|
"name": "convertx-frontend",
|
||||||
"version": "0.8.1",
|
"version": "0.10.1",
|
||||||
"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": "eslint --fix .",
|
"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",
|
||||||
@@ -13,10 +13,11 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@elysiajs/cookie": "^0.8.0",
|
"@elysiajs/cookie": "^0.8.0",
|
||||||
"@elysiajs/html": "1.0.2",
|
"@elysiajs/html": "^1.2.0",
|
||||||
"@elysiajs/jwt": "^1.1.1",
|
"@elysiajs/jwt": "^1.2.0",
|
||||||
"@elysiajs/static": "1.0.3",
|
"@elysiajs/static": "^1.2.0",
|
||||||
"elysia": "^1.1.17"
|
"@kitajs/html": "^4.2.7",
|
||||||
|
"elysia": "^1.2.10"
|
||||||
},
|
},
|
||||||
"module": "src/index.tsx",
|
"module": "src/index.tsx",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -24,38 +25,31 @@
|
|||||||
"start": "bun run src/index.tsx"
|
"start": "bun run src/index.tsx"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/compat": "^1.1.1",
|
"@eslint/compat": "^1.2.5",
|
||||||
"@eslint/js": "^9.12.0",
|
"@eslint/js": "^9.18.0",
|
||||||
"@ianvs/prettier-plugin-sort-imports": "^4.3.1",
|
"@ianvs/prettier-plugin-sort-imports": "^4.4.1",
|
||||||
"@kitajs/ts-html-plugin": "^4.1.0",
|
"@kitajs/ts-html-plugin": "^4.1.1",
|
||||||
"@total-typescript/ts-reset": "^0.6.1",
|
"@total-typescript/ts-reset": "^0.6.1",
|
||||||
"@types/bun": "^1.1.10",
|
"@types/bun": "^1.1.16",
|
||||||
"@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.7.4",
|
"@types/node": "^22.10.7",
|
||||||
"@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.12.0",
|
"eslint": "^9.18.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
|
||||||
"eslint-plugin-deprecation": "^3.0.0",
|
"eslint-plugin-deprecation": "^3.0.0",
|
||||||
"eslint-plugin-isaacscript": "^4.0.0",
|
"eslint-plugin-readable-tailwind": "^1.8.2",
|
||||||
"eslint-plugin-prettier": "^5.2.1",
|
|
||||||
"eslint-plugin-readable-tailwind": "^1.8.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.14.0",
|
||||||
"knip": "^5.30.6",
|
"knip": "^5.42.1",
|
||||||
"npm-run-all2": "^6.2.3",
|
"npm-run-all2": "^7.0.2",
|
||||||
"postcss": "^8.4.47",
|
"postcss": "^8.5.1",
|
||||||
"postcss-cli": "^11.0.0",
|
"postcss-cli": "^11.0.0",
|
||||||
"postcss-lightningcss": "^1.0.1",
|
"prettier": "^3.4.2",
|
||||||
"prettier": "^3.3.3",
|
|
||||||
"tailwind-scrollbar": "^3.1.0",
|
"tailwind-scrollbar": "^3.1.0",
|
||||||
"tailwindcss": "^3.4.13",
|
"tailwindcss": "^3.4.17",
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.7.3",
|
||||||
"typescript-eslint": "^8.8.0"
|
"typescript-eslint": "^8.20.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
|
|
||||||
module.exports = {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
|
|
||||||
...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
8
postcss.config.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import autoprefixer from "autoprefixer";
|
||||||
|
import cssnano from "cssnano";
|
||||||
|
import tailwind from "tailwindcss";
|
||||||
|
import tailwindConfig from "./tailwind.config.js";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
plugins: [autoprefixer, tailwind(tailwindConfig), cssnano],
|
||||||
|
};
|
||||||
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 476 B After Width: | Height: | Size: 476 B |
|
Before Width: | Height: | Size: 960 B After Width: | Height: | Size: 960 B |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -1,3 +1,5 @@
|
|||||||
|
const webroot = document.querySelector("meta[name='webroot']").content;
|
||||||
|
|
||||||
window.downloadAll = function () {
|
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())
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
// Select the file input element
|
const webroot = document.querySelector("meta[name='webroot']").content;
|
||||||
const fileInput = document.querySelector('input[type="file"]');
|
const fileInput = document.querySelector('input[type="file"]');
|
||||||
const dropZone = document.getElementById("dropzone");
|
const dropZone = document.getElementById("dropzone");
|
||||||
|
const convertButton = document.querySelector("input[type='submit']");
|
||||||
const fileNames = [];
|
const fileNames = [];
|
||||||
let fileType;
|
let fileType;
|
||||||
|
let pendingFiles = 0;
|
||||||
|
let formatSelected = false;
|
||||||
|
|
||||||
dropZone.addEventListener("dragover", () => {
|
dropZone.addEventListener("dragover", () => {
|
||||||
dropZone.classList.add("dragover");
|
dropZone.classList.add("dragover");
|
||||||
@@ -12,6 +15,10 @@ dropZone.addEventListener("dragleave", () => {
|
|||||||
dropZone.classList.remove("dragover");
|
dropZone.classList.remove("dragover");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener("drop", () => {
|
||||||
|
dropZone.classList.remove("dragover");
|
||||||
|
});
|
||||||
|
|
||||||
const selectContainer = document.querySelector("form .select_container");
|
const selectContainer = document.querySelector("form .select_container");
|
||||||
|
|
||||||
const updateSearchBar = () => {
|
const updateSearchBar = () => {
|
||||||
@@ -22,7 +29,6 @@ const updateSearchBar = () => {
|
|||||||
const convertToGroupElements = document.querySelectorAll(".convert_to_group");
|
const convertToGroupElements = document.querySelectorAll(".convert_to_group");
|
||||||
const convertToGroups = {};
|
const convertToGroups = {};
|
||||||
const convertToElement = document.querySelector("select[name='convert_to']");
|
const convertToElement = document.querySelector("select[name='convert_to']");
|
||||||
const convertButton = document.querySelector("input[type='submit']");
|
|
||||||
|
|
||||||
const showMatching = (search) => {
|
const showMatching = (search) => {
|
||||||
for (const [targets, groupElement] of Object.values(convertToGroups)) {
|
for (const [targets, groupElement] of Object.values(convertToGroups)) {
|
||||||
@@ -58,7 +64,10 @@ const updateSearchBar = () => {
|
|||||||
target.onmousedown = () => {
|
target.onmousedown = () => {
|
||||||
convertToElement.value = target.dataset.value;
|
convertToElement.value = target.dataset.value;
|
||||||
convertToInput.value = `${target.dataset.target} using ${target.dataset.converter}`;
|
convertToInput.value = `${target.dataset.target} using ${target.dataset.converter}`;
|
||||||
convertButton.disabled = false;
|
formatSelected = true;
|
||||||
|
if (pendingFiles === 0 && fileNames.length > 0) {
|
||||||
|
convertButton.disabled = false;
|
||||||
|
}
|
||||||
showMatching("");
|
showMatching("");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -73,6 +82,7 @@ const updateSearchBar = () => {
|
|||||||
convertToInput.addEventListener("search", () => {
|
convertToInput.addEventListener("search", () => {
|
||||||
// when the user clears the search bar using the 'x' button
|
// when the user clears the search bar using the 'x' button
|
||||||
convertButton.disabled = true;
|
convertButton.disabled = true;
|
||||||
|
formatSelected = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
convertToInput.addEventListener("blur", (e) => {
|
convertToInput.addEventListener("blur", (e) => {
|
||||||
@@ -94,11 +104,8 @@ const updateSearchBar = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// const convertFromSelect = document.querySelector("select[name='convert_from']");
|
|
||||||
|
|
||||||
// Add a 'change' event listener to the file input element
|
// Add a 'change' event listener to the file input element
|
||||||
fileInput.addEventListener("change", (e) => {
|
fileInput.addEventListener("change", (e) => {
|
||||||
// console.log(e.target.files);
|
|
||||||
// Get the selected files from the event target
|
// Get the selected files from the event target
|
||||||
const files = e.target.files;
|
const files = e.target.files;
|
||||||
|
|
||||||
@@ -112,12 +119,11 @@ fileInput.addEventListener("change", (e) => {
|
|||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<td>${file.name}</td>
|
<td>${file.name}</td>
|
||||||
<td>${(file.size / 1024).toFixed(2)} kB</td>
|
<td>${(file.size / 1024).toFixed(2)} kB</td>
|
||||||
<td><a class="secondary" onclick="deleteRow(this)" style="cursor: pointer">Remove</a></td>
|
<td><a onclick="deleteRow(this)">Remove</a></td>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
if (!fileType) {
|
if (!fileType) {
|
||||||
fileType = file.name.split(".").pop();
|
fileType = file.name.split(".").pop();
|
||||||
console.log("fileType", fileType);
|
|
||||||
fileInput.setAttribute("accept", `.${fileType}`);
|
fileInput.setAttribute("accept", `.${fileType}`);
|
||||||
setTitle();
|
setTitle();
|
||||||
|
|
||||||
@@ -129,7 +135,7 @@ fileInput.addEventListener("change", (e) => {
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
fetch("/conversions", {
|
fetch(`${webroot}/conversions`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ fileType: fileType }),
|
body: JSON.stringify({ fileType: fileType }),
|
||||||
headers: {
|
headers: {
|
||||||
@@ -170,46 +176,57 @@ const deleteRow = (target) => {
|
|||||||
const index = fileNames.indexOf(filename);
|
const index = fileNames.indexOf(filename);
|
||||||
fileNames.splice(index, 1);
|
fileNames.splice(index, 1);
|
||||||
|
|
||||||
|
// reset fileInput
|
||||||
|
fileInput.value = "";
|
||||||
|
|
||||||
// if fileNames is empty, reset fileType
|
// if fileNames is empty, reset fileType
|
||||||
if (fileNames.length === 0) {
|
if (fileNames.length === 0) {
|
||||||
fileType = null;
|
fileType = null;
|
||||||
fileInput.removeAttribute("accept");
|
fileInput.removeAttribute("accept");
|
||||||
|
convertButton.disabled = true;
|
||||||
setTitle();
|
setTitle();
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch("/delete", {
|
fetch(`${webroot}/delete`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ filename: filename }),
|
body: JSON.stringify({ filename: filename }),
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((res) => res.json())
|
|
||||||
.then((data) => {
|
|
||||||
console.log(data);
|
|
||||||
})
|
|
||||||
.catch((err) => console.log(err));
|
.catch((err) => console.log(err));
|
||||||
};
|
};
|
||||||
|
|
||||||
const uploadFiles = (files) => {
|
const uploadFiles = (files) => {
|
||||||
|
convertButton.disabled = true;
|
||||||
|
convertButton.textContent = "Uploading...";
|
||||||
|
pendingFiles += 1;
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
formData.append("file", file, file.name);
|
formData.append("file", file, file.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch("/upload", {
|
fetch(`${webroot}/upload`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: formData,
|
body: formData,
|
||||||
})
|
})
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
|
pendingFiles -= 1;
|
||||||
|
if (pendingFiles === 0) {
|
||||||
|
if (formatSelected) {
|
||||||
|
convertButton.disabled = false;
|
||||||
|
}
|
||||||
|
convertButton.textContent = "Convert";
|
||||||
|
}
|
||||||
console.log(data);
|
console.log(data);
|
||||||
})
|
})
|
||||||
.catch((err) => console.log(err));
|
.catch((err) => console.log(err));
|
||||||
};
|
};
|
||||||
|
|
||||||
const formConvert = document.querySelector("form[action='/convert']");
|
const formConvert = document.querySelector(`form[action='${webroot}/convert']`);
|
||||||
|
|
||||||
formConvert.addEventListener("submit", () => {
|
formConvert.addEventListener("submit", () => {
|
||||||
const hiddenInput = document.querySelector("input[name='file_names']");
|
const hiddenInput = document.querySelector("input[name='file_names']");
|
||||||
@@ -3,5 +3,9 @@
|
|||||||
"extends": [
|
"extends": [
|
||||||
"config:recommended",
|
"config:recommended",
|
||||||
":disableDependencyDashboard"
|
":disableDependencyDashboard"
|
||||||
]
|
],
|
||||||
|
"lockFileMaintenance": {
|
||||||
|
"enabled": true,
|
||||||
|
"automerge": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,31 +1,39 @@
|
|||||||
|
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-neutral-900 text-neutral-200">{children}</body>
|
<body class="w-full bg-neutral-900 text-neutral-200">{children}</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
|
import { Html } from "@kitajs/html";
|
||||||
|
|
||||||
export const Header = ({
|
export const Header = ({
|
||||||
loggedIn,
|
loggedIn,
|
||||||
accountRegistration,
|
accountRegistration,
|
||||||
}: { loggedIn?: boolean; accountRegistration?: boolean }) => {
|
allowUnauthenticated,
|
||||||
|
webroot = "",
|
||||||
|
}: {
|
||||||
|
loggedIn?: boolean;
|
||||||
|
accountRegistration?: boolean;
|
||||||
|
allowUnauthenticated?: boolean;
|
||||||
|
webroot?: string;
|
||||||
|
}) => {
|
||||||
let rightNav: JSX.Element;
|
let rightNav: JSX.Element;
|
||||||
if (loggedIn) {
|
if (loggedIn) {
|
||||||
rightNav = (
|
rightNav = (
|
||||||
@@ -12,20 +21,24 @@ export const Header = ({
|
|||||||
text-accent-600 transition-all
|
text-accent-600 transition-all
|
||||||
hover:text-accent-500 hover:underline
|
hover:text-accent-500 hover:underline
|
||||||
`}
|
`}
|
||||||
href="/history">
|
href={`${webroot}/history`}
|
||||||
|
>
|
||||||
History
|
History
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
{!allowUnauthenticated ? (
|
||||||
<a
|
<li>
|
||||||
class={`
|
<a
|
||||||
text-accent-600 transition-all
|
class={`
|
||||||
hover:text-accent-500 hover:underline
|
text-accent-600 transition-all
|
||||||
`}
|
hover:text-accent-500 hover:underline
|
||||||
href="/logoff">
|
`}
|
||||||
Logout
|
href={`${webroot}/logoff`}
|
||||||
</a>
|
>
|
||||||
</li>
|
Logout
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
) : null}
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -37,7 +50,8 @@ export const Header = ({
|
|||||||
text-accent-600 transition-all
|
text-accent-600 transition-all
|
||||||
hover:text-accent-500 hover:underline
|
hover:text-accent-500 hover:underline
|
||||||
`}
|
`}
|
||||||
href="/login">
|
href={`${webroot}/login`}
|
||||||
|
>
|
||||||
Login
|
Login
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -48,7 +62,8 @@ export const Header = ({
|
|||||||
text-accent-600 transition-all
|
text-accent-600 transition-all
|
||||||
hover:text-accent-500 hover:underline
|
hover:text-accent-500 hover:underline
|
||||||
`}
|
`}
|
||||||
href="/register">
|
href={`${webroot}/register`}
|
||||||
|
>
|
||||||
Register
|
Register
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -63,7 +78,7 @@ export const Header = ({
|
|||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<strong>
|
<strong>
|
||||||
<a href="/">ConvertX</a>
|
<a href={`${webroot}/`}>ConvertX</a>
|
||||||
</strong>
|
</strong>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { exec } from "node:child_process";
|
import { exec } from "node:child_process";
|
||||||
|
|
||||||
// This could be done dynamically by running `ffmpeg -formats` and parsing the output
|
|
||||||
export const properties = {
|
export const properties = {
|
||||||
from: {
|
from: {
|
||||||
muxer: [
|
object: [
|
||||||
"3d",
|
"3d",
|
||||||
"3ds",
|
"3ds",
|
||||||
"3mf",
|
"3mf",
|
||||||
@@ -11,6 +10,7 @@ export const properties = {
|
|||||||
"ac3d",
|
"ac3d",
|
||||||
"acc",
|
"acc",
|
||||||
"amf",
|
"amf",
|
||||||
|
"amj",
|
||||||
"ase",
|
"ase",
|
||||||
"ask",
|
"ask",
|
||||||
"assbin",
|
"assbin",
|
||||||
@@ -26,6 +26,7 @@ export const properties = {
|
|||||||
"fbx",
|
"fbx",
|
||||||
"glb",
|
"glb",
|
||||||
"gltf",
|
"gltf",
|
||||||
|
"hmb",
|
||||||
"hmp",
|
"hmp",
|
||||||
"ifc",
|
"ifc",
|
||||||
"ifczip",
|
"ifczip",
|
||||||
@@ -35,6 +36,7 @@ export const properties = {
|
|||||||
"lwo",
|
"lwo",
|
||||||
"lws",
|
"lws",
|
||||||
"lxo",
|
"lxo",
|
||||||
|
"m3d",
|
||||||
"md2",
|
"md2",
|
||||||
"md3",
|
"md3",
|
||||||
"md5anim",
|
"md5anim",
|
||||||
@@ -81,7 +83,7 @@ export const properties = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
to: {
|
to: {
|
||||||
muxer: [
|
object: [
|
||||||
"3ds",
|
"3ds",
|
||||||
"3mf",
|
"3mf",
|
||||||
"assbin",
|
"assbin",
|
||||||
@@ -95,8 +97,7 @@ export const properties = {
|
|||||||
"glb2",
|
"glb2",
|
||||||
"gltf",
|
"gltf",
|
||||||
"gltf2",
|
"gltf2",
|
||||||
"m3d",
|
"json",
|
||||||
"m3da",
|
|
||||||
"obj",
|
"obj",
|
||||||
"objnomtl",
|
"objnomtl",
|
||||||
"pbrt",
|
"pbrt",
|
||||||
@@ -106,7 +107,6 @@ export const properties = {
|
|||||||
"stlb",
|
"stlb",
|
||||||
"stp",
|
"stp",
|
||||||
"x",
|
"x",
|
||||||
"x3d",
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -119,8 +119,6 @@ export async function convert(
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
options?: unknown,
|
options?: unknown,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
// let command = "ffmpeg";
|
|
||||||
|
|
||||||
const command = `assimp export "${filePath}" "${targetPath}"`;
|
const command = `assimp export "${filePath}" "${targetPath}"`;
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|||||||
86
src/converters/calibre.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { exec } from "node:child_process";
|
||||||
|
|
||||||
|
export const properties = {
|
||||||
|
from: {
|
||||||
|
document: [
|
||||||
|
"azw4",
|
||||||
|
"chm",
|
||||||
|
"cbr",
|
||||||
|
"cbz",
|
||||||
|
"cbt",
|
||||||
|
"cba",
|
||||||
|
"cb7",
|
||||||
|
"djvu",
|
||||||
|
"docx",
|
||||||
|
"epub",
|
||||||
|
"fb2",
|
||||||
|
"htlz",
|
||||||
|
"html",
|
||||||
|
"lit",
|
||||||
|
"lrf",
|
||||||
|
"mobi",
|
||||||
|
"odt",
|
||||||
|
"pdb",
|
||||||
|
"pdf",
|
||||||
|
"pml",
|
||||||
|
"rb",
|
||||||
|
"rtf",
|
||||||
|
"recipe",
|
||||||
|
"snb",
|
||||||
|
"tcr",
|
||||||
|
"txt",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
document: [
|
||||||
|
"azw3",
|
||||||
|
"docx",
|
||||||
|
"epub",
|
||||||
|
"fb2",
|
||||||
|
"html",
|
||||||
|
"htmlz",
|
||||||
|
"lit",
|
||||||
|
"lrf",
|
||||||
|
"mobi",
|
||||||
|
"oeb",
|
||||||
|
"pdb",
|
||||||
|
"pdf",
|
||||||
|
"pml",
|
||||||
|
"rb",
|
||||||
|
"rtf",
|
||||||
|
"snb",
|
||||||
|
"tcr",
|
||||||
|
"txt",
|
||||||
|
"txtz",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function convert(
|
||||||
|
filePath: string,
|
||||||
|
fileType: string,
|
||||||
|
convertTo: string,
|
||||||
|
targetPath: string,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
options?: unknown,
|
||||||
|
): Promise<string> {
|
||||||
|
const command = `ebook-convert "${filePath}" "${targetPath}"`;
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
exec(command, (error, stdout, stderr) => {
|
||||||
|
if (error) {
|
||||||
|
reject(`error: ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stdout) {
|
||||||
|
console.log(`stdout: ${stdout}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stderr) {
|
||||||
|
console.error(`stderr: ${stderr}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve("Done");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -700,7 +700,7 @@ export async function convert(
|
|||||||
message = "Done: resized to 256x256";
|
message = "Done: resized to 256x256";
|
||||||
}
|
}
|
||||||
|
|
||||||
const command = `ffmpeg -i "${filePath}" ${extra} "${targetPath}"`;
|
const command = `ffmpeg ${process.env.FFMPEG_ARGS || ""} -i "${filePath}" ${extra} "${targetPath}"`;
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
exec(command, (error, stdout, stderr) => {
|
exec(command, (error, stdout, stderr) => {
|
||||||
|
|||||||
64
src/converters/inkscape.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { exec } from "node:child_process";
|
||||||
|
|
||||||
|
export const properties = {
|
||||||
|
from: {
|
||||||
|
images: [
|
||||||
|
"svg",
|
||||||
|
"pdf",
|
||||||
|
"eps",
|
||||||
|
"ps",
|
||||||
|
"wmf",
|
||||||
|
"emf",
|
||||||
|
"png"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
images: [
|
||||||
|
"dxf",
|
||||||
|
"emf",
|
||||||
|
"eps",
|
||||||
|
"fxg",
|
||||||
|
"gpl",
|
||||||
|
"hpgl",
|
||||||
|
"html",
|
||||||
|
"odg",
|
||||||
|
"pdf",
|
||||||
|
"png",
|
||||||
|
"pov",
|
||||||
|
"ps",
|
||||||
|
"sif",
|
||||||
|
"svg",
|
||||||
|
"svgz",
|
||||||
|
"tex",
|
||||||
|
"wmf",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function convert(
|
||||||
|
filePath: string,
|
||||||
|
fileType: string,
|
||||||
|
convertTo: string,
|
||||||
|
targetPath: string,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
options?: unknown,
|
||||||
|
): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
exec(`inkscape "${filePath}" -o "${targetPath}"`, (error, stdout, stderr) => {
|
||||||
|
if (error) {
|
||||||
|
reject(`error: ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stdout) {
|
||||||
|
console.log(`stdout: ${stdout}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stderr) {
|
||||||
|
console.error(`stderr: ${stderr}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve("Done");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@@ -2,11 +2,13 @@ import { normalizeFiletype } from "../helpers/normalizeFiletype";
|
|||||||
import { convert as convertassimp, properties as propertiesassimp } from "./assimp";
|
import { convert as convertassimp, properties as propertiesassimp } from "./assimp";
|
||||||
import { convert as convertFFmpeg, properties as propertiesFFmpeg } from "./ffmpeg";
|
import { convert as convertFFmpeg, properties as propertiesFFmpeg } from "./ffmpeg";
|
||||||
import { convert as convertGraphicsmagick, properties as propertiesGraphicsmagick } from "./graphicsmagick";
|
import { convert as convertGraphicsmagick, properties as propertiesGraphicsmagick } from "./graphicsmagick";
|
||||||
|
import { convert as convertInkscape, properties as propertiesInkscape } from "./inkscape";
|
||||||
import { convert as convertLibjxl, properties as propertiesLibjxl } from "./libjxl";
|
import { convert as convertLibjxl, properties as propertiesLibjxl } from "./libjxl";
|
||||||
import { convert as convertPandoc, properties as propertiesPandoc } from "./pandoc";
|
import { convert as convertPandoc, properties as propertiesPandoc } from "./pandoc";
|
||||||
import { convert as convertresvg, properties as propertiesresvg } from "./resvg";
|
import { convert as convertresvg, properties as propertiesresvg } from "./resvg";
|
||||||
import { convert as convertImage, properties as propertiesImage } from "./vips";
|
import { convert as convertImage, properties as propertiesImage } from "./vips";
|
||||||
import { convert as convertxelatex, properties as propertiesxelatex } from "./xelatex";
|
import { convert as convertxelatex, properties as propertiesxelatex } from "./xelatex";
|
||||||
|
import { convert as convertCalibre, properties as propertiesCalibre } from "./calibre";
|
||||||
|
|
||||||
|
|
||||||
// This should probably be reconstructed so that the functions are not imported instead the functions hook into this to make the converters more modular
|
// This should probably be reconstructed so that the functions are not imported instead the functions hook into this to make the converters more modular
|
||||||
@@ -55,6 +57,10 @@ const properties: Record<
|
|||||||
properties: propertiesxelatex,
|
properties: propertiesxelatex,
|
||||||
converter: convertxelatex,
|
converter: convertxelatex,
|
||||||
},
|
},
|
||||||
|
calibre: {
|
||||||
|
properties: propertiesCalibre,
|
||||||
|
converter: convertCalibre,
|
||||||
|
},
|
||||||
pandoc: {
|
pandoc: {
|
||||||
properties: propertiesPandoc,
|
properties: propertiesPandoc,
|
||||||
converter: convertPandoc,
|
converter: convertPandoc,
|
||||||
@@ -63,6 +69,10 @@ const properties: Record<
|
|||||||
properties: propertiesGraphicsmagick,
|
properties: propertiesGraphicsmagick,
|
||||||
converter: convertGraphicsmagick,
|
converter: convertGraphicsmagick,
|
||||||
},
|
},
|
||||||
|
inkscape: {
|
||||||
|
properties: propertiesInkscape,
|
||||||
|
converter: convertInkscape,
|
||||||
|
},
|
||||||
assimp: {
|
assimp: {
|
||||||
properties: propertiesassimp,
|
properties: propertiesassimp,
|
||||||
converter: convertassimp,
|
converter: convertassimp,
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ export const normalizeFiletype = (filetype: string): string => {
|
|||||||
return "latex";
|
return "latex";
|
||||||
case "md":
|
case "md":
|
||||||
return "markdown";
|
return "markdown";
|
||||||
|
case "unknown":
|
||||||
|
return "m4a";
|
||||||
default:
|
default:
|
||||||
return lowercaseFiletype;
|
return lowercaseFiletype;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,16 @@ if (process.env.NODE_ENV === "production") {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
exec("inkscape --version", (error, stdout) => {
|
||||||
|
if (error) {
|
||||||
|
console.error("Inkscape is not installed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stdout) {
|
||||||
|
console.log(stdout.split("\n")[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
exec("djxl --version", (error, stdout) => {
|
exec("djxl --version", (error, stdout) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error("libjxl-tools is not installed.");
|
console.error("libjxl-tools is not installed.");
|
||||||
@@ -93,6 +103,16 @@ if (process.env.NODE_ENV === "production") {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
exec("ebook-convert --version", (error, stdout) => {
|
||||||
|
if (error) {
|
||||||
|
console.error("ebook-convert (calibre) is not installed");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stdout) {
|
||||||
|
console.log(stdout.split("\n")[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
exec("bun -v", (error, stdout) => {
|
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");
|
||||||
|
|||||||
221
src/index.tsx
@@ -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,24 +130,36 @@ 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-neutral-800 p-4">
|
<header class="w-full bg-neutral-800 p-4">
|
||||||
Create your account
|
Create your account
|
||||||
</header>
|
</header>
|
||||||
<form method="post" action="/register" class="p-4">
|
<form method="post" action={`${WEBROOT}/register`} class="p-4">
|
||||||
<fieldset class="mb-4 flex flex-col gap-4">
|
<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
|
||||||
@@ -191,13 +206,17 @@ 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}
|
||||||
|
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
|
||||||
|
/>
|
||||||
<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">
|
||||||
@@ -241,7 +260,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) {
|
||||||
@@ -296,13 +315,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
|
||||||
@@ -310,16 +329,20 @@ 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}
|
||||||
|
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
|
||||||
|
/>
|
||||||
<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">
|
||||||
@@ -350,7 +373,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"
|
||||||
>
|
>
|
||||||
@@ -417,7 +440,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() }) },
|
||||||
)
|
)
|
||||||
@@ -426,46 +449,29 @@ 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 (!ALLOW_UNAUTHENTICATED) {
|
||||||
return redirect("/setup", 302);
|
if (FIRST_RUN) {
|
||||||
}
|
return redirect(`${WEBROOT}/setup`, 302);
|
||||||
|
}
|
||||||
|
|
||||||
if (!auth?.value && !ALLOW_UNAUTHENTICATED) {
|
if (!auth?.value) {
|
||||||
return redirect("/login", 302);
|
return redirect(`${WEBROOT}/login`, 302);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate jwt
|
// validate jwt
|
||||||
let user: ({ id: string } & JWTPayloadSpec) | false = false;
|
let user: ({ id: string } & JWTPayloadSpec) | false = false;
|
||||||
if (auth?.value) {
|
if (ALLOW_UNAUTHENTICATED) {
|
||||||
user = await jwt.verify(auth.value);
|
|
||||||
|
|
||||||
if (user !== false && user.id) {
|
|
||||||
if (Number.parseInt(user.id) < 2 ** 24 || !ALLOW_UNAUTHENTICATED) {
|
|
||||||
// make sure user exists in db
|
|
||||||
const existingUser = db
|
|
||||||
.query("SELECT * FROM users WHERE id = ?")
|
|
||||||
.as(User)
|
|
||||||
.get(user.id);
|
|
||||||
|
|
||||||
if (!existingUser) {
|
|
||||||
if (auth?.value) {
|
|
||||||
auth.remove();
|
|
||||||
}
|
|
||||||
return redirect("/login", 302);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (ALLOW_UNAUTHENTICATED) {
|
|
||||||
const newUserId = String(
|
const newUserId = String(
|
||||||
randomInt(
|
randomInt(
|
||||||
2 ** 24,
|
2 ** 24,
|
||||||
@@ -491,10 +497,29 @@ const app = new Elysia({
|
|||||||
maxAge: 24 * 60 * 60,
|
maxAge: 24 * 60 * 60,
|
||||||
sameSite: "strict",
|
sameSite: "strict",
|
||||||
});
|
});
|
||||||
|
} else if (auth?.value) {
|
||||||
|
user = await jwt.verify(auth.value);
|
||||||
|
|
||||||
|
if (user !== false && user.id) {
|
||||||
|
if (Number.parseInt(user.id) < 2 ** 24 || !ALLOW_UNAUTHENTICATED) {
|
||||||
|
// make sure user exists in db
|
||||||
|
const existingUser = db
|
||||||
|
.query("SELECT * FROM users WHERE id = ?")
|
||||||
|
.as(User)
|
||||||
|
.get(user.id);
|
||||||
|
|
||||||
|
if (!existingUser) {
|
||||||
|
if (auth?.value) {
|
||||||
|
auth.remove();
|
||||||
|
}
|
||||||
|
return redirect(`${WEBROOT}/login`, 302);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return redirect("/login", 302);
|
return redirect(`${WEBROOT}/login`, 302);
|
||||||
}
|
}
|
||||||
|
|
||||||
// create a new job
|
// create a new job
|
||||||
@@ -524,9 +549,13 @@ 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}
|
||||||
|
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
|
||||||
|
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>
|
||||||
@@ -562,7 +591,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" />
|
||||||
@@ -727,16 +756,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
|
||||||
@@ -744,7 +773,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}/`;
|
||||||
@@ -769,16 +798,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
|
||||||
@@ -786,7 +815,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}/`;
|
||||||
@@ -799,16 +828,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
|
||||||
@@ -817,7 +846,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}/`;
|
||||||
@@ -838,7 +867,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(
|
||||||
@@ -891,7 +920,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({
|
||||||
@@ -902,12 +931,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
|
||||||
@@ -928,9 +957,13 @@ 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}
|
||||||
|
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
|
||||||
|
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>
|
||||||
@@ -963,7 +996,7 @@ const app = new Elysia({
|
|||||||
text-accent-500 underline
|
text-accent-500 underline
|
||||||
hover:text-accent-400
|
hover:text-accent-400
|
||||||
`}
|
`}
|
||||||
href={`/results/${job.id}`}
|
href={`${WEBROOT}/results/${job.id}`}
|
||||||
>
|
>
|
||||||
View
|
View
|
||||||
</a>
|
</a>
|
||||||
@@ -982,7 +1015,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) {
|
||||||
@@ -992,7 +1025,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
|
||||||
@@ -1015,9 +1048,13 @@ 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}
|
||||||
|
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
|
||||||
|
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">
|
||||||
@@ -1075,7 +1112,7 @@ const app = new Elysia({
|
|||||||
text-accent-500 underline
|
text-accent-500 underline
|
||||||
hover:text-accent-400
|
hover:text-accent-400
|
||||||
`}
|
`}
|
||||||
href={`/download/${outputPath}${file.output_file_name}`}
|
href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
|
||||||
>
|
>
|
||||||
View
|
View
|
||||||
</a>
|
</a>
|
||||||
@@ -1086,7 +1123,7 @@ const app = new Elysia({
|
|||||||
text-accent-500 underline
|
text-accent-500 underline
|
||||||
hover:text-accent-400
|
hover:text-accent-400
|
||||||
`}
|
`}
|
||||||
href={`/download/${outputPath}${file.output_file_name}`}
|
href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
|
||||||
download={file.output_file_name}
|
download={file.output_file_name}
|
||||||
>
|
>
|
||||||
Download
|
Download
|
||||||
@@ -1098,7 +1135,7 @@ const app = new Elysia({
|
|||||||
</table>
|
</table>
|
||||||
</article>
|
</article>
|
||||||
</main>
|
</main>
|
||||||
<script src="/results.js" defer />
|
<script src={`${WEBROOT}/results.js`} defer />
|
||||||
</>
|
</>
|
||||||
</BaseHtml>
|
</BaseHtml>
|
||||||
);
|
);
|
||||||
@@ -1108,7 +1145,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) {
|
||||||
@@ -1118,7 +1155,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
|
||||||
@@ -1197,7 +1234,7 @@ const app = new Elysia({
|
|||||||
text-accent-500 underline
|
text-accent-500 underline
|
||||||
hover:text-accent-400
|
hover:text-accent-400
|
||||||
`}
|
`}
|
||||||
href={`/download/${outputPath}${file.output_file_name}`}
|
href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
|
||||||
>
|
>
|
||||||
View
|
View
|
||||||
</a>
|
</a>
|
||||||
@@ -1208,7 +1245,7 @@ const app = new Elysia({
|
|||||||
text-accent-500 underline
|
text-accent-500 underline
|
||||||
hover:text-accent-400
|
hover:text-accent-400
|
||||||
`}
|
`}
|
||||||
href={`/download/${outputPath}${file.output_file_name}`}
|
href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
|
||||||
download={file.output_file_name}
|
download={file.output_file_name}
|
||||||
>
|
>
|
||||||
Download
|
Download
|
||||||
@@ -1226,12 +1263,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
|
||||||
@@ -1239,7 +1276,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);
|
||||||
@@ -1252,18 +1289,22 @@ 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}
|
||||||
|
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
|
||||||
|
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>
|
||||||
@@ -1322,12 +1363,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
|
||||||
@@ -1335,12 +1376,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);
|
||||||
},
|
},
|
||||||
@@ -1364,7 +1405,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 = () => {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
|
||||||
module.exports = {
|
import tailwindScrollbar from "tailwind-scrollbar";
|
||||||
|
|
||||||
|
export default {
|
||||||
content: ["./src/**/*.{html,js,tsx,jsx,cjs,mjs}"],
|
content: ["./src/**/*.{html,js,tsx,jsx,cjs,mjs}"],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
@@ -22,5 +23,5 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [require("tailwind-scrollbar")],
|
plugins: [tailwindScrollbar],
|
||||||
};
|
};
|
||||||
|
|||||||