mirror of
				https://github.com/C4illin/ConvertX.git
				synced 2025-10-31 12:03:31 +00:00 
			
		
		
		
	Compare commits
	
		
			17 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | d3af9688c6 | ||
|  | 7d0cbb9844 | ||
|  | 88173891ba | ||
|  | 2b4b8f9551 | ||
|  | 63a4328d4a | ||
|  | 413f5dc7b4 | ||
|  | ebccdf9169 | ||
|  | 47139a550b | ||
|  | fa5446c446 | ||
|  | 8772e582b0 | ||
|  | 45922ed3a3 | ||
|  | 4c747e8908 | ||
|  | e573997aa9 | ||
|  | c57b69991c | ||
|  | eee983a56a | ||
|  | 22f823c535 | ||
|  | ed59cd7aa4 | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -48,3 +48,4 @@ package-lock.json | ||||
| /data | ||||
| /Bruno | ||||
| /tsconfig.tsbuildinfo | ||||
| /src/public/generated.css | ||||
							
								
								
									
										24
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -1,5 +1,29 @@ | ||||
| # 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) | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -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" ] | ||||
							
								
								
									
										14
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								Dockerfile
									
									
									
									
									
								
							| @@ -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 . . | ||||
|   | ||||
| @@ -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         | | ||||
|   | ||||
							
								
								
									
										18
									
								
								biome.json
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								biome.json
									
									
									
									
									
								
							| @@ -10,9 +10,14 @@ | ||||
|     "attributePosition": "auto" | ||||
|   }, | ||||
|   "files": { | ||||
|     "ignore": ["**/node_modules/**", "**/pico.lime.min.css"] | ||||
|     "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" | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   | ||||
							
								
								
									
										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", | ||||
|     }, | ||||
|   }, | ||||
| ); | ||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 53 KiB | 
							
								
								
									
										42
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										42
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,11 +1,11 @@ | ||||
| { | ||||
|   "name": "convertx-frontend", | ||||
|   "version": "0.5.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", | ||||
| @@ -16,7 +16,7 @@ | ||||
|     "@elysiajs/html": "1.0.2", | ||||
|     "@elysiajs/jwt": "^1.1.1", | ||||
|     "@elysiajs/static": "1.0.3", | ||||
|     "elysia": "^1.1.12" | ||||
|     "elysia": "^1.1.16" | ||||
|   }, | ||||
|   "module": "src/index.tsx", | ||||
|   "type": "module", | ||||
| @@ -25,31 +25,41 @@ | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@biomejs/biome": "1.9.2", | ||||
|     "@eslint-community/eslint-plugin-eslint-comments": "^4.4.0", | ||||
|     "@eslint/compat": "^1.1.1", | ||||
|     "@eslint/js": "^9.9.1", | ||||
|     "@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.1", | ||||
|     "@types/bun": "^1.1.8", | ||||
|     "@types/bun": "^1.1.10", | ||||
|     "@types/eslint": "^9.6.1", | ||||
|     "@types/node": "^22.5.4", | ||||
|     "@typescript-eslint/eslint-plugin": "^8.4.0", | ||||
|     "@typescript-eslint/parser": "^8.4.0", | ||||
|     "cpy-cli": "^5.0.0", | ||||
|     "eslint": "^9.9.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.30.0", | ||||
|     "eslint-plugin-isaacscript": "^4.0.0", | ||||
|     "eslint-plugin-prettier": "^5.2.1", | ||||
|     "eslint-plugin-simple-import-sort": "^12.1.1", | ||||
|     "knip": "^5.29.2", | ||||
|     "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.4.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; | ||||
|  | ||||
|   | ||||
| @@ -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; | ||||
| }; | ||||
							
								
								
									
										371
									
								
								src/index.tsx
									
									
									
									
									
								
							
							
						
						
									
										371
									
								
								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 { | ||||
| @@ -138,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> | ||||
| @@ -183,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> | ||||
| @@ -299,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 | ||||
| @@ -325,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> | ||||
| @@ -435,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, | ||||
|       }); | ||||
| @@ -452,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", | ||||
|       }); | ||||
|     } | ||||
| @@ -491,87 +522,64 @@ 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" }} | ||||
|                             safe> | ||||
|                           <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" | ||||
|                                 safe | ||||
|                               > | ||||
| @@ -589,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> | ||||
| @@ -609,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 /> | ||||
| @@ -622,53 +629,25 @@ 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" }} safe> | ||||
|                   <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" | ||||
|                         safe | ||||
|                       > | ||||
| @@ -687,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> | ||||
| @@ -736,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); | ||||
|         } | ||||
|       } | ||||
| @@ -913,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> | ||||
|                   ))} | ||||
| @@ -987,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> | ||||
| @@ -1083,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> | ||||
| @@ -1178,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> | ||||
| @@ -1194,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> | ||||
| @@ -1210,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> | ||||
| @@ -1258,8 +1253,20 @@ 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}`, | ||||
|   | ||||
							
								
								
									
										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