Compare commits

47 Commits

Author SHA1 Message Date
Emrik Östling
d3af9688c6 Merge pull request #149 from C4illin/release-please--branches--main--components--convertx-frontend
chore(main): release 0.7.0
2024-09-28 11:11:11 +02:00
Emrik Östling
7d0cbb9844 chore(main): release 0.7.0 2024-09-26 23:37:45 +02:00
C4illin
88173891ba fix: wrong layout on search with few options 2024-09-26 23:37:19 +02:00
Emrik Östling
2b4b8f9551 Merge pull request #148 from Aymendje/feat-assimp 2024-09-26 23:01:33 +02:00
Aymen Djellal
63a4328d4a feat: Add support for 3d assets through assimp converter
This is a start for #144
It does not support all the 3d formats, but its a good few
2024-09-26 22:55:42 +02:00
Emrik Östling
413f5dc7b4 Merge pull request #138 from C4illin/release-please--branches--main--components--convertx-frontend 2024-09-25 23:56:29 +02:00
Emrik Östling
ebccdf9169 chore(main): release 0.6.0 2024-09-25 23:47:47 +02:00
C4illin
47139a550b fix: rename css file to force update cache, fixes #141 2024-09-25 23:47:18 +02:00
C4illin
fa5446c446 chore: fix eslint config 2024-09-25 23:46:21 +02:00
Emrik Östling
8772e582b0 Merge pull request #142 from Aymendje/patch-2 2024-09-25 23:31:23 +02:00
Aymen Djellal
45922ed3a3 [FIX] Fix broken CSS
This is a fix for #141 
The CSS was broken due to the import in the tailwind.config.js, it should be inside the module.exports and not outside. Thats why bun was giving a warning before :

```
# bun run dev
warn - The `content` option in your Tailwind CSS configuration is missing or empty.
warn - Configure your content sources or your generated CSS will be missing styles.
warn - https://tailwindcss.com/docs/content-configuration
```

it is now fixed
2024-09-25 09:12:54 -04:00
C4illin
4c747e8908 chore: format and update deps 2024-09-24 23:49:14 +02:00
Emrik Östling
e573997aa9 Merge pull request #140 from Aymendje/patch-1
Fix UNAUTHENTICATED mode
2024-09-24 09:12:28 +02:00
Aymen Djellal
c57b69991c Fix UNAUTHENTICATED mode
the function used here, randmInt(Min, Max) has an issue.
When running the code, I get a 500 error, with the error being 

```
 |       const newUserId = String(randomInt(2 ** 24,  Number.MAX_SAFE_INTEGER));
      ^
RangeError: The "max - min" is out of range. It must be <= 281474976710655. Received 9007199237963775
 code: "ERR_OUT_OF_RANGE"

      at randomInt (native:1:1)
      at /.../ConvertX/src/index.tsx:460:32
      at /.../ConvertX/src/index.tsx:594:29
      at file:///.../ConvertX/node_modules/elysia/dist/bun/index.js:76:22
```

When digging deeper in the implementation, it seems that the official node doc says : 
> The range (max - min) must be less than 2**48. min and max must be safe integers.

See : https://nodejs.org/api/crypto.html#cryptorandomintmin-max-callback

Feel free to close this PR and do the fix another way (it: by using a uuid instead of randomInt, etc.)
2024-09-23 11:44:37 -04:00
Emrik Östling
eee983a56a Merge pull request #137 from C4illin/feature-tailwind
feat: ui remake with tailwind
2024-09-23 10:20:01 +02:00
C4illin
22f823c535 feat: ui remake with tailwind 2024-09-23 03:58:29 +02:00
Emrik Östling
ed59cd7aa4 chore: clarify that batch processing is possible 2024-09-22 23:53:37 +02:00
Emrik Östling
b28977ffe2 Merge pull request #136 from C4illin/release-please--branches--main--components--convertx-frontend 2024-09-21 14:33:34 +02:00
deepsource-io[bot]
a47bb682a5 ci: add .deepsource.toml 2024-09-20 14:28:59 +00:00
C4illin
a17eca0a09 chore: format 2024-09-20 13:27:54 +02:00
Emrik Östling
ea9250543e chore(main): release 0.5.0 2024-09-20 13:24:55 +02:00
C4illin
317c932c2a feat: add option to customize how often files are automatically deleted 2024-09-20 13:24:18 +02:00
C4illin
5b1703db68 chore: add safe attribute to input element 2024-09-20 12:55:00 +02:00
C4illin
60ba7c93fb fix: improve file name replacement logic 2024-09-20 12:49:19 +02:00
C4illin
22227130dd chore: add screenshot, fixes #110 2024-09-20 12:42:58 +02:00
Emrik Östling
5daf66f5d0 Merge pull request #135 from C4illin/renovate/oven-bun-1.x 2024-09-20 12:30:55 +02:00
renovate[bot]
aee1962607 chore(deps): update oven/bun docker tag to v1.1.29 2024-09-20 09:09:55 +00:00
Emrik Östling
0d42762b36 Merge pull request #134 from C4illin/renovate/biomejs-biome-1.x 2024-09-19 17:51:12 +02:00
renovate[bot]
b97b12b449 chore(deps): update dependency @biomejs/biome to v1.9.2 2024-09-19 14:43:48 +00:00
Emrik Östling
bdf651df82 Merge pull request #133 from C4illin/renovate/oven-bun-1.x
chore(deps): update oven/bun docker tag to v1.1.28
2024-09-19 09:35:06 +02:00
renovate[bot]
267ef14789 chore(deps): update oven/bun docker tag to v1.1.28 2024-09-18 20:12:09 +00:00
Emrik Östling
905adc5e1c Merge pull request #130 from C4illin/release-please--branches--main--components--convertx-frontend 2024-09-18 16:19:47 +02:00
Emrik Östling
52ed7274e9 chore(main): release 0.4.1 2024-09-15 23:24:06 +02:00
Emrik Östling
a29238c265 Merge pull request #132 from C4illin/renovate/biomejs-biome-1.x
chore(deps): update dependency @biomejs/biome to v1.9.1
2024-09-15 23:23:41 +02:00
renovate[bot]
48c6fb79fc chore(deps): update dependency @biomejs/biome to v1.9.1 2024-09-15 20:09:54 +00:00
Emrik Östling
8358396656 Merge pull request #131 from C4illin/renovate/biomejs-biome-1.x 2024-09-12 17:40:23 +02:00
renovate[bot]
b30e5800c3 chore(deps): update dependency @biomejs/biome to v1.9.0 2024-09-12 15:03:57 +00:00
Emrik Östling
21a1b50ed8 Merge pull request #129 from C4illin/fix/#122/lowercase-env-variables 2024-09-12 13:02:45 +02:00
C4illin
e6a94fb21d chore: format 2024-09-12 12:59:59 +02:00
C4illin
bef1710e33 fix: allow non lowercase true and false values, fixes #122 2024-09-12 12:58:28 +02:00
Emrik Östling
16b322d4e6 Merge pull request #128 from C4illin/renovate/eslint-plugin-isaacscript-4.x
chore(deps): update dependency eslint-plugin-isaacscript to v4
2024-09-12 09:32:38 +02:00
renovate[bot]
9bf64e42d5 chore(deps): update dependency eslint-plugin-isaacscript to v4 2024-09-11 21:56:10 +00:00
C4illin
5988fe8212 chore: fix docker run command fixes #127 2024-09-09 13:17:37 +02:00
Emrik Östling
5df9c0b751 Merge pull request #126 from C4illin/renovate/oven-bun-1.x
chore(deps): update oven/bun docker tag to v1.1.27
2024-09-07 20:40:34 +02:00
renovate[bot]
136a8b2d74 chore(deps): update oven/bun docker tag to v1.1.27 2024-09-07 13:48:15 +00:00
C4illin
ccfb574d5d chore: Update dependencies 2024-09-05 12:22:27 +02:00
Emrik Östling
ad6eedea69 chore: Update README.md 2024-08-27 18:19:07 +02:00
28 changed files with 661 additions and 436 deletions

7
.deepsource.toml Normal file
View File

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

3
.gitignore vendored
View File

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

View File

@@ -1,5 +1,48 @@
# Changelog
## [0.7.0](https://github.com/C4illin/ConvertX/compare/v0.6.0...v0.7.0) (2024-09-26)
### Features
* Add support for 3d assets through assimp converter ([63a4328](https://github.com/C4illin/ConvertX/commit/63a4328d4a1e01df3e0ec4a877bad8c8ffe71129))
### Bug Fixes
* wrong layout on search with few options ([8817389](https://github.com/C4illin/ConvertX/commit/88173891ba2d69da46eda46f3f598a9b54f26f96))
## [0.6.0](https://github.com/C4illin/ConvertX/compare/v0.5.0...v0.6.0) (2024-09-25)
### Features
* ui remake with tailwind ([22f823c](https://github.com/C4illin/ConvertX/commit/22f823c535b20382981f86a13616b830a1f3392f))
### Bug Fixes
* rename css file to force update cache, fixes [#141](https://github.com/C4illin/ConvertX/issues/141) ([47139a5](https://github.com/C4illin/ConvertX/commit/47139a550bd3d847da288c61bf8f88953b79c673))
## [0.5.0](https://github.com/C4illin/ConvertX/compare/v0.4.1...v0.5.0) (2024-09-20)
### Features
* add option to customize how often files are automatically deleted ([317c932](https://github.com/C4illin/ConvertX/commit/317c932c2a26280bf37ed3d3bf9b879413590f5a))
### Bug Fixes
* improve file name replacement logic ([60ba7c9](https://github.com/C4illin/ConvertX/commit/60ba7c93fbdc961f3569882fade7cc13dee7a7a5))
## [0.4.1](https://github.com/C4illin/ConvertX/compare/v0.4.0...v0.4.1) (2024-09-15)
### Bug Fixes
* allow non lowercase true and false values, fixes [#122](https://github.com/C4illin/ConvertX/issues/122) ([bef1710](https://github.com/C4illin/ConvertX/commit/bef1710e3376baa7e25c107ded20a40d18b8c6b0))
## [0.4.0](https://github.com/C4illin/ConvertX/compare/v0.3.3...v0.4.0) (2024-08-26)

View File

@@ -1,63 +0,0 @@
FROM oven/bun:1-debian as base
WORKDIR /app
# install dependencies into temp directory
# 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/
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/
RUN cd /temp/prod && bun install --frozen-lockfile --production
# FROM base AS install-libjxl-tools
# download
# copy node_modules from temp directory
# then copy all (non-ignored) project files into the image
# FROM base AS prerelease
# COPY --from=install /temp/dev/node_modules node_modules
# COPY . .
# # [optional] tests & build
# ENV NODE_ENV=production
# RUN bun test
# 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"
# install additional dependencies
RUN rm -rf /var/lib/apt/lists/partial && apt-get update -o Acquire::CompressionTypes::Order::=gz \
&& apt-get install -y \
pandoc \
texlive-latex-recommended \
texlive-fonts-recommended \
texlive-latex-extra \
ffmpeg \
graphicsmagick \
ghostscript \
libvips-tools
# # libjxl is not available in the official debian repositories
# RUN wget https://github.com/libjxl/libjxl/releases/download/v0.10.2/jxl-debs-amd64-debian-bullseye-v0.10.2.tar.gz -O /tmp/jxl-debs-amd64-debian-bullseye-v0.10.2.tar.gz \
# && mkdir -p /tmp/libjxl \
# && tar -xvf /tmp/jxl-debs-amd64-debian-bullseye-v0.10.2.tar.gz -C /tmp/libjxl \
# && dpkg -i /tmp/libjxl/libjxl_0.10.2_amd64.deb /tmp/libjxl/jxl_0.10.2_amd64.deb \
# && rm -rf /tmp/jxl-debs-amd64-debian-bullseye-v0.10.2.tar.gz /tmp/libjxl
COPY --from=install /temp/prod/node_modules node_modules
# COPY --from=prerelease /app/src/index.tsx /app/src/
# COPY --from=prerelease /app/package.json .
COPY . .
EXPOSE 3000/tcp
ENTRYPOINT [ "bun", "run", "./src/index.tsx" ]

View File

@@ -1,4 +1,4 @@
FROM oven/bun:1.1.26-alpine AS base
FROM oven/bun:1.1.29-alpine AS base
LABEL org.opencontainers.image.source="https://github.com/C4illin/ConvertX"
WORKDIR /app
@@ -22,14 +22,14 @@ RUN cargo install resvg
# copy node_modules from temp directory
# then copy all (non-ignored) project files into the image
# FROM base AS prerelease
# COPY --from=install /temp/dev/node_modules node_modules
# COPY . .
FROM base AS prerelease
COPY --from=install /temp/dev/node_modules node_modules
COPY . .
# # [optional] tests & build
# ENV NODE_ENV=production
ENV NODE_ENV=production
# RUN bun test
# RUN bun run build
RUN bun run build
# copy production dependencies and source code into final image
FROM base AS release
@@ -49,13 +49,15 @@ RUN apk --no-cache add \
vips-tools \
vips-poppler \
vips-jxl \
libjxl-tools
libjxl-tools \
assimp
# this might be needed for some latex use cases, will add it if needed.
# texmf-dist-fontsextra \
COPY --from=install /temp/prod/node_modules node_modules
COPY --from=builder /root/.cargo/bin/resvg /usr/local/bin/resvg
COPY --from=prerelease /app/src/public/generated.css /app/src/public/
# COPY --from=prerelease /app/src/index.tsx /app/src/
# COPY --from=prerelease /app/package.json .
COPY . .

View File

@@ -7,11 +7,12 @@
![Docker container size](https://ghcr-badge.egpl.dev/c4illin/convertx/size?color=%230375b6&tag=latest&label=image+size&trim=)
![GitHub top language](https://img.shields.io/github/languages/top/C4illin/ConvertX)
A self-hosted online file converter. Supports 831 different formats. Written with TypeScript, Bun and Elysia.
A self-hosted online file converter. Supports over a thousand different formats. Written with TypeScript, Bun and Elysia.
## Features
- Convert files to different formats
- Process multiple files at once
- Password protection
- Multiple accounts
@@ -22,6 +23,7 @@ A self-hosted online file converter. Supports 831 different formats. Written wit
| [libjxl](https://github.com/libjxl/libjxl) | JPEG XL | 11 | 11 |
| [resvg](https://github.com/RazrFalcon/resvg) | SVG | 1 | 1 |
| [Vips](https://github.com/libvips/libvips) | Images | 45 | 23 |
| [Assimp](https://github.com/assimp/assimp) | 3D Assets | 70 | 24 |
| [XeLaTeX](https://tug.org/xetex/) | LaTeX | 1 | 1 |
| [Pandoc](https://pandoc.org/) | Documents | 43 | 65 |
| [GraphicsMagick](http://www.graphicsmagick.org/) | Images | 166 | 133 |
@@ -47,6 +49,7 @@ services:
- JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234 # will use randomUUID() by default
- HTTP_ALLOWED=false # setting this to true is unsafe, only set this to true locally
- ALLOW_UNAUTHENTICATED=false # allows anyone to use the service without logging in, only set this to true locally
- AUTO_DELETE_EVERY_N_HOURS=24 # checks every n hours for files older then n hours and deletes them, set to 0 to disable
volumes:
- convertx:/app/data
```
@@ -54,7 +57,7 @@ services:
or
```bash
docker run ghcr.io/c4illin/convertx -p 3000:3000 -v ./data:/app/data
docker run -p 3000:3000 -v ./data:/app/data ghcr.io/c4illin/convertx
```
Then visit `http://localhost:3000` in your browser and create your account. Don't leave it unconfigured and open, as anyone can register the first account.
@@ -65,6 +68,10 @@ If you get unable to open database file run `chown -R $USER:$USER path` on the p
Tutorial in french: https://belginux.com/installer-convertx-avec-docker/
## Screenshots
![ConvertX Preview](images/preview.png)
## Development
0. Install [Bun](https://bun.sh/) and Git
@@ -76,12 +83,12 @@ Pull requests are welcome! See below and open issues for the list of todos.
## Todo
- [x] Add messages for errors in converters
- [x] Add searchable list of formats
- [ ] Add options for converters
- [ ] Add more converters
- [ ] Divide index.tsx into smaller components
- [ ] Add tests
- [ ] Add searchable list of formats
- [ ] Make the upload button nicer and more easy to drop files on. Support copy paste as well if possible.
- [ ] Make errors logs visible from the web ui
- [ ] Add more converters:
- [ ] [deark](https://github.com/jsummers/deark)
- [ ] LibreOffice

View File

@@ -10,9 +10,14 @@
"attributePosition": "auto"
},
"files": {
"ignore": ["**/node_modules/**"]
"ignore": [
"**/node_modules/**",
"**/pico.lime.min.css"
]
},
"organizeImports": {
"enabled": true
},
"organizeImports": { "enabled": true },
"linter": {
"enabled": true,
"rules": {
@@ -25,7 +30,11 @@
"useLiteralKeys": "error",
"useOptionalChain": "error"
},
"correctness": { "noPrecisionLoss": "error", "noUnusedVariables": "off" },
"correctness": {
"noPrecisionLoss": "error",
"noUnusedVariables": "off",
"useJsxKeyInIterable": "off"
},
"style": {
"noInferrableTypes": "error",
"noNamespace": "error",
@@ -45,6 +54,9 @@
"noUnsafeDeclarationMerging": "error",
"useAwait": "error",
"useNamespaceKeyword": "error"
},
"nursery": {
"useSortedClasses": "error"
}
}
},
@@ -60,4 +72,4 @@
"attributePosition": "auto"
}
}
}
}

BIN
bun.lockb

Binary file not shown.

View File

@@ -5,9 +5,11 @@ services:
# dockerfile: Debian.Dockerfile
volumes:
- ./data:/app/data
environment:
- ACCOUNT_REGISTRATION=true
- JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234
- ALLOW_UNAUTHENTICATED=true
environment: # Defaults are listed below. All are optional.
- ACCOUNT_REGISTRATION=true # true or false, doesn't matter for the first account (e.g. keep this to false if you only want one account)
- JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234 # will use randomUUID() by default
- HTTP_ALLOWED=true # setting this to true is unsafe, only set this to true locally
- ALLOW_UNAUTHENTICATED=true # allows anyone to use the service without logging in, only set this to true locally
- AUTO_DELETE_EVERY_N_HOURS=1 # checks every n hours for files older then n hours and deletes them, set to 0 to disable
ports:
- 3000:3000

View File

@@ -1,28 +1,27 @@
import comments from "@eslint-community/eslint-plugin-eslint-comments/configs";
import { fixupPluginRules } from "@eslint/compat";
import tseslint from "typescript-eslint";
import eslint from "@eslint/js";
import js from "@eslint/js";
import deprecationPlugin from "eslint-plugin-deprecation";
import eslintCommentsPlugin from "eslint-plugin-eslint-comments";
import importPlugin from "eslint-plugin-import";
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(
js.configs.recommended,
importPlugin.flatConfigs.recommended,
comments.recommended,
...tseslint.configs.recommended,
...tailwind.configs["flat/recommended"],
{
plugins: {
"@typescript-eslint": tseslint.plugin,
deprecation: fixupPluginRules(deprecationPlugin),
"eslint-comments": eslintCommentsPlugin,
import: fixupPluginRules(importPlugin),
"simple-import-sort": simpleImportSortPlugin,
},
},
{
ignores: ["**/node_modules/**", "**/public/**"],
},
eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
...tseslint.configs.stylisticTypeChecked,
{
languageOptions: {
parserOptions: {
projectService: true,
@@ -31,6 +30,26 @@ export default tseslint.config(
sourceType: "module",
project: ["./tsconfig.json"],
},
globals: {
...globals.node,
},
},
files: ["**/*.{js,mjs,cjs}"],
rules: {
"tailwindcss/no-custom-classname": [
"error",
{
config: "./tailwind.config.js",
whitelist: [
"select_container",
"convert_to_popup",
"convert_to_group",
"target",
"convert_to_target",
],
},
],
"import/no-named-as-default": "off",
},
},
);

BIN
images/preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -1,11 +1,11 @@
{
"name": "convertx-frontend",
"version": "0.4.0",
"version": "0.7.0",
"scripts": {
"dev": "bun run --watch src/index.tsx",
"hot": "bun run --hot src/index.tsx",
"format": "biome format --write ./src",
"css": "cpy 'node_modules/@picocss/pico/css/pico.lime.min.css' 'src/public/' --flat",
"build": "postcss ./src/main.css -o ./src/public/generated.css",
"lint": "run-p 'lint:*'",
"lint:tsc": "tsc --noEmit",
"lint:knip": "knip",
@@ -14,9 +14,9 @@
"dependencies": {
"@elysiajs/cookie": "^0.8.0",
"@elysiajs/html": "1.0.2",
"@elysiajs/jwt": "^1.1.0",
"@elysiajs/jwt": "^1.1.1",
"@elysiajs/static": "1.0.3",
"elysia": "^1.1.7"
"elysia": "^1.1.16"
},
"module": "src/index.tsx",
"type": "module",
@@ -24,34 +24,44 @@
"start": "bun run src/index.tsx"
},
"devDependencies": {
"@biomejs/biome": "1.8.3",
"@biomejs/biome": "1.9.2",
"@eslint-community/eslint-plugin-eslint-comments": "^4.4.0",
"@eslint/compat": "^1.1.1",
"@eslint/js": "^9.9.0",
"@eslint/js": "^9.11.1",
"@ianvs/prettier-plugin-sort-imports": "^4.3.1",
"@kitajs/ts-html-plugin": "^4.0.2",
"@kitajs/ts-html-plugin": "^4.1.0",
"@picocss/pico": "^2.0.6",
"@total-typescript/ts-reset": "^0.6.0",
"@types/bun": "^1.1.6",
"@types/eslint": "^9.6.0",
"@types/node": "^22.5.0",
"@typescript-eslint/eslint-plugin": "^8.2.0",
"@typescript-eslint/parser": "^8.2.0",
"cpy-cli": "^5.0.0",
"eslint": "^9.9.0",
"@total-typescript/ts-reset": "^0.6.1",
"@types/bun": "^1.1.10",
"@types/eslint": "^9.6.1",
"@types/eslint-plugin-tailwindcss": "^3.17.0",
"@types/eslint__js": "^8.42.3",
"@types/node": "^22.6.1",
"@typescript-eslint/eslint-plugin": "^8.7.0",
"@typescript-eslint/parser": "^8.7.0",
"autoprefixer": "^10.4.20",
"cssnano": "^7.0.6",
"eslint": "^9.11.1",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-deprecation": "^3.0.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-isaacscript": "^3.12.2",
"eslint-plugin-import": "^2.30.0",
"eslint-plugin-isaacscript": "^4.0.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-simple-import-sort": "^12.1.1",
"knip": "^5.27.3",
"npm-run-all2": "^6.2.2",
"eslint-plugin-tailwindcss": "^3.17.4",
"globals": "^15.9.0",
"knip": "^5.30.5",
"npm-run-all2": "^6.2.3",
"postcss": "^8.4.47",
"postcss-cli": "^11.0.0",
"postcss-lightningcss": "^1.0.1",
"prettier": "^3.3.3",
"typescript": "^5.5.4",
"typescript-eslint": "^8.2.0"
"tailwind-scrollbar": "^3.1.0",
"tailwindcss": "^3.4.13",
"typescript": "^5.6.2",
"typescript-eslint": "^8.7.0"
},
"trustedDependencies": [
"@biomejs/biome"
]
}
}

9
postcss.config.cjs Normal file
View File

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

View File

@@ -7,8 +7,7 @@ export const BaseHtml = ({
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title safe>{title}</title>
<link rel="stylesheet" href="/pico.lime.min.css" />
<link rel="stylesheet" href="/style.css" />
<link rel="stylesheet" href="/generated.css" />
<link
rel="apple-touch-icon"
sizes="180x180"
@@ -28,6 +27,6 @@ export const BaseHtml = ({
/>
<link rel="manifest" href="/site.webmanifest" />
</head>
<body>{children}</body>
<body class="w-full bg-gray-900 text-gray-200">{children}</body>
</html>
);

View File

@@ -5,44 +5,53 @@ export const Header = ({
let rightNav: JSX.Element;
if (loggedIn) {
rightNav = (
<ul>
<ul class="flex gap-4 ">
<li>
<a href="/history">History</a>
<a
class="text-lime-600 transition-all hover:text-lime-500 hover:underline"
href="/history">
History
</a>
</li>
<li>
<a href="/logoff">Logout</a>
<a
class="text-lime-600 transition-all hover:text-lime-500 hover:underline"
href="/logoff">
Logout
</a>
</li>
</ul>
);
} else {
rightNav = (
<ul>
<ul class="flex gap-4">
<li>
<a href="/login">Login</a>
<a
class="text-lime-600 transition-all hover:text-lime-500 hover:underline"
href="/login">
Login
</a>
</li>
{accountRegistration && (
{accountRegistration ? (
<li>
<a href="/register">Register</a>
<a
class="text-lime-600 transition-all hover:text-lime-500 hover:underline"
href="/register">
Register
</a>
</li>
)}
) : null}
</ul>
);
}
return (
<header class="container">
<nav>
<header class="w-full p-4">
<nav class="mx-auto flex max-w-4xl justify-between rounded bg-gray-900 p-4">
<ul>
<li>
<strong>
<a
href="/"
style={{
textDecoration: "none",
color: "inherit",
}}>
ConvertX
</a>
<a href="/">ConvertX</a>
</strong>
</li>
</ul>

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

@@ -0,0 +1,139 @@
import { exec } from "node:child_process";
// This could be done dynamically by running `ffmpeg -formats` and parsing the output
export const properties = {
from: {
muxer: [
"3d",
"3ds",
"3mf",
"ac",
"ac3d",
"acc",
"amf",
"ase",
"ask",
"assbin",
"b3d",
"blend",
"bsp",
"bvh",
"cob",
"csm",
"dae",
"dxf",
"enff",
"fbx",
"glb",
"gltf",
"hmp",
"ifc",
"ifczip",
"iqm",
"irr",
"irrmesh",
"lwo",
"lws",
"lxo",
"md2",
"md3",
"md5anim",
"md5camera",
"md5mesh",
"mdc",
"mdl",
"mesh",
"mesh.xml",
"mot",
"ms3d",
"ndo",
"nff",
"obj",
"off",
"ogex",
"pk3",
"ply",
"pmx",
"prj",
"q3o",
"q3s",
"raw",
"scn",
"sib",
"smd",
"step",
"stl",
"stp",
"ter",
"uc",
"vta",
"x",
"x3d",
"x3db",
"xgl",
"xml",
"zae",
"zgl",
],
},
to: {
muxer: [
"collada",
"x",
"stp",
"obj",
"objnomtl",
"stl",
"stlb",
"ply",
"plyb",
"3ds",
"gltf2",
"glb2",
"gltf",
"glb",
"assbin",
"assxml",
"x3d",
"fbx",
"fbxa",
"m3d",
"m3da",
"3mf",
"pbrt",
"assjson",
],
},
};
export async function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
options?: any,
): Promise<string> {
// let command = "ffmpeg";
const command = `assimp export "${filePath}" "${targetPath}"`;
return new Promise((resolve, reject) => {
exec(command, (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("success");
});
});
}

View File

@@ -143,6 +143,7 @@ export const properties = {
"svgz",
"text",
"tga",
"tif",
"tiff",
"tile",
"tim",
@@ -227,7 +228,6 @@ export const properties = {
"jbig",
"jng",
"jpeg",
"jpg",
"k",
"m",
"m2v",

View File

@@ -1,4 +1,7 @@
import { convert as convertImage, properties as propertiesImage } from "./vips";
import {
convert as convertImage,
properties as propertiesImage
} from "./vips";
import {
convert as convertPandoc,
@@ -30,24 +33,24 @@ import {
properties as propertiesresvg,
} from "./resvg";
import {
convert as convertassimp,
properties as propertiesassimp,
} from "./assimp";
import { normalizeFiletype } from "../helpers/normalizeFiletype";
// This should probably be reconstructed so that the functions are not imported instead the functions hook into this to make the converters more modular
const properties: {
[key: string]: {
const properties: Record<string, {
properties: {
from: { [key: string]: string[] };
to: { [key: string]: string[] };
options?: {
[key: string]: {
[key: string]: {
from: Record<string, string[]>;
to: Record<string, string[]>;
options?: Record<string, Record<string, {
description: string;
type: string;
default: number;
};
};
};
}>>;
};
converter: (
filePath: string,
@@ -58,8 +61,7 @@ const properties: {
options?: any,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
) => any;
};
} = {
}> = {
libjxl: {
properties: propertiesLibjxl,
converter: convertLibjxl,
@@ -84,6 +86,10 @@ const properties: {
properties: propertiesGraphicsmagick,
converter: convertGraphicsmagick,
},
assimp: {
properties: propertiesassimp,
converter: convertassimp,
},
ffmpeg: {
properties: propertiesFFmpeg,
converter: convertFFmpeg,
@@ -159,7 +165,7 @@ export async function mainConverter(
}
}
const possibleTargets: { [key: string]: { [key: string]: string[] } } = {};
const possibleTargets: Record<string, Record<string, string[]>> = {};
for (const converterName in properties) {
const converterProperties = properties[converterName]?.properties;
@@ -186,7 +192,7 @@ for (const converterName in properties) {
export const getPossibleTargets = (
from: string,
): { [key: string]: string[] } => {
): Record<string, string[]> => {
const fromClean = normalizeFiletype(from);
return possibleTargets[fromClean] || {};
@@ -214,7 +220,7 @@ const getPossibleInputs = () => {
return possibleInputs;
};
const allTargets: { [key: string]: string[] } = {};
const allTargets: Record<string, string[]> = {};
for (const converterName in properties) {
const converterProperties = properties[converterName]?.properties;
@@ -236,7 +242,7 @@ export const getAllTargets = () => {
return allTargets;
};
const allInputs: { [key: string]: string[] } = {};
const allInputs: Record<string, string[]> = {};
for (const converterName in properties) {
const converterProperties = properties[converterName]?.properties;

View File

@@ -9,7 +9,6 @@ export const properties = {
},
};
export function convert(
filePath: string,
fileType: string,
@@ -19,23 +18,20 @@ export function convert(
options?: any,
): Promise<string> {
return new Promise((resolve, reject) => {
exec(
`resvg "${filePath}" "${targetPath}"`,
(error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
exec(`resvg "${filePath}" "${targetPath}"`, (error, stdout, stderr) => {
if (error) {
reject(`error: ${error}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
resolve("success");
},
);
resolve("success");
});
});
}
}

View File

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

17
src/helpers/tailwind.ts Normal file
View File

@@ -0,0 +1,17 @@
import tw from "tailwindcss";
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, {
from: "./src/main.css",
to: "./public/generated.css",
});
});
return result;
};

View File

@@ -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 } 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 { randomInt, randomUUID } from "node:crypto";
import { rmSync } from "node:fs";
import { mkdir, unlink } from "node:fs/promises";
import { BaseHtml } from "./components/base";
import { Header } from "./components/header";
import {
@@ -27,11 +27,15 @@ const uploadsDir = "./data/uploads/";
const outputDir = "./data/output/";
const ACCOUNT_REGISTRATION =
process.env.ACCOUNT_REGISTRATION === "true" || false;
process.env.ACCOUNT_REGISTRATION?.toLowerCase() === "true" || false;
const HTTP_ALLOWED = process.env.HTTP_ALLOWED === "true" || false;
const HTTP_ALLOWED =
process.env.HTTP_ALLOWED?.toLowerCase() === "true" || false;
const ALLOW_UNAUTHENTICATED =
process.env.ALLOW_UNAUTHENTICATED === "true" || false;
process.env.ALLOW_UNAUTHENTICATED?.toLowerCase() === "true" || false;
const AUTO_DELETE_EVERY_N_HOURS = process.env.AUTO_DELETE_EVERY_N_HOURS
? Number(process.env.AUTO_DELETE_EVERY_N_HOURS)
: 24;
// fileNames: fileNames,
// filesToConvert: fileNames.length,
@@ -134,36 +138,46 @@ const app = new Elysia({
return (
<BaseHtml title="ConvertX | Setup">
<main class="container">
<h1>Welcome to ConvertX</h1>
<article>
<header>Create your account</header>
<form method="post" action="/register">
<fieldset>
<label>
Email/Username
<main class="mx-auto w-full max-w-4xl px-4">
<h1 class="my-8 text-3xl">Welcome to ConvertX!</h1>
<article class="article p-0">
<header class="w-full bg-gray-800 p-4">Create your account</header>
<form method="post" action="/register" class="p-4">
<fieldset class="mb-4 flex flex-col gap-4">
<label class="flex flex-col gap-1">
Email
<input
type="email"
name="email"
class="rounded bg-gray-800 p-3"
placeholder="Email"
autocomplete="email"
required
/>
</label>
<label>
<label class="flex flex-col gap-1">
Password
<input
type="password"
name="password"
class="rounded bg-gray-800 p-3"
placeholder="Password"
autocomplete="current-password"
required
/>
</label>
</fieldset>
<input type="submit" value="Create account" />
<input type="submit" value="Create account" class="btn-primary" />
</form>
<footer>
<footer class="p-4">
Report any issues on{" "}
<a href="https://github.com/C4illin/ConvertX">GitHub</a>.
<a
class="text-lime-500 underline hover:text-lime-400"
href="https://github.com/C4illin/ConvertX"
>
GitHub
</a>
.
</footer>
</article>
</main>
@@ -179,32 +193,38 @@ const app = new Elysia({
<BaseHtml title="ConvertX | Register">
<>
<Header accountRegistration={ACCOUNT_REGISTRATION} />
<main class="container">
<article>
<form method="post">
<fieldset>
<label>
<main class="w-full px-4">
<article class="article">
<form method="post" class="flex flex-col gap-4">
<fieldset class="mb-4 flex flex-col gap-4">
<label class="flex flex-col gap-1">
Email
<input
type="email"
name="email"
class="rounded bg-gray-800 p-3"
placeholder="Email"
autocomplete="email"
required
/>
</label>
<label>
<label class="flex flex-col gap-1">
Password
<input
type="password"
name="password"
class="rounded bg-gray-800 p-3"
placeholder="Password"
autocomplete="new-password"
autocomplete="current-password"
required
/>
</label>
</fieldset>
<input type="submit" value="Register" />
<input
type="submit"
value="Register"
class="btn-primary w-full"
/>
</form>
</article>
</main>
@@ -295,25 +315,27 @@ const app = new Elysia({
<BaseHtml title="ConvertX | Login">
<>
<Header accountRegistration={ACCOUNT_REGISTRATION} />
<main class="container">
<article>
<form method="post">
<fieldset>
<label>
<main class="w-full px-4">
<article class="article">
<form method="post" class="flex flex-col gap-4">
<fieldset class="mb-4 flex flex-col gap-4">
<label class="flex flex-col gap-1">
Email
<input
type="email"
name="email"
class="rounded bg-gray-800 p-3"
placeholder="Email"
autocomplete="email"
required
/>
</label>
<label>
<label class="flex flex-col gap-1">
Password
<input
type="password"
name="password"
class="rounded bg-gray-800 p-3"
placeholder="Password"
autocomplete="current-password"
required
@@ -321,12 +343,20 @@ const app = new Elysia({
</label>
</fieldset>
<div role="group">
{ACCOUNT_REGISTRATION && (
<a href="/register" role="button" class="secondary">
{ACCOUNT_REGISTRATION ? (
<a
href="/register"
role="button"
class="btn-primary w-full"
>
Register an account
</a>
)}
<input type="submit" value="Login" />
) : null}
<input
type="submit"
value="Login"
class="btn-primary w-full"
/>
</div>
</form>
</article>
@@ -431,7 +461,12 @@ const app = new Elysia({
}
}
} else if (ALLOW_UNAUTHENTICATED) {
const newUserId = String(randomInt(2 ** 24, Number.MAX_SAFE_INTEGER));
const newUserId = String(
randomInt(
2 ** 24,
Math.min(2 ** 48 + 2 ** 24 - 1, Number.MAX_SAFE_INTEGER),
),
);
const accessToken = await jwt.sign({
id: newUserId,
});
@@ -448,7 +483,7 @@ const app = new Elysia({
value: accessToken,
httpOnly: true,
secure: !HTTP_ALLOWED,
maxAge: 60 * 60 * 24 * 1,
maxAge: 24 * 60 * 60,
sameSite: "strict",
});
}
@@ -487,86 +522,67 @@ const app = new Elysia({
<BaseHtml>
<>
<Header loggedIn />
<main class="container">
<article>
<h1>Convert</h1>
<div style={{ maxHeight: "50vh", overflowY: "auto" }}>
<table id="file-list" class="striped" />
<main class="w-full 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">
<table
id="file-list"
class="w-full table-auto rounded bg-gray-900 [&_td]:p-4 [&_tr]:rounded [&_tr]:border-b [&_tr]:border-gray-800"
/>
</div>
<div
id="dropzone"
class="relative flex h-48 w-full items-center justify-center rounded border border-dashed border-gray-700 transition-all hover:border-gray-600 [&.dragover]:border-4 [&.dragover]:border-gray-500"
>
<span>
<b>Choose a file</b> or drag it here
</span>
<input
type="file"
name="file"
multiple
class="absolute inset-0 size-full cursor-pointer opacity-0"
/>
</div>
<input type="file" name="file" multiple />
{/* <label for="convert_from">Convert from</label> */}
{/* <select name="convert_from" aria-label="Convert from" required>
<option selected disabled value="">
Convert from
</option>
{getPossibleInputs().map((input) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<option>{input}</option>
))}
</select> */}
</article>
<form
method="post"
action="/convert"
style={{ position: "relative" }}>
class="relative mx-auto mb-[35vh] w-full max-w-4xl"
>
<input type="hidden" name="file_names" id="file_names" />
<article>
<article class="article w-full">
<input
type="search"
name="convert_to_search"
placeholder="Search for conversions"
autocomplete="off"
class="w-full rounded bg-gray-800 p-4"
/>
<div class="select_container">
<article
class="convert_to_popup"
hidden
style={{
flexDirection: "column",
display: "flex",
zIndex: 2,
position: "absolute",
maxHeight: "50vh",
width: "90vw",
overflowY: "scroll",
margin: "0px",
overflowX: "hidden",
}}>
<div class="select_container relative">
<article class="convert_to_popup absolute z-[2] m-0 hidden h-[30vh] max-h-[50vh] w-full flex-col overflow-y-auto overflow-x-hidden rounded bg-gray-800 sm:h-[30vh]">
{Object.entries(getAllTargets()).map(
([converter, targets]) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<article
class="convert_to_group"
class="convert_to_group w-full border-b border-gray-700 p-4 flex flex-col"
data-converter={converter}
style={{
borderColor: "gray",
padding: "2px",
}}>
<header
style={{ fontSize: "20px", fontWeight: "bold" }}>
>
<header class="mb-2 w-full text-xl font-bold" safe>
{converter}
</header>
<ul
class="convert_to_target"
style={{
display: "flex",
flexDirection: "row",
gap: "5px",
flexWrap: "wrap",
}}>
<ul class="convert_to_target flex flex-row flex-wrap gap-1">
{targets.map((target) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<button
// https://stackoverflow.com/questions/121499/when-a-blur-event-occurs-how-can-i-find-out-which-element-focus-went-to#comment82388679_33325953
tabindex={0}
class="target"
class="target rounded bg-gray-700 p-1 text-base hover:bg-gray-600"
data-value={`${target},${converter}`}
data-target={target}
data-converter={converter}
style={{ fontSize: "15px", padding: "5px" }}
type="button">
type="button"
safe
>
{target}
</button>
))}
@@ -581,16 +597,15 @@ const app = new Elysia({
name="convert_to"
aria-label="Convert to"
required
hidden>
hidden
>
<option selected disabled value="">
Convert to
</option>
{Object.entries(getAllTargets()).map(
([converter, targets]) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<optgroup label={converter}>
{targets.map((target) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<option value={`${target},${converter}`} safe>
{target}
</option>
@@ -601,7 +616,7 @@ const app = new Elysia({
</select>
</div>
</article>
<input type="submit" value="Convert" />
<input class="btn-primary w-full" type="submit" value="Convert" />
</form>
</main>
<script src="script.js" defer />
@@ -614,53 +629,28 @@ const app = new Elysia({
({ body }) => {
return (
<>
<article
class="convert_to_popup"
hidden
style={{
flexDirection: "column",
display: "flex",
zIndex: 2,
position: "absolute",
maxHeight: "50vh",
width: "90vw",
overflowY: "scroll",
margin: "0px",
overflowX: "hidden",
}}>
<article class="convert_to_popup absolute z-[2] m-0 hidden h-[50vh] max-h-[50vh] w-full flex-col overflow-y-auto overflow-x-hidden rounded bg-gray-800 sm:h-[30vh]">
{Object.entries(getPossibleTargets(body.fileType)).map(
([converter, targets]) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<article
class="convert_to_group"
class="convert_to_group w-full border-b border-gray-700 p-4 flex flex-col"
data-converter={converter}
style={{
borderColor: "gray",
padding: "2px",
}}>
<header style={{ fontSize: "20px", fontWeight: "bold" }}>
>
<header class="mb-2 w-full text-xl font-bold" safe>
{converter}
</header>
<ul
class="convert_to_target"
style={{
display: "flex",
flexDirection: "row",
gap: "5px",
flexWrap: "wrap",
}}>
<ul class="convert_to_target flex flex-row flex-wrap gap-1">
{targets.map((target) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<button
// https://stackoverflow.com/questions/121499/when-a-blur-event-occurs-how-can-i-find-out-which-element-focus-went-to#comment82388679_33325953
tabindex={0}
class="target"
class="target rounded bg-gray-700 p-1 text-base hover:bg-gray-600"
data-value={`${target},${converter}`}
data-target={target}
data-converter={converter}
style={{ fontSize: "15px", padding: "5px" }}
type="button">
type="button"
safe
>
{target}
</button>
))}
@@ -676,10 +666,8 @@ const app = new Elysia({
</option>
{Object.entries(getPossibleTargets(body.fileType)).map(
([converter, targets]) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<optgroup label={converter}>
{targets.map((target) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<option value={`${target},${converter}`} safe>
{target}
</option>
@@ -725,7 +713,7 @@ const app = new Elysia({
await Bun.write(`${userUploadsDir}${file.name}`, file);
}
} else {
// biome-ignore lint/complexity/useLiteralKeys: weird error
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/dot-notation
await Bun.write(`${userUploadsDir}${body.file["name"]}`, body.file);
}
}
@@ -827,7 +815,10 @@ const app = new Elysia({
const fileTypeOrig = fileName.split(".").pop() ?? "";
const fileType = normalizeFiletype(fileTypeOrig);
const newFileExt = normalizeOutputFiletype(convertTo);
const newFileName = fileName.replace(fileTypeOrig, newFileExt);
const newFileName = fileName.replace(
new RegExp(`${fileTypeOrig}(?!.*${fileTypeOrig})`),
newFileExt,
);
const targetPath = `${userOutputDir}${newFileName}`;
const result = await mainConverter(
@@ -899,29 +890,33 @@ const app = new Elysia({
<BaseHtml title="ConvertX | Results">
<>
<Header loggedIn />
<main class="container">
<article>
<h1>Results</h1>
<table>
<main class="w-full px-4">
<article class="article">
<h1 class="mb-4 text-xl">Results</h1>
<table class="w-full table-auto rounded bg-gray-900 text-left [&_td]:p-4 [&_tr]:rounded [&_tr]:border-b [&_tr]:border-gray-800">
<thead>
<tr>
<th>Time</th>
<th>Files</th>
<th>Files Done</th>
<th>Status</th>
<th>View</th>
<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>
</tr>
</thead>
<tbody>
{userJobs.map((job) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<tr>
<td safe>{job.date_created}</td>
<td>{job.num_files}</td>
<td>{job.finished_files}</td>
<td safe>{job.status}</td>
<td>
<a href={`/results/${job.id}`}>View</a>
<a
class="text-lime-500 underline hover:text-lime-400"
href={`/results/${job.id}`}
>
View
</a>
</td>
</tr>
))}
@@ -973,50 +968,58 @@ const app = new Elysia({
<BaseHtml title="ConvertX | Result">
<>
<Header loggedIn />
<main class="container">
<article>
<div class="grid">
<h1>Results</h1>
<main class="w-full px-4">
<article class="article">
<div class="mb-4 flex items-center justify-between">
<h1 class="text-xl">Results</h1>
<div>
<button
type="button"
style={{ width: "10rem", float: "right" }}
class="btn-primary float-right w-40"
onclick="downloadAll()"
{...(files.length !== job.num_files
? { disabled: true, "aria-busy": "true" }
: "")}>
: "")}
>
{files.length === job.num_files
? "Download All"
: "Converting..."}
</button>
</div>
</div>
<progress max={job.num_files} value={files.length} />
<table>
<progress
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-gray-700 bg-none text-lime-500 accent-lime-500 [&::-moz-progress-bar]:bg-gray-700 [&::-webkit-progress-value]:rounded-full [&::-webkit-progress-value]:[background:none] [&[value]::-webkit-progress-value]:bg-lime-500 [&[value]::-webkit-progress-value]:transition-[inline-size]"
/>
<table class="w-full table-auto rounded bg-gray-900 text-left [&_td]:p-4 [&_tr]:rounded [&_tr]:border-b [&_tr]:border-gray-800">
<thead>
<tr>
<th>Converted File Name</th>
<th>Status</th>
<th>View</th>
<th>Download</th>
<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>
</tr>
</thead>
<tbody>
{files.map((file) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<tr>
<td safe>{file.output_file_name}</td>
<td safe>{file.status}</td>
<td>
<a
href={`/download/${outputPath}${file.output_file_name}`}>
class="text-lime-500 underline hover:text-lime-400"
href={`/download/${outputPath}${file.output_file_name}`}
>
View
</a>
</td>
<td>
<a
class="text-lime-500 underline hover:text-lime-400"
href={`/download/${outputPath}${file.output_file_name}`}
download={file.output_file_name}>
download={file.output_file_name}
>
Download
</a>
</td>
@@ -1069,48 +1072,57 @@ const app = new Elysia({
.all(params.jobId);
return (
<article>
<div class="grid">
<h1>Results</h1>
<article class="article">
<div class="mb-4 flex items-center justify-between">
<h1 class="text-xl">Results</h1>
<div>
<button
type="button"
style={{ width: "10rem", float: "right" }}
class="btn-primary float-right w-40"
onclick="downloadAll()"
{...(files.length !== job.num_files
? { disabled: true, "aria-busy": "true" }
: "")}>
: "")}
>
{files.length === job.num_files
? "Download All"
: "Converting..."}
</button>
</div>
</div>
<progress max={job.num_files} value={files.length} />
<table>
<progress
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-gray-700 bg-none text-lime-500 accent-lime-500 [&::-moz-progress-bar]:bg-gray-700 [&::-webkit-progress-value]:rounded-full [&::-webkit-progress-value]:[background:none] [&[value]::-webkit-progress-value]:bg-lime-500 [&[value]::-webkit-progress-value]:transition-[inline-size]"
/>
<table class="w-full table-auto rounded bg-gray-900 text-left [&_td]:p-4 [&_tr]:rounded [&_tr]:border-b [&_tr]:border-gray-800">
<thead>
<tr>
<th>Converted File Name</th>
<th>Status</th>
<th>View</th>
<th>Download</th>
<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>
</tr>
</thead>
<tbody>
{files.map((file) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<tr>
<td safe>{file.output_file_name}</td>
<td safe>{file.status}</td>
<td>
<a href={`/download/${outputPath}${file.output_file_name}`}>
<a
class="text-lime-500 underline hover:text-lime-400"
href={`/download/${outputPath}${file.output_file_name}`}
>
View
</a>
</td>
<td>
<a
class="text-lime-500 underline hover:text-lime-400"
href={`/download/${outputPath}${file.output_file_name}`}
download={file.output_file_name}>
download={file.output_file_name}
>
Download
</a>
</td>
@@ -1164,15 +1176,15 @@ const app = new Elysia({
<BaseHtml title="ConvertX | Converters">
<>
<Header loggedIn />
<main class="container">
<article>
<h1>Converters</h1>
<table>
<main class="w-full px-4">
<article class="article">
<h1 class="mb-4 text-xl">Converters</h1>
<table class="w-full table-auto rounded bg-gray-900 text-left [&_td]:p-4 [&_tr]:rounded [&_tr]:border-b [&_tr]:border-gray-800 [&_ul]:list-inside [&_ul]:list-disc">
<thead>
<tr>
<th>Converter</th>
<th>From (Count)</th>
<th>To (Count)</th>
<th class="mx-4 my-2">Converter</th>
<th class="mx-4 my-2">From (Count)</th>
<th class="mx-4 my-2">To (Count)</th>
</tr>
</thead>
<tbody>
@@ -1180,14 +1192,12 @@ const app = new Elysia({
([converter, targets]) => {
const inputs = getAllInputs(converter);
return (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<tr>
<td safe>{converter}</td>
<td>
Count: {inputs.length}
<ul>
{inputs.map((input) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<li safe>{input}</li>
))}
</ul>
@@ -1196,7 +1206,6 @@ const app = new Elysia({
Count: {targets.length}
<ul>
{targets.map((target) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<li safe>{target}</li>
))}
</ul>
@@ -1244,20 +1253,34 @@ const app = new Elysia({
.onError(({ error }) => {
// log.error(` ${request.method} ${request.url}`, code, error);
console.error(error);
})
.listen(3000);
});
if (process.env.NODE_ENV !== "production") {
await import("./helpers/tailwind").then(async ({ generateTailwind }) => {
const result = await generateTailwind();
app.get("/generated.css", ({ set }) => {
set.headers["content-type"] = "text/css";
return result;
});
});
}
app.listen(3000);
console.log(
`🦊 Elysia is running at http://${app.server?.hostname}:${app.server?.port}`,
);
const clearJobs = () => {
// clear all jobs older than 24 hours
// get all files older than 24 hours
const jobs = db
.query("SELECT * FROM jobs WHERE date_created < ?")
.as(Jobs)
.all(new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString());
.all(
new Date(
Date.now() - AUTO_DELETE_EVERY_N_HOURS * 60 * 60 * 1000,
).toISOString(),
);
for (const job of jobs) {
// delete the directories
@@ -1268,7 +1291,9 @@ const clearJobs = () => {
db.query("DELETE FROM jobs WHERE id = ?").run(job.id);
}
// run every 24 hours
setTimeout(clearJobs, 24 * 60 * 60 * 1000);
setTimeout(clearJobs, AUTO_DELETE_EVERY_N_HOURS * 60 * 60 * 1000);
};
clearJobs();
if (AUTO_DELETE_EVERY_N_HOURS > 0) {
clearJobs();
}

12
src/main.css Normal file
View File

@@ -0,0 +1,12 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
.article {
@apply p-4 mb-4 bg-gray-800/40 w-full mx-auto max-w-4xl rounded;
}
.btn-primary {
@apply bg-lime-500 text-black rounded p-4 hover:bg-lime-400 cursor-pointer;
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,8 +1,17 @@
// Select the file input element
const fileInput = document.querySelector('input[type="file"]');
const dropZone = document.getElementById("dropzone");
const fileNames = [];
let fileType;
dropZone.addEventListener("dragover", (e) => {
dropZone.classList.add("dragover");
});
dropZone.addEventListener("dragleave", (e) => {
dropZone.classList.remove("dragover");
});
const selectContainer = document.querySelector("form .select_container");
const updateSearchBar = () => {
@@ -20,16 +29,20 @@ const updateSearchBar = () => {
for (const target of targets) {
if (target.dataset.target.includes(search)) {
matchingTargetsFound++;
target.hidden = false;
target.classList.remove("hidden");
target.classList.add("flex");
} else {
target.hidden = true;
target.classList.add("hidden");
target.classList.remove("flex");
}
}
if (matchingTargetsFound === 0) {
groupElement.hidden = true;
groupElement.classList.add("hidden");
groupElement.classList.remove("flex");
} else {
groupElement.hidden = false;
groupElement.classList.remove("hidden");
groupElement.classList.add("flex");
}
}
};
@@ -59,15 +72,18 @@ const updateSearchBar = () => {
// 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.hidden = true;
convertToPopup.classList.add("hidden");
convertToPopup.classList.remove("flex");
return;
}
convertToPopup.hidden = true;
convertToPopup.classList.add("hidden");
convertToPopup.classList.remove("flex");
});
convertToInput.addEventListener("focus", () => {
convertToPopup.hidden = false;
convertToPopup.classList.remove("hidden");
convertToPopup.classList.add("flex");
});
};
@@ -94,6 +110,7 @@ fileInput.addEventListener("change", (e) => {
if (!fileType) {
fileType = file.name.split(".").pop();
console.log("fileType", fileType);
fileInput.setAttribute("accept", `.${fileType}`);
setTitle();

View File

@@ -1,59 +0,0 @@
div.icon {
height: 100px;
width: 100px;
}
button[type="submit"] {
width: 50%
}
div.center {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
@media (max-width: 99999999999px) {
.convert_to_popup {
width: 50vw !important;
height: 50vh;
}
}
@media (max-width: 850px) {
.convert_to_popup {
width: 60vw !important;
height: 60vh;
}
}
@media (max-width: 575px) {
.convert_to_popup {
width: 80vw !important;
height: 75vh;
}
}
@media (max-height: 1000px) {
.convert_to_popup {
height: 40vh;
}
}
@media (max-height: 650px) {
.convert_to_popup {
height: 30vh;
}
}
@media (max-height: 500px) {
.convert_to_popup {
height: 25vh;
}
}
@media (max-height: 400px) {
.convert_to_popup {
height: 15vh;
}
}

9
tailwind.config.js Normal file
View File

@@ -0,0 +1,9 @@
/** @type {import('tailwindcss').Config} */
// eslint-disable-next-line no-undef
module.exports = {
content: ['./src/**/*.{html,js,tsx,jsx,cjs,mjs}'],
theme: {
extend: {},
},
plugins: [require('tailwind-scrollbar')],
}