Compare commits
	
		
			306 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 0f2172d61f | ||
|  | 2baa69ca17 | ||
|  | 3bbfa9186e | ||
|  | c1428f5c2b | ||
|  | 1be11708c4 | ||
|  | 8c04b318fd | ||
|  | ff2c0057e8 | ||
|  | c830721e02 | ||
|  | 625e1a51f6 | ||
|  | 6af1e8f326 | ||
|  | 82f0e14abf | ||
|  | 9e759a75de | ||
|  | 2490c3a7e7 | ||
|  | 7f86c352e3 | ||
|  | 2a3b08487e | ||
|  | 29ba229bc2 | ||
|  | 50725edd02 | ||
|  | 40d1d8a191 | ||
|  | 3417564278 | ||
|  | 9a49dedaca | ||
|  | d9076bf42a | ||
|  | b9bbf7792f | ||
|  | 5cc6678ceb | ||
|  | b47e5755f6 | ||
|  | af5c768dc7 | ||
|  | 3b573cccae | ||
|  | 0c6f6d6904 | ||
|  | 6e2fe27f31 | ||
|  | b6cdd3741a | ||
|  | 00f95b6daa | ||
|  | 2d05bbf86b | ||
|  | 2c87a6c8c2 | ||
|  | 254509db5e | ||
|  | 4e4c029cb8 | ||
|  | 6dc60679bb | ||
|  | 6e5d5d9de0 | ||
|  | 6289c033c8 | ||
|  | b200049a81 | ||
|  | 5646f79f99 | ||
|  | 5083968b80 | ||
|  | 2eb9b8fe96 | ||
|  | 8dc60b41ff | ||
|  | b4be479d02 | ||
|  | f56a93a1b2 | ||
|  | ff8b9fca67 | ||
|  | 0579f1852b | ||
|  | 52d4cc0d03 | ||
|  | 2c68016ca6 | ||
|  | 7914194856 | ||
|  | 2dac7f1362 | ||
|  | a17e5fd614 | ||
|  | 21994fb6a2 | ||
|  | a5eaaa422a | ||
|  | ff2ef74135 | ||
|  | 70705c1850 | ||
|  | fd9c151e01 | ||
|  | 4f0573963f | ||
|  | 6bb6bce8a4 | ||
|  | 448557bece | ||
|  | bdbd4a122c | ||
|  | cb9d0ec680 | ||
|  | fb60ef66f5 | ||
|  | c1ae43075f | ||
|  | 377f69ae8d | ||
|  | cb131cd0a0 | ||
|  | fcc83c5ea8 | ||
|  | 96d4717d13 | ||
|  | 4d73bf9760 | ||
|  | 725a94bc95 | ||
|  | 0a366b447a | ||
|  | 4a27a7bc03 | ||
|  | 3ca5803bda | ||
|  | 239041294c | ||
|  | 31fdd8f214 | ||
|  | c3319c09eb | ||
|  | d460e94d52 | ||
|  | 4b5c732380 | ||
|  | f42665ca40 | ||
|  | bed52cef17 | ||
|  | 9d1c93155c | ||
|  | 794cc7c474 | ||
|  | d7d584e497 | ||
|  | f5320df86e | ||
|  | 056fd4ba93 | ||
|  | 5b6e70eb3a | ||
|  | f437a8e7e2 | ||
|  | cdae798fcf | ||
|  | bcc827a81b | ||
|  | 84274b9c55 | ||
|  | 20c6f8249e | ||
|  | 8f0ea2a592 | ||
|  | a29e4a930a | ||
|  | 4549c96ae3 | ||
|  | bc64094c04 | ||
|  | fa58827ad5 | ||
|  | 8f27be0e3d | ||
|  | df43df1178 | ||
|  | c2beb4a227 | ||
|  | 9277c27a50 | ||
|  | 171ecd6884 | ||
|  | dca29f7e5a | ||
|  | 318acc20bd | ||
|  | f433493d57 | ||
|  | 19970fc132 | ||
|  | 24394ca3c5 | ||
|  | 10ff0b464a | ||
|  | 9263d17609 | ||
|  | c1b75a13fd | ||
|  | a8ed60d48f | ||
|  | dc82a438d4 | ||
|  | cc54bdcbe7 | ||
|  | ae4bbc8baa | ||
|  | ad98499da0 | ||
|  | db60f355b2 | ||
|  | eb91d8b298 | ||
|  | b8312be4b7 | ||
|  | 326a8e3404 | ||
|  | f017e13ac1 | ||
|  | 67a5fe353e | ||
|  | 51d49d7ff3 | ||
|  | d42b820b36 | ||
|  | 07d32776d3 | ||
|  | ef027e81b5 | ||
|  | a75e4b495d | ||
|  | fba5e212e8 | ||
|  | 62f44fb052 | ||
|  | 6b9254047c | ||
|  | decfea5dc9 | ||
|  | eacded6848 | ||
|  | 279ca72c64 | ||
|  | b8fc9383ca | ||
|  | bec58ac59f | ||
|  | 83d7126820 | ||
|  | f0e9c6d794 | ||
|  | 0e61051fc6 | ||
|  | 480ba77ebe | ||
|  | 16f27c13bb | ||
|  | afe5c50d66 | ||
|  | 72ea859ebb | ||
|  | 8edf3834c4 | ||
|  | e595014fcd | ||
|  | 8bebf7e569 | ||
|  | c825ec06e2 | ||
|  | 8c75f273fb | ||
|  | 0ba776c129 | ||
|  | 2bbbd03554 | ||
|  | 0a5d0487b1 | ||
|  | 583cd2dd3b | ||
|  | e1f7fc1ecb | ||
|  | 961a55cbe5 | ||
|  | cdf9bad903 | ||
|  | 6769fa2f83 | ||
|  | 3b7ea88b73 | ||
|  | 59310c095d | ||
|  | c47f0c12fe | ||
|  | ca71a40485 | ||
|  | d4e8f376c1 | ||
|  | 14c6ea1e6b | ||
|  | d6e4d8fbd6 | ||
|  | 460bda62d5 | ||
|  | d2702ab673 | ||
|  | e2581f42f5 | ||
|  | f0fcfc159f | ||
|  | 538c5b60c9 | ||
|  | 2fabb7bbb2 | ||
|  | e7c34a9c94 | ||
|  | 618f9fce5a | ||
|  | 95dbc9f678 | ||
|  | aa87bc5c51 | ||
|  | 815de531ed | ||
|  | cf2b026dc4 | ||
|  | 9ce46aefba | ||
|  | 98b2db7818 | ||
|  | 0229851bf9 | ||
|  | 9e15114fe8 | ||
|  | 7f66a76bb0 | ||
|  | e9cc8392bb | ||
|  | d0b89ce74f | ||
|  | f537c81db7 | ||
|  | 03d3edfff6 | ||
|  | 447b4c5e5c | ||
|  | cb143209ae | ||
|  | 9c24fe73b5 | ||
|  | 19ae85424b | ||
|  | 22fad99552 | ||
|  | 8144bbef74 | ||
|  | aad6da0ae8 | ||
|  | c5f8162a22 | ||
|  | f0f30224b5 | ||
|  | d0d888e356 | ||
|  | 2c64122224 | ||
|  | 3b2eee96a9 | ||
|  | 465aacbf9b | ||
|  | d1a2a66170 | ||
|  | 4c05fd72bb | ||
|  | f04fe760e3 | ||
|  | 834d19bcc6 | ||
|  | 6808c4642c | ||
|  | d0ce307f94 | ||
|  | f3740e9ded | ||
|  | b485bc9445 | ||
|  | 2d14c1bb26 | ||
|  | 1a442d6e69 | ||
|  | 2386543e5c | ||
|  | 58e220e82d | ||
|  | 24bea6e4d2 | ||
|  | 43497ad8d1 | ||
|  | f22b61fe4c | ||
|  | 5b08f4cd19 | ||
|  | 1589f8d24e | ||
|  | 7d1db72cf5 | ||
|  | 53a8f66414 | ||
|  | 36cb6cc589 | ||
|  | f3a4aece46 | ||
|  | 580a6a869a | ||
|  | 008eaac493 | ||
|  | b450623bb4 | ||
|  | 8ac2ecb673 | ||
|  | 0a10a56ae3 | ||
|  | 9378ba9208 | ||
|  | 0c586e324b | ||
|  | 91c4a64284 | ||
|  | c599e98d9d | ||
|  | d2cd6706c9 | ||
|  | e8ed10dde8 | ||
|  | 5fe0b79802 | ||
|  | 34a6722a68 | ||
|  | 5b0d769c63 | ||
|  | 718401a28b | ||
|  | 3112cd57f6 | ||
|  | 410fc777a7 | ||
|  | 8eed99e732 | ||
|  | 663b1d4171 | ||
|  | c3067ca12d | ||
|  | 4561ca3760 | ||
|  | 698cce58ce | ||
|  | 339b79f786 | ||
|  | 4f98f778f0 | ||
|  | 8479b33a47 | ||
|  | 78844d7bd5 | ||
|  | 64e4a271e1 | ||
|  | 5fb8c3575b | ||
|  | a6b8bcecae | ||
|  | bc9c820820 | ||
|  | ee9207a7f4 | ||
|  | a34e215202 | ||
|  | b4e53dbb8e | ||
|  | b5e8d82bfa | ||
|  | 5d9000bb33 | ||
|  | ccb065ef0f | ||
|  | 883fad806b | ||
|  | feacd1b816 | ||
|  | 094e7a0d1c | ||
|  | 72636c5059 | ||
|  | 291cfc80c6 | ||
|  | ae1dfafc9d | ||
|  | 6caa583c35 | ||
|  | 2057167576 | ||
|  | 1c9e67fc32 | ||
|  | 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 | 
| @@ -1,16 +1,25 @@ | ||||
| node_modules | ||||
| Dockerfile* | ||||
| docker-compose* | ||||
| .dockerignore | ||||
| .git | ||||
| .gitignore | ||||
| README.md | ||||
| LICENSE | ||||
| .vscode | ||||
| Makefile | ||||
| helm-charts | ||||
| .env | ||||
| .editorconfig | ||||
| .idea | ||||
| coverage* | ||||
| data | ||||
| .dockerignore | ||||
| .editorconfig | ||||
| .env | ||||
| .git | ||||
| .gitignore | ||||
| .github | ||||
| .idea | ||||
| .vscode | ||||
| biome.json | ||||
| CHANGELOG.md | ||||
| compose.yaml | ||||
| coverage* | ||||
| data | ||||
| docker-compose* | ||||
| Dockerfile* | ||||
| eslint.config.js | ||||
| helm-charts | ||||
| images | ||||
| LICENSE | ||||
| Makefile | ||||
| node_modules | ||||
| prettier.config.js | ||||
| README.md | ||||
| renovate.json | ||||
| SECURITY.md | ||||
|   | ||||
							
								
								
									
										15
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,15 @@ | ||||
| # These are supported funding model platforms | ||||
|  | ||||
| github: [C4illin] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] | ||||
| patreon: # Replace with a single Patreon username | ||||
| open_collective: # Replace with a single Open Collective username | ||||
| ko_fi: # Replace with a single Ko-fi username | ||||
| tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel | ||||
| community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry | ||||
| liberapay: # Replace with a single Liberapay username | ||||
| issuehunt: # Replace with a single IssueHunt username | ||||
| lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry | ||||
| polar: # Replace with a single Polar username | ||||
| buy_me_a_coffee: # Replace with a single Buy Me a Coffee username | ||||
| thanks_dev: # Replace with a single thanks.dev username | ||||
| custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] | ||||
							
								
								
									
										21
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,21 @@ | ||||
| --- | ||||
| name: Bug report | ||||
| about: Create a report to help us improve | ||||
| title: '' | ||||
| labels: bug | ||||
| assignees: '' | ||||
|  | ||||
| --- | ||||
|  | ||||
| **Describe the bug** | ||||
| A clear and concise description of what the bug is. | ||||
|  | ||||
| **To Reproduce** | ||||
| Steps to reproduce the behavior: | ||||
| 1. Go to '...' | ||||
| 2. Click on '....' | ||||
| 3. Scroll down to '....' | ||||
| 4. See error | ||||
|  | ||||
| **Checklist:** | ||||
| - [ ] I am accessing ConvertX over HTTPS or have `HTTP_ALLOWED=true` | ||||
							
								
								
									
										14
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,14 @@ | ||||
| --- | ||||
| name: Feature request | ||||
| about: Suggest an idea for this project | ||||
| title: "[Feature Request]" | ||||
| labels: enhancement | ||||
| assignees: '' | ||||
|  | ||||
| --- | ||||
|  | ||||
| **Describe the solution you'd like** | ||||
| A clear and concise description of what you want to happen. | ||||
|  | ||||
| **Additional context** | ||||
| Add any other context or screenshots about the feature request here. | ||||
							
								
								
									
										173
									
								
								.github/workflows/docker-publish.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,69 +1,164 @@ | ||||
| name: Docker | ||||
|  | ||||
| # This workflow uses actions that are not certified by GitHub. | ||||
| # They are provided by a third-party and are governed by | ||||
| # separate terms of service, privacy policy, and support | ||||
| # documentation. | ||||
| # thanks to https://github.com/sredevopsorg/multi-arch-docker-github-workflow | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: [ "main" ] | ||||
|     # Publish semver tags as releases. | ||||
|     tags: [ 'v*.*.*' ] | ||||
|     branches: ["main"] | ||||
|     tags: ["v*.*.*"] | ||||
|   pull_request: | ||||
|     branches: [ "main" ] | ||||
|     branches: ["main"] | ||||
|   workflow_dispatch: | ||||
|  | ||||
| env: | ||||
|   # Use docker.io for Docker Hub if empty | ||||
|   REGISTRY: ghcr.io | ||||
|   # github.repository as <account>/<repo> | ||||
|   GHCR_IMAGE: ghcr.io/c4illin/convertx | ||||
|   IMAGE_NAME: ${{ github.repository }} | ||||
|   DOCKERHUB_USERNAME: c4illin | ||||
|  | ||||
| concurrency: | ||||
|   group: ${{ github.workflow }}-${{ github.ref }} | ||||
|   cancel-in-progress: true | ||||
|  | ||||
| jobs: | ||||
|   # The build job builds the Docker image for each platform specified in the matrix. | ||||
|   build: | ||||
|     runs-on: ubuntu-latest | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         platform: | ||||
|           - linux/amd64 | ||||
|           - linux/arm64 | ||||
|  | ||||
|     permissions: | ||||
|       contents: read | ||||
|       contents: write | ||||
|       packages: write | ||||
|       attestations: write | ||||
|       checks: write | ||||
|       actions: read | ||||
|  | ||||
|     runs-on: ${{ matrix.platform == 'linux/amd64' && 'ubuntu-24.04' || matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' }} | ||||
|  | ||||
|     name: Build Docker image for ${{ matrix.platform }} | ||||
|  | ||||
|     steps: | ||||
|       - name: Prepare environment for current platform | ||||
|         # This step sets up the environment for the current platform being built. | ||||
|         # It replaces the '/' character in the platform name with '-' and sets it as an environment variable. | ||||
|         # This is useful for naming artifacts and other resources that cannot contain '/'. | ||||
|         # The environment variable PLATFORMS_PAIR will be used later in the workflow. | ||||
|         id: prepare | ||||
|         run: | | ||||
|           platform=${{ matrix.platform }} | ||||
|           echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV | ||||
|  | ||||
|       - name: Checkout repository | ||||
|         uses: actions/checkout@v4 | ||||
|  | ||||
|       # Workaround: https://github.com/docker/build-push-action/issues/461 | ||||
|       - name: Setup Docker buildx | ||||
|         uses: docker/setup-buildx-action@v3 | ||||
|  | ||||
|       # Login against a Docker registry except on PR | ||||
|       # https://github.com/docker/login-action | ||||
|       - name: Log into registry ${{ env.REGISTRY }} | ||||
|         if: github.event_name != 'pull_request' | ||||
|         uses: docker/login-action@v3 | ||||
|       - name: Docker meta default | ||||
|         id: meta | ||||
|         uses: docker/metadata-action@v5 | ||||
|         with: | ||||
|           registry: ${{ env.REGISTRY }} | ||||
|           images: ${{ env.GHCR_IMAGE }} | ||||
|  | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v3.10.0 | ||||
|         with: | ||||
|           platforms: ${{ matrix.platform }} | ||||
|  | ||||
|       - name: Login to GitHub Container Registry | ||||
|         # here we only login to ghcr.io since the this only pushes internal images | ||||
|         uses: docker/login-action@v3.4.0 | ||||
|         with: | ||||
|           registry: ghcr.io | ||||
|           username: ${{ github.actor }} | ||||
|           password: ${{ secrets.GITHUB_TOKEN }} | ||||
|  | ||||
|       # Extract metadata (tags, labels) for Docker | ||||
|       # https://github.com/docker/metadata-action | ||||
|       - name: Build and push by digest | ||||
|         id: build | ||||
|         uses: docker/build-push-action@v6.18.0 | ||||
|         env: | ||||
|           DOCKER_BUILDKIT: 1 | ||||
|         with: | ||||
|           context: . | ||||
|           platforms: ${{ matrix.platform }} | ||||
|           labels: ${{ steps.meta.outputs.labels }} | ||||
|           annotations: ${{ steps.meta.outputs.annotations }} | ||||
|           outputs: type=image,name=${{ env.GHCR_IMAGE }},push-by-digest=true,name-canonical=true,push=true,oci-mediatypes=true | ||||
|           cache-from: type=gha,scope=${{ matrix.platform }} | ||||
|           cache-to: type=gha,mode=max,scope=${{ matrix.platform }} | ||||
|  | ||||
|       - name: Export digest | ||||
|         run: | | ||||
|           mkdir -p /tmp/digests | ||||
|           digest="${{ steps.build.outputs.digest }}" | ||||
|           touch "/tmp/digests/${digest#sha256:}" | ||||
|  | ||||
|       - name: Upload digest | ||||
|         uses: actions/upload-artifact@v4 | ||||
|         with: | ||||
|           name: digests-${{ env.PLATFORM_PAIR }} | ||||
|           path: /tmp/digests/* | ||||
|           if-no-files-found: error | ||||
|           retention-days: 1 | ||||
|  | ||||
|   merge: | ||||
|     name: Merge Docker manifests | ||||
|     runs-on: ubuntu-latest | ||||
|  | ||||
|     permissions: | ||||
|       attestations: write | ||||
|       contents: read | ||||
|       packages: write | ||||
|  | ||||
|     needs: | ||||
|       - build | ||||
|     steps: | ||||
|       - name: Download digests | ||||
|         uses: actions/download-artifact@v4 | ||||
|         with: | ||||
|           path: /tmp/digests | ||||
|           pattern: digests-* | ||||
|           merge-multiple: true | ||||
|  | ||||
|       - name: Extract Docker metadata | ||||
|         id: meta | ||||
|         uses: docker/metadata-action@v5 | ||||
|         with: | ||||
|           images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | ||||
|           images: | | ||||
|             ${{ env.GHCR_IMAGE }} | ||||
|             ${{ env.IMAGE_NAME }} | ||||
|  | ||||
|       # Build and push Docker image with Buildx (don't push on PR) | ||||
|       # https://github.com/docker/build-push-action | ||||
|       - name: Build and push Docker image | ||||
|         id: build-and-push | ||||
|         uses: docker/build-push-action@v6 | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v3 | ||||
|  | ||||
|       - name: Login to GitHub Container Registry | ||||
|         uses: docker/login-action@v3 | ||||
|         with: | ||||
|           context: . | ||||
|           platforms: linux/amd64,linux/arm64 | ||||
|           push: ${{ github.event_name != 'pull_request' }} | ||||
|           tags: ${{ steps.meta.outputs.tags }} | ||||
|           labels: ${{ steps.meta.outputs.labels }} | ||||
|           cache-from: type=gha | ||||
|           cache-to: type=gha,mode=max | ||||
|           registry: ghcr.io | ||||
|           username: ${{ github.actor }} | ||||
|           password: ${{ secrets.GITHUB_TOKEN }} | ||||
|  | ||||
|       - name: Login to Docker Hub | ||||
|         uses: docker/login-action@v3 | ||||
|         with: | ||||
|           username: ${{ env.DOCKERHUB_USERNAME }} | ||||
|           password: ${{ secrets.DOCKERHUB_TOKEN }} | ||||
|  | ||||
|       - name: Get execution timestamp with RFC3339 format | ||||
|         id: timestamp | ||||
|         run: | | ||||
|           echo "timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> $GITHUB_OUTPUT | ||||
|  | ||||
|       - name: Create manifest list and push | ||||
|         working-directory: /tmp/digests | ||||
|         run: | | ||||
|           docker buildx imagetools create \ | ||||
|             $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ | ||||
|             --annotation='index:org.opencontainers.image.description=${{ github.event.repository.description }}' \ | ||||
|             --annotation='index:org.opencontainers.image.created=${{ steps.timestamp.outputs.timestamp }}' \ | ||||
|             --annotation='index:org.opencontainers.image.url=${{ github.event.repository.url }}' \ | ||||
|             --annotation='index:org.opencontainers.image.source=${{ github.event.repository.url }}' \ | ||||
|             $(printf '${{ env.GHCR_IMAGE }}@sha256:%s ' *) | ||||
|  | ||||
|       - name: Inspect image | ||||
|         run: | | ||||
|           docker buildx imagetools inspect '${{ env.GHCR_IMAGE }}:${{ steps.meta.outputs.version }}' | ||||
|   | ||||
							
								
								
									
										27
									
								
								.github/workflows/dockerhub-description.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,27 @@ | ||||
| name: Update Docker Hub Description | ||||
|  | ||||
| env: | ||||
|   IMAGE_NAME: ${{ github.repository }} | ||||
|   DOCKERHUB_USERNAME: c4illin | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: | ||||
|       - main | ||||
|     paths: | ||||
|       - README.md | ||||
|       - .github/workflows/dockerhub-description.yml | ||||
| jobs: | ||||
|   dockerHubDescription: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4 | ||||
|  | ||||
|       - name: Docker Hub Description | ||||
|         uses: peter-evans/dockerhub-description@v4 | ||||
|         with: | ||||
|           username: ${{ env.DOCKERHUB_USERNAME }} | ||||
|           password: ${{ secrets.DOCKERHUB_TOKEN }} | ||||
|           repository: ${{ env.IMAGE_NAME }} | ||||
|           short-description: ${{ github.event.repository.description }} | ||||
|           enable-url-completion: true | ||||
							
								
								
									
										10
									
								
								.github/workflows/remove-docker-tag.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -14,8 +14,8 @@ jobs: | ||||
|       packages: write | ||||
|  | ||||
|     steps: | ||||
|     - name: Remove Docker Tag | ||||
|       uses: ArchieAtkinson/remove-dockertag-action@v0.0 | ||||
|       with: | ||||
|         tag_name: master | ||||
|         github_token: ${{ secrets.GITHUB_TOKEN }} | ||||
|       - name: Remove Docker Tag | ||||
|         uses: ArchieAtkinson/remove-dockertag-action@v0.0 | ||||
|         with: | ||||
|           tag_name: master | ||||
|           github_token: ${{ secrets.GITHUB_TOKEN }} | ||||
|   | ||||
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -47,4 +47,5 @@ package-lock.json | ||||
| /db | ||||
| /data | ||||
| /Bruno | ||||
| /tsconfig.tsbuildinfo | ||||
| /tsconfig.tsbuildinfo | ||||
| /public/generated.css | ||||
|   | ||||
							
								
								
									
										2
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,4 +1,4 @@ | ||||
| { | ||||
|   "typescript.tsdk": "node_modules/typescript/lib", | ||||
|   "typescript.enablePromptUseWorkspaceTsdk": true | ||||
| } | ||||
| } | ||||
|   | ||||
							
								
								
									
										208
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						| @@ -1,82 +1,230 @@ | ||||
| # Changelog | ||||
|  | ||||
| ## [0.14.0](https://github.com/C4illin/ConvertX/compare/v0.13.0...v0.14.0) (2025-06-03) | ||||
|  | ||||
|  | ||||
| ### Features | ||||
|  | ||||
| * add dvisvgm ([625e1a5](https://github.com/C4illin/ConvertX/commit/625e1a51f620fe9da79d0127eb6c95f468d9ea2b)) | ||||
| * add ImageMagick ([b47e575](https://github.com/C4illin/ConvertX/commit/b47e5755f677056e8acecad54c0c2e28a5e137f3)) | ||||
| * enhance job details display with file information ([50725ed](https://github.com/C4illin/ConvertX/commit/50725edd021bb9a7f58c85b79c1eab355ad22ced)) | ||||
| * improve job details interaction and accessibility ([2a3b084](https://github.com/C4illin/ConvertX/commit/2a3b08487ec4bf215e1e80059dbdc1dcccea68c8)) | ||||
| * improve job details interaction and accessibility ([29ba229](https://github.com/C4illin/ConvertX/commit/29ba229bc23d2019d2ee9829da7852f884ffa611)) | ||||
| * show version in footer ([9a49ded](https://github.com/C4illin/ConvertX/commit/9a49dedacac7e67a432b6da0daf1967038d97d26)) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * add av1 and h26X with containers ([af5c768](https://github.com/C4illin/ConvertX/commit/af5c768dc74b3124fd7ef4b29e27c83a5d19ad49)) | ||||
| * progress bars on firefox ([ff2c005](https://github.com/C4illin/ConvertX/commit/ff2c0057e890b9ecb552df30914333349ea20eb7)) | ||||
| * register button style ([b9bbf77](https://github.com/C4illin/ConvertX/commit/b9bbf7792f01fcaa77e3520925de107e856926f1)) | ||||
| * switch from alpine to debian trixie ([4e4c029](https://github.com/C4illin/ConvertX/commit/4e4c029cb800df86affb99c3a82dda9e6708bdde)) | ||||
|  | ||||
| ## [0.13.0](https://github.com/C4illin/ConvertX/compare/v0.12.1...v0.13.0) (2025-05-14) | ||||
|  | ||||
| ### Features | ||||
|  | ||||
| - add HIDE_HISTORY option to control visibility of history page ([9d1c931](https://github.com/C4illin/ConvertX/commit/9d1c93155cc33ed6c83f9e5122afff8f28d0e4bf)) | ||||
| - add potrace converter ([bdbd4a1](https://github.com/C4illin/ConvertX/commit/bdbd4a122c09559b089b985ea12c5f3e085107da)) | ||||
| - Add support for .HIF files ([70705c1](https://github.com/C4illin/ConvertX/commit/70705c1850d470296df85958c02a01fb5bc3a25f)) | ||||
| - add support for drag/drop of images ([ff2ef74](https://github.com/C4illin/ConvertX/commit/ff2ef7413542cf10ba7a6e246763bcecd6829ec1)) | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| - add timezone support ([4b5c732](https://github.com/C4illin/ConvertX/commit/4b5c732380bc844dccf340ea1eb4f8bfe3bb44a5)), closes [#258](https://github.com/C4illin/ConvertX/issues/258) | ||||
|  | ||||
| ## [0.12.1](https://github.com/C4illin/ConvertX/compare/v0.12.0...v0.12.1) (2025-03-20) | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| - rollback to bun 1.2.2 ([cdae798](https://github.com/C4illin/ConvertX/commit/cdae798fcf5879e4adea87386a38748b9a1e1ddc)) | ||||
|  | ||||
| ## [0.12.0](https://github.com/C4illin/ConvertX/compare/v0.11.1...v0.12.0) (2025-03-06) | ||||
|  | ||||
| ### Features | ||||
|  | ||||
| - added progress bar for file upload ([db60f35](https://github.com/C4illin/ConvertX/commit/db60f355b2973f43f8e5990e6fe4e351b959b659)) | ||||
| - made every upload file independent ([cc54bdc](https://github.com/C4illin/ConvertX/commit/cc54bdcbe764c41cc3273485d072fd3178ad2dca)) | ||||
| - replace exec with execFile ([9263d17](https://github.com/C4illin/ConvertX/commit/9263d17609dc4b2b367eb7fee67b3182e283b3a3)) | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| - add libheif ([6b92540](https://github.com/C4illin/ConvertX/commit/6b9254047c0598963aee1d99e20ba1650a0368bf)) | ||||
| - add libheif ([decfea5](https://github.com/C4illin/ConvertX/commit/decfea5dc9627b216bb276a9e1578c32cfa1deb6)), closes [#202](https://github.com/C4illin/ConvertX/issues/202) | ||||
| - added onerror log ([ae4bbc8](https://github.com/C4illin/ConvertX/commit/ae4bbc8baacbaf67763c62ea44140bb21cc17230)) | ||||
| - refactored uploadFile to only accept a single file instead of multiple ([dc82a43](https://github.com/C4illin/ConvertX/commit/dc82a438d4104b79ff423d502a6779a43928968a)) | ||||
| - update libheif to 1.19.5 ([fba5e21](https://github.com/C4illin/ConvertX/commit/fba5e212e8d0eaba8971e239e35aeb521f3cd813)), closes [#202](https://github.com/C4illin/ConvertX/issues/202) | ||||
|  | ||||
| ## [0.11.1](https://github.com/C4illin/ConvertX/compare/v0.11.0...v0.11.1) (2025-02-07) | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| - mobile view overflow ([bec58ac](https://github.com/C4illin/ConvertX/commit/bec58ac59f9600e35385b9e21d174f3ab1b42b1d)) | ||||
|  | ||||
| ## [0.11.0](https://github.com/C4illin/ConvertX/compare/v0.10.1...v0.11.0) (2025-02-05) | ||||
|  | ||||
| ### Features | ||||
|  | ||||
| - add deps for vaapi ([2bbbd03](https://github.com/C4illin/ConvertX/commit/2bbbd03554d384a4488143f29e5fc863cfdf333b)), closes [#192](https://github.com/C4illin/ConvertX/issues/192) | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| - don't crash if file is not found ([16f27c1](https://github.com/C4illin/ConvertX/commit/16f27c13bbc1c0e5fa2316f3db11d0918524053b)) | ||||
| - install numpy for inkscape ([0e61051](https://github.com/C4illin/ConvertX/commit/0e61051fc6be188164c3865b4fb579c140859fdc)) | ||||
|  | ||||
| ## [0.10.1](https://github.com/C4illin/ConvertX/compare/v0.10.0...v0.10.1) (2025-01-21) | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| - ffmpeg works without ffmpeg_args ([3b7ea88](https://github.com/C4illin/ConvertX/commit/3b7ea88b7382f7c21b120bdc9bda5bb10547f55d)), closes [#212](https://github.com/C4illin/ConvertX/issues/212) | ||||
|  | ||||
| ## [0.10.0](https://github.com/C4illin/ConvertX/compare/v0.9.0...v0.10.0) (2025-01-18) | ||||
|  | ||||
| ### Features | ||||
|  | ||||
| - add calibre ([03d3edf](https://github.com/C4illin/ConvertX/commit/03d3edfff65c252dd4b8922fc98257c089c1ff74)), closes [#191](https://github.com/C4illin/ConvertX/issues/191) | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| - add FFMPEG_ARGS env variable ([f537c81](https://github.com/C4illin/ConvertX/commit/f537c81db7815df8017f834e3162291197e1c40f)), closes [#190](https://github.com/C4illin/ConvertX/issues/190) | ||||
| - add qt6-qtbase-private-dev from community repo ([95dbc9f](https://github.com/C4illin/ConvertX/commit/95dbc9f678bec7e6e2c03587e1473fb8ff708ea3)) | ||||
| - skip account setup when ALLOW_UNAUTHENTICATED is true ([538c5b6](https://github.com/C4illin/ConvertX/commit/538c5b60c9e27a8184740305475245da79bae143)) | ||||
|  | ||||
| ## [0.9.0](https://github.com/C4illin/ConvertX/compare/v0.8.1...v0.9.0) (2024-11-21) | ||||
|  | ||||
| ### Features | ||||
|  | ||||
| - add inkscape for vector images ([f3740e9](https://github.com/C4illin/ConvertX/commit/f3740e9ded100b8500f3613517960248bbd3c210)) | ||||
| - Allow to chose webroot ([36cb6cc](https://github.com/C4illin/ConvertX/commit/36cb6cc589d80d0a87fa8dbe605db71a9a2570f9)), closes [#180](https://github.com/C4illin/ConvertX/issues/180) | ||||
| - disable convert when uploading ([58e220e](https://github.com/C4illin/ConvertX/commit/58e220e82d7f9c163d6ea4dc31092c08a3e254f4)), closes [#177](https://github.com/C4illin/ConvertX/issues/177) | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| - treat unknown as m4a ([1a442d6](https://github.com/C4illin/ConvertX/commit/1a442d6e69606afef63b1e7df36aa83d111fa23d)), closes [#178](https://github.com/C4illin/ConvertX/issues/178) | ||||
| - wait for both upload and selection ([4c05fd7](https://github.com/C4illin/ConvertX/commit/4c05fd72bbbf91ee02327f6fcbf749b78272376b)), closes [#177](https://github.com/C4illin/ConvertX/issues/177) | ||||
|  | ||||
| ## [0.8.1](https://github.com/C4illin/ConvertX/compare/v0.8.0...v0.8.1) (2024-10-05) | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| - disable convert button when input is empty ([78844d7](https://github.com/C4illin/ConvertX/commit/78844d7bd55990789ed07c81e49043e688cbe656)), closes [#151](https://github.com/C4illin/ConvertX/issues/151) | ||||
| - resize to fit for ico ([b4e53db](https://github.com/C4illin/ConvertX/commit/b4e53dbb8e70b3a95b44e5b756759d16117a87e1)), closes [#157](https://github.com/C4illin/ConvertX/issues/157) | ||||
| - treat jfif as jpeg ([339b79f](https://github.com/C4illin/ConvertX/commit/339b79f786131deb93f0d5683e03178fdcab1ef5)), closes [#163](https://github.com/C4illin/ConvertX/issues/163) | ||||
|  | ||||
| ## [0.8.0](https://github.com/C4illin/ConvertX/compare/v0.7.0...v0.8.0) (2024-09-30) | ||||
|  | ||||
| ### Features | ||||
|  | ||||
| - add light theme, fixes [#156](https://github.com/C4illin/ConvertX/issues/156) ([72636c5](https://github.com/C4illin/ConvertX/commit/72636c5059ebf09c8fece2e268293650b2f8ccf6)) | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| - add support for usd for assimp, [#144](https://github.com/C4illin/ConvertX/issues/144) ([2057167](https://github.com/C4illin/ConvertX/commit/20571675766209ad1251f07e687d29a6791afc8b)) | ||||
| - cleanup formats and add opus, fixes [#159](https://github.com/C4illin/ConvertX/issues/159) ([ae1dfaf](https://github.com/C4illin/ConvertX/commit/ae1dfafc9d9116a57b08c2f7fc326990e00824b0)) | ||||
| - support .awb and clean up, fixes [#153](https://github.com/C4illin/ConvertX/issues/153), [#92](https://github.com/C4illin/ConvertX/issues/92) ([1c9e67f](https://github.com/C4illin/ConvertX/commit/1c9e67fc3201e0e5dee91e8981adf34daaabf33a)) | ||||
|  | ||||
| ## [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) | ||||
|  | ||||
|  | ||||
| ### Features | ||||
|  | ||||
| * add option for unauthenticated file conversions [#114](https://github.com/C4illin/ConvertX/issues/114) ([f0d0e43](https://github.com/C4illin/ConvertX/commit/f0d0e4392983c3e4c530304ea88e023fda9bcac0)) | ||||
| * add resvg converter ([d5eeef9](https://github.com/C4illin/ConvertX/commit/d5eeef9f6884b2bb878508bed97ea9ceaa662995)) | ||||
| * add robots.txt ([6597c1d](https://github.com/C4illin/ConvertX/commit/6597c1d7caeb4dfb6bc47b442e4dfc9840ad12b7)) | ||||
| * Add search bar for formats ([53fff59](https://github.com/C4illin/ConvertX/commit/53fff594fc4d69306abcb2a5cad890fcd0953a58)) | ||||
|  | ||||
| - add option for unauthenticated file conversions [#114](https://github.com/C4illin/ConvertX/issues/114) ([f0d0e43](https://github.com/C4illin/ConvertX/commit/f0d0e4392983c3e4c530304ea88e023fda9bcac0)) | ||||
| - add resvg converter ([d5eeef9](https://github.com/C4illin/ConvertX/commit/d5eeef9f6884b2bb878508bed97ea9ceaa662995)) | ||||
| - add robots.txt ([6597c1d](https://github.com/C4illin/ConvertX/commit/6597c1d7caeb4dfb6bc47b442e4dfc9840ad12b7)) | ||||
| - Add search bar for formats ([53fff59](https://github.com/C4illin/ConvertX/commit/53fff594fc4d69306abcb2a5cad890fcd0953a58)) | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * keep unauthenticated user logged in if allowed [#114](https://github.com/C4illin/ConvertX/issues/114) ([bc4ad49](https://github.com/C4illin/ConvertX/commit/bc4ad492852fad8cb832a0c03485cccdd7f7b117)) | ||||
| * pdf support in vips ([8ca4f15](https://github.com/C4illin/ConvertX/commit/8ca4f1587df7f358893941c656d78d75f04dac93)) | ||||
| * Slow click on conversion popup does not work ([4d9c4d6](https://github.com/C4illin/ConvertX/commit/4d9c4d64aa0266f3928935ada68d91ac81f638aa)) | ||||
| - keep unauthenticated user logged in if allowed [#114](https://github.com/C4illin/ConvertX/issues/114) ([bc4ad49](https://github.com/C4illin/ConvertX/commit/bc4ad492852fad8cb832a0c03485cccdd7f7b117)) | ||||
| - pdf support in vips ([8ca4f15](https://github.com/C4illin/ConvertX/commit/8ca4f1587df7f358893941c656d78d75f04dac93)) | ||||
| - Slow click on conversion popup does not work ([4d9c4d6](https://github.com/C4illin/ConvertX/commit/4d9c4d64aa0266f3928935ada68d91ac81f638aa)) | ||||
|  | ||||
| ## [0.3.3](https://github.com/C4illin/ConvertX/compare/v0.3.2...v0.3.3) (2024-07-30) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * downgrade @elysiajs/html dependency to version 1.0.2 ([c714ade](https://github.com/C4illin/ConvertX/commit/c714ade3e23865ba6cfaf76c9e7259df1cda222c)) | ||||
| - downgrade @elysiajs/html dependency to version 1.0.2 ([c714ade](https://github.com/C4illin/ConvertX/commit/c714ade3e23865ba6cfaf76c9e7259df1cda222c)) | ||||
|  | ||||
| ## [0.3.2](https://github.com/C4illin/ConvertX/compare/v0.3.1...v0.3.2) (2024-07-09) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * increase max request body to support large uploads ([3ae2db5](https://github.com/C4illin/ConvertX/commit/3ae2db5d9b36fe3dcd4372ddcd32aa573ea59aa6)), closes [#64](https://github.com/C4illin/ConvertX/issues/64) | ||||
| - increase max request body to support large uploads ([3ae2db5](https://github.com/C4illin/ConvertX/commit/3ae2db5d9b36fe3dcd4372ddcd32aa573ea59aa6)), closes [#64](https://github.com/C4illin/ConvertX/issues/64) | ||||
|  | ||||
| ## [0.3.1](https://github.com/C4illin/ConvertX/compare/v0.3.0...v0.3.1) (2024-06-27) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * release releases ([4d4c13a](https://github.com/C4illin/ConvertX/commit/4d4c13a8d85ec7c9209ad41cdbea7d4380b0edbf)) | ||||
| - release releases ([4d4c13a](https://github.com/C4illin/ConvertX/commit/4d4c13a8d85ec7c9209ad41cdbea7d4380b0edbf)) | ||||
|  | ||||
| ## [0.3.0](https://github.com/C4illin/ConvertX/compare/v0.2.0...v0.3.0) (2024-06-27) | ||||
|  | ||||
|  | ||||
| ### Features | ||||
|  | ||||
| * add version number to log ([4dcb796](https://github.com/C4illin/ConvertX/commit/4dcb796e1bd27badc078d0638076cd9f1e81c4a4)), closes [#44](https://github.com/C4illin/ConvertX/issues/44) | ||||
| * change to xelatex ([fae2ba9](https://github.com/C4illin/ConvertX/commit/fae2ba9c54461dccdccd1bfb5e76398540d11d0b)) | ||||
| * print version of installed converters to log ([801cf28](https://github.com/C4illin/ConvertX/commit/801cf28d1e5edac9353b0b16be75a4fb48470b8a)) | ||||
| - add version number to log ([4dcb796](https://github.com/C4illin/ConvertX/commit/4dcb796e1bd27badc078d0638076cd9f1e81c4a4)), closes [#44](https://github.com/C4illin/ConvertX/issues/44) | ||||
| - change to xelatex ([fae2ba9](https://github.com/C4illin/ConvertX/commit/fae2ba9c54461dccdccd1bfb5e76398540d11d0b)) | ||||
| - print version of installed converters to log ([801cf28](https://github.com/C4illin/ConvertX/commit/801cf28d1e5edac9353b0b16be75a4fb48470b8a)) | ||||
|  | ||||
| ## [0.2.0](https://github.com/C4illin/ConvertX/compare/v0.1.2...v0.2.0) (2024-06-20) | ||||
|  | ||||
|  | ||||
| ### Features | ||||
|  | ||||
| * add libjxl for jpegxl conversion ([ff680cb](https://github.com/C4illin/ConvertX/commit/ff680cb29534a25c3148a90fd064bb86c71fb482)) | ||||
| * change from debian to alpine ([1316957](https://github.com/C4illin/ConvertX/commit/13169574f0134ae236f8d41287bb73930b575e82)), closes [#34](https://github.com/C4illin/ConvertX/issues/34) | ||||
| - add libjxl for jpegxl conversion ([ff680cb](https://github.com/C4illin/ConvertX/commit/ff680cb29534a25c3148a90fd064bb86c71fb482)) | ||||
| - change from debian to alpine ([1316957](https://github.com/C4illin/ConvertX/commit/13169574f0134ae236f8d41287bb73930b575e82)), closes [#34](https://github.com/C4illin/ConvertX/issues/34) | ||||
|  | ||||
| ## [0.1.2](https://github.com/C4illin/ConvertX/compare/v0.1.1...v0.1.2) (2024-06-10) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * fix incorrect redirect ([25df58b](https://github.com/C4illin/ConvertX/commit/25df58ba82321aaa6617811a6995cb96c2a00a40)), closes [#23](https://github.com/C4illin/ConvertX/issues/23) | ||||
| - fix incorrect redirect ([25df58b](https://github.com/C4illin/ConvertX/commit/25df58ba82321aaa6617811a6995cb96c2a00a40)), closes [#23](https://github.com/C4illin/ConvertX/issues/23) | ||||
|  | ||||
| ## [0.1.1](https://github.com/C4illin/ConvertX/compare/v0.1.0...v0.1.1) (2024-05-30) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * :bug: make sure all redirects are 302 ([9970fd3](https://github.com/C4illin/ConvertX/commit/9970fd3f89190af96f8762edc3817d1e03082b3a)), closes [#12](https://github.com/C4illin/ConvertX/issues/12) | ||||
| - :bug: make sure all redirects are 302 ([9970fd3](https://github.com/C4illin/ConvertX/commit/9970fd3f89190af96f8762edc3817d1e03082b3a)), closes [#12](https://github.com/C4illin/ConvertX/issues/12) | ||||
|  | ||||
| ## 0.1.0 (2024-05-30) | ||||
|  | ||||
|  | ||||
| ### Features | ||||
|  | ||||
| * remove file from file list in index.html ([787ff97](https://github.com/C4illin/ConvertX/commit/787ff9741ecbbf4fb4c02b43bd22a214a173fd7b)) | ||||
|  | ||||
| - remove file from file list in index.html ([787ff97](https://github.com/C4illin/ConvertX/commit/787ff9741ecbbf4fb4c02b43bd22a214a173fd7b)) | ||||
|  | ||||
| ### Miscellaneous Chores | ||||
|  | ||||
| * release 0.1.0 ([54d9aec](https://github.com/C4illin/ConvertX/commit/54d9aecbf949689b12aa7e5e8e9be7b9032f4431)) | ||||
| - release 0.1.0 ([54d9aec](https://github.com/C4illin/ConvertX/commit/54d9aecbf949689b12aa7e5e8e9be7b9032f4431)) | ||||
|   | ||||
| @@ -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" ] | ||||
							
								
								
									
										78
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						| @@ -1,63 +1,69 @@ | ||||
| FROM oven/bun:1.1.26-alpine AS base | ||||
| FROM debian:trixie-slim AS base | ||||
| LABEL org.opencontainers.image.source="https://github.com/C4illin/ConvertX" | ||||
| WORKDIR /app | ||||
|  | ||||
| # install bun | ||||
| ENV BUN_INSTALL=/etc/.bun | ||||
| ENV PATH=$BUN_INSTALL/bin:$PATH | ||||
| ENV BUN_RUNTIME_TRANSPILER_CACHE_PATH=0 | ||||
| RUN apt-get update && apt-get install -y \ | ||||
|   curl \ | ||||
|   unzip \ | ||||
|   && rm -rf /var/lib/apt/lists/* | ||||
| RUN curl -fsSL https://bun.sh/install | bash -s "bun-v1.2.2" | ||||
|  | ||||
| # 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/ | ||||
| COPY package.json bun.lock /temp/dev/ | ||||
| RUN cd /temp/dev && bun install --frozen-lockfile | ||||
|  | ||||
| # install with --production (exclude devDependencies) | ||||
| RUN mkdir -p /temp/prod | ||||
| COPY package.json bun.lockb /temp/prod/ | ||||
| COPY package.json bun.lock /temp/prod/ | ||||
| RUN cd /temp/prod && bun install --frozen-lockfile --production | ||||
|  | ||||
| FROM base AS builder | ||||
| RUN apk --no-cache add curl gcc | ||||
| ENV PATH=/root/.cargo/bin:$PATH | ||||
| RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y | ||||
| RUN cargo install resvg | ||||
| FROM base AS prerelease | ||||
| WORKDIR /app | ||||
| COPY --from=install /temp/dev/node_modules node_modules | ||||
| COPY . . | ||||
|  | ||||
| # 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 | ||||
| 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 apk --no-cache add  \ | ||||
|   pandoc \ | ||||
|   texlive \ | ||||
|   texlive-xetex \ | ||||
|   texmf-dist-latexextra \ | ||||
| RUN apt-get update && apt-get install -y \ | ||||
|   assimp-utils \ | ||||
|   calibre \ | ||||
|   dcraw \ | ||||
|   dvisvgm \ | ||||
|   ffmpeg \ | ||||
|   graphicsmagick \ | ||||
|   ghostscript \ | ||||
|   vips-tools \ | ||||
|   vips-poppler \ | ||||
|   vips-jxl \ | ||||
|   libjxl-tools | ||||
|  | ||||
| # this might be needed for some latex use cases, will add it if needed. | ||||
| #   texmf-dist-fontsextra \ | ||||
|   graphicsmagick \ | ||||
|   imagemagick-7.q16 \ | ||||
|   inkscape \ | ||||
|   libheif-examples \ | ||||
|   libjxl-tools \ | ||||
|   libva2 \ | ||||
|   libvips-tools \ | ||||
|   mupdf-tools \ | ||||
|   pandoc \ | ||||
|   poppler-utils \ | ||||
|   potrace \ | ||||
|   python3-numpy \ | ||||
|   resvg \ | ||||
|   texlive \ | ||||
|   texlive-latex-extra \ | ||||
|   texlive-xetex \ | ||||
|   --no-install-recommends \ | ||||
|   && rm -rf /var/lib/apt/lists/* | ||||
|  | ||||
| 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/index.tsx /app/src/ | ||||
| # COPY --from=prerelease /app/package.json . | ||||
| COPY --from=prerelease /app/public/generated.css /app/public/ | ||||
| COPY . . | ||||
|  | ||||
| EXPOSE 3000/tcp | ||||
|   | ||||
							
								
								
									
										109
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @@ -1,31 +1,46 @@ | ||||
|  | ||||
|  | ||||
| # ConvertX | ||||
|  | ||||
| [](https://github.com/C4illin/ConvertX/actions/workflows/docker-publish.yml) | ||||
| [](https://github.com/C4illin/ConvertX/pkgs/container/ConvertX) | ||||
| [](https://hub.docker.com/r/c4illin/convertx) | ||||
| [](https://github.com/C4illin/ConvertX/pkgs/container/convertx) | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| A self-hosted online file converter. Supports 831 different formats. Written with TypeScript, Bun and Elysia. | ||||
| <a href="https://trendshift.io/repositories/13818" target="_blank"><img src="https://trendshift.io/api/badge/repositories/13818" alt="C4illin%2FConvertX | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a> | ||||
|  | ||||
| <!--  --> | ||||
|  | ||||
| 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 | ||||
|  | ||||
| ## Converters supported | ||||
|  | ||||
| | Converter                                                                    | Use case      | Converts from | Converts to | | ||||
| |------------------------------------------------------------------------------|---------------|---------------|-------------| | ||||
| | [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          | | ||||
| | [XeLaTeX](https://tug.org/xetex/)                                            | LaTeX         | 1             | 1           | | ||||
| | [Pandoc](https://pandoc.org/)                                                | Documents     | 43            | 65          | | ||||
| | [GraphicsMagick](http://www.graphicsmagick.org/)                             | Images        | 166           | 133         | | ||||
| | [FFmpeg](https://ffmpeg.org/)                                                | Video         | ~473          | ~280        | | ||||
| | Converter                                        | Use case         | Converts from | Converts to | | ||||
| | ------------------------------------------------ | ---------------- | ------------- | ----------- | | ||||
| | [libjxl](https://github.com/libjxl/libjxl)       | JPEG XL          | 11            | 11          | | ||||
| | [resvg](https://github.com/RazrFalcon/resvg)     | SVG              | 1             | 1           | | ||||
| | [Vips](https://github.com/libvips/libvips)       | Images           | 45            | 23          | | ||||
| | [libheif](https://github.com/strukturag/libheif) | HEIF             | 2             | 4           | | ||||
| | [XeLaTeX](https://tug.org/xetex/)                | LaTeX            | 1             | 1           | | ||||
| | [Calibre](https://calibre-ebook.com/)            | E-books          | 26            | 19          | | ||||
| | [Pandoc](https://pandoc.org/)                    | Documents        | 43            | 65          | | ||||
| | [dvisvgm](https://dvisvgm.de/)                   | Vector images    | 4             | 2           | | ||||
| | [ImageMagick](https://imagemagick.org/)          | Images           | 245           | 183         | | ||||
| | [GraphicsMagick](http://www.graphicsmagick.org/) | Images           | 167           | 130         | | ||||
| | [Inkscape](https://inkscape.org/)                | Vector images    | 7             | 17          | | ||||
| | [Assimp](https://github.com/assimp/assimp)       | 3D Assets        | 77            | 23          | | ||||
| | [FFmpeg](https://ffmpeg.org/)                    | Video            | ~472          | ~199        | | ||||
| | [Potrace](https://potrace.sourceforge.net/)      | Raster to vector | 4             | 11          | | ||||
|  | ||||
| <!-- many ffmpeg fileformats are duplicates --> | ||||
|  | ||||
| @@ -33,37 +48,79 @@ Any missing converter? Open an issue or pull request! | ||||
|  | ||||
| ## Deployment | ||||
|  | ||||
| > [!WARNING] | ||||
| > If you can't login, make sure you are accessing the service over localhost or https otherwise set HTTP_ALLOWED=true | ||||
|  | ||||
| ```yml | ||||
| # docker-compose.yml | ||||
| services: | ||||
|   convertx:  | ||||
|   convertx: | ||||
|     image: ghcr.io/c4illin/convertx | ||||
|     container_name: convertx | ||||
|     restart: unless-stopped | ||||
|     ports: | ||||
|       - "3000:3000" | ||||
|     environment: # Defaults are listed below. All are optional. | ||||
|       - ACCOUNT_REGISTRATION=false # 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=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 | ||||
|     environment: | ||||
|       - JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234 # will use randomUUID() if unset | ||||
|     volumes: | ||||
|       - convertx:/app/data | ||||
|       - ./data:/app/data | ||||
| ``` | ||||
|  | ||||
| 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. | ||||
|  | ||||
| If you get unable to open database file run `chown -R $USER:$USER path` on the path you choose. | ||||
|  | ||||
| ### Environment variables | ||||
|  | ||||
| All are optional, JWT_SECRET is recommended to be set. | ||||
|  | ||||
| | Name                      | Default                                            | Description                                                                                              | | ||||
| | ------------------------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | ||||
| | JWT_SECRET                | when unset it will use the value from randomUUID() | A long and secret string used to sign the JSON Web Token                                                 | | ||||
| | ACCOUNT_REGISTRATION      | false                                              | Allow users to register accounts                                                                         | | ||||
| | HTTP_ALLOWED              | false                                              | Allow HTTP connections, only set this to true locally                                                    | | ||||
| | ALLOW_UNAUTHENTICATED     | false                                              | Allow unauthenticated users to use the service, 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                  | | ||||
| | WEBROOT                   |                                                    | The address to the root path setting this to "/convert" will serve the website on "example.com/convert/" | | ||||
| | FFMPEG_ARGS               |                                                    | Arguments to pass to ffmpeg, e.g. `-preset veryfast`                                                     | | ||||
| | HIDE_HISTORY              | false                                              | Hide the history page                                                                                    | | ||||
|  | ||||
| ### Docker images | ||||
|  | ||||
| There is a `:latest` tag that is updated with every release and a `:main` tag that is updated with every push to the main branch. `:latest` is recommended for normal use. | ||||
|  | ||||
| The image is available on [GitHub Container Registry](https://github.com/C4illin/ConvertX/pkgs/container/ConvertX) and [Docker Hub](https://hub.docker.com/r/c4illin/convertx). | ||||
|  | ||||
| | Image                                  | What it is                       | | ||||
| | -------------------------------------- | -------------------------------- | | ||||
| | `image: ghcr.io/c4illin/convertx`      | The latest release on ghcr       | | ||||
| | `image: ghcr.io/c4illin/convertx:main` | The latest commit on ghcr        | | ||||
| | `image: c4illin/convertx`              | The latest release on docker hub | | ||||
| | `image: c4illin/convertx:main`         | The latest commit on docker hub  | | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| <!-- Dockerhub was introduced in 0.9.0 and older releases --> | ||||
|  | ||||
| ### Tutorial | ||||
|  | ||||
| Tutorial in french: https://belginux.com/installer-convertx-avec-docker/ | ||||
| > [!NOTE] | ||||
| > These are written by other people, and may be outdated, incorrect or wrong. | ||||
|  | ||||
| Tutorial in french: <https://belginux.com/installer-convertx-avec-docker/> | ||||
|  | ||||
| Tutorial in chinese: <https://xzllll.com/24092901/> | ||||
|  | ||||
| ## Screenshots | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Development | ||||
|  | ||||
| @@ -74,23 +131,21 @@ Tutorial in french: https://belginux.com/installer-convertx-avec-docker/ | ||||
|  | ||||
| Pull requests are welcome! See below and open issues for the list of todos. | ||||
|  | ||||
| Use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) for commit messages. | ||||
|  | ||||
| ## Todo | ||||
| - [x] Add messages for errors in converters | ||||
|  | ||||
| - [ ] 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 | ||||
|   - [ ] [dvisvgm](https://github.com/mgieseki/dvisvgm) | ||||
|  | ||||
| ## Contributors | ||||
|  | ||||
| <a href="https://github.com/C4illin/ConvertX/graphs/contributors"> | ||||
|   <img src="https://contrib.rocks/image?repo=C4illin/ConvertX" /> | ||||
|   <img src="https://contrib.rocks/image?repo=C4illin/ConvertX" alt="Image with all contributors"/> | ||||
| </a> | ||||
|  | ||||
| ## Star History | ||||
|   | ||||
							
								
								
									
										9
									
								
								SECURITY.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,9 @@ | ||||
| # Security Policy | ||||
|  | ||||
| ## Supported Versions | ||||
|  | ||||
| Only the latest release is supported | ||||
|  | ||||
| ## Reporting a Vulnerability | ||||
|  | ||||
| To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/C4illin/ConvertX/security/advisories/new) tab. | ||||
							
								
								
									
										15
									
								
								biome.json
									
									
									
									
									
								
							
							
						
						| @@ -10,9 +10,11 @@ | ||||
|     "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 +27,11 @@ | ||||
|         "useLiteralKeys": "error", | ||||
|         "useOptionalChain": "error" | ||||
|       }, | ||||
|       "correctness": { "noPrecisionLoss": "error", "noUnusedVariables": "off" }, | ||||
|       "correctness": { | ||||
|         "noPrecisionLoss": "error", | ||||
|         "noUnusedVariables": "off", | ||||
|         "useJsxKeyInIterable": "off" | ||||
|       }, | ||||
|       "style": { | ||||
|         "noInferrableTypes": "error", | ||||
|         "noNamespace": "error", | ||||
| @@ -45,6 +51,9 @@ | ||||
|         "noUnsafeDeclarationMerging": "error", | ||||
|         "useAwait": "error", | ||||
|         "useNamespaceKeyword": "error" | ||||
|       }, | ||||
|       "nursery": { | ||||
|         "useSortedClasses": "error" | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   | ||||
							
								
								
									
										674
									
								
								bun.lock
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,674 @@ | ||||
| { | ||||
|   "lockfileVersion": 1, | ||||
|   "workspaces": { | ||||
|     "": { | ||||
|       "name": "convertx-frontend", | ||||
|       "dependencies": { | ||||
|         "@elysiajs/html": "^1.3.0", | ||||
|         "@elysiajs/jwt": "^1.3.0", | ||||
|         "@elysiajs/static": "^1.3.0", | ||||
|         "@kitajs/html": "^4.2.9", | ||||
|         "elysia": "^1.3.1", | ||||
|         "sanitize-filename": "^1.6.3", | ||||
|       }, | ||||
|       "devDependencies": { | ||||
|         "@eslint/js": "^9.27.0", | ||||
|         "@ianvs/prettier-plugin-sort-imports": "^4.4.1", | ||||
|         "@kitajs/ts-html-plugin": "^4.1.1", | ||||
|         "@tailwindcss/cli": "^4.1.7", | ||||
|         "@tailwindcss/postcss": "^4.1.7", | ||||
|         "@total-typescript/ts-reset": "^0.6.1", | ||||
|         "@types/bun": "^1.2.14", | ||||
|         "@types/node": "^22.15.21", | ||||
|         "@typescript-eslint/parser": "^8.33.1", | ||||
|         "eslint": "^9.27.0", | ||||
|         "eslint-plugin-better-tailwindcss": "^3.0.0", | ||||
|         "eslint-plugin-simple-import-sort": "^12.1.1", | ||||
|         "globals": "^16.1.0", | ||||
|         "knip": "^5.57.2", | ||||
|         "npm-run-all2": "^8.0.3", | ||||
|         "postcss": "^8.5.3", | ||||
|         "prettier": "^3.5.3", | ||||
|         "tailwind-scrollbar": "^4.0.2", | ||||
|         "tailwindcss": "^4.1.7", | ||||
|         "typescript": "^5.8.3", | ||||
|         "typescript-eslint": "^8.32.1", | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
|   "trustedDependencies": [ | ||||
|     "@tailwindcss/oxide", | ||||
|     "@parcel/watcher", | ||||
|   ], | ||||
|   "packages": { | ||||
|     "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], | ||||
|  | ||||
|     "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], | ||||
|  | ||||
|     "@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="], | ||||
|  | ||||
|     "@babel/generator": ["@babel/generator@7.26.5", "", { "dependencies": { "@babel/parser": "^7.26.5", "@babel/types": "^7.26.5", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw=="], | ||||
|  | ||||
|     "@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="], | ||||
|  | ||||
|     "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="], | ||||
|  | ||||
|     "@babel/parser": ["@babel/parser@7.26.7", "", { "dependencies": { "@babel/types": "^7.26.7" }, "bin": "./bin/babel-parser.js" }, "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w=="], | ||||
|  | ||||
|     "@babel/template": ["@babel/template@7.25.9", "", { "dependencies": { "@babel/code-frame": "^7.25.9", "@babel/parser": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg=="], | ||||
|  | ||||
|     "@babel/traverse": ["@babel/traverse@7.26.7", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.26.5", "@babel/parser": "^7.26.7", "@babel/template": "^7.25.9", "@babel/types": "^7.26.7", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA=="], | ||||
|  | ||||
|     "@babel/types": ["@babel/types@7.26.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg=="], | ||||
|  | ||||
|     "@elysiajs/html": ["@elysiajs/html@1.3.0", "", { "dependencies": { "@kitajs/html": "^4.1.0", "@kitajs/ts-html-plugin": "^4.0.1" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-NpujllWwiEXdsX8GJhbBppOv7+aJr+OU7Gn3K8fVXpwieutwau0/B/M6vzjYXsh9OaoGByUTpL8U9rA/tVSn7w=="], | ||||
|  | ||||
|     "@elysiajs/jwt": ["@elysiajs/jwt@1.3.1", "", { "dependencies": { "jose": "^6.0.11" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-BVLAp0ER4839bR82ElgTsI7OoPxvFWP5u02KMgqpNoAM6xirJBYTKqANzY9ghuMfQoVEuD4B1/8lZwdPKAVg9Q=="], | ||||
|  | ||||
|     "@elysiajs/static": ["@elysiajs/static@1.3.0", "", { "dependencies": { "node-cache": "^5.1.2" }, "peerDependencies": { "elysia": ">= 1.3.0" } }, "sha512-7mWlj2U/AZvH27IfRKqpUjDP1W9ZRldF9NmdnatFEtx0AOy7YYgyk0rt5hXrH6wPcR//2gO2Qy+k5rwswpEhJA=="], | ||||
|  | ||||
|     "@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" } }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="], | ||||
|  | ||||
|     "@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="], | ||||
|  | ||||
|     "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="], | ||||
|  | ||||
|     "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="], | ||||
|  | ||||
|     "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="], | ||||
|  | ||||
|     "@eslint/config-array": ["@eslint/config-array@0.20.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ=="], | ||||
|  | ||||
|     "@eslint/config-helpers": ["@eslint/config-helpers@0.2.2", "", {}, "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg=="], | ||||
|  | ||||
|     "@eslint/core": ["@eslint/core@0.14.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg=="], | ||||
|  | ||||
|     "@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="], | ||||
|  | ||||
|     "@eslint/js": ["@eslint/js@9.28.0", "", {}, "sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg=="], | ||||
|  | ||||
|     "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], | ||||
|  | ||||
|     "@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.1", "", { "dependencies": { "@eslint/core": "^0.14.0", "levn": "^0.4.1" } }, "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w=="], | ||||
|  | ||||
|     "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], | ||||
|  | ||||
|     "@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="], | ||||
|  | ||||
|     "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], | ||||
|  | ||||
|     "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.2", "", {}, "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ=="], | ||||
|  | ||||
|     "@ianvs/prettier-plugin-sort-imports": ["@ianvs/prettier-plugin-sort-imports@4.4.2", "", { "dependencies": { "@babel/generator": "^7.26.2", "@babel/parser": "^7.26.2", "@babel/traverse": "^7.25.9", "@babel/types": "^7.26.0", "semver": "^7.5.2" }, "peerDependencies": { "@vue/compiler-sfc": "2.7.x || 3.x", "prettier": "2 || 3 || ^4.0.0-0" }, "optionalPeers": ["@vue/compiler-sfc"] }, "sha512-KkVFy3TLh0OFzimbZglMmORi+vL/i2OFhEs5M07R9w0IwWAGpsNNyE4CY/2u0YoMF5bawKC2+8/fUH60nnNtjw=="], | ||||
|  | ||||
|     "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], | ||||
|  | ||||
|     "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="], | ||||
|  | ||||
|     "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], | ||||
|  | ||||
|     "@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="], | ||||
|  | ||||
|     "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], | ||||
|  | ||||
|     "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], | ||||
|  | ||||
|     "@kitajs/html": ["@kitajs/html@4.2.9", "", { "dependencies": { "csstype": "^3.1.3" } }, "sha512-FDHHf5Mi5nR0D+Btq86IV1O9XfsePVCiC5rwU4PXjw2aHja16FmIiwLZBO0CS16rJxKkibjMldyRLAW2ni2mzA=="], | ||||
|  | ||||
|     "@kitajs/ts-html-plugin": ["@kitajs/ts-html-plugin@4.1.1", "", { "dependencies": { "chalk": "^4.1.2", "tslib": "^2.8.1", "yargs": "^17.7.2" }, "peerDependencies": { "@kitajs/html": "^4.2.5", "typescript": "^5.6.2" }, "bin": { "ts-html-plugin": "dist/cli.js", "xss-scan": "dist/cli.js" } }, "sha512-wmjyV8hmJmDOnUM/ZyPkc0UBYgUYmf32/93rkW8wr8h+HiHVMU0tEKFnmRdBjTcy9jwoC9Bnt2NuzS9l67lq5g=="], | ||||
|  | ||||
|     "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.9", "", { "dependencies": { "@emnapi/core": "^1.4.0", "@emnapi/runtime": "^1.4.0", "@tybys/wasm-util": "^0.9.0" } }, "sha512-OKRBiajrrxB9ATokgEQoG87Z25c67pCpYcCwmXYX8PBftC9pBfN18gnm/fh1wurSLEKIAt+QRFLFCQISrb66Jg=="], | ||||
|  | ||||
|     "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], | ||||
|  | ||||
|     "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], | ||||
|  | ||||
|     "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], | ||||
|  | ||||
|     "@oxc-resolver/binding-darwin-arm64": ["@oxc-resolver/binding-darwin-arm64@9.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MVyRgP2gzJJtAowjG/cHN3VQXwNLWnY+FpOEsyvDepJki1SdAX/8XDijM1yN6ESD1kr9uhBKjGelC6h3qtT+rA=="], | ||||
|  | ||||
|     "@oxc-resolver/binding-darwin-x64": ["@oxc-resolver/binding-darwin-x64@9.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-7kV0EOFEZ3sk5Hjy4+bfA6XOQpCwbDiDkkHN4BHHyrBHsXxUR05EcEJPPL1WjItefg+9+8hrBmoK0xRoDs41+A=="], | ||||
|  | ||||
|     "@oxc-resolver/binding-freebsd-x64": ["@oxc-resolver/binding-freebsd-x64@9.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6OvkEtRXrt8sJ4aVfxHRikjain9nV1clIsWtJ1J3J8NG1ZhjyJFgT00SCvqxbK+pzeWJq6XzHyTCN78ML+lY2w=="], | ||||
|  | ||||
|     "@oxc-resolver/binding-linux-arm-gnueabihf": ["@oxc-resolver/binding-linux-arm-gnueabihf@9.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-aYpNL6o5IRAUIdoweW21TyLt54Hy/ZS9tvzNzF6ya1ckOQ8DLaGVPjGpmzxdNja9j/bbV6aIzBH7lNcBtiOTkQ=="], | ||||
|  | ||||
|     "@oxc-resolver/binding-linux-arm64-gnu": ["@oxc-resolver/binding-linux-arm64-gnu@9.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-RGFW4vCfKMFEIzb9VCY0oWyyY9tR1/o+wDdNePhiUXZU4SVniRPQaZ1SJ0sUFI1k25pXZmzQmIP6cBmazi/Dew=="], | ||||
|  | ||||
|     "@oxc-resolver/binding-linux-arm64-musl": ["@oxc-resolver/binding-linux-arm64-musl@9.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-lxx/PibBfzqYvut2Y8N2D0Ritg9H8pKO+7NUSJb9YjR/bfk2KRmP8iaUz3zB0JhPtf/W3REs65oKpWxgflGToA=="], | ||||
|  | ||||
|     "@oxc-resolver/binding-linux-riscv64-gnu": ["@oxc-resolver/binding-linux-riscv64-gnu@9.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-yD28ptS/OuNhwkpXRPNf+/FvrO7lwURLsEbRVcL1kIE0GxNJNMtKgIE4xQvtKDzkhk6ZRpLho5VSrkkF+3ARTQ=="], | ||||
|  | ||||
|     "@oxc-resolver/binding-linux-s390x-gnu": ["@oxc-resolver/binding-linux-s390x-gnu@9.0.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-WBwEJdspoga2w+aly6JVZeHnxuPVuztw3fPfWrei2P6rNM5hcKxBGWKKT6zO1fPMCB4sdDkFohGKkMHVV1eryQ=="], | ||||
|  | ||||
|     "@oxc-resolver/binding-linux-x64-gnu": ["@oxc-resolver/binding-linux-x64-gnu@9.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-a2z3/cbOOTUq0UTBG8f3EO/usFcdwwXnCejfXv42HmV/G8GjrT4fp5+5mVDoMByH3Ce3iVPxj1LmS6OvItKMYQ=="], | ||||
|  | ||||
|     "@oxc-resolver/binding-linux-x64-musl": ["@oxc-resolver/binding-linux-x64-musl@9.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-bHZF+WShYQWpuswB9fyxcgMIWVk4sZQT0wnwpnZgQuvGTZLkYJ1JTCXJMtaX5mIFHf69ngvawnwPIUA4Feil0g=="], | ||||
|  | ||||
|     "@oxc-resolver/binding-wasm32-wasi": ["@oxc-resolver/binding-wasm32-wasi@9.0.2", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.9" }, "cpu": "none" }, "sha512-I5cSgCCh5nFozGSHz+PjIOfrqW99eUszlxKLgoNNzQ1xQ2ou9ZJGzcZ94BHsM9SpyYHLtgHljmOZxCT9bgxYNA=="], | ||||
|  | ||||
|     "@oxc-resolver/binding-win32-arm64-msvc": ["@oxc-resolver/binding-win32-arm64-msvc@9.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-5IhoOpPr38YWDWRCA5kP30xlUxbIJyLAEsAK7EMyUgqygBHEYLkElaKGgS0X5jRXUQ6l5yNxuW73caogb2FYaw=="], | ||||
|  | ||||
|     "@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@9.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-Qc40GDkaad9rZksSQr2l/V9UubigIHsW69g94Gswc2sKYB3XfJXfIfyV8WTJ67u6ZMXsZ7BH1msSC6Aen75mCg=="], | ||||
|  | ||||
|     "@parcel/watcher": ["@parcel/watcher@2.5.1", "", { "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", "micromatch": "^4.0.5", "node-addon-api": "^7.0.0" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-freebsd-x64": "2.5.1", "@parcel/watcher-linux-arm-glibc": "2.5.1", "@parcel/watcher-linux-arm-musl": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", "@parcel/watcher-linux-arm64-musl": "2.5.1", "@parcel/watcher-linux-x64-glibc": "2.5.1", "@parcel/watcher-linux-x64-musl": "2.5.1", "@parcel/watcher-win32-arm64": "2.5.1", "@parcel/watcher-win32-ia32": "2.5.1", "@parcel/watcher-win32-x64": "2.5.1" } }, "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg=="], | ||||
|  | ||||
|     "@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.1", "", { "os": "android", "cpu": "arm64" }, "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA=="], | ||||
|  | ||||
|     "@parcel/watcher-darwin-arm64": ["@parcel/watcher-darwin-arm64@2.5.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw=="], | ||||
|  | ||||
|     "@parcel/watcher-darwin-x64": ["@parcel/watcher-darwin-x64@2.5.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg=="], | ||||
|  | ||||
|     "@parcel/watcher-freebsd-x64": ["@parcel/watcher-freebsd-x64@2.5.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ=="], | ||||
|  | ||||
|     "@parcel/watcher-linux-arm-glibc": ["@parcel/watcher-linux-arm-glibc@2.5.1", "", { "os": "linux", "cpu": "arm" }, "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA=="], | ||||
|  | ||||
|     "@parcel/watcher-linux-arm-musl": ["@parcel/watcher-linux-arm-musl@2.5.1", "", { "os": "linux", "cpu": "arm" }, "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q=="], | ||||
|  | ||||
|     "@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w=="], | ||||
|  | ||||
|     "@parcel/watcher-linux-arm64-musl": ["@parcel/watcher-linux-arm64-musl@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg=="], | ||||
|  | ||||
|     "@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A=="], | ||||
|  | ||||
|     "@parcel/watcher-linux-x64-musl": ["@parcel/watcher-linux-x64-musl@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg=="], | ||||
|  | ||||
|     "@parcel/watcher-win32-arm64": ["@parcel/watcher-win32-arm64@2.5.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw=="], | ||||
|  | ||||
|     "@parcel/watcher-win32-ia32": ["@parcel/watcher-win32-ia32@2.5.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ=="], | ||||
|  | ||||
|     "@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="], | ||||
|  | ||||
|     "@pkgr/core": ["@pkgr/core@0.1.1", "", {}, "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA=="], | ||||
|  | ||||
|     "@sinclair/typebox": ["@sinclair/typebox@0.34.33", "", {}, "sha512-5HAV9exOMcXRUxo+9iYB5n09XxzCXnfy4VTNW4xnDv+FgjzAGY989C28BIdljKqmF+ZltUwujE3aossvcVtq6g=="], | ||||
|  | ||||
|     "@tailwindcss/cli": ["@tailwindcss/cli@4.1.8", "", { "dependencies": { "@parcel/watcher": "^2.5.1", "@tailwindcss/node": "4.1.8", "@tailwindcss/oxide": "4.1.8", "enhanced-resolve": "^5.18.1", "mri": "^1.2.0", "picocolors": "^1.1.1", "tailwindcss": "4.1.8" }, "bin": { "tailwindcss": "dist/index.mjs" } }, "sha512-+6lkjXSr/68zWiabK3mVYVHmOq/SAHjJ13mR8spyB4LgUWZbWzU9kCSErlAUo+gK5aVfgqe8kY6Ltz9+nz5XYA=="], | ||||
|  | ||||
|     "@tailwindcss/node": ["@tailwindcss/node@4.1.8", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.8" } }, "sha512-OWwBsbC9BFAJelmnNcrKuf+bka2ZxCE2A4Ft53Tkg4uoiE67r/PMEYwCsourC26E+kmxfwE0hVzMdxqeW+xu7Q=="], | ||||
|  | ||||
|     "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.8", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.8", "@tailwindcss/oxide-darwin-arm64": "4.1.8", "@tailwindcss/oxide-darwin-x64": "4.1.8", "@tailwindcss/oxide-freebsd-x64": "4.1.8", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.8", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.8", "@tailwindcss/oxide-linux-arm64-musl": "4.1.8", "@tailwindcss/oxide-linux-x64-gnu": "4.1.8", "@tailwindcss/oxide-linux-x64-musl": "4.1.8", "@tailwindcss/oxide-wasm32-wasi": "4.1.8", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.8", "@tailwindcss/oxide-win32-x64-msvc": "4.1.8" } }, "sha512-d7qvv9PsM5N3VNKhwVUhpK6r4h9wtLkJ6lz9ZY9aeZgrUWk1Z8VPyqyDT9MZlem7GTGseRQHkeB1j3tC7W1P+A=="], | ||||
|  | ||||
|     "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.8", "", { "os": "android", "cpu": "arm64" }, "sha512-Fbz7qni62uKYceWYvUjRqhGfZKwhZDQhlrJKGtnZfuNtHFqa8wmr+Wn74CTWERiW2hn3mN5gTpOoxWKk0jRxjg=="], | ||||
|  | ||||
|     "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RdRvedGsT0vwVVDztvyXhKpsU2ark/BjgG0huo4+2BluxdXo8NDgzl77qh0T1nUxmM11eXwR8jA39ibvSTbi7A=="], | ||||
|  | ||||
|     "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-t6PgxjEMLp5Ovf7uMb2OFmb3kqzVTPPakWpBIFzppk4JE4ix0yEtbtSjPbU8+PZETpaYMtXvss2Sdkx8Vs4XRw=="], | ||||
|  | ||||
|     "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.8", "", { "os": "freebsd", "cpu": "x64" }, "sha512-g8C8eGEyhHTqwPStSwZNSrOlyx0bhK/V/+zX0Y+n7DoRUzyS8eMbVshVOLJTDDC+Qn9IJnilYbIKzpB9n4aBsg=="], | ||||
|  | ||||
|     "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.8", "", { "os": "linux", "cpu": "arm" }, "sha512-Jmzr3FA4S2tHhaC6yCjac3rGf7hG9R6Gf2z9i9JFcuyy0u79HfQsh/thifbYTF2ic82KJovKKkIB6Z9TdNhCXQ=="], | ||||
|  | ||||
|     "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-qq7jXtO1+UEtCmCeBBIRDrPFIVI4ilEQ97qgBGdwXAARrUqSn/L9fUrkb1XP/mvVtoVeR2bt/0L77xx53bPZ/Q=="], | ||||
|  | ||||
|     "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-O6b8QesPbJCRshsNApsOIpzKt3ztG35gfX9tEf4arD7mwNinsoCKxkj8TgEE0YRjmjtO3r9FlJnT/ENd9EVefQ=="], | ||||
|  | ||||
|     "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.8", "", { "os": "linux", "cpu": "x64" }, "sha512-32iEXX/pXwikshNOGnERAFwFSfiltmijMIAbUhnNyjFr3tmWmMJWQKU2vNcFX0DACSXJ3ZWcSkzNbaKTdngH6g=="], | ||||
|  | ||||
|     "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.8", "", { "os": "linux", "cpu": "x64" }, "sha512-s+VSSD+TfZeMEsCaFaHTaY5YNj3Dri8rST09gMvYQKwPphacRG7wbuQ5ZJMIJXN/puxPcg/nU+ucvWguPpvBDg=="], | ||||
|  | ||||
|     "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.8", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@emnapi/wasi-threads": "^1.0.2", "@napi-rs/wasm-runtime": "^0.2.10", "@tybys/wasm-util": "^0.9.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-CXBPVFkpDjM67sS1psWohZ6g/2/cd+cq56vPxK4JeawelxwK4YECgl9Y9TjkE2qfF+9/s1tHHJqrC4SS6cVvSg=="], | ||||
|  | ||||
|     "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-7GmYk1n28teDHUjPlIx4Z6Z4hHEgvP5ZW2QS9ygnDAdI/myh3HTHjDqtSqgu1BpRoI4OiLx+fThAyA1JePoENA=="], | ||||
|  | ||||
|     "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.8", "", { "os": "win32", "cpu": "x64" }, "sha512-fou+U20j+Jl0EHwK92spoWISON2OBnCazIc038Xj2TdweYV33ZRkS9nwqiUi2d/Wba5xg5UoHfvynnb/UB49cQ=="], | ||||
|  | ||||
|     "@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.8", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.8", "@tailwindcss/oxide": "4.1.8", "postcss": "^8.4.41", "tailwindcss": "4.1.8" } }, "sha512-vB/vlf7rIky+w94aWMw34bWW1ka6g6C3xIOdICKX2GC0VcLtL6fhlLiafF0DVIwa9V6EHz8kbWMkS2s2QvvNlw=="], | ||||
|  | ||||
|     "@tokenizer/inflate": ["@tokenizer/inflate@0.2.7", "", { "dependencies": { "debug": "^4.4.0", "fflate": "^0.8.2", "token-types": "^6.0.0" } }, "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg=="], | ||||
|  | ||||
|     "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], | ||||
|  | ||||
|     "@total-typescript/ts-reset": ["@total-typescript/ts-reset@0.6.1", "", {}, "sha512-cka47fVSo6lfQDIATYqb/vO1nvFfbPw7uWLayIXIhGETj0wcOOlrlkobOMDNQOFr9QOafegUPq13V2+6vtD7yg=="], | ||||
|  | ||||
|     "@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="], | ||||
|  | ||||
|     "@types/bun": ["@types/bun@1.2.15", "", { "dependencies": { "bun-types": "1.2.15" } }, "sha512-U1ljPdBEphF0nw1MIk0hI7kPg7dFdPyM7EenHsp6W5loNHl7zqy6JQf/RKCgnUn2KDzUpkBwHPnEJEjII594bA=="], | ||||
|  | ||||
|     "@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="], | ||||
|  | ||||
|     "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], | ||||
|  | ||||
|     "@types/node": ["@types/node@22.15.29", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ=="], | ||||
|  | ||||
|     "@types/prismjs": ["@types/prismjs@1.26.5", "", {}, "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ=="], | ||||
|  | ||||
|     "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.33.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.33.1", "@typescript-eslint/type-utils": "8.33.1", "@typescript-eslint/utils": "8.33.1", "@typescript-eslint/visitor-keys": "8.33.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.33.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-TDCXj+YxLgtvxvFlAvpoRv9MAncDLBV2oT9Bd7YBGC/b/sEURoOYuIwLI99rjWOfY3QtDzO+mk0n4AmdFExW8A=="], | ||||
|  | ||||
|     "@typescript-eslint/parser": ["@typescript-eslint/parser@8.33.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.33.1", "@typescript-eslint/types": "8.33.1", "@typescript-eslint/typescript-estree": "8.33.1", "@typescript-eslint/visitor-keys": "8.33.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-qwxv6dq682yVvgKKp2qWwLgRbscDAYktPptK4JPojCwwi3R9cwrvIxS4lvBpzmcqzR4bdn54Z0IG1uHFskW4dA=="], | ||||
|  | ||||
|     "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.33.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.33.1", "@typescript-eslint/types": "^8.33.1", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-DZR0efeNklDIHHGRpMpR5gJITQpu6tLr9lDJnKdONTC7vvzOlLAG/wcfxcdxEWrbiZApcoBCzXqU/Z458Za5Iw=="], | ||||
|  | ||||
|     "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.33.1", "", { "dependencies": { "@typescript-eslint/types": "8.33.1", "@typescript-eslint/visitor-keys": "8.33.1" } }, "sha512-dM4UBtgmzHR9bS0Rv09JST0RcHYearoEoo3pG5B6GoTR9XcyeqX87FEhPo+5kTvVfKCvfHaHrcgeJQc6mrDKrA=="], | ||||
|  | ||||
|     "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.33.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-STAQsGYbHCF0/e+ShUQ4EatXQ7ceh3fBCXkNU7/MZVKulrlq1usH7t2FhxvCpuCi5O5oi1vmVaAjrGeL71OK1g=="], | ||||
|  | ||||
|     "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.33.1", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.33.1", "@typescript-eslint/utils": "8.33.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-1cG37d9xOkhlykom55WVwG2QRNC7YXlxMaMzqw2uPeJixBFfKWZgaP/hjAObqMN/u3fr5BrTwTnc31/L9jQ2ww=="], | ||||
|  | ||||
|     "@typescript-eslint/types": ["@typescript-eslint/types@8.33.1", "", {}, "sha512-xid1WfizGhy/TKMTwhtVOgalHwPtV8T32MS9MaH50Cwvz6x6YqRIPdD2WvW0XaqOzTV9p5xdLY0h/ZusU5Lokg=="], | ||||
|  | ||||
|     "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.33.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.33.1", "@typescript-eslint/tsconfig-utils": "8.33.1", "@typescript-eslint/types": "8.33.1", "@typescript-eslint/visitor-keys": "8.33.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-+s9LYcT8LWjdYWu7IWs7FvUxpQ/DGkdjZeE/GGulHvv8rvYwQvVaUZ6DE+j5x/prADUgSbbCWZ2nPI3usuVeOA=="], | ||||
|  | ||||
|     "@typescript-eslint/utils": ["@typescript-eslint/utils@8.33.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.33.1", "@typescript-eslint/types": "8.33.1", "@typescript-eslint/typescript-estree": "8.33.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-52HaBiEQUaRYqAXpfzWSR2U3gxk92Kw006+xZpElaPMg3C4PgM+A5LqwoQI1f9E5aZ/qlxAZxzm42WX+vn92SQ=="], | ||||
|  | ||||
|     "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.33.1", "", { "dependencies": { "@typescript-eslint/types": "8.33.1", "eslint-visitor-keys": "^4.2.0" } }, "sha512-3i8NrFcZeeDHJ+7ZUuDkGT+UHq+XoFGsymNK2jZCOHcfEzRQ0BdpRtdpSx/Iyf3MHLWIcLS0COuOPibKQboIiQ=="], | ||||
|  | ||||
|     "acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], | ||||
|  | ||||
|     "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], | ||||
|  | ||||
|     "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], | ||||
|  | ||||
|     "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], | ||||
|  | ||||
|     "ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], | ||||
|  | ||||
|     "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], | ||||
|  | ||||
|     "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], | ||||
|  | ||||
|     "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], | ||||
|  | ||||
|     "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], | ||||
|  | ||||
|     "bun-types": ["bun-types@1.2.15", "", { "dependencies": { "@types/node": "*" } }, "sha512-NarRIaS+iOaQU1JPfyKhZm4AsUOrwUOqRNHY0XxI8GI8jYxiLXLcdjYMG9UKS+fwWasc1uw1htV9AX24dD+p4w=="], | ||||
|  | ||||
|     "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], | ||||
|  | ||||
|     "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], | ||||
|  | ||||
|     "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], | ||||
|  | ||||
|     "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], | ||||
|  | ||||
|     "clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="], | ||||
|  | ||||
|     "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], | ||||
|  | ||||
|     "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], | ||||
|  | ||||
|     "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], | ||||
|  | ||||
|     "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], | ||||
|  | ||||
|     "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], | ||||
|  | ||||
|     "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], | ||||
|  | ||||
|     "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], | ||||
|  | ||||
|     "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], | ||||
|  | ||||
|     "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], | ||||
|  | ||||
|     "detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="], | ||||
|  | ||||
|     "elysia": ["elysia@1.3.4", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.1.2", "fast-decode-uri-component": "^1.0.1" }, "optionalDependencies": { "@sinclair/typebox": "^0.34.33", "openapi-types": "^12.1.3" }, "peerDependencies": { "file-type": ">= 20.0.0", "typescript": ">= 5.0.0" } }, "sha512-kAfM3Zwovy3z255IZgTKVxBw91HbgKhYl3TqrGRdZqqr+Fd+4eKOfvxgaKij22+MZLczPzIHtscAmvfpI3+q/A=="], | ||||
|  | ||||
|     "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], | ||||
|  | ||||
|     "enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="], | ||||
|  | ||||
|     "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], | ||||
|  | ||||
|     "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], | ||||
|  | ||||
|     "eslint": ["eslint@9.28.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.20.0", "@eslint/config-helpers": "^0.2.1", "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.28.0", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ=="], | ||||
|  | ||||
|     "eslint-plugin-better-tailwindcss": ["eslint-plugin-better-tailwindcss@3.1.0", "", { "dependencies": { "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "postcss": "^8.5.4", "postcss-import": "^16.1.0", "synckit": "0.9.2" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0", "tailwindcss": "^3.3.0 || ^4.0.0" } }, "sha512-qaOnCBBvkxq5O1CPwzD8NWTNbBLY5RtjfpbOXiv0MtjX5GHnraj/cKBvjrfAPGH7FWrDeycR+kQ52aYqNWpujw=="], | ||||
|  | ||||
|     "eslint-plugin-simple-import-sort": ["eslint-plugin-simple-import-sort@12.1.1", "", { "peerDependencies": { "eslint": ">=5.0.0" } }, "sha512-6nuzu4xwQtE3332Uz0to+TxDQYRLTKRESSc2hefVT48Zc8JthmN23Gx9lnYhu0FtkRSL1oxny3kJ2aveVhmOVA=="], | ||||
|  | ||||
|     "eslint-scope": ["eslint-scope@8.3.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ=="], | ||||
|  | ||||
|     "eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="], | ||||
|  | ||||
|     "espree": ["espree@10.3.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.0" } }, "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg=="], | ||||
|  | ||||
|     "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="], | ||||
|  | ||||
|     "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], | ||||
|  | ||||
|     "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], | ||||
|  | ||||
|     "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], | ||||
|  | ||||
|     "exact-mirror": ["exact-mirror@0.1.2", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-wFCPCDLmHbKGUb8TOi/IS7jLsgR8WVDGtDK3CzcB4Guf/weq7G+I+DkXiRSZfbemBFOxOINKpraM6ml78vo8Zw=="], | ||||
|  | ||||
|     "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], | ||||
|  | ||||
|     "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], | ||||
|  | ||||
|     "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], | ||||
|  | ||||
|     "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], | ||||
|  | ||||
|     "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], | ||||
|  | ||||
|     "fastq": ["fastq@1.19.0", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA=="], | ||||
|  | ||||
|     "fd-package-json": ["fd-package-json@1.2.0", "", { "dependencies": { "walk-up-path": "^3.0.1" } }, "sha512-45LSPmWf+gC5tdCQMNH4s9Sr00bIkiD9aN7dc5hqkrEw1geRYyDQS1v1oMHAW3ysfxfndqGsrDREHHjNNbKUfA=="], | ||||
|  | ||||
|     "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], | ||||
|  | ||||
|     "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], | ||||
|  | ||||
|     "file-type": ["file-type@20.5.0", "", { "dependencies": { "@tokenizer/inflate": "^0.2.6", "strtok3": "^10.2.0", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-BfHZtG/l9iMm4Ecianu7P8HRD2tBHLtjXinm4X62XBOYzi7CYA7jyqfJzOvXHqzVrVPYqBo2/GvbARMaaJkKVg=="], | ||||
|  | ||||
|     "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], | ||||
|  | ||||
|     "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], | ||||
|  | ||||
|     "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], | ||||
|  | ||||
|     "flatted": ["flatted@3.3.2", "", {}, "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA=="], | ||||
|  | ||||
|     "formatly": ["formatly@0.2.3", "", { "dependencies": { "fd-package-json": "^1.2.0" }, "bin": { "formatly": "bin/index.mjs" } }, "sha512-WH01vbXEjh9L3bqn5V620xUAWs32CmK4IzWRRY6ep5zpa/mrisL4d9+pRVuETORVDTQw8OycSO1WC68PL51RaA=="], | ||||
|  | ||||
|     "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], | ||||
|  | ||||
|     "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], | ||||
|  | ||||
|     "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], | ||||
|  | ||||
|     "globals": ["globals@16.2.0", "", {}, "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg=="], | ||||
|  | ||||
|     "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], | ||||
|  | ||||
|     "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], | ||||
|  | ||||
|     "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], | ||||
|  | ||||
|     "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], | ||||
|  | ||||
|     "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], | ||||
|  | ||||
|     "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], | ||||
|  | ||||
|     "import-fresh": ["import-fresh@3.3.0", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw=="], | ||||
|  | ||||
|     "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], | ||||
|  | ||||
|     "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], | ||||
|  | ||||
|     "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], | ||||
|  | ||||
|     "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], | ||||
|  | ||||
|     "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], | ||||
|  | ||||
|     "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], | ||||
|  | ||||
|     "isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="], | ||||
|  | ||||
|     "jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="], | ||||
|  | ||||
|     "jose": ["jose@6.0.11", "", {}, "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg=="], | ||||
|  | ||||
|     "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], | ||||
|  | ||||
|     "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], | ||||
|  | ||||
|     "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], | ||||
|  | ||||
|     "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], | ||||
|  | ||||
|     "json-parse-even-better-errors": ["json-parse-even-better-errors@4.0.0", "", {}, "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA=="], | ||||
|  | ||||
|     "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], | ||||
|  | ||||
|     "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], | ||||
|  | ||||
|     "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], | ||||
|  | ||||
|     "knip": ["knip@5.59.1", "", { "dependencies": { "@nodelib/fs.walk": "^1.2.3", "fast-glob": "^3.3.3", "formatly": "^0.2.3", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "minimist": "^1.2.8", "oxc-resolver": "^9.0.2", "picocolors": "^1.1.0", "picomatch": "^4.0.1", "smol-toml": "^1.3.1", "strip-json-comments": "5.0.1", "zod": "^3.22.4", "zod-validation-error": "^3.0.3" }, "peerDependencies": { "@types/node": ">=18", "typescript": ">=5.0.4" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-pOMBw6sLQhi/RfnpI6TwBY6NrAtKXDO5wkmMm+pCsSK5eWbVfDnDtPXbLDGNCoZPXiuAojb27y4XOpp4JPNxlA=="], | ||||
|  | ||||
|     "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], | ||||
|  | ||||
|     "lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="], | ||||
|  | ||||
|     "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="], | ||||
|  | ||||
|     "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="], | ||||
|  | ||||
|     "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig=="], | ||||
|  | ||||
|     "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q=="], | ||||
|  | ||||
|     "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw=="], | ||||
|  | ||||
|     "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ=="], | ||||
|  | ||||
|     "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw=="], | ||||
|  | ||||
|     "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ=="], | ||||
|  | ||||
|     "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA=="], | ||||
|  | ||||
|     "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="], | ||||
|  | ||||
|     "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], | ||||
|  | ||||
|     "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], | ||||
|  | ||||
|     "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], | ||||
|  | ||||
|     "memorystream": ["memorystream@0.3.1", "", {}, "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw=="], | ||||
|  | ||||
|     "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], | ||||
|  | ||||
|     "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], | ||||
|  | ||||
|     "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], | ||||
|  | ||||
|     "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], | ||||
|  | ||||
|     "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], | ||||
|  | ||||
|     "minizlib": ["minizlib@3.0.2", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA=="], | ||||
|  | ||||
|     "mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="], | ||||
|  | ||||
|     "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], | ||||
|  | ||||
|     "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], | ||||
|  | ||||
|     "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], | ||||
|  | ||||
|     "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], | ||||
|  | ||||
|     "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], | ||||
|  | ||||
|     "node-cache": ["node-cache@5.1.2", "", { "dependencies": { "clone": "2.x" } }, "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg=="], | ||||
|  | ||||
|     "npm-normalize-package-bin": ["npm-normalize-package-bin@4.0.0", "", {}, "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w=="], | ||||
|  | ||||
|     "npm-run-all2": ["npm-run-all2@8.0.4", "", { "dependencies": { "ansi-styles": "^6.2.1", "cross-spawn": "^7.0.6", "memorystream": "^0.3.1", "picomatch": "^4.0.2", "pidtree": "^0.6.0", "read-package-json-fast": "^4.0.0", "shell-quote": "^1.7.3", "which": "^5.0.0" }, "bin": { "run-p": "bin/run-p/index.js", "run-s": "bin/run-s/index.js", "npm-run-all": "bin/npm-run-all/index.js", "npm-run-all2": "bin/npm-run-all/index.js" } }, "sha512-wdbB5My48XKp2ZfJUlhnLVihzeuA1hgBnqB2J9ahV77wLS+/YAJAlN8I+X3DIFIPZ3m5L7nplmlbhNiFDmXRDA=="], | ||||
|  | ||||
|     "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], | ||||
|  | ||||
|     "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], | ||||
|  | ||||
|     "oxc-resolver": ["oxc-resolver@9.0.2", "", { "optionalDependencies": { "@oxc-resolver/binding-darwin-arm64": "9.0.2", "@oxc-resolver/binding-darwin-x64": "9.0.2", "@oxc-resolver/binding-freebsd-x64": "9.0.2", "@oxc-resolver/binding-linux-arm-gnueabihf": "9.0.2", "@oxc-resolver/binding-linux-arm64-gnu": "9.0.2", "@oxc-resolver/binding-linux-arm64-musl": "9.0.2", "@oxc-resolver/binding-linux-riscv64-gnu": "9.0.2", "@oxc-resolver/binding-linux-s390x-gnu": "9.0.2", "@oxc-resolver/binding-linux-x64-gnu": "9.0.2", "@oxc-resolver/binding-linux-x64-musl": "9.0.2", "@oxc-resolver/binding-wasm32-wasi": "9.0.2", "@oxc-resolver/binding-win32-arm64-msvc": "9.0.2", "@oxc-resolver/binding-win32-x64-msvc": "9.0.2" } }, "sha512-w838ygc1p7rF+7+h5vR9A+Y9Fc4imy6C3xPthCMkdFUgFvUWkmABeNB8RBDQ6+afk44Q60/UMMQ+gfDUW99fBA=="], | ||||
|  | ||||
|     "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], | ||||
|  | ||||
|     "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], | ||||
|  | ||||
|     "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], | ||||
|  | ||||
|     "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], | ||||
|  | ||||
|     "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], | ||||
|  | ||||
|     "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], | ||||
|  | ||||
|     "peek-readable": ["peek-readable@7.0.0", "", {}, "sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ=="], | ||||
|  | ||||
|     "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], | ||||
|  | ||||
|     "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], | ||||
|  | ||||
|     "pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="], | ||||
|  | ||||
|     "pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], | ||||
|  | ||||
|     "postcss": ["postcss@8.5.4", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w=="], | ||||
|  | ||||
|     "postcss-import": ["postcss-import@16.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-7hsAZ4xGXl4MW+OKEWCnF6T5jqBw80/EE9aXg1r2yyn1RsVEU8EtKXbijEODa+rg7iih4bKf7vlvTGYR4CnPNg=="], | ||||
|  | ||||
|     "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], | ||||
|  | ||||
|     "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], | ||||
|  | ||||
|     "prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="], | ||||
|  | ||||
|     "prism-react-renderer": ["prism-react-renderer@2.4.1", "", { "dependencies": { "@types/prismjs": "^1.26.0", "clsx": "^2.0.0" }, "peerDependencies": { "react": ">=16.0.0" } }, "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig=="], | ||||
|  | ||||
|     "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], | ||||
|  | ||||
|     "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], | ||||
|  | ||||
|     "react": ["react@19.0.0", "", {}, "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="], | ||||
|  | ||||
|     "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="], | ||||
|  | ||||
|     "read-package-json-fast": ["read-package-json-fast@4.0.0", "", { "dependencies": { "json-parse-even-better-errors": "^4.0.0", "npm-normalize-package-bin": "^4.0.0" } }, "sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg=="], | ||||
|  | ||||
|     "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], | ||||
|  | ||||
|     "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], | ||||
|  | ||||
|     "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], | ||||
|  | ||||
|     "reusify": ["reusify@1.0.4", "", {}, "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="], | ||||
|  | ||||
|     "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], | ||||
|  | ||||
|     "sanitize-filename": ["sanitize-filename@1.6.3", "", { "dependencies": { "truncate-utf8-bytes": "^1.0.0" } }, "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg=="], | ||||
|  | ||||
|     "semver": ["semver@7.7.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ=="], | ||||
|  | ||||
|     "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], | ||||
|  | ||||
|     "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], | ||||
|  | ||||
|     "shell-quote": ["shell-quote@1.8.2", "", {}, "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA=="], | ||||
|  | ||||
|     "smol-toml": ["smol-toml@1.3.1", "", {}, "sha512-tEYNll18pPKHroYSmLLrksq233j021G0giwW7P3D24jC54pQ5W5BXMsQ/Mvw1OJCmEYDgY+lrzT+3nNUtoNfXQ=="], | ||||
|  | ||||
|     "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], | ||||
|  | ||||
|     "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], | ||||
|  | ||||
|     "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], | ||||
|  | ||||
|     "strip-json-comments": ["strip-json-comments@5.0.1", "", {}, "sha512-0fk9zBqO67Nq5M/m45qHCJxylV/DhBlIOVExqgOMiCCrzrhU6tCibRXNqE3jwJLftzE9SNuZtYbpzcO+i9FiKw=="], | ||||
|  | ||||
|     "strtok3": ["strtok3@10.2.2", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^7.0.0" } }, "sha512-Xt18+h4s7Z8xyZ0tmBoRmzxcop97R4BAh+dXouUDCYn+Em+1P3qpkUfI5ueWLT8ynC5hZ+q4iPEmGG1urvQGBg=="], | ||||
|  | ||||
|     "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], | ||||
|  | ||||
|     "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], | ||||
|  | ||||
|     "synckit": ["synckit@0.9.2", "", { "dependencies": { "@pkgr/core": "^0.1.0", "tslib": "^2.6.2" } }, "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw=="], | ||||
|  | ||||
|     "tailwind-scrollbar": ["tailwind-scrollbar@4.0.2", "", { "dependencies": { "prism-react-renderer": "^2.4.1" }, "peerDependencies": { "tailwindcss": "4.x" } }, "sha512-wAQiIxAPqk0MNTPptVe/xoyWi27y+NRGnTwvn4PQnbvB9kp8QUBiGl/wsfoVBHnQxTmhXJSNt9NHTmcz9EivFA=="], | ||||
|  | ||||
|     "tailwindcss": ["tailwindcss@4.1.8", "", {}, "sha512-kjeW8gjdxasbmFKpVGrGd5T4i40mV5J2Rasw48QARfYeQ8YS9x02ON9SFWax3Qf616rt4Cp3nVNIj6Hd1mP3og=="], | ||||
|  | ||||
|     "tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="], | ||||
|  | ||||
|     "tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="], | ||||
|  | ||||
|     "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], | ||||
|  | ||||
|     "token-types": ["token-types@6.0.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA=="], | ||||
|  | ||||
|     "truncate-utf8-bytes": ["truncate-utf8-bytes@1.0.2", "", { "dependencies": { "utf8-byte-length": "^1.0.1" } }, "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ=="], | ||||
|  | ||||
|     "ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="], | ||||
|  | ||||
|     "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], | ||||
|  | ||||
|     "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], | ||||
|  | ||||
|     "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], | ||||
|  | ||||
|     "typescript-eslint": ["typescript-eslint@8.33.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.33.1", "@typescript-eslint/parser": "8.33.1", "@typescript-eslint/utils": "8.33.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-AgRnV4sKkWOiZ0Kjbnf5ytTJXMUZQ0qhSVdQtDNYLPLnjsATEYhaO94GlRQwi4t4gO8FfjM6NnikHeKjUm8D7A=="], | ||||
|  | ||||
|     "uint8array-extras": ["uint8array-extras@1.4.0", "", {}, "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ=="], | ||||
|  | ||||
|     "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], | ||||
|  | ||||
|     "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], | ||||
|  | ||||
|     "utf8-byte-length": ["utf8-byte-length@1.0.5", "", {}, "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA=="], | ||||
|  | ||||
|     "walk-up-path": ["walk-up-path@3.0.1", "", {}, "sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA=="], | ||||
|  | ||||
|     "which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], | ||||
|  | ||||
|     "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], | ||||
|  | ||||
|     "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], | ||||
|  | ||||
|     "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], | ||||
|  | ||||
|     "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], | ||||
|  | ||||
|     "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], | ||||
|  | ||||
|     "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], | ||||
|  | ||||
|     "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], | ||||
|  | ||||
|     "zod": ["zod@3.24.4", "", {}, "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg=="], | ||||
|  | ||||
|     "zod-validation-error": ["zod-validation-error@3.4.0", "", { "peerDependencies": { "zod": "^3.18.0" } }, "sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ=="], | ||||
|  | ||||
|     "@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], | ||||
|  | ||||
|     "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], | ||||
|  | ||||
|     "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], | ||||
|  | ||||
|     "@eslint/eslintrc/strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], | ||||
|  | ||||
|     "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], | ||||
|  | ||||
|     "@tailwindcss/oxide/detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="], | ||||
|  | ||||
|     "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="], | ||||
|  | ||||
|     "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="], | ||||
|  | ||||
|     "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="], | ||||
|  | ||||
|     "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.10", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.9.0" }, "bundled": true }, "sha512-bCsCyeZEwVErsGmyPNSzwfwFn4OdxBj0mmv6hOFucB/k81Ojdu68RbZdxYsRQUPc9l6SU5F/cG+bXgWs3oUgsQ=="], | ||||
|  | ||||
|     "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="], | ||||
|  | ||||
|     "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], | ||||
|  | ||||
|     "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.4", "", {}, "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A=="], | ||||
|  | ||||
|     "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], | ||||
|  | ||||
|     "chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], | ||||
|  | ||||
|     "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], | ||||
|  | ||||
|     "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], | ||||
|  | ||||
|     "lightningcss/detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="], | ||||
|  | ||||
|     "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], | ||||
|  | ||||
|     "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], | ||||
|  | ||||
|     "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], | ||||
|  | ||||
|     "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], | ||||
|   } | ||||
| } | ||||
							
								
								
									
										14
									
								
								compose.yaml
									
									
									
									
									
								
							
							
						
						| @@ -5,9 +5,15 @@ 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=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=1 # checks every n hours for files older then n hours and deletes them, set to 0 to disable | ||||
|       # - FFMPEG_ARGS=-hwaccel vulkan # additional arguments to pass to ffmpeg | ||||
|       # - WEBROOT=/convertx # the root path of the web interface, leave empty to disable | ||||
|       # - HIDE_HISTORY=true # hides the history tab in the web interface, defaults to false | ||||
|       - TZ=Europe/Stockholm # set your timezone, defaults to UTC | ||||
|     ports: | ||||
|       - 3000:3000 | ||||
|   | ||||
| @@ -1,36 +0,0 @@ | ||||
| import { fixupPluginRules } from "@eslint/compat"; | ||||
| import tseslint from "typescript-eslint"; | ||||
| import eslint 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"; | ||||
|  | ||||
| export default tseslint.config( | ||||
|   { | ||||
|     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, | ||||
|         tsconfigRootDir: import.meta.dirname, | ||||
|         ecmaVersion: "latest", | ||||
|         sourceType: "module", | ||||
|         project: ["./tsconfig.json"], | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
| ); | ||||
							
								
								
									
										66
									
								
								eslint.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,66 @@ | ||||
| import js from "@eslint/js"; | ||||
| import eslintParserTypeScript from "@typescript-eslint/parser"; | ||||
| import type { Linter } from "eslint"; | ||||
| import eslintPluginBetterTailwindcss from "eslint-plugin-better-tailwindcss"; | ||||
| import simpleImportSortPlugin from "eslint-plugin-simple-import-sort"; | ||||
| import globals from "globals"; | ||||
| import tseslint from "typescript-eslint"; | ||||
|  | ||||
| export default [ | ||||
|   js.configs.recommended, | ||||
|   ...tseslint.configs.recommended, | ||||
|   // ...tailwind.configs["flat/recommended"], | ||||
|   { | ||||
|     plugins: { | ||||
|       "simple-import-sort": simpleImportSortPlugin, | ||||
|       "better-tailwindcss": eslintPluginBetterTailwindcss, | ||||
|     }, | ||||
|     ignores: ["**/node_modules/**"], | ||||
|     languageOptions: { | ||||
|       parser: eslintParserTypeScript, | ||||
|       parserOptions: { | ||||
|         project: true, | ||||
|         ecmaFeatures: { | ||||
|           jsx: true, | ||||
|         }, | ||||
|       }, | ||||
|       globals: { | ||||
|         ...globals.node, | ||||
|         ...globals.browser, | ||||
|       }, | ||||
|     }, | ||||
|     files: ["**/*.{js,mjs,cjs,jsx,tsx,ts}"], | ||||
|     settings: { | ||||
|       "better-tailwindcss": { | ||||
|         entryPoint: "src/main.css", | ||||
|       }, | ||||
|     }, | ||||
|     rules: { | ||||
|       ...(eslintPluginBetterTailwindcss.configs["recommended-warn"] ?? {}).rules, | ||||
|       ...(eslintPluginBetterTailwindcss.configs["stylistic-warn"] ?? {}).rules, | ||||
|       // "tailwindcss/classnames-order": "off", | ||||
|       "better-tailwindcss/multiline": [ | ||||
|         "warn", | ||||
|         { | ||||
|           group: "newLine", | ||||
|           printWidth: 100, | ||||
|         }, | ||||
|       ], | ||||
|       "better-tailwindcss/no-unregistered-classes": [ | ||||
|         "warn", | ||||
|         { | ||||
|           ignore: [ | ||||
|             "^group(?:\\/(\\S*))?$", | ||||
|             "^peer(?:\\/(\\S*))?$", | ||||
|             "select_container", | ||||
|             "convert_to_popup", | ||||
|             "convert_to_group", | ||||
|             "target", | ||||
|             "convert_to_target", | ||||
|             "job-details-toggle", | ||||
|           ], | ||||
|         }, | ||||
|       ], | ||||
|     }, | ||||
|   }, | ||||
| ] as Linter.Config[]; | ||||
							
								
								
									
										
											BIN
										
									
								
								images/preview.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 53 KiB | 
							
								
								
									
										9
									
								
								knip.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,9 @@ | ||||
| { | ||||
|   "$schema": "https://unpkg.com/knip@5/schema.json", | ||||
|   "entry": ["src/index.tsx"], | ||||
|   "project": ["src/**/*.ts", "src/**/*.tsx", "src/main.css"], | ||||
|   "tailwind": { | ||||
|     "entry": ["src/main.css"] | ||||
|   }, | ||||
|   "ignoreDependencies": ["tailwind-scrollbar"] | ||||
| } | ||||
							
								
								
									
										70
									
								
								package.json
									
									
									
									
									
								
							
							
						
						| @@ -1,22 +1,26 @@ | ||||
| { | ||||
|   "name": "convertx-frontend", | ||||
|   "version": "0.4.0", | ||||
|   "version": "0.14.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", | ||||
|     "format": "run-p 'format:*'", | ||||
|     "format:eslint": "eslint --fix .", | ||||
|     "format:prettier": "prettier --write .", | ||||
|     "build": "bunx @tailwindcss/cli -i ./src/main.css -o ./public/generated.css", | ||||
|     "lint": "run-p 'lint:*'", | ||||
|     "lint:tsc": "tsc --noEmit", | ||||
|     "lint:knip": "knip", | ||||
|     "lint:biome": "biome lint --error-on-warnings ./src" | ||||
|     "lint:eslint": "eslint .", | ||||
|     "lint:prettier": "prettier --check ." | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@elysiajs/cookie": "^0.8.0", | ||||
|     "@elysiajs/html": "1.0.2", | ||||
|     "@elysiajs/jwt": "^1.1.0", | ||||
|     "@elysiajs/static": "1.0.3", | ||||
|     "elysia": "^1.1.7" | ||||
|     "@elysiajs/html": "^1.3.0", | ||||
|     "@elysiajs/jwt": "^1.3.1", | ||||
|     "@elysiajs/static": "^1.3.0", | ||||
|     "@kitajs/html": "^4.2.9", | ||||
|     "elysia": "^1.3.4", | ||||
|     "sanitize-filename": "^1.6.3" | ||||
|   }, | ||||
|   "module": "src/index.tsx", | ||||
|   "type": "module", | ||||
| @@ -24,34 +28,30 @@ | ||||
|     "start": "bun run src/index.tsx" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@biomejs/biome": "1.8.3", | ||||
|     "@eslint/compat": "^1.1.1", | ||||
|     "@eslint/js": "^9.9.0", | ||||
|     "@ianvs/prettier-plugin-sort-imports": "^4.3.1", | ||||
|     "@kitajs/ts-html-plugin": "^4.0.2", | ||||
|     "@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", | ||||
|     "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-prettier": "^5.2.1", | ||||
|     "@eslint/js": "^9.28.0", | ||||
|     "@ianvs/prettier-plugin-sort-imports": "^4.4.2", | ||||
|     "@kitajs/ts-html-plugin": "^4.1.1", | ||||
|     "@tailwindcss/cli": "^4.1.8", | ||||
|     "@tailwindcss/postcss": "^4.1.8", | ||||
|     "@total-typescript/ts-reset": "^0.6.1", | ||||
|     "@types/bun": "^1.2.15", | ||||
|     "@types/node": "^22.15.29", | ||||
|     "@typescript-eslint/parser": "^8.33.1", | ||||
|     "eslint": "^9.28.0", | ||||
|     "eslint-plugin-better-tailwindcss": "^3.1.0", | ||||
|     "eslint-plugin-simple-import-sort": "^12.1.1", | ||||
|     "knip": "^5.27.3", | ||||
|     "npm-run-all2": "^6.2.2", | ||||
|     "prettier": "^3.3.3", | ||||
|     "typescript": "^5.5.4", | ||||
|     "typescript-eslint": "^8.2.0" | ||||
|     "globals": "^16.2.0", | ||||
|     "knip": "^5.59.1", | ||||
|     "npm-run-all2": "^8.0.4", | ||||
|     "postcss": "^8.5.4", | ||||
|     "prettier": "^3.5.3", | ||||
|     "tailwind-scrollbar": "^4.0.2", | ||||
|     "tailwindcss": "^4.1.8", | ||||
|     "typescript": "^5.8.3", | ||||
|     "typescript-eslint": "^8.33.1" | ||||
|   }, | ||||
|   "trustedDependencies": [ | ||||
|     "@biomejs/biome" | ||||
|     "@parcel/watcher", | ||||
|     "@tailwindcss/oxide" | ||||
|   ] | ||||
| } | ||||
|   | ||||
							
								
								
									
										5
									
								
								postcss.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,5 @@ | ||||
| export default { | ||||
|   plugins: { | ||||
|     "@tailwindcss/postcss": {}, | ||||
|   }, | ||||
| }; | ||||
| @@ -3,7 +3,7 @@ | ||||
|  */ | ||||
| const config = { | ||||
|   arrowParens: "always", | ||||
|   printWidth: 80, | ||||
|   printWidth: 100, | ||||
|   singleQuote: false, | ||||
|   semi: true, | ||||
|   tabWidth: 2, | ||||
| Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.1 KiB | 
| Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB | 
| Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB | 
| Before Width: | Height: | Size: 476 B After Width: | Height: | Size: 476 B | 
| Before Width: | Height: | Size: 960 B After Width: | Height: | Size: 960 B | 
| Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB | 
| @@ -1,3 +1,5 @@ | ||||
| const webroot = document.querySelector("meta[name='webroot']").content; | ||||
| 
 | ||||
| window.downloadAll = function () { | ||||
|   // Get all download links
 | ||||
|   const downloadLinks = document.querySelectorAll("a[download]"); | ||||
| @@ -18,7 +20,7 @@ let progressElem = document.querySelector("progress"); | ||||
| const refreshData = () => { | ||||
|   // console.log("Refreshing data...", progressElem.value, progressElem.max);
 | ||||
|   if (progressElem.value !== progressElem.max) { | ||||
|     fetch(`/progress/${jobId}`, { | ||||
|     fetch(`${webroot}/progress/${jobId}`, { | ||||
|       method: "POST", | ||||
|     }) | ||||
|       .then((res) => res.text()) | ||||
							
								
								
									
										251
									
								
								public/script.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,251 @@ | ||||
| const webroot = document.querySelector("meta[name='webroot']").content; | ||||
| const fileInput = document.querySelector('input[type="file"]'); | ||||
| const dropZone = document.getElementById("dropzone"); | ||||
| const convertButton = document.querySelector("input[type='submit']"); | ||||
| const fileNames = []; | ||||
| let fileType; | ||||
| let pendingFiles = 0; | ||||
| let formatSelected = false; | ||||
|  | ||||
| dropZone.addEventListener("dragover", (e) => { | ||||
|   e.preventDefault(); | ||||
|   dropZone.classList.add("dragover"); | ||||
| }); | ||||
|  | ||||
| dropZone.addEventListener("dragleave", () => { | ||||
|   dropZone.classList.remove("dragover"); | ||||
| }); | ||||
|  | ||||
| dropZone.addEventListener("drop", (e) => { | ||||
|   e.preventDefault(); | ||||
|   dropZone.classList.remove("dragover"); | ||||
|  | ||||
|   const files = e.dataTransfer.files; | ||||
|  | ||||
|   if (files.length === 0) { | ||||
|     console.warn("No files dropped — likely a URL or unsupported source."); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   for (const file of files) { | ||||
|     console.log("Handling dropped file:", file.name); | ||||
|     handleFile(file); | ||||
|   } | ||||
| }); | ||||
|  | ||||
| // Extracted handleFile function for reusability in drag-and-drop and file input | ||||
| function handleFile(file) { | ||||
|   const fileList = document.querySelector("#file-list"); | ||||
|  | ||||
|   const row = document.createElement("tr"); | ||||
|   row.innerHTML = ` | ||||
|     <td>${file.name}</td> | ||||
|     <td><progress max="100" class="inline-block h-2 appearance-none overflow-hidden rounded-full border-0 bg-neutral-700 bg-none text-accent-500 accent-accent-500 [&::-moz-progress-bar]:bg-accent-500 [&::-webkit-progress-value]:rounded-full [&::-webkit-progress-value]:[background:none] [&[value]::-webkit-progress-value]:bg-accent-500 [&[value]::-webkit-progress-value]:transition-[inline-size]"></progress></td> | ||||
|     <td>${(file.size / 1024).toFixed(2)} kB</td> | ||||
|     <td><a onclick="deleteRow(this)">Remove</a></td> | ||||
|   `; | ||||
|  | ||||
|   if (!fileType) { | ||||
|     fileType = file.name.split(".").pop(); | ||||
|     fileInput.setAttribute("accept", `.${fileType}`); | ||||
|     setTitle(); | ||||
|  | ||||
|     fetch(`${webroot}/conversions`, { | ||||
|       method: "POST", | ||||
|       body: JSON.stringify({ fileType }), | ||||
|       headers: { "Content-Type": "application/json" }, | ||||
|     }) | ||||
|       .then((res) => res.text()) | ||||
|       .then((html) => { | ||||
|         selectContainer.innerHTML = html; | ||||
|         updateSearchBar(); | ||||
|       }) | ||||
|       .catch(console.error); | ||||
|   } | ||||
|  | ||||
|   fileList.appendChild(row); | ||||
|   file.htmlRow = row; | ||||
|   fileNames.push(file.name); | ||||
|   uploadFile(file); | ||||
| } | ||||
|  | ||||
| const selectContainer = document.querySelector("form .select_container"); | ||||
|  | ||||
| const updateSearchBar = () => { | ||||
|   const convertToInput = document.querySelector("input[name='convert_to_search']"); | ||||
|   const convertToPopup = document.querySelector(".convert_to_popup"); | ||||
|   const convertToGroupElements = document.querySelectorAll(".convert_to_group"); | ||||
|   const convertToGroups = {}; | ||||
|   const convertToElement = document.querySelector("select[name='convert_to']"); | ||||
|  | ||||
|   const showMatching = (search) => { | ||||
|     for (const [targets, groupElement] of Object.values(convertToGroups)) { | ||||
|       let matchingTargetsFound = 0; | ||||
|       for (const target of targets) { | ||||
|         if (target.dataset.target.includes(search)) { | ||||
|           matchingTargetsFound++; | ||||
|           target.classList.remove("hidden"); | ||||
|           target.classList.add("flex"); | ||||
|         } else { | ||||
|           target.classList.add("hidden"); | ||||
|           target.classList.remove("flex"); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       if (matchingTargetsFound === 0) { | ||||
|         groupElement.classList.add("hidden"); | ||||
|         groupElement.classList.remove("flex"); | ||||
|       } else { | ||||
|         groupElement.classList.remove("hidden"); | ||||
|         groupElement.classList.add("flex"); | ||||
|       } | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   for (const groupElement of convertToGroupElements) { | ||||
|     const groupName = groupElement.dataset.converter; | ||||
|  | ||||
|     const targetElements = groupElement.querySelectorAll(".target"); | ||||
|     const targets = Array.from(targetElements); | ||||
|  | ||||
|     for (const target of targets) { | ||||
|       target.onmousedown = () => { | ||||
|         convertToElement.value = target.dataset.value; | ||||
|         convertToInput.value = `${target.dataset.target} using ${target.dataset.converter}`; | ||||
|         formatSelected = true; | ||||
|         if (pendingFiles === 0 && fileNames.length > 0) { | ||||
|           convertButton.disabled = false; | ||||
|         } | ||||
|         showMatching(""); | ||||
|       }; | ||||
|     } | ||||
|  | ||||
|     convertToGroups[groupName] = [targets, groupElement]; | ||||
|   } | ||||
|  | ||||
|   convertToInput.addEventListener("input", (e) => { | ||||
|     showMatching(e.target.value.toLowerCase()); | ||||
|   }); | ||||
|  | ||||
|   convertToInput.addEventListener("search", () => { | ||||
|     // when the user clears the search bar using the 'x' button | ||||
|     convertButton.disabled = true; | ||||
|     formatSelected = false; | ||||
|   }); | ||||
|  | ||||
|   convertToInput.addEventListener("blur", (e) => { | ||||
|     // Keep the popup open even when clicking on a target button | ||||
|     // for a split second to allow the click to go through | ||||
|     if (e?.relatedTarget?.classList?.contains("target")) { | ||||
|       convertToPopup.classList.add("hidden"); | ||||
|       convertToPopup.classList.remove("flex"); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     convertToPopup.classList.add("hidden"); | ||||
|     convertToPopup.classList.remove("flex"); | ||||
|   }); | ||||
|  | ||||
|   convertToInput.addEventListener("focus", () => { | ||||
|     convertToPopup.classList.remove("hidden"); | ||||
|     convertToPopup.classList.add("flex"); | ||||
|   }); | ||||
| }; | ||||
|  | ||||
| // Add a 'change' event listener to the file input element | ||||
| fileInput.addEventListener("change", (e) => { | ||||
|   const files = e.target.files; | ||||
|   for (const file of files) { | ||||
|     handleFile(file); | ||||
|   } | ||||
| }); | ||||
|  | ||||
| const setTitle = () => { | ||||
|   const title = document.querySelector("h1"); | ||||
|   title.textContent = `Convert ${fileType ? `.${fileType}` : ""}`; | ||||
| }; | ||||
|  | ||||
| // Add a onclick for the delete button | ||||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||||
| const deleteRow = (target) => { | ||||
|   const filename = target.parentElement.parentElement.children[0].textContent; | ||||
|   const row = target.parentElement.parentElement; | ||||
|   row.remove(); | ||||
|  | ||||
|   // remove from fileNames | ||||
|   const index = fileNames.indexOf(filename); | ||||
|   fileNames.splice(index, 1); | ||||
|  | ||||
|   // reset fileInput | ||||
|   fileInput.value = ""; | ||||
|  | ||||
|   // if fileNames is empty, reset fileType | ||||
|   if (fileNames.length === 0) { | ||||
|     fileType = null; | ||||
|     fileInput.removeAttribute("accept"); | ||||
|     convertButton.disabled = true; | ||||
|     setTitle(); | ||||
|   } | ||||
|  | ||||
|   fetch(`${webroot}/delete`, { | ||||
|     method: "POST", | ||||
|     body: JSON.stringify({ filename: filename }), | ||||
|     headers: { | ||||
|       "Content-Type": "application/json", | ||||
|     }, | ||||
|   }).catch((err) => console.log(err)); | ||||
| }; | ||||
|  | ||||
| const uploadFile = (file) => { | ||||
|   convertButton.disabled = true; | ||||
|   convertButton.textContent = "Uploading..."; | ||||
|   pendingFiles += 1; | ||||
|  | ||||
|   const formData = new FormData(); | ||||
|   formData.append("file", file, file.name); | ||||
|  | ||||
|   let xhr = new XMLHttpRequest(); | ||||
|  | ||||
|   xhr.open("POST", `${webroot}/upload`, true); | ||||
|  | ||||
|   xhr.onload = () => { | ||||
|     let data = JSON.parse(xhr.responseText); | ||||
|  | ||||
|     pendingFiles -= 1; | ||||
|     if (pendingFiles === 0) { | ||||
|       if (formatSelected) { | ||||
|         convertButton.disabled = false; | ||||
|       } | ||||
|       convertButton.textContent = "Convert"; | ||||
|     } | ||||
|  | ||||
|     //Remove the progress bar when upload is done | ||||
|     let progressbar = file.htmlRow.getElementsByTagName("progress"); | ||||
|     progressbar[0].parentElement.remove(); | ||||
|     console.log(data); | ||||
|   }; | ||||
|  | ||||
|   xhr.upload.onprogress = (e) => { | ||||
|     let sent = e.loaded; | ||||
|     let total = e.total; | ||||
|     console.log(`upload progress (${file.name}):`, (100 * sent) / total); | ||||
|  | ||||
|     let progressbar = file.htmlRow.getElementsByTagName("progress"); | ||||
|     progressbar[0].value = (100 * sent) / total; | ||||
|   }; | ||||
|  | ||||
|   xhr.onerror = (e) => { | ||||
|     console.log(e); | ||||
|   }; | ||||
|  | ||||
|   xhr.send(formData); | ||||
| }; | ||||
|  | ||||
| const formConvert = document.querySelector(`form[action='${webroot}/convert']`); | ||||
|  | ||||
| formConvert.addEventListener("submit", () => { | ||||
|   const hiddenInput = document.querySelector("input[name='file_names']"); | ||||
|   hiddenInput.value = JSON.stringify(fileNames); | ||||
| }); | ||||
|  | ||||
| updateSearchBar(); | ||||
| @@ -1,6 +1,8 @@ | ||||
| { | ||||
|   "$schema": "https://docs.renovatebot.com/renovate-schema.json", | ||||
|   "extends": [ | ||||
|     "config:recommended" | ||||
|   ] | ||||
|   "extends": ["config:recommended", ":disableDependencyDashboard"], | ||||
|   "lockFileMaintenance": { | ||||
|     "enabled": true, | ||||
|     "automerge": true | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										2
									
								
								reset.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1 +1 @@ | ||||
| import "@total-typescript/ts-reset"; | ||||
| import "@total-typescript/ts-reset"; | ||||
|   | ||||
| @@ -1,33 +1,44 @@ | ||||
| import { Html } from "@elysiajs/html"; | ||||
| import { version } from "../../package.json"; | ||||
|  | ||||
| export const BaseHtml = ({ | ||||
|   children, | ||||
|   title = "ConvertX", | ||||
| }: { children: JSX.Element; title?: string }) => ( | ||||
|   webroot = "", | ||||
| }: { | ||||
|   children: JSX.Element; | ||||
|   title?: string; | ||||
|   webroot?: string; | ||||
| }) => ( | ||||
|   <html lang="en"> | ||||
|     <head> | ||||
|       <meta charset="UTF-8" /> | ||||
|       <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||
|       <meta name="webroot" content={webroot} /> | ||||
|       <title safe>{title}</title> | ||||
|       <link rel="stylesheet" href="/pico.lime.min.css" /> | ||||
|       <link rel="stylesheet" href="/style.css" /> | ||||
|       <link | ||||
|         rel="apple-touch-icon" | ||||
|         sizes="180x180" | ||||
|         href="/apple-touch-icon.png" | ||||
|       /> | ||||
|       <link | ||||
|         rel="icon" | ||||
|         type="image/png" | ||||
|         sizes="32x32" | ||||
|         href="/favicon-32x32.png" | ||||
|       /> | ||||
|       <link | ||||
|         rel="icon" | ||||
|         type="image/png" | ||||
|         sizes="16x16" | ||||
|         href="/favicon-16x16.png" | ||||
|       /> | ||||
|       <link rel="manifest" href="/site.webmanifest" /> | ||||
|       <link rel="stylesheet" href={`${webroot}/generated.css`} /> | ||||
|       <link rel="apple-touch-icon" sizes="180x180" href={`${webroot}/apple-touch-icon.png`} /> | ||||
|       <link rel="icon" type="image/png" sizes="32x32" href={`${webroot}/favicon-32x32.png`} /> | ||||
|       <link rel="icon" type="image/png" sizes="16x16" href={`${webroot}/favicon-16x16.png`} /> | ||||
|       <link rel="manifest" href={`${webroot}/site.webmanifest`} /> | ||||
|     </head> | ||||
|     <body>{children}</body> | ||||
|     <body class="flex min-h-screen w-full flex-col bg-neutral-900 text-neutral-200"> | ||||
|       {children} | ||||
|       <footer class="w-full"> | ||||
|         <div class="p-4 text-center text-sm text-neutral-500"> | ||||
|           <span>Powered by </span> | ||||
|           <a | ||||
|             href="https://github.com/C4illin/ConvertX" | ||||
|             class={` | ||||
|               text-neutral-400 | ||||
|               hover:text-accent-500 | ||||
|             `} | ||||
|           > | ||||
|             ConvertX{" "} | ||||
|           </a> | ||||
|           <span safe>v{version || ""}</span> | ||||
|         </div> | ||||
|       </footer> | ||||
|     </body> | ||||
|   </html> | ||||
| ); | ||||
|   | ||||
| @@ -1,48 +1,101 @@ | ||||
| import { Html } from "@kitajs/html"; | ||||
|  | ||||
| export const Header = ({ | ||||
|   loggedIn, | ||||
|   accountRegistration, | ||||
| }: { loggedIn?: boolean; accountRegistration?: boolean }) => { | ||||
|   allowUnauthenticated, | ||||
|   hideHistory, | ||||
|   webroot = "", | ||||
| }: { | ||||
|   loggedIn?: boolean; | ||||
|   accountRegistration?: boolean; | ||||
|   allowUnauthenticated?: boolean; | ||||
|   hideHistory?: boolean; | ||||
|   webroot?: string; | ||||
| }) => { | ||||
|   let rightNav: JSX.Element; | ||||
|   if (loggedIn) { | ||||
|     rightNav = ( | ||||
|       <ul> | ||||
|         <li> | ||||
|           <a href="/history">History</a> | ||||
|         </li> | ||||
|         <li> | ||||
|           <a href="/logoff">Logout</a> | ||||
|         </li> | ||||
|       <ul class="flex gap-4"> | ||||
|         {!hideHistory && ( | ||||
|           <li> | ||||
|             <a | ||||
|               class={` | ||||
|                 text-accent-600 transition-all | ||||
|                 hover:text-accent-500 hover:underline | ||||
|               `} | ||||
|               href={`${webroot}/history`} | ||||
|             > | ||||
|               History | ||||
|             </a> | ||||
|           </li> | ||||
|         )} | ||||
|         {!allowUnauthenticated ? ( | ||||
|           <li> | ||||
|             <a | ||||
|               class={` | ||||
|                 text-accent-600 transition-all | ||||
|                 hover:text-accent-500 hover:underline | ||||
|               `} | ||||
|               href={`${webroot}/account`} | ||||
|             > | ||||
|               Account | ||||
|             </a> | ||||
|           </li> | ||||
|         ) : null} | ||||
|         {!allowUnauthenticated ? ( | ||||
|           <li> | ||||
|             <a | ||||
|               class={` | ||||
|                 text-accent-600 transition-all | ||||
|                 hover:text-accent-500 hover:underline | ||||
|               `} | ||||
|               href={`${webroot}/logoff`} | ||||
|             > | ||||
|               Logout | ||||
|             </a> | ||||
|           </li> | ||||
|         ) : null} | ||||
|       </ul> | ||||
|     ); | ||||
|   } else { | ||||
|     rightNav = ( | ||||
|       <ul> | ||||
|       <ul class="flex gap-4"> | ||||
|         <li> | ||||
|           <a href="/login">Login</a> | ||||
|           <a | ||||
|             class={` | ||||
|               text-accent-600 transition-all | ||||
|               hover:text-accent-500 hover:underline | ||||
|             `} | ||||
|             href={`${webroot}/login`} | ||||
|           > | ||||
|             Login | ||||
|           </a> | ||||
|         </li> | ||||
|         {accountRegistration && ( | ||||
|         {accountRegistration ? ( | ||||
|           <li> | ||||
|             <a href="/register">Register</a> | ||||
|             <a | ||||
|               class={` | ||||
|                 text-accent-600 transition-all | ||||
|                 hover:text-accent-500 hover:underline | ||||
|               `} | ||||
|               href={`${webroot}/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-sm bg-neutral-900 p-4"> | ||||
|         <ul> | ||||
|           <li> | ||||
|             <strong> | ||||
|               <a | ||||
|                 href="/" | ||||
|                 style={{ | ||||
|                   textDecoration: "none", | ||||
|                   color: "inherit", | ||||
|                 }}> | ||||
|                 ConvertX | ||||
|               </a> | ||||
|               <a href={`${webroot}/`}>ConvertX</a> | ||||
|             </strong> | ||||
|           </li> | ||||
|         </ul> | ||||
|   | ||||
							
								
								
									
										139
									
								
								src/converters/assimp.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,139 @@ | ||||
| import { execFile } from "node:child_process"; | ||||
|  | ||||
| export const properties = { | ||||
|   from: { | ||||
|     object: [ | ||||
|       "3d", | ||||
|       "3ds", | ||||
|       "3mf", | ||||
|       "ac", | ||||
|       "ac3d", | ||||
|       "acc", | ||||
|       "amf", | ||||
|       "amj", | ||||
|       "ase", | ||||
|       "ask", | ||||
|       "assbin", | ||||
|       "b3d", | ||||
|       "blend", | ||||
|       "bsp", | ||||
|       "bvh", | ||||
|       "cob", | ||||
|       "csm", | ||||
|       "dae", | ||||
|       "dxf", | ||||
|       "enff", | ||||
|       "fbx", | ||||
|       "glb", | ||||
|       "gltf", | ||||
|       "hmb", | ||||
|       "hmp", | ||||
|       "ifc", | ||||
|       "ifczip", | ||||
|       "iqm", | ||||
|       "irr", | ||||
|       "irrmesh", | ||||
|       "lwo", | ||||
|       "lws", | ||||
|       "lxo", | ||||
|       "m3d", | ||||
|       "md2", | ||||
|       "md3", | ||||
|       "md5anim", | ||||
|       "md5camera", | ||||
|       "md5mesh", | ||||
|       "mdc", | ||||
|       "mdl", | ||||
|       "mesh.xml", | ||||
|       "mesh", | ||||
|       "mot", | ||||
|       "ms3d", | ||||
|       "ndo", | ||||
|       "nff", | ||||
|       "obj", | ||||
|       "off", | ||||
|       "ogex", | ||||
|       "pk3", | ||||
|       "ply", | ||||
|       "pmx", | ||||
|       "prj", | ||||
|       "q3o", | ||||
|       "q3s", | ||||
|       "raw", | ||||
|       "scn", | ||||
|       "sib", | ||||
|       "smd", | ||||
|       "step", | ||||
|       "stl", | ||||
|       "stp", | ||||
|       "ter", | ||||
|       "uc", | ||||
|       "usd", | ||||
|       "usda", | ||||
|       "usdc", | ||||
|       "usdz", | ||||
|       "vta", | ||||
|       "x", | ||||
|       "x3d", | ||||
|       "x3db", | ||||
|       "xgl", | ||||
|       "xml", | ||||
|       "zae", | ||||
|       "zgl", | ||||
|     ], | ||||
|   }, | ||||
|   to: { | ||||
|     object: [ | ||||
|       "3ds", | ||||
|       "3mf", | ||||
|       "assbin", | ||||
|       "assjson", | ||||
|       "assxml", | ||||
|       "collada", | ||||
|       "dae", | ||||
|       "fbx", | ||||
|       "fbxa", | ||||
|       "glb", | ||||
|       "glb2", | ||||
|       "gltf", | ||||
|       "gltf2", | ||||
|       "json", | ||||
|       "obj", | ||||
|       "objnomtl", | ||||
|       "pbrt", | ||||
|       "ply", | ||||
|       "plyb", | ||||
|       "stl", | ||||
|       "stlb", | ||||
|       "stp", | ||||
|       "x", | ||||
|     ], | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| export async function convert( | ||||
|   filePath: string, | ||||
|   fileType: string, | ||||
|   convertTo: string, | ||||
|   targetPath: string, | ||||
|   // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||||
|   options?: unknown, | ||||
| ): Promise<string> { | ||||
|   return new Promise((resolve, reject) => { | ||||
|     execFile("assimp", ["export", filePath, targetPath], (error, stdout, stderr) => { | ||||
|       if (error) { | ||||
|         reject(`error: ${error}`); | ||||
|       } | ||||
|  | ||||
|       if (stdout) { | ||||
|         console.log(`stdout: ${stdout}`); | ||||
|       } | ||||
|  | ||||
|       if (stderr) { | ||||
|         console.error(`stderr: ${stderr}`); | ||||
|       } | ||||
|  | ||||
|       resolve("Done"); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
							
								
								
									
										84
									
								
								src/converters/calibre.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,84 @@ | ||||
| import { execFile } from "node:child_process"; | ||||
|  | ||||
| export const properties = { | ||||
|   from: { | ||||
|     document: [ | ||||
|       "azw4", | ||||
|       "chm", | ||||
|       "cbr", | ||||
|       "cbz", | ||||
|       "cbt", | ||||
|       "cba", | ||||
|       "cb7", | ||||
|       "djvu", | ||||
|       "docx", | ||||
|       "epub", | ||||
|       "fb2", | ||||
|       "htlz", | ||||
|       "html", | ||||
|       "lit", | ||||
|       "lrf", | ||||
|       "mobi", | ||||
|       "odt", | ||||
|       "pdb", | ||||
|       "pdf", | ||||
|       "pml", | ||||
|       "rb", | ||||
|       "rtf", | ||||
|       "recipe", | ||||
|       "snb", | ||||
|       "tcr", | ||||
|       "txt", | ||||
|     ], | ||||
|   }, | ||||
|   to: { | ||||
|     document: [ | ||||
|       "azw3", | ||||
|       "docx", | ||||
|       "epub", | ||||
|       "fb2", | ||||
|       "html", | ||||
|       "htmlz", | ||||
|       "lit", | ||||
|       "lrf", | ||||
|       "mobi", | ||||
|       "oeb", | ||||
|       "pdb", | ||||
|       "pdf", | ||||
|       "pml", | ||||
|       "rb", | ||||
|       "rtf", | ||||
|       "snb", | ||||
|       "tcr", | ||||
|       "txt", | ||||
|       "txtz", | ||||
|     ], | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| export async function convert( | ||||
|   filePath: string, | ||||
|   fileType: string, | ||||
|   convertTo: string, | ||||
|   targetPath: string, | ||||
|   // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||||
|   options?: unknown, | ||||
| ): Promise<string> { | ||||
|   return new Promise((resolve, reject) => { | ||||
|     execFile("ebook-convert", [filePath, targetPath], (error, stdout, stderr) => { | ||||
|       if (error) { | ||||
|         reject(`error: ${error}`); | ||||
|       } | ||||
|  | ||||
|       if (stdout) { | ||||
|         console.log(`stdout: ${stdout}`); | ||||
|       } | ||||
|  | ||||
|       if (stderr) { | ||||
|         console.error(`stderr: ${stderr}`); | ||||
|       } | ||||
|  | ||||
|       resolve("Done"); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
							
								
								
									
										48
									
								
								src/converters/dvisvgm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,48 @@ | ||||
| import { execFile } from "node:child_process"; | ||||
|  | ||||
| export const properties = { | ||||
|   from: { | ||||
|     images: ["dvi", "xdv", "pdf", "eps"], | ||||
|   }, | ||||
|   to: { | ||||
|     images: ["svg", "svgz"], | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| export function convert( | ||||
|   filePath: string, | ||||
|   fileType: string, | ||||
|   convertTo: string, | ||||
|   targetPath: string, | ||||
|   // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||||
|   options?: unknown, | ||||
| ): Promise<string> { | ||||
|   const inputArgs: string[] = []; | ||||
|   if (fileType === "eps") { | ||||
|     inputArgs.push("--eps"); | ||||
|   } | ||||
|   if (fileType === "pdf") { | ||||
|     inputArgs.push("--pdf"); | ||||
|   } | ||||
|   if (convertTo === "svgz") { | ||||
|     inputArgs.push("-z"); | ||||
|   } | ||||
|  | ||||
|   return new Promise((resolve, reject) => { | ||||
|     execFile("dvisvgm", [...inputArgs, filePath, "-o", targetPath], (error, stdout, stderr) => { | ||||
|       if (error) { | ||||
|         reject(`error: ${error}`); | ||||
|       } | ||||
|  | ||||
|       if (stdout) { | ||||
|         console.log(`stdout: ${stdout}`); | ||||
|       } | ||||
|  | ||||
|       if (stderr) { | ||||
|         console.error(`stderr: ${stderr}`); | ||||
|       } | ||||
|  | ||||
|       resolve("Done"); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { exec } from "node:child_process"; | ||||
| import { execFile } from "node:child_process"; | ||||
|  | ||||
| // This could be done dynamically by running `ffmpeg -formats` and parsing the output | ||||
| export const properties = { | ||||
| @@ -6,6 +6,7 @@ export const properties = { | ||||
|     muxer: [ | ||||
|       "264", | ||||
|       "265", | ||||
|       "266", | ||||
|       "302", | ||||
|       "3dostr", | ||||
|       "3g2", | ||||
| @@ -18,6 +19,7 @@ export const properties = { | ||||
|       "aac", | ||||
|       "aax", | ||||
|       "ac3", | ||||
|       "ac4", | ||||
|       "ace", | ||||
|       "acm", | ||||
|       "act", | ||||
| @@ -48,7 +50,6 @@ export const properties = { | ||||
|       "apng", | ||||
|       "aptx", | ||||
|       "aptxhd", | ||||
|       "aptx_hd", | ||||
|       "aqt", | ||||
|       "aqtitle", | ||||
|       "argo_asf", | ||||
| @@ -63,10 +64,12 @@ export const properties = { | ||||
|       "av1", | ||||
|       "avc", | ||||
|       "avi", | ||||
|       "avif", | ||||
|       "avr", | ||||
|       "avs", | ||||
|       "avs2", | ||||
|       "avs3", | ||||
|       "awb", | ||||
|       "bcstm", | ||||
|       "bethsoftvid", | ||||
|       "bfi", | ||||
| @@ -75,8 +78,10 @@ export const properties = { | ||||
|       "bink", | ||||
|       "binka", | ||||
|       "bit", | ||||
|       "bmp_pipe", | ||||
|       "bitpacked", | ||||
|       "bmv", | ||||
|       "bmp", | ||||
|       "bonk", | ||||
|       "boa", | ||||
|       "brender_pix", | ||||
|       "brstm", | ||||
| @@ -93,7 +98,7 @@ export const properties = { | ||||
|       "codec2", | ||||
|       "codec2raw", | ||||
|       "concat", | ||||
|       "cri_pipe", | ||||
|       "cri", | ||||
|       "dash", | ||||
|       "dat", | ||||
|       "data", | ||||
| @@ -101,8 +106,9 @@ export const properties = { | ||||
|       "dav", | ||||
|       "dbm", | ||||
|       "dcstr", | ||||
|       "dds_pipe", | ||||
|       "dds", | ||||
|       "derf", | ||||
|       "dfpwm", | ||||
|       "dfa", | ||||
|       "dhav", | ||||
|       "dif", | ||||
| @@ -131,6 +137,8 @@ export const properties = { | ||||
|       "exr_pipe", | ||||
|       "f32be", | ||||
|       "f32le", | ||||
|       "ec3", | ||||
|       "evc", | ||||
|       "f4v", | ||||
|       "f64be", | ||||
|       "f64le", | ||||
| @@ -157,13 +165,13 @@ export const properties = { | ||||
|       "gdv", | ||||
|       "genh", | ||||
|       "gif", | ||||
|       "gif_pipe", | ||||
|       "gsm", | ||||
|       "gxf", | ||||
|       "h261", | ||||
|       "h263", | ||||
|       "h264", | ||||
|       "h265", | ||||
|       "h266", | ||||
|       "h26l", | ||||
|       "hca", | ||||
|       "hcom", | ||||
| @@ -180,7 +188,6 @@ export const properties = { | ||||
|       "ifv", | ||||
|       "ilbc", | ||||
|       "image2", | ||||
|       "image2pipe", | ||||
|       "imf", | ||||
|       "imx", | ||||
|       "ingenient", | ||||
| @@ -197,21 +204,17 @@ export const properties = { | ||||
|       "ivr", | ||||
|       "j2b", | ||||
|       "j2k", | ||||
|       "j2k_pipe", | ||||
|       "jack", | ||||
|       "jacosub", | ||||
|       "jpegls_pipe", | ||||
|       "jpeg_pipe", | ||||
|       "jv", | ||||
|       "jpegls", | ||||
|       "jpeg", | ||||
|       "jxl", | ||||
|       "kmsgrab", | ||||
|       "kux", | ||||
|       "kvag", | ||||
|       "lavfi", | ||||
|       "libcdio", | ||||
|       "libdc1394", | ||||
|       "libgme", | ||||
|       "libopenmpt", | ||||
|       "live_flv", | ||||
|       "laf", | ||||
|       "lmlm4", | ||||
|       "loas", | ||||
|       "lrc", | ||||
| @@ -224,16 +227,13 @@ export const properties = { | ||||
|       "m4b", | ||||
|       "m4v", | ||||
|       "mac", | ||||
|       "matroska", | ||||
|       "mca", | ||||
|       "mcc", | ||||
|       "mdl", | ||||
|       "med", | ||||
|       "mgsts", | ||||
|       "microdvd", | ||||
|       "mj2", | ||||
|       "mjpeg", | ||||
|       "mjpeg_2000", | ||||
|       "mjpg", | ||||
|       "mk3d", | ||||
|       "mka", | ||||
| @@ -257,9 +257,6 @@ export const properties = { | ||||
|       "mpc", | ||||
|       "mpc8", | ||||
|       "mpeg", | ||||
|       "mpegts", | ||||
|       "mpegtsraw", | ||||
|       "mpegvideo", | ||||
|       "mpg", | ||||
|       "mpjpeg", | ||||
|       "mpl2", | ||||
| @@ -294,25 +291,27 @@ export const properties = { | ||||
|       "okt", | ||||
|       "oma", | ||||
|       "omg", | ||||
|       "opus", | ||||
|       "openal", | ||||
|       "oss", | ||||
|       "osq", | ||||
|       "paf", | ||||
|       "pam_pipe", | ||||
|       "pbm_pipe", | ||||
|       "pcx_pipe", | ||||
|       "pgmyuv_pipe", | ||||
|       "pgm_pipe", | ||||
|       "pgx_pipe", | ||||
|       "photocd_pipe", | ||||
|       "pictor_pipe", | ||||
|       "pdv", | ||||
|       "pam", | ||||
|       "pbm", | ||||
|       "pcx", | ||||
|       "pgmyuv", | ||||
|       "pgm", | ||||
|       "pgx", | ||||
|       "photocd", | ||||
|       "pictor", | ||||
|       "pjs", | ||||
|       "plm", | ||||
|       "pmp", | ||||
|       "png_pipe", | ||||
|       "png", | ||||
|       "ppm", | ||||
|       "ppm_pipe", | ||||
|       "pp_bnk", | ||||
|       "psd_pipe", | ||||
|       "pp", | ||||
|       "psd", | ||||
|       "psm", | ||||
|       "psp", | ||||
|       "psxstr", | ||||
| @@ -323,7 +322,7 @@ export const properties = { | ||||
|       "pvf", | ||||
|       "qcif", | ||||
|       "qcp", | ||||
|       "qdraw_pipe", | ||||
|       "qdraw", | ||||
|       "r3d", | ||||
|       "rawvideo", | ||||
|       "rco", | ||||
| @@ -335,6 +334,7 @@ export const properties = { | ||||
|       "rm", | ||||
|       "roq", | ||||
|       "rpl", | ||||
|       "rka", | ||||
|       "rsd", | ||||
|       "rso", | ||||
|       "rt", | ||||
| @@ -355,6 +355,7 @@ export const properties = { | ||||
|       "sbc", | ||||
|       "sbg", | ||||
|       "scc", | ||||
|       "sdns", | ||||
|       "sdp", | ||||
|       "sdr2", | ||||
|       "sds", | ||||
| @@ -364,10 +365,9 @@ export const properties = { | ||||
|       "sfx", | ||||
|       "sfx2", | ||||
|       "sga", | ||||
|       "sgi_pipe", | ||||
|       "sgi", | ||||
|       "shn", | ||||
|       "siff", | ||||
|       "simbiosis_imx", | ||||
|       "sln", | ||||
|       "smi", | ||||
|       "smjpeg", | ||||
| @@ -389,12 +389,9 @@ export const properties = { | ||||
|       "stp", | ||||
|       "str", | ||||
|       "sub", | ||||
|       "subviewer", | ||||
|       "subviewer1", | ||||
|       "sunrast_pipe", | ||||
|       "sup", | ||||
|       "svag", | ||||
|       "svg_pipe", | ||||
|       "svg", | ||||
|       "svs", | ||||
|       "sw", | ||||
|       "swf", | ||||
| @@ -404,7 +401,8 @@ export const properties = { | ||||
|       "thd", | ||||
|       "thp", | ||||
|       "tiertexseq", | ||||
|       "tiff_pipe", | ||||
|       "tif", | ||||
|       "tiff", | ||||
|       "tmv", | ||||
|       "truehd", | ||||
|       "tta", | ||||
| @@ -424,6 +422,7 @@ export const properties = { | ||||
|       "ul", | ||||
|       "ult", | ||||
|       "umx", | ||||
|       "usm", | ||||
|       "uw", | ||||
|       "v", | ||||
|       "v210", | ||||
| @@ -447,12 +446,14 @@ export const properties = { | ||||
|       "vql", | ||||
|       "vt", | ||||
|       "vtt", | ||||
|       "vvc", | ||||
|       "w64", | ||||
|       "wa", | ||||
|       "wav", | ||||
|       "way", | ||||
|       "wc3movie", | ||||
|       "webm", | ||||
|       "webm_dash_manifest", | ||||
|       "webp_pipe", | ||||
|       "webp", | ||||
|       "webvtt", | ||||
|       "wow", | ||||
|       "wsaud", | ||||
| @@ -464,32 +465,31 @@ export const properties = { | ||||
|       "x11grab", | ||||
|       "xa", | ||||
|       "xbin", | ||||
|       "xbm_pipe", | ||||
|       "xl", | ||||
|       "xm", | ||||
|       "xmd", | ||||
|       "xmv", | ||||
|       "xpk", | ||||
|       "xpm_pipe", | ||||
|       "xvag", | ||||
|       "xwd_pipe", | ||||
|       "xwma", | ||||
|       "y4m", | ||||
|       "yop", | ||||
|       "yuv", | ||||
|       "yuv10", | ||||
|       "yuv4mpegpipe", | ||||
|     ], | ||||
|   }, | ||||
|   to: { | ||||
|     muxer: [ | ||||
|       "264", | ||||
|       "265", | ||||
|       "266", | ||||
|       "302", | ||||
|       "3g2", | ||||
|       "3gp", | ||||
|       "a64", | ||||
|       "aac", | ||||
|       "ac3", | ||||
|       "ac4", | ||||
|       "adts", | ||||
|       "adx", | ||||
|       "afc", | ||||
| @@ -497,43 +497,33 @@ export const properties = { | ||||
|       "aifc", | ||||
|       "aiff", | ||||
|       "al", | ||||
|       "alaw", | ||||
|       "alp", | ||||
|       "alsa", | ||||
|       "amr", | ||||
|       "amv", | ||||
|       "apm", | ||||
|       "apng", | ||||
|       "aptx", | ||||
|       "aptxhd", | ||||
|       "aptx_hd", | ||||
|       "argo_asf", | ||||
|       "asf", | ||||
|       "asf_stream", | ||||
|       "ass", | ||||
|       "ast", | ||||
|       "au", | ||||
|       "aud", | ||||
|       "av1.mkv", | ||||
|       "av1.mp4", | ||||
|       "avi", | ||||
|       "avm2", | ||||
|       "avif", | ||||
|       "avs", | ||||
|       "avs2", | ||||
|       "avs3", | ||||
|       "bit", | ||||
|       "bmp", | ||||
|       "c2", | ||||
|       "caca", | ||||
|       "caf", | ||||
|       "cavs", | ||||
|       "cavsvideo", | ||||
|       "chk", | ||||
|       "chromaprint", | ||||
|       "codec2", | ||||
|       "codec2raw", | ||||
|       "cpk", | ||||
|       "crc", | ||||
|       "dash", | ||||
|       "data", | ||||
|       "daud", | ||||
|       "dirac", | ||||
|       "cvg", | ||||
|       "dfpwm", | ||||
|       "dnxhd", | ||||
|       "dnxhr", | ||||
|       "dpx", | ||||
| @@ -542,63 +532,45 @@ export const properties = { | ||||
|       "dv", | ||||
|       "dvd", | ||||
|       "eac3", | ||||
|       "ec3", | ||||
|       "evc", | ||||
|       "exr", | ||||
|       "f32be", | ||||
|       "f32le", | ||||
|       "f4v", | ||||
|       "f64be", | ||||
|       "f64le", | ||||
|       "fbdev", | ||||
|       "ffmeta", | ||||
|       "ffmetadata", | ||||
|       "fifo", | ||||
|       "fifo_test", | ||||
|       "filmstrip", | ||||
|       "film_cpk", | ||||
|       "fits", | ||||
|       "flac", | ||||
|       "flm", | ||||
|       "flv", | ||||
|       "framecrc", | ||||
|       "framehash", | ||||
|       "framemd5", | ||||
|       "g722", | ||||
|       "g723_1", | ||||
|       "g726", | ||||
|       "g726le", | ||||
|       "gif", | ||||
|       "gsm", | ||||
|       "gxf", | ||||
|       "h261", | ||||
|       "h263", | ||||
|       "h264", | ||||
|       "h265", | ||||
|       "hash", | ||||
|       "hds", | ||||
|       "h264.mkv", | ||||
|       "h264.mp4", | ||||
|       "h265.mkv", | ||||
|       "h265.mp4", | ||||
|       "h266.mkv", | ||||
|       "hdr", | ||||
|       "hevc", | ||||
|       "hls", | ||||
|       "ico", | ||||
|       "ilbc", | ||||
|       "im1", | ||||
|       "im24", | ||||
|       "im8", | ||||
|       "image2", | ||||
|       "image2pipe", | ||||
|       "ipod", | ||||
|       "ircam", | ||||
|       "isma", | ||||
|       "ismv", | ||||
|       "ivf", | ||||
|       "j2c", | ||||
|       "j2k", | ||||
|       "jacosub", | ||||
|       "jls", | ||||
|       "jp2", | ||||
|       "jpeg", | ||||
|       "jpg", | ||||
|       "js", | ||||
|       "jss", | ||||
|       "kvag", | ||||
|       "jxl", | ||||
|       "latm", | ||||
|       "lbc", | ||||
|       "ljpg", | ||||
| @@ -613,13 +585,9 @@ export const properties = { | ||||
|       "m4a", | ||||
|       "m4b", | ||||
|       "m4v", | ||||
|       "matroska", | ||||
|       "md5", | ||||
|       "microdvd", | ||||
|       "mjpeg", | ||||
|       "mjpg", | ||||
|       "mkv", | ||||
|       "mkvtimestamp_v2", | ||||
|       "mlp", | ||||
|       "mmf", | ||||
|       "mov", | ||||
| @@ -629,26 +597,17 @@ export const properties = { | ||||
|       "mpa", | ||||
|       "mpd", | ||||
|       "mpeg", | ||||
|       "mpeg1video", | ||||
|       "mpeg2video", | ||||
|       "mpegts", | ||||
|       "mpg", | ||||
|       "mpjpeg", | ||||
|       "msbc", | ||||
|       "mts", | ||||
|       "mulaw", | ||||
|       "mxf", | ||||
|       "mxf_d10", | ||||
|       "mxf_opatom", | ||||
|       "null", | ||||
|       "nut", | ||||
|       "obu", | ||||
|       "oga", | ||||
|       "ogg", | ||||
|       "ogv", | ||||
|       "oma", | ||||
|       "opengl", | ||||
|       "opus", | ||||
|       "oss", | ||||
|       "pam", | ||||
|       "pbm", | ||||
|       "pcm", | ||||
| @@ -656,14 +615,14 @@ export const properties = { | ||||
|       "pfm", | ||||
|       "pgm", | ||||
|       "pgmyuv", | ||||
|       "phm", | ||||
|       "pix", | ||||
|       "png", | ||||
|       "ppm", | ||||
|       "psp", | ||||
|       "pulse", | ||||
|       "qoi", | ||||
|       "ra", | ||||
|       "ras", | ||||
|       "rawvideo", | ||||
|       "rco", | ||||
|       "rcv", | ||||
|       "rgb", | ||||
| @@ -671,84 +630,47 @@ export const properties = { | ||||
|       "roq", | ||||
|       "rs", | ||||
|       "rso", | ||||
|       "rtp", | ||||
|       "rtp_mpegts", | ||||
|       "rtsp", | ||||
|       "s16be", | ||||
|       "s16le", | ||||
|       "s24be", | ||||
|       "s24le", | ||||
|       "s32be", | ||||
|       "s32le", | ||||
|       "s8", | ||||
|       "sap", | ||||
|       "sb", | ||||
|       "sbc", | ||||
|       "scc", | ||||
|       "sdl", | ||||
|       "sdl2", | ||||
|       "segment", | ||||
|       "sf", | ||||
|       "sgi", | ||||
|       "singlejpeg", | ||||
|       "smjpeg", | ||||
|       "smoothstreaming", | ||||
|       "sndio", | ||||
|       "sox", | ||||
|       "spdif", | ||||
|       "spx", | ||||
|       "srt", | ||||
|       "ssa", | ||||
|       "ssegment", | ||||
|       "streamhash", | ||||
|       "stream_segment", | ||||
|       "sub", | ||||
|       "sun", | ||||
|       "sunras", | ||||
|       "sup", | ||||
|       "svcd", | ||||
|       "sw", | ||||
|       "swf", | ||||
|       "tco", | ||||
|       "tee", | ||||
|       "tga", | ||||
|       "thd", | ||||
|       "tif", | ||||
|       "tiff", | ||||
|       "truehd", | ||||
|       "ts", | ||||
|       "tta", | ||||
|       "ttml", | ||||
|       "tun", | ||||
|       "u16be", | ||||
|       "u16le", | ||||
|       "u24be", | ||||
|       "u24le", | ||||
|       "u32be", | ||||
|       "u32le", | ||||
|       "u8", | ||||
|       "ub", | ||||
|       "ul", | ||||
|       "uncodedframecrc", | ||||
|       "uw", | ||||
|       "v4l2", | ||||
|       "vag", | ||||
|       "vbn", | ||||
|       "vc1", | ||||
|       "vc1test", | ||||
|       "vc2", | ||||
|       "vcd", | ||||
|       "vidc", | ||||
|       "video4linux2", | ||||
|       "vob", | ||||
|       "voc", | ||||
|       "vtt", | ||||
|       "vvc", | ||||
|       "w64", | ||||
|       "wav", | ||||
|       "wbmp", | ||||
|       "webm", | ||||
|       "webm_chunk", | ||||
|       "webm_dash_manifest", | ||||
|       "webp", | ||||
|       "webvtt", | ||||
|       "wma", | ||||
|       "wmv", | ||||
|       "wtv", | ||||
| @@ -756,12 +678,10 @@ export const properties = { | ||||
|       "xbm", | ||||
|       "xface", | ||||
|       "xml", | ||||
|       "xv", | ||||
|       "xwd", | ||||
|       "y", | ||||
|       "y4m", | ||||
|       "yuv", | ||||
|       "yuv4mpegpipe", | ||||
|     ], | ||||
|   }, | ||||
| }; | ||||
| @@ -771,58 +691,64 @@ export async function convert( | ||||
|   fileType: string, | ||||
|   convertTo: string, | ||||
|   targetPath: string, | ||||
|   // biome-ignore lint/suspicious/noExplicitAny: <explanation> | ||||
|   options?: any, | ||||
|   // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||||
|   options?: unknown, | ||||
| ): Promise<string> { | ||||
|   // let command = "ffmpeg"; | ||||
|   let extraArgs: string[] = []; | ||||
|   let message = "Done"; | ||||
|  | ||||
|   // these are containers that can contain multiple formats | ||||
|   // const autoDetect = [ | ||||
|   //   "mp4", | ||||
|   //   "mkv", | ||||
|   //   "avi", | ||||
|   //   "mov", | ||||
|   //   "m4a", | ||||
|   //   "3gp", | ||||
|   //   "3g2", | ||||
|   //   "mj2", | ||||
|   //   "psp", | ||||
|   //   "m4b", | ||||
|   //   "ism", | ||||
|   //   "ismv", | ||||
|   //   "isma", | ||||
|   //   "f4v", | ||||
|   // ]; | ||||
|   if (convertTo === "ico") { | ||||
|     // make sure image is 256x256 or smaller | ||||
|     extraArgs = [ | ||||
|       "-filter:v", | ||||
|       "scale='min(256,iw)':min'(256,ih)':force_original_aspect_ratio=decrease", | ||||
|     ]; | ||||
|     message = "Done: resized to 256x256"; | ||||
|   } | ||||
|  | ||||
|   // if (!(fileType in autoDetect)) { | ||||
|   //   command += ` -f "${fileType}"`; | ||||
|   // } | ||||
|   if (convertTo.split(".").length > 1) { | ||||
|     // support av1.mkv and av1.mp4 and h265.mp4 etc. | ||||
|     const split = convertTo.split("."); | ||||
|     const codec_short = split[0]; | ||||
|  | ||||
|   // command += ` -i "${filePath}"`; | ||||
|     switch (codec_short) { | ||||
|       case "av1": | ||||
|         extraArgs.push("-c:v", "libaom-av1"); | ||||
|         break; | ||||
|       case "h264": | ||||
|         extraArgs.push("-c:v", "libx264"); | ||||
|         break; | ||||
|       case "h265": | ||||
|         extraArgs.push("-c:v", "libx265"); | ||||
|         break; | ||||
|       case "h266": | ||||
|         extraArgs.push("-c:v", "libx266"); | ||||
|         break; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // if (!(convertTo in autoDetect)) { | ||||
|   //   command += ` -f "${convertTo}"`; | ||||
|   // } | ||||
|  | ||||
|   // command += ` "${targetPath}"`; | ||||
|  | ||||
|   const command = `ffmpeg -i "${filePath}" "${targetPath}"`; | ||||
|   // Parse FFMPEG_ARGS environment variable into array | ||||
|   const ffmpegArgs = process.env.FFMPEG_ARGS ? process.env.FFMPEG_ARGS.split(/\s+/) : []; | ||||
|  | ||||
|   return new Promise((resolve, reject) => { | ||||
|     exec(command, (error, stdout, stderr) => { | ||||
|       if (error) { | ||||
|         reject(`error: ${error}`); | ||||
|       } | ||||
|     execFile( | ||||
|       "ffmpeg", | ||||
|       [...ffmpegArgs, "-i", filePath, ...extraArgs, 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(message); | ||||
|       }, | ||||
|     ); | ||||
|   }); | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { exec } from "node:child_process"; | ||||
| import { execFile } from "node:child_process"; | ||||
|  | ||||
| export const properties = { | ||||
|   from: { | ||||
| @@ -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", | ||||
| @@ -313,27 +313,24 @@ export function convert( | ||||
|   fileType: string, | ||||
|   convertTo: string, | ||||
|   targetPath: string, | ||||
|   // biome-ignore lint/suspicious/noExplicitAny: <explanation> | ||||
|   options?: any, | ||||
|   // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||||
|   options?: unknown, | ||||
| ): Promise<string> { | ||||
|   return new Promise((resolve, reject) => { | ||||
|     exec( | ||||
|       `gm convert "${filePath}" "${targetPath}"`, | ||||
|       (error, stdout, stderr) => { | ||||
|         if (error) { | ||||
|           reject(`error: ${error}`); | ||||
|         } | ||||
|     execFile("gm", ["convert", 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("Done"); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
|   | ||||
							
								
								
									
										484
									
								
								src/converters/imagemagick.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,484 @@ | ||||
| import { execFile } from "node:child_process"; | ||||
|  | ||||
| // declare possible conversions | ||||
| export const properties = { | ||||
|   from: { | ||||
|     images: [ | ||||
|       "3fr", | ||||
|       "3g2", | ||||
|       "3gp", | ||||
|       "aai", | ||||
|       "ai", | ||||
|       "apng", | ||||
|       "art", | ||||
|       "arw", | ||||
|       "avci", | ||||
|       "avi", | ||||
|       "avif", | ||||
|       "avs", | ||||
|       "bayer", | ||||
|       "bayera", | ||||
|       "bgr", | ||||
|       "bgra", | ||||
|       "bgro", | ||||
|       "bmp", | ||||
|       "bmp2", | ||||
|       "bmp3", | ||||
|       "cal", | ||||
|       "cals", | ||||
|       "canvas", | ||||
|       "caption", | ||||
|       "cin", | ||||
|       "clip", | ||||
|       "clipboard", | ||||
|       "cmyk", | ||||
|       "cmyka", | ||||
|       "cr2", | ||||
|       "cr3", | ||||
|       "crw", | ||||
|       "cube", | ||||
|       "cur", | ||||
|       "cut", | ||||
|       "data", | ||||
|       "dcm", | ||||
|       "dcr", | ||||
|       "dcraw", | ||||
|       "dcx", | ||||
|       "dds", | ||||
|       "dfont", | ||||
|       "dng", | ||||
|       "dpx", | ||||
|       "dxt1", | ||||
|       "dxt5", | ||||
|       "emf", | ||||
|       "epdf", | ||||
|       "epi", | ||||
|       "eps", | ||||
|       "epsf", | ||||
|       "epsi", | ||||
|       "ept", | ||||
|       "ept2", | ||||
|       "ept3", | ||||
|       "erf", | ||||
|       "exr", | ||||
|       "farbfeld", | ||||
|       "fax", | ||||
|       "ff", | ||||
|       "fff", | ||||
|       "file", | ||||
|       "fits", | ||||
|       "fl32", | ||||
|       "flif", | ||||
|       "flv", | ||||
|       "fractal", | ||||
|       "ftp", | ||||
|       "fts", | ||||
|       "ftxt", | ||||
|       "g3", | ||||
|       "g4", | ||||
|       "gif", | ||||
|       "gif87", | ||||
|       "gradient", | ||||
|       "gray", | ||||
|       "graya", | ||||
|       "group4", | ||||
|       "hald", | ||||
|       "hdr", | ||||
|       "heic", | ||||
|       "heif", | ||||
|       "hrz", | ||||
|       "http", | ||||
|       "https", | ||||
|       "icb", | ||||
|       "ico", | ||||
|       "icon", | ||||
|       "iiq", | ||||
|       "inline", | ||||
|       "ipl", | ||||
|       "j2c", | ||||
|       "j2k", | ||||
|       "jng", | ||||
|       "jnx", | ||||
|       "jp2", | ||||
|       "jpc", | ||||
|       "jpe", | ||||
|       "jpeg", | ||||
|       "jpg", | ||||
|       "jpm", | ||||
|       "jps", | ||||
|       "jpt", | ||||
|       "jxl", | ||||
|       "k25", | ||||
|       "kdc", | ||||
|       "label", | ||||
|       "m2v", | ||||
|       "m4v", | ||||
|       "mac", | ||||
|       "map", | ||||
|       "mask", | ||||
|       "mat", | ||||
|       "mdc", | ||||
|       "mef", | ||||
|       "miff", | ||||
|       "mkv", | ||||
|       "mng", | ||||
|       "mono", | ||||
|       "mos", | ||||
|       "mov", | ||||
|       "mp4", | ||||
|       "mpc", | ||||
|       "mpeg", | ||||
|       "mpg", | ||||
|       "mpo", | ||||
|       "mrw", | ||||
|       "msl", | ||||
|       "msvg", | ||||
|       "mtv", | ||||
|       "mvg", | ||||
|       "nef", | ||||
|       "nrw", | ||||
|       "null", | ||||
|       "ora", | ||||
|       "orf", | ||||
|       "otb", | ||||
|       "otf", | ||||
|       "pal", | ||||
|       "palm", | ||||
|       "pam", | ||||
|       "pango", | ||||
|       "pattern", | ||||
|       "pbm", | ||||
|       "pcd", | ||||
|       "pcds", | ||||
|       "pcl", | ||||
|       "pct", | ||||
|       "pcx", | ||||
|       "pdb", | ||||
|       "pdf", | ||||
|       "pdfa", | ||||
|       "pef", | ||||
|       "pes", | ||||
|       "pfa", | ||||
|       "pfb", | ||||
|       "pfm", | ||||
|       "pgm", | ||||
|       "pgx", | ||||
|       "phm", | ||||
|       "picon", | ||||
|       "pict", | ||||
|       "pix", | ||||
|       "pjpeg", | ||||
|       "plasma", | ||||
|       "png", | ||||
|       "png00", | ||||
|       "png24", | ||||
|       "png32", | ||||
|       "png48", | ||||
|       "png64", | ||||
|       "png8", | ||||
|       "pnm", | ||||
|       "pocketmod", | ||||
|       "ppm", | ||||
|       "ps", | ||||
|       "psb", | ||||
|       "psd", | ||||
|       "ptif", | ||||
|       "pwp", | ||||
|       "qoi", | ||||
|       "radial", | ||||
|       "raf", | ||||
|       "ras", | ||||
|       "raw", | ||||
|       "rgb", | ||||
|       "rgb565", | ||||
|       "rgba", | ||||
|       "rgbo", | ||||
|       "rgf", | ||||
|       "rla", | ||||
|       "rle", | ||||
|       "rmf", | ||||
|       "rsvg", | ||||
|       "rw2", | ||||
|       "rwl", | ||||
|       "scr", | ||||
|       "screenshot", | ||||
|       "sct", | ||||
|       "sfw", | ||||
|       "sgi", | ||||
|       "six", | ||||
|       "sixel", | ||||
|       "sr2", | ||||
|       "srf", | ||||
|       "srw", | ||||
|       "stegano", | ||||
|       "sti", | ||||
|       "strimg", | ||||
|       "sun", | ||||
|       "svg", | ||||
|       "svgz", | ||||
|       "text", | ||||
|       "tga", | ||||
|       "tiff", | ||||
|       "tiff64", | ||||
|       "tile", | ||||
|       "tim", | ||||
|       "tm2", | ||||
|       "ttc", | ||||
|       "ttf", | ||||
|       "txt", | ||||
|       "uyvy", | ||||
|       "vda", | ||||
|       "vicar", | ||||
|       "vid", | ||||
|       "viff", | ||||
|       "vips", | ||||
|       "vst", | ||||
|       "wbmp", | ||||
|       "webm", | ||||
|       "webp", | ||||
|       "wmf", | ||||
|       "wmv", | ||||
|       "wpg", | ||||
|       "x3f", | ||||
|       "xbm", | ||||
|       "xc", | ||||
|       "xcf", | ||||
|       "xpm", | ||||
|       "xps", | ||||
|       "xv", | ||||
|       "ycbcr", | ||||
|       "ycbcra", | ||||
|       "yuv", | ||||
|     ], | ||||
|   }, | ||||
|   to: { | ||||
|     images: [ | ||||
|       "aai", | ||||
|       "ai", | ||||
|       "apng", | ||||
|       "art", | ||||
|       "ashlar", | ||||
|       "avif", | ||||
|       "avs", | ||||
|       "bayer", | ||||
|       "bayera", | ||||
|       "bgr", | ||||
|       "bgra", | ||||
|       "bgro", | ||||
|       "bmp", | ||||
|       "bmp2", | ||||
|       "bmp3", | ||||
|       "brf", | ||||
|       "cal", | ||||
|       "cals", | ||||
|       "cin", | ||||
|       "cip", | ||||
|       "clip", | ||||
|       "clipboard", | ||||
|       "cmyk", | ||||
|       "cmyka", | ||||
|       "cur", | ||||
|       "data", | ||||
|       "dcx", | ||||
|       "dds", | ||||
|       "dpx", | ||||
|       "dxt1", | ||||
|       "dxt5", | ||||
|       "epdf", | ||||
|       "epi", | ||||
|       "eps", | ||||
|       "eps2", | ||||
|       "eps3", | ||||
|       "epsf", | ||||
|       "epsi", | ||||
|       "ept", | ||||
|       "ept2", | ||||
|       "ept3", | ||||
|       "exr", | ||||
|       "farbfeld", | ||||
|       "fax", | ||||
|       "ff", | ||||
|       "fits", | ||||
|       "fl32", | ||||
|       "flif", | ||||
|       "flv", | ||||
|       "fts", | ||||
|       "ftxt", | ||||
|       "g3", | ||||
|       "g4", | ||||
|       "gif", | ||||
|       "gif87", | ||||
|       "gray", | ||||
|       "graya", | ||||
|       "group4", | ||||
|       "hdr", | ||||
|       "histogram", | ||||
|       "hrz", | ||||
|       "htm", | ||||
|       "html", | ||||
|       "icb", | ||||
|       "ico", | ||||
|       "icon", | ||||
|       "info", | ||||
|       "inline", | ||||
|       "ipl", | ||||
|       "isobrl", | ||||
|       "isobrl6", | ||||
|       "j2c", | ||||
|       "j2k", | ||||
|       "jng", | ||||
|       "jp2", | ||||
|       "jpc", | ||||
|       "jpe", | ||||
|       "jpeg", | ||||
|       "jpg", | ||||
|       "jpm", | ||||
|       "jps", | ||||
|       "jpt", | ||||
|       "json", | ||||
|       "jxl", | ||||
|       "m2v", | ||||
|       "m4v", | ||||
|       "map", | ||||
|       "mask", | ||||
|       "mat", | ||||
|       "matte", | ||||
|       "miff", | ||||
|       "mkv", | ||||
|       "mng", | ||||
|       "mono", | ||||
|       "mov", | ||||
|       "mp4", | ||||
|       "mpc", | ||||
|       "mpeg", | ||||
|       "mpg", | ||||
|       "msl", | ||||
|       "msvg", | ||||
|       "mtv", | ||||
|       "mvg", | ||||
|       "null", | ||||
|       "otb", | ||||
|       "pal", | ||||
|       "palm", | ||||
|       "pam", | ||||
|       "pbm", | ||||
|       "pcd", | ||||
|       "pcds", | ||||
|       "pcl", | ||||
|       "pct", | ||||
|       "pcx", | ||||
|       "pdb", | ||||
|       "pdf", | ||||
|       "pdfa", | ||||
|       "pfm", | ||||
|       "pgm", | ||||
|       "pgx", | ||||
|       "phm", | ||||
|       "picon", | ||||
|       "pict", | ||||
|       "pjpeg", | ||||
|       "png", | ||||
|       "png00", | ||||
|       "png24", | ||||
|       "png32", | ||||
|       "png48", | ||||
|       "png64", | ||||
|       "png8", | ||||
|       "pnm", | ||||
|       "pocketmod", | ||||
|       "ppm", | ||||
|       "ps", | ||||
|       "ps2", | ||||
|       "ps3", | ||||
|       "psb", | ||||
|       "psd", | ||||
|       "ptif", | ||||
|       "qoi", | ||||
|       "ras", | ||||
|       "rgb", | ||||
|       "rgba", | ||||
|       "rgbo", | ||||
|       "rgf", | ||||
|       "rsvg", | ||||
|       "sgi", | ||||
|       "shtml", | ||||
|       "six", | ||||
|       "sixel", | ||||
|       "sparse", | ||||
|       "strimg", | ||||
|       "sun", | ||||
|       "svg", | ||||
|       "svgz", | ||||
|       "tga", | ||||
|       "thumbnail", | ||||
|       "tiff", | ||||
|       "tiff64", | ||||
|       "txt", | ||||
|       "ubrl", | ||||
|       "ubrl6", | ||||
|       "uil", | ||||
|       "uyvy", | ||||
|       "vda", | ||||
|       "vicar", | ||||
|       "vid", | ||||
|       "viff", | ||||
|       "vips", | ||||
|       "vst", | ||||
|       "wbmp", | ||||
|       "webm", | ||||
|       "webp", | ||||
|       "wmv", | ||||
|       "wpg", | ||||
|       "xbm", | ||||
|       "xpm", | ||||
|       "xv", | ||||
|       "yaml", | ||||
|       "ycbcr", | ||||
|       "ycbcra", | ||||
|       "yuv", | ||||
|     ], | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| export function convert( | ||||
|   filePath: string, | ||||
|   fileType: string, | ||||
|   convertTo: string, | ||||
|   targetPath: string, | ||||
|   // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||||
|   options?: unknown, | ||||
| ): Promise<string> { | ||||
|   let outputArgs: string[] = []; | ||||
|   let inputArgs: string[] = []; | ||||
|  | ||||
|   if (convertTo === "ico") { | ||||
|     outputArgs = ["-define", "icon:auto-resize=256,128,64,48,32,16", "-background", "none"]; | ||||
|  | ||||
|     if (fileType === "svg") { | ||||
|       // this might be a bit too much, but it works | ||||
|       inputArgs = ["-background", "none", "-density", "512"]; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return new Promise((resolve, reject) => { | ||||
|     execFile( | ||||
|       "magick", | ||||
|       [...inputArgs, filePath, ...outputArgs, targetPath], | ||||
|       (error, stdout, stderr) => { | ||||
|         if (error) { | ||||
|           reject(`error: ${error}`); | ||||
|         } | ||||
|  | ||||
|         if (stdout) { | ||||
|           console.log(`stdout: ${stdout}`); | ||||
|         } | ||||
|  | ||||
|         if (stderr) { | ||||
|           console.error(`stderr: ${stderr}`); | ||||
|         } | ||||
|  | ||||
|         resolve("Done"); | ||||
|       }, | ||||
|     ); | ||||
|   }); | ||||
| } | ||||
							
								
								
									
										55
									
								
								src/converters/inkscape.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,55 @@ | ||||
| import { execFile } from "node:child_process"; | ||||
|  | ||||
| export const properties = { | ||||
|   from: { | ||||
|     images: ["svg", "pdf", "eps", "ps", "wmf", "emf", "png"], | ||||
|   }, | ||||
|   to: { | ||||
|     images: [ | ||||
|       "dxf", | ||||
|       "emf", | ||||
|       "eps", | ||||
|       "fxg", | ||||
|       "gpl", | ||||
|       "hpgl", | ||||
|       "html", | ||||
|       "odg", | ||||
|       "pdf", | ||||
|       "png", | ||||
|       "pov", | ||||
|       "ps", | ||||
|       "sif", | ||||
|       "svg", | ||||
|       "svgz", | ||||
|       "tex", | ||||
|       "wmf", | ||||
|     ], | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| export function convert( | ||||
|   filePath: string, | ||||
|   fileType: string, | ||||
|   convertTo: string, | ||||
|   targetPath: string, | ||||
|   // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||||
|   options?: unknown, | ||||
| ): Promise<string> { | ||||
|   return new Promise((resolve, reject) => { | ||||
|     execFile("inkscape", [filePath, "-o", targetPath], (error, stdout, stderr) => { | ||||
|       if (error) { | ||||
|         reject(`error: ${error}`); | ||||
|       } | ||||
|  | ||||
|       if (stdout) { | ||||
|         console.log(`stdout: ${stdout}`); | ||||
|       } | ||||
|  | ||||
|       if (stderr) { | ||||
|         console.error(`stderr: ${stderr}`); | ||||
|       } | ||||
|  | ||||
|       resolve("Done"); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
							
								
								
									
										37
									
								
								src/converters/libheif.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,37 @@ | ||||
| import { execFile } from "child_process"; | ||||
|  | ||||
| export const properties = { | ||||
|   from: { | ||||
|     images: ["avci", "avcs", "avif", "h264", "heic", "heics", "heif", "heifs", "hif", "mkv", "mp4"], | ||||
|   }, | ||||
|   to: { | ||||
|     images: ["jpeg", "png", "y4m"], | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| export function convert( | ||||
|   filePath: string, | ||||
|   fileType: string, | ||||
|   convertTo: string, | ||||
|   targetPath: string, | ||||
|   // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||||
|   options?: unknown, | ||||
| ): Promise<string> { | ||||
|   return new Promise((resolve, reject) => { | ||||
|     execFile("heif-convert", [filePath, targetPath], (error, stdout, stderr) => { | ||||
|       if (error) { | ||||
|         reject(`error: ${error}`); | ||||
|       } | ||||
|  | ||||
|       if (stdout) { | ||||
|         console.log(`stdout: ${stdout}`); | ||||
|       } | ||||
|  | ||||
|       if (stderr) { | ||||
|         console.error(`stderr: ${stderr}`); | ||||
|       } | ||||
|  | ||||
|       resolve("Done"); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
| @@ -1,35 +1,13 @@ | ||||
| import { exec } from "node:child_process"; | ||||
| import { execFile } from "node:child_process"; | ||||
|  | ||||
| // declare possible conversions | ||||
| export const properties = { | ||||
|   from: { | ||||
|     jxl: ["jxl"], | ||||
|     images: [ | ||||
|       "apng", | ||||
|       "exr", | ||||
|       "gif", | ||||
|       "jpeg", | ||||
|       "pam", | ||||
|       "pfm", | ||||
|       "pgm", | ||||
|       "pgx", | ||||
|       "png", | ||||
|       "ppm", | ||||
|     ], | ||||
|     images: ["apng", "exr", "gif", "jpeg", "pam", "pfm", "pgm", "pgx", "png", "ppm"], | ||||
|   }, | ||||
|   to: { | ||||
|     jxl: [ | ||||
|       "apng", | ||||
|       "exr", | ||||
|       "gif", | ||||
|       "jpeg", | ||||
|       "pam", | ||||
|       "pfm", | ||||
|       "pgm", | ||||
|       "pgx", | ||||
|       "png", | ||||
|       "ppm", | ||||
|     ], | ||||
|     jxl: ["apng", "exr", "gif", "jpeg", "pam", "pfm", "pgm", "pgx", "png", "ppm"], | ||||
|     images: ["jxl"], | ||||
|   }, | ||||
| }; | ||||
| @@ -39,8 +17,8 @@ export function convert( | ||||
|   fileType: string, | ||||
|   convertTo: string, | ||||
|   targetPath: string, | ||||
|   // biome-ignore lint/suspicious/noExplicitAny: <explanation> | ||||
|   options?: any, | ||||
|   // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||||
|   options?: unknown, | ||||
| ): Promise<string> { | ||||
|   let tool = ""; | ||||
|   if (fileType === "jxl") { | ||||
| @@ -52,7 +30,7 @@ export function convert( | ||||
|   } | ||||
|  | ||||
|   return new Promise((resolve, reject) => { | ||||
|     exec(`${tool} "${filePath}" "${targetPath}"`, (error, stdout, stderr) => { | ||||
|     execFile(tool, [filePath, targetPath], (error, stdout, stderr) => { | ||||
|       if (error) { | ||||
|         reject(`error: ${error}`); | ||||
|       } | ||||
| @@ -65,7 +43,7 @@ export function convert( | ||||
|         console.error(`stderr: ${stderr}`); | ||||
|       } | ||||
|  | ||||
|       resolve("success"); | ||||
|       resolve("Done"); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
|   | ||||
| @@ -1,65 +1,52 @@ | ||||
| import { convert as convertImage, properties as propertiesImage } from "./vips"; | ||||
|  | ||||
| import { | ||||
|   convert as convertPandoc, | ||||
|   properties as propertiesPandoc, | ||||
| } from "./pandoc"; | ||||
|  | ||||
| import { | ||||
|   convert as convertFFmpeg, | ||||
|   properties as propertiesFFmpeg, | ||||
| } from "./ffmpeg"; | ||||
|  | ||||
| import { normalizeFiletype } from "../helpers/normalizeFiletype"; | ||||
| import { convert as convertassimp, properties as propertiesassimp } from "./assimp"; | ||||
| import { convert as convertCalibre, properties as propertiesCalibre } from "./calibre"; | ||||
| import { convert as convertDvisvgm, properties as propertiesDvisvgm } from "./dvisvgm"; | ||||
| import { convert as convertFFmpeg, properties as propertiesFFmpeg } from "./ffmpeg"; | ||||
| import { | ||||
|   convert as convertGraphicsmagick, | ||||
|   properties as propertiesGraphicsmagick, | ||||
| } from "./graphicsmagick"; | ||||
|  | ||||
| import { | ||||
|   convert as convertxelatex, | ||||
|   properties as propertiesxelatex, | ||||
| } from "./xelatex"; | ||||
|  | ||||
| import { | ||||
|   convert as convertLibjxl, | ||||
|   properties as propertiesLibjxl, | ||||
| } from "./libjxl"; | ||||
|  | ||||
| import { | ||||
|   convert as convertresvg, | ||||
|   properties as propertiesresvg, | ||||
| } from "./resvg"; | ||||
|  | ||||
| import { normalizeFiletype } from "../helpers/normalizeFiletype"; | ||||
| import { convert as convertImagemagick, properties as propertiesImagemagick } from "./imagemagick"; | ||||
| import { convert as convertInkscape, properties as propertiesInkscape } from "./inkscape"; | ||||
| import { convert as convertLibheif, properties as propertiesLibheif } from "./libheif"; | ||||
| import { convert as convertLibjxl, properties as propertiesLibjxl } from "./libjxl"; | ||||
| import { convert as convertPandoc, properties as propertiesPandoc } from "./pandoc"; | ||||
| import { convert as convertPotrace, properties as propertiesPotrace } from "./potrace"; | ||||
| import { convert as convertresvg, properties as propertiesresvg } from "./resvg"; | ||||
| import { convert as convertImage, properties as propertiesImage } from "./vips"; | ||||
| import { convert as convertxelatex, properties as propertiesxelatex } from "./xelatex"; | ||||
|  | ||||
| // 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, | ||||
|       fileType: string, | ||||
|       convertTo: string, | ||||
|       targetPath: string, | ||||
|       // biome-ignore lint/suspicious/noExplicitAny: <explanation> | ||||
|       options?: any, | ||||
|       // biome-ignore lint/suspicious/noExplicitAny: <explanation> | ||||
|     ) => any; | ||||
|   }; | ||||
| } = { | ||||
|  | ||||
|       options?: unknown, | ||||
|     ) => unknown; | ||||
|   } | ||||
| > = { | ||||
|   libjxl: { | ||||
|     properties: propertiesLibjxl, | ||||
|     converter: convertLibjxl, | ||||
| @@ -72,45 +59,68 @@ const properties: { | ||||
|     properties: propertiesImage, | ||||
|     converter: convertImage, | ||||
|   }, | ||||
|   libheif: { | ||||
|     properties: propertiesLibheif, | ||||
|     converter: convertLibheif, | ||||
|   }, | ||||
|   xelatex: { | ||||
|     properties: propertiesxelatex, | ||||
|     converter: convertxelatex, | ||||
|   }, | ||||
|   calibre: { | ||||
|     properties: propertiesCalibre, | ||||
|     converter: convertCalibre, | ||||
|   }, | ||||
|   pandoc: { | ||||
|     properties: propertiesPandoc, | ||||
|     converter: convertPandoc, | ||||
|   }, | ||||
|   dvisvgm: { | ||||
|     properties: propertiesDvisvgm, | ||||
|     converter: convertDvisvgm, | ||||
|   }, | ||||
|   imagemagick: { | ||||
|     properties: propertiesImagemagick, | ||||
|     converter: convertImagemagick, | ||||
|   }, | ||||
|   graphicsmagick: { | ||||
|     properties: propertiesGraphicsmagick, | ||||
|     converter: convertGraphicsmagick, | ||||
|   }, | ||||
|   inkscape: { | ||||
|     properties: propertiesInkscape, | ||||
|     converter: convertInkscape, | ||||
|   }, | ||||
|   assimp: { | ||||
|     properties: propertiesassimp, | ||||
|     converter: convertassimp, | ||||
|   }, | ||||
|   ffmpeg: { | ||||
|     properties: propertiesFFmpeg, | ||||
|     converter: convertFFmpeg, | ||||
|   }, | ||||
|   potrace: { | ||||
|     properties: propertiesPotrace, | ||||
|     converter: convertPotrace, | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| export async function mainConverter( | ||||
|   inputFilePath: string, | ||||
|   fileTypeOriginal: string, | ||||
|   // biome-ignore lint/suspicious/noExplicitAny: <explanation> | ||||
|   convertTo: any, | ||||
|   convertTo: string, | ||||
|   targetPath: string, | ||||
|   // biome-ignore lint/suspicious/noExplicitAny: <explanation> | ||||
|   options?: any, | ||||
|   options?: unknown, | ||||
|   converterName?: string, | ||||
| ) { | ||||
|   const fileType = normalizeFiletype(fileTypeOriginal); | ||||
|  | ||||
|   // biome-ignore lint/suspicious/noExplicitAny: <explanation> | ||||
|   let converterFunc: any; | ||||
|   // let converterName = converterName; | ||||
|   let converterFunc: (typeof properties)["libjxl"]["converter"] | undefined; | ||||
|  | ||||
|   if (converterName) { | ||||
|     converterFunc = properties[converterName]?.converter; | ||||
|   } else { | ||||
|     // Iterate over each converter in properties | ||||
|     // biome-ignore lint/style/noParameterAssign: <explanation> | ||||
|     for (converterName in properties) { | ||||
|       const converterObj = properties[converterName]; | ||||
|  | ||||
| @@ -131,24 +141,22 @@ export async function mainConverter( | ||||
|   } | ||||
|  | ||||
|   if (!converterFunc) { | ||||
|     console.log( | ||||
|       `No available converter supports converting from ${fileType} to ${convertTo}.`, | ||||
|     ); | ||||
|     console.log(`No available converter supports converting from ${fileType} to ${convertTo}.`); | ||||
|     return "File type not supported"; | ||||
|   } | ||||
|  | ||||
|   try { | ||||
|     await converterFunc( | ||||
|       inputFilePath, | ||||
|       fileType, | ||||
|       convertTo, | ||||
|       targetPath, | ||||
|       options, | ||||
|     ); | ||||
|     const result = await converterFunc(inputFilePath, fileType, convertTo, targetPath, options); | ||||
|  | ||||
|     console.log( | ||||
|       `Converted ${inputFilePath} from ${fileType} to ${convertTo} successfully using ${converterName}.`, | ||||
|       result, | ||||
|     ); | ||||
|  | ||||
|     if (typeof result === "string") { | ||||
|       return result; | ||||
|     } | ||||
|  | ||||
|     return "Done"; | ||||
|   } catch (error) { | ||||
|     console.error( | ||||
| @@ -159,7 +167,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; | ||||
| @@ -178,15 +186,12 @@ for (const converterName in properties) { | ||||
|         possibleTargets[extension] = {}; | ||||
|       } | ||||
|  | ||||
|       possibleTargets[extension][converterName] = | ||||
|         converterProperties.to[key] || []; | ||||
|       possibleTargets[extension][converterName] = converterProperties.to[key] || []; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| export const getPossibleTargets = ( | ||||
|   from: string, | ||||
| ): { [key: string]: string[] } => { | ||||
| export const getPossibleTargets = (from: string): Record<string, string[]> => { | ||||
|   const fromClean = normalizeFiletype(from); | ||||
|  | ||||
|   return possibleTargets[fromClean] || {}; | ||||
| @@ -210,11 +215,12 @@ for (const converterName in properties) { | ||||
| } | ||||
| possibleInputs.sort(); | ||||
|  | ||||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||||
| 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; | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { exec } from "node:child_process"; | ||||
| import { execFile } from "node:child_process"; | ||||
|  | ||||
| export const properties = { | ||||
|   from: { | ||||
| @@ -124,33 +124,39 @@ export function convert( | ||||
|   fileType: string, | ||||
|   convertTo: string, | ||||
|   targetPath: string, | ||||
|   // biome-ignore lint/suspicious/noExplicitAny: <explanation> | ||||
|   options?: any, | ||||
|   // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||||
|   options?: unknown, | ||||
| ): Promise<string> { | ||||
|   // set xelatex here | ||||
|   const xelatex = ["pdf", "latex"]; | ||||
|   let option = ""; | ||||
|  | ||||
|   // Build arguments array | ||||
|   const args: string[] = []; | ||||
|  | ||||
|   if (xelatex.includes(convertTo)) { | ||||
|     option = "--pdf-engine=xelatex"; | ||||
|     args.push("--pdf-engine=xelatex"); | ||||
|   } | ||||
|  | ||||
|   args.push(filePath); | ||||
|   args.push("-f", fileType); | ||||
|   args.push("-t", convertTo); | ||||
|   args.push("-o", targetPath); | ||||
|  | ||||
|   return new Promise((resolve, reject) => { | ||||
|     exec( | ||||
|       `pandoc ${option} "${filePath}" -f ${fileType} -t ${convertTo} -o "${targetPath}"`, | ||||
|       (error, stdout, stderr) => { | ||||
|         if (error) { | ||||
|           reject(`error: ${error}`); | ||||
|         } | ||||
|     execFile("pandoc", args, (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("Done"); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
|   | ||||
							
								
								
									
										49
									
								
								src/converters/potrace.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,49 @@ | ||||
| import { execFile } from "node:child_process"; | ||||
|  | ||||
| export const properties = { | ||||
|   from: { | ||||
|     images: ["pnm", "pbm", "pgm", "bmp"], | ||||
|   }, | ||||
|   to: { | ||||
|     images: [ | ||||
|       "svg", | ||||
|       "pdf", | ||||
|       "pdfpage", | ||||
|       "eps", | ||||
|       "postscript", | ||||
|       "ps", | ||||
|       "dxf", | ||||
|       "geojson", | ||||
|       "pgm", | ||||
|       "gimppath", | ||||
|       "xfig", | ||||
|     ], | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| export function convert( | ||||
|   filePath: string, | ||||
|   fileType: string, | ||||
|   convertTo: string, | ||||
|   targetPath: string, | ||||
|   // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||||
|   options?: unknown, | ||||
| ): Promise<string> { | ||||
|   return new Promise((resolve, reject) => { | ||||
|     execFile("potrace", [filePath, "-o", targetPath, "-b", convertTo], (error, stdout, stderr) => { | ||||
|       if (error) { | ||||
|         reject(`error: ${error}`); | ||||
|       } | ||||
|  | ||||
|       if (stdout) { | ||||
|         console.log(`stdout: ${stdout}`); | ||||
|       } | ||||
|  | ||||
|       if (stderr) { | ||||
|         console.error(`stderr: ${stderr}`); | ||||
|       } | ||||
|  | ||||
|       resolve("Done"); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { exec } from "node:child_process"; | ||||
| import { execFile } from "node:child_process"; | ||||
|  | ||||
| export const properties = { | ||||
|   from: { | ||||
| @@ -9,33 +9,29 @@ export const properties = { | ||||
|   }, | ||||
| }; | ||||
|  | ||||
|  | ||||
| export function convert( | ||||
|   filePath: string, | ||||
|   fileType: string, | ||||
|   convertTo: string, | ||||
|   targetPath: string, | ||||
|   // biome-ignore lint/suspicious/noExplicitAny: <explanation> | ||||
|   options?: any, | ||||
|   // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||||
|   options?: unknown, | ||||
| ): Promise<string> { | ||||
|   return new Promise((resolve, reject) => { | ||||
|     exec( | ||||
|       `resvg "${filePath}" "${targetPath}"`, | ||||
|       (error, stdout, stderr) => { | ||||
|         if (error) { | ||||
|           reject(`error: ${error}`); | ||||
|         } | ||||
|     execFile("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("Done"); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { exec } from "node:child_process"; | ||||
| import { execFile } from "node:child_process"; | ||||
|  | ||||
| // declare possible conversions | ||||
| export const properties = { | ||||
| @@ -94,8 +94,8 @@ export function convert( | ||||
|   fileType: string, | ||||
|   convertTo: string, | ||||
|   targetPath: string, | ||||
|   // biome-ignore lint/suspicious/noExplicitAny: <explanation> | ||||
|   options?: any, | ||||
|   // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||||
|   options?: unknown, | ||||
| ): Promise<string> { | ||||
|   // if (fileType === "svg") { | ||||
|   //   const scale = options.scale || 1; | ||||
| @@ -119,23 +119,20 @@ export function convert( | ||||
|   } | ||||
|  | ||||
|   return new Promise((resolve, reject) => { | ||||
|     exec( | ||||
|       `vips ${action} "${filePath}" "${targetPath}"`, | ||||
|       (error, stdout, stderr) => { | ||||
|         if (error) { | ||||
|           reject(`error: ${error}`); | ||||
|         } | ||||
|     execFile("vips", [action, 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("Done"); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { exec } from "node:child_process"; | ||||
| import { execFile } from "node:child_process"; | ||||
|  | ||||
| export const properties = { | ||||
|   from: { | ||||
| @@ -14,18 +14,16 @@ export function convert( | ||||
|   fileType: string, | ||||
|   convertTo: string, | ||||
|   targetPath: string, | ||||
|   // biome-ignore lint/suspicious/noExplicitAny: <explanation> | ||||
|   options?: any, | ||||
|   // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||||
|   options?: unknown, | ||||
| ): Promise<string> { | ||||
|   return new Promise((resolve, reject) => { | ||||
|     // const fileName: string = (targetPath.split("/").pop() as string).replace(".pdf", "") | ||||
|     const outputPath = targetPath | ||||
|       .split("/") | ||||
|       .slice(0, -1) | ||||
|       .join("/") | ||||
|       .replace("./", ""); | ||||
|     exec( | ||||
|       `latexmk -xelatex -interaction=nonstopmode -output-directory="${outputPath}" "${filePath}"`, | ||||
|     const outputPath = targetPath.split("/").slice(0, -1).join("/").replace("./", ""); | ||||
|  | ||||
|     execFile( | ||||
|       "latexmk", | ||||
|       ["-xelatex", "-interaction=nonstopmode", `-output-directory=${outputPath}`, filePath], | ||||
|       (error, stdout, stderr) => { | ||||
|         if (error) { | ||||
|           reject(`error: ${error}`); | ||||
| @@ -39,7 +37,7 @@ export function convert( | ||||
|           console.error(`stderr: ${stderr}`); | ||||
|         } | ||||
|  | ||||
|         resolve("success"); | ||||
|         resolve("Done"); | ||||
|       }, | ||||
|     ); | ||||
|   }); | ||||
|   | ||||
							
								
								
									
										41
									
								
								src/db/db.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,41 @@ | ||||
| import { Database } from "bun:sqlite"; | ||||
|  | ||||
| const db = new Database("./data/mydb.sqlite", { create: true }); | ||||
|  | ||||
| if (!db.query("SELECT * FROM sqlite_master WHERE type='table'").get()) { | ||||
|   db.exec(` | ||||
| CREATE TABLE IF NOT EXISTS users ( | ||||
| 	id INTEGER PRIMARY KEY AUTOINCREMENT, | ||||
| 	email TEXT NOT NULL, | ||||
| 	password TEXT NOT NULL | ||||
| ); | ||||
| CREATE TABLE IF NOT EXISTS file_names ( | ||||
|   id INTEGER PRIMARY KEY AUTOINCREMENT, | ||||
|   job_id INTEGER NOT NULL, | ||||
|   file_name TEXT NOT NULL, | ||||
|   output_file_name TEXT NOT NULL, | ||||
|   status TEXT DEFAULT 'not started', | ||||
|   FOREIGN KEY (job_id) REFERENCES jobs(id) | ||||
| ); | ||||
| CREATE TABLE IF NOT EXISTS jobs ( | ||||
| 	id INTEGER PRIMARY KEY AUTOINCREMENT, | ||||
| 	user_id INTEGER NOT NULL, | ||||
| 	date_created TEXT NOT NULL, | ||||
|   status TEXT DEFAULT 'not started', | ||||
|   num_files INTEGER DEFAULT 0, | ||||
|   FOREIGN KEY (user_id) REFERENCES users(id) | ||||
| ); | ||||
| PRAGMA user_version = 1;`); | ||||
| } | ||||
|  | ||||
| const dbVersion = (db.query("PRAGMA user_version").get() as { user_version?: number }).user_version; | ||||
| if (dbVersion === 0) { | ||||
|   db.exec("ALTER TABLE file_names ADD COLUMN status TEXT DEFAULT 'not started';"); | ||||
|   db.exec("PRAGMA user_version = 1;"); | ||||
|   console.log("Updated database to version 1."); | ||||
| } | ||||
|  | ||||
| // enable WAL mode | ||||
| db.exec("PRAGMA journal_mode = WAL;"); | ||||
|  | ||||
| export default db; | ||||
							
								
								
									
										23
									
								
								src/db/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,23 @@ | ||||
| export class Filename { | ||||
|   id!: number; | ||||
|   job_id!: number; | ||||
|   file_name!: string; | ||||
|   output_file_name!: string; | ||||
|   status!: string; | ||||
| } | ||||
|  | ||||
| export class Jobs { | ||||
|   finished_files!: number; | ||||
|   id!: number; | ||||
|   user_id!: number; | ||||
|   date_created!: string; | ||||
|   status!: string; | ||||
|   num_files!: number; | ||||
|   files_detailed!: Filename[]; | ||||
| } | ||||
|  | ||||
| export class User { | ||||
|   id!: number; | ||||
|   email!: string; | ||||
|   password!: string; | ||||
| } | ||||
							
								
								
									
										15
									
								
								src/helpers/env.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,15 @@ | ||||
| export const ACCOUNT_REGISTRATION = | ||||
|   process.env.ACCOUNT_REGISTRATION?.toLowerCase() === "true" || false; | ||||
|  | ||||
| export const HTTP_ALLOWED = process.env.HTTP_ALLOWED?.toLowerCase() === "true" || false; | ||||
|  | ||||
| export const ALLOW_UNAUTHENTICATED = | ||||
|   process.env.ALLOW_UNAUTHENTICATED?.toLowerCase() === "true" || false; | ||||
|  | ||||
| export const AUTO_DELETE_EVERY_N_HOURS = process.env.AUTO_DELETE_EVERY_N_HOURS | ||||
|   ? Number(process.env.AUTO_DELETE_EVERY_N_HOURS) | ||||
|   : 24; | ||||
|  | ||||
| export const HIDE_HISTORY = process.env.HIDE_HISTORY?.toLowerCase() === "true" || false; | ||||
|  | ||||
| export const WEBROOT = process.env.WEBROOT ?? ""; | ||||
| @@ -2,6 +2,7 @@ export const normalizeFiletype = (filetype: string): string => { | ||||
|   const lowercaseFiletype = filetype.toLowerCase(); | ||||
|  | ||||
|   switch (lowercaseFiletype) { | ||||
|     case "jfif": | ||||
|     case "jpg": | ||||
|       return "jpeg"; | ||||
|     case "htm": | ||||
| @@ -10,6 +11,8 @@ export const normalizeFiletype = (filetype: string): string => { | ||||
|       return "latex"; | ||||
|     case "md": | ||||
|       return "markdown"; | ||||
|     case "unknown": | ||||
|       return "m4a"; | ||||
|     default: | ||||
|       return lowercaseFiletype; | ||||
|   } | ||||
| @@ -23,6 +26,9 @@ export const normalizeOutputFiletype = (filetype: string): string => { | ||||
|       return "jpg"; | ||||
|     case "latex": | ||||
|       return "tex"; | ||||
|     case "markdown_phpextra": | ||||
|     case "markdown_strict": | ||||
|     case "markdown_mmd": | ||||
|     case "markdown": | ||||
|       return "md"; | ||||
|     default: | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { exec } from "node:child_process"; | ||||
| import { version } from "../../package.json"; | ||||
|  | ||||
| console.log(`ConvertX v${version}`); | ||||
|  | ||||
| if (process.env.NODE_ENV === "production") { | ||||
| @@ -43,6 +44,16 @@ if (process.env.NODE_ENV === "production") { | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   exec("magick --version", (error, stdout) => { | ||||
|     if (error) { | ||||
|       console.error("ImageMagick is not installed."); | ||||
|     } | ||||
|  | ||||
|     if (stdout) { | ||||
|       console.log(stdout.split("\n")[0]?.replace("Version: ", "")); | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   exec("gm version", (error, stdout) => { | ||||
|     if (error) { | ||||
|       console.error("GraphicsMagick is not installed."); | ||||
| @@ -53,6 +64,16 @@ if (process.env.NODE_ENV === "production") { | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   exec("inkscape --version", (error, stdout) => { | ||||
|     if (error) { | ||||
|       console.error("Inkscape is not installed."); | ||||
|     } | ||||
|  | ||||
|     if (stdout) { | ||||
|       console.log(stdout.split("\n")[0]); | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   exec("djxl --version", (error, stdout) => { | ||||
|     if (error) { | ||||
|       console.error("libjxl-tools is not installed."); | ||||
| @@ -83,6 +104,46 @@ if (process.env.NODE_ENV === "production") { | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   exec("assimp version", (error, stdout) => { | ||||
|     if (error) { | ||||
|       console.error("assimp is not installed"); | ||||
|     } | ||||
|  | ||||
|     if (stdout) { | ||||
|       console.log(`assimp ${stdout.split("\n")[5]}`); | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   exec("ebook-convert --version", (error, stdout) => { | ||||
|     if (error) { | ||||
|       console.error("ebook-convert (calibre) is not installed"); | ||||
|     } | ||||
|  | ||||
|     if (stdout) { | ||||
|       console.log(stdout.split("\n")[0]); | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   exec("heif-info -v", (error, stdout) => { | ||||
|     if (error) { | ||||
|       console.error("libheif is not installed"); | ||||
|     } | ||||
|  | ||||
|     if (stdout) { | ||||
|       console.log(`libheif v${stdout.split("\n")[0]}`); | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   exec("potrace -v", (error, stdout) => { | ||||
|     if (error) { | ||||
|       console.error("potrace is not installed"); | ||||
|     } | ||||
|  | ||||
|     if (stdout) { | ||||
|       console.log(stdout.split("\n")[0]); | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   exec("bun -v", (error, stdout) => { | ||||
|     if (error) { | ||||
|       console.error("Bun is not installed. wait what"); | ||||
|   | ||||
							
								
								
									
										15
									
								
								src/helpers/tailwind.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,15 @@ | ||||
| import tailwind from "@tailwindcss/postcss"; | ||||
| import postcss from "postcss"; | ||||
|  | ||||
| export const generateTailwind = async () => { | ||||
|   const result = await Bun.file("./src/main.css") | ||||
|     .text() | ||||
|     .then((sourceText) => { | ||||
|       return postcss([tailwind]).process(sourceText, { | ||||
|         from: "./src/main.css", | ||||
|         to: "./public/generated.css", | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|   return result; | ||||
| }; | ||||
							
								
								
									
										1302
									
								
								src/index.tsx
									
									
									
									
									
								
							
							
						
						
							
								
								
									
										64
									
								
								src/main.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,64 @@ | ||||
| @import "tailwindcss"; | ||||
|  | ||||
| @plugin 'tailwind-scrollbar'; | ||||
|  | ||||
| @theme { | ||||
|   --color-contrast: rgba(var(--contrast)); | ||||
|   --color-neutral-900: rgba(var(--neutral-900)); | ||||
|   --color-neutral-800: rgba(var(--neutral-800)); | ||||
|   --color-neutral-700: rgba(var(--neutral-700)); | ||||
|   --color-neutral-600: rgba(var(--neutral-600)); | ||||
|   --color-neutral-500: rgba(var(--neutral-500)); | ||||
|   --color-neutral-400: rgba(var(--neutral-400)); | ||||
|   --color-neutral-300: rgba(var(--neutral-300)); | ||||
|   --color-neutral-200: rgba(var(--neutral-200)); | ||||
|   --color-neutral-100: rgba(var(--neutral-100)); | ||||
|   --color-accent-600: rgba(var(--accent-600)); | ||||
|   --color-accent-500: rgba(var(--accent-500)); | ||||
|   --color-accent-400: rgba(var(--accent-400)); | ||||
| } | ||||
|  | ||||
| @utility article { | ||||
|   @apply px-2 sm:px-4 py-4 mb-4 bg-neutral-800/40 w-full mx-auto max-w-4xl rounded-sm; | ||||
| } | ||||
|  | ||||
| @utility btn-primary { | ||||
|   @apply bg-accent-500 text-contrast rounded-sm p-2 sm:p-4 hover:bg-accent-400 cursor-pointer transition-colors; | ||||
| } | ||||
|  | ||||
| @utility btn-secondary { | ||||
|   @apply bg-neutral-400 text-contrast rounded-sm p-2 sm:p-4 hover:bg-neutral-300 cursor-pointer transition-colors; | ||||
| } | ||||
|  | ||||
| :root { | ||||
|   --contrast: 255, 255, 255; | ||||
|   --neutral-900: 243, 244, 246; | ||||
|   --neutral-800: 229, 231, 235; | ||||
|   --neutral-700: 209, 213, 219; | ||||
|   --neutral-600: 156, 163, 175; | ||||
|   --neutral-500: 180, 180, 180; | ||||
|   --neutral-400: 75, 85, 99; | ||||
|   --neutral-300: 55, 65, 81; | ||||
|   --neutral-200: 31, 41, 55; | ||||
|   --neutral-100: 17, 24, 39; | ||||
|   --accent-400: 132, 204, 22; | ||||
|   --accent-500: 101, 163, 13; | ||||
|   --accent-600: 77, 124, 15; | ||||
| } | ||||
|  | ||||
| @media (prefers-color-scheme: dark) { | ||||
|   :root { | ||||
|     --contrast: 0, 0, 0; | ||||
|     --neutral-900: 17, 24, 39; | ||||
|     --neutral-800: 31, 41, 55; | ||||
|     --neutral-700: 55, 65, 81; | ||||
|     --neutral-600: 75, 85, 99; | ||||
|     --neutral-500: 107, 114, 128; | ||||
|     --neutral-300: 209, 213, 219; | ||||
|     --neutral-400: 156, 163, 175; | ||||
|     --neutral-200: 229, 231, 235; | ||||
|     --accent-600: 101, 163, 13; | ||||
|     --accent-500: 132, 204, 22; | ||||
|     --accent-400: 163, 230, 53; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										67
									
								
								src/pages/chooseConverter.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,67 @@ | ||||
| import { Html } from "@elysiajs/html"; | ||||
| import Elysia, { t } from "elysia"; | ||||
| import { getPossibleTargets } from "../converters/main"; | ||||
| import { userService } from "./user"; | ||||
|  | ||||
| export const chooseConverter = new Elysia().use(userService).post( | ||||
|   "/conversions", | ||||
|   ({ body }) => { | ||||
|     return ( | ||||
|       <> | ||||
|         <article | ||||
|           class={` | ||||
|             convert_to_popup absolute z-2 m-0 hidden h-[50vh] max-h-[50vh] w-full flex-col | ||||
|             overflow-x-hidden overflow-y-auto rounded bg-neutral-800 | ||||
|             sm:h-[30vh] | ||||
|           `} | ||||
|         > | ||||
|           {Object.entries(getPossibleTargets(body.fileType)).map(([converter, targets]) => ( | ||||
|             <article | ||||
|               class="convert_to_group flex w-full flex-col border-b border-neutral-700 p-4" | ||||
|               data-converter={converter} | ||||
|             > | ||||
|               <header class="mb-2 w-full text-xl font-bold" safe> | ||||
|                 {converter} | ||||
|               </header> | ||||
|               <ul class="convert_to_target flex flex-row flex-wrap gap-1"> | ||||
|                 {targets.map((target) => ( | ||||
|                   <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 rounded bg-neutral-700 p-1 text-base | ||||
|                       hover:bg-neutral-600 | ||||
|                     `} | ||||
|                     data-value={`${target},${converter}`} | ||||
|                     data-target={target} | ||||
|                     data-converter={converter} | ||||
|                     type="button" | ||||
|                     safe | ||||
|                   > | ||||
|                     {target} | ||||
|                   </button> | ||||
|                 ))} | ||||
|               </ul> | ||||
|             </article> | ||||
|           ))} | ||||
|         </article> | ||||
|  | ||||
|         <select name="convert_to" aria-label="Convert to" required hidden> | ||||
|           <option selected disabled value=""> | ||||
|             Convert to | ||||
|           </option> | ||||
|           {Object.entries(getPossibleTargets(body.fileType)).map(([converter, targets]) => ( | ||||
|             <optgroup label={converter}> | ||||
|               {targets.map((target) => ( | ||||
|                 <option value={`${target},${converter}`} safe> | ||||
|                   {target} | ||||
|                 </option> | ||||
|               ))} | ||||
|             </optgroup> | ||||
|           ))} | ||||
|         </select> | ||||
|       </> | ||||
|     ); | ||||
|   }, | ||||
|   { body: t.Object({ fileType: t.String() }) }, | ||||
| ); | ||||
							
								
								
									
										116
									
								
								src/pages/convert.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,116 @@ | ||||
| import { mkdir } from "node:fs/promises"; | ||||
| import { Elysia, t } from "elysia"; | ||||
| import sanitize from "sanitize-filename"; | ||||
| import { outputDir, uploadsDir } from ".."; | ||||
| import { mainConverter } from "../converters/main"; | ||||
| import db from "../db/db"; | ||||
| import { Jobs } from "../db/types"; | ||||
| import { WEBROOT } from "../helpers/env"; | ||||
| import { normalizeFiletype, normalizeOutputFiletype } from "../helpers/normalizeFiletype"; | ||||
| import { userService } from "./user"; | ||||
|  | ||||
| export const convert = new Elysia().use(userService).post( | ||||
|   "/convert", | ||||
|   async ({ body, redirect, jwt, cookie: { auth, jobId } }) => { | ||||
|     if (!auth?.value) { | ||||
|       return redirect(`${WEBROOT}/login`, 302); | ||||
|     } | ||||
|  | ||||
|     const user = await jwt.verify(auth.value); | ||||
|     if (!user) { | ||||
|       return redirect(`${WEBROOT}/login`, 302); | ||||
|     } | ||||
|  | ||||
|     if (!jobId?.value) { | ||||
|       return redirect(`${WEBROOT}/`, 302); | ||||
|     } | ||||
|  | ||||
|     const existingJob = db | ||||
|       .query("SELECT * FROM jobs WHERE id = ? AND user_id = ?") | ||||
|       .as(Jobs) | ||||
|       .get(jobId.value, user.id); | ||||
|  | ||||
|     if (!existingJob) { | ||||
|       return redirect(`${WEBROOT}/`, 302); | ||||
|     } | ||||
|  | ||||
|     const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`; | ||||
|     const userOutputDir = `${outputDir}${user.id}/${jobId.value}/`; | ||||
|  | ||||
|     // create the output directory | ||||
|     try { | ||||
|       await mkdir(userOutputDir, { recursive: true }); | ||||
|     } catch (error) { | ||||
|       console.error(`Failed to create the output directory: ${userOutputDir}.`, error); | ||||
|     } | ||||
|  | ||||
|     const convertTo = normalizeFiletype(body.convert_to.split(",")[0] ?? ""); | ||||
|     const converterName = body.convert_to.split(",")[1]; | ||||
|     const fileNames = JSON.parse(body.file_names) as string[]; | ||||
|  | ||||
|     for (let i = 0; i < fileNames.length; i++) { | ||||
|       fileNames[i] = sanitize(fileNames[i] || ""); | ||||
|     } | ||||
|  | ||||
|     if (!Array.isArray(fileNames) || fileNames.length === 0) { | ||||
|       return redirect(`${WEBROOT}/`, 302); | ||||
|     } | ||||
|  | ||||
|     db.query("UPDATE jobs SET num_files = ?1, status = 'pending' WHERE id = ?2").run( | ||||
|       fileNames.length, | ||||
|       jobId.value, | ||||
|     ); | ||||
|  | ||||
|     const query = db.query( | ||||
|       "INSERT INTO file_names (job_id, file_name, output_file_name, status) VALUES (?1, ?2, ?3, ?4)", | ||||
|     ); | ||||
|  | ||||
|     // Start the conversion process in the background | ||||
|     Promise.all( | ||||
|       fileNames.map(async (fileName) => { | ||||
|         const filePath = `${userUploadsDir}${fileName}`; | ||||
|         const fileTypeOrig = fileName.split(".").pop() ?? ""; | ||||
|         const fileType = normalizeFiletype(fileTypeOrig); | ||||
|         const newFileExt = normalizeOutputFiletype(convertTo); | ||||
|         const newFileName = fileName.replace( | ||||
|           new RegExp(`${fileTypeOrig}(?!.*${fileTypeOrig})`), | ||||
|           newFileExt, | ||||
|         ); | ||||
|         const targetPath = `${userOutputDir}${newFileName}`; | ||||
|  | ||||
|         const result = await mainConverter( | ||||
|           filePath, | ||||
|           fileType, | ||||
|           convertTo, | ||||
|           targetPath, | ||||
|           {}, | ||||
|           converterName, | ||||
|         ); | ||||
|         if (jobId.value) { | ||||
|           query.run(jobId.value, fileName, newFileName, result); | ||||
|         } | ||||
|       }), | ||||
|     ) | ||||
|       .then(() => { | ||||
|         // All conversions are done, update the job status to 'completed' | ||||
|         if (jobId.value) { | ||||
|           db.query("UPDATE jobs SET status = 'completed' WHERE id = ?1").run(jobId.value); | ||||
|         } | ||||
|  | ||||
|         // delete all uploaded files in userUploadsDir | ||||
|         // rmSync(userUploadsDir, { recursive: true, force: true }); | ||||
|       }) | ||||
|       .catch((error) => { | ||||
|         console.error("Error in conversion process:", error); | ||||
|       }); | ||||
|  | ||||
|     // Redirect the client immediately | ||||
|     return redirect(`${WEBROOT}/results/${jobId.value}`, 302); | ||||
|   }, | ||||
|   { | ||||
|     body: t.Object({ | ||||
|       convert_to: t.String(), | ||||
|       file_names: t.String(), | ||||
|     }), | ||||
|   }, | ||||
| ); | ||||
							
								
								
									
										41
									
								
								src/pages/deleteFile.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,41 @@ | ||||
| import { unlink } from "node:fs/promises"; | ||||
| import { Elysia, t } from "elysia"; | ||||
| import { uploadsDir } from ".."; | ||||
| import db from "../db/db"; | ||||
| import { WEBROOT } from "../helpers/env"; | ||||
| import { userService } from "./user"; | ||||
|  | ||||
| export const deleteFile = new Elysia().use(userService).post( | ||||
|   "/delete", | ||||
|   async ({ body, redirect, jwt, cookie: { auth, jobId } }) => { | ||||
|     if (!auth?.value) { | ||||
|       return redirect(`${WEBROOT}/login`, 302); | ||||
|     } | ||||
|  | ||||
|     const user = await jwt.verify(auth.value); | ||||
|     if (!user) { | ||||
|       return redirect(`${WEBROOT}/login`, 302); | ||||
|     } | ||||
|  | ||||
|     if (!jobId?.value) { | ||||
|       return redirect(`${WEBROOT}/`, 302); | ||||
|     } | ||||
|  | ||||
|     const existingJob = await db | ||||
|       .query("SELECT * FROM jobs WHERE id = ? AND user_id = ?") | ||||
|       .get(jobId.value, user.id); | ||||
|  | ||||
|     if (!existingJob) { | ||||
|       return redirect(`${WEBROOT}/`, 302); | ||||
|     } | ||||
|  | ||||
|     const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`; | ||||
|  | ||||
|     await unlink(`${userUploadsDir}${body.filename}`); | ||||
|  | ||||
|     return { | ||||
|       message: "File deleted successfully.", | ||||
|     }; | ||||
|   }, | ||||
|   { body: t.Object({ filename: t.String() }) }, | ||||
| ); | ||||
							
								
								
									
										62
									
								
								src/pages/download.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,62 @@ | ||||
| import { Elysia } from "elysia"; | ||||
| import sanitize from "sanitize-filename"; | ||||
| import { outputDir } from ".."; | ||||
| import db from "../db/db"; | ||||
| import { WEBROOT } from "../helpers/env"; | ||||
| import { userService } from "./user"; | ||||
|  | ||||
| export const download = new Elysia() | ||||
|   .use(userService) | ||||
|   .get( | ||||
|     "/download/:userId/:jobId/:fileName", | ||||
|     async ({ params, jwt, redirect, cookie: { auth } }) => { | ||||
|       if (!auth?.value) { | ||||
|         return redirect(`${WEBROOT}/login`, 302); | ||||
|       } | ||||
|  | ||||
|       const user = await jwt.verify(auth.value); | ||||
|       if (!user) { | ||||
|         return redirect(`${WEBROOT}/login`, 302); | ||||
|       } | ||||
|  | ||||
|       const job = await db | ||||
|         .query("SELECT * FROM jobs WHERE user_id = ? AND id = ?") | ||||
|         .get(user.id, params.jobId); | ||||
|  | ||||
|       if (!job) { | ||||
|         return redirect(`${WEBROOT}/results`, 302); | ||||
|       } | ||||
|       // parse from url encoded string | ||||
|       const userId = decodeURIComponent(params.userId); | ||||
|       const jobId = decodeURIComponent(params.jobId); | ||||
|       const fileName = sanitize(decodeURIComponent(params.fileName)); | ||||
|  | ||||
|       const filePath = `${outputDir}${userId}/${jobId}/${fileName}`; | ||||
|       return Bun.file(filePath); | ||||
|     }, | ||||
|   ) | ||||
|   .get("/zip/:userId/:jobId", async ({ params, jwt, redirect, cookie: { auth } }) => { | ||||
|     // TODO: Implement zip download | ||||
|     if (!auth?.value) { | ||||
|       return redirect(`${WEBROOT}/login`, 302); | ||||
|     } | ||||
|  | ||||
|     const user = await jwt.verify(auth.value); | ||||
|     if (!user) { | ||||
|       return redirect(`${WEBROOT}/login`, 302); | ||||
|     } | ||||
|  | ||||
|     const job = await db | ||||
|       .query("SELECT * FROM jobs WHERE user_id = ? AND id = ?") | ||||
|       .get(user.id, params.jobId); | ||||
|  | ||||
|     if (!job) { | ||||
|       return redirect(`${WEBROOT}/results`, 302); | ||||
|     } | ||||
|  | ||||
|     // const userId = decodeURIComponent(params.userId); | ||||
|     // const jobId = decodeURIComponent(params.jobId); | ||||
|     // const outputPath = `${outputDir}${userId}/`{jobId}/); | ||||
|  | ||||
|     // return Bun.zip(outputPath); | ||||
|   }); | ||||
							
								
								
									
										216
									
								
								src/pages/history.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,216 @@ | ||||
| import { Html } from "@elysiajs/html"; | ||||
| import { Elysia } from "elysia"; | ||||
| import { BaseHtml } from "../components/base"; | ||||
| import { Header } from "../components/header"; | ||||
| import db from "../db/db"; | ||||
| import { Filename, Jobs } from "../db/types"; | ||||
| import { ALLOW_UNAUTHENTICATED, HIDE_HISTORY, WEBROOT } from "../helpers/env"; | ||||
| import { userService } from "./user"; | ||||
|  | ||||
| export const history = new Elysia() | ||||
|   .use(userService) | ||||
|   .get("/history", async ({ jwt, redirect, cookie: { auth } }) => { | ||||
|     if (HIDE_HISTORY) { | ||||
|       return redirect(`${WEBROOT}/`, 302); | ||||
|     } | ||||
|  | ||||
|     if (!auth?.value) { | ||||
|       return redirect(`${WEBROOT}/login`, 302); | ||||
|     } | ||||
|     const user = await jwt.verify(auth.value); | ||||
|  | ||||
|     if (!user) { | ||||
|       return redirect(`${WEBROOT}/login`, 302); | ||||
|     } | ||||
|  | ||||
|     let userJobs = db.query("SELECT * FROM jobs WHERE user_id = ?").as(Jobs).all(user.id).reverse(); | ||||
|  | ||||
|     for (const job of userJobs) { | ||||
|       const files = db.query("SELECT * FROM file_names WHERE job_id = ?").as(Filename).all(job.id); | ||||
|  | ||||
|       job.finished_files = files.length; | ||||
|       job.files_detailed = files; | ||||
|     } | ||||
|  | ||||
|     // filter out jobs with no files | ||||
|     userJobs = userJobs.filter((job) => job.num_files > 0); | ||||
|  | ||||
|     return ( | ||||
|       <BaseHtml webroot={WEBROOT} title="ConvertX | Results"> | ||||
|         <> | ||||
|           <Header | ||||
|             webroot={WEBROOT} | ||||
|             allowUnauthenticated={ALLOW_UNAUTHENTICATED} | ||||
|             hideHistory={HIDE_HISTORY} | ||||
|             loggedIn | ||||
|           /> | ||||
|           <main | ||||
|             class={` | ||||
|               w-full flex-1 px-2 | ||||
|               sm:px-4 | ||||
|             `} | ||||
|           > | ||||
|             <article class="article"> | ||||
|               <h1 class="mb-4 text-xl">Results</h1> | ||||
|               <table | ||||
|                 class={` | ||||
|                   w-full table-auto overflow-y-auto rounded bg-neutral-900 text-left | ||||
|                   [&_td]:p-4 | ||||
|                   [&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800 | ||||
|                 `} | ||||
|               > | ||||
|                 <thead> | ||||
|                   <tr> | ||||
|                     <th | ||||
|                       class={` | ||||
|                         px-2 py-2 | ||||
|                         sm:px-4 | ||||
|                       `} | ||||
|                     > | ||||
|                       <span class="sr-only">Expand details</span> | ||||
|                     </th> | ||||
|                     <th | ||||
|                       class={` | ||||
|                         px-2 py-2 | ||||
|                         sm:px-4 | ||||
|                       `} | ||||
|                     > | ||||
|                       Time | ||||
|                     </th> | ||||
|                     <th | ||||
|                       class={` | ||||
|                         px-2 py-2 | ||||
|                         sm:px-4 | ||||
|                       `} | ||||
|                     > | ||||
|                       Files | ||||
|                     </th> | ||||
|                     <th | ||||
|                       class={` | ||||
|                         px-2 py-2 | ||||
|                         max-sm:hidden | ||||
|                         sm:px-4 | ||||
|                       `} | ||||
|                     > | ||||
|                       Files Done | ||||
|                     </th> | ||||
|                     <th | ||||
|                       class={` | ||||
|                         px-2 py-2 | ||||
|                         sm:px-4 | ||||
|                       `} | ||||
|                     > | ||||
|                       Status | ||||
|                     </th> | ||||
|                     <th | ||||
|                       class={` | ||||
|                         px-2 py-2 | ||||
|                         sm:px-4 | ||||
|                       `} | ||||
|                     > | ||||
|                       View | ||||
|                     </th> | ||||
|                   </tr> | ||||
|                 </thead> | ||||
|                 <tbody> | ||||
|                   {userJobs.map((job) => ( | ||||
|                     <> | ||||
|                       <tr id={`job-row-${job.id}`}> | ||||
|                         <td class="job-details-toggle cursor-pointer" data-job-id={job.id}> | ||||
|                           <svg | ||||
|                             id={`arrow-${job.id}`} | ||||
|                             xmlns="http://www.w3.org/2000/svg" | ||||
|                             fill="none" | ||||
|                             viewBox="0 0 24 24" | ||||
|                             stroke-width="1.5" | ||||
|                             stroke="currentColor" | ||||
|                             class="inline-block h-4 w-4" | ||||
|                           > | ||||
|                             <path | ||||
|                               stroke-linecap="round" | ||||
|                               stroke-linejoin="round" | ||||
|                               d="M8.25 4.5l7.5 7.5-7.5 7.5" | ||||
|                             /> | ||||
|                           </svg> | ||||
|                         </td> | ||||
|                         <td safe>{new Date(job.date_created).toLocaleTimeString()}</td> | ||||
|                         <td>{job.num_files}</td> | ||||
|                         <td class="max-sm:hidden">{job.finished_files}</td> | ||||
|                         <td safe>{job.status}</td> | ||||
|                         <td> | ||||
|                           <a | ||||
|                             class={` | ||||
|                               text-accent-500 underline | ||||
|                               hover:text-accent-400 | ||||
|                             `} | ||||
|                             href={`${WEBROOT}/results/${job.id}`} | ||||
|                           > | ||||
|                             View | ||||
|                           </a> | ||||
|                         </td> | ||||
|                       </tr> | ||||
|                       <tr id={`details-${job.id}`} class="hidden"> | ||||
|                         <td colspan="6"> | ||||
|                           <div class="p-2 text-sm text-neutral-500"> | ||||
|                             <div class="mb-1 font-semibold">Detailed File Information:</div> | ||||
|                             {job.files_detailed.map((file: Filename) => ( | ||||
|                               <div class="flex items-center"> | ||||
|                                 <span class="w-5/12 truncate" title={file.file_name} safe> | ||||
|                                   {file.file_name} | ||||
|                                 </span> | ||||
|                                 <svg | ||||
|                                   xmlns="http://www.w3.org/2000/svg" | ||||
|                                   viewBox="0 0 20 20" | ||||
|                                   fill="currentColor" | ||||
|                                   class="mx-2 inline-block h-4 w-4 text-neutral-500" | ||||
|                                 > | ||||
|                                   <path | ||||
|                                     fill-rule="evenodd" | ||||
|                                     d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z" | ||||
|                                     clip-rule="evenodd" | ||||
|                                   /> | ||||
|                                 </svg> | ||||
|                                 <span class="w-5/12 truncate" title={file.output_file_name} safe> | ||||
|                                   {file.output_file_name} | ||||
|                                 </span> | ||||
|                               </div> | ||||
|                             ))} | ||||
|                           </div> | ||||
|                         </td> | ||||
|                       </tr> | ||||
|                     </> | ||||
|                   ))} | ||||
|                 </tbody> | ||||
|               </table> | ||||
|             </article> | ||||
|           </main> | ||||
|           <script> | ||||
|             {` | ||||
|               document.addEventListener('DOMContentLoaded', () => { | ||||
|                 const toggles = document.querySelectorAll('.job-details-toggle'); | ||||
|                 toggles.forEach(toggle => { | ||||
|                   toggle.addEventListener('click', function() { | ||||
|                     const jobId = this.dataset.jobId; | ||||
|                     const detailsRow = document.getElementById(\`details-\${jobId}\`); | ||||
|                     // The arrow SVG itself has the ID arrow-\${jobId} | ||||
|                     const arrow = document.getElementById(\`arrow-\${jobId}\`); | ||||
|  | ||||
|                     if (detailsRow && arrow) { | ||||
|                       detailsRow.classList.toggle("hidden"); | ||||
|                       if (detailsRow.classList.contains("hidden")) { | ||||
|                         // Right-facing arrow (collapsed) | ||||
|                         arrow.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />'; | ||||
|                       } else { | ||||
|                         // Down-facing arrow (expanded) | ||||
|                         arrow.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />'; | ||||
|                       } | ||||
|                     } | ||||
|                   }); | ||||
|                 }); | ||||
|               }); | ||||
|             `} | ||||
|           </script> | ||||
|         </> | ||||
|       </BaseHtml> | ||||
|     ); | ||||
|   }); | ||||
							
								
								
									
										80
									
								
								src/pages/listConverters.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,80 @@ | ||||
| import { Html } from "@elysiajs/html"; | ||||
| import Elysia from "elysia"; | ||||
| import { BaseHtml } from "../components/base"; | ||||
| import { Header } from "../components/header"; | ||||
| import { getAllInputs, getAllTargets } from "../converters/main"; | ||||
| import { ALLOW_UNAUTHENTICATED, WEBROOT } from "../helpers/env"; | ||||
| import { userService } from "./user"; | ||||
|  | ||||
| export const listConverters = new Elysia() | ||||
|   .use(userService) | ||||
|   .get("/converters", async ({ jwt, redirect, cookie: { auth } }) => { | ||||
|     if (!auth?.value) { | ||||
|       return redirect(`${WEBROOT}/login`, 302); | ||||
|     } | ||||
|  | ||||
|     const user = await jwt.verify(auth.value); | ||||
|     if (!user) { | ||||
|       return redirect(`${WEBROOT}/login`, 302); | ||||
|     } | ||||
|  | ||||
|     return ( | ||||
|       <BaseHtml webroot={WEBROOT} title="ConvertX | Converters"> | ||||
|         <> | ||||
|           <Header webroot={WEBROOT} allowUnauthenticated={ALLOW_UNAUTHENTICATED} loggedIn /> | ||||
|           <main | ||||
|             class={` | ||||
|               w-full flex-1 px-2 | ||||
|               sm:px-4 | ||||
|             `} | ||||
|           > | ||||
|             <article class="article"> | ||||
|               <h1 class="mb-4 text-xl">Converters</h1> | ||||
|               <table | ||||
|                 class={` | ||||
|                   w-full table-auto rounded bg-neutral-900 text-left | ||||
|                   [&_td]:p-4 | ||||
|                   [&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800 | ||||
|                   [&_ul]:list-inside [&_ul]:list-disc | ||||
|                 `} | ||||
|               > | ||||
|                 <thead> | ||||
|                   <tr> | ||||
|                     <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> | ||||
|                   {Object.entries(getAllTargets()).map(([converter, targets]) => { | ||||
|                     const inputs = getAllInputs(converter); | ||||
|                     return ( | ||||
|                       <tr> | ||||
|                         <td safe>{converter}</td> | ||||
|                         <td> | ||||
|                           Count: {inputs.length} | ||||
|                           <ul> | ||||
|                             {inputs.map((input) => ( | ||||
|                               <li safe>{input}</li> | ||||
|                             ))} | ||||
|                           </ul> | ||||
|                         </td> | ||||
|                         <td> | ||||
|                           Count: {targets.length} | ||||
|                           <ul> | ||||
|                             {targets.map((target) => ( | ||||
|                               <li safe>{target}</li> | ||||
|                             ))} | ||||
|                           </ul> | ||||
|                         </td> | ||||
|                       </tr> | ||||
|                     ); | ||||
|                   })} | ||||
|                 </tbody> | ||||
|               </table> | ||||
|             </article> | ||||
|           </main> | ||||
|         </> | ||||
|       </BaseHtml> | ||||
|     ); | ||||
|   }); | ||||
							
								
								
									
										215
									
								
								src/pages/results.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,215 @@ | ||||
| import { Html } from "@elysiajs/html"; | ||||
| import { Elysia } from "elysia"; | ||||
| import { BaseHtml } from "../components/base"; | ||||
| import { Header } from "../components/header"; | ||||
| import db from "../db/db"; | ||||
| import { Filename, Jobs } from "../db/types"; | ||||
| import { ALLOW_UNAUTHENTICATED, WEBROOT } from "../helpers/env"; | ||||
| import { userService } from "./user"; | ||||
|  | ||||
| function ResultsArticle({ | ||||
|   job, | ||||
|   files, | ||||
|   outputPath, | ||||
| }: { | ||||
|   job: Jobs; | ||||
|   files: Filename[]; | ||||
|   outputPath: string; | ||||
| }) { | ||||
|   return ( | ||||
|     <article class="article"> | ||||
|       <div class="mb-4 flex items-center justify-between"> | ||||
|         <h1 class="text-xl">Results</h1> | ||||
|         <div> | ||||
|           <button | ||||
|             type="button" | ||||
|             class="float-right w-40 btn-primary" | ||||
|             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} | ||||
|         class={` | ||||
|           mb-4 inline-block h-2 w-full appearance-none overflow-hidden rounded-full border-0 | ||||
|           bg-neutral-700 bg-none text-accent-500 accent-accent-500 | ||||
|           [&::-moz-progress-bar]:bg-accent-500 [&::-webkit-progress-value]:rounded-full | ||||
|           [&::-webkit-progress-value]:[background:none] | ||||
|           [&[value]::-webkit-progress-value]:bg-accent-500 | ||||
|           [&[value]::-webkit-progress-value]:transition-[inline-size] | ||||
|         `} | ||||
|       /> | ||||
|       <table | ||||
|         class={` | ||||
|           w-full table-auto rounded bg-neutral-900 text-left | ||||
|           [&_td]:p-4 | ||||
|           [&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800 | ||||
|         `} | ||||
|       > | ||||
|         <thead> | ||||
|           <tr> | ||||
|             <th | ||||
|               class={` | ||||
|                 px-2 py-2 | ||||
|                 sm:px-4 | ||||
|               `} | ||||
|             > | ||||
|               Converted File Name | ||||
|             </th> | ||||
|             <th | ||||
|               class={` | ||||
|                 px-2 py-2 | ||||
|                 sm:px-4 | ||||
|               `} | ||||
|             > | ||||
|               Status | ||||
|             </th> | ||||
|             <th | ||||
|               class={` | ||||
|                 px-2 py-2 | ||||
|                 sm:px-4 | ||||
|               `} | ||||
|             > | ||||
|               View | ||||
|             </th> | ||||
|             <th | ||||
|               class={` | ||||
|                 px-2 py-2 | ||||
|                 sm:px-4 | ||||
|               `} | ||||
|             > | ||||
|               Download | ||||
|             </th> | ||||
|           </tr> | ||||
|         </thead> | ||||
|         <tbody> | ||||
|           {files.map((file) => ( | ||||
|             <tr> | ||||
|               <td safe class="max-w-[20vw] truncate"> | ||||
|                 {file.output_file_name} | ||||
|               </td> | ||||
|               <td safe>{file.status}</td> | ||||
|               <td> | ||||
|                 <a | ||||
|                   class={` | ||||
|                     text-accent-500 underline | ||||
|                     hover:text-accent-400 | ||||
|                   `} | ||||
|                   href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`} | ||||
|                 > | ||||
|                   View | ||||
|                 </a> | ||||
|               </td> | ||||
|               <td> | ||||
|                 <a | ||||
|                   class={` | ||||
|                     text-accent-500 underline | ||||
|                     hover:text-accent-400 | ||||
|                   `} | ||||
|                   href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`} | ||||
|                   download={file.output_file_name} | ||||
|                 > | ||||
|                   Download | ||||
|                 </a> | ||||
|               </td> | ||||
|             </tr> | ||||
|           ))} | ||||
|         </tbody> | ||||
|       </table> | ||||
|     </article> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export const results = new Elysia() | ||||
|   .use(userService) | ||||
|   .get("/results/:jobId", async ({ params, jwt, set, redirect, cookie: { auth, job_id } }) => { | ||||
|     if (!auth?.value) { | ||||
|       return redirect(`${WEBROOT}/login`, 302); | ||||
|     } | ||||
|  | ||||
|     if (job_id?.value) { | ||||
|       // clear the job_id cookie since we are viewing the results | ||||
|       job_id.remove(); | ||||
|     } | ||||
|  | ||||
|     const user = await jwt.verify(auth.value); | ||||
|     if (!user) { | ||||
|       return redirect(`${WEBROOT}/login`, 302); | ||||
|     } | ||||
|  | ||||
|     const job = db | ||||
|       .query("SELECT * FROM jobs WHERE user_id = ? AND id = ?") | ||||
|       .as(Jobs) | ||||
|       .get(user.id, params.jobId); | ||||
|  | ||||
|     if (!job) { | ||||
|       set.status = 404; | ||||
|       return { | ||||
|         message: "Job not found.", | ||||
|       }; | ||||
|     } | ||||
|  | ||||
|     const outputPath = `${user.id}/${params.jobId}/`; | ||||
|  | ||||
|     const files = db | ||||
|       .query("SELECT * FROM file_names WHERE job_id = ?") | ||||
|       .as(Filename) | ||||
|       .all(params.jobId); | ||||
|  | ||||
|     return ( | ||||
|       <BaseHtml webroot={WEBROOT} title="ConvertX | Result"> | ||||
|         <> | ||||
|           <Header webroot={WEBROOT} allowUnauthenticated={ALLOW_UNAUTHENTICATED} loggedIn /> | ||||
|           <main | ||||
|             class={` | ||||
|               w-full flex-1 px-2 | ||||
|               sm:px-4 | ||||
|             `} | ||||
|           > | ||||
|             <ResultsArticle job={job} files={files} outputPath={outputPath} /> | ||||
|           </main> | ||||
|           <script src={`${WEBROOT}/results.js`} defer /> | ||||
|         </> | ||||
|       </BaseHtml> | ||||
|     ); | ||||
|   }) | ||||
|   .post("/progress/:jobId", async ({ jwt, set, params, redirect, cookie: { auth, job_id } }) => { | ||||
|     if (!auth?.value) { | ||||
|       return redirect(`${WEBROOT}/login`, 302); | ||||
|     } | ||||
|  | ||||
|     if (job_id?.value) { | ||||
|       // clear the job_id cookie since we are viewing the results | ||||
|       job_id.remove(); | ||||
|     } | ||||
|  | ||||
|     const user = await jwt.verify(auth.value); | ||||
|     if (!user) { | ||||
|       return redirect(`${WEBROOT}/login`, 302); | ||||
|     } | ||||
|  | ||||
|     const job = db | ||||
|       .query("SELECT * FROM jobs WHERE user_id = ? AND id = ?") | ||||
|       .as(Jobs) | ||||
|       .get(user.id, params.jobId); | ||||
|  | ||||
|     if (!job) { | ||||
|       set.status = 404; | ||||
|       return { | ||||
|         message: "Job not found.", | ||||
|       }; | ||||
|     } | ||||
|  | ||||
|     const outputPath = `${user.id}/${params.jobId}/`; | ||||
|  | ||||
|     const files = db | ||||
|       .query("SELECT * FROM file_names WHERE job_id = ?") | ||||
|       .as(Filename) | ||||
|       .all(params.jobId); | ||||
|  | ||||
|     return <ResultsArticle job={job} files={files} outputPath={outputPath} />; | ||||
|   }); | ||||
							
								
								
									
										240
									
								
								src/pages/root.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,240 @@ | ||||
| import { randomInt } from "node:crypto"; | ||||
| import { Html } from "@elysiajs/html"; | ||||
| import { JWTPayloadSpec } from "@elysiajs/jwt"; | ||||
| import { Elysia } from "elysia"; | ||||
| import { BaseHtml } from "../components/base"; | ||||
| import { Header } from "../components/header"; | ||||
| import { getAllTargets } from "../converters/main"; | ||||
| import db from "../db/db"; | ||||
| import { User } from "../db/types"; | ||||
| import { | ||||
|   ACCOUNT_REGISTRATION, | ||||
|   ALLOW_UNAUTHENTICATED, | ||||
|   HIDE_HISTORY, | ||||
|   HTTP_ALLOWED, | ||||
|   WEBROOT, | ||||
| } from "../helpers/env"; | ||||
| import { FIRST_RUN, userService } from "./user"; | ||||
|  | ||||
| export const root = new Elysia() | ||||
|   .use(userService) | ||||
|   .get("/", async ({ jwt, redirect, cookie: { auth, jobId } }) => { | ||||
|     if (!ALLOW_UNAUTHENTICATED) { | ||||
|       if (FIRST_RUN) { | ||||
|         return redirect(`${WEBROOT}/setup`, 302); | ||||
|       } | ||||
|  | ||||
|       if (!auth?.value) { | ||||
|         return redirect(`${WEBROOT}/login`, 302); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // validate jwt | ||||
|     let user: ({ id: string } & JWTPayloadSpec) | false = false; | ||||
|     if (ALLOW_UNAUTHENTICATED) { | ||||
|       const newUserId = String( | ||||
|         randomInt(2 ** 24, Math.min(2 ** 48 + 2 ** 24 - 1, Number.MAX_SAFE_INTEGER)), | ||||
|       ); | ||||
|       const accessToken = await jwt.sign({ | ||||
|         id: newUserId, | ||||
|       }); | ||||
|  | ||||
|       user = { id: newUserId }; | ||||
|       if (!auth) { | ||||
|         return { | ||||
|           message: "No auth cookie, perhaps your browser is blocking cookies.", | ||||
|         }; | ||||
|       } | ||||
|  | ||||
|       // set cookie | ||||
|       auth.set({ | ||||
|         value: accessToken, | ||||
|         httpOnly: true, | ||||
|         secure: !HTTP_ALLOWED, | ||||
|         maxAge: 24 * 60 * 60, | ||||
|         sameSite: "strict", | ||||
|       }); | ||||
|     } else if (auth?.value) { | ||||
|       user = await jwt.verify(auth.value); | ||||
|  | ||||
|       if ( | ||||
|         user !== false && | ||||
|         user.id && | ||||
|         (Number.parseInt(user.id) < 2 ** 24 || !ALLOW_UNAUTHENTICATED) | ||||
|       ) { | ||||
|         // make sure user exists in db | ||||
|         const existingUser = db.query("SELECT * FROM users WHERE id = ?").as(User).get(user.id); | ||||
|  | ||||
|         if (!existingUser) { | ||||
|           if (auth?.value) { | ||||
|             auth.remove(); | ||||
|           } | ||||
|           return redirect(`${WEBROOT}/login`, 302); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     if (!user) { | ||||
|       return redirect(`${WEBROOT}/login`, 302); | ||||
|     } | ||||
|  | ||||
|     // create a new job | ||||
|     db.query("INSERT INTO jobs (user_id, date_created) VALUES (?, ?)").run( | ||||
|       user.id, | ||||
|       new Date().toISOString(), | ||||
|     ); | ||||
|  | ||||
|     const { id } = db | ||||
|       .query("SELECT id FROM jobs WHERE user_id = ? ORDER BY id DESC") | ||||
|       .get(user.id) as { id: number }; | ||||
|  | ||||
|     if (!jobId) { | ||||
|       return { message: "Cookies should be enabled to use this app." }; | ||||
|     } | ||||
|  | ||||
|     jobId.set({ | ||||
|       value: id, | ||||
|       httpOnly: true, | ||||
|       secure: !HTTP_ALLOWED, | ||||
|       maxAge: 24 * 60 * 60, | ||||
|       sameSite: "strict", | ||||
|     }); | ||||
|  | ||||
|     console.log("jobId set to:", id); | ||||
|  | ||||
|     return ( | ||||
|       <BaseHtml webroot={WEBROOT}> | ||||
|         <> | ||||
|           <Header | ||||
|             webroot={WEBROOT} | ||||
|             accountRegistration={ACCOUNT_REGISTRATION} | ||||
|             allowUnauthenticated={ALLOW_UNAUTHENTICATED} | ||||
|             hideHistory={HIDE_HISTORY} | ||||
|             loggedIn | ||||
|           /> | ||||
|           <main | ||||
|             class={` | ||||
|               w-full flex-1 px-2 | ||||
|               sm:px-4 | ||||
|             `} | ||||
|           > | ||||
|             <article class="article"> | ||||
|               <h1 class="mb-4 text-xl">Convert</h1> | ||||
|               <div class="mb-4 scrollbar-thin max-h-[50vh] overflow-y-auto"> | ||||
|                 <table | ||||
|                   id="file-list" | ||||
|                   class={` | ||||
|                     w-full table-auto rounded bg-neutral-900 | ||||
|                     [&_td]:p-4 [&_td]:first:max-w-[30vw] [&_td]:first:truncate | ||||
|                     [&_tr]:rounded-sm [&_tr]:border-b [&_tr]:border-neutral-800 | ||||
|                   `} | ||||
|                 /> | ||||
|               </div> | ||||
|               <div | ||||
|                 id="dropzone" | ||||
|                 class={` | ||||
|                   relative flex h-48 w-full items-center justify-center rounded border border-dashed | ||||
|                   border-neutral-700 transition-all | ||||
|                   hover:border-neutral-600 | ||||
|                   [&.dragover]:border-4 [&.dragover]:border-neutral-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> | ||||
|             </article> | ||||
|             <form | ||||
|               method="post" | ||||
|               action={`${WEBROOT}/convert`} | ||||
|               class="relative mx-auto mb-[35vh] w-full max-w-4xl" | ||||
|             > | ||||
|               <input type="hidden" name="file_names" id="file_names" /> | ||||
|               <article class="article w-full"> | ||||
|                 <input | ||||
|                   type="search" | ||||
|                   name="convert_to_search" | ||||
|                   placeholder="Search for conversions" | ||||
|                   autocomplete="off" | ||||
|                   class="w-full rounded-sm bg-neutral-800 p-4" | ||||
|                 /> | ||||
|                 <div class="select_container relative"> | ||||
|                   <article | ||||
|                     class={` | ||||
|                       convert_to_popup absolute z-2 m-0 hidden h-[30vh] max-h-[50vh] w-full flex-col | ||||
|                       overflow-x-hidden overflow-y-auto rounded bg-neutral-800 | ||||
|                       sm:h-[30vh] | ||||
|                     `} | ||||
|                   > | ||||
|                     {Object.entries(getAllTargets()).map(([converter, targets]) => ( | ||||
|                       <article | ||||
|                         class={` | ||||
|                           convert_to_group flex w-full flex-col border-b border-neutral-700 p-4 | ||||
|                         `} | ||||
|                         data-converter={converter} | ||||
|                       > | ||||
|                         <header class="mb-2 w-full text-xl font-bold" safe> | ||||
|                           {converter} | ||||
|                         </header> | ||||
|                         <ul class="convert_to_target flex flex-row flex-wrap gap-1"> | ||||
|                           {targets.map((target) => ( | ||||
|                             <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 rounded bg-neutral-700 p-1 text-base | ||||
|                                 hover:bg-neutral-600 | ||||
|                               `} | ||||
|                               data-value={`${target},${converter}`} | ||||
|                               data-target={target} | ||||
|                               data-converter={converter} | ||||
|                               type="button" | ||||
|                               safe | ||||
|                             > | ||||
|                               {target} | ||||
|                             </button> | ||||
|                           ))} | ||||
|                         </ul> | ||||
|                       </article> | ||||
|                     ))} | ||||
|                   </article> | ||||
|  | ||||
|                   {/* Hidden element which determines the format to convert the file too and the converter to use */} | ||||
|                   <select name="convert_to" aria-label="Convert to" required hidden> | ||||
|                     <option selected disabled value=""> | ||||
|                       Convert to | ||||
|                     </option> | ||||
|                     {Object.entries(getAllTargets()).map(([converter, targets]) => ( | ||||
|                       <optgroup label={converter}> | ||||
|                         {targets.map((target) => ( | ||||
|                           <option value={`${target},${converter}`} safe> | ||||
|                             {target} | ||||
|                           </option> | ||||
|                         ))} | ||||
|                       </optgroup> | ||||
|                     ))} | ||||
|                   </select> | ||||
|                 </div> | ||||
|               </article> | ||||
|               <input | ||||
|                 class={` | ||||
|                   w-full btn-primary opacity-100 | ||||
|                   disabled:cursor-not-allowed disabled:opacity-50 | ||||
|                 `} | ||||
|                 type="submit" | ||||
|                 value="Convert" | ||||
|                 disabled | ||||
|               /> | ||||
|             </form> | ||||
|           </main> | ||||
|           <script src="script.js" defer /> | ||||
|         </> | ||||
|       </BaseHtml> | ||||
|     ); | ||||
|   }); | ||||
							
								
								
									
										48
									
								
								src/pages/upload.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,48 @@ | ||||
| import { Elysia, t } from "elysia"; | ||||
| import db from "../db/db"; | ||||
| import { WEBROOT } from "../helpers/env"; | ||||
| import { uploadsDir } from "../index"; | ||||
| import { userService } from "./user"; | ||||
|  | ||||
| export const upload = new Elysia().use(userService).post( | ||||
|   "/upload", | ||||
|   async ({ body, redirect, jwt, cookie: { auth, jobId } }) => { | ||||
|     if (!auth?.value) { | ||||
|       return redirect(`${WEBROOT}/login`, 302); | ||||
|     } | ||||
|  | ||||
|     const user = await jwt.verify(auth.value); | ||||
|     if (!user) { | ||||
|       return redirect(`${WEBROOT}/login`, 302); | ||||
|     } | ||||
|  | ||||
|     if (!jobId?.value) { | ||||
|       return redirect(`${WEBROOT}/`, 302); | ||||
|     } | ||||
|  | ||||
|     const existingJob = await db | ||||
|       .query("SELECT * FROM jobs WHERE id = ? AND user_id = ?") | ||||
|       .get(jobId.value, user.id); | ||||
|  | ||||
|     if (!existingJob) { | ||||
|       return redirect(`${WEBROOT}/`, 302); | ||||
|     } | ||||
|  | ||||
|     const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`; | ||||
|  | ||||
|     if (body?.file) { | ||||
|       if (Array.isArray(body.file)) { | ||||
|         for (const file of body.file) { | ||||
|           await Bun.write(`${userUploadsDir}${file.name}`, file); | ||||
|         } | ||||
|       } else { | ||||
|         await Bun.write(`${userUploadsDir}${body.file["name"]}`, body.file); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|       message: "Files uploaded successfully.", | ||||
|     }; | ||||
|   }, | ||||
|   { body: t.Object({ file: t.Files() }) }, | ||||
| ); | ||||
							
								
								
									
										509
									
								
								src/pages/user.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,509 @@ | ||||
| import { randomUUID } from "node:crypto"; | ||||
| import { Html } from "@elysiajs/html"; | ||||
| import { jwt } from "@elysiajs/jwt"; | ||||
| import { Elysia, t } from "elysia"; | ||||
| import { BaseHtml } from "../components/base"; | ||||
| import { Header } from "../components/header"; | ||||
| import db from "../db/db"; | ||||
| import { User } from "../db/types"; | ||||
| import { | ||||
|   ACCOUNT_REGISTRATION, | ||||
|   ALLOW_UNAUTHENTICATED, | ||||
|   HIDE_HISTORY, | ||||
|   HTTP_ALLOWED, | ||||
|   WEBROOT, | ||||
| } from "../helpers/env"; | ||||
|  | ||||
| export let FIRST_RUN = db.query("SELECT * FROM users").get() === null || false; | ||||
|  | ||||
| export const userService = new Elysia({ name: "user/service" }) | ||||
|   .use( | ||||
|     jwt({ | ||||
|       name: "jwt", | ||||
|       schema: t.Object({ | ||||
|         id: t.String(), | ||||
|       }), | ||||
|       secret: process.env.JWT_SECRET ?? randomUUID(), | ||||
|       exp: "7d", | ||||
|     }), | ||||
|   ) | ||||
|   .model({ | ||||
|     signIn: t.Object({ | ||||
|       email: t.String(), | ||||
|       password: t.String(), | ||||
|     }), | ||||
|   }) | ||||
|   .macro({ | ||||
|     isSignIn(enabled: boolean) { | ||||
|       if (!enabled) return; | ||||
|  | ||||
|       return { | ||||
|         async beforeHandle({ status, jwt, cookie: { auth } }) { | ||||
|           if (auth?.value) { | ||||
|             const user = await jwt.verify(auth.value); | ||||
|             return { | ||||
|               success: true, | ||||
|               user, | ||||
|             }; | ||||
|           } | ||||
|  | ||||
|           return status(401, { | ||||
|             success: false, | ||||
|             message: "Unauthorized", | ||||
|           }); | ||||
|         }, | ||||
|       }; | ||||
|     }, | ||||
|   }); | ||||
|  | ||||
| export const user = new Elysia() | ||||
|   .use(userService) | ||||
|   .get("/setup", ({ redirect }) => { | ||||
|     if (!FIRST_RUN) { | ||||
|       return redirect(`${WEBROOT}/login`, 302); | ||||
|     } | ||||
|  | ||||
|     return ( | ||||
|       <BaseHtml title="ConvertX | Setup" webroot={WEBROOT}> | ||||
|         <main | ||||
|           class={` | ||||
|             mx-auto w-full max-w-4xl flex-1 px-2 | ||||
|             sm:px-4 | ||||
|           `} | ||||
|         > | ||||
|           <h1 class="my-8 text-3xl">Welcome to ConvertX!</h1> | ||||
|           <article class="article p-0"> | ||||
|             <header class="w-full bg-neutral-800 p-4">Create your account</header> | ||||
|             <form method="post" action={`${WEBROOT}/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-sm bg-neutral-800 p-3" | ||||
|                     placeholder="Email" | ||||
|                     autocomplete="email" | ||||
|                     required | ||||
|                   /> | ||||
|                 </label> | ||||
|                 <label class="flex flex-col gap-1"> | ||||
|                   Password | ||||
|                   <input | ||||
|                     type="password" | ||||
|                     name="password" | ||||
|                     class="rounded-sm bg-neutral-800 p-3" | ||||
|                     placeholder="Password" | ||||
|                     autocomplete="current-password" | ||||
|                     required | ||||
|                   /> | ||||
|                 </label> | ||||
|               </fieldset> | ||||
|               <input type="submit" value="Create account" class="btn-primary" /> | ||||
|             </form> | ||||
|             <footer class="p-4"> | ||||
|               Report any issues on{" "} | ||||
|               <a | ||||
|                 class={` | ||||
|                   text-accent-500 underline | ||||
|                   hover:text-accent-400 | ||||
|                 `} | ||||
|                 href="https://github.com/C4illin/ConvertX" | ||||
|               > | ||||
|                 GitHub | ||||
|               </a> | ||||
|               . | ||||
|             </footer> | ||||
|           </article> | ||||
|         </main> | ||||
|       </BaseHtml> | ||||
|     ); | ||||
|   }) | ||||
|   .get("/register", ({ redirect }) => { | ||||
|     if (!ACCOUNT_REGISTRATION) { | ||||
|       return redirect(`${WEBROOT}/login`, 302); | ||||
|     } | ||||
|  | ||||
|     return ( | ||||
|       <BaseHtml webroot={WEBROOT} title="ConvertX | Register"> | ||||
|         <> | ||||
|           <Header | ||||
|             webroot={WEBROOT} | ||||
|             accountRegistration={ACCOUNT_REGISTRATION} | ||||
|             allowUnauthenticated={ALLOW_UNAUTHENTICATED} | ||||
|             hideHistory={HIDE_HISTORY} | ||||
|           /> | ||||
|           <main | ||||
|             class={` | ||||
|               w-full flex-1 px-2 | ||||
|               sm:px-4 | ||||
|             `} | ||||
|           > | ||||
|             <article class="article"> | ||||
|               <form method="post" class="flex flex-col gap-4"> | ||||
|                 <fieldset class="mb-4 flex flex-col gap-4"> | ||||
|                   <label class="flex flex-col gap-1"> | ||||
|                     Email | ||||
|                     <input | ||||
|                       type="email" | ||||
|                       name="email" | ||||
|                       class="rounded-sm bg-neutral-800 p-3" | ||||
|                       placeholder="Email" | ||||
|                       autocomplete="email" | ||||
|                       required | ||||
|                     /> | ||||
|                   </label> | ||||
|                   <label class="flex flex-col gap-1"> | ||||
|                     Password | ||||
|                     <input | ||||
|                       type="password" | ||||
|                       name="password" | ||||
|                       class="rounded-sm bg-neutral-800 p-3" | ||||
|                       placeholder="Password" | ||||
|                       autocomplete="current-password" | ||||
|                       required | ||||
|                     /> | ||||
|                   </label> | ||||
|                 </fieldset> | ||||
|                 <input type="submit" value="Register" class="w-full btn-primary" /> | ||||
|               </form> | ||||
|             </article> | ||||
|           </main> | ||||
|         </> | ||||
|       </BaseHtml> | ||||
|     ); | ||||
|   }) | ||||
|   .post( | ||||
|     "/register", | ||||
|     async ({ body: { email, password }, set, redirect, jwt, cookie: { auth } }) => { | ||||
|       if (!ACCOUNT_REGISTRATION && !FIRST_RUN) { | ||||
|         return redirect(`${WEBROOT}/login`, 302); | ||||
|       } | ||||
|  | ||||
|       if (FIRST_RUN) { | ||||
|         FIRST_RUN = false; | ||||
|       } | ||||
|  | ||||
|       const existingUser = await db.query("SELECT * FROM users WHERE email = ?").get(email); | ||||
|       if (existingUser) { | ||||
|         set.status = 400; | ||||
|         return { | ||||
|           message: "Email already in use.", | ||||
|         }; | ||||
|       } | ||||
|       const savedPassword = await Bun.password.hash(password); | ||||
|  | ||||
|       db.query("INSERT INTO users (email, password) VALUES (?, ?)").run(email, savedPassword); | ||||
|  | ||||
|       const user = db.query("SELECT * FROM users WHERE email = ?").as(User).get(email); | ||||
|  | ||||
|       if (!user) { | ||||
|         set.status = 500; | ||||
|         return { | ||||
|           message: "Failed to create user.", | ||||
|         }; | ||||
|       } | ||||
|  | ||||
|       const accessToken = await jwt.sign({ | ||||
|         id: String(user.id), | ||||
|       }); | ||||
|  | ||||
|       if (!auth) { | ||||
|         set.status = 500; | ||||
|         return { | ||||
|           message: "No auth cookie, perhaps your browser is blocking cookies.", | ||||
|         }; | ||||
|       } | ||||
|  | ||||
|       // set cookie | ||||
|       auth.set({ | ||||
|         value: accessToken, | ||||
|         httpOnly: true, | ||||
|         secure: !HTTP_ALLOWED, | ||||
|         maxAge: 60 * 60 * 24 * 7, | ||||
|         sameSite: "strict", | ||||
|       }); | ||||
|  | ||||
|       return redirect(`${WEBROOT}/`, 302); | ||||
|     }, | ||||
|     { body: "signIn" }, | ||||
|   ) | ||||
|   .get("/login", async ({ jwt, redirect, cookie: { auth } }) => { | ||||
|     if (FIRST_RUN) { | ||||
|       return redirect(`${WEBROOT}/setup`, 302); | ||||
|     } | ||||
|  | ||||
|     // if already logged in, redirect to home | ||||
|     if (auth?.value) { | ||||
|       const user = await jwt.verify(auth.value); | ||||
|  | ||||
|       if (user) { | ||||
|         return redirect(`${WEBROOT}/`, 302); | ||||
|       } | ||||
|  | ||||
|       auth.remove(); | ||||
|     } | ||||
|  | ||||
|     return ( | ||||
|       <BaseHtml webroot={WEBROOT} title="ConvertX | Login"> | ||||
|         <> | ||||
|           <Header | ||||
|             webroot={WEBROOT} | ||||
|             accountRegistration={ACCOUNT_REGISTRATION} | ||||
|             allowUnauthenticated={ALLOW_UNAUTHENTICATED} | ||||
|             hideHistory={HIDE_HISTORY} | ||||
|           /> | ||||
|           <main | ||||
|             class={` | ||||
|               w-full flex-1 px-2 | ||||
|               sm:px-4 | ||||
|             `} | ||||
|           > | ||||
|             <article class="article"> | ||||
|               <form method="post" class="flex flex-col gap-4"> | ||||
|                 <fieldset class="mb-4 flex flex-col gap-4"> | ||||
|                   <label class="flex flex-col gap-1"> | ||||
|                     Email | ||||
|                     <input | ||||
|                       type="email" | ||||
|                       name="email" | ||||
|                       class="rounded-sm bg-neutral-800 p-3" | ||||
|                       placeholder="Email" | ||||
|                       autocomplete="email" | ||||
|                       required | ||||
|                     /> | ||||
|                   </label> | ||||
|                   <label class="flex flex-col gap-1"> | ||||
|                     Password | ||||
|                     <input | ||||
|                       type="password" | ||||
|                       name="password" | ||||
|                       class="rounded-sm bg-neutral-800 p-3" | ||||
|                       placeholder="Password" | ||||
|                       autocomplete="current-password" | ||||
|                       required | ||||
|                     /> | ||||
|                   </label> | ||||
|                 </fieldset> | ||||
|                 <div class="flex flex-row gap-4"> | ||||
|                   {ACCOUNT_REGISTRATION ? ( | ||||
|                     <a | ||||
|                       href={`${WEBROOT}/register`} | ||||
|                       role="button" | ||||
|                       class="w-full btn-secondary text-center" | ||||
|                     > | ||||
|                       Register | ||||
|                     </a> | ||||
|                   ) : null} | ||||
|                   <input type="submit" value="Login" class="w-full btn-primary" /> | ||||
|                 </div> | ||||
|               </form> | ||||
|             </article> | ||||
|           </main> | ||||
|         </> | ||||
|       </BaseHtml> | ||||
|     ); | ||||
|   }) | ||||
|   .post( | ||||
|     "/login", | ||||
|     async function handler({ body, set, redirect, jwt, cookie: { auth } }) { | ||||
|       const existingUser = db.query("SELECT * FROM users WHERE email = ?").as(User).get(body.email); | ||||
|  | ||||
|       if (!existingUser) { | ||||
|         set.status = 403; | ||||
|         return { | ||||
|           message: "Invalid credentials.", | ||||
|         }; | ||||
|       } | ||||
|  | ||||
|       const validPassword = await Bun.password.verify(body.password, existingUser.password); | ||||
|  | ||||
|       if (!validPassword) { | ||||
|         set.status = 403; | ||||
|         return { | ||||
|           message: "Invalid credentials.", | ||||
|         }; | ||||
|       } | ||||
|  | ||||
|       const accessToken = await jwt.sign({ | ||||
|         id: String(existingUser.id), | ||||
|       }); | ||||
|  | ||||
|       if (!auth) { | ||||
|         set.status = 500; | ||||
|         return { | ||||
|           message: "No auth cookie, perhaps your browser is blocking cookies.", | ||||
|         }; | ||||
|       } | ||||
|  | ||||
|       // set cookie | ||||
|       auth.set({ | ||||
|         value: accessToken, | ||||
|         httpOnly: true, | ||||
|         secure: !HTTP_ALLOWED, | ||||
|         maxAge: 60 * 60 * 24 * 7, | ||||
|         sameSite: "strict", | ||||
|       }); | ||||
|  | ||||
|       return redirect(`${WEBROOT}/`, 302); | ||||
|     }, | ||||
|     { body: "signIn" }, | ||||
|   ) | ||||
|   .get("/logoff", ({ redirect, cookie: { auth } }) => { | ||||
|     if (auth?.value) { | ||||
|       auth.remove(); | ||||
|     } | ||||
|  | ||||
|     return redirect(`${WEBROOT}/login`, 302); | ||||
|   }) | ||||
|   .post("/logoff", ({ redirect, cookie: { auth } }) => { | ||||
|     if (auth?.value) { | ||||
|       auth.remove(); | ||||
|     } | ||||
|  | ||||
|     return redirect(`${WEBROOT}/login`, 302); | ||||
|   }) | ||||
|   .get("/account", async ({ jwt, redirect, cookie: { auth } }) => { | ||||
|     if (!auth?.value) { | ||||
|       return redirect(`${WEBROOT}/`); | ||||
|     } | ||||
|     const user = await jwt.verify(auth.value); | ||||
|  | ||||
|     if (!user) { | ||||
|       return redirect(`${WEBROOT}/`, 302); | ||||
|     } | ||||
|  | ||||
|     const userData = db.query("SELECT * FROM users WHERE id = ?").as(User).get(user.id); | ||||
|  | ||||
|     if (!userData) { | ||||
|       return redirect(`${WEBROOT}/`, 302); | ||||
|     } | ||||
|  | ||||
|     return ( | ||||
|       <BaseHtml webroot={WEBROOT} title="ConvertX | Account"> | ||||
|         <> | ||||
|           <Header | ||||
|             webroot={WEBROOT} | ||||
|             accountRegistration={ACCOUNT_REGISTRATION} | ||||
|             allowUnauthenticated={ALLOW_UNAUTHENTICATED} | ||||
|             hideHistory={HIDE_HISTORY} | ||||
|             loggedIn | ||||
|           /> | ||||
|           <main | ||||
|             class={` | ||||
|               w-full flex-1 px-2 | ||||
|               sm:px-4 | ||||
|             `} | ||||
|           > | ||||
|             <article class="article"> | ||||
|               <form method="post" class="flex flex-col gap-4"> | ||||
|                 <fieldset class="mb-4 flex flex-col gap-4"> | ||||
|                   <label class="flex flex-col gap-1"> | ||||
|                     Email | ||||
|                     <input | ||||
|                       type="email" | ||||
|                       name="email" | ||||
|                       class="rounded-sm bg-neutral-800 p-3" | ||||
|                       placeholder="Email" | ||||
|                       autocomplete="email" | ||||
|                       value={userData.email} | ||||
|                       required | ||||
|                     /> | ||||
|                   </label> | ||||
|                   <label class="flex flex-col gap-1"> | ||||
|                     Password (leave blank for unchanged) | ||||
|                     <input | ||||
|                       type="password" | ||||
|                       name="newPassword" | ||||
|                       class="rounded-sm bg-neutral-800 p-3" | ||||
|                       placeholder="Password" | ||||
|                       autocomplete="new-password" | ||||
|                     /> | ||||
|                   </label> | ||||
|                   <label class="flex flex-col gap-1"> | ||||
|                     Current Password | ||||
|                     <input | ||||
|                       type="password" | ||||
|                       name="password" | ||||
|                       class="rounded-sm bg-neutral-800 p-3" | ||||
|                       placeholder="Password" | ||||
|                       autocomplete="current-password" | ||||
|                       required | ||||
|                     /> | ||||
|                   </label> | ||||
|                 </fieldset> | ||||
|                 <div role="group"> | ||||
|                   <input type="submit" value="Update" class="w-full btn-primary" /> | ||||
|                 </div> | ||||
|               </form> | ||||
|             </article> | ||||
|           </main> | ||||
|         </> | ||||
|       </BaseHtml> | ||||
|     ); | ||||
|   }) | ||||
|   .post( | ||||
|     "/account", | ||||
|     async function handler({ body, set, redirect, jwt, cookie: { auth } }) { | ||||
|       if (!auth?.value) { | ||||
|         return redirect(`${WEBROOT}/login`, 302); | ||||
|       } | ||||
|  | ||||
|       const user = await jwt.verify(auth.value); | ||||
|       if (!user) { | ||||
|         return redirect(`${WEBROOT}/login`, 302); | ||||
|       } | ||||
|       const existingUser = db.query("SELECT * FROM users WHERE id = ?").as(User).get(user.id); | ||||
|  | ||||
|       if (!existingUser) { | ||||
|         if (auth?.value) { | ||||
|           auth.remove(); | ||||
|         } | ||||
|         return redirect(`${WEBROOT}/login`, 302); | ||||
|       } | ||||
|  | ||||
|       const validPassword = await Bun.password.verify(body.password, existingUser.password); | ||||
|  | ||||
|       if (!validPassword) { | ||||
|         set.status = 403; | ||||
|         return { | ||||
|           message: "Invalid credentials.", | ||||
|         }; | ||||
|       } | ||||
|  | ||||
|       const fields = []; | ||||
|       const values = []; | ||||
|  | ||||
|       if (body.email) { | ||||
|         const existingUser = await db | ||||
|           .query("SELECT id FROM users WHERE email = ?") | ||||
|           .as(User) | ||||
|           .get(body.email); | ||||
|         if (existingUser && existingUser.id.toString() !== user.id) { | ||||
|           set.status = 409; | ||||
|           return { message: "Email already in use." }; | ||||
|         } | ||||
|         fields.push("email"); | ||||
|         values.push(body.email); | ||||
|       } | ||||
|       if (body.newPassword) { | ||||
|         fields.push("password"); | ||||
|         values.push(await Bun.password.hash(body.newPassword)); | ||||
|       } | ||||
|  | ||||
|       if (fields.length > 0) { | ||||
|         db.query( | ||||
|           `UPDATE users SET ${fields.map((field) => `${field}=?`).join(", ")} WHERE id=?`, | ||||
|         ).run(...values, user.id); | ||||
|       } | ||||
|  | ||||
|       return redirect(`${WEBROOT}/`, 302); | ||||
|     }, | ||||
|     { | ||||
|       body: t.Object({ | ||||
|         email: t.MaybeEmpty(t.String()), | ||||
|         newPassword: t.MaybeEmpty(t.String()), | ||||
|         password: t.String(), | ||||
|       }), | ||||
|     }, | ||||
|   ); | ||||
							
								
								
									
										4
									
								
								src/public/pico.lime.min.css
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,194 +0,0 @@ | ||||
| // Select the file input element | ||||
| const fileInput = document.querySelector('input[type="file"]'); | ||||
| const fileNames = []; | ||||
| let fileType; | ||||
|  | ||||
| const selectContainer = document.querySelector("form .select_container"); | ||||
|  | ||||
| const updateSearchBar = () => { | ||||
|   const convertToInput = document.querySelector( | ||||
|     "input[name='convert_to_search']", | ||||
|   ); | ||||
|   const convertToPopup = document.querySelector(".convert_to_popup"); | ||||
|   const convertToGroupElements = document.querySelectorAll(".convert_to_group"); | ||||
|   const convertToGroups = {}; | ||||
|   const convertToElement = document.querySelector("select[name='convert_to']"); | ||||
|  | ||||
|   const showMatching = (search) => { | ||||
|     for (const [targets, groupElement] of Object.values(convertToGroups)) { | ||||
|       let matchingTargetsFound = 0; | ||||
|       for (const target of targets) { | ||||
|         if (target.dataset.target.includes(search)) { | ||||
|           matchingTargetsFound++; | ||||
|           target.hidden = false; | ||||
|         } else { | ||||
|           target.hidden = true; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       if (matchingTargetsFound === 0) { | ||||
|         groupElement.hidden = true; | ||||
|       } else { | ||||
|         groupElement.hidden = false; | ||||
|       } | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   for (const groupElement of convertToGroupElements) { | ||||
|     const groupName = groupElement.dataset.converter; | ||||
|  | ||||
|     const targetElements = groupElement.querySelectorAll(".target"); | ||||
|     const targets = Array.from(targetElements); | ||||
|  | ||||
|     for (const target of targets) { | ||||
|       target.onmousedown = () => { | ||||
|         convertToElement.value = target.dataset.value; | ||||
|         convertToInput.value = `${target.dataset.target} using ${target.dataset.converter}`; | ||||
|         showMatching(""); | ||||
|       }; | ||||
|     } | ||||
|  | ||||
|     convertToGroups[groupName] = [targets, groupElement]; | ||||
|   } | ||||
|  | ||||
|   convertToInput.addEventListener("input", (e) => { | ||||
|     showMatching(e.target.value.toLowerCase()); | ||||
|   }); | ||||
|  | ||||
|   convertToInput.addEventListener("blur", (e) => { | ||||
|     // Keep the popup open even when clicking on a target button | ||||
|     // for a split second to allow the click to go through | ||||
|     if (e?.relatedTarget?.classList?.contains("target")) { | ||||
|       convertToPopup.hidden = true; | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     convertToPopup.hidden = true; | ||||
|   }); | ||||
|  | ||||
|   convertToInput.addEventListener("focus", () => { | ||||
|     convertToPopup.hidden = false; | ||||
|   }); | ||||
| }; | ||||
|  | ||||
| // const convertFromSelect = document.querySelector("select[name='convert_from']"); | ||||
|  | ||||
| // Add a 'change' event listener to the file input element | ||||
| fileInput.addEventListener("change", (e) => { | ||||
|   // console.log(e.target.files); | ||||
|   // Get the selected files from the event target | ||||
|   const files = e.target.files; | ||||
|  | ||||
|   // Select the file-list table | ||||
|   const fileList = document.querySelector("#file-list"); | ||||
|  | ||||
|   // Loop through the selected files | ||||
|   for (const file of files) { | ||||
|     // Create a new table row for each file | ||||
|     const row = document.createElement("tr"); | ||||
|     row.innerHTML = ` | ||||
|       <td>${file.name}</td> | ||||
|       <td>${(file.size / 1024).toFixed(2)} kB</td> | ||||
|       <td><a class="secondary" onclick="deleteRow(this)" style="cursor: pointer">Remove</a></td> | ||||
|     `; | ||||
|  | ||||
|     if (!fileType) { | ||||
|       fileType = file.name.split(".").pop(); | ||||
|       fileInput.setAttribute("accept", `.${fileType}`); | ||||
|       setTitle(); | ||||
|  | ||||
|       // choose the option that matches the file type | ||||
|       // for (const option of convertFromSelect.children) { | ||||
|       //   console.log(option.value); | ||||
|       //   if (option.value === fileType) { | ||||
|       //     option.selected = true; | ||||
|       //   } | ||||
|       // } | ||||
|  | ||||
|       fetch("/conversions", { | ||||
|         method: "POST", | ||||
|         body: JSON.stringify({ fileType: fileType }), | ||||
|         headers: { | ||||
|           "Content-Type": "application/json", | ||||
|         }, | ||||
|       }) | ||||
|         .then((res) => res.text()) | ||||
|         .then((html) => { | ||||
|           selectContainer.innerHTML = html; | ||||
|           updateSearchBar(); | ||||
|         }) | ||||
|         .catch((err) => console.log(err)); | ||||
|     } | ||||
|  | ||||
|     // Append the row to the file-list table | ||||
|     fileList.appendChild(row); | ||||
|  | ||||
|     // Append the file to the hidden input | ||||
|     fileNames.push(file.name); | ||||
|   } | ||||
|  | ||||
|   uploadFiles(files); | ||||
| }); | ||||
|  | ||||
| const setTitle = () => { | ||||
|   const title = document.querySelector("h1"); | ||||
|   title.textContent = `Convert ${fileType ? `.${fileType}` : ""}`; | ||||
| }; | ||||
|  | ||||
| // Add a onclick for the delete button | ||||
| const deleteRow = (target) => { | ||||
|   const filename = target.parentElement.parentElement.children[0].textContent; | ||||
|   const row = target.parentElement.parentElement; | ||||
|   row.remove(); | ||||
|  | ||||
|   // remove from fileNames | ||||
|   const index = fileNames.indexOf(filename); | ||||
|   fileNames.splice(index, 1); | ||||
|  | ||||
|   // if fileNames is empty, reset fileType | ||||
|   if (fileNames.length === 0) { | ||||
|     fileType = null; | ||||
|     fileInput.removeAttribute("accept"); | ||||
|     setTitle(); | ||||
|   } | ||||
|  | ||||
|   fetch("/delete", { | ||||
|     method: "POST", | ||||
|     body: JSON.stringify({ filename: filename }), | ||||
|     headers: { | ||||
|       "Content-Type": "application/json", | ||||
|     }, | ||||
|   }) | ||||
|     .then((res) => res.json()) | ||||
|     .then((data) => { | ||||
|       console.log(data); | ||||
|     }) | ||||
|     .catch((err) => console.log(err)); | ||||
| }; | ||||
|  | ||||
| const uploadFiles = (files) => { | ||||
|   const formData = new FormData(); | ||||
|  | ||||
|   for (const file of files) { | ||||
|     formData.append("file", file, file.name); | ||||
|   } | ||||
|  | ||||
|   fetch("/upload", { | ||||
|     method: "POST", | ||||
|     body: formData, | ||||
|   }) | ||||
|     .then((res) => res.json()) | ||||
|     .then((data) => { | ||||
|       console.log(data); | ||||
|     }) | ||||
|     .catch((err) => console.log(err)); | ||||
| }; | ||||
|  | ||||
| const formConvert = document.querySelector("form[action='/convert']"); | ||||
|  | ||||
| formConvert.addEventListener("submit", (e) => { | ||||
|   const hiddenInput = document.querySelector("input[name='file_names']"); | ||||
|   hiddenInput.value = JSON.stringify(fileNames); | ||||
| }); | ||||
|  | ||||
| updateSearchBar(); | ||||
| @@ -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; | ||||
|   } | ||||
| } | ||||
| @@ -27,4 +27,4 @@ | ||||
|     "noImplicitOverride": true | ||||
|     // "noImplicitReturns": true | ||||
|   } | ||||
| } | ||||
| } | ||||
|   | ||||