mirror of
https://github.com/C4illin/ConvertX.git
synced 2025-10-24 08:33:56 +00:00
Compare commits
111 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bba420386f | ||
|
|
74df47531c | ||
|
|
1adac8c31c | ||
|
|
0579f1852b | ||
|
|
52d4cc0d03 | ||
|
|
2c68016ca6 | ||
|
|
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 | ||
|
|
961a55cbe5 | ||
|
|
cdf9bad903 | ||
|
|
6769fa2f83 | ||
|
|
3b7ea88b73 |
@@ -1,20 +1,19 @@
|
||||
.dockerignore
|
||||
.editorconfig
|
||||
.env
|
||||
.git
|
||||
.gitignore
|
||||
.idea
|
||||
.vscode
|
||||
CHANGELOG.md
|
||||
coverage*
|
||||
data
|
||||
docker-compose*
|
||||
Dockerfile*
|
||||
eslint.config.js
|
||||
helm-charts
|
||||
LICENSE
|
||||
Makefile
|
||||
node_modules
|
||||
prettier.config.js
|
||||
README.md
|
||||
.dockerignore
|
||||
.editorconfig
|
||||
.env
|
||||
.git
|
||||
.idea
|
||||
.vscode
|
||||
CHANGELOG.md
|
||||
coverage*
|
||||
data
|
||||
docker-compose*
|
||||
Dockerfile*
|
||||
eslint.config.js
|
||||
helm-charts
|
||||
LICENSE
|
||||
Makefile
|
||||
node_modules
|
||||
prettier.config.js
|
||||
README.md
|
||||
renovate.json
|
||||
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']
|
||||
67
CHANGELOG.md
67
CHANGELOG.md
@@ -1,5 +1,72 @@
|
||||
# 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 ([9d1c931](https://github.com/C4illin/ConvertX/commit/9d1c93155cc33ed6c83f9e5122afff8f28d0e4bf))
|
||||
* add potrace converter ([bdbd4a1](https://github.com/C4illin/ConvertX/commit/bdbd4a122c09559b089b985ea12c5f3e085107da))
|
||||
* Add support for .HIF files ([70705c1](https://github.com/C4illin/ConvertX/commit/70705c1850d470296df85958c02a01fb5bc3a25f))
|
||||
* add support for drag/drop of images ([ff2ef74](https://github.com/C4illin/ConvertX/commit/ff2ef7413542cf10ba7a6e246763bcecd6829ec1))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add timezone support ([4b5c732](https://github.com/C4illin/ConvertX/commit/4b5c732380bc844dccf340ea1eb4f8bfe3bb44a5)), closes [#258](https://github.com/C4illin/ConvertX/issues/258)
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
### 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)
|
||||
|
||||
|
||||
|
||||
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"
|
||||
WORKDIR /app
|
||||
|
||||
@@ -6,12 +6,12 @@ WORKDIR /app
|
||||
# this will cache them and speed up future builds
|
||||
FROM base AS install
|
||||
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
|
||||
|
||||
# install with --production (exclude devDependencies)
|
||||
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
|
||||
|
||||
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 cargo install resvg
|
||||
|
||||
# copy node_modules from temp directory
|
||||
# then copy all (non-ignored) project files into the image
|
||||
FROM base AS prerelease
|
||||
WORKDIR /app
|
||||
COPY --from=install /temp/dev/node_modules node_modules
|
||||
COPY . .
|
||||
|
||||
# # [optional] tests & build
|
||||
ENV NODE_ENV=production
|
||||
# RUN bun test
|
||||
# ENV NODE_ENV=production
|
||||
RUN bun run build
|
||||
|
||||
# copy production dependencies and source code into final image
|
||||
FROM base AS release
|
||||
LABEL maintainer="Emrik Östling (C4illin)"
|
||||
LABEL description="ConvertX: self-hosted online file converter supporting 700+ file formats."
|
||||
LABEL repo="https://github.com/C4illin/ConvertX"
|
||||
|
||||
RUN apk --no-cache add libheif-tools --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community/
|
||||
|
||||
# install additional dependencies
|
||||
RUN apk --no-cache add \
|
||||
@@ -49,14 +45,18 @@ RUN apk --no-cache add \
|
||||
vips-tools \
|
||||
vips-poppler \
|
||||
vips-jxl \
|
||||
vips-heif \
|
||||
vips-magick \
|
||||
libjxl-tools \
|
||||
assimp \
|
||||
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 --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing/
|
||||
# RUN apk --no-cache add calibre@testing --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main/
|
||||
|
||||
# this might be needed for some latex use cases, will add it if needed.
|
||||
# texmf-dist-fontsextra \
|
||||
@@ -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=builder /root/.cargo/bin/resvg /usr/local/bin/resvg
|
||||
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 . .
|
||||
|
||||
EXPOSE 3000/tcp
|
||||
|
||||
17
README.md
17
README.md
@@ -27,13 +27,16 @@ A self-hosted online file converter. Supports over a thousand different formats.
|
||||
| [libjxl](https://github.com/libjxl/libjxl) | JPEG XL | 11 | 11 |
|
||||
| [resvg](https://github.com/RazrFalcon/resvg) | SVG | 1 | 1 |
|
||||
| [Vips](https://github.com/libvips/libvips) | Images | 45 | 23 |
|
||||
| [libheif](https://github.com/strukturag/libheif) | HEIF | 2 | 4 |
|
||||
| [XeLaTeX](https://tug.org/xetex/) | LaTeX | 1 | 1 |
|
||||
| [Calibre](https://calibre-ebook.com/) | E-books | 26 | 19 |
|
||||
| [Pandoc](https://pandoc.org/) | Documents | 43 | 65 |
|
||||
| [GraphicsMagick](http://www.graphicsmagick.org/) | Images | 167 | 130 |
|
||||
| [Inkscape](https://inkscape.org/) | Vector images | 7 | 17 |
|
||||
| [Assimp](https://github.com/assimp/assimp) | 3D Assets | 77 | 23 |
|
||||
| [FFmpeg](https://ffmpeg.org/) | Video | ~472 | ~199 |
|
||||
| [Potrace](https://potrace.sourceforge.net/) | Raster to vector | 4 | 11 |
|
||||
|
||||
<!-- | [Calibre](https://calibre-ebook.com/) | E-books | 26 | 19 | -->
|
||||
|
||||
<!-- many ffmpeg fileformats are duplicates -->
|
||||
|
||||
@@ -41,6 +44,9 @@ Any missing converter? Open an issue or pull request!
|
||||
|
||||
## Deployment
|
||||
|
||||
> [!WARNING]
|
||||
> If you can't login, make sure you are accessing the service over localhost or https otherwise set HTTP_ALLOWED=true
|
||||
|
||||
```yml
|
||||
# docker-compose.yml
|
||||
services:
|
||||
@@ -79,9 +85,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 |
|
||||
| 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
|
||||
| HIDE_HISTORY | false | Hide the history page |
|
||||
|
||||
### Docker images
|
||||
|
||||
@@ -96,10 +100,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:main` | The latest commit on docker hub |
|
||||
|
||||

|
||||

|
||||
<!-- Dockerhub was introduced in 0.9.0 and older releases -->
|
||||
|
||||
### Tutorial
|
||||
|
||||
> [!NOTE]
|
||||
> These are written by other people, and may be outdated, incorrect or wrong.
|
||||
|
||||
Tutorial in french: <https://belginux.com/installer-convertx-avec-docker/>
|
||||
|
||||
Tutorial in chinese: <https://xzllll.com/24092901/>
|
||||
|
||||
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
|
||||
# - FFMPEG_ARGS=-hwaccel vulkan # additional arguments to pass to ffmpeg
|
||||
# - WEBROOT=/convertx # the root path of the web interface, leave empty to disable
|
||||
# - HIDE_HISTORY=true # hides the history tab in the web interface, defaults to false
|
||||
ports:
|
||||
- 3000:3000
|
||||
|
||||
@@ -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[];
|
||||
61
flake.lock
generated
Normal file
61
flake.lock
generated
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1747179050,
|
||||
"narHash": "sha256-qhFMmDkeJX9KJwr5H32f1r7Prs7XbQWtO0h3V0a0rFY=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "adaa24fbf46737f3f1b5497bf64bae750f82942e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
60
flake.nix
Normal file
60
flake.nix
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
description = "ConvertX";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils, ... }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let pkgs = import nixpkgs { inherit system; };
|
||||
appSrc = ./.;
|
||||
|
||||
app = pkgs.dockerTools.buildLayeredImage {
|
||||
name = "convertx";
|
||||
tag = "latest";
|
||||
|
||||
contents = [
|
||||
pkgs.bun
|
||||
pkgs.resvg
|
||||
pkgs.ffmpeg
|
||||
pkgs.graphicsmagick
|
||||
pkgs.ghostscript
|
||||
pkgs.vips
|
||||
pkgs.pandoc
|
||||
pkgs.texlive.combined.scheme-full
|
||||
pkgs.calibre
|
||||
pkgs.inkscape
|
||||
pkgs.poppler_utils
|
||||
pkgs.assimp
|
||||
pkgs.jxrlib
|
||||
pkgs.libheif
|
||||
pkgs.libjxl
|
||||
pkgs.python3Packages.numpy
|
||||
];
|
||||
|
||||
config = {
|
||||
Env = [
|
||||
"NODE_ENV=production"
|
||||
"PATH=/bin:/usr/bin"
|
||||
];
|
||||
WorkingDir = "/app";
|
||||
Cmd = [ "bun" "run" "./src/index.tsx" ];
|
||||
ExposedPorts = {
|
||||
"3000/tcp" = {};
|
||||
};
|
||||
};
|
||||
|
||||
extraCommands = ''
|
||||
export PATH=${pkgs.bun}/bin:$PATH
|
||||
mkdir -p /app
|
||||
cp -r ${./dist}/* /app/
|
||||
'';
|
||||
};
|
||||
|
||||
in {
|
||||
packages.default = app;
|
||||
}
|
||||
);
|
||||
}
|
||||
60
package.json
60
package.json
@@ -1,23 +1,24 @@
|
||||
{
|
||||
"name": "convertx-frontend",
|
||||
"version": "0.10.0",
|
||||
"version": "0.13.0",
|
||||
"scripts": {
|
||||
"dev": "bun run --watch src/index.tsx",
|
||||
"hot": "bun run --hot src/index.tsx",
|
||||
"start": "bun run src/index.tsx",
|
||||
"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:tsc": "tsc --noEmit",
|
||||
"lint:knip": "knip",
|
||||
"lint:eslint": "eslint ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@elysiajs/cookie": "^0.8.0",
|
||||
"@elysiajs/html": "^1.2.0",
|
||||
"@elysiajs/jwt": "^1.2.0",
|
||||
"@elysiajs/static": "^1.2.0",
|
||||
"@kitajs/html": "^4.2.7",
|
||||
"elysia": "^1.2.10"
|
||||
"@elysiajs/html": "^1.3.0",
|
||||
"@elysiajs/jwt": "^1.3.0",
|
||||
"@elysiajs/static": "^1.3.0",
|
||||
"@kitajs/html": "^4.2.9",
|
||||
"elysia": "^1.3.1",
|
||||
"sanitize-filename": "^1.6.3"
|
||||
},
|
||||
"module": "src/index.tsx",
|
||||
"type": "module",
|
||||
@@ -25,31 +26,30 @@
|
||||
"start": "bun run src/index.tsx"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.2.5",
|
||||
"@eslint/js": "^9.18.0",
|
||||
"@eslint/js": "^9.26.0",
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.4.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",
|
||||
"@types/bun": "^1.1.16",
|
||||
"@types/bun": "^1.2.13",
|
||||
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
||||
"@types/eslint__js": "^8.42.3",
|
||||
"@types/node": "^22.10.7",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"cssnano": "^7.0.6",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-plugin-deprecation": "^3.0.0",
|
||||
"eslint-plugin-readable-tailwind": "^1.8.2",
|
||||
"@types/node": "^22.15.17",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"cssnano": "^7.0.7",
|
||||
"eslint": "^9.26.0",
|
||||
"eslint-plugin-readable-tailwind": "^2.1.1",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"eslint-plugin-tailwindcss": "^3.17.5",
|
||||
"globals": "^15.14.0",
|
||||
"knip": "^5.42.1",
|
||||
"npm-run-all2": "^7.0.2",
|
||||
"postcss": "^8.5.1",
|
||||
"postcss-cli": "^11.0.0",
|
||||
"prettier": "^3.4.2",
|
||||
"tailwind-scrollbar": "^3.1.0",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.20.0"
|
||||
"eslint-plugin-tailwindcss": "4.0.0-alpha.0",
|
||||
"globals": "^16.1.0",
|
||||
"knip": "^5.55.1",
|
||||
"npm-run-all2": "^8.0.1",
|
||||
"postcss": "^8.5.3",
|
||||
"postcss-cli": "^11.0.1",
|
||||
"prettier": "^3.5.3",
|
||||
"tailwind-scrollbar": "^4.0.2",
|
||||
"tailwindcss": "^4.1.6",
|
||||
"typescript": "^5.8.3",
|
||||
"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 {
|
||||
plugins: [autoprefixer, tailwind(tailwindConfig), cssnano],
|
||||
};
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
490
public/script.js
490
public/script.js
@@ -1,236 +1,254 @@
|
||||
const webroot = document.querySelector("meta[name='webroot']").content;
|
||||
const fileInput = document.querySelector('input[type="file"]');
|
||||
const dropZone = document.getElementById("dropzone");
|
||||
const convertButton = document.querySelector("input[type='submit']");
|
||||
const fileNames = [];
|
||||
let fileType;
|
||||
let pendingFiles = 0;
|
||||
let formatSelected = false;
|
||||
|
||||
dropZone.addEventListener("dragover", () => {
|
||||
dropZone.classList.add("dragover");
|
||||
});
|
||||
|
||||
dropZone.addEventListener("dragleave", () => {
|
||||
dropZone.classList.remove("dragover");
|
||||
});
|
||||
|
||||
dropZone.addEventListener("drop", () => {
|
||||
dropZone.classList.remove("dragover");
|
||||
});
|
||||
|
||||
const selectContainer = document.querySelector("form .select_container");
|
||||
|
||||
const updateSearchBar = () => {
|
||||
const convertToInput = document.querySelector(
|
||||
"input[name='convert_to_search']",
|
||||
);
|
||||
const convertToPopup = document.querySelector(".convert_to_popup");
|
||||
const convertToGroupElements = document.querySelectorAll(".convert_to_group");
|
||||
const convertToGroups = {};
|
||||
const convertToElement = document.querySelector("select[name='convert_to']");
|
||||
|
||||
const showMatching = (search) => {
|
||||
for (const [targets, groupElement] of Object.values(convertToGroups)) {
|
||||
let matchingTargetsFound = 0;
|
||||
for (const target of targets) {
|
||||
if (target.dataset.target.includes(search)) {
|
||||
matchingTargetsFound++;
|
||||
target.classList.remove("hidden");
|
||||
target.classList.add("flex");
|
||||
} else {
|
||||
target.classList.add("hidden");
|
||||
target.classList.remove("flex");
|
||||
}
|
||||
}
|
||||
|
||||
if (matchingTargetsFound === 0) {
|
||||
groupElement.classList.add("hidden");
|
||||
groupElement.classList.remove("flex");
|
||||
} else {
|
||||
groupElement.classList.remove("hidden");
|
||||
groupElement.classList.add("flex");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (const groupElement of convertToGroupElements) {
|
||||
const groupName = groupElement.dataset.converter;
|
||||
|
||||
const targetElements = groupElement.querySelectorAll(".target");
|
||||
const targets = Array.from(targetElements);
|
||||
|
||||
for (const target of targets) {
|
||||
target.onmousedown = () => {
|
||||
convertToElement.value = target.dataset.value;
|
||||
convertToInput.value = `${target.dataset.target} using ${target.dataset.converter}`;
|
||||
formatSelected = true;
|
||||
if (pendingFiles === 0 && fileNames.length > 0) {
|
||||
convertButton.disabled = false;
|
||||
}
|
||||
showMatching("");
|
||||
};
|
||||
}
|
||||
|
||||
convertToGroups[groupName] = [targets, groupElement];
|
||||
}
|
||||
|
||||
convertToInput.addEventListener("input", (e) => {
|
||||
showMatching(e.target.value.toLowerCase());
|
||||
});
|
||||
|
||||
convertToInput.addEventListener("search", () => {
|
||||
// when the user clears the search bar using the 'x' button
|
||||
convertButton.disabled = true;
|
||||
formatSelected = false;
|
||||
});
|
||||
|
||||
convertToInput.addEventListener("blur", (e) => {
|
||||
// Keep the popup open even when clicking on a target button
|
||||
// for a split second to allow the click to go through
|
||||
if (e?.relatedTarget?.classList?.contains("target")) {
|
||||
convertToPopup.classList.add("hidden");
|
||||
convertToPopup.classList.remove("flex");
|
||||
return;
|
||||
}
|
||||
|
||||
convertToPopup.classList.add("hidden");
|
||||
convertToPopup.classList.remove("flex");
|
||||
});
|
||||
|
||||
convertToInput.addEventListener("focus", () => {
|
||||
convertToPopup.classList.remove("hidden");
|
||||
convertToPopup.classList.add("flex");
|
||||
});
|
||||
};
|
||||
|
||||
// Add a 'change' event listener to the file input element
|
||||
fileInput.addEventListener("change", (e) => {
|
||||
// Get the selected files from the event target
|
||||
const files = e.target.files;
|
||||
|
||||
// Select the file-list table
|
||||
const fileList = document.querySelector("#file-list");
|
||||
|
||||
// Loop through the selected files
|
||||
for (const file of files) {
|
||||
// Create a new table row for each file
|
||||
const row = document.createElement("tr");
|
||||
row.innerHTML = `
|
||||
<td>${file.name}</td>
|
||||
<td>${(file.size / 1024).toFixed(2)} kB</td>
|
||||
<td><a onclick="deleteRow(this)">Remove</a></td>
|
||||
`;
|
||||
|
||||
if (!fileType) {
|
||||
fileType = file.name.split(".").pop();
|
||||
fileInput.setAttribute("accept", `.${fileType}`);
|
||||
setTitle();
|
||||
|
||||
// choose the option that matches the file type
|
||||
// for (const option of convertFromSelect.children) {
|
||||
// console.log(option.value);
|
||||
// if (option.value === fileType) {
|
||||
// option.selected = true;
|
||||
// }
|
||||
// }
|
||||
|
||||
fetch(`${webroot}/conversions`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ fileType: fileType }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((res) => res.text())
|
||||
.then((html) => {
|
||||
selectContainer.innerHTML = html;
|
||||
updateSearchBar();
|
||||
})
|
||||
.catch((err) => console.log(err));
|
||||
}
|
||||
|
||||
// Append the row to the file-list table
|
||||
fileList.appendChild(row);
|
||||
|
||||
// Append the file to the hidden input
|
||||
fileNames.push(file.name);
|
||||
}
|
||||
|
||||
uploadFiles(files);
|
||||
});
|
||||
|
||||
const setTitle = () => {
|
||||
const title = document.querySelector("h1");
|
||||
title.textContent = `Convert ${fileType ? `.${fileType}` : ""}`;
|
||||
};
|
||||
|
||||
// Add a onclick for the delete button
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const deleteRow = (target) => {
|
||||
const filename = target.parentElement.parentElement.children[0].textContent;
|
||||
const row = target.parentElement.parentElement;
|
||||
row.remove();
|
||||
|
||||
// remove from fileNames
|
||||
const index = fileNames.indexOf(filename);
|
||||
fileNames.splice(index, 1);
|
||||
|
||||
// reset fileInput
|
||||
fileInput.value = "";
|
||||
|
||||
// if fileNames is empty, reset fileType
|
||||
if (fileNames.length === 0) {
|
||||
fileType = null;
|
||||
fileInput.removeAttribute("accept");
|
||||
convertButton.disabled = true;
|
||||
setTitle();
|
||||
}
|
||||
|
||||
fetch(`${webroot}/delete`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ filename: filename }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.catch((err) => console.log(err));
|
||||
};
|
||||
|
||||
const uploadFiles = (files) => {
|
||||
convertButton.disabled = true;
|
||||
convertButton.textContent = "Uploading...";
|
||||
pendingFiles += 1;
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
for (const file of files) {
|
||||
formData.append("file", file, file.name);
|
||||
}
|
||||
|
||||
fetch(`${webroot}/upload`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
pendingFiles -= 1;
|
||||
if (pendingFiles === 0) {
|
||||
if (formatSelected) {
|
||||
convertButton.disabled = false;
|
||||
}
|
||||
convertButton.textContent = "Convert";
|
||||
}
|
||||
console.log(data);
|
||||
})
|
||||
.catch((err) => console.log(err));
|
||||
};
|
||||
|
||||
const formConvert = document.querySelector(`form[action='${webroot}/convert']`);
|
||||
|
||||
formConvert.addEventListener("submit", () => {
|
||||
const hiddenInput = document.querySelector("input[name='file_names']");
|
||||
hiddenInput.value = JSON.stringify(fileNames);
|
||||
});
|
||||
|
||||
updateSearchBar();
|
||||
const webroot = document.querySelector("meta[name='webroot']").content;
|
||||
const fileInput = document.querySelector('input[type="file"]');
|
||||
const dropZone = document.getElementById("dropzone");
|
||||
const convertButton = document.querySelector("input[type='submit']");
|
||||
const fileNames = [];
|
||||
let fileType;
|
||||
let pendingFiles = 0;
|
||||
let formatSelected = false;
|
||||
|
||||
dropZone.addEventListener("dragover", (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add("dragover");
|
||||
});
|
||||
|
||||
dropZone.addEventListener("dragleave", () => {
|
||||
dropZone.classList.remove("dragover");
|
||||
});
|
||||
|
||||
dropZone.addEventListener("drop", (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove("dragover");
|
||||
|
||||
const files = e.dataTransfer.files;
|
||||
|
||||
if (files.length === 0) {
|
||||
console.warn("No files dropped — likely a URL or unsupported source.");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
console.log("Handling dropped file:", file.name);
|
||||
handleFile(file);
|
||||
}
|
||||
});
|
||||
|
||||
// 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 updateSearchBar = () => {
|
||||
const convertToInput = document.querySelector(
|
||||
"input[name='convert_to_search']",
|
||||
);
|
||||
const convertToPopup = document.querySelector(".convert_to_popup");
|
||||
const convertToGroupElements = document.querySelectorAll(".convert_to_group");
|
||||
const convertToGroups = {};
|
||||
const convertToElement = document.querySelector("select[name='convert_to']");
|
||||
|
||||
const showMatching = (search) => {
|
||||
for (const [targets, groupElement] of Object.values(convertToGroups)) {
|
||||
let matchingTargetsFound = 0;
|
||||
for (const target of targets) {
|
||||
if (target.dataset.target.includes(search)) {
|
||||
matchingTargetsFound++;
|
||||
target.classList.remove("hidden");
|
||||
target.classList.add("flex");
|
||||
} else {
|
||||
target.classList.add("hidden");
|
||||
target.classList.remove("flex");
|
||||
}
|
||||
}
|
||||
|
||||
if (matchingTargetsFound === 0) {
|
||||
groupElement.classList.add("hidden");
|
||||
groupElement.classList.remove("flex");
|
||||
} else {
|
||||
groupElement.classList.remove("hidden");
|
||||
groupElement.classList.add("flex");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (const groupElement of convertToGroupElements) {
|
||||
const groupName = groupElement.dataset.converter;
|
||||
|
||||
const targetElements = groupElement.querySelectorAll(".target");
|
||||
const targets = Array.from(targetElements);
|
||||
|
||||
for (const target of targets) {
|
||||
target.onmousedown = () => {
|
||||
convertToElement.value = target.dataset.value;
|
||||
convertToInput.value = `${target.dataset.target} using ${target.dataset.converter}`;
|
||||
formatSelected = true;
|
||||
if (pendingFiles === 0 && fileNames.length > 0) {
|
||||
convertButton.disabled = false;
|
||||
}
|
||||
showMatching("");
|
||||
};
|
||||
}
|
||||
|
||||
convertToGroups[groupName] = [targets, groupElement];
|
||||
}
|
||||
|
||||
convertToInput.addEventListener("input", (e) => {
|
||||
showMatching(e.target.value.toLowerCase());
|
||||
});
|
||||
|
||||
convertToInput.addEventListener("search", () => {
|
||||
// when the user clears the search bar using the 'x' button
|
||||
convertButton.disabled = true;
|
||||
formatSelected = false;
|
||||
});
|
||||
|
||||
convertToInput.addEventListener("blur", (e) => {
|
||||
// Keep the popup open even when clicking on a target button
|
||||
// for a split second to allow the click to go through
|
||||
if (e?.relatedTarget?.classList?.contains("target")) {
|
||||
convertToPopup.classList.add("hidden");
|
||||
convertToPopup.classList.remove("flex");
|
||||
return;
|
||||
}
|
||||
|
||||
convertToPopup.classList.add("hidden");
|
||||
convertToPopup.classList.remove("flex");
|
||||
});
|
||||
|
||||
convertToInput.addEventListener("focus", () => {
|
||||
convertToPopup.classList.remove("hidden");
|
||||
convertToPopup.classList.add("flex");
|
||||
});
|
||||
};
|
||||
|
||||
// Add a 'change' event listener to the file input element
|
||||
fileInput.addEventListener("change", (e) => {
|
||||
const files = e.target.files;
|
||||
for (const file of files) {
|
||||
handleFile(file);
|
||||
}
|
||||
});
|
||||
|
||||
const setTitle = () => {
|
||||
const title = document.querySelector("h1");
|
||||
title.textContent = `Convert ${fileType ? `.${fileType}` : ""}`;
|
||||
};
|
||||
|
||||
// Add a onclick for the delete button
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const deleteRow = (target) => {
|
||||
const filename = target.parentElement.parentElement.children[0].textContent;
|
||||
const row = target.parentElement.parentElement;
|
||||
row.remove();
|
||||
|
||||
// remove from fileNames
|
||||
const index = fileNames.indexOf(filename);
|
||||
fileNames.splice(index, 1);
|
||||
|
||||
// reset fileInput
|
||||
fileInput.value = "";
|
||||
|
||||
// if fileNames is empty, reset fileType
|
||||
if (fileNames.length === 0) {
|
||||
fileType = null;
|
||||
fileInput.removeAttribute("accept");
|
||||
convertButton.disabled = true;
|
||||
setTitle();
|
||||
}
|
||||
|
||||
fetch(`${webroot}/delete`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ filename: filename }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.catch((err) => console.log(err));
|
||||
};
|
||||
|
||||
const uploadFile = (file) => {
|
||||
convertButton.disabled = true;
|
||||
convertButton.textContent = "Uploading...";
|
||||
pendingFiles += 1;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("file", file, file.name);
|
||||
|
||||
let xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.open("POST", `${webroot}/upload`, true);
|
||||
|
||||
xhr.onload = () => {
|
||||
let data = JSON.parse(xhr.responseText);
|
||||
|
||||
pendingFiles -= 1;
|
||||
if (pendingFiles === 0) {
|
||||
if (formatSelected) {
|
||||
convertButton.disabled = false;
|
||||
}
|
||||
convertButton.textContent = "Convert";
|
||||
}
|
||||
|
||||
//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']`);
|
||||
|
||||
formConvert.addEventListener("submit", () => {
|
||||
const hiddenInput = document.querySelector("input[name='file_names']");
|
||||
hiddenInput.value = JSON.stringify(fileNames);
|
||||
});
|
||||
|
||||
updateSearchBar();
|
||||
|
||||
@@ -1,40 +1,40 @@
|
||||
import { Html } from "@elysiajs/html";
|
||||
|
||||
export const BaseHtml = ({
|
||||
children,
|
||||
title = "ConvertX",
|
||||
webroot = "",
|
||||
}: {
|
||||
children: JSX.Element;
|
||||
title?: string;
|
||||
webroot?: string;
|
||||
}) => (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="webroot" content={webroot} />
|
||||
<title safe>{title}</title>
|
||||
<link rel="stylesheet" href={`${webroot}/generated.css`} />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href={`${webroot}/apple-touch-icon.png`}
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href={`${webroot}/favicon-32x32.png`}
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href={`${webroot}/favicon-16x16.png`}
|
||||
/>
|
||||
<link rel="manifest" href={`${webroot}/site.webmanifest`} />
|
||||
</head>
|
||||
<body class="w-full bg-neutral-900 text-neutral-200">{children}</body>
|
||||
</html>
|
||||
);
|
||||
import { Html } from "@elysiajs/html";
|
||||
|
||||
export const BaseHtml = ({
|
||||
children,
|
||||
title = "ConvertX",
|
||||
webroot = "",
|
||||
}: {
|
||||
children: JSX.Element;
|
||||
title?: string;
|
||||
webroot?: string;
|
||||
}) => (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="webroot" content={webroot} />
|
||||
<title safe>{title}</title>
|
||||
<link rel="stylesheet" href={`${webroot}/generated.css`} />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href={`${webroot}/apple-touch-icon.png`}
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href={`${webroot}/favicon-32x32.png`}
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href={`${webroot}/favicon-16x16.png`}
|
||||
/>
|
||||
<link rel="manifest" href={`${webroot}/site.webmanifest`} />
|
||||
</head>
|
||||
<body class="w-full bg-neutral-900 text-neutral-200">{children}</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -4,28 +4,32 @@ export const Header = ({
|
||||
loggedIn,
|
||||
accountRegistration,
|
||||
allowUnauthenticated,
|
||||
hideHistory,
|
||||
webroot = "",
|
||||
}: {
|
||||
loggedIn?: boolean;
|
||||
accountRegistration?: boolean;
|
||||
allowUnauthenticated?: boolean;
|
||||
hideHistory?: boolean;
|
||||
webroot?: string;
|
||||
}) => {
|
||||
let rightNav: JSX.Element;
|
||||
if (loggedIn) {
|
||||
rightNav = (
|
||||
<ul class="flex gap-4">
|
||||
<li>
|
||||
<a
|
||||
class={`
|
||||
text-accent-600 transition-all
|
||||
hover:text-accent-500 hover:underline
|
||||
`}
|
||||
href={`${webroot}/history`}
|
||||
>
|
||||
History
|
||||
</a>
|
||||
</li>
|
||||
{!hideHistory && (
|
||||
<li>
|
||||
<a
|
||||
class={`
|
||||
text-accent-600 transition-all
|
||||
hover:text-accent-500 hover:underline
|
||||
`}
|
||||
href={`${webroot}/history`}
|
||||
>
|
||||
History
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
{!allowUnauthenticated ? (
|
||||
<li>
|
||||
<a
|
||||
@@ -74,7 +78,7 @@ export const Header = ({
|
||||
|
||||
return (
|
||||
<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>
|
||||
<li>
|
||||
<strong>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { exec } from "node:child_process";
|
||||
import { execFile } from "node:child_process";
|
||||
|
||||
export const properties = {
|
||||
from: {
|
||||
@@ -119,10 +119,8 @@ export async function convert(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
options?: unknown,
|
||||
): Promise<string> {
|
||||
const command = `assimp export "${filePath}" "${targetPath}"`;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(command, (error, stdout, stderr) => {
|
||||
execFile("assimp", ["export", filePath, targetPath], (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(`error: ${error}`);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { exec } from "node:child_process";
|
||||
import { execFile } from "node:child_process";
|
||||
|
||||
export const properties = {
|
||||
from: {
|
||||
@@ -64,10 +64,8 @@ export async function convert(
|
||||
// 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) => {
|
||||
execFile("ebook-convert", [filePath, targetPath], (error, stdout, stderr) => {
|
||||
if (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
|
||||
export const properties = {
|
||||
@@ -691,19 +691,28 @@ export async function convert(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
options?: unknown,
|
||||
): Promise<string> {
|
||||
let extra = "";
|
||||
let extraArgs: string[] = [];
|
||||
let message = "Done";
|
||||
|
||||
if (convertTo === "ico") {
|
||||
// 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";
|
||||
}
|
||||
|
||||
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) => {
|
||||
exec(command, (error, stdout, stderr) => {
|
||||
execFile("ffmpeg", args, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(`error: ${error}`);
|
||||
}
|
||||
|
||||
@@ -1,339 +1,340 @@
|
||||
import { exec } from "node:child_process";
|
||||
|
||||
export const properties = {
|
||||
from: {
|
||||
image: [
|
||||
"3fr",
|
||||
"8bim",
|
||||
"8bimtext",
|
||||
"8bimwtext",
|
||||
"app1",
|
||||
"app1jpeg",
|
||||
"art",
|
||||
"arw",
|
||||
"avs",
|
||||
"b",
|
||||
"bie",
|
||||
"bigtiff",
|
||||
"bmp",
|
||||
"c",
|
||||
"cals",
|
||||
"caption",
|
||||
"cin",
|
||||
"cmyk",
|
||||
"cmyka",
|
||||
"cr2",
|
||||
"crw",
|
||||
"cur",
|
||||
"cut",
|
||||
"dcm",
|
||||
"dcr",
|
||||
"dcx",
|
||||
"dng",
|
||||
"dpx",
|
||||
"epdf",
|
||||
"epi",
|
||||
"eps",
|
||||
"epsf",
|
||||
"epsi",
|
||||
"ept",
|
||||
"ept2",
|
||||
"ept3",
|
||||
"erf",
|
||||
"exif",
|
||||
"fax",
|
||||
"file",
|
||||
"fits",
|
||||
"fractal",
|
||||
"ftp",
|
||||
"g",
|
||||
"gif",
|
||||
"gif87",
|
||||
"gradient",
|
||||
"gray",
|
||||
"graya",
|
||||
"heic",
|
||||
"heif",
|
||||
"hrz",
|
||||
"http",
|
||||
"icb",
|
||||
"icc",
|
||||
"icm",
|
||||
"ico",
|
||||
"icon",
|
||||
"identity",
|
||||
"image",
|
||||
"iptc",
|
||||
"iptctext",
|
||||
"iptcwtext",
|
||||
"jbg",
|
||||
"jbig",
|
||||
"jng",
|
||||
"jnx",
|
||||
"jpeg",
|
||||
"jpg",
|
||||
"k",
|
||||
"k25",
|
||||
"kdc",
|
||||
"label",
|
||||
"m",
|
||||
"mac",
|
||||
"map",
|
||||
"mat",
|
||||
"mef",
|
||||
"miff",
|
||||
"mng",
|
||||
"mono",
|
||||
"mpc",
|
||||
"mrw",
|
||||
"msl",
|
||||
"mtv",
|
||||
"mvg",
|
||||
"nef",
|
||||
"null",
|
||||
"o",
|
||||
"orf",
|
||||
"otb",
|
||||
"p7",
|
||||
"pal",
|
||||
"palm",
|
||||
"pam",
|
||||
"pbm",
|
||||
"pcd",
|
||||
"pcds",
|
||||
"pct",
|
||||
"pcx",
|
||||
"pdb",
|
||||
"pdf",
|
||||
"pef",
|
||||
"pfa",
|
||||
"pfb",
|
||||
"pgm",
|
||||
"picon",
|
||||
"pict",
|
||||
"pix",
|
||||
"plasma",
|
||||
"png",
|
||||
"png00",
|
||||
"png24",
|
||||
"png32",
|
||||
"png48",
|
||||
"png64",
|
||||
"png8",
|
||||
"pnm",
|
||||
"ppm",
|
||||
"ps",
|
||||
"ptif",
|
||||
"pwp",
|
||||
"r",
|
||||
"raf",
|
||||
"ras",
|
||||
"rgb",
|
||||
"rgba",
|
||||
"rla",
|
||||
"rle",
|
||||
"sct",
|
||||
"sfw",
|
||||
"sgi",
|
||||
"sr2",
|
||||
"srf",
|
||||
"stegano",
|
||||
"sun",
|
||||
"svg",
|
||||
"svgz",
|
||||
"text",
|
||||
"tga",
|
||||
"tif",
|
||||
"tiff",
|
||||
"tile",
|
||||
"tim",
|
||||
"topol",
|
||||
"ttf",
|
||||
"txt",
|
||||
"uyvy",
|
||||
"vda",
|
||||
"vicar",
|
||||
"vid",
|
||||
"viff",
|
||||
"vst",
|
||||
"wbmp",
|
||||
"webp",
|
||||
"wmf",
|
||||
"wpg",
|
||||
"x3f",
|
||||
"xbm",
|
||||
"xc",
|
||||
"xcf",
|
||||
"xmp",
|
||||
"xpm",
|
||||
"xv",
|
||||
"xwd",
|
||||
"y",
|
||||
"yuv",
|
||||
],
|
||||
},
|
||||
to: {
|
||||
image: [
|
||||
"8bim",
|
||||
"8bimtext",
|
||||
"8bimwtext",
|
||||
"app1",
|
||||
"app1jpeg",
|
||||
"art",
|
||||
"avs",
|
||||
"b",
|
||||
"bie",
|
||||
"bigtiff",
|
||||
"bmp",
|
||||
"bmp2",
|
||||
"bmp3",
|
||||
"brf",
|
||||
"c",
|
||||
"cals",
|
||||
"cin",
|
||||
"cmyk",
|
||||
"cmyka",
|
||||
"dcx",
|
||||
"dpx",
|
||||
"epdf",
|
||||
"epi",
|
||||
"eps",
|
||||
"eps2",
|
||||
"eps3",
|
||||
"epsf",
|
||||
"epsi",
|
||||
"ept",
|
||||
"ept2",
|
||||
"ept3",
|
||||
"exif",
|
||||
"fax",
|
||||
"fits",
|
||||
"g",
|
||||
"gif",
|
||||
"gif87",
|
||||
"gray",
|
||||
"graya",
|
||||
"histogram",
|
||||
"html",
|
||||
"icb",
|
||||
"icc",
|
||||
"icm",
|
||||
"info",
|
||||
"iptc",
|
||||
"iptctext",
|
||||
"iptcwtext",
|
||||
"isobrl",
|
||||
"isobrl6",
|
||||
"jbg",
|
||||
"jbig",
|
||||
"jng",
|
||||
"jpeg",
|
||||
"k",
|
||||
"m",
|
||||
"m2v",
|
||||
"map",
|
||||
"mat",
|
||||
"matte",
|
||||
"miff",
|
||||
"mng",
|
||||
"mono",
|
||||
"mpc",
|
||||
"mpeg",
|
||||
"mpg",
|
||||
"msl",
|
||||
"mtv",
|
||||
"mvg",
|
||||
"null",
|
||||
"o",
|
||||
"otb",
|
||||
"p7",
|
||||
"pal",
|
||||
"pam",
|
||||
"pbm",
|
||||
"pcd",
|
||||
"pcds",
|
||||
"pcl",
|
||||
"pct",
|
||||
"pcx",
|
||||
"pdb",
|
||||
"pdf",
|
||||
"pgm",
|
||||
"picon",
|
||||
"pict",
|
||||
"png",
|
||||
"png00",
|
||||
"png24",
|
||||
"png32",
|
||||
"png48",
|
||||
"png64",
|
||||
"png8",
|
||||
"pnm",
|
||||
"ppm",
|
||||
"preview",
|
||||
"ps",
|
||||
"ps2",
|
||||
"ps3",
|
||||
"ptif",
|
||||
"r",
|
||||
"ras",
|
||||
"rgb",
|
||||
"rgba",
|
||||
"sgi",
|
||||
"shtml",
|
||||
"sun",
|
||||
"text",
|
||||
"tga",
|
||||
"tiff",
|
||||
"txt",
|
||||
"ubrl",
|
||||
"ubrl6",
|
||||
"uil",
|
||||
"uyvy",
|
||||
"vda",
|
||||
"vicar",
|
||||
"vid",
|
||||
"viff",
|
||||
"vst",
|
||||
"wbmp",
|
||||
"webp",
|
||||
"x",
|
||||
"xbm",
|
||||
"xmp",
|
||||
"xpm",
|
||||
"xv",
|
||||
"xwd",
|
||||
"y",
|
||||
"yuv",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export function convert(
|
||||
filePath: string,
|
||||
fileType: string,
|
||||
convertTo: string,
|
||||
targetPath: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
options?: unknown,
|
||||
): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(
|
||||
`gm convert "${filePath}" "${targetPath}"`,
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(`error: ${error}`);
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(`stdout: ${stdout}`);
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
console.error(`stderr: ${stderr}`);
|
||||
}
|
||||
|
||||
resolve("Done");
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
import { execFile } from "node:child_process";
|
||||
|
||||
export const properties = {
|
||||
from: {
|
||||
image: [
|
||||
"3fr",
|
||||
"8bim",
|
||||
"8bimtext",
|
||||
"8bimwtext",
|
||||
"app1",
|
||||
"app1jpeg",
|
||||
"art",
|
||||
"arw",
|
||||
"avs",
|
||||
"b",
|
||||
"bie",
|
||||
"bigtiff",
|
||||
"bmp",
|
||||
"c",
|
||||
"cals",
|
||||
"caption",
|
||||
"cin",
|
||||
"cmyk",
|
||||
"cmyka",
|
||||
"cr2",
|
||||
"crw",
|
||||
"cur",
|
||||
"cut",
|
||||
"dcm",
|
||||
"dcr",
|
||||
"dcx",
|
||||
"dng",
|
||||
"dpx",
|
||||
"epdf",
|
||||
"epi",
|
||||
"eps",
|
||||
"epsf",
|
||||
"epsi",
|
||||
"ept",
|
||||
"ept2",
|
||||
"ept3",
|
||||
"erf",
|
||||
"exif",
|
||||
"fax",
|
||||
"file",
|
||||
"fits",
|
||||
"fractal",
|
||||
"ftp",
|
||||
"g",
|
||||
"gif",
|
||||
"gif87",
|
||||
"gradient",
|
||||
"gray",
|
||||
"graya",
|
||||
"heic",
|
||||
"heif",
|
||||
"hrz",
|
||||
"http",
|
||||
"icb",
|
||||
"icc",
|
||||
"icm",
|
||||
"ico",
|
||||
"icon",
|
||||
"identity",
|
||||
"image",
|
||||
"iptc",
|
||||
"iptctext",
|
||||
"iptcwtext",
|
||||
"jbg",
|
||||
"jbig",
|
||||
"jng",
|
||||
"jnx",
|
||||
"jpeg",
|
||||
"jpg",
|
||||
"k",
|
||||
"k25",
|
||||
"kdc",
|
||||
"label",
|
||||
"m",
|
||||
"mac",
|
||||
"map",
|
||||
"mat",
|
||||
"mef",
|
||||
"miff",
|
||||
"mng",
|
||||
"mono",
|
||||
"mpc",
|
||||
"mrw",
|
||||
"msl",
|
||||
"mtv",
|
||||
"mvg",
|
||||
"nef",
|
||||
"null",
|
||||
"o",
|
||||
"orf",
|
||||
"otb",
|
||||
"p7",
|
||||
"pal",
|
||||
"palm",
|
||||
"pam",
|
||||
"pbm",
|
||||
"pcd",
|
||||
"pcds",
|
||||
"pct",
|
||||
"pcx",
|
||||
"pdb",
|
||||
"pdf",
|
||||
"pef",
|
||||
"pfa",
|
||||
"pfb",
|
||||
"pgm",
|
||||
"picon",
|
||||
"pict",
|
||||
"pix",
|
||||
"plasma",
|
||||
"png",
|
||||
"png00",
|
||||
"png24",
|
||||
"png32",
|
||||
"png48",
|
||||
"png64",
|
||||
"png8",
|
||||
"pnm",
|
||||
"ppm",
|
||||
"ps",
|
||||
"ptif",
|
||||
"pwp",
|
||||
"r",
|
||||
"raf",
|
||||
"ras",
|
||||
"rgb",
|
||||
"rgba",
|
||||
"rla",
|
||||
"rle",
|
||||
"sct",
|
||||
"sfw",
|
||||
"sgi",
|
||||
"sr2",
|
||||
"srf",
|
||||
"stegano",
|
||||
"sun",
|
||||
"svg",
|
||||
"svgz",
|
||||
"text",
|
||||
"tga",
|
||||
"tif",
|
||||
"tiff",
|
||||
"tile",
|
||||
"tim",
|
||||
"topol",
|
||||
"ttf",
|
||||
"txt",
|
||||
"uyvy",
|
||||
"vda",
|
||||
"vicar",
|
||||
"vid",
|
||||
"viff",
|
||||
"vst",
|
||||
"wbmp",
|
||||
"webp",
|
||||
"wmf",
|
||||
"wpg",
|
||||
"x3f",
|
||||
"xbm",
|
||||
"xc",
|
||||
"xcf",
|
||||
"xmp",
|
||||
"xpm",
|
||||
"xv",
|
||||
"xwd",
|
||||
"y",
|
||||
"yuv",
|
||||
],
|
||||
},
|
||||
to: {
|
||||
image: [
|
||||
"8bim",
|
||||
"8bimtext",
|
||||
"8bimwtext",
|
||||
"app1",
|
||||
"app1jpeg",
|
||||
"art",
|
||||
"avs",
|
||||
"b",
|
||||
"bie",
|
||||
"bigtiff",
|
||||
"bmp",
|
||||
"bmp2",
|
||||
"bmp3",
|
||||
"brf",
|
||||
"c",
|
||||
"cals",
|
||||
"cin",
|
||||
"cmyk",
|
||||
"cmyka",
|
||||
"dcx",
|
||||
"dpx",
|
||||
"epdf",
|
||||
"epi",
|
||||
"eps",
|
||||
"eps2",
|
||||
"eps3",
|
||||
"epsf",
|
||||
"epsi",
|
||||
"ept",
|
||||
"ept2",
|
||||
"ept3",
|
||||
"exif",
|
||||
"fax",
|
||||
"fits",
|
||||
"g",
|
||||
"gif",
|
||||
"gif87",
|
||||
"gray",
|
||||
"graya",
|
||||
"histogram",
|
||||
"html",
|
||||
"icb",
|
||||
"icc",
|
||||
"icm",
|
||||
"info",
|
||||
"iptc",
|
||||
"iptctext",
|
||||
"iptcwtext",
|
||||
"isobrl",
|
||||
"isobrl6",
|
||||
"jbg",
|
||||
"jbig",
|
||||
"jng",
|
||||
"jpeg",
|
||||
"k",
|
||||
"m",
|
||||
"m2v",
|
||||
"map",
|
||||
"mat",
|
||||
"matte",
|
||||
"miff",
|
||||
"mng",
|
||||
"mono",
|
||||
"mpc",
|
||||
"mpeg",
|
||||
"mpg",
|
||||
"msl",
|
||||
"mtv",
|
||||
"mvg",
|
||||
"null",
|
||||
"o",
|
||||
"otb",
|
||||
"p7",
|
||||
"pal",
|
||||
"pam",
|
||||
"pbm",
|
||||
"pcd",
|
||||
"pcds",
|
||||
"pcl",
|
||||
"pct",
|
||||
"pcx",
|
||||
"pdb",
|
||||
"pdf",
|
||||
"pgm",
|
||||
"picon",
|
||||
"pict",
|
||||
"png",
|
||||
"png00",
|
||||
"png24",
|
||||
"png32",
|
||||
"png48",
|
||||
"png64",
|
||||
"png8",
|
||||
"pnm",
|
||||
"ppm",
|
||||
"preview",
|
||||
"ps",
|
||||
"ps2",
|
||||
"ps3",
|
||||
"ptif",
|
||||
"r",
|
||||
"ras",
|
||||
"rgb",
|
||||
"rgba",
|
||||
"sgi",
|
||||
"shtml",
|
||||
"sun",
|
||||
"text",
|
||||
"tga",
|
||||
"tiff",
|
||||
"txt",
|
||||
"ubrl",
|
||||
"ubrl6",
|
||||
"uil",
|
||||
"uyvy",
|
||||
"vda",
|
||||
"vicar",
|
||||
"vid",
|
||||
"viff",
|
||||
"vst",
|
||||
"wbmp",
|
||||
"webp",
|
||||
"x",
|
||||
"xbm",
|
||||
"xmp",
|
||||
"xpm",
|
||||
"xv",
|
||||
"xwd",
|
||||
"y",
|
||||
"yuv",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export function convert(
|
||||
filePath: string,
|
||||
fileType: string,
|
||||
convertTo: string,
|
||||
targetPath: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
options?: unknown,
|
||||
): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
execFile(
|
||||
"gm",
|
||||
["convert", filePath, targetPath],
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(`error: ${error}`);
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(`stdout: ${stdout}`);
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
console.error(`stderr: ${stderr}`);
|
||||
}
|
||||
|
||||
resolve("Done");
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,64 +1,59 @@
|
||||
import { exec } from "node:child_process";
|
||||
import { execFile } 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",
|
||||
]
|
||||
},
|
||||
};
|
||||
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) => {
|
||||
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(
|
||||
"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");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
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,71 +1,71 @@
|
||||
import { exec } from "node:child_process";
|
||||
|
||||
// declare possible conversions
|
||||
export const properties = {
|
||||
from: {
|
||||
jxl: ["jxl"],
|
||||
images: [
|
||||
"apng",
|
||||
"exr",
|
||||
"gif",
|
||||
"jpeg",
|
||||
"pam",
|
||||
"pfm",
|
||||
"pgm",
|
||||
"pgx",
|
||||
"png",
|
||||
"ppm",
|
||||
],
|
||||
},
|
||||
to: {
|
||||
jxl: [
|
||||
"apng",
|
||||
"exr",
|
||||
"gif",
|
||||
"jpeg",
|
||||
"pam",
|
||||
"pfm",
|
||||
"pgm",
|
||||
"pgx",
|
||||
"png",
|
||||
"ppm",
|
||||
],
|
||||
images: ["jxl"],
|
||||
},
|
||||
};
|
||||
|
||||
export function convert(
|
||||
filePath: string,
|
||||
fileType: string,
|
||||
convertTo: string,
|
||||
targetPath: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
options?: unknown,
|
||||
): Promise<string> {
|
||||
let tool = "";
|
||||
if (fileType === "jxl") {
|
||||
tool = "djxl";
|
||||
}
|
||||
|
||||
if (convertTo === "jxl") {
|
||||
tool = "cjxl";
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(`${tool} "${filePath}" "${targetPath}"`, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(`error: ${error}`);
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(`stdout: ${stdout}`);
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
console.error(`stderr: ${stderr}`);
|
||||
}
|
||||
|
||||
resolve("Done");
|
||||
});
|
||||
});
|
||||
}
|
||||
import { execFile } from "node:child_process";
|
||||
|
||||
// declare possible conversions
|
||||
export const properties = {
|
||||
from: {
|
||||
jxl: ["jxl"],
|
||||
images: [
|
||||
"apng",
|
||||
"exr",
|
||||
"gif",
|
||||
"jpeg",
|
||||
"pam",
|
||||
"pfm",
|
||||
"pgm",
|
||||
"pgx",
|
||||
"png",
|
||||
"ppm",
|
||||
],
|
||||
},
|
||||
to: {
|
||||
jxl: [
|
||||
"apng",
|
||||
"exr",
|
||||
"gif",
|
||||
"jpeg",
|
||||
"pam",
|
||||
"pfm",
|
||||
"pgm",
|
||||
"pgx",
|
||||
"png",
|
||||
"ppm",
|
||||
],
|
||||
images: ["jxl"],
|
||||
},
|
||||
};
|
||||
|
||||
export function convert(
|
||||
filePath: string,
|
||||
fileType: string,
|
||||
convertTo: string,
|
||||
targetPath: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
options?: unknown,
|
||||
): Promise<string> {
|
||||
let tool = "";
|
||||
if (fileType === "jxl") {
|
||||
tool = "djxl";
|
||||
}
|
||||
|
||||
if (convertTo === "jxl") {
|
||||
tool = "cjxl";
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
execFile(tool, [filePath, targetPath], (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(`error: ${error}`);
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(`stdout: ${stdout}`);
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
console.error(`stderr: ${stderr}`);
|
||||
}
|
||||
|
||||
resolve("Done");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 convertImage, properties as propertiesImage } from "./vips";
|
||||
import { convert as convertxelatex, properties as propertiesxelatex } from "./xelatex";
|
||||
import { convert as convertCalibre, properties as propertiesCalibre } from "./calibre";
|
||||
// import { convert as 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
|
||||
@@ -53,14 +55,18 @@ const properties: Record<
|
||||
properties: propertiesImage,
|
||||
converter: convertImage,
|
||||
},
|
||||
libheif: {
|
||||
properties: propertiesLibheif,
|
||||
converter: convertLibheif,
|
||||
},
|
||||
xelatex: {
|
||||
properties: propertiesxelatex,
|
||||
converter: convertxelatex,
|
||||
},
|
||||
calibre: {
|
||||
properties: propertiesCalibre,
|
||||
converter: convertCalibre,
|
||||
},
|
||||
// calibre: {
|
||||
// properties: propertiesCalibre,
|
||||
// converter: convertCalibre,
|
||||
// },
|
||||
pandoc: {
|
||||
properties: propertiesPandoc,
|
||||
converter: convertPandoc,
|
||||
@@ -81,6 +87,10 @@ const properties: Record<
|
||||
properties: propertiesFFmpeg,
|
||||
converter: convertFFmpeg,
|
||||
},
|
||||
potrace: {
|
||||
properties: propertiespotrace,
|
||||
converter: convertpotrace,
|
||||
},
|
||||
};
|
||||
|
||||
export async function mainConverter(
|
||||
|
||||
@@ -1,156 +1,162 @@
|
||||
import { exec } from "node:child_process";
|
||||
|
||||
export const properties = {
|
||||
from: {
|
||||
text: [
|
||||
"textile",
|
||||
"tikiwiki",
|
||||
"tsv",
|
||||
"twiki",
|
||||
"typst",
|
||||
"vimwiki",
|
||||
"biblatex",
|
||||
"bibtex",
|
||||
"bits",
|
||||
"commonmark",
|
||||
"commonmark_x",
|
||||
"creole",
|
||||
"csljson",
|
||||
"csv",
|
||||
"djot",
|
||||
"docbook",
|
||||
"docx",
|
||||
"dokuwiki",
|
||||
"endnotexml",
|
||||
"epub",
|
||||
"fb2",
|
||||
"gfm",
|
||||
"haddock",
|
||||
"html",
|
||||
"ipynb",
|
||||
"jats",
|
||||
"jira",
|
||||
"json",
|
||||
"latex",
|
||||
"man",
|
||||
"markdown",
|
||||
"markdown_mmd",
|
||||
"markdown_phpextra",
|
||||
"markdown_strict",
|
||||
"mediawiki",
|
||||
"muse",
|
||||
"pandoc native",
|
||||
"opml",
|
||||
"org",
|
||||
"ris",
|
||||
"rst",
|
||||
"rtf",
|
||||
"t2t",
|
||||
],
|
||||
},
|
||||
to: {
|
||||
text: [
|
||||
"tei",
|
||||
"texinfo",
|
||||
"textile",
|
||||
"typst",
|
||||
"xwiki",
|
||||
"zimwiki",
|
||||
"asciidoc",
|
||||
"asciidoc_legacy",
|
||||
"asciidoctor",
|
||||
"beamer",
|
||||
"biblatex",
|
||||
"bibtex",
|
||||
"chunkedhtml",
|
||||
"commonmark",
|
||||
"commonmark_x",
|
||||
"context",
|
||||
"csljson",
|
||||
"djot",
|
||||
"docbook",
|
||||
"docbook4",
|
||||
"docbook5",
|
||||
"docx",
|
||||
"dokuwiki",
|
||||
"dzslides",
|
||||
"epub",
|
||||
"epub2",
|
||||
"epub3",
|
||||
"fb2",
|
||||
"gfm",
|
||||
"haddock",
|
||||
"html",
|
||||
"html4",
|
||||
"html5",
|
||||
"icml",
|
||||
"ipynb",
|
||||
"jats",
|
||||
"jats_archiving",
|
||||
"jats_articleauthoring",
|
||||
"jats_publishing",
|
||||
"jira",
|
||||
"json",
|
||||
"latex",
|
||||
"man",
|
||||
"markdown",
|
||||
"markdown_mmd",
|
||||
"markdown_phpextra",
|
||||
"markdown_strict",
|
||||
"markua",
|
||||
"mediawiki",
|
||||
"ms",
|
||||
"muse",
|
||||
"pandoc native",
|
||||
"odt",
|
||||
"opendocument",
|
||||
"opml",
|
||||
"org",
|
||||
"pdf",
|
||||
"plain",
|
||||
"pptx",
|
||||
"revealjs",
|
||||
"rst",
|
||||
"rtf",
|
||||
"s5",
|
||||
"slideous",
|
||||
"slidy",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export function convert(
|
||||
filePath: string,
|
||||
fileType: string,
|
||||
convertTo: string,
|
||||
targetPath: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
options?: unknown,
|
||||
): Promise<string> {
|
||||
// set xelatex here
|
||||
const xelatex = ["pdf", "latex"];
|
||||
let option = "";
|
||||
if (xelatex.includes(convertTo)) {
|
||||
option = "--pdf-engine=xelatex";
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(
|
||||
`pandoc ${option} "${filePath}" -f ${fileType} -t ${convertTo} -o "${targetPath}"`,
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(`error: ${error}`);
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(`stdout: ${stdout}`);
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
console.error(`stderr: ${stderr}`);
|
||||
}
|
||||
|
||||
resolve("Done");
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
import { execFile } from "node:child_process";
|
||||
|
||||
export const properties = {
|
||||
from: {
|
||||
text: [
|
||||
"textile",
|
||||
"tikiwiki",
|
||||
"tsv",
|
||||
"twiki",
|
||||
"typst",
|
||||
"vimwiki",
|
||||
"biblatex",
|
||||
"bibtex",
|
||||
"bits",
|
||||
"commonmark",
|
||||
"commonmark_x",
|
||||
"creole",
|
||||
"csljson",
|
||||
"csv",
|
||||
"djot",
|
||||
"docbook",
|
||||
"docx",
|
||||
"dokuwiki",
|
||||
"endnotexml",
|
||||
"epub",
|
||||
"fb2",
|
||||
"gfm",
|
||||
"haddock",
|
||||
"html",
|
||||
"ipynb",
|
||||
"jats",
|
||||
"jira",
|
||||
"json",
|
||||
"latex",
|
||||
"man",
|
||||
"markdown",
|
||||
"markdown_mmd",
|
||||
"markdown_phpextra",
|
||||
"markdown_strict",
|
||||
"mediawiki",
|
||||
"muse",
|
||||
"pandoc native",
|
||||
"opml",
|
||||
"org",
|
||||
"ris",
|
||||
"rst",
|
||||
"rtf",
|
||||
"t2t",
|
||||
],
|
||||
},
|
||||
to: {
|
||||
text: [
|
||||
"tei",
|
||||
"texinfo",
|
||||
"textile",
|
||||
"typst",
|
||||
"xwiki",
|
||||
"zimwiki",
|
||||
"asciidoc",
|
||||
"asciidoc_legacy",
|
||||
"asciidoctor",
|
||||
"beamer",
|
||||
"biblatex",
|
||||
"bibtex",
|
||||
"chunkedhtml",
|
||||
"commonmark",
|
||||
"commonmark_x",
|
||||
"context",
|
||||
"csljson",
|
||||
"djot",
|
||||
"docbook",
|
||||
"docbook4",
|
||||
"docbook5",
|
||||
"docx",
|
||||
"dokuwiki",
|
||||
"dzslides",
|
||||
"epub",
|
||||
"epub2",
|
||||
"epub3",
|
||||
"fb2",
|
||||
"gfm",
|
||||
"haddock",
|
||||
"html",
|
||||
"html4",
|
||||
"html5",
|
||||
"icml",
|
||||
"ipynb",
|
||||
"jats",
|
||||
"jats_archiving",
|
||||
"jats_articleauthoring",
|
||||
"jats_publishing",
|
||||
"jira",
|
||||
"json",
|
||||
"latex",
|
||||
"man",
|
||||
"markdown",
|
||||
"markdown_mmd",
|
||||
"markdown_phpextra",
|
||||
"markdown_strict",
|
||||
"markua",
|
||||
"mediawiki",
|
||||
"ms",
|
||||
"muse",
|
||||
"pandoc native",
|
||||
"odt",
|
||||
"opendocument",
|
||||
"opml",
|
||||
"org",
|
||||
"pdf",
|
||||
"plain",
|
||||
"pptx",
|
||||
"revealjs",
|
||||
"rst",
|
||||
"rtf",
|
||||
"s5",
|
||||
"slideous",
|
||||
"slidy",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export function convert(
|
||||
filePath: string,
|
||||
fileType: string,
|
||||
convertTo: string,
|
||||
targetPath: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
options?: unknown,
|
||||
): Promise<string> {
|
||||
// set xelatex here
|
||||
const xelatex = ["pdf", "latex"];
|
||||
|
||||
// Build arguments array
|
||||
const args: string[] = [];
|
||||
|
||||
if (xelatex.includes(convertTo)) {
|
||||
args.push("--pdf-engine=xelatex");
|
||||
}
|
||||
|
||||
args.push(filePath);
|
||||
args.push("-f", fileType);
|
||||
args.push("-t", convertTo);
|
||||
args.push("-o", targetPath);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
execFile("pandoc", args, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(`error: ${error}`);
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(`stdout: ${stdout}`);
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
console.error(`stderr: ${stderr}`);
|
||||
}
|
||||
|
||||
resolve("Done");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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,37 +1,37 @@
|
||||
import { exec } from "node:child_process";
|
||||
|
||||
export const properties = {
|
||||
from: {
|
||||
images: ["svg"],
|
||||
},
|
||||
to: {
|
||||
images: ["png"],
|
||||
},
|
||||
};
|
||||
|
||||
export function convert(
|
||||
filePath: string,
|
||||
fileType: string,
|
||||
convertTo: string,
|
||||
targetPath: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
options?: unknown,
|
||||
): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(`resvg "${filePath}" "${targetPath}"`, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(`error: ${error}`);
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(`stdout: ${stdout}`);
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
console.error(`stderr: ${stderr}`);
|
||||
}
|
||||
|
||||
resolve("Done");
|
||||
});
|
||||
});
|
||||
}
|
||||
import { execFile } from "node:child_process";
|
||||
|
||||
export const properties = {
|
||||
from: {
|
||||
images: ["svg"],
|
||||
},
|
||||
to: {
|
||||
images: ["png"],
|
||||
},
|
||||
};
|
||||
|
||||
export function convert(
|
||||
filePath: string,
|
||||
fileType: string,
|
||||
convertTo: string,
|
||||
targetPath: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
options?: unknown,
|
||||
): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
execFile("resvg", [filePath, targetPath], (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(`error: ${error}`);
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(`stdout: ${stdout}`);
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
console.error(`stderr: ${stderr}`);
|
||||
}
|
||||
|
||||
resolve("Done");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,142 +1,142 @@
|
||||
import { exec } from "node:child_process";
|
||||
|
||||
|
||||
// declare possible conversions
|
||||
export const properties = {
|
||||
from: {
|
||||
images: [
|
||||
"avif",
|
||||
"bif",
|
||||
"csv",
|
||||
"exr",
|
||||
"fits",
|
||||
"gif",
|
||||
"hdr.gz",
|
||||
"hdr",
|
||||
"heic",
|
||||
"heif",
|
||||
"img.gz",
|
||||
"img",
|
||||
"j2c",
|
||||
"j2k",
|
||||
"jp2",
|
||||
"jpeg",
|
||||
"jpx",
|
||||
"jxl",
|
||||
"mat",
|
||||
"mrxs",
|
||||
"ndpi",
|
||||
"nia.gz",
|
||||
"nia",
|
||||
"nii.gz",
|
||||
"nii",
|
||||
"pdf",
|
||||
"pfm",
|
||||
"pgm",
|
||||
"pic",
|
||||
"png",
|
||||
"ppm",
|
||||
"raw",
|
||||
"scn",
|
||||
"svg",
|
||||
"svs",
|
||||
"svslide",
|
||||
"szi",
|
||||
"tif",
|
||||
"tiff",
|
||||
"v",
|
||||
"vips",
|
||||
"vms",
|
||||
"vmu",
|
||||
"webp",
|
||||
"zip",
|
||||
],
|
||||
},
|
||||
to: {
|
||||
images: [
|
||||
"avif",
|
||||
"dzi",
|
||||
"fits",
|
||||
"gif",
|
||||
"hdr.gz",
|
||||
"heic",
|
||||
"heif",
|
||||
"img.gz",
|
||||
"j2c",
|
||||
"j2k",
|
||||
"jp2",
|
||||
"jpeg",
|
||||
"jpx",
|
||||
"jxl",
|
||||
"mat",
|
||||
"nia.gz",
|
||||
"nia",
|
||||
"nii.gz",
|
||||
"nii",
|
||||
"png",
|
||||
"tiff",
|
||||
"vips",
|
||||
"webp",
|
||||
],
|
||||
},
|
||||
options: {
|
||||
svg: {
|
||||
scale: {
|
||||
description: "Scale the image up or down",
|
||||
type: "number",
|
||||
default: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function convert(
|
||||
filePath: string,
|
||||
fileType: string,
|
||||
convertTo: string,
|
||||
targetPath: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
options?: unknown,
|
||||
): Promise<string> {
|
||||
// if (fileType === "svg") {
|
||||
// const scale = options.scale || 1;
|
||||
// const metadata = await sharp(filePath).metadata();
|
||||
|
||||
// if (!metadata || !metadata.width || !metadata.height) {
|
||||
// throw new Error("Could not get metadata from image");
|
||||
// }
|
||||
|
||||
// const newWidth = Math.round(metadata.width * scale);
|
||||
// const newHeight = Math.round(metadata.height * scale);
|
||||
|
||||
// return await sharp(filePath)
|
||||
// .resize(newWidth, newHeight)
|
||||
// .toFormat(convertTo)
|
||||
// .toFile(targetPath);
|
||||
// }
|
||||
let action = "copy";
|
||||
if (fileType === "pdf") {
|
||||
action = "pdfload";
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(
|
||||
`vips ${action} "${filePath}" "${targetPath}"`,
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(`error: ${error}`);
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(`stdout: ${stdout}`);
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
console.error(`stderr: ${stderr}`);
|
||||
}
|
||||
|
||||
resolve("Done");
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
import { execFile } from "node:child_process";
|
||||
|
||||
// declare possible conversions
|
||||
export const properties = {
|
||||
from: {
|
||||
images: [
|
||||
"avif",
|
||||
"bif",
|
||||
"csv",
|
||||
"exr",
|
||||
"fits",
|
||||
"gif",
|
||||
"hdr.gz",
|
||||
"hdr",
|
||||
"heic",
|
||||
"heif",
|
||||
"img.gz",
|
||||
"img",
|
||||
"j2c",
|
||||
"j2k",
|
||||
"jp2",
|
||||
"jpeg",
|
||||
"jpx",
|
||||
"jxl",
|
||||
"mat",
|
||||
"mrxs",
|
||||
"ndpi",
|
||||
"nia.gz",
|
||||
"nia",
|
||||
"nii.gz",
|
||||
"nii",
|
||||
"pdf",
|
||||
"pfm",
|
||||
"pgm",
|
||||
"pic",
|
||||
"png",
|
||||
"ppm",
|
||||
"raw",
|
||||
"scn",
|
||||
"svg",
|
||||
"svs",
|
||||
"svslide",
|
||||
"szi",
|
||||
"tif",
|
||||
"tiff",
|
||||
"v",
|
||||
"vips",
|
||||
"vms",
|
||||
"vmu",
|
||||
"webp",
|
||||
"zip",
|
||||
],
|
||||
},
|
||||
to: {
|
||||
images: [
|
||||
"avif",
|
||||
"dzi",
|
||||
"fits",
|
||||
"gif",
|
||||
"hdr.gz",
|
||||
"heic",
|
||||
"heif",
|
||||
"img.gz",
|
||||
"j2c",
|
||||
"j2k",
|
||||
"jp2",
|
||||
"jpeg",
|
||||
"jpx",
|
||||
"jxl",
|
||||
"mat",
|
||||
"nia.gz",
|
||||
"nia",
|
||||
"nii.gz",
|
||||
"nii",
|
||||
"png",
|
||||
"tiff",
|
||||
"vips",
|
||||
"webp",
|
||||
],
|
||||
},
|
||||
options: {
|
||||
svg: {
|
||||
scale: {
|
||||
description: "Scale the image up or down",
|
||||
type: "number",
|
||||
default: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function convert(
|
||||
filePath: string,
|
||||
fileType: string,
|
||||
convertTo: string,
|
||||
targetPath: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
options?: unknown,
|
||||
): Promise<string> {
|
||||
// if (fileType === "svg") {
|
||||
// const scale = options.scale || 1;
|
||||
// const metadata = await sharp(filePath).metadata();
|
||||
|
||||
// if (!metadata || !metadata.width || !metadata.height) {
|
||||
// throw new Error("Could not get metadata from image");
|
||||
// }
|
||||
|
||||
// const newWidth = Math.round(metadata.width * scale);
|
||||
// const newHeight = Math.round(metadata.height * scale);
|
||||
|
||||
// return await sharp(filePath)
|
||||
// .resize(newWidth, newHeight)
|
||||
// .toFormat(convertTo)
|
||||
// .toFile(targetPath);
|
||||
// }
|
||||
let action = "copy";
|
||||
if (fileType === "pdf") {
|
||||
action = "pdfload";
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
execFile(
|
||||
"vips",
|
||||
[action, filePath, targetPath],
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(`error: ${error}`);
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(`stdout: ${stdout}`);
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
console.error(`stderr: ${stderr}`);
|
||||
}
|
||||
|
||||
resolve("Done");
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,46 +1,53 @@
|
||||
import { exec } from "node:child_process";
|
||||
|
||||
export const properties = {
|
||||
from: {
|
||||
text: ["tex", "latex"],
|
||||
},
|
||||
to: {
|
||||
text: ["pdf"],
|
||||
},
|
||||
};
|
||||
|
||||
export function convert(
|
||||
filePath: string,
|
||||
fileType: string,
|
||||
convertTo: string,
|
||||
targetPath: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
options?: unknown,
|
||||
): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// const fileName: string = (targetPath.split("/").pop() as string).replace(".pdf", "")
|
||||
const outputPath = targetPath
|
||||
.split("/")
|
||||
.slice(0, -1)
|
||||
.join("/")
|
||||
.replace("./", "");
|
||||
exec(
|
||||
`latexmk -xelatex -interaction=nonstopmode -output-directory="${outputPath}" "${filePath}"`,
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(`error: ${error}`);
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(`stdout: ${stdout}`);
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
console.error(`stderr: ${stderr}`);
|
||||
}
|
||||
|
||||
resolve("Done");
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
import { execFile } from "node:child_process";
|
||||
|
||||
export const properties = {
|
||||
from: {
|
||||
text: ["tex", "latex"],
|
||||
},
|
||||
to: {
|
||||
text: ["pdf"],
|
||||
},
|
||||
};
|
||||
|
||||
export function convert(
|
||||
filePath: string,
|
||||
fileType: string,
|
||||
convertTo: string,
|
||||
targetPath: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
options?: unknown,
|
||||
): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// const fileName: string = (targetPath.split("/").pop() as string).replace(".pdf", "")
|
||||
const outputPath = targetPath
|
||||
.split("/")
|
||||
.slice(0, -1)
|
||||
.join("/")
|
||||
.replace("./", "");
|
||||
|
||||
execFile(
|
||||
"latexmk",
|
||||
[
|
||||
"-xelatex",
|
||||
"-interaction=nonstopmode",
|
||||
`-output-directory=${outputPath}`,
|
||||
filePath,
|
||||
],
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(`error: ${error}`);
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(`stdout: ${stdout}`);
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
console.error(`stderr: ${stderr}`);
|
||||
}
|
||||
|
||||
resolve("Done");
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { exec } from "node:child_process";
|
||||
import { version } from "../../package.json";
|
||||
|
||||
console.log(`ConvertX v${version}`);
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
@@ -99,7 +100,7 @@ if (process.env.NODE_ENV === "production") {
|
||||
}
|
||||
|
||||
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) => {
|
||||
if (error) {
|
||||
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";
|
||||
|
||||
export const generateTailwind = async () => {
|
||||
const result = await Bun.file("./src/main.css")
|
||||
.text()
|
||||
.then((sourceText) => {
|
||||
const config = "./tailwind.config.js";
|
||||
|
||||
return postcss([tw(config)]).process(sourceText, {
|
||||
return postcss([tailwind]).process(sourceText, {
|
||||
from: "./src/main.css",
|
||||
to: "./public/generated.css",
|
||||
});
|
||||
|
||||
269
src/index.tsx
269
src/index.tsx
@@ -1,12 +1,12 @@
|
||||
import { randomInt, randomUUID } from "node:crypto";
|
||||
import { rmSync } from "node:fs";
|
||||
import { mkdir, unlink } from "node:fs/promises";
|
||||
import cookie from "@elysiajs/cookie";
|
||||
import { html, Html } from "@elysiajs/html";
|
||||
import { jwt, type JWTPayloadSpec } from "@elysiajs/jwt";
|
||||
import { staticPlugin } from "@elysiajs/static";
|
||||
import { Database } from "bun:sqlite";
|
||||
import { Elysia, t } from "elysia";
|
||||
import sanitize from "sanitize-filename";
|
||||
import { BaseHtml } from "./components/base";
|
||||
import { Header } from "./components/header";
|
||||
import {
|
||||
@@ -36,6 +36,8 @@ const ALLOW_UNAUTHENTICATED =
|
||||
const AUTO_DELETE_EVERY_N_HOURS = process.env.AUTO_DELETE_EVERY_N_HOURS
|
||||
? Number(process.env.AUTO_DELETE_EVERY_N_HOURS)
|
||||
: 24;
|
||||
const HIDE_HISTORY =
|
||||
process.env.HIDE_HISTORY?.toLowerCase() === "true" || false;
|
||||
|
||||
const WEBROOT = process.env.WEBROOT ?? "";
|
||||
|
||||
@@ -116,7 +118,6 @@ const app = new Elysia({
|
||||
},
|
||||
prefix: WEBROOT,
|
||||
})
|
||||
.use(cookie())
|
||||
.use(html())
|
||||
.use(
|
||||
jwt({
|
||||
@@ -153,7 +154,12 @@ const app = new Elysia({
|
||||
|
||||
return (
|
||||
<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>
|
||||
<article class="article p-0">
|
||||
<header class="w-full bg-neutral-800 p-4">
|
||||
@@ -166,7 +172,7 @@ const app = new Elysia({
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
class="rounded bg-neutral-800 p-3"
|
||||
class="rounded-sm bg-neutral-800 p-3"
|
||||
placeholder="Email"
|
||||
autocomplete="email"
|
||||
required
|
||||
@@ -177,7 +183,7 @@ const app = new Elysia({
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
class="rounded bg-neutral-800 p-3"
|
||||
class="rounded-sm bg-neutral-800 p-3"
|
||||
placeholder="Password"
|
||||
autocomplete="current-password"
|
||||
required
|
||||
@@ -217,7 +223,12 @@ const app = new Elysia({
|
||||
accountRegistration={ACCOUNT_REGISTRATION}
|
||||
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
|
||||
/>
|
||||
<main class="w-full px-4">
|
||||
<main
|
||||
class={`
|
||||
w-full px-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
<article class="article">
|
||||
<form method="post" class="flex flex-col gap-4">
|
||||
<fieldset class="mb-4 flex flex-col gap-4">
|
||||
@@ -226,7 +237,7 @@ const app = new Elysia({
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
class="rounded bg-neutral-800 p-3"
|
||||
class="rounded-sm bg-neutral-800 p-3"
|
||||
placeholder="Email"
|
||||
autocomplete="email"
|
||||
required
|
||||
@@ -237,7 +248,7 @@ const app = new Elysia({
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
class="rounded bg-neutral-800 p-3"
|
||||
class="rounded-sm bg-neutral-800 p-3"
|
||||
placeholder="Password"
|
||||
autocomplete="current-password"
|
||||
required
|
||||
@@ -342,8 +353,14 @@ const app = new Elysia({
|
||||
webroot={WEBROOT}
|
||||
accountRegistration={ACCOUNT_REGISTRATION}
|
||||
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
|
||||
hideHistory={HIDE_HISTORY}
|
||||
/>
|
||||
<main class="w-full px-4">
|
||||
<main
|
||||
class={`
|
||||
w-full px-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
<article class="article">
|
||||
<form method="post" class="flex flex-col gap-4">
|
||||
<fieldset class="mb-4 flex flex-col gap-4">
|
||||
@@ -352,7 +369,7 @@ const app = new Elysia({
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
class="rounded bg-neutral-800 p-3"
|
||||
class="rounded-sm bg-neutral-800 p-3"
|
||||
placeholder="Email"
|
||||
autocomplete="email"
|
||||
required
|
||||
@@ -363,7 +380,7 @@ const app = new Elysia({
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
class="rounded bg-neutral-800 p-3"
|
||||
class="rounded-sm bg-neutral-800 p-3"
|
||||
placeholder="Password"
|
||||
autocomplete="current-password"
|
||||
required
|
||||
@@ -556,16 +573,21 @@ const app = new Elysia({
|
||||
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
|
||||
loggedIn
|
||||
/>
|
||||
<main class="w-full px-4">
|
||||
<main
|
||||
class={`
|
||||
w-full px-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
<article class="article">
|
||||
<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
|
||||
id="file-list"
|
||||
class={`
|
||||
w-full table-auto rounded bg-neutral-900
|
||||
[&_td]:p-4
|
||||
[&_tr]:rounded [&_tr]:border-b [&_tr]:border-neutral-800
|
||||
[&_td]:p-4 [&_td]:first:max-w-[30vw] [&_td]:first:truncate
|
||||
[&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
@@ -574,8 +596,8 @@ const app = new Elysia({
|
||||
class={`
|
||||
relative flex h-48 w-full items-center justify-center rounded border border-dashed
|
||||
border-neutral-700 transition-all
|
||||
[&.dragover]:border-4 [&.dragover]:border-neutral-500
|
||||
hover:border-neutral-600
|
||||
[&.dragover]:border-4 [&.dragover]:border-neutral-500
|
||||
`}
|
||||
>
|
||||
<span>
|
||||
@@ -601,13 +623,13 @@ const app = new Elysia({
|
||||
name="convert_to_search"
|
||||
placeholder="Search for conversions"
|
||||
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">
|
||||
<article
|
||||
class={`
|
||||
convert_to_popup absolute z-[2] m-0 hidden h-[30vh] max-h-[50vh] w-full
|
||||
flex-col overflow-y-auto overflow-x-hidden rounded bg-neutral-800
|
||||
convert_to_popup absolute z-2 m-0 hidden h-[30vh] max-h-[50vh] w-full flex-col
|
||||
overflow-x-hidden overflow-y-auto rounded bg-neutral-800
|
||||
sm:h-[30vh]
|
||||
`}
|
||||
>
|
||||
@@ -672,7 +694,7 @@ const app = new Elysia({
|
||||
</article>
|
||||
<input
|
||||
class={`
|
||||
btn-primary w-full
|
||||
btn-primary w-full opacity-100
|
||||
disabled:cursor-not-allowed disabled:opacity-50
|
||||
`}
|
||||
type="submit"
|
||||
@@ -693,8 +715,8 @@ const app = new Elysia({
|
||||
<>
|
||||
<article
|
||||
class={`
|
||||
convert_to_popup absolute z-[2] m-0 hidden h-[50vh] max-h-[50vh] w-full flex-col
|
||||
overflow-y-auto overflow-x-hidden rounded bg-neutral-800
|
||||
convert_to_popup absolute z-2 m-0 hidden h-[50vh] max-h-[50vh] w-full flex-col
|
||||
overflow-x-hidden overflow-y-auto rounded bg-neutral-800
|
||||
sm:h-[30vh]
|
||||
`}
|
||||
>
|
||||
@@ -866,6 +888,10 @@ const app = new Elysia({
|
||||
const converterName = body.convert_to.split(",")[1];
|
||||
const fileNames = JSON.parse(body.file_names) as string[];
|
||||
|
||||
for (let i = 0; i < fileNames.length; i++) {
|
||||
fileNames[i] = sanitize(fileNames[i] || "");
|
||||
}
|
||||
|
||||
if (!Array.isArray(fileNames) || fileNames.length === 0) {
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
}
|
||||
@@ -930,6 +956,10 @@ const app = new Elysia({
|
||||
},
|
||||
)
|
||||
.get("/history", async ({ jwt, redirect, cookie: { auth } }) => {
|
||||
if (HIDE_HISTORY) {
|
||||
return redirect(`${WEBROOT}/`, 302);
|
||||
}
|
||||
|
||||
if (!auth?.value) {
|
||||
return redirect(`${WEBROOT}/login`, 302);
|
||||
}
|
||||
@@ -942,7 +972,8 @@ const app = new Elysia({
|
||||
let userJobs = db
|
||||
.query("SELECT * FROM jobs WHERE user_id = ?")
|
||||
.as(Jobs)
|
||||
.all(user.id);
|
||||
.all(user.id)
|
||||
.reverse();
|
||||
|
||||
for (const job of userJobs) {
|
||||
const files = db
|
||||
@@ -962,33 +993,77 @@ const app = new Elysia({
|
||||
<Header
|
||||
webroot={WEBROOT}
|
||||
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
|
||||
hideHistory={HIDE_HISTORY}
|
||||
loggedIn
|
||||
/>
|
||||
<main class="w-full px-4">
|
||||
<main
|
||||
class={`
|
||||
w-full px-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
<article class="article">
|
||||
<h1 class="mb-4 text-xl">Results</h1>
|
||||
<table
|
||||
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
|
||||
[&_tr]:rounded [&_tr]:border-b [&_tr]:border-neutral-800
|
||||
[&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
|
||||
`}
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-4 py-2">Time</th>
|
||||
<th class="px-4 py-2">Files</th>
|
||||
<th class="px-4 py-2">Files Done</th>
|
||||
<th class="px-4 py-2">Status</th>
|
||||
<th class="px-4 py-2">View</th>
|
||||
<th
|
||||
class={`
|
||||
px-2 py-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
Time
|
||||
</th>
|
||||
<th
|
||||
class={`
|
||||
px-2 py-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
Files
|
||||
</th>
|
||||
<th
|
||||
class={`
|
||||
px-2 py-2
|
||||
max-sm:hidden
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
Files Done
|
||||
</th>
|
||||
<th
|
||||
class={`
|
||||
px-2 py-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
Status
|
||||
</th>
|
||||
<th
|
||||
class={`
|
||||
px-2 py-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
View
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{userJobs.map((job) => (
|
||||
<tr>
|
||||
<td safe>{job.date_created}</td>
|
||||
<td safe>
|
||||
{new Date(job.date_created).toLocaleTimeString()}
|
||||
</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>
|
||||
<a
|
||||
@@ -1055,7 +1130,12 @@ const app = new Elysia({
|
||||
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
|
||||
loggedIn
|
||||
/>
|
||||
<main class="w-full px-4">
|
||||
<main
|
||||
class={`
|
||||
w-full px-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
<article class="article">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h1 class="text-xl">Results</h1>
|
||||
@@ -1078,33 +1158,63 @@ const app = new Elysia({
|
||||
max={job.num_files}
|
||||
value={files.length}
|
||||
class={`
|
||||
mb-4 inline-block h-2 w-full appearance-none overflow-hidden rounded-full
|
||||
border-0 bg-neutral-700 bg-none text-accent-500 accent-accent-500
|
||||
[&::-moz-progress-bar]:bg-neutral-700 [&::-webkit-progress-value]:rounded-full
|
||||
[&::-webkit-progress-value]:[background:none]
|
||||
text-accent-500 accent-accent-500 mb-4 inline-block h-2 w-full appearance-none
|
||||
overflow-hidden rounded-full border-0 bg-neutral-700 bg-none
|
||||
[&[value]::-webkit-progress-value]:bg-accent-500
|
||||
[&[value]::-webkit-progress-value]:transition-[inline-size]
|
||||
[&::-moz-progress-bar]:bg-neutral-700 [&::-webkit-progress-value]:rounded-full
|
||||
[&::-webkit-progress-value]:[background:none]
|
||||
`}
|
||||
/>
|
||||
<table
|
||||
class={`
|
||||
w-full table-auto rounded bg-neutral-900 text-left
|
||||
[&_td]:p-4
|
||||
[&_tr]:rounded [&_tr]:border-b [&_tr]:border-neutral-800
|
||||
[&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
|
||||
`}
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-4 py-2">Converted File Name</th>
|
||||
<th class="px-4 py-2">Status</th>
|
||||
<th class="px-4 py-2">View</th>
|
||||
<th class="px-4 py-2">Download</th>
|
||||
<th
|
||||
class={`
|
||||
px-2 py-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
Converted File Name
|
||||
</th>
|
||||
<th
|
||||
class={`
|
||||
px-2 py-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
Status
|
||||
</th>
|
||||
<th
|
||||
class={`
|
||||
px-2 py-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
View
|
||||
</th>
|
||||
<th
|
||||
class={`
|
||||
px-2 py-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
Download
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{files.map((file) => (
|
||||
<tr>
|
||||
<td safe>{file.output_file_name}</td>
|
||||
<td safe class="max-w-[20vw] truncate">
|
||||
{file.output_file_name}
|
||||
</td>
|
||||
<td safe>{file.status}</td>
|
||||
<td>
|
||||
<a
|
||||
@@ -1200,33 +1310,63 @@ const app = new Elysia({
|
||||
max={job.num_files}
|
||||
value={files.length}
|
||||
class={`
|
||||
mb-4 inline-block h-2 w-full appearance-none overflow-hidden rounded-full border-0
|
||||
bg-neutral-700 bg-none text-accent-500 accent-accent-500
|
||||
[&::-moz-progress-bar]:bg-neutral-700 [&::-webkit-progress-value]:rounded-full
|
||||
[&::-webkit-progress-value]:[background:none]
|
||||
text-accent-500 accent-accent-500 mb-4 inline-block h-2 w-full appearance-none
|
||||
overflow-hidden rounded-full border-0 bg-neutral-700 bg-none
|
||||
[&[value]::-webkit-progress-value]:bg-accent-500
|
||||
[&[value]::-webkit-progress-value]:transition-[inline-size]
|
||||
[&::-moz-progress-bar]:bg-neutral-700 [&::-webkit-progress-value]:rounded-full
|
||||
[&::-webkit-progress-value]:[background:none]
|
||||
`}
|
||||
/>
|
||||
<table
|
||||
class={`
|
||||
w-full table-auto rounded bg-neutral-900 text-left
|
||||
[&_td]:p-4
|
||||
[&_tr]:rounded [&_tr]:border-b [&_tr]:border-neutral-800
|
||||
[&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
|
||||
`}
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-4 py-2">Converted File Name</th>
|
||||
<th class="px-4 py-2">Status</th>
|
||||
<th class="px-4 py-2">View</th>
|
||||
<th class="px-4 py-2">Download</th>
|
||||
<th
|
||||
class={`
|
||||
px-2 py-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
Converted File Name
|
||||
</th>
|
||||
<th
|
||||
class={`
|
||||
px-2 py-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
Status
|
||||
</th>
|
||||
<th
|
||||
class={`
|
||||
px-2 py-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
View
|
||||
</th>
|
||||
<th
|
||||
class={`
|
||||
px-2 py-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
Download
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{files.map((file) => (
|
||||
<tr>
|
||||
<td safe>{file.output_file_name}</td>
|
||||
<td safe class="max-w-[20vw] truncate">
|
||||
{file.output_file_name}
|
||||
</td>
|
||||
<td safe>{file.status}</td>
|
||||
<td>
|
||||
<a
|
||||
@@ -1281,7 +1421,7 @@ const app = new Elysia({
|
||||
// parse from url encoded string
|
||||
const userId = decodeURIComponent(params.userId);
|
||||
const jobId = decodeURIComponent(params.jobId);
|
||||
const fileName = decodeURIComponent(params.fileName);
|
||||
const fileName = sanitize(decodeURIComponent(params.fileName));
|
||||
|
||||
const filePath = `${outputDir}${userId}/${jobId}/${fileName}`;
|
||||
return Bun.file(filePath);
|
||||
@@ -1305,14 +1445,19 @@ const app = new Elysia({
|
||||
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
|
||||
loggedIn
|
||||
/>
|
||||
<main class="w-full px-4">
|
||||
<main
|
||||
class={`
|
||||
w-full px-2
|
||||
sm:px-4
|
||||
`}
|
||||
>
|
||||
<article class="article">
|
||||
<h1 class="mb-4 text-xl">Converters</h1>
|
||||
<table
|
||||
class={`
|
||||
w-full table-auto rounded bg-neutral-900 text-left
|
||||
[&_td]:p-4
|
||||
[&_tr]:rounded [&_tr]:border-b [&_tr]:border-neutral-800
|
||||
[&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800
|
||||
[&_ul]:list-inside [&_ul]:list-disc
|
||||
`}
|
||||
>
|
||||
@@ -1420,8 +1565,14 @@ const clearJobs = () => {
|
||||
|
||||
for (const job of jobs) {
|
||||
// delete the directories
|
||||
rmSync(`${outputDir}${job.user_id}/${job.id}`, { recursive: true });
|
||||
rmSync(`${uploadsDir}${job.user_id}/${job.id}`, { recursive: true });
|
||||
rmSync(`${outputDir}${job.user_id}/${job.id}`, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
rmSync(`${uploadsDir}${job.user_id}/${job.id}`, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
|
||||
// delete the job
|
||||
db.query("DELETE FROM jobs WHERE id = ?").run(job.id);
|
||||
|
||||
79
src/main.css
79
src/main.css
@@ -1,31 +1,64 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import "tailwindcss";
|
||||
|
||||
@layer components {
|
||||
.article {
|
||||
@apply p-4 mb-4 bg-neutral-800/40 w-full mx-auto max-w-4xl rounded;
|
||||
}
|
||||
.btn-primary {
|
||||
@apply bg-accent-500 text-contrast rounded p-4 hover:bg-accent-400 cursor-pointer transition-colors;
|
||||
@plugin 'tailwind-scrollbar';
|
||||
|
||||
@theme {
|
||||
--color-contrast: rgba(var(--contrast));
|
||||
--color-neutral-900: rgba(var(--neutral-900));
|
||||
--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 {
|
||||
--contrast: 255, 255, 255;
|
||||
--neutral-900: 243, 244, 246;
|
||||
--neutral-800: 229, 231, 235;
|
||||
--neutral-700: 209, 213, 219;
|
||||
--neutral-600: 156, 163, 175;
|
||||
--neutral-500: 180, 180, 180;
|
||||
--neutral-400: 75, 85, 99;
|
||||
--neutral-300: 55, 65, 81;
|
||||
--neutral-200: 31, 41, 55;
|
||||
--neutral-100: 17, 24, 39;
|
||||
--accent-400: 132, 204, 22;
|
||||
--accent-500: 101, 163, 13;
|
||||
--accent-600: 77, 124, 15;
|
||||
}
|
||||
--contrast: 255, 255, 255;
|
||||
--neutral-900: 243, 244, 246;
|
||||
--neutral-800: 229, 231, 235;
|
||||
--neutral-700: 209, 213, 219;
|
||||
--neutral-600: 156, 163, 175;
|
||||
--neutral-500: 180, 180, 180;
|
||||
--neutral-400: 75, 85, 99;
|
||||
--neutral-300: 55, 65, 81;
|
||||
--neutral-200: 31, 41, 55;
|
||||
--neutral-100: 17, 24, 39;
|
||||
--accent-400: 132, 204, 22;
|
||||
--accent-500: 101, 163, 13;
|
||||
--accent-600: 77, 124, 15;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
|
||||
@@ -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