mirror of
https://github.com/C4illin/ConvertX.git
synced 2025-11-04 22:13:32 +00:00
Compare commits
101 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7914194856 | ||
|
|
2dac7f1362 | ||
|
|
a17e5fd614 | ||
|
|
21994fb6a2 | ||
|
|
a5eaaa422a | ||
|
|
ff2ef74135 | ||
|
|
70705c1850 | ||
|
|
fd9c151e01 | ||
|
|
4f0573963f | ||
|
|
6bb6bce8a4 | ||
|
|
448557bece | ||
|
|
bdbd4a122c | ||
|
|
cb9d0ec680 | ||
|
|
fb60ef66f5 | ||
|
|
c1ae43075f | ||
|
|
377f69ae8d | ||
|
|
cb131cd0a0 | ||
|
|
fcc83c5ea8 | ||
|
|
96d4717d13 | ||
|
|
4d73bf9760 | ||
|
|
725a94bc95 | ||
|
|
0a366b447a | ||
|
|
4a27a7bc03 | ||
|
|
3ca5803bda | ||
|
|
239041294c | ||
|
|
31fdd8f214 | ||
|
|
c3319c09eb | ||
|
|
d460e94d52 | ||
|
|
4b5c732380 | ||
|
|
f42665ca40 | ||
|
|
bed52cef17 | ||
|
|
9d1c93155c | ||
|
|
794cc7c474 | ||
|
|
d7d584e497 | ||
|
|
f5320df86e | ||
|
|
056fd4ba93 | ||
|
|
5b6e70eb3a | ||
|
|
f437a8e7e2 | ||
|
|
cdae798fcf | ||
|
|
bcc827a81b | ||
|
|
84274b9c55 | ||
|
|
20c6f8249e | ||
|
|
8f0ea2a592 | ||
|
|
a29e4a930a | ||
|
|
4549c96ae3 | ||
|
|
bc64094c04 | ||
|
|
fa58827ad5 | ||
|
|
8f27be0e3d | ||
|
|
df43df1178 | ||
|
|
c2beb4a227 | ||
|
|
9277c27a50 | ||
|
|
171ecd6884 | ||
|
|
dca29f7e5a | ||
|
|
318acc20bd | ||
|
|
f433493d57 | ||
|
|
19970fc132 | ||
|
|
24394ca3c5 | ||
|
|
10ff0b464a | ||
|
|
9263d17609 | ||
|
|
c1b75a13fd | ||
|
|
a8ed60d48f | ||
|
|
dc82a438d4 | ||
|
|
cc54bdcbe7 | ||
|
|
ae4bbc8baa | ||
|
|
ad98499da0 | ||
|
|
db60f355b2 | ||
|
|
eb91d8b298 | ||
|
|
b8312be4b7 | ||
|
|
326a8e3404 | ||
|
|
f017e13ac1 | ||
|
|
67a5fe353e | ||
|
|
51d49d7ff3 | ||
|
|
d42b820b36 | ||
|
|
07d32776d3 | ||
|
|
ef027e81b5 | ||
|
|
a75e4b495d | ||
|
|
fba5e212e8 | ||
|
|
62f44fb052 | ||
|
|
6b9254047c | ||
|
|
decfea5dc9 | ||
|
|
eacded6848 | ||
|
|
279ca72c64 | ||
|
|
b8fc9383ca | ||
|
|
bec58ac59f | ||
|
|
83d7126820 | ||
|
|
f0e9c6d794 | ||
|
|
0e61051fc6 | ||
|
|
480ba77ebe | ||
|
|
16f27c13bb | ||
|
|
afe5c50d66 | ||
|
|
72ea859ebb | ||
|
|
8edf3834c4 | ||
|
|
e595014fcd | ||
|
|
8bebf7e569 | ||
|
|
c825ec06e2 | ||
|
|
8c75f273fb | ||
|
|
0ba776c129 | ||
|
|
2bbbd03554 | ||
|
|
0a5d0487b1 | ||
|
|
583cd2dd3b | ||
|
|
e1f7fc1ecb |
@@ -2,7 +2,6 @@
|
|||||||
.editorconfig
|
.editorconfig
|
||||||
.env
|
.env
|
||||||
.git
|
.git
|
||||||
.gitignore
|
|
||||||
.idea
|
.idea
|
||||||
.vscode
|
.vscode
|
||||||
CHANGELOG.md
|
CHANGELOG.md
|
||||||
|
|||||||
15
.github/FUNDING.yml
vendored
Normal file
15
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: [C4illin] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||||
|
patreon: # Replace with a single Patreon username
|
||||||
|
open_collective: # Replace with a single Open Collective username
|
||||||
|
ko_fi: # Replace with a single Ko-fi username
|
||||||
|
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
|
liberapay: # Replace with a single Liberapay username
|
||||||
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
|
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||||
|
polar: # Replace with a single Polar username
|
||||||
|
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
||||||
|
thanks_dev: # Replace with a single thanks.dev username
|
||||||
|
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||||
62
CHANGELOG.md
62
CHANGELOG.md
@@ -1,5 +1,67 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [0.13.0](https://github.com/C4illin/ConvertX/compare/v0.12.1...v0.13.0) (2025-05-14)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add HIDE_HISTORY option to control visibility of history page ([bed52ce](https://github.com/C4illin/ConvertX/commit/bed52cef17ff68ec5e8770705a1fdf038e02e607))
|
||||||
|
* add HIDE_HISTORY option to control visibility of history page ([9d1c931](https://github.com/C4illin/ConvertX/commit/9d1c93155cc33ed6c83f9e5122afff8f28d0e4bf))
|
||||||
|
* add potrace converter ([bdbd4a1](https://github.com/C4illin/ConvertX/commit/bdbd4a122c09559b089b985ea12c5f3e085107da))
|
||||||
|
* Add support for .HIF files ([a5eaaa4](https://github.com/C4illin/ConvertX/commit/a5eaaa422a64506dd16d90d48a240556de33bc93))
|
||||||
|
* Add support for .HIF files ([70705c1](https://github.com/C4illin/ConvertX/commit/70705c1850d470296df85958c02a01fb5bc3a25f))
|
||||||
|
* add support for drag/drop of images ([ff2ef74](https://github.com/C4illin/ConvertX/commit/ff2ef7413542cf10ba7a6e246763bcecd6829ec1))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add timezone support ([4b5c732](https://github.com/C4illin/ConvertX/commit/4b5c732380bc844dccf340ea1eb4f8bfe3bb44a5)), closes [#258](https://github.com/C4illin/ConvertX/issues/258)
|
||||||
|
|
||||||
|
## [0.12.1](https://github.com/C4illin/ConvertX/compare/v0.12.0...v0.12.1) (2025-03-20)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* rollback to bun 1.2.2 ([cdae798](https://github.com/C4illin/ConvertX/commit/cdae798fcf5879e4adea87386a38748b9a1e1ddc))
|
||||||
|
|
||||||
|
## [0.12.0](https://github.com/C4illin/ConvertX/compare/v0.11.1...v0.12.0) (2025-03-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* added progress bar for file upload ([db60f35](https://github.com/C4illin/ConvertX/commit/db60f355b2973f43f8e5990e6fe4e351b959b659))
|
||||||
|
* made every upload file independent ([cc54bdc](https://github.com/C4illin/ConvertX/commit/cc54bdcbe764c41cc3273485d072fd3178ad2dca))
|
||||||
|
* replace exec with execFile ([9263d17](https://github.com/C4illin/ConvertX/commit/9263d17609dc4b2b367eb7fee67b3182e283b3a3))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add libheif ([6b92540](https://github.com/C4illin/ConvertX/commit/6b9254047c0598963aee1d99e20ba1650a0368bf))
|
||||||
|
* add libheif ([decfea5](https://github.com/C4illin/ConvertX/commit/decfea5dc9627b216bb276a9e1578c32cfa1deb6)), closes [#202](https://github.com/C4illin/ConvertX/issues/202)
|
||||||
|
* added onerror log ([ae4bbc8](https://github.com/C4illin/ConvertX/commit/ae4bbc8baacbaf67763c62ea44140bb21cc17230))
|
||||||
|
* refactored uploadFile to only accept a single file instead of multiple ([dc82a43](https://github.com/C4illin/ConvertX/commit/dc82a438d4104b79ff423d502a6779a43928968a))
|
||||||
|
* update libheif to 1.19.5 ([fba5e21](https://github.com/C4illin/ConvertX/commit/fba5e212e8d0eaba8971e239e35aeb521f3cd813)), closes [#202](https://github.com/C4illin/ConvertX/issues/202)
|
||||||
|
|
||||||
|
## [0.11.1](https://github.com/C4illin/ConvertX/compare/v0.11.0...v0.11.1) (2025-02-07)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* mobile view overflow ([bec58ac](https://github.com/C4illin/ConvertX/commit/bec58ac59f9600e35385b9e21d174f3ab1b42b1d))
|
||||||
|
|
||||||
|
## [0.11.0](https://github.com/C4illin/ConvertX/compare/v0.10.1...v0.11.0) (2025-02-05)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add deps for vaapi ([2bbbd03](https://github.com/C4illin/ConvertX/commit/2bbbd03554d384a4488143f29e5fc863cfdf333b)), closes [#192](https://github.com/C4illin/ConvertX/issues/192)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* don't crash if file is not found ([16f27c1](https://github.com/C4illin/ConvertX/commit/16f27c13bbc1c0e5fa2316f3db11d0918524053b))
|
||||||
|
* install numpy for inkscape ([0e61051](https://github.com/C4illin/ConvertX/commit/0e61051fc6be188164c3865b4fb579c140859fdc))
|
||||||
|
|
||||||
## [0.10.1](https://github.com/C4illin/ConvertX/compare/v0.10.0...v0.10.1) (2025-01-21)
|
## [0.10.1](https://github.com/C4illin/ConvertX/compare/v0.10.0...v0.10.1) (2025-01-21)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
32
Dockerfile
32
Dockerfile
@@ -1,4 +1,4 @@
|
|||||||
FROM oven/bun:1.1.45-alpine AS base
|
FROM oven/bun:1.2.2-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
|
||||||
|
|
||||||
@@ -6,12 +6,12 @@ WORKDIR /app
|
|||||||
# this will cache them and speed up future builds
|
# this will cache them and speed up future builds
|
||||||
FROM base AS install
|
FROM base AS install
|
||||||
RUN mkdir -p /temp/dev
|
RUN mkdir -p /temp/dev
|
||||||
COPY package.json bun.lockb /temp/dev/
|
COPY package.json bun.lock /temp/dev/
|
||||||
RUN cd /temp/dev && bun install --frozen-lockfile
|
RUN cd /temp/dev && bun install --frozen-lockfile
|
||||||
|
|
||||||
# install with --production (exclude devDependencies)
|
# install with --production (exclude devDependencies)
|
||||||
RUN mkdir -p /temp/prod
|
RUN mkdir -p /temp/prod
|
||||||
COPY package.json bun.lockb /temp/prod/
|
COPY package.json bun.lock /temp/prod/
|
||||||
RUN cd /temp/prod && bun install --frozen-lockfile --production
|
RUN cd /temp/prod && bun install --frozen-lockfile --production
|
||||||
|
|
||||||
FROM base AS builder
|
FROM base AS builder
|
||||||
@@ -20,22 +20,18 @@ ENV PATH=/root/.cargo/bin:$PATH
|
|||||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||||
RUN cargo install resvg
|
RUN cargo install resvg
|
||||||
|
|
||||||
# copy node_modules from temp directory
|
|
||||||
# then copy all (non-ignored) project files into the image
|
|
||||||
FROM base AS prerelease
|
FROM base AS prerelease
|
||||||
|
WORKDIR /app
|
||||||
COPY --from=install /temp/dev/node_modules node_modules
|
COPY --from=install /temp/dev/node_modules node_modules
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# # [optional] tests & build
|
# ENV NODE_ENV=production
|
||||||
ENV NODE_ENV=production
|
|
||||||
# RUN bun test
|
|
||||||
RUN bun run build
|
RUN bun run build
|
||||||
|
|
||||||
# copy production dependencies and source code into final image
|
# copy production dependencies and source code into final image
|
||||||
FROM base AS release
|
FROM base AS release
|
||||||
LABEL maintainer="Emrik Östling (C4illin)"
|
|
||||||
LABEL description="ConvertX: self-hosted online file converter supporting 700+ file formats."
|
RUN apk --no-cache add libheif-tools --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community/
|
||||||
LABEL repo="https://github.com/C4illin/ConvertX"
|
|
||||||
|
|
||||||
# install additional dependencies
|
# install additional dependencies
|
||||||
RUN apk --no-cache add \
|
RUN apk --no-cache add \
|
||||||
@@ -49,14 +45,18 @@ RUN apk --no-cache add \
|
|||||||
vips-tools \
|
vips-tools \
|
||||||
vips-poppler \
|
vips-poppler \
|
||||||
vips-jxl \
|
vips-jxl \
|
||||||
|
vips-heif \
|
||||||
|
vips-magick \
|
||||||
libjxl-tools \
|
libjxl-tools \
|
||||||
assimp \
|
assimp \
|
||||||
inkscape \
|
inkscape \
|
||||||
poppler-utils
|
poppler-utils \
|
||||||
|
gcompat \
|
||||||
|
libva-utils \
|
||||||
|
py3-numpy \
|
||||||
|
potrace
|
||||||
|
|
||||||
RUN apk --no-cache add qt6-qtbase-private-dev --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community/
|
# RUN apk --no-cache add calibre@testing --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main/
|
||||||
|
|
||||||
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 \
|
||||||
@@ -64,8 +64,6 @@ RUN apk --no-cache add calibre --repository=http://dl-cdn.alpinelinux.org/alpine
|
|||||||
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/public/generated.css /app/public/
|
COPY --from=prerelease /app/public/generated.css /app/public/
|
||||||
# COPY --from=prerelease /app/src/index.tsx /app/src/
|
|
||||||
# COPY --from=prerelease /app/package.json .
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
EXPOSE 3000/tcp
|
EXPOSE 3000/tcp
|
||||||
|
|||||||
13
README.md
13
README.md
@@ -27,6 +27,7 @@ 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 |
|
||||||
|
| [libheif](https://github.com/strukturag/libheif) | HEIF | 2 | 4 |
|
||||||
| [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 |
|
| [Calibre](https://calibre-ebook.com/) | E-books | 26 | 19 |
|
||||||
| [Pandoc](https://pandoc.org/) | Documents | 43 | 65 |
|
| [Pandoc](https://pandoc.org/) | Documents | 43 | 65 |
|
||||||
@@ -41,6 +42,9 @@ Any missing converter? Open an issue or pull request!
|
|||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> If you can't login, make sure you are accessing the service over localhost or https otherwise set HTTP_ALLOWED=true
|
||||||
|
|
||||||
```yml
|
```yml
|
||||||
# docker-compose.yml
|
# docker-compose.yml
|
||||||
services:
|
services:
|
||||||
@@ -79,9 +83,7 @@ All are optional, JWT_SECRET is recommended to be set.
|
|||||||
| AUTO_DELETE_EVERY_N_HOURS | 24 | Checks every n hours for files older then n hours and deletes them, set to 0 to disable |
|
| 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/" |
|
| 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` |
|
| FFMPEG_ARGS | | Arguments to pass to ffmpeg, e.g. `-preset veryfast` |
|
||||||
|
| HIDE_HISTORY | false | Hide the history page |
|
||||||
> [!WARNING]
|
|
||||||
> If you can't login, make sure you are accessing the service over https or set HTTP_ALLOWED=true
|
|
||||||
|
|
||||||
### Docker images
|
### Docker images
|
||||||
|
|
||||||
@@ -96,10 +98,15 @@ The image is available on [GitHub Container Registry](https://github.com/C4illin
|
|||||||
| `image: c4illin/convertx` | The latest release on docker hub |
|
| `image: c4illin/convertx` | The latest release on docker hub |
|
||||||
| `image: c4illin/convertx:main` | The latest commit on docker hub |
|
| `image: c4illin/convertx:main` | The latest commit on docker hub |
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
<!-- Dockerhub was introduced in 0.9.0 and older releases -->
|
<!-- Dockerhub was introduced in 0.9.0 and older releases -->
|
||||||
|
|
||||||
### Tutorial
|
### Tutorial
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> These are written by other people, and may be outdated, incorrect or wrong.
|
||||||
|
|
||||||
Tutorial in french: <https://belginux.com/installer-convertx-avec-docker/>
|
Tutorial in french: <https://belginux.com/installer-convertx-avec-docker/>
|
||||||
|
|
||||||
Tutorial in chinese: <https://xzllll.com/24092901/>
|
Tutorial in chinese: <https://xzllll.com/24092901/>
|
||||||
|
|||||||
9
SECURITY.md
Normal file
9
SECURITY.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
Only the latest release is supported
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/C4illin/ConvertX/security/advisories/new) tab.
|
||||||
@@ -13,5 +13,6 @@ services:
|
|||||||
- 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
|
# - FFMPEG_ARGS=-hwaccel vulkan # additional arguments to pass to ffmpeg
|
||||||
# - WEBROOT=/convertx # the root path of the web interface, leave empty to disable
|
# - WEBROOT=/convertx # the root path of the web interface, leave empty to disable
|
||||||
|
# - HIDE_HISTORY=true # hides the history tab in the web interface, defaults to false
|
||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 3000:3000
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
import { fixupPluginRules } from "@eslint/compat";
|
|
||||||
import eslint from "@eslint/js";
|
|
||||||
import deprecationPlugin from "eslint-plugin-deprecation";
|
|
||||||
import eslintPluginReadableTailwind from "eslint-plugin-readable-tailwind";
|
|
||||||
import simpleImportSortPlugin from "eslint-plugin-simple-import-sort";
|
|
||||||
import tailwind from "eslint-plugin-tailwindcss";
|
|
||||||
import globals from "globals";
|
|
||||||
import tseslint from "typescript-eslint";
|
|
||||||
|
|
||||||
export default tseslint.config(
|
|
||||||
eslint.configs.recommended,
|
|
||||||
...tseslint.configs.recommended,
|
|
||||||
...tailwind.configs["flat/recommended"],
|
|
||||||
{
|
|
||||||
plugins: {
|
|
||||||
deprecation: fixupPluginRules(deprecationPlugin),
|
|
||||||
"simple-import-sort": simpleImportSortPlugin,
|
|
||||||
"readable-tailwind": eslintPluginReadableTailwind,
|
|
||||||
},
|
|
||||||
ignores: ["**/node_modules/**"],
|
|
||||||
languageOptions: {
|
|
||||||
parserOptions: {
|
|
||||||
projectService: true,
|
|
||||||
tsconfigRootDir: import.meta.dirname,
|
|
||||||
ecmaVersion: "latest",
|
|
||||||
sourceType: "module",
|
|
||||||
project: ["./tsconfig.json"],
|
|
||||||
},
|
|
||||||
globals: {
|
|
||||||
...globals.node,
|
|
||||||
...globals.browser,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
files: ["**/*.{js,mjs,cjs,tsx,ts}"],
|
|
||||||
rules: {
|
|
||||||
...eslintPluginReadableTailwind.configs.warning.rules,
|
|
||||||
"tailwindcss/classnames-order": "off",
|
|
||||||
"readable-tailwind/multiline": [
|
|
||||||
"warn",
|
|
||||||
{
|
|
||||||
group: "newLine",
|
|
||||||
printWidth: 100,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"tailwindcss/no-custom-classname": [
|
|
||||||
"warn",
|
|
||||||
{
|
|
||||||
whitelist: [
|
|
||||||
"select_container",
|
|
||||||
"convert_to_popup",
|
|
||||||
"convert_to_group",
|
|
||||||
"target",
|
|
||||||
"convert_to_target",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
57
eslint.config.ts
Normal file
57
eslint.config.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import js from "@eslint/js";
|
||||||
|
import eslintParserTypeScript from "@typescript-eslint/parser";
|
||||||
|
import type { Linter } from "eslint";
|
||||||
|
import eslintPluginReadableTailwind from "eslint-plugin-readable-tailwind";
|
||||||
|
import simpleImportSortPlugin from "eslint-plugin-simple-import-sort";
|
||||||
|
import globals from "globals";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
js.configs.recommended,
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
// ...tailwind.configs["flat/recommended"],
|
||||||
|
{
|
||||||
|
plugins: {
|
||||||
|
"simple-import-sort": simpleImportSortPlugin,
|
||||||
|
"readable-tailwind": eslintPluginReadableTailwind,
|
||||||
|
},
|
||||||
|
ignores: ["**/node_modules/**"],
|
||||||
|
languageOptions: {
|
||||||
|
parser: eslintParserTypeScript,
|
||||||
|
parserOptions: {
|
||||||
|
project: true,
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
globals: {
|
||||||
|
...globals.node,
|
||||||
|
...globals.browser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
files: ["**/*.{js,mjs,cjs,jsx,tsx,ts}"],
|
||||||
|
rules: {
|
||||||
|
...eslintPluginReadableTailwind.configs.warning.rules,
|
||||||
|
// "tailwindcss/classnames-order": "off",
|
||||||
|
"readable-tailwind/multiline": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
group: "newLine",
|
||||||
|
printWidth: 100,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// "tailwindcss/no-custom-classname": [
|
||||||
|
// "warn",
|
||||||
|
// {
|
||||||
|
// whitelist: [
|
||||||
|
// "select_container",
|
||||||
|
// "convert_to_popup",
|
||||||
|
// "convert_to_group",
|
||||||
|
// "target",
|
||||||
|
// "convert_to_target",
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as Linter.Config[];
|
||||||
57
package.json
57
package.json
@@ -1,23 +1,23 @@
|
|||||||
{
|
{
|
||||||
"name": "convertx-frontend",
|
"name": "convertx-frontend",
|
||||||
"version": "0.10.1",
|
"version": "0.13.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": "eslint --fix .",
|
"format": "eslint --fix .",
|
||||||
"build": "postcss ./src/main.css -o ./public/generated.css",
|
"build": "bunx @tailwindcss/cli -i ./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:eslint": "eslint ."
|
"lint:eslint": "eslint ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@elysiajs/cookie": "^0.8.0",
|
"@elysiajs/html": "^1.3.0",
|
||||||
"@elysiajs/html": "^1.2.0",
|
"@elysiajs/jwt": "^1.3.0",
|
||||||
"@elysiajs/jwt": "^1.2.0",
|
"@elysiajs/static": "^1.3.0",
|
||||||
"@elysiajs/static": "^1.2.0",
|
"@kitajs/html": "^4.2.9",
|
||||||
"@kitajs/html": "^4.2.7",
|
"elysia": "^1.3.1",
|
||||||
"elysia": "^1.2.10"
|
"sanitize-filename": "^1.6.3"
|
||||||
},
|
},
|
||||||
"module": "src/index.tsx",
|
"module": "src/index.tsx",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -25,31 +25,30 @@
|
|||||||
"start": "bun run src/index.tsx"
|
"start": "bun run src/index.tsx"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/compat": "^1.2.5",
|
"@eslint/js": "^9.26.0",
|
||||||
"@eslint/js": "^9.18.0",
|
|
||||||
"@ianvs/prettier-plugin-sort-imports": "^4.4.1",
|
"@ianvs/prettier-plugin-sort-imports": "^4.4.1",
|
||||||
"@kitajs/ts-html-plugin": "^4.1.1",
|
"@kitajs/ts-html-plugin": "^4.1.1",
|
||||||
|
"@tailwindcss/cli": "^4.1.6",
|
||||||
|
"@tailwindcss/postcss": "^4.1.6",
|
||||||
"@total-typescript/ts-reset": "^0.6.1",
|
"@total-typescript/ts-reset": "^0.6.1",
|
||||||
"@types/bun": "^1.1.16",
|
"@types/bun": "^1.2.13",
|
||||||
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
||||||
"@types/eslint__js": "^8.42.3",
|
"@types/node": "^22.15.17",
|
||||||
"@types/node": "^22.10.7",
|
"autoprefixer": "^10.4.21",
|
||||||
"autoprefixer": "^10.4.20",
|
"cssnano": "^7.0.7",
|
||||||
"cssnano": "^7.0.6",
|
"eslint": "^9.26.0",
|
||||||
"eslint": "^9.18.0",
|
"eslint-plugin-readable-tailwind": "^2.1.1",
|
||||||
"eslint-plugin-deprecation": "^3.0.0",
|
|
||||||
"eslint-plugin-readable-tailwind": "^1.8.2",
|
|
||||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
"eslint-plugin-tailwindcss": "^3.17.5",
|
"eslint-plugin-tailwindcss": "4.0.0-alpha.0",
|
||||||
"globals": "^15.14.0",
|
"globals": "^16.1.0",
|
||||||
"knip": "^5.42.1",
|
"knip": "^5.55.1",
|
||||||
"npm-run-all2": "^7.0.2",
|
"npm-run-all2": "^8.0.1",
|
||||||
"postcss": "^8.5.1",
|
"postcss": "^8.5.3",
|
||||||
"postcss-cli": "^11.0.0",
|
"postcss-cli": "^11.0.1",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.5.3",
|
||||||
"tailwind-scrollbar": "^3.1.0",
|
"tailwind-scrollbar": "^4.0.2",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^4.1.6",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.8.3",
|
||||||
"typescript-eslint": "^8.20.0"
|
"typescript-eslint": "^8.32.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
import autoprefixer from "autoprefixer";
|
|
||||||
import cssnano from "cssnano";
|
|
||||||
import tailwind from "tailwindcss";
|
|
||||||
import tailwindConfig from "./tailwind.config.js";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
plugins: [autoprefixer, tailwind(tailwindConfig), cssnano],
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
160
public/script.js
160
public/script.js
@@ -7,7 +7,8 @@ let fileType;
|
|||||||
let pendingFiles = 0;
|
let pendingFiles = 0;
|
||||||
let formatSelected = false;
|
let formatSelected = false;
|
||||||
|
|
||||||
dropZone.addEventListener("dragover", () => {
|
dropZone.addEventListener("dragover", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
dropZone.classList.add("dragover");
|
dropZone.classList.add("dragover");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -15,10 +16,59 @@ dropZone.addEventListener("dragleave", () => {
|
|||||||
dropZone.classList.remove("dragover");
|
dropZone.classList.remove("dragover");
|
||||||
});
|
});
|
||||||
|
|
||||||
dropZone.addEventListener("drop", () => {
|
dropZone.addEventListener("drop", (e) => {
|
||||||
dropZone.classList.remove("dragover");
|
e.preventDefault();
|
||||||
|
dropZone.classList.remove("dragover");
|
||||||
|
|
||||||
|
const files = e.dataTransfer.files;
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
console.warn("No files dropped — likely a URL or unsupported source.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
console.log("Handling dropped file:", file.name);
|
||||||
|
handleFile(file);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Extracted handleFile function for reusability in drag-and-drop and file input
|
||||||
|
function handleFile(file) {
|
||||||
|
const fileList = document.querySelector("#file-list");
|
||||||
|
|
||||||
|
const row = document.createElement("tr");
|
||||||
|
row.innerHTML = `
|
||||||
|
<td>${file.name}</td>
|
||||||
|
<td><progress max="100"></progress></td>
|
||||||
|
<td>${(file.size / 1024).toFixed(2)} kB</td>
|
||||||
|
<td><a onclick="deleteRow(this)">Remove</a></td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (!fileType) {
|
||||||
|
fileType = file.name.split(".").pop();
|
||||||
|
fileInput.setAttribute("accept", `.${fileType}`);
|
||||||
|
setTitle();
|
||||||
|
|
||||||
|
fetch(`${webroot}/conversions`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ fileType }),
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
})
|
||||||
|
.then((res) => res.text())
|
||||||
|
.then((html) => {
|
||||||
|
selectContainer.innerHTML = html;
|
||||||
|
updateSearchBar();
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
fileList.appendChild(row);
|
||||||
|
file.htmlRow = row;
|
||||||
|
fileNames.push(file.name);
|
||||||
|
uploadFile(file);
|
||||||
|
}
|
||||||
|
|
||||||
const selectContainer = document.querySelector("form .select_container");
|
const selectContainer = document.querySelector("form .select_container");
|
||||||
|
|
||||||
const updateSearchBar = () => {
|
const updateSearchBar = () => {
|
||||||
@@ -106,58 +156,10 @@ const updateSearchBar = () => {
|
|||||||
|
|
||||||
// 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) => {
|
||||||
// Get the selected files from the event target
|
|
||||||
const files = e.target.files;
|
const files = e.target.files;
|
||||||
|
|
||||||
// Select the file-list table
|
|
||||||
const fileList = document.querySelector("#file-list");
|
|
||||||
|
|
||||||
// Loop through the selected files
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
// Create a new table row for each file
|
handleFile(file);
|
||||||
const row = document.createElement("tr");
|
|
||||||
row.innerHTML = `
|
|
||||||
<td>${file.name}</td>
|
|
||||||
<td>${(file.size / 1024).toFixed(2)} kB</td>
|
|
||||||
<td><a onclick="deleteRow(this)">Remove</a></td>
|
|
||||||
`;
|
|
||||||
|
|
||||||
if (!fileType) {
|
|
||||||
fileType = file.name.split(".").pop();
|
|
||||||
fileInput.setAttribute("accept", `.${fileType}`);
|
|
||||||
setTitle();
|
|
||||||
|
|
||||||
// choose the option that matches the file type
|
|
||||||
// for (const option of convertFromSelect.children) {
|
|
||||||
// console.log(option.value);
|
|
||||||
// if (option.value === fileType) {
|
|
||||||
// option.selected = true;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
fetch(`${webroot}/conversions`, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({ fileType: fileType }),
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((res) => res.text())
|
|
||||||
.then((html) => {
|
|
||||||
selectContainer.innerHTML = html;
|
|
||||||
updateSearchBar();
|
|
||||||
})
|
|
||||||
.catch((err) => console.log(err));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append the row to the file-list table
|
|
||||||
fileList.appendChild(row);
|
|
||||||
|
|
||||||
// Append the file to the hidden input
|
|
||||||
fileNames.push(file.name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadFiles(files);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const setTitle = () => {
|
const setTitle = () => {
|
||||||
@@ -197,33 +199,49 @@ const deleteRow = (target) => {
|
|||||||
.catch((err) => console.log(err));
|
.catch((err) => console.log(err));
|
||||||
};
|
};
|
||||||
|
|
||||||
const uploadFiles = (files) => {
|
const uploadFile = (file) => {
|
||||||
convertButton.disabled = true;
|
convertButton.disabled = true;
|
||||||
convertButton.textContent = "Uploading...";
|
convertButton.textContent = "Uploading...";
|
||||||
pendingFiles += 1;
|
pendingFiles += 1;
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
formData.append("file", file, file.name);
|
||||||
|
|
||||||
for (const file of files) {
|
let xhr = new XMLHttpRequest();
|
||||||
formData.append("file", file, file.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch(`${webroot}/upload`, {
|
xhr.open("POST", `${webroot}/upload`, true);
|
||||||
method: "POST",
|
|
||||||
body: formData,
|
xhr.onload = () => {
|
||||||
})
|
let data = JSON.parse(xhr.responseText);
|
||||||
.then((res) => res.json())
|
|
||||||
.then((data) => {
|
pendingFiles -= 1;
|
||||||
pendingFiles -= 1;
|
if (pendingFiles === 0) {
|
||||||
if (pendingFiles === 0) {
|
if (formatSelected) {
|
||||||
if (formatSelected) {
|
convertButton.disabled = false;
|
||||||
convertButton.disabled = false;
|
|
||||||
}
|
|
||||||
convertButton.textContent = "Convert";
|
|
||||||
}
|
}
|
||||||
console.log(data);
|
convertButton.textContent = "Convert";
|
||||||
})
|
}
|
||||||
.catch((err) => console.log(err));
|
|
||||||
|
//Remove the progress bar when upload is done
|
||||||
|
let progressbar = file.htmlRow.getElementsByTagName("progress");
|
||||||
|
progressbar[0].parentElement.remove();
|
||||||
|
console.log(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.upload.onprogress = (e) => {
|
||||||
|
let sent = e.loaded;
|
||||||
|
let total = e.total;
|
||||||
|
console.log(`upload progress (${file.name}):`, (100 * sent) / total);
|
||||||
|
|
||||||
|
let progressbar = file.htmlRow.getElementsByTagName("progress");
|
||||||
|
progressbar[0].value = ((100 * sent) / total);
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.onerror = (e) => {
|
||||||
|
console.log(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.send(formData);
|
||||||
};
|
};
|
||||||
|
|
||||||
const formConvert = document.querySelector(`form[action='${webroot}/convert']`);
|
const formConvert = document.querySelector(`form[action='${webroot}/convert']`);
|
||||||
|
|||||||
@@ -4,28 +4,32 @@ export const Header = ({
|
|||||||
loggedIn,
|
loggedIn,
|
||||||
accountRegistration,
|
accountRegistration,
|
||||||
allowUnauthenticated,
|
allowUnauthenticated,
|
||||||
|
hideHistory,
|
||||||
webroot = "",
|
webroot = "",
|
||||||
}: {
|
}: {
|
||||||
loggedIn?: boolean;
|
loggedIn?: boolean;
|
||||||
accountRegistration?: boolean;
|
accountRegistration?: boolean;
|
||||||
allowUnauthenticated?: boolean;
|
allowUnauthenticated?: boolean;
|
||||||
|
hideHistory?: boolean;
|
||||||
webroot?: string;
|
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>
|
{!hideHistory && (
|
||||||
<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={`${webroot}/history`}
|
`}
|
||||||
>
|
href={`${webroot}/history`}
|
||||||
History
|
>
|
||||||
</a>
|
History
|
||||||
</li>
|
</a>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
{!allowUnauthenticated ? (
|
{!allowUnauthenticated ? (
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
@@ -74,7 +78,7 @@ 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-neutral-900 p-4">
|
<nav class="mx-auto flex max-w-4xl justify-between rounded-sm bg-neutral-900 p-4">
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<strong>
|
<strong>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { exec } from "node:child_process";
|
import { execFile } from "node:child_process";
|
||||||
|
|
||||||
export const properties = {
|
export const properties = {
|
||||||
from: {
|
from: {
|
||||||
@@ -119,10 +119,8 @@ 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> {
|
||||||
const command = `assimp export "${filePath}" "${targetPath}"`;
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
exec(command, (error, stdout, stderr) => {
|
execFile("assimp", ["export", filePath, targetPath], (error, stdout, stderr) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
reject(`error: ${error}`);
|
reject(`error: ${error}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { exec } from "node:child_process";
|
import { execFile } from "node:child_process";
|
||||||
|
|
||||||
export const properties = {
|
export const properties = {
|
||||||
from: {
|
from: {
|
||||||
@@ -64,10 +64,8 @@ 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> {
|
||||||
const command = `ebook-convert "${filePath}" "${targetPath}"`;
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
exec(command, (error, stdout, stderr) => {
|
execFile("ebook-convert", [filePath, targetPath], (error, stdout, stderr) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
reject(`error: ${error}`);
|
reject(`error: ${error}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { exec } from "node:child_process";
|
import { execFile } from "node:child_process";
|
||||||
|
|
||||||
// This could be done dynamically by running `ffmpeg -formats` and parsing the output
|
// This could be done dynamically by running `ffmpeg -formats` and parsing the output
|
||||||
export const properties = {
|
export const properties = {
|
||||||
@@ -691,19 +691,28 @@ 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 extra = "";
|
let extraArgs: string[] = [];
|
||||||
let message = "Done";
|
let message = "Done";
|
||||||
|
|
||||||
if (convertTo === "ico") {
|
if (convertTo === "ico") {
|
||||||
// make sure image is 256x256 or smaller
|
// make sure image is 256x256 or smaller
|
||||||
extra = `-filter:v "scale='min(256,iw)':min'(256,ih)':force_original_aspect_ratio=decrease"`;
|
extraArgs = ['-filter:v', "scale='min(256,iw)':min'(256,ih)':force_original_aspect_ratio=decrease"];
|
||||||
message = "Done: resized to 256x256";
|
message = "Done: resized to 256x256";
|
||||||
}
|
}
|
||||||
|
|
||||||
const command = `ffmpeg ${process.env.FFMPEG_ARGS || ""} -i "${filePath}" ${extra} "${targetPath}"`;
|
// Parse FFMPEG_ARGS environment variable into array
|
||||||
|
const ffmpegArgs = process.env.FFMPEG_ARGS ? process.env.FFMPEG_ARGS.split(/\s+/) : [];
|
||||||
|
|
||||||
|
// Build arguments array
|
||||||
|
const args = [
|
||||||
|
...ffmpegArgs,
|
||||||
|
"-i", filePath,
|
||||||
|
...extraArgs,
|
||||||
|
targetPath
|
||||||
|
];
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
exec(command, (error, stdout, stderr) => {
|
execFile("ffmpeg", args, (error, stdout, stderr) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
reject(`error: ${error}`);
|
reject(`error: ${error}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { exec } from "node:child_process";
|
import { execFile } from "node:child_process";
|
||||||
|
|
||||||
export const properties = {
|
export const properties = {
|
||||||
from: {
|
from: {
|
||||||
@@ -317,8 +317,9 @@ export function convert(
|
|||||||
options?: unknown,
|
options?: unknown,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
exec(
|
execFile(
|
||||||
`gm convert "${filePath}" "${targetPath}"`,
|
"gm",
|
||||||
|
["convert", filePath, targetPath],
|
||||||
(error, stdout, stderr) => {
|
(error, stdout, stderr) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
reject(`error: ${error}`);
|
reject(`error: ${error}`);
|
||||||
|
|||||||
@@ -1,50 +1,45 @@
|
|||||||
import { exec } from "node:child_process";
|
import { execFile } from "node:child_process";
|
||||||
|
|
||||||
export const properties = {
|
export const properties = {
|
||||||
from: {
|
from: {
|
||||||
images: [
|
images: ["svg", "pdf", "eps", "ps", "wmf", "emf", "png"],
|
||||||
"svg",
|
},
|
||||||
"pdf",
|
to: {
|
||||||
"eps",
|
images: [
|
||||||
"ps",
|
"dxf",
|
||||||
"wmf",
|
"emf",
|
||||||
"emf",
|
"eps",
|
||||||
"png"
|
"fxg",
|
||||||
]
|
"gpl",
|
||||||
},
|
"hpgl",
|
||||||
to: {
|
"html",
|
||||||
images: [
|
"odg",
|
||||||
"dxf",
|
"pdf",
|
||||||
"emf",
|
"png",
|
||||||
"eps",
|
"pov",
|
||||||
"fxg",
|
"ps",
|
||||||
"gpl",
|
"sif",
|
||||||
"hpgl",
|
"svg",
|
||||||
"html",
|
"svgz",
|
||||||
"odg",
|
"tex",
|
||||||
"pdf",
|
"wmf",
|
||||||
"png",
|
],
|
||||||
"pov",
|
},
|
||||||
"ps",
|
};
|
||||||
"sif",
|
|
||||||
"svg",
|
|
||||||
"svgz",
|
|
||||||
"tex",
|
|
||||||
"wmf",
|
|
||||||
]
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export function convert(
|
export function convert(
|
||||||
filePath: string,
|
filePath: string,
|
||||||
fileType: string,
|
fileType: string,
|
||||||
convertTo: string,
|
convertTo: string,
|
||||||
targetPath: string,
|
targetPath: string,
|
||||||
// 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> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
exec(`inkscape "${filePath}" -o "${targetPath}"`, (error, stdout, stderr) => {
|
execFile(
|
||||||
|
"inkscape",
|
||||||
|
[filePath, "-o", targetPath],
|
||||||
|
(error, stdout, stderr) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
reject(`error: ${error}`);
|
reject(`error: ${error}`);
|
||||||
}
|
}
|
||||||
@@ -58,7 +53,7 @@ export const properties = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resolve("Done");
|
resolve("Done");
|
||||||
});
|
},
|
||||||
});
|
);
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
|||||||
53
src/converters/libheif.ts
Normal file
53
src/converters/libheif.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { execFile } from "child_process";
|
||||||
|
|
||||||
|
export const properties = {
|
||||||
|
from: {
|
||||||
|
images: [
|
||||||
|
"avci",
|
||||||
|
"avcs",
|
||||||
|
"avif",
|
||||||
|
"h264",
|
||||||
|
"heic",
|
||||||
|
"heics",
|
||||||
|
"heif",
|
||||||
|
"heifs",
|
||||||
|
"hif",
|
||||||
|
"mkv",
|
||||||
|
"mp4",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
images: ["jpeg", "png", "y4m"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function convert(
|
||||||
|
filePath: string,
|
||||||
|
fileType: string,
|
||||||
|
convertTo: string,
|
||||||
|
targetPath: string,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
options?: unknown,
|
||||||
|
): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
execFile(
|
||||||
|
"heif-convert",
|
||||||
|
[filePath, targetPath],
|
||||||
|
(error, stdout, stderr) => {
|
||||||
|
if (error) {
|
||||||
|
reject(`error: ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stdout) {
|
||||||
|
console.log(`stdout: ${stdout}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stderr) {
|
||||||
|
console.error(`stderr: ${stderr}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve("Done");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { exec } from "node:child_process";
|
import { execFile } from "node:child_process";
|
||||||
|
|
||||||
// declare possible conversions
|
// declare possible conversions
|
||||||
export const properties = {
|
export const properties = {
|
||||||
@@ -52,7 +52,7 @@ export function convert(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
exec(`${tool} "${filePath}" "${targetPath}"`, (error, stdout, stderr) => {
|
execFile(tool, [filePath, targetPath], (error, stdout, stderr) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
reject(`error: ${error}`);
|
reject(`error: ${error}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import { convert as convertPandoc, properties as propertiesPandoc } from "./pand
|
|||||||
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";
|
// import { convert as convertCalibre, properties as propertiesCalibre } from "./calibre";
|
||||||
|
import { convert as convertLibheif, properties as propertiesLibheif } from "./libheif";
|
||||||
|
import { convert as convertpotrace, properties as propertiespotrace } from "./potrace";
|
||||||
|
|
||||||
|
|
||||||
// This should probably be reconstructed so that the functions are not imported instead the functions hook into this to make the converters more modular
|
// This should probably be reconstructed so that the functions are not imported instead the functions hook into this to make the converters more modular
|
||||||
@@ -53,14 +55,18 @@ const properties: Record<
|
|||||||
properties: propertiesImage,
|
properties: propertiesImage,
|
||||||
converter: convertImage,
|
converter: convertImage,
|
||||||
},
|
},
|
||||||
|
libheif: {
|
||||||
|
properties: propertiesLibheif,
|
||||||
|
converter: convertLibheif,
|
||||||
|
},
|
||||||
xelatex: {
|
xelatex: {
|
||||||
properties: propertiesxelatex,
|
properties: propertiesxelatex,
|
||||||
converter: convertxelatex,
|
converter: convertxelatex,
|
||||||
},
|
},
|
||||||
calibre: {
|
// calibre: {
|
||||||
properties: propertiesCalibre,
|
// properties: propertiesCalibre,
|
||||||
converter: convertCalibre,
|
// converter: convertCalibre,
|
||||||
},
|
// },
|
||||||
pandoc: {
|
pandoc: {
|
||||||
properties: propertiesPandoc,
|
properties: propertiesPandoc,
|
||||||
converter: convertPandoc,
|
converter: convertPandoc,
|
||||||
@@ -81,6 +87,10 @@ const properties: Record<
|
|||||||
properties: propertiesFFmpeg,
|
properties: propertiesFFmpeg,
|
||||||
converter: convertFFmpeg,
|
converter: convertFFmpeg,
|
||||||
},
|
},
|
||||||
|
potrace: {
|
||||||
|
properties: propertiespotrace,
|
||||||
|
converter: convertpotrace,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function mainConverter(
|
export async function mainConverter(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { exec } from "node:child_process";
|
import { execFile } from "node:child_process";
|
||||||
|
|
||||||
export const properties = {
|
export const properties = {
|
||||||
from: {
|
from: {
|
||||||
@@ -129,28 +129,34 @@ export function convert(
|
|||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
// set xelatex here
|
// set xelatex here
|
||||||
const xelatex = ["pdf", "latex"];
|
const xelatex = ["pdf", "latex"];
|
||||||
let option = "";
|
|
||||||
|
// Build arguments array
|
||||||
|
const args: string[] = [];
|
||||||
|
|
||||||
if (xelatex.includes(convertTo)) {
|
if (xelatex.includes(convertTo)) {
|
||||||
option = "--pdf-engine=xelatex";
|
args.push("--pdf-engine=xelatex");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
args.push(filePath);
|
||||||
|
args.push("-f", fileType);
|
||||||
|
args.push("-t", convertTo);
|
||||||
|
args.push("-o", targetPath);
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
exec(
|
execFile("pandoc", args, (error, stdout, stderr) => {
|
||||||
`pandoc ${option} "${filePath}" -f ${fileType} -t ${convertTo} -o "${targetPath}"`,
|
if (error) {
|
||||||
(error, stdout, stderr) => {
|
reject(`error: ${error}`);
|
||||||
if (error) {
|
}
|
||||||
reject(`error: ${error}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stdout) {
|
if (stdout) {
|
||||||
console.log(`stdout: ${stdout}`);
|
console.log(`stdout: ${stdout}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stderr) {
|
if (stderr) {
|
||||||
console.error(`stderr: ${stderr}`);
|
console.error(`stderr: ${stderr}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve("Done");
|
resolve("Done");
|
||||||
},
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
37
src/converters/potrace.ts
Normal file
37
src/converters/potrace.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { execFile } from "node:child_process";
|
||||||
|
|
||||||
|
export const properties = {
|
||||||
|
from: {
|
||||||
|
images: ["pnm", "pbm", "pgm", "bmp"],
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
images: ["svg", "pdf", "pdfpage", "eps", "postscript", "ps", "dxf", "geojson", "pgm", "gimppath", "xfig"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function convert(
|
||||||
|
filePath: string,
|
||||||
|
fileType: string,
|
||||||
|
convertTo: string,
|
||||||
|
targetPath: string,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
options?: unknown,
|
||||||
|
): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
execFile("potrace", [filePath, "-o", targetPath, "-b", convertTo], (error, stdout, stderr) => {
|
||||||
|
if (error) {
|
||||||
|
reject(`error: ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stdout) {
|
||||||
|
console.log(`stdout: ${stdout}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stderr) {
|
||||||
|
console.error(`stderr: ${stderr}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve("Done");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { exec } from "node:child_process";
|
import { execFile } from "node:child_process";
|
||||||
|
|
||||||
export const properties = {
|
export const properties = {
|
||||||
from: {
|
from: {
|
||||||
@@ -18,7 +18,7 @@ export function convert(
|
|||||||
options?: unknown,
|
options?: unknown,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
exec(`resvg "${filePath}" "${targetPath}"`, (error, stdout, stderr) => {
|
execFile("resvg", [filePath, targetPath], (error, stdout, stderr) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
reject(`error: ${error}`);
|
reject(`error: ${error}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { exec } from "node:child_process";
|
import { execFile } from "node:child_process";
|
||||||
|
|
||||||
|
|
||||||
// declare possible conversions
|
// declare possible conversions
|
||||||
export const properties = {
|
export const properties = {
|
||||||
@@ -120,8 +119,9 @@ export function convert(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
exec(
|
execFile(
|
||||||
`vips ${action} "${filePath}" "${targetPath}"`,
|
"vips",
|
||||||
|
[action, filePath, targetPath],
|
||||||
(error, stdout, stderr) => {
|
(error, stdout, stderr) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
reject(`error: ${error}`);
|
reject(`error: ${error}`);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { exec } from "node:child_process";
|
import { execFile } from "node:child_process";
|
||||||
|
|
||||||
export const properties = {
|
export const properties = {
|
||||||
from: {
|
from: {
|
||||||
@@ -24,8 +24,15 @@ export function convert(
|
|||||||
.slice(0, -1)
|
.slice(0, -1)
|
||||||
.join("/")
|
.join("/")
|
||||||
.replace("./", "");
|
.replace("./", "");
|
||||||
exec(
|
|
||||||
`latexmk -xelatex -interaction=nonstopmode -output-directory="${outputPath}" "${filePath}"`,
|
execFile(
|
||||||
|
"latexmk",
|
||||||
|
[
|
||||||
|
"-xelatex",
|
||||||
|
"-interaction=nonstopmode",
|
||||||
|
`-output-directory=${outputPath}`,
|
||||||
|
filePath,
|
||||||
|
],
|
||||||
(error, stdout, stderr) => {
|
(error, stdout, stderr) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
reject(`error: ${error}`);
|
reject(`error: ${error}`);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { exec } from "node:child_process";
|
import { exec } from "node:child_process";
|
||||||
import { version } from "../../package.json";
|
import { version } from "../../package.json";
|
||||||
|
|
||||||
console.log(`ConvertX v${version}`);
|
console.log(`ConvertX v${version}`);
|
||||||
|
|
||||||
if (process.env.NODE_ENV === "production") {
|
if (process.env.NODE_ENV === "production") {
|
||||||
@@ -99,7 +100,7 @@ if (process.env.NODE_ENV === "production") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (stdout) {
|
if (stdout) {
|
||||||
console.log(`assimp v${stdout.split("\n")[5]}`);
|
console.log(`assimp ${stdout.split("\n")[5]}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -113,6 +114,26 @@ if (process.env.NODE_ENV === "production") {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
exec("heif-info -v", (error, stdout) => {
|
||||||
|
if (error) {
|
||||||
|
console.error("libheif is not installed");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stdout) {
|
||||||
|
console.log(`libheif v${stdout.split("\n")[0]}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
exec("potrace -v", (error, stdout) => {
|
||||||
|
if (error) {
|
||||||
|
console.error("potrace 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");
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import tw from "tailwindcss";
|
import tailwind from "@tailwindcss/postcss";
|
||||||
import postcss from "postcss";
|
import postcss from "postcss";
|
||||||
|
|
||||||
export const generateTailwind = async () => {
|
export const generateTailwind = async () => {
|
||||||
const result = await Bun.file("./src/main.css")
|
const result = await Bun.file("./src/main.css")
|
||||||
.text()
|
.text()
|
||||||
.then((sourceText) => {
|
.then((sourceText) => {
|
||||||
const config = "./tailwind.config.js";
|
return postcss([tailwind]).process(sourceText, {
|
||||||
|
|
||||||
return postcss([tw(config)]).process(sourceText, {
|
|
||||||
from: "./src/main.css",
|
from: "./src/main.css",
|
||||||
to: "./public/generated.css",
|
to: "./public/generated.css",
|
||||||
});
|
});
|
||||||
|
|||||||
269
src/index.tsx
269
src/index.tsx
@@ -1,12 +1,12 @@
|
|||||||
import { randomInt, randomUUID } from "node:crypto";
|
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 { html, 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";
|
||||||
import { Elysia, t } from "elysia";
|
import { Elysia, t } from "elysia";
|
||||||
|
import sanitize from "sanitize-filename";
|
||||||
import { BaseHtml } from "./components/base";
|
import { BaseHtml } from "./components/base";
|
||||||
import { Header } from "./components/header";
|
import { Header } from "./components/header";
|
||||||
import {
|
import {
|
||||||
@@ -36,6 +36,8 @@ const ALLOW_UNAUTHENTICATED =
|
|||||||
const AUTO_DELETE_EVERY_N_HOURS = process.env.AUTO_DELETE_EVERY_N_HOURS
|
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 HIDE_HISTORY =
|
||||||
|
process.env.HIDE_HISTORY?.toLowerCase() === "true" || false;
|
||||||
|
|
||||||
const WEBROOT = process.env.WEBROOT ?? "";
|
const WEBROOT = process.env.WEBROOT ?? "";
|
||||||
|
|
||||||
@@ -116,7 +118,6 @@ const app = new Elysia({
|
|||||||
},
|
},
|
||||||
prefix: WEBROOT,
|
prefix: WEBROOT,
|
||||||
})
|
})
|
||||||
.use(cookie())
|
|
||||||
.use(html())
|
.use(html())
|
||||||
.use(
|
.use(
|
||||||
jwt({
|
jwt({
|
||||||
@@ -153,7 +154,12 @@ const app = new Elysia({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseHtml title="ConvertX | Setup" webroot={WEBROOT}>
|
<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-2
|
||||||
|
sm: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">
|
||||||
@@ -166,7 +172,7 @@ const app = new Elysia({
|
|||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
name="email"
|
name="email"
|
||||||
class="rounded bg-neutral-800 p-3"
|
class="rounded-sm bg-neutral-800 p-3"
|
||||||
placeholder="Email"
|
placeholder="Email"
|
||||||
autocomplete="email"
|
autocomplete="email"
|
||||||
required
|
required
|
||||||
@@ -177,7 +183,7 @@ const app = new Elysia({
|
|||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
name="password"
|
name="password"
|
||||||
class="rounded bg-neutral-800 p-3"
|
class="rounded-sm bg-neutral-800 p-3"
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
autocomplete="current-password"
|
autocomplete="current-password"
|
||||||
required
|
required
|
||||||
@@ -217,7 +223,12 @@ const app = new Elysia({
|
|||||||
accountRegistration={ACCOUNT_REGISTRATION}
|
accountRegistration={ACCOUNT_REGISTRATION}
|
||||||
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
|
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
|
||||||
/>
|
/>
|
||||||
<main class="w-full px-4">
|
<main
|
||||||
|
class={`
|
||||||
|
w-full px-2
|
||||||
|
sm: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">
|
||||||
<fieldset class="mb-4 flex flex-col gap-4">
|
<fieldset class="mb-4 flex flex-col gap-4">
|
||||||
@@ -226,7 +237,7 @@ const app = new Elysia({
|
|||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
name="email"
|
name="email"
|
||||||
class="rounded bg-neutral-800 p-3"
|
class="rounded-sm bg-neutral-800 p-3"
|
||||||
placeholder="Email"
|
placeholder="Email"
|
||||||
autocomplete="email"
|
autocomplete="email"
|
||||||
required
|
required
|
||||||
@@ -237,7 +248,7 @@ const app = new Elysia({
|
|||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
name="password"
|
name="password"
|
||||||
class="rounded bg-neutral-800 p-3"
|
class="rounded-sm bg-neutral-800 p-3"
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
autocomplete="current-password"
|
autocomplete="current-password"
|
||||||
required
|
required
|
||||||
@@ -342,8 +353,14 @@ const app = new Elysia({
|
|||||||
webroot={WEBROOT}
|
webroot={WEBROOT}
|
||||||
accountRegistration={ACCOUNT_REGISTRATION}
|
accountRegistration={ACCOUNT_REGISTRATION}
|
||||||
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
|
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
|
||||||
|
hideHistory={HIDE_HISTORY}
|
||||||
/>
|
/>
|
||||||
<main class="w-full px-4">
|
<main
|
||||||
|
class={`
|
||||||
|
w-full px-2
|
||||||
|
sm: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">
|
||||||
<fieldset class="mb-4 flex flex-col gap-4">
|
<fieldset class="mb-4 flex flex-col gap-4">
|
||||||
@@ -352,7 +369,7 @@ const app = new Elysia({
|
|||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
name="email"
|
name="email"
|
||||||
class="rounded bg-neutral-800 p-3"
|
class="rounded-sm bg-neutral-800 p-3"
|
||||||
placeholder="Email"
|
placeholder="Email"
|
||||||
autocomplete="email"
|
autocomplete="email"
|
||||||
required
|
required
|
||||||
@@ -363,7 +380,7 @@ const app = new Elysia({
|
|||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
name="password"
|
name="password"
|
||||||
class="rounded bg-neutral-800 p-3"
|
class="rounded-sm bg-neutral-800 p-3"
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
autocomplete="current-password"
|
autocomplete="current-password"
|
||||||
required
|
required
|
||||||
@@ -556,16 +573,21 @@ const app = new Elysia({
|
|||||||
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
|
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
|
||||||
loggedIn
|
loggedIn
|
||||||
/>
|
/>
|
||||||
<main class="w-full px-4">
|
<main
|
||||||
|
class={`
|
||||||
|
w-full px-2
|
||||||
|
sm: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="scrollbar-thin mb-4 max-h-[50vh] overflow-y-auto">
|
||||||
<table
|
<table
|
||||||
id="file-list"
|
id="file-list"
|
||||||
class={`
|
class={`
|
||||||
w-full table-auto rounded bg-neutral-900
|
w-full table-auto rounded bg-neutral-900
|
||||||
[&_td]:p-4
|
[&_td]:p-4 [&_td]:first:max-w-[30vw] [&_td]:first:truncate
|
||||||
[&_tr]:rounded [&_tr]:border-b [&_tr]:border-neutral-800
|
[&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
|
||||||
`}
|
`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -574,8 +596,8 @@ const app = new Elysia({
|
|||||||
class={`
|
class={`
|
||||||
relative flex h-48 w-full items-center justify-center rounded border border-dashed
|
relative flex h-48 w-full items-center justify-center rounded border border-dashed
|
||||||
border-neutral-700 transition-all
|
border-neutral-700 transition-all
|
||||||
[&.dragover]:border-4 [&.dragover]:border-neutral-500
|
|
||||||
hover:border-neutral-600
|
hover:border-neutral-600
|
||||||
|
[&.dragover]:border-4 [&.dragover]:border-neutral-500
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
@@ -601,13 +623,13 @@ 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-neutral-800 p-4"
|
class="w-full rounded-sm bg-neutral-800 p-4"
|
||||||
/>
|
/>
|
||||||
<div class="select_container relative">
|
<div class="select_container relative">
|
||||||
<article
|
<article
|
||||||
class={`
|
class={`
|
||||||
convert_to_popup absolute z-[2] m-0 hidden h-[30vh] max-h-[50vh] w-full
|
convert_to_popup absolute z-2 m-0 hidden h-[30vh] max-h-[50vh] w-full flex-col
|
||||||
flex-col overflow-y-auto overflow-x-hidden rounded bg-neutral-800
|
overflow-x-hidden overflow-y-auto rounded bg-neutral-800
|
||||||
sm:h-[30vh]
|
sm:h-[30vh]
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
@@ -672,7 +694,7 @@ const app = new Elysia({
|
|||||||
</article>
|
</article>
|
||||||
<input
|
<input
|
||||||
class={`
|
class={`
|
||||||
btn-primary w-full
|
btn-primary w-full opacity-100
|
||||||
disabled:cursor-not-allowed disabled:opacity-50
|
disabled:cursor-not-allowed disabled:opacity-50
|
||||||
`}
|
`}
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -693,8 +715,8 @@ const app = new Elysia({
|
|||||||
<>
|
<>
|
||||||
<article
|
<article
|
||||||
class={`
|
class={`
|
||||||
convert_to_popup absolute z-[2] m-0 hidden h-[50vh] max-h-[50vh] w-full flex-col
|
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
|
overflow-x-hidden overflow-y-auto rounded bg-neutral-800
|
||||||
sm:h-[30vh]
|
sm:h-[30vh]
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
@@ -866,6 +888,10 @@ const app = new Elysia({
|
|||||||
const converterName = body.convert_to.split(",")[1];
|
const converterName = body.convert_to.split(",")[1];
|
||||||
const fileNames = JSON.parse(body.file_names) as string[];
|
const fileNames = JSON.parse(body.file_names) as string[];
|
||||||
|
|
||||||
|
for (let i = 0; i < fileNames.length; i++) {
|
||||||
|
fileNames[i] = sanitize(fileNames[i] || "");
|
||||||
|
}
|
||||||
|
|
||||||
if (!Array.isArray(fileNames) || fileNames.length === 0) {
|
if (!Array.isArray(fileNames) || fileNames.length === 0) {
|
||||||
return redirect(`${WEBROOT}/`, 302);
|
return redirect(`${WEBROOT}/`, 302);
|
||||||
}
|
}
|
||||||
@@ -930,6 +956,10 @@ const app = new Elysia({
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.get("/history", async ({ jwt, redirect, cookie: { auth } }) => {
|
.get("/history", async ({ jwt, redirect, cookie: { auth } }) => {
|
||||||
|
if (HIDE_HISTORY) {
|
||||||
|
return redirect(`${WEBROOT}/`, 302);
|
||||||
|
}
|
||||||
|
|
||||||
if (!auth?.value) {
|
if (!auth?.value) {
|
||||||
return redirect(`${WEBROOT}/login`, 302);
|
return redirect(`${WEBROOT}/login`, 302);
|
||||||
}
|
}
|
||||||
@@ -942,7 +972,8 @@ const app = new Elysia({
|
|||||||
let userJobs = db
|
let userJobs = db
|
||||||
.query("SELECT * FROM jobs WHERE user_id = ?")
|
.query("SELECT * FROM jobs WHERE user_id = ?")
|
||||||
.as(Jobs)
|
.as(Jobs)
|
||||||
.all(user.id);
|
.all(user.id)
|
||||||
|
.reverse();
|
||||||
|
|
||||||
for (const job of userJobs) {
|
for (const job of userJobs) {
|
||||||
const files = db
|
const files = db
|
||||||
@@ -962,33 +993,77 @@ const app = new Elysia({
|
|||||||
<Header
|
<Header
|
||||||
webroot={WEBROOT}
|
webroot={WEBROOT}
|
||||||
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
|
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
|
||||||
|
hideHistory={HIDE_HISTORY}
|
||||||
loggedIn
|
loggedIn
|
||||||
/>
|
/>
|
||||||
<main class="w-full px-4">
|
<main
|
||||||
|
class={`
|
||||||
|
w-full px-2
|
||||||
|
sm: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
|
<table
|
||||||
class={`
|
class={`
|
||||||
w-full table-auto rounded bg-neutral-900 text-left
|
w-full table-auto overflow-y-auto rounded bg-neutral-900 text-left
|
||||||
[&_td]:p-4
|
[&_td]:p-4
|
||||||
[&_tr]:rounded [&_tr]:border-b [&_tr]:border-neutral-800
|
[&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-4 py-2">Time</th>
|
<th
|
||||||
<th class="px-4 py-2">Files</th>
|
class={`
|
||||||
<th class="px-4 py-2">Files Done</th>
|
px-2 py-2
|
||||||
<th class="px-4 py-2">Status</th>
|
sm:px-4
|
||||||
<th class="px-4 py-2">View</th>
|
`}
|
||||||
|
>
|
||||||
|
Time
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class={`
|
||||||
|
px-2 py-2
|
||||||
|
sm:px-4
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
Files
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class={`
|
||||||
|
px-2 py-2
|
||||||
|
max-sm:hidden
|
||||||
|
sm:px-4
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
Files Done
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class={`
|
||||||
|
px-2 py-2
|
||||||
|
sm:px-4
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
Status
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class={`
|
||||||
|
px-2 py-2
|
||||||
|
sm:px-4
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{userJobs.map((job) => (
|
{userJobs.map((job) => (
|
||||||
<tr>
|
<tr>
|
||||||
<td safe>{job.date_created}</td>
|
<td safe>
|
||||||
|
{new Date(job.date_created).toLocaleTimeString()}
|
||||||
|
</td>
|
||||||
<td>{job.num_files}</td>
|
<td>{job.num_files}</td>
|
||||||
<td>{job.finished_files}</td>
|
<td class="max-sm:hidden">{job.finished_files}</td>
|
||||||
<td safe>{job.status}</td>
|
<td safe>{job.status}</td>
|
||||||
<td>
|
<td>
|
||||||
<a
|
<a
|
||||||
@@ -1055,7 +1130,12 @@ const app = new Elysia({
|
|||||||
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
|
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
|
||||||
loggedIn
|
loggedIn
|
||||||
/>
|
/>
|
||||||
<main class="w-full px-4">
|
<main
|
||||||
|
class={`
|
||||||
|
w-full px-2
|
||||||
|
sm: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">
|
||||||
<h1 class="text-xl">Results</h1>
|
<h1 class="text-xl">Results</h1>
|
||||||
@@ -1078,33 +1158,63 @@ const app = new Elysia({
|
|||||||
max={job.num_files}
|
max={job.num_files}
|
||||||
value={files.length}
|
value={files.length}
|
||||||
class={`
|
class={`
|
||||||
mb-4 inline-block h-2 w-full appearance-none overflow-hidden rounded-full
|
text-accent-500 accent-accent-500 mb-4 inline-block h-2 w-full appearance-none
|
||||||
border-0 bg-neutral-700 bg-none text-accent-500 accent-accent-500
|
overflow-hidden rounded-full border-0 bg-neutral-700 bg-none
|
||||||
[&::-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]:bg-accent-500
|
||||||
[&[value]::-webkit-progress-value]:transition-[inline-size]
|
[&[value]::-webkit-progress-value]:transition-[inline-size]
|
||||||
|
[&::-moz-progress-bar]:bg-neutral-700 [&::-webkit-progress-value]:rounded-full
|
||||||
|
[&::-webkit-progress-value]:[background:none]
|
||||||
`}
|
`}
|
||||||
/>
|
/>
|
||||||
<table
|
<table
|
||||||
class={`
|
class={`
|
||||||
w-full table-auto rounded bg-neutral-900 text-left
|
w-full table-auto rounded bg-neutral-900 text-left
|
||||||
[&_td]:p-4
|
[&_td]:p-4
|
||||||
[&_tr]:rounded [&_tr]:border-b [&_tr]:border-neutral-800
|
[&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-4 py-2">Converted File Name</th>
|
<th
|
||||||
<th class="px-4 py-2">Status</th>
|
class={`
|
||||||
<th class="px-4 py-2">View</th>
|
px-2 py-2
|
||||||
<th class="px-4 py-2">Download</th>
|
sm:px-4
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
Converted File Name
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class={`
|
||||||
|
px-2 py-2
|
||||||
|
sm:px-4
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
Status
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class={`
|
||||||
|
px-2 py-2
|
||||||
|
sm:px-4
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class={`
|
||||||
|
px-2 py-2
|
||||||
|
sm:px-4
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{files.map((file) => (
|
{files.map((file) => (
|
||||||
<tr>
|
<tr>
|
||||||
<td safe>{file.output_file_name}</td>
|
<td safe class="max-w-[20vw] truncate">
|
||||||
|
{file.output_file_name}
|
||||||
|
</td>
|
||||||
<td safe>{file.status}</td>
|
<td safe>{file.status}</td>
|
||||||
<td>
|
<td>
|
||||||
<a
|
<a
|
||||||
@@ -1200,33 +1310,63 @@ const app = new Elysia({
|
|||||||
max={job.num_files}
|
max={job.num_files}
|
||||||
value={files.length}
|
value={files.length}
|
||||||
class={`
|
class={`
|
||||||
mb-4 inline-block h-2 w-full appearance-none overflow-hidden rounded-full border-0
|
text-accent-500 accent-accent-500 mb-4 inline-block h-2 w-full appearance-none
|
||||||
bg-neutral-700 bg-none text-accent-500 accent-accent-500
|
overflow-hidden rounded-full border-0 bg-neutral-700 bg-none
|
||||||
[&::-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]:bg-accent-500
|
||||||
[&[value]::-webkit-progress-value]:transition-[inline-size]
|
[&[value]::-webkit-progress-value]:transition-[inline-size]
|
||||||
|
[&::-moz-progress-bar]:bg-neutral-700 [&::-webkit-progress-value]:rounded-full
|
||||||
|
[&::-webkit-progress-value]:[background:none]
|
||||||
`}
|
`}
|
||||||
/>
|
/>
|
||||||
<table
|
<table
|
||||||
class={`
|
class={`
|
||||||
w-full table-auto rounded bg-neutral-900 text-left
|
w-full table-auto rounded bg-neutral-900 text-left
|
||||||
[&_td]:p-4
|
[&_td]:p-4
|
||||||
[&_tr]:rounded [&_tr]:border-b [&_tr]:border-neutral-800
|
[&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-4 py-2">Converted File Name</th>
|
<th
|
||||||
<th class="px-4 py-2">Status</th>
|
class={`
|
||||||
<th class="px-4 py-2">View</th>
|
px-2 py-2
|
||||||
<th class="px-4 py-2">Download</th>
|
sm:px-4
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
Converted File Name
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class={`
|
||||||
|
px-2 py-2
|
||||||
|
sm:px-4
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
Status
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class={`
|
||||||
|
px-2 py-2
|
||||||
|
sm:px-4
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class={`
|
||||||
|
px-2 py-2
|
||||||
|
sm:px-4
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{files.map((file) => (
|
{files.map((file) => (
|
||||||
<tr>
|
<tr>
|
||||||
<td safe>{file.output_file_name}</td>
|
<td safe class="max-w-[20vw] truncate">
|
||||||
|
{file.output_file_name}
|
||||||
|
</td>
|
||||||
<td safe>{file.status}</td>
|
<td safe>{file.status}</td>
|
||||||
<td>
|
<td>
|
||||||
<a
|
<a
|
||||||
@@ -1281,7 +1421,7 @@ const app = new Elysia({
|
|||||||
// parse from url encoded string
|
// parse from url encoded string
|
||||||
const userId = decodeURIComponent(params.userId);
|
const userId = decodeURIComponent(params.userId);
|
||||||
const jobId = decodeURIComponent(params.jobId);
|
const jobId = decodeURIComponent(params.jobId);
|
||||||
const fileName = decodeURIComponent(params.fileName);
|
const fileName = sanitize(decodeURIComponent(params.fileName));
|
||||||
|
|
||||||
const filePath = `${outputDir}${userId}/${jobId}/${fileName}`;
|
const filePath = `${outputDir}${userId}/${jobId}/${fileName}`;
|
||||||
return Bun.file(filePath);
|
return Bun.file(filePath);
|
||||||
@@ -1305,14 +1445,19 @@ const app = new Elysia({
|
|||||||
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
|
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
|
||||||
loggedIn
|
loggedIn
|
||||||
/>
|
/>
|
||||||
<main class="w-full px-4">
|
<main
|
||||||
|
class={`
|
||||||
|
w-full px-2
|
||||||
|
sm: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
|
<table
|
||||||
class={`
|
class={`
|
||||||
w-full table-auto rounded bg-neutral-900 text-left
|
w-full table-auto rounded bg-neutral-900 text-left
|
||||||
[&_td]:p-4
|
[&_td]:p-4
|
||||||
[&_tr]:rounded [&_tr]:border-b [&_tr]:border-neutral-800
|
[&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
|
||||||
[&_ul]:list-inside [&_ul]:list-disc
|
[&_ul]:list-inside [&_ul]:list-disc
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
@@ -1420,8 +1565,14 @@ const clearJobs = () => {
|
|||||||
|
|
||||||
for (const job of jobs) {
|
for (const job of jobs) {
|
||||||
// delete the directories
|
// delete the directories
|
||||||
rmSync(`${outputDir}${job.user_id}/${job.id}`, { recursive: true });
|
rmSync(`${outputDir}${job.user_id}/${job.id}`, {
|
||||||
rmSync(`${uploadsDir}${job.user_id}/${job.id}`, { recursive: true });
|
recursive: true,
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
rmSync(`${uploadsDir}${job.user_id}/${job.id}`, {
|
||||||
|
recursive: true,
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
|
||||||
// delete the job
|
// delete the job
|
||||||
db.query("DELETE FROM jobs WHERE id = ?").run(job.id);
|
db.query("DELETE FROM jobs WHERE id = ?").run(job.id);
|
||||||
|
|||||||
79
src/main.css
79
src/main.css
@@ -1,31 +1,64 @@
|
|||||||
@tailwind base;
|
@import "tailwindcss";
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
@layer components {
|
@plugin 'tailwind-scrollbar';
|
||||||
.article {
|
|
||||||
@apply p-4 mb-4 bg-neutral-800/40 w-full mx-auto max-w-4xl rounded;
|
@theme {
|
||||||
}
|
--color-contrast: rgba(var(--contrast));
|
||||||
.btn-primary {
|
--color-neutral-900: rgba(var(--neutral-900));
|
||||||
@apply bg-accent-500 text-contrast rounded p-4 hover:bg-accent-400 cursor-pointer transition-colors;
|
--color-neutral-800: rgba(var(--neutral-800));
|
||||||
|
--color-neutral-700: rgba(var(--neutral-700));
|
||||||
|
--color-neutral-600: rgba(var(--neutral-600));
|
||||||
|
--color-neutral-500: rgba(var(--neutral-500));
|
||||||
|
--color-neutral-400: rgba(var(--neutral-400));
|
||||||
|
--color-neutral-300: rgba(var(--neutral-300));
|
||||||
|
--color-neutral-200: rgba(var(--neutral-200));
|
||||||
|
--color-neutral-100: rgba(var(--neutral-100));
|
||||||
|
--color-accent-600: rgba(var(--accent-600));
|
||||||
|
--color-accent-500: rgba(var(--accent-500));
|
||||||
|
--color-accent-400: rgba(var(--accent-400));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
The default border color has changed to `currentColor` in Tailwind CSS v4,
|
||||||
|
so we've added these compatibility styles to make sure everything still
|
||||||
|
looks the same as it did with Tailwind CSS v3.
|
||||||
|
|
||||||
|
If we ever want to remove these styles, we need to add an explicit border
|
||||||
|
color utility to any element that depends on these defaults.
|
||||||
|
*/
|
||||||
|
@layer base {
|
||||||
|
*,
|
||||||
|
::after,
|
||||||
|
::before,
|
||||||
|
::backdrop,
|
||||||
|
::file-selector-button {
|
||||||
|
border-color: var(--color-gray-200, currentColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@utility article {
|
||||||
|
@apply px-2 sm:px-4 py-4 mb-4 bg-neutral-800/40 w-full mx-auto max-w-4xl rounded-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility btn-primary {
|
||||||
|
@apply bg-accent-500 text-contrast rounded-sm p-2 sm:p-4 hover:bg-accent-400 cursor-pointer transition-colors;
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--contrast: 255, 255, 255;
|
--contrast: 255, 255, 255;
|
||||||
--neutral-900: 243, 244, 246;
|
--neutral-900: 243, 244, 246;
|
||||||
--neutral-800: 229, 231, 235;
|
--neutral-800: 229, 231, 235;
|
||||||
--neutral-700: 209, 213, 219;
|
--neutral-700: 209, 213, 219;
|
||||||
--neutral-600: 156, 163, 175;
|
--neutral-600: 156, 163, 175;
|
||||||
--neutral-500: 180, 180, 180;
|
--neutral-500: 180, 180, 180;
|
||||||
--neutral-400: 75, 85, 99;
|
--neutral-400: 75, 85, 99;
|
||||||
--neutral-300: 55, 65, 81;
|
--neutral-300: 55, 65, 81;
|
||||||
--neutral-200: 31, 41, 55;
|
--neutral-200: 31, 41, 55;
|
||||||
--neutral-100: 17, 24, 39;
|
--neutral-100: 17, 24, 39;
|
||||||
--accent-400: 132, 204, 22;
|
--accent-400: 132, 204, 22;
|
||||||
--accent-500: 101, 163, 13;
|
--accent-500: 101, 163, 13;
|
||||||
--accent-600: 77, 124, 15;
|
--accent-600: 77, 124, 15;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
:root {
|
:root {
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
|
|
||||||
import tailwindScrollbar from "tailwind-scrollbar";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
content: ["./src/**/*.{html,js,tsx,jsx,cjs,mjs}"],
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
colors: {
|
|
||||||
contrast: "rgba(var(--contrast))",
|
|
||||||
"neutral-900": "rgba(var(--neutral-900))",
|
|
||||||
"neutral-800": "rgba(var(--neutral-800))",
|
|
||||||
"neutral-700": "rgba(var(--neutral-700))",
|
|
||||||
"neutral-600": "rgba(var(--neutral-600))",
|
|
||||||
"neutral-500": "rgba(var(--neutral-500))",
|
|
||||||
"neutral-400": "rgba(var(--neutral-400))",
|
|
||||||
"neutral-300": "rgba(var(--neutral-300))",
|
|
||||||
"neutral-200": "rgba(var(--neutral-200))",
|
|
||||||
"neutral-100": "rgba(var(--neutral-100))",
|
|
||||||
"accent-600": "rgba(var(--accent-600))",
|
|
||||||
"accent-500": "rgba(var(--accent-500))",
|
|
||||||
"accent-400": "rgba(var(--accent-400))",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [tailwindScrollbar],
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user