mirror of
https://github.com/C4illin/ConvertX.git
synced 2025-11-05 14:35:27 +00:00
Compare commits
1 Commits
v0.5.0
...
feature-po
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8f2cd4e9e |
@@ -1,7 +0,0 @@
|
|||||||
version = 1
|
|
||||||
|
|
||||||
[[analyzers]]
|
|
||||||
name = "javascript"
|
|
||||||
|
|
||||||
[analyzers.meta]
|
|
||||||
environment = ["nodejs"]
|
|
||||||
55
.eslintrc.cjs
Normal file
55
.eslintrc.cjs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
/** @type {import("eslint").Linter.Config} */
|
||||||
|
const config = {
|
||||||
|
root: true,
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
plugins: ["isaacscript", "import"],
|
||||||
|
extends: [
|
||||||
|
"plugin:@typescript-eslint/recommended-type-checked",
|
||||||
|
"plugin:@typescript-eslint/stylistic-type-checked",
|
||||||
|
"plugin:prettier/recommended",
|
||||||
|
],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: "latest",
|
||||||
|
sourceType: "module",
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
project: [
|
||||||
|
"./tsconfig.json",
|
||||||
|
"./cli/tsconfig.eslint.json", // separate eslint config for the CLI since we want to lint and typecheck differently due to template files
|
||||||
|
"./upgrade/tsconfig.json",
|
||||||
|
"./www/tsconfig.json",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
overrides: [
|
||||||
|
// Template files don't have reliable type information
|
||||||
|
{
|
||||||
|
files: ["./cli/template/**/*.{ts,tsx}"],
|
||||||
|
extends: ["plugin:@typescript-eslint/disable-type-checked"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
// These off/not-configured-the-way-we-want lint rules we like & opt into
|
||||||
|
"@typescript-eslint/no-explicit-any": "error",
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{ argsIgnorePattern: "^_", destructuredArrayIgnorePattern: "^_" },
|
||||||
|
],
|
||||||
|
"@typescript-eslint/consistent-type-imports": [
|
||||||
|
"error",
|
||||||
|
{ prefer: "type-imports", fixStyle: "inline-type-imports" },
|
||||||
|
],
|
||||||
|
"import/consistent-type-specifier-style": ["error", "prefer-inline"],
|
||||||
|
|
||||||
|
// For educational purposes we format our comments/jsdoc nicely
|
||||||
|
"isaacscript/complete-sentences-jsdoc": "warn",
|
||||||
|
"isaacscript/format-jsdoc-comments": "warn",
|
||||||
|
|
||||||
|
// These lint rules don't make sense for us but are enabled in the preset configs
|
||||||
|
"@typescript-eslint/no-confusing-void-expression": "off",
|
||||||
|
"@typescript-eslint/restrict-template-expressions": "off",
|
||||||
|
|
||||||
|
// This rule doesn't seem to be working properly
|
||||||
|
"@typescript-eslint/prefer-nullish-coalescing": "off",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = config;
|
||||||
23
.github/dependabot.yml
vendored
Normal file
23
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for all configuration options:
|
||||||
|
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
# Maintain dependencies for npm
|
||||||
|
- package-ecosystem: "npm" # See documentation for possible values
|
||||||
|
versioning-strategy: increase
|
||||||
|
directory: "/" # Location of package manifests
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
# Maintain dependencies for GitHub Actions
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
# Maintain dependencies for Docker
|
||||||
|
- package-ecosystem: "docker"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
28
.github/workflows/bun-dependabot.yml
vendored
Normal file
28
.github/workflows/bun-dependabot.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
name: 'Dependabot: Update bun.lockb'
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "package.json"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-bun-lockb:
|
||||||
|
name: "Update bun.lockb"
|
||||||
|
if: github.actor == 'dependabot[bot]'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: oven-sh/setup-bun@v1
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
ref: ${{ github.event.pull_request.head.ref }}
|
||||||
|
- run: |
|
||||||
|
bun install
|
||||||
|
git add bun.lockb
|
||||||
|
git config --global user.name 'dependabot[bot]'
|
||||||
|
git config --global user.email 'dependabot[bot]@users.noreply.github.com'
|
||||||
|
git commit --amend --no-edit
|
||||||
|
git push --force
|
||||||
3
.github/workflows/docker-publish.yml
vendored
3
.github/workflows/docker-publish.yml
vendored
@@ -58,10 +58,9 @@ jobs:
|
|||||||
# https://github.com/docker/build-push-action
|
# https://github.com/docker/build-push-action
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
id: build-and-push
|
id: build-and-push
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|||||||
25
.github/workflows/release-please.yml
vendored
25
.github/workflows/release-please.yml
vendored
@@ -1,25 +0,0 @@
|
|||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
name: release-please
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release-please:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: googleapis/release-please-action@v4
|
|
||||||
with:
|
|
||||||
# this assumes that you have created a personal access token
|
|
||||||
# (PAT) and configured it as a GitHub action secret named
|
|
||||||
# `MY_RELEASE_PLEASE_TOKEN` (this secret name is not important).
|
|
||||||
token: ${{ secrets.MY_RELEASE_PLEASE_TOKEN }}
|
|
||||||
# token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
# this is a built-in strategy in release-please, see "Action Inputs"
|
|
||||||
# for more options
|
|
||||||
release-type: node
|
|
||||||
21
.github/workflows/remove-docker-tag.yml
vendored
21
.github/workflows/remove-docker-tag.yml
vendored
@@ -1,21 +0,0 @@
|
|||||||
name: Remove Docker Tag
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
remove-docker-tag:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
# Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
|
|
||||||
# (required)
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Remove Docker Tag
|
|
||||||
uses: ArchieAtkinson/remove-dockertag-action@v0.0
|
|
||||||
with:
|
|
||||||
tag_name: master
|
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -47,4 +47,3 @@ package-lock.json
|
|||||||
/db
|
/db
|
||||||
/data
|
/data
|
||||||
/Bruno
|
/Bruno
|
||||||
/tsconfig.tsbuildinfo
|
|
||||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"typescript.tsdk": "node_modules/typescript/lib",
|
|
||||||
"typescript.enablePromptUseWorkspaceTsdk": true
|
|
||||||
}
|
|
||||||
101
CHANGELOG.md
101
CHANGELOG.md
@@ -1,101 +0,0 @@
|
|||||||
# Changelog
|
|
||||||
|
|
||||||
## [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))
|
|
||||||
|
|
||||||
|
|
||||||
### 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))
|
|
||||||
|
|
||||||
## [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))
|
|
||||||
|
|
||||||
## [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)
|
|
||||||
|
|
||||||
## [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))
|
|
||||||
|
|
||||||
## [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))
|
|
||||||
|
|
||||||
## [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)
|
|
||||||
|
|
||||||
## [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)
|
|
||||||
|
|
||||||
## [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)
|
|
||||||
|
|
||||||
## 0.1.0 (2024-05-30)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* 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))
|
|
||||||
@@ -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" ]
|
|
||||||
28
Dockerfile
28
Dockerfile
@@ -1,5 +1,4 @@
|
|||||||
FROM oven/bun:1.1.29-alpine AS base
|
FROM oven/bun:1-debian as base
|
||||||
LABEL org.opencontainers.image.source="https://github.com/C4illin/ConvertX"
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# install dependencies into temp directory
|
# install dependencies into temp directory
|
||||||
@@ -14,12 +13,6 @@ RUN mkdir -p /temp/prod
|
|||||||
COPY package.json bun.lockb /temp/prod/
|
COPY package.json bun.lockb /temp/prod/
|
||||||
RUN cd /temp/prod && bun install --frozen-lockfile --production
|
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
|
|
||||||
|
|
||||||
# copy node_modules from temp directory
|
# copy node_modules from temp directory
|
||||||
# then copy all (non-ignored) project files into the image
|
# then copy all (non-ignored) project files into the image
|
||||||
# FROM base AS prerelease
|
# FROM base AS prerelease
|
||||||
@@ -38,28 +31,21 @@ LABEL description="ConvertX: self-hosted online file converter supporting 700+ f
|
|||||||
LABEL repo="https://github.com/C4illin/ConvertX"
|
LABEL repo="https://github.com/C4illin/ConvertX"
|
||||||
|
|
||||||
# install additional dependencies
|
# install additional dependencies
|
||||||
RUN apk --no-cache add \
|
RUN rm -rf /var/lib/apt/lists/partial && apt-get update -o Acquire::CompressionTypes::Order::=gz \
|
||||||
|
&& apt-get install -y \
|
||||||
pandoc \
|
pandoc \
|
||||||
texlive \
|
texlive-latex-recommended \
|
||||||
texlive-xetex \
|
texlive-fonts-recommended \
|
||||||
texmf-dist-latexextra \
|
texlive-latex-extra \
|
||||||
ffmpeg \
|
ffmpeg \
|
||||||
graphicsmagick \
|
graphicsmagick \
|
||||||
ghostscript \
|
ghostscript \
|
||||||
vips-tools \
|
libvips-tools
|
||||||
vips-poppler \
|
|
||||||
vips-jxl \
|
|
||||||
libjxl-tools
|
|
||||||
|
|
||||||
# this might be needed for some latex use cases, will add it if needed.
|
|
||||||
# texmf-dist-fontsextra \
|
|
||||||
|
|
||||||
COPY --from=install /temp/prod/node_modules node_modules
|
COPY --from=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/src/index.tsx /app/src/
|
||||||
# COPY --from=prerelease /app/package.json .
|
# COPY --from=prerelease /app/package.json .
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
EXPOSE 3000/tcp
|
EXPOSE 3000/tcp
|
||||||
ENV NODE_ENV=production
|
|
||||||
ENTRYPOINT [ "bun", "run", "./src/index.tsx" ]
|
ENTRYPOINT [ "bun", "run", "./src/index.tsx" ]
|
||||||
57
README.md
57
README.md
@@ -1,13 +1,8 @@
|
|||||||

|

|
||||||
# ConvertX
|
# ConvertX
|
||||||
[](https://github.com/C4illin/ConvertX/actions/workflows/docker-publish.yml)
|
[](https://github.com/C4illin/ConvertX/actions/workflows/docker-publish.yml)
|
||||||
[](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 self-hosted online file converter. Supports 831 different formats. Written with Typescript, Bun and Elysia.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -18,45 +13,37 @@ A self-hosted online file converter. Supports 831 different formats. Written wit
|
|||||||
## Converters supported
|
## Converters supported
|
||||||
|
|
||||||
| Converter | Use case | Converts from | Converts to |
|
| Converter | Use case | Converts from | Converts to |
|
||||||
|------------------------------------------------------------------------------|---------------|---------------|-------------|
|
|----------------|---------------|---------------|-------------|
|
||||||
| [libjxl](https://github.com/libjxl/libjxl) | JPEG XL | 11 | 11 |
|
| Vips | Images (fast) | 45 | 23 |
|
||||||
| [resvg](https://github.com/RazrFalcon/resvg) | SVG | 1 | 1 |
|
| PDFLaTeX | Documents | 1 | 1 |
|
||||||
| [Vips](https://github.com/libvips/libvips) | Images | 45 | 23 |
|
| Pandoc | Documents | 43 | 65 |
|
||||||
| [XeLaTeX](https://tug.org/xetex/) | LaTeX | 1 | 1 |
|
| GraphicsMagick | Images | 166 | 133 |
|
||||||
| [Pandoc](https://pandoc.org/) | Documents | 43 | 65 |
|
| FFmpeg | Video | ~473 | ~280 |
|
||||||
| [GraphicsMagick](http://www.graphicsmagick.org/) | Images | 166 | 133 |
|
|
||||||
| [FFmpeg](https://ffmpeg.org/) | Video | ~473 | ~280 |
|
|
||||||
|
|
||||||
<!-- many ffmpeg fileformats are duplicates -->
|
<!-- many ffmpeg fileformats are duplicates -->
|
||||||
|
|
||||||
Any missing converter? Open an issue or pull request!
|
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
```yml
|
```yml
|
||||||
# docker-compose.yml
|
# docker-compose.yml
|
||||||
services:
|
services:
|
||||||
convertx:
|
convertx:
|
||||||
image: ghcr.io/c4illin/convertx
|
image: ghcr.io/c4illin/convertx:main
|
||||||
container_name: convertx
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
environment: # Defaults are listed below. All are optional.
|
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)
|
- 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
|
- JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234 # will use randomUUID() by default
|
||||||
- HTTP_ALLOWED=false # setting this to true is unsafe, only set this to true locally
|
- HTTP_ALLOWED=false # setting this to true is unsafe, only set this to true locally
|
||||||
- ALLOW_UNAUTHENTICATED=false # allows anyone to use the service without logging in, only set this to true locally
|
|
||||||
- AUTO_DELETE_EVERY_N_HOURS=24 # checks every n hours for files older then n hours and deletes them, set to 0 to disable
|
|
||||||
volumes:
|
volumes:
|
||||||
- convertx:/app/data
|
- convertx:/app/data
|
||||||
```
|
```
|
||||||
|
|
||||||
or
|
<!-- or
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -p 3000:3000 -v ./data:/app/data ghcr.io/c4illin/convertx
|
docker run ghcr.io/c4illin/convertx:master -p 3000:3000 -e ACCOUNT_REGISTRATION=false -v /path/you/want:/app/data
|
||||||
```
|
``` -->
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
@@ -66,31 +53,13 @@ If you get unable to open database file run `chown -R $USER:$USER path` on the p
|
|||||||
|
|
||||||
Tutorial in french: https://belginux.com/installer-convertx-avec-docker/
|
Tutorial in french: https://belginux.com/installer-convertx-avec-docker/
|
||||||
|
|
||||||
## Screenshots
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
0. Install [Bun](https://bun.sh/) and Git
|
|
||||||
1. Clone the repository
|
|
||||||
2. `bun install`
|
|
||||||
3. `bun run dev`
|
|
||||||
|
|
||||||
Pull requests are welcome! See below and open issues for the list of todos.
|
|
||||||
|
|
||||||
## Todo
|
## Todo
|
||||||
- [x] Add messages for errors in converters
|
- [x] Add messages for errors in converters
|
||||||
- [x] Add searchable list of formats
|
|
||||||
- [ ] Add options for converters
|
- [ ] Add options for converters
|
||||||
|
- [ ] Add more converters
|
||||||
- [ ] Divide index.tsx into smaller components
|
- [ ] Divide index.tsx into smaller components
|
||||||
- [ ] Add tests
|
- [ ] Add tests
|
||||||
- [ ] Make the upload button nicer and more easy to drop files on. Support copy paste as well if possible.
|
- [ ] Add searchable list of formats
|
||||||
- [ ] 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
|
## Contributors
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
"$schema": "https://biomejs.dev/schemas/1.7.3/schema.json",
|
||||||
"formatter": {
|
"formatter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"formatWithErrors": true,
|
"formatWithErrors": true,
|
||||||
@@ -9,9 +9,6 @@
|
|||||||
"lineWidth": 80,
|
"lineWidth": 80,
|
||||||
"attributePosition": "auto"
|
"attributePosition": "auto"
|
||||||
},
|
},
|
||||||
"files": {
|
|
||||||
"ignore": ["**/node_modules/**", "**/pico.lime.min.css"]
|
|
||||||
},
|
|
||||||
"organizeImports": { "enabled": true },
|
"organizeImports": { "enabled": true },
|
||||||
"linter": {
|
"linter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
|||||||
@@ -2,12 +2,10 @@ services:
|
|||||||
convertx:
|
convertx:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
# dockerfile: Debian.Dockerfile
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
environment:
|
environment:
|
||||||
- ACCOUNT_REGISTRATION=true
|
- ACCOUNT_REGISTRATION=true
|
||||||
- JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234
|
- JWT_SECRET=aLongAndSecretStringUsedToSignTheJSONWebToken1234
|
||||||
- ALLOW_UNAUTHENTICATED=true
|
|
||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 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"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 52 KiB |
59
package.json
59
package.json
@@ -1,57 +1,40 @@
|
|||||||
{
|
{
|
||||||
"name": "convertx-frontend",
|
"name": "convertx-frontend",
|
||||||
"version": "0.5.0",
|
"version": "1.0.50",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run --watch src/index.tsx",
|
"dev": "bun run --watch src/index.tsx",
|
||||||
"hot": "bun run --hot src/index.tsx",
|
"hot": "bun run --hot src/index.tsx",
|
||||||
"format": "biome format --write ./src",
|
"format": "biome format --write ./src",
|
||||||
"css": "cpy 'node_modules/@picocss/pico/css/pico.lime.min.css' 'src/public/' --flat",
|
"css": "cpy 'node_modules/@picocss/pico/css/pico.lime.min.css' 'src/public/' --flat"
|
||||||
"lint": "run-p 'lint:*'",
|
|
||||||
"lint:tsc": "tsc --noEmit",
|
|
||||||
"lint:knip": "knip",
|
|
||||||
"lint:biome": "biome lint --error-on-warnings ./src"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@elysiajs/cookie": "^0.8.0",
|
"@elysiajs/cookie": "^0.8.0",
|
||||||
"@elysiajs/html": "1.0.2",
|
"@elysiajs/html": "^1.0.2",
|
||||||
"@elysiajs/jwt": "^1.1.1",
|
"@elysiajs/jwt": "^1.0.2",
|
||||||
"@elysiajs/static": "1.0.3",
|
"@elysiajs/static": "^1.0.3",
|
||||||
"elysia": "^1.1.12"
|
"elysia": "^1.0.22",
|
||||||
|
"node-poppler": "^7.2.0"
|
||||||
},
|
},
|
||||||
"module": "src/index.tsx",
|
"module": "src/index.tsx",
|
||||||
"type": "module",
|
|
||||||
"bun-create": {
|
"bun-create": {
|
||||||
"start": "bun run src/index.tsx"
|
"start": "bun run src/index.tsx"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.9.2",
|
"@biomejs/biome": "1.7.3",
|
||||||
"@eslint/compat": "^1.1.1",
|
"@ianvs/prettier-plugin-sort-imports": "^4.2.1",
|
||||||
"@eslint/js": "^9.9.1",
|
"@kitajs/ts-html-plugin": "^4.0.1",
|
||||||
"@ianvs/prettier-plugin-sort-imports": "^4.3.1",
|
|
||||||
"@kitajs/ts-html-plugin": "^4.0.2",
|
|
||||||
"@picocss/pico": "^2.0.6",
|
"@picocss/pico": "^2.0.6",
|
||||||
"@total-typescript/ts-reset": "^0.6.1",
|
"@total-typescript/ts-reset": "^0.5.1",
|
||||||
"@types/bun": "^1.1.8",
|
"@types/bun": "^1.1.3",
|
||||||
"@types/eslint": "^9.6.1",
|
"@types/eslint": "^8.56.10",
|
||||||
"@types/node": "^22.5.4",
|
"@types/node": "^20.12.13",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.4.0",
|
"@types/ws": "^8.5.10",
|
||||||
"@typescript-eslint/parser": "^8.4.0",
|
"@typescript-eslint/eslint-plugin": "^7.11.0",
|
||||||
|
"@typescript-eslint/parser": "^7.11.0",
|
||||||
"cpy-cli": "^5.0.0",
|
"cpy-cli": "^5.0.0",
|
||||||
"eslint": "^9.9.1",
|
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-deprecation": "^3.0.0",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-eslint-comments": "^3.2.0",
|
"prettier": "^3.2.5",
|
||||||
"eslint-plugin-import": "^2.30.0",
|
"typescript": "^5.4.5"
|
||||||
"eslint-plugin-isaacscript": "^4.0.0",
|
}
|
||||||
"eslint-plugin-prettier": "^5.2.1",
|
|
||||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
||||||
"knip": "^5.29.2",
|
|
||||||
"npm-run-all2": "^6.2.2",
|
|
||||||
"prettier": "^3.3.3",
|
|
||||||
"typescript": "^5.5.4",
|
|
||||||
"typescript-eslint": "^8.4.0"
|
|
||||||
},
|
|
||||||
"trustedDependencies": [
|
|
||||||
"@biomejs/biome"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
|
||||||
"extends": [
|
|
||||||
"config:recommended"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,4 @@
|
|||||||
export const BaseHtml = ({
|
export const BaseHtml = ({ children, title = "ConvertX" }) => (
|
||||||
children,
|
|
||||||
title = "ConvertX",
|
|
||||||
}: { children: JSX.Element; title?: string }) => (
|
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export const Header = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header class="container">
|
<header className="container">
|
||||||
<nav>
|
<nav>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
|
|||||||
@@ -260,7 +260,6 @@ export const properties = {
|
|||||||
"mpegts",
|
"mpegts",
|
||||||
"mpegtsraw",
|
"mpegtsraw",
|
||||||
"mpegvideo",
|
"mpegvideo",
|
||||||
"mpg",
|
|
||||||
"mpjpeg",
|
"mpjpeg",
|
||||||
"mpl2",
|
"mpl2",
|
||||||
"mpo",
|
"mpo",
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
import { exec } from "node:child_process";
|
|
||||||
|
|
||||||
// declare possible conversions
|
|
||||||
export const properties = {
|
|
||||||
from: {
|
|
||||||
jxl: ["jxl"],
|
|
||||||
images: [
|
|
||||||
"apng",
|
|
||||||
"exr",
|
|
||||||
"gif",
|
|
||||||
"jpeg",
|
|
||||||
"pam",
|
|
||||||
"pfm",
|
|
||||||
"pgm",
|
|
||||||
"pgx",
|
|
||||||
"png",
|
|
||||||
"ppm",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
to: {
|
|
||||||
jxl: [
|
|
||||||
"apng",
|
|
||||||
"exr",
|
|
||||||
"gif",
|
|
||||||
"jpeg",
|
|
||||||
"pam",
|
|
||||||
"pfm",
|
|
||||||
"pgm",
|
|
||||||
"pgx",
|
|
||||||
"png",
|
|
||||||
"ppm",
|
|
||||||
],
|
|
||||||
images: ["jxl"],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export function convert(
|
|
||||||
filePath: string,
|
|
||||||
fileType: string,
|
|
||||||
convertTo: string,
|
|
||||||
targetPath: string,
|
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
|
||||||
options?: any,
|
|
||||||
): Promise<string> {
|
|
||||||
let tool = "";
|
|
||||||
if (fileType === "jxl") {
|
|
||||||
tool = "djxl";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (convertTo === "jxl") {
|
|
||||||
tool = "cjxl";
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
exec(`${tool} "${filePath}" "${targetPath}"`, (error, stdout, stderr) => {
|
|
||||||
if (error) {
|
|
||||||
reject(`error: ${error}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stdout) {
|
|
||||||
console.log(`stdout: ${stdout}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stderr) {
|
|
||||||
console.error(`stderr: ${stderr}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve("success");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -16,19 +16,11 @@ import {
|
|||||||
} from "./graphicsmagick";
|
} from "./graphicsmagick";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
convert as convertxelatex,
|
convert as convertPdflatex,
|
||||||
properties as propertiesxelatex,
|
properties as propertiesPdflatex,
|
||||||
} from "./xelatex";
|
} from "./pdflatex";
|
||||||
|
|
||||||
import {
|
import { convert as convertPoppler, properties as propertiesPoppler } from "./poppler";
|
||||||
convert as convertLibjxl,
|
|
||||||
properties as propertiesLibjxl,
|
|
||||||
} from "./libjxl";
|
|
||||||
|
|
||||||
import {
|
|
||||||
convert as convertresvg,
|
|
||||||
properties as propertiesresvg,
|
|
||||||
} from "./resvg";
|
|
||||||
|
|
||||||
import { normalizeFiletype } from "../helpers/normalizeFiletype";
|
import { normalizeFiletype } from "../helpers/normalizeFiletype";
|
||||||
|
|
||||||
@@ -60,21 +52,17 @@ const properties: {
|
|||||||
) => any;
|
) => any;
|
||||||
};
|
};
|
||||||
} = {
|
} = {
|
||||||
libjxl: {
|
|
||||||
properties: propertiesLibjxl,
|
|
||||||
converter: convertLibjxl,
|
|
||||||
},
|
|
||||||
resvg: {
|
|
||||||
properties: propertiesresvg,
|
|
||||||
converter: convertresvg,
|
|
||||||
},
|
|
||||||
vips: {
|
vips: {
|
||||||
properties: propertiesImage,
|
properties: propertiesImage,
|
||||||
converter: convertImage,
|
converter: convertImage,
|
||||||
},
|
},
|
||||||
xelatex: {
|
pdflatex: {
|
||||||
properties: propertiesxelatex,
|
properties: propertiesPdflatex,
|
||||||
converter: convertxelatex,
|
converter: convertPdflatex,
|
||||||
|
},
|
||||||
|
poppler: {
|
||||||
|
properties: propertiesPoppler,
|
||||||
|
converter: convertPoppler,
|
||||||
},
|
},
|
||||||
pandoc: {
|
pandoc: {
|
||||||
properties: propertiesPandoc,
|
properties: propertiesPandoc,
|
||||||
@@ -120,8 +108,9 @@ export async function mainConverter(
|
|||||||
|
|
||||||
for (const key in converterObj.properties.from) {
|
for (const key in converterObj.properties.from) {
|
||||||
if (
|
if (
|
||||||
converterObj?.properties?.from[key]?.includes(fileType) &&
|
// HOW??
|
||||||
converterObj?.properties?.to[key]?.includes(convertTo)
|
converterObj.properties.from[key].includes(fileType) &&
|
||||||
|
converterObj.properties.to[key].includes(convertTo)
|
||||||
) {
|
) {
|
||||||
converterFunc = converterObj.converter;
|
converterFunc = converterObj.converter;
|
||||||
break;
|
break;
|
||||||
@@ -210,7 +199,7 @@ for (const converterName in properties) {
|
|||||||
}
|
}
|
||||||
possibleInputs.sort();
|
possibleInputs.sort();
|
||||||
|
|
||||||
const getPossibleInputs = () => {
|
export const getPossibleInputs = () => {
|
||||||
return possibleInputs;
|
return possibleInputs;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -225,9 +214,9 @@ for (const converterName in properties) {
|
|||||||
|
|
||||||
for (const key in converterProperties.to) {
|
for (const key in converterProperties.to) {
|
||||||
if (allTargets[converterName]) {
|
if (allTargets[converterName]) {
|
||||||
allTargets[converterName].push(...(converterProperties.to[key] || []));
|
allTargets[converterName].push(...converterProperties.to[key]);
|
||||||
} else {
|
} else {
|
||||||
allTargets[converterName] = converterProperties.to[key] || [];
|
allTargets[converterName] = converterProperties.to[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -246,9 +235,9 @@ for (const converterName in properties) {
|
|||||||
|
|
||||||
for (const key in converterProperties.from) {
|
for (const key in converterProperties.from) {
|
||||||
if (allInputs[converterName]) {
|
if (allInputs[converterName]) {
|
||||||
allInputs[converterName].push(...(converterProperties.from[key] || []));
|
allInputs[converterName].push(...converterProperties.from[key]);
|
||||||
} else {
|
} else {
|
||||||
allInputs[converterName] = converterProperties.from[key] || [];
|
allInputs[converterName] = converterProperties.from[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
119
src/converters/old.sharp.ts
Normal file
119
src/converters/old.sharp.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import sharp from "sharp";
|
||||||
|
import type { FormatEnum } from "sharp";
|
||||||
|
|
||||||
|
// declare possible conversions
|
||||||
|
export const properties = {
|
||||||
|
from: {
|
||||||
|
images: [
|
||||||
|
"avif",
|
||||||
|
"bif",
|
||||||
|
"csv",
|
||||||
|
"exr",
|
||||||
|
"fits",
|
||||||
|
"gif",
|
||||||
|
"hdr.gz",
|
||||||
|
"hdr",
|
||||||
|
"heic",
|
||||||
|
"heif",
|
||||||
|
"img.gz",
|
||||||
|
"img",
|
||||||
|
"j2c",
|
||||||
|
"j2k",
|
||||||
|
"jp2",
|
||||||
|
"jpeg",
|
||||||
|
"jpx",
|
||||||
|
"jxl",
|
||||||
|
"mat",
|
||||||
|
"mrxs",
|
||||||
|
"ndpi",
|
||||||
|
"nia.gz",
|
||||||
|
"nia",
|
||||||
|
"nii.gz",
|
||||||
|
"nii",
|
||||||
|
"pdf",
|
||||||
|
"pfm",
|
||||||
|
"pgm",
|
||||||
|
"pic",
|
||||||
|
"png",
|
||||||
|
"ppm",
|
||||||
|
"raw",
|
||||||
|
"scn",
|
||||||
|
"svg",
|
||||||
|
"svs",
|
||||||
|
"svslide",
|
||||||
|
"szi",
|
||||||
|
"tif",
|
||||||
|
"tiff",
|
||||||
|
"v",
|
||||||
|
"vips",
|
||||||
|
"vms",
|
||||||
|
"vmu",
|
||||||
|
"webp",
|
||||||
|
"zip",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
images: [
|
||||||
|
"avif",
|
||||||
|
"dzi",
|
||||||
|
"fits",
|
||||||
|
"gif",
|
||||||
|
"hdr.gz",
|
||||||
|
"heic",
|
||||||
|
"heif",
|
||||||
|
"img.gz",
|
||||||
|
"j2c",
|
||||||
|
"j2k",
|
||||||
|
"jp2",
|
||||||
|
"jpeg",
|
||||||
|
"jpx",
|
||||||
|
"jxl",
|
||||||
|
"mat",
|
||||||
|
"nia.gz",
|
||||||
|
"nia",
|
||||||
|
"nii.gz",
|
||||||
|
"nii",
|
||||||
|
"png",
|
||||||
|
"tiff",
|
||||||
|
"vips",
|
||||||
|
"webp",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
svg: {
|
||||||
|
scale: {
|
||||||
|
description: "Scale the image up or down",
|
||||||
|
type: "number",
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function convert(
|
||||||
|
filePath: string,
|
||||||
|
fileType: string,
|
||||||
|
convertTo: keyof FormatEnum,
|
||||||
|
targetPath: string,
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||||
|
options?: any,
|
||||||
|
) {
|
||||||
|
if (fileType === "svg") {
|
||||||
|
const scale = options.scale || 1;
|
||||||
|
const metadata = await sharp(filePath).metadata();
|
||||||
|
|
||||||
|
if (!metadata || !metadata.width || !metadata.height) {
|
||||||
|
throw new Error("Could not get metadata from image");
|
||||||
|
}
|
||||||
|
|
||||||
|
const newWidth = Math.round(metadata.width * scale);
|
||||||
|
const newHeight = Math.round(metadata.height * scale);
|
||||||
|
|
||||||
|
return await sharp(filePath)
|
||||||
|
.resize(newWidth, newHeight)
|
||||||
|
.toFormat(convertTo)
|
||||||
|
.toFile(targetPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await sharp(filePath).toFormat(convertTo).toFile(targetPath);
|
||||||
|
}
|
||||||
@@ -127,15 +127,9 @@ export function convert(
|
|||||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||||
options?: any,
|
options?: any,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
// set xelatex here
|
|
||||||
const xelatex = ["pdf", "latex"];
|
|
||||||
let option = "";
|
|
||||||
if (xelatex.includes(convertTo)) {
|
|
||||||
option = "--pdf-engine=xelatex";
|
|
||||||
}
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
exec(
|
exec(
|
||||||
`pandoc ${option} "${filePath}" -f ${fileType} -t ${convertTo} -o "${targetPath}"`,
|
`pandoc "${filePath}" -f ${fileType} -t ${convertTo} -o "${targetPath}"`,
|
||||||
(error, stdout, stderr) => {
|
(error, stdout, stderr) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
reject(`error: ${error}`);
|
reject(`error: ${error}`);
|
||||||
|
|||||||
@@ -19,13 +19,9 @@ export function convert(
|
|||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// const fileName: string = (targetPath.split("/").pop() as string).replace(".pdf", "")
|
// const fileName: string = (targetPath.split("/").pop() as string).replace(".pdf", "")
|
||||||
const outputPath = targetPath
|
const outputPath = targetPath.split("/").slice(0, -1).join("/").replace("./", "")
|
||||||
.split("/")
|
|
||||||
.slice(0, -1)
|
|
||||||
.join("/")
|
|
||||||
.replace("./", "");
|
|
||||||
exec(
|
exec(
|
||||||
`latexmk -xelatex -interaction=nonstopmode -output-directory="${outputPath}" "${filePath}"`,
|
`pdflatex -interaction=nonstopmode -output-directory="${outputPath}" "${filePath}"`,
|
||||||
(error, stdout, stderr) => {
|
(error, stdout, stderr) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
reject(`error: ${error}`);
|
reject(`error: ${error}`);
|
||||||
115
src/converters/poppler.ts
Normal file
115
src/converters/poppler.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
const { Poppler } = require("node-poppler");
|
||||||
|
const poppler = new Poppler();
|
||||||
|
|
||||||
|
export const properties = {
|
||||||
|
from: {
|
||||||
|
text: ["pdf"],
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
text: [
|
||||||
|
"jpeg",
|
||||||
|
"png",
|
||||||
|
"tiff",
|
||||||
|
"eps",
|
||||||
|
"icc",
|
||||||
|
"pdf",
|
||||||
|
"svg",
|
||||||
|
"ps",
|
||||||
|
"html",
|
||||||
|
"text",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function convert(
|
||||||
|
filePath: string,
|
||||||
|
fileType: string,
|
||||||
|
convertTo: string,
|
||||||
|
targetPath: string,
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||||
|
options?: any,
|
||||||
|
): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const cairoFiles = [
|
||||||
|
"jpeg",
|
||||||
|
"png",
|
||||||
|
"tiff",
|
||||||
|
"eps",
|
||||||
|
"icc",
|
||||||
|
"pdf",
|
||||||
|
"svg",
|
||||||
|
"ps",
|
||||||
|
];
|
||||||
|
|
||||||
|
if (cairoFiles.includes(convertTo)) {
|
||||||
|
const popplerOptions: {
|
||||||
|
jpegFile?: boolean;
|
||||||
|
pngFile?: boolean;
|
||||||
|
tiffFile?: boolean;
|
||||||
|
epsFile?: boolean;
|
||||||
|
iccFile?: boolean;
|
||||||
|
pdfFile?: boolean;
|
||||||
|
svgFile?: boolean;
|
||||||
|
psFile?: boolean;
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
switch (convertTo) {
|
||||||
|
case "jpeg":
|
||||||
|
popplerOptions.jpegFile = true;
|
||||||
|
break;
|
||||||
|
case "png":
|
||||||
|
popplerOptions.pngFile = true;
|
||||||
|
break;
|
||||||
|
case "tiff":
|
||||||
|
popplerOptions.tiffFile = true;
|
||||||
|
break;
|
||||||
|
case "eps":
|
||||||
|
popplerOptions.epsFile = true;
|
||||||
|
break;
|
||||||
|
case "icc":
|
||||||
|
popplerOptions.iccFile = true;
|
||||||
|
break;
|
||||||
|
case "pdf":
|
||||||
|
popplerOptions.pdfFile = true;
|
||||||
|
break;
|
||||||
|
case "svg":
|
||||||
|
popplerOptions.svgFile = true;
|
||||||
|
break;
|
||||||
|
case "ps":
|
||||||
|
popplerOptions.psFile = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
reject(`Invalid convertTo option: ${convertTo}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
poppler
|
||||||
|
.pdfToCairo(filePath, targetPath, popplerOptions)
|
||||||
|
.then(() => {
|
||||||
|
resolve("success");
|
||||||
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
} else if (convertTo === "html") {
|
||||||
|
poppler
|
||||||
|
.pdfToHtml(filePath, targetPath)
|
||||||
|
.then(() => {
|
||||||
|
resolve("success");
|
||||||
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
} else if (convertTo === "text") {
|
||||||
|
poppler
|
||||||
|
.pdfToText(filePath, targetPath)
|
||||||
|
.then(() => {
|
||||||
|
resolve("success");
|
||||||
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
reject(`Invalid convertTo option: ${convertTo}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import { exec } from "node:child_process";
|
|
||||||
|
|
||||||
export const properties = {
|
|
||||||
from: {
|
|
||||||
images: ["svg"],
|
|
||||||
},
|
|
||||||
to: {
|
|
||||||
images: ["png"],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export function convert(
|
|
||||||
filePath: string,
|
|
||||||
fileType: string,
|
|
||||||
convertTo: string,
|
|
||||||
targetPath: string,
|
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
|
||||||
options?: any,
|
|
||||||
): Promise<string> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
exec(`resvg "${filePath}" "${targetPath}"`, (error, stdout, stderr) => {
|
|
||||||
if (error) {
|
|
||||||
reject(`error: ${error}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stdout) {
|
|
||||||
console.log(`stdout: ${stdout}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stderr) {
|
|
||||||
console.error(`stderr: ${stderr}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve("success");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -113,15 +113,9 @@ export function convert(
|
|||||||
// .toFormat(convertTo)
|
// .toFormat(convertTo)
|
||||||
// .toFile(targetPath);
|
// .toFile(targetPath);
|
||||||
// }
|
// }
|
||||||
let action = "copy";
|
|
||||||
if (fileType === "pdf") {
|
|
||||||
action = "pdfload";
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
exec(
|
exec(`vips copy "${filePath}" "${targetPath}"`, (error, stdout, stderr) => {
|
||||||
`vips ${action} "${filePath}" "${targetPath}"`,
|
|
||||||
(error, stdout, stderr) => {
|
|
||||||
if (error) {
|
if (error) {
|
||||||
reject(`error: ${error}`);
|
reject(`error: ${error}`);
|
||||||
}
|
}
|
||||||
@@ -135,7 +129,6 @@ export function convert(
|
|||||||
}
|
}
|
||||||
|
|
||||||
resolve("success");
|
resolve("success");
|
||||||
},
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ export const normalizeOutputFiletype = (filetype: string): string => {
|
|||||||
return "tex";
|
return "tex";
|
||||||
case "markdown":
|
case "markdown":
|
||||||
return "md";
|
return "md";
|
||||||
|
case "text":
|
||||||
|
return "txt";
|
||||||
default:
|
default:
|
||||||
return lowercaseFiletype;
|
return lowercaseFiletype;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
import { exec } from "node:child_process";
|
|
||||||
import { version } from "../../package.json";
|
|
||||||
console.log(`ConvertX v${version}`);
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV === "production") {
|
|
||||||
exec("cat /etc/os-release", (error, stdout) => {
|
|
||||||
if (error) {
|
|
||||||
console.error("Not running on docker, this is not supported.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stdout) {
|
|
||||||
console.log(stdout.split('PRETTY_NAME="')[1]?.split('"')[0]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
exec("pandoc -v", (error, stdout) => {
|
|
||||||
if (error) {
|
|
||||||
console.error("Pandoc is not installed.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stdout) {
|
|
||||||
console.log(stdout.split("\n")[0]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
exec("ffmpeg -version", (error, stdout) => {
|
|
||||||
if (error) {
|
|
||||||
console.error("FFmpeg is not installed.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stdout) {
|
|
||||||
console.log(stdout.split("\n")[0]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
exec("vips -v", (error, stdout) => {
|
|
||||||
if (error) {
|
|
||||||
console.error("Vips is not installed.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stdout) {
|
|
||||||
console.log(stdout.split("\n")[0]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
exec("gm version", (error, stdout) => {
|
|
||||||
if (error) {
|
|
||||||
console.error("GraphicsMagick 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.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stdout) {
|
|
||||||
console.log(stdout.split("\n")[0]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
exec("xelatex -version", (error, stdout) => {
|
|
||||||
if (error) {
|
|
||||||
console.error("Tex Live with XeTeX is not installed.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stdout) {
|
|
||||||
console.log(stdout.split("\n")[0]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
exec("resvg -V", (error, stdout) => {
|
|
||||||
if (error) {
|
|
||||||
console.error("resvg is not installed");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stdout) {
|
|
||||||
console.log(`resvg v${stdout.split("\n")[0]}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
exec("bun -v", (error, stdout) => {
|
|
||||||
if (error) {
|
|
||||||
console.error("Bun is not installed. wait what");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stdout) {
|
|
||||||
console.log(`Bun v${stdout.split("\n")[0]}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
467
src/index.tsx
467
src/index.tsx
@@ -1,12 +1,12 @@
|
|||||||
import cookie from "@elysiajs/cookie";
|
|
||||||
import { html } from "@elysiajs/html";
|
|
||||||
import { jwt, type JWTPayloadSpec } from "@elysiajs/jwt";
|
|
||||||
import { staticPlugin } from "@elysiajs/static";
|
|
||||||
import { Database } from "bun:sqlite";
|
import { Database } from "bun:sqlite";
|
||||||
import { Elysia, t } from "elysia";
|
import { randomUUID } from "node:crypto";
|
||||||
import { randomInt, randomUUID } from "node:crypto";
|
|
||||||
import { rmSync } from "node:fs";
|
import { rmSync } from "node:fs";
|
||||||
import { mkdir, unlink } from "node:fs/promises";
|
import { mkdir, unlink } from "node:fs/promises";
|
||||||
|
import cookie from "@elysiajs/cookie";
|
||||||
|
import { html } from "@elysiajs/html";
|
||||||
|
import { jwt } from "@elysiajs/jwt";
|
||||||
|
import { staticPlugin } from "@elysiajs/static";
|
||||||
|
import { Elysia, t } from "elysia";
|
||||||
import { BaseHtml } from "./components/base";
|
import { BaseHtml } from "./components/base";
|
||||||
import { Header } from "./components/header";
|
import { Header } from "./components/header";
|
||||||
import {
|
import {
|
||||||
@@ -19,23 +19,15 @@ import {
|
|||||||
normalizeFiletype,
|
normalizeFiletype,
|
||||||
normalizeOutputFiletype,
|
normalizeOutputFiletype,
|
||||||
} from "./helpers/normalizeFiletype";
|
} from "./helpers/normalizeFiletype";
|
||||||
import "./helpers/printVersions";
|
|
||||||
|
|
||||||
mkdir("./data", { recursive: true }).catch(console.error);
|
|
||||||
const db = new Database("./data/mydb.sqlite", { create: true });
|
const db = new Database("./data/mydb.sqlite", { create: true });
|
||||||
const uploadsDir = "./data/uploads/";
|
const uploadsDir = "./data/uploads/";
|
||||||
const outputDir = "./data/output/";
|
const outputDir = "./data/output/";
|
||||||
|
|
||||||
const ACCOUNT_REGISTRATION =
|
const ACCOUNT_REGISTRATION =
|
||||||
process.env.ACCOUNT_REGISTRATION?.toLowerCase() === "true" || false;
|
process.env.ACCOUNT_REGISTRATION === "true" || false;
|
||||||
|
|
||||||
const HTTP_ALLOWED =
|
const HTTP_ALLOWED = process.env.HTTP_ALLOWED === "true" || false;
|
||||||
process.env.HTTP_ALLOWED?.toLowerCase() === "true" || false;
|
|
||||||
const ALLOW_UNAUTHENTICATED =
|
|
||||||
process.env.ALLOW_UNAUTHENTICATED?.toLowerCase() === "true" || false;
|
|
||||||
const AUTO_DELETE_EVERY_N_HOURS = process.env.AUTO_DELETE_EVERY_N_HOURS
|
|
||||||
? Number(process.env.AUTO_DELETE_EVERY_N_HOURS)
|
|
||||||
: 24;
|
|
||||||
|
|
||||||
// fileNames: fileNames,
|
// fileNames: fileNames,
|
||||||
// filesToConvert: fileNames.length,
|
// filesToConvert: fileNames.length,
|
||||||
@@ -82,37 +74,33 @@ if (dbVersion === 0) {
|
|||||||
|
|
||||||
let FIRST_RUN = db.query("SELECT * FROM users").get() === null || false;
|
let FIRST_RUN = db.query("SELECT * FROM users").get() === null || false;
|
||||||
|
|
||||||
class User {
|
interface IUser {
|
||||||
id!: number;
|
id: number;
|
||||||
email!: string;
|
email: string;
|
||||||
password!: string;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Filename {
|
interface IFileNames {
|
||||||
id!: number;
|
id: number;
|
||||||
job_id!: number;
|
job_id: number;
|
||||||
file_name!: string;
|
file_name: string;
|
||||||
output_file_name!: string;
|
output_file_name: string;
|
||||||
status!: string;
|
status: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Jobs {
|
interface IJobs {
|
||||||
finished_files!: number;
|
finished_files: number;
|
||||||
id!: number;
|
id: number;
|
||||||
user_id!: number;
|
user_id: number;
|
||||||
date_created!: string;
|
date_created: string;
|
||||||
status!: string;
|
status: string;
|
||||||
num_files!: number;
|
num_files: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// enable WAL mode
|
// enable WAL mode
|
||||||
db.exec("PRAGMA journal_mode = WAL;");
|
db.exec("PRAGMA journal_mode = WAL;");
|
||||||
|
|
||||||
const app = new Elysia({
|
const app = new Elysia()
|
||||||
serve: {
|
|
||||||
maxRequestBodySize: Number.MAX_SAFE_INTEGER,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.use(cookie())
|
.use(cookie())
|
||||||
.use(html())
|
.use(html())
|
||||||
.use(
|
.use(
|
||||||
@@ -121,7 +109,7 @@ const app = new Elysia({
|
|||||||
schema: t.Object({
|
schema: t.Object({
|
||||||
id: t.String(),
|
id: t.String(),
|
||||||
}),
|
}),
|
||||||
secret: process.env.JWT_SECRET ?? randomUUID(),
|
secret: process.env.JWT_SECRET || randomUUID(),
|
||||||
exp: "7d",
|
exp: "7d",
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -133,7 +121,7 @@ const app = new Elysia({
|
|||||||
)
|
)
|
||||||
.get("/setup", ({ redirect }) => {
|
.get("/setup", ({ redirect }) => {
|
||||||
if (!FIRST_RUN) {
|
if (!FIRST_RUN) {
|
||||||
return redirect("/login", 302);
|
return redirect("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -176,12 +164,11 @@ const app = new Elysia({
|
|||||||
})
|
})
|
||||||
.get("/register", ({ redirect }) => {
|
.get("/register", ({ redirect }) => {
|
||||||
if (!ACCOUNT_REGISTRATION) {
|
if (!ACCOUNT_REGISTRATION) {
|
||||||
return redirect("/login", 302);
|
return redirect("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseHtml title="ConvertX | Register">
|
<BaseHtml title="ConvertX | Register">
|
||||||
<>
|
|
||||||
<Header accountRegistration={ACCOUNT_REGISTRATION} />
|
<Header accountRegistration={ACCOUNT_REGISTRATION} />
|
||||||
<main class="container">
|
<main class="container">
|
||||||
<article>
|
<article>
|
||||||
@@ -212,7 +199,6 @@ const app = new Elysia({
|
|||||||
</form>
|
</form>
|
||||||
</article>
|
</article>
|
||||||
</main>
|
</main>
|
||||||
</>
|
|
||||||
</BaseHtml>
|
</BaseHtml>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@@ -220,7 +206,7 @@ const app = new Elysia({
|
|||||||
"/register",
|
"/register",
|
||||||
async ({ body, set, redirect, jwt, cookie: { auth } }) => {
|
async ({ body, set, redirect, jwt, cookie: { auth } }) => {
|
||||||
if (!ACCOUNT_REGISTRATION && !FIRST_RUN) {
|
if (!ACCOUNT_REGISTRATION && !FIRST_RUN) {
|
||||||
return redirect("/login", 302);
|
return redirect("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (FIRST_RUN) {
|
if (FIRST_RUN) {
|
||||||
@@ -243,17 +229,9 @@ const app = new Elysia({
|
|||||||
savedPassword,
|
savedPassword,
|
||||||
);
|
);
|
||||||
|
|
||||||
const user = db
|
const user = (await db
|
||||||
.query("SELECT * FROM users WHERE email = ?")
|
.query("SELECT * FROM users WHERE email = ?")
|
||||||
.as(User)
|
.get(body.email)) as IUser;
|
||||||
.get(body.email);
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
set.status = 500;
|
|
||||||
return {
|
|
||||||
message: "Failed to create user.",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const accessToken = await jwt.sign({
|
const accessToken = await jwt.sign({
|
||||||
id: String(user.id),
|
id: String(user.id),
|
||||||
@@ -275,13 +253,13 @@ const app = new Elysia({
|
|||||||
sameSite: "strict",
|
sameSite: "strict",
|
||||||
});
|
});
|
||||||
|
|
||||||
return redirect("/", 302);
|
return redirect("/");
|
||||||
},
|
},
|
||||||
{ body: t.Object({ email: t.String(), password: t.String() }) },
|
{ body: t.Object({ email: t.String(), password: t.String() }) },
|
||||||
)
|
)
|
||||||
.get("/login", async ({ jwt, redirect, cookie: { auth } }) => {
|
.get("/login", async ({ jwt, redirect, cookie: { auth } }) => {
|
||||||
if (FIRST_RUN) {
|
if (FIRST_RUN) {
|
||||||
return redirect("/setup", 302);
|
return redirect("/setup");
|
||||||
}
|
}
|
||||||
|
|
||||||
// if already logged in, redirect to home
|
// if already logged in, redirect to home
|
||||||
@@ -289,7 +267,7 @@ const app = new Elysia({
|
|||||||
const user = await jwt.verify(auth.value);
|
const user = await jwt.verify(auth.value);
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
return redirect("/", 302);
|
return redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
auth.remove();
|
auth.remove();
|
||||||
@@ -297,7 +275,6 @@ const app = new Elysia({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseHtml title="ConvertX | Login">
|
<BaseHtml title="ConvertX | Login">
|
||||||
<>
|
|
||||||
<Header accountRegistration={ACCOUNT_REGISTRATION} />
|
<Header accountRegistration={ACCOUNT_REGISTRATION} />
|
||||||
<main class="container">
|
<main class="container">
|
||||||
<article>
|
<article>
|
||||||
@@ -335,17 +312,15 @@ const app = new Elysia({
|
|||||||
</form>
|
</form>
|
||||||
</article>
|
</article>
|
||||||
</main>
|
</main>
|
||||||
</>
|
|
||||||
</BaseHtml>
|
</BaseHtml>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.post(
|
.post(
|
||||||
"/login",
|
"/login",
|
||||||
async function handler({ body, set, redirect, jwt, cookie: { auth } }) {
|
async function handler({ body, set, redirect, jwt, cookie: { auth } }) {
|
||||||
const existingUser = db
|
const existingUser = (await db
|
||||||
.query("SELECT * FROM users WHERE email = ?")
|
.query("SELECT * FROM users WHERE email = ?")
|
||||||
.as(User)
|
.get(body.email)) as IUser;
|
||||||
.get(body.email);
|
|
||||||
|
|
||||||
if (!existingUser) {
|
if (!existingUser) {
|
||||||
set.status = 403;
|
set.status = 403;
|
||||||
@@ -386,7 +361,7 @@ const app = new Elysia({
|
|||||||
sameSite: "strict",
|
sameSite: "strict",
|
||||||
});
|
});
|
||||||
|
|
||||||
return redirect("/", 302);
|
return redirect("/");
|
||||||
},
|
},
|
||||||
{ body: t.Object({ email: t.String(), password: t.String() }) },
|
{ body: t.Object({ email: t.String(), password: t.String() }) },
|
||||||
)
|
)
|
||||||
@@ -395,70 +370,39 @@ const app = new Elysia({
|
|||||||
auth.remove();
|
auth.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect("/login", 302);
|
return redirect("/login");
|
||||||
})
|
})
|
||||||
.post("/logoff", ({ redirect, cookie: { auth } }) => {
|
.post("/logoff", ({ redirect, cookie: { auth } }) => {
|
||||||
if (auth?.value) {
|
if (auth?.value) {
|
||||||
auth.remove();
|
auth.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect("/login", 302);
|
return redirect("/login");
|
||||||
})
|
})
|
||||||
.get("/", async ({ jwt, redirect, cookie: { auth, jobId } }) => {
|
.get("/", async ({ jwt, redirect, cookie: { auth, jobId } }) => {
|
||||||
if (FIRST_RUN) {
|
if (FIRST_RUN) {
|
||||||
return redirect("/setup", 302);
|
return redirect("/setup");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!auth?.value && !ALLOW_UNAUTHENTICATED) {
|
if (!auth?.value) {
|
||||||
return redirect("/login", 302);
|
return redirect("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate jwt
|
// validate jwt
|
||||||
let user: ({ id: string } & JWTPayloadSpec) | false = false;
|
const user = await jwt.verify(auth.value);
|
||||||
if (auth?.value) {
|
if (!user) {
|
||||||
user = await jwt.verify(auth.value);
|
return redirect("/login");
|
||||||
|
}
|
||||||
|
|
||||||
if (user !== false && user.id) {
|
|
||||||
if (Number.parseInt(user.id) < 2 ** 24 || !ALLOW_UNAUTHENTICATED) {
|
|
||||||
// make sure user exists in db
|
// make sure user exists in db
|
||||||
const existingUser = db
|
const existingUser = (await db
|
||||||
.query("SELECT * FROM users WHERE id = ?")
|
.query("SELECT * FROM users WHERE id = ?")
|
||||||
.as(User)
|
.get(user.id)) as IUser;
|
||||||
.get(user.id);
|
|
||||||
|
|
||||||
if (!existingUser) {
|
if (!existingUser) {
|
||||||
if (auth?.value) {
|
if (auth?.value) {
|
||||||
auth.remove();
|
auth.remove();
|
||||||
}
|
}
|
||||||
return redirect("/login", 302);
|
return redirect("/login");
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (ALLOW_UNAUTHENTICATED) {
|
|
||||||
const newUserId = String(randomInt(2 ** 24, 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: 60 * 60 * 24 * 1,
|
|
||||||
sameSite: "strict",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return redirect("/login", 302);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// create a new job
|
// create a new job
|
||||||
@@ -489,7 +433,6 @@ const app = new Elysia({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseHtml>
|
<BaseHtml>
|
||||||
<>
|
|
||||||
<Header loggedIn />
|
<Header loggedIn />
|
||||||
<main class="container">
|
<main class="container">
|
||||||
<article>
|
<article>
|
||||||
@@ -509,92 +452,14 @@ const app = new Elysia({
|
|||||||
))}
|
))}
|
||||||
</select> */}
|
</select> */}
|
||||||
</article>
|
</article>
|
||||||
<form
|
<form method="post" action="/convert">
|
||||||
method="post"
|
|
||||||
action="/convert"
|
|
||||||
style={{ position: "relative" }}>
|
|
||||||
<input type="hidden" name="file_names" id="file_names" />
|
<input type="hidden" name="file_names" id="file_names" />
|
||||||
<article>
|
<article>
|
||||||
<input
|
<select name="convert_to" aria-label="Convert to" required>
|
||||||
type="search"
|
|
||||||
name="convert_to_search"
|
|
||||||
placeholder="Search for conversions"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="select_container">
|
|
||||||
<article
|
|
||||||
class="convert_to_popup"
|
|
||||||
hidden
|
|
||||||
style={{
|
|
||||||
flexDirection: "column",
|
|
||||||
display: "flex",
|
|
||||||
zIndex: 2,
|
|
||||||
position: "absolute",
|
|
||||||
maxHeight: "50vh",
|
|
||||||
width: "90vw",
|
|
||||||
overflowY: "scroll",
|
|
||||||
margin: "0px",
|
|
||||||
overflowX: "hidden",
|
|
||||||
}}>
|
|
||||||
{Object.entries(getAllTargets()).map(
|
|
||||||
([converter, targets]) => (
|
|
||||||
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
|
|
||||||
<article
|
|
||||||
class="convert_to_group"
|
|
||||||
data-converter={converter}
|
|
||||||
style={{
|
|
||||||
borderColor: "gray",
|
|
||||||
padding: "2px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<header
|
|
||||||
style={{ fontSize: "20px", fontWeight: "bold" }}
|
|
||||||
safe>
|
|
||||||
{converter}
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<ul
|
|
||||||
class="convert_to_target"
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "row",
|
|
||||||
gap: "5px",
|
|
||||||
flexWrap: "wrap",
|
|
||||||
}}>
|
|
||||||
{targets.map((target) => (
|
|
||||||
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
|
|
||||||
<button
|
|
||||||
// https://stackoverflow.com/questions/121499/when-a-blur-event-occurs-how-can-i-find-out-which-element-focus-went-to#comment82388679_33325953
|
|
||||||
tabindex={0}
|
|
||||||
class="target"
|
|
||||||
data-value={`${target},${converter}`}
|
|
||||||
data-target={target}
|
|
||||||
data-converter={converter}
|
|
||||||
style={{ fontSize: "15px", padding: "5px" }}
|
|
||||||
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="">
|
<option selected disabled value="">
|
||||||
Convert to
|
Convert to
|
||||||
</option>
|
</option>
|
||||||
{Object.entries(getAllTargets()).map(
|
{Object.entries(getAllTargets()).map(([converter, targets]) => (
|
||||||
([converter, targets]) => (
|
|
||||||
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
|
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
|
||||||
<optgroup label={converter}>
|
<optgroup label={converter}>
|
||||||
{targets.map((target) => (
|
{targets.map((target) => (
|
||||||
@@ -604,16 +469,13 @@ const app = new Elysia({
|
|||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</optgroup>
|
</optgroup>
|
||||||
),
|
))}
|
||||||
)}
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
|
||||||
</article>
|
</article>
|
||||||
<input type="submit" value="Convert" />
|
<input type="submit" value="Convert" />
|
||||||
</form>
|
</form>
|
||||||
</main>
|
</main>
|
||||||
<script src="script.js" defer />
|
<script src="script.js" defer />
|
||||||
</>
|
|
||||||
</BaseHtml>
|
</BaseHtml>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@@ -621,67 +483,7 @@ const app = new Elysia({
|
|||||||
"/conversions",
|
"/conversions",
|
||||||
({ body }) => {
|
({ body }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<select name="convert_to" aria-label="Convert to" required>
|
||||||
<article
|
|
||||||
class="convert_to_popup"
|
|
||||||
hidden
|
|
||||||
style={{
|
|
||||||
flexDirection: "column",
|
|
||||||
display: "flex",
|
|
||||||
zIndex: 2,
|
|
||||||
position: "absolute",
|
|
||||||
maxHeight: "50vh",
|
|
||||||
width: "90vw",
|
|
||||||
overflowY: "scroll",
|
|
||||||
margin: "0px",
|
|
||||||
overflowX: "hidden",
|
|
||||||
}}>
|
|
||||||
{Object.entries(getPossibleTargets(body.fileType)).map(
|
|
||||||
([converter, targets]) => (
|
|
||||||
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
|
|
||||||
<article
|
|
||||||
class="convert_to_group"
|
|
||||||
data-converter={converter}
|
|
||||||
style={{
|
|
||||||
borderColor: "gray",
|
|
||||||
padding: "2px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<header style={{ fontSize: "20px", fontWeight: "bold" }} safe>
|
|
||||||
{converter}
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<ul
|
|
||||||
class="convert_to_target"
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "row",
|
|
||||||
gap: "5px",
|
|
||||||
flexWrap: "wrap",
|
|
||||||
}}>
|
|
||||||
{targets.map((target) => (
|
|
||||||
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
|
|
||||||
<button
|
|
||||||
// https://stackoverflow.com/questions/121499/when-a-blur-event-occurs-how-can-i-find-out-which-element-focus-went-to#comment82388679_33325953
|
|
||||||
tabindex={0}
|
|
||||||
class="target"
|
|
||||||
data-value={`${target},${converter}`}
|
|
||||||
data-target={target}
|
|
||||||
data-converter={converter}
|
|
||||||
style={{ fontSize: "15px", padding: "5px" }}
|
|
||||||
type="button"
|
|
||||||
safe
|
|
||||||
>
|
|
||||||
{target}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</article>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<select name="convert_to" aria-label="Convert to" required hidden>
|
|
||||||
<option selected disabled value="">
|
<option selected disabled value="">
|
||||||
Convert to
|
Convert to
|
||||||
</option>
|
</option>
|
||||||
@@ -699,7 +501,6 @@ const app = new Elysia({
|
|||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
</select>
|
</select>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
{ body: t.Object({ fileType: t.String() }) },
|
{ body: t.Object({ fileType: t.String() }) },
|
||||||
@@ -708,16 +509,16 @@ const app = new Elysia({
|
|||||||
"/upload",
|
"/upload",
|
||||||
async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
|
async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
|
||||||
if (!auth?.value) {
|
if (!auth?.value) {
|
||||||
return redirect("/login", 302);
|
return redirect("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await jwt.verify(auth.value);
|
const user = await jwt.verify(auth.value);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return redirect("/login", 302);
|
return redirect("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!jobId?.value) {
|
if (!jobId?.value) {
|
||||||
return redirect("/", 302);
|
return redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingJob = await db
|
const existingJob = await db
|
||||||
@@ -725,7 +526,7 @@ const app = new Elysia({
|
|||||||
.get(jobId.value, user.id);
|
.get(jobId.value, user.id);
|
||||||
|
|
||||||
if (!existingJob) {
|
if (!existingJob) {
|
||||||
return redirect("/", 302);
|
return redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
|
const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
|
||||||
@@ -736,8 +537,13 @@ const app = new Elysia({
|
|||||||
await Bun.write(`${userUploadsDir}${file.name}`, file);
|
await Bun.write(`${userUploadsDir}${file.name}`, file);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: weird error
|
await Bun.write(
|
||||||
await Bun.write(`${userUploadsDir}${body.file["name"]}`, body.file);
|
`${userUploadsDir}${
|
||||||
|
// biome-ignore lint/complexity/useLiteralKeys: ts bug
|
||||||
|
body.file["name"]
|
||||||
|
}`,
|
||||||
|
body.file,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -751,16 +557,16 @@ const app = new Elysia({
|
|||||||
"/delete",
|
"/delete",
|
||||||
async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
|
async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
|
||||||
if (!auth?.value) {
|
if (!auth?.value) {
|
||||||
return redirect("/login", 302);
|
return redirect("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await jwt.verify(auth.value);
|
const user = await jwt.verify(auth.value);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return redirect("/login", 302);
|
return redirect("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!jobId?.value) {
|
if (!jobId?.value) {
|
||||||
return redirect("/", 302);
|
return redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingJob = await db
|
const existingJob = await db
|
||||||
@@ -768,7 +574,7 @@ const app = new Elysia({
|
|||||||
.get(jobId.value, user.id);
|
.get(jobId.value, user.id);
|
||||||
|
|
||||||
if (!existingJob) {
|
if (!existingJob) {
|
||||||
return redirect("/", 302);
|
return redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
|
const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
|
||||||
@@ -781,25 +587,24 @@ const app = new Elysia({
|
|||||||
"/convert",
|
"/convert",
|
||||||
async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
|
async ({ body, redirect, jwt, cookie: { auth, jobId } }) => {
|
||||||
if (!auth?.value) {
|
if (!auth?.value) {
|
||||||
return redirect("/login", 302);
|
return redirect("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await jwt.verify(auth.value);
|
const user = await jwt.verify(auth.value);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return redirect("/login", 302);
|
return redirect("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!jobId?.value) {
|
if (!jobId?.value) {
|
||||||
return redirect("/", 302);
|
return redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingJob = db
|
const existingJob = (await db
|
||||||
.query("SELECT * FROM jobs WHERE id = ? AND user_id = ?")
|
.query("SELECT * FROM jobs WHERE id = ? AND user_id = ?")
|
||||||
.as(Jobs)
|
.get(jobId.value, user.id)) as IJobs;
|
||||||
.get(jobId.value, user.id);
|
|
||||||
|
|
||||||
if (!existingJob) {
|
if (!existingJob) {
|
||||||
return redirect("/", 302);
|
return redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
|
const userUploadsDir = `${uploadsDir}${user.id}/${jobId.value}/`;
|
||||||
@@ -815,33 +620,34 @@ const app = new Elysia({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const convertTo = normalizeFiletype(body.convert_to.split(",")[0] ?? "");
|
const convertTo = normalizeFiletype(
|
||||||
|
body.convert_to.split(",")[0] as string,
|
||||||
|
);
|
||||||
const converterName = body.convert_to.split(",")[1];
|
const converterName = body.convert_to.split(",")[1];
|
||||||
const fileNames = JSON.parse(body.file_names) as string[];
|
const fileNames = JSON.parse(body.file_names) as string[];
|
||||||
|
|
||||||
if (!Array.isArray(fileNames) || fileNames.length === 0) {
|
if (!Array.isArray(fileNames) || fileNames.length === 0) {
|
||||||
return redirect("/", 302);
|
return redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
db.query(
|
db.run(
|
||||||
"UPDATE jobs SET num_files = ?1, status = 'pending' WHERE id = ?2",
|
"UPDATE jobs SET num_files = ?, status = 'pending' WHERE id = ?",
|
||||||
).run(fileNames.length, jobId.value);
|
fileNames.length,
|
||||||
|
jobId.value,
|
||||||
|
);
|
||||||
|
|
||||||
const query = db.query(
|
const query = db.query(
|
||||||
"INSERT INTO file_names (job_id, file_name, output_file_name, status) VALUES (?1, ?2, ?3, ?4)",
|
"INSERT INTO file_names (job_id, file_name, output_file_name, status) VALUES (?, ?, ?, ?)",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Start the conversion process in the background
|
// Start the conversion process in the background
|
||||||
Promise.all(
|
Promise.all(
|
||||||
fileNames.map(async (fileName) => {
|
fileNames.map(async (fileName) => {
|
||||||
const filePath = `${userUploadsDir}${fileName}`;
|
const filePath = `${userUploadsDir}${fileName}`;
|
||||||
const fileTypeOrig = fileName.split(".").pop() ?? "";
|
const fileTypeOrig = fileName.split(".").pop() as string;
|
||||||
const fileType = normalizeFiletype(fileTypeOrig);
|
const fileType = normalizeFiletype(fileTypeOrig);
|
||||||
const newFileExt = normalizeOutputFiletype(convertTo);
|
const newFileExt = normalizeOutputFiletype(convertTo);
|
||||||
const newFileName = fileName.replace(
|
const newFileName = fileName.replace(fileTypeOrig, newFileExt);
|
||||||
new RegExp(`${fileTypeOrig}(?!.*${fileTypeOrig})`),
|
|
||||||
newFileExt,
|
|
||||||
);
|
|
||||||
const targetPath = `${userOutputDir}${newFileName}`;
|
const targetPath = `${userOutputDir}${newFileName}`;
|
||||||
|
|
||||||
const result = await mainConverter(
|
const result = await mainConverter(
|
||||||
@@ -852,18 +658,16 @@ const app = new Elysia({
|
|||||||
{},
|
{},
|
||||||
converterName,
|
converterName,
|
||||||
);
|
);
|
||||||
if (jobId.value) {
|
|
||||||
query.run(jobId.value, fileName, newFileName, result);
|
query.run(jobId.value, fileName, newFileName, result);
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// All conversions are done, update the job status to 'completed'
|
// All conversions are done, update the job status to 'completed'
|
||||||
if (jobId.value) {
|
db.run(
|
||||||
db.query("UPDATE jobs SET status = 'completed' WHERE id = ?1").run(
|
"UPDATE jobs SET status = 'completed' WHERE id = ?",
|
||||||
jobId.value,
|
jobId.value,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
// delete all uploaded files in userUploadsDir
|
// delete all uploaded files in userUploadsDir
|
||||||
// rmSync(userUploadsDir, { recursive: true, force: true });
|
// rmSync(userUploadsDir, { recursive: true, force: true });
|
||||||
@@ -873,7 +677,7 @@ const app = new Elysia({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Redirect the client immediately
|
// Redirect the client immediately
|
||||||
return redirect(`/results/${jobId.value}`, 302);
|
return redirect(`/results/${jobId.value}`);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
body: t.Object({
|
body: t.Object({
|
||||||
@@ -884,24 +688,22 @@ const app = new Elysia({
|
|||||||
)
|
)
|
||||||
.get("/history", async ({ jwt, redirect, cookie: { auth } }) => {
|
.get("/history", async ({ jwt, redirect, cookie: { auth } }) => {
|
||||||
if (!auth?.value) {
|
if (!auth?.value) {
|
||||||
return redirect("/login", 302);
|
return redirect("/login");
|
||||||
}
|
}
|
||||||
const user = await jwt.verify(auth.value);
|
const user = await jwt.verify(auth.value);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return redirect("/login", 302);
|
return redirect("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
let userJobs = db
|
let userJobs = db
|
||||||
.query("SELECT * FROM jobs WHERE user_id = ?")
|
.query("SELECT * FROM jobs WHERE user_id = ?")
|
||||||
.as(Jobs)
|
.all(user.id) as IJobs[];
|
||||||
.all(user.id);
|
|
||||||
|
|
||||||
for (const job of userJobs) {
|
for (const job of userJobs) {
|
||||||
const files = db
|
const files = db
|
||||||
.query("SELECT * FROM file_names WHERE job_id = ?")
|
.query("SELECT * FROM file_names WHERE job_id = ?")
|
||||||
.as(Filename)
|
.all(job.id) as IFileNames[];
|
||||||
.all(job.id);
|
|
||||||
|
|
||||||
job.finished_files = files.length;
|
job.finished_files = files.length;
|
||||||
}
|
}
|
||||||
@@ -911,7 +713,6 @@ const app = new Elysia({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseHtml title="ConvertX | Results">
|
<BaseHtml title="ConvertX | Results">
|
||||||
<>
|
|
||||||
<Header loggedIn />
|
<Header loggedIn />
|
||||||
<main class="container">
|
<main class="container">
|
||||||
<article>
|
<article>
|
||||||
@@ -943,7 +744,6 @@ const app = new Elysia({
|
|||||||
</table>
|
</table>
|
||||||
</article>
|
</article>
|
||||||
</main>
|
</main>
|
||||||
</>
|
|
||||||
</BaseHtml>
|
</BaseHtml>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@@ -951,7 +751,7 @@ const app = new Elysia({
|
|||||||
"/results/:jobId",
|
"/results/:jobId",
|
||||||
async ({ params, jwt, set, redirect, cookie: { auth, job_id } }) => {
|
async ({ params, jwt, set, redirect, cookie: { auth, job_id } }) => {
|
||||||
if (!auth?.value) {
|
if (!auth?.value) {
|
||||||
return redirect("/login", 302);
|
return redirect("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (job_id?.value) {
|
if (job_id?.value) {
|
||||||
@@ -961,13 +761,12 @@ const app = new Elysia({
|
|||||||
|
|
||||||
const user = await jwt.verify(auth.value);
|
const user = await jwt.verify(auth.value);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return redirect("/login", 302);
|
return redirect("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
const job = db
|
const job = (await db
|
||||||
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
|
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
|
||||||
.as(Jobs)
|
.get(user.id, params.jobId)) as IJobs;
|
||||||
.get(user.id, params.jobId);
|
|
||||||
|
|
||||||
if (!job) {
|
if (!job) {
|
||||||
set.status = 404;
|
set.status = 404;
|
||||||
@@ -980,12 +779,10 @@ const app = new Elysia({
|
|||||||
|
|
||||||
const files = db
|
const files = db
|
||||||
.query("SELECT * FROM file_names WHERE job_id = ?")
|
.query("SELECT * FROM file_names WHERE job_id = ?")
|
||||||
.as(Filename)
|
.all(params.jobId) as IFileNames[];
|
||||||
.all(params.jobId);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseHtml title="ConvertX | Result">
|
<BaseHtml title="ConvertX | Result">
|
||||||
<>
|
|
||||||
<Header loggedIn />
|
<Header loggedIn />
|
||||||
<main class="container">
|
<main class="container">
|
||||||
<article>
|
<article>
|
||||||
@@ -1041,7 +838,6 @@ const app = new Elysia({
|
|||||||
</article>
|
</article>
|
||||||
</main>
|
</main>
|
||||||
<script src="/results.js" defer />
|
<script src="/results.js" defer />
|
||||||
</>
|
|
||||||
</BaseHtml>
|
</BaseHtml>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -1050,7 +846,7 @@ const app = new Elysia({
|
|||||||
"/progress/:jobId",
|
"/progress/:jobId",
|
||||||
async ({ jwt, set, params, redirect, cookie: { auth, job_id } }) => {
|
async ({ jwt, set, params, redirect, cookie: { auth, job_id } }) => {
|
||||||
if (!auth?.value) {
|
if (!auth?.value) {
|
||||||
return redirect("/login", 302);
|
return redirect("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (job_id?.value) {
|
if (job_id?.value) {
|
||||||
@@ -1060,13 +856,12 @@ const app = new Elysia({
|
|||||||
|
|
||||||
const user = await jwt.verify(auth.value);
|
const user = await jwt.verify(auth.value);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return redirect("/login", 302);
|
return redirect("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
const job = db
|
const job = (await db
|
||||||
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
|
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
|
||||||
.as(Jobs)
|
.get(user.id, params.jobId)) as IJobs;
|
||||||
.get(user.id, params.jobId);
|
|
||||||
|
|
||||||
if (!job) {
|
if (!job) {
|
||||||
set.status = 404;
|
set.status = 404;
|
||||||
@@ -1079,8 +874,7 @@ const app = new Elysia({
|
|||||||
|
|
||||||
const files = db
|
const files = db
|
||||||
.query("SELECT * FROM file_names WHERE job_id = ?")
|
.query("SELECT * FROM file_names WHERE job_id = ?")
|
||||||
.as(Filename)
|
.all(params.jobId) as IFileNames[];
|
||||||
.all(params.jobId);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article>
|
<article>
|
||||||
@@ -1140,12 +934,12 @@ const app = new Elysia({
|
|||||||
"/download/:userId/:jobId/:fileName",
|
"/download/:userId/:jobId/:fileName",
|
||||||
async ({ params, jwt, redirect, cookie: { auth } }) => {
|
async ({ params, jwt, redirect, cookie: { auth } }) => {
|
||||||
if (!auth?.value) {
|
if (!auth?.value) {
|
||||||
return redirect("/login", 302);
|
return redirect("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await jwt.verify(auth.value);
|
const user = await jwt.verify(auth.value);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return redirect("/login", 302);
|
return redirect("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
const job = await db
|
const job = await db
|
||||||
@@ -1153,7 +947,7 @@ const app = new Elysia({
|
|||||||
.get(user.id, params.jobId);
|
.get(user.id, params.jobId);
|
||||||
|
|
||||||
if (!job) {
|
if (!job) {
|
||||||
return redirect("/results", 302);
|
return redirect("/results");
|
||||||
}
|
}
|
||||||
// parse from url encoded string
|
// parse from url encoded string
|
||||||
const userId = decodeURIComponent(params.userId);
|
const userId = decodeURIComponent(params.userId);
|
||||||
@@ -1166,17 +960,16 @@ const app = new Elysia({
|
|||||||
)
|
)
|
||||||
.get("/converters", async ({ jwt, redirect, cookie: { auth } }) => {
|
.get("/converters", async ({ jwt, redirect, cookie: { auth } }) => {
|
||||||
if (!auth?.value) {
|
if (!auth?.value) {
|
||||||
return redirect("/login", 302);
|
return redirect("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await jwt.verify(auth.value);
|
const user = await jwt.verify(auth.value);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return redirect("/login", 302);
|
return redirect("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseHtml title="ConvertX | Converters">
|
<BaseHtml title="ConvertX | Converters">
|
||||||
<>
|
|
||||||
<Header loggedIn />
|
<Header loggedIn />
|
||||||
<main class="container">
|
<main class="container">
|
||||||
<article>
|
<article>
|
||||||
@@ -1190,8 +983,7 @@ const app = new Elysia({
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{Object.entries(getAllTargets()).map(
|
{Object.entries(getAllTargets()).map(([converter, targets]) => {
|
||||||
([converter, targets]) => {
|
|
||||||
const inputs = getAllInputs(converter);
|
const inputs = getAllInputs(converter);
|
||||||
return (
|
return (
|
||||||
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
|
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
|
||||||
@@ -1217,13 +1009,11 @@ const app = new Elysia({
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
},
|
})}
|
||||||
)}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</article>
|
</article>
|
||||||
</main>
|
</main>
|
||||||
</>
|
|
||||||
</BaseHtml>
|
</BaseHtml>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@@ -1232,12 +1022,12 @@ const app = new Elysia({
|
|||||||
async ({ params, jwt, redirect, cookie: { auth } }) => {
|
async ({ params, jwt, redirect, cookie: { auth } }) => {
|
||||||
// TODO: Implement zip download
|
// TODO: Implement zip download
|
||||||
if (!auth?.value) {
|
if (!auth?.value) {
|
||||||
return redirect("/login", 302);
|
return redirect("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await jwt.verify(auth.value);
|
const user = await jwt.verify(auth.value);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return redirect("/login", 302);
|
return redirect("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
const job = await db
|
const job = await db
|
||||||
@@ -1245,17 +1035,17 @@ const app = new Elysia({
|
|||||||
.get(user.id, params.jobId);
|
.get(user.id, params.jobId);
|
||||||
|
|
||||||
if (!job) {
|
if (!job) {
|
||||||
return redirect("/results", 302);
|
return redirect("/results");
|
||||||
}
|
}
|
||||||
|
|
||||||
// const userId = decodeURIComponent(params.userId);
|
const userId = decodeURIComponent(params.userId);
|
||||||
// const jobId = decodeURIComponent(params.jobId);
|
const jobId = decodeURIComponent(params.jobId);
|
||||||
// const outputPath = `${outputDir}${userId}/${jobId}/`;
|
const outputPath = `${outputDir}${userId}/${jobId}/`;
|
||||||
|
|
||||||
// return Bun.zip(outputPath);
|
// return Bun.zip(outputPath);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.onError(({ error }) => {
|
.onError(({ code, error, request }) => {
|
||||||
// log.error(` ${request.method} ${request.url}`, code, error);
|
// log.error(` ${request.method} ${request.url}`, code, error);
|
||||||
console.error(error);
|
console.error(error);
|
||||||
})
|
})
|
||||||
@@ -1266,14 +1056,11 @@ console.log(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const clearJobs = () => {
|
const clearJobs = () => {
|
||||||
|
// clear all jobs older than 24 hours
|
||||||
|
// get all files older than 24 hours
|
||||||
const jobs = db
|
const jobs = db
|
||||||
.query("SELECT * FROM jobs WHERE date_created < ?")
|
.query("SELECT * FROM jobs WHERE date_created < ?")
|
||||||
.as(Jobs)
|
.all(new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString()) as IJobs[];
|
||||||
.all(
|
|
||||||
new Date(
|
|
||||||
Date.now() - AUTO_DELETE_EVERY_N_HOURS * 60 * 60 * 1000,
|
|
||||||
).toISOString(),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const job of jobs) {
|
for (const job of jobs) {
|
||||||
// delete the directories
|
// delete the directories
|
||||||
@@ -1284,9 +1071,7 @@ const clearJobs = () => {
|
|||||||
db.query("DELETE FROM jobs WHERE id = ?").run(job.id);
|
db.query("DELETE FROM jobs WHERE id = ?").run(job.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(clearJobs, AUTO_DELETE_EVERY_N_HOURS * 60 * 60 * 1000);
|
// run every 24 hours
|
||||||
|
setTimeout(clearJobs, 24 * 60 * 60 * 1000);
|
||||||
};
|
};
|
||||||
|
clearJobs();
|
||||||
if (AUTO_DELETE_EVERY_N_HOURS > 0) {
|
|
||||||
clearJobs();
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
User-agent: *
|
|
||||||
Disallow: /
|
|
||||||
@@ -3,73 +3,7 @@ const fileInput = document.querySelector('input[type="file"]');
|
|||||||
const fileNames = [];
|
const fileNames = [];
|
||||||
let fileType;
|
let fileType;
|
||||||
|
|
||||||
const selectContainer = document.querySelector("form .select_container");
|
const selectContainer = document.querySelector("form > article");
|
||||||
|
|
||||||
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']");
|
// const convertFromSelect = document.querySelector("select[name='convert_from']");
|
||||||
|
|
||||||
@@ -115,7 +49,6 @@ fileInput.addEventListener("change", (e) => {
|
|||||||
.then((res) => res.text())
|
.then((res) => res.text())
|
||||||
.then((html) => {
|
.then((html) => {
|
||||||
selectContainer.innerHTML = html;
|
selectContainer.innerHTML = html;
|
||||||
updateSearchBar();
|
|
||||||
})
|
})
|
||||||
.catch((err) => console.log(err));
|
.catch((err) => console.log(err));
|
||||||
}
|
}
|
||||||
@@ -190,5 +123,3 @@ formConvert.addEventListener("submit", (e) => {
|
|||||||
const hiddenInput = document.querySelector("input[name='file_names']");
|
const hiddenInput = document.querySelector("input[name='file_names']");
|
||||||
hiddenInput.value = JSON.stringify(fileNames);
|
hiddenInput.value = JSON.stringify(fileNames);
|
||||||
});
|
});
|
||||||
|
|
||||||
updateSearchBar();
|
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
|
article {
|
||||||
|
/* height: 300px; */
|
||||||
|
/* width: 300px; */
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
div.icon {
|
div.icon {
|
||||||
height: 100px;
|
height: 100px;
|
||||||
width: 100px;
|
width: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
button[type="submit"] {
|
button[type="submit"] {
|
||||||
width: 50%;
|
width: 50%
|
||||||
}
|
}
|
||||||
|
|
||||||
div.center {
|
div.center {
|
||||||
@@ -13,47 +19,3 @@ div.center {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["ESNext"],
|
"lib": ["ESNext"],
|
||||||
"module": "ESNext",
|
"module": "esnext",
|
||||||
"target": "ES2021",
|
"target": "esnext",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
@@ -17,6 +17,9 @@
|
|||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
|
"types": [
|
||||||
|
"bun-types" // add Bun global
|
||||||
|
],
|
||||||
// non bun init
|
// non bun init
|
||||||
"plugins": [{ "name": "@kitajs/ts-html-plugin" }],
|
"plugins": [{ "name": "@kitajs/ts-html-plugin" }],
|
||||||
"noUncheckedIndexedAccess": true,
|
"noUncheckedIndexedAccess": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user