mirror of
https://github.com/C4illin/ConvertX.git
synced 2025-10-28 10:33:30 +00:00
Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3af9688c6 | ||
|
|
7d0cbb9844 | ||
|
|
88173891ba | ||
|
|
2b4b8f9551 | ||
|
|
63a4328d4a | ||
|
|
413f5dc7b4 | ||
|
|
ebccdf9169 | ||
|
|
47139a550b | ||
|
|
fa5446c446 | ||
|
|
8772e582b0 | ||
|
|
45922ed3a3 | ||
|
|
4c747e8908 | ||
|
|
e573997aa9 | ||
|
|
c57b69991c | ||
|
|
eee983a56a | ||
|
|
22f823c535 | ||
|
|
ed59cd7aa4 | ||
|
|
b28977ffe2 | ||
|
|
a47bb682a5 | ||
|
|
a17eca0a09 | ||
|
|
ea9250543e | ||
|
|
317c932c2a | ||
|
|
5b1703db68 | ||
|
|
60ba7c93fb | ||
|
|
22227130dd | ||
|
|
5daf66f5d0 | ||
|
|
aee1962607 | ||
|
|
0d42762b36 | ||
|
|
b97b12b449 | ||
|
|
bdf651df82 | ||
|
|
267ef14789 | ||
|
|
905adc5e1c | ||
|
|
52ed7274e9 | ||
|
|
a29238c265 | ||
|
|
48c6fb79fc | ||
|
|
8358396656 | ||
|
|
b30e5800c3 | ||
|
|
21a1b50ed8 | ||
|
|
e6a94fb21d | ||
|
|
bef1710e33 | ||
|
|
16b322d4e6 | ||
|
|
9bf64e42d5 | ||
|
|
5988fe8212 | ||
|
|
5df9c0b751 | ||
|
|
136a8b2d74 | ||
|
|
ccfb574d5d | ||
|
|
ad6eedea69 |
7
.deepsource.toml
Normal file
7
.deepsource.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
version = 1
|
||||
|
||||
[[analyzers]]
|
||||
name = "javascript"
|
||||
|
||||
[analyzers.meta]
|
||||
environment = ["nodejs"]
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -47,4 +47,5 @@ package-lock.json
|
||||
/db
|
||||
/data
|
||||
/Bruno
|
||||
/tsconfig.tsbuildinfo
|
||||
/tsconfig.tsbuildinfo
|
||||
/src/public/generated.css
|
||||
43
CHANGELOG.md
43
CHANGELOG.md
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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" ]
|
||||
16
Dockerfile
16
Dockerfile
@@ -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 . .
|
||||
|
||||
15
README.md
15
README.md
@@ -7,11 +7,12 @@
|
||||

|
||||

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

|
||||
|
||||
## 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
|
||||
|
||||
20
biome.json
20
biome.json
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
compose.yaml
10
compose.yaml
@@ -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
|
||||
|
||||
@@ -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
BIN
images/preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
56
package.json
56
package.json
@@ -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
9
postcss.config.cjs
Normal 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: {} } : {})
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
139
src/converters/assimp.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
17
src/helpers/tailwind.ts
Normal 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;
|
||||
};
|
||||
411
src/index.tsx
411
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 } 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
12
src/main.css
Normal 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;
|
||||
}
|
||||
}
|
||||
4
src/public/pico.lime.min.css
vendored
4
src/public/pico.lime.min.css
vendored
File diff suppressed because one or more lines are too long
@@ -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();
|
||||
|
||||
|
||||
@@ -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
9
tailwind.config.js
Normal 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')],
|
||||
}
|
||||
Reference in New Issue
Block a user