add graphicsmagick

This commit is contained in:
C4illin 2024-05-24 22:58:25 +02:00
parent 3e2338a7c5
commit 4cf06d9406
9 changed files with 706 additions and 87 deletions

View file

@ -1,5 +1,3 @@
# use the official Bun image
# see all versions at https://hub.docker.com/r/oven/bun/tags
FROM oven/bun:1-debian as base FROM oven/bun:1-debian as base
WORKDIR /app WORKDIR /app
@ -28,14 +26,20 @@ RUN cd /temp/prod && bun install --frozen-lockfile --production
# copy production dependencies and source code into final image # copy production dependencies and source code into final image
FROM base AS release FROM base AS release
# 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 \
ffmpeg \
graphicsmagick \
ghostscript
COPY --from=install /temp/prod/node_modules node_modules COPY --from=install /temp/prod/node_modules node_modules
# 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 . .
# install additional dependencies
RUN apt-get update && apt-get install -y pandoc texlive-latex-recommended ffmpeg
# run the app # run the app
USER bun USER bun
EXPOSE 3000/tcp EXPOSE 3000/tcp

41
README.md Normal file
View file

@ -0,0 +1,41 @@
# ConvertX
A self-hosted online file converter. Supports 708 different formats.
## Features
- Convert files to different formats
- Password protection
- Multiple accounts
## Converters supported
| Converter | Use case | Converts from | Converts to |
|----------------|---------------|---------------|-------------|
| Sharp | Images (fast) | 7 | 6 |
| Pandoc | Documents | 43 | 65 |
| GraphicsMagick | Images | 166 | 133 |
| FFmpeg | Video | 461 | 170 |
## Deployment
```yml
# docker-compose.yml
services:
convertx:
image: ghcr.io/c4illin/convertx:master
ports:
- "3000:3000"
environment: # Defaults are listed below
- ACCOUNT_REGISTRATION=false # true or false
volumes:
- /path/you/want:/app/data
```
<!-- or
```bash
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.

View file

@ -2,8 +2,8 @@
"name": "convertx-frontend", "name": "convertx-frontend",
"version": "1.0.50", "version": "1.0.50",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "dev": "bun run --watch src/index.tsx",
"dev": "bun run --hot src/index.tsx" "hot": "bun run --hot src/index.tsx"
}, },
"dependencies": { "dependencies": {
"@elysiajs/cookie": "^0.8.0", "@elysiajs/cookie": "^0.8.0",

View file

@ -659,16 +659,34 @@ export async function convert(
// biome-ignore lint/suspicious/noExplicitAny: <explanation> // biome-ignore lint/suspicious/noExplicitAny: <explanation>
options?: any, options?: any,
) { ) {
let command = "ffmpeg";
return exec( const notWorking = ["bmp"];
`ffmpeg -f "${fileType}" -i "${filePath}" -f "${convertTo}" "${targetPath}"`,
(error, stdout, stderr) => { if (!(fileType in notWorking)) {
command += ` -f "${fileType}"`;
}
command += ` -i "${filePath}"`;
if (!(convertTo in notWorking)) {
command += ` -f "${convertTo}"`;
}
command += " ${targetPath}";
return exec(command, (error, stdout, stderr) => {
if (error) { if (error) {
console.error(`exec error: ${error}`); console.error(`exec error: ${error}`);
return; return;
} }
if (stdout) {
console.log(`stdout: ${stdout}`); console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`); console.error(`stderr: ${stderr}`);
}, }
); });
} }

View file

@ -0,0 +1,335 @@
import { exec } from "node:child_process";
export const properties = {
from: {
image: [
"3fr",
"8bim",
"8bimtext",
"8bimwtext",
"app1",
"app1jpeg",
"art",
"arw",
"avs",
"b",
"bie",
"bigtiff",
"bmp",
"c",
"cals",
"caption",
"cin",
"cmyk",
"cmyka",
"cr2",
"crw",
"cur",
"cut",
"dcm",
"dcr",
"dcx",
"dng",
"dpx",
"epdf",
"epi",
"eps",
"epsf",
"epsi",
"ept",
"ept2",
"ept3",
"erf",
"exif",
"fax",
"file",
"fits",
"fractal",
"ftp",
"g",
"gif",
"gif87",
"gradient",
"gray",
"graya",
"heic",
"heif",
"hrz",
"http",
"icb",
"icc",
"icm",
"ico",
"icon",
"identity",
"image",
"iptc",
"iptctext",
"iptcwtext",
"jbg",
"jbig",
"jng",
"jnx",
"jpeg",
"jpg",
"k",
"k25",
"kdc",
"label",
"m",
"mac",
"map",
"mat",
"mef",
"miff",
"mng",
"mono",
"mpc",
"mrw",
"msl",
"mtv",
"mvg",
"nef",
"null",
"o",
"orf",
"otb",
"p7",
"pal",
"palm",
"pam",
"pbm",
"pcd",
"pcds",
"pct",
"pcx",
"pdb",
"pdf",
"pef",
"pfa",
"pfb",
"pgm",
"picon",
"pict",
"pix",
"plasma",
"png",
"png00",
"png24",
"png32",
"png48",
"png64",
"png8",
"pnm",
"ppm",
"ps",
"ptif",
"pwp",
"r",
"raf",
"ras",
"rgb",
"rgba",
"rla",
"rle",
"sct",
"sfw",
"sgi",
"sr2",
"srf",
"stegano",
"sun",
"svg",
"svgz",
"text",
"tga",
"tiff",
"tile",
"tim",
"topol",
"ttf",
"txt",
"uyvy",
"vda",
"vicar",
"vid",
"viff",
"vst",
"wbmp",
"webp",
"wmf",
"wpg",
"x3f",
"xbm",
"xc",
"xcf",
"xmp",
"xpm",
"xv",
"xwd",
"y",
"yuv",
],
},
to: {
image: [
"8bim",
"8bimtext",
"8bimwtext",
"app1",
"app1jpeg",
"art",
"avs",
"b",
"bie",
"bigtiff",
"bmp",
"bmp2",
"bmp3",
"brf",
"c",
"cals",
"cin",
"cmyk",
"cmyka",
"dcx",
"dpx",
"epdf",
"epi",
"eps",
"eps2",
"eps3",
"epsf",
"epsi",
"ept",
"ept2",
"ept3",
"exif",
"fax",
"fits",
"g",
"gif",
"gif87",
"gray",
"graya",
"histogram",
"html",
"icb",
"icc",
"icm",
"info",
"iptc",
"iptctext",
"iptcwtext",
"isobrl",
"isobrl6",
"jbg",
"jbig",
"jng",
"jpeg",
"jpg",
"k",
"m",
"m2v",
"map",
"mat",
"matte",
"miff",
"mng",
"mono",
"mpc",
"mpeg",
"mpg",
"msl",
"mtv",
"mvg",
"null",
"o",
"otb",
"p7",
"pal",
"pam",
"pbm",
"pcd",
"pcds",
"pcl",
"pct",
"pcx",
"pdb",
"pdf",
"pgm",
"picon",
"pict",
"png",
"png00",
"png24",
"png32",
"png48",
"png64",
"png8",
"pnm",
"ppm",
"preview",
"ps",
"ps2",
"ps3",
"ptif",
"r",
"ras",
"rgb",
"rgba",
"sgi",
"shtml",
"sun",
"text",
"tga",
"tiff",
"txt",
"ubrl",
"ubrl6",
"uil",
"uyvy",
"vda",
"vicar",
"vid",
"viff",
"vst",
"wbmp",
"webp",
"x",
"xbm",
"xmp",
"xpm",
"xv",
"xwd",
"y",
"yuv",
],
},
};
export function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
options?: any,
) {
return exec(
`gm convert "${filePath}" "${targetPath}"`,
(error, stdout, stderr) => {
if (error) {
console.error(`exec error: ${error}`);
return;
}
if (stdout) {
console.log(`stdout: ${stdout}`);
}
if (stderr) {
console.error(`stderr: ${stderr}`);
}
},
);
}

View file

@ -9,10 +9,15 @@ import {
} from "./pandoc"; } from "./pandoc";
import { import {
properties as propertiesFfmpeg, properties as propertiesFFmpeg,
convert as convertFfmpeg, convert as convertFFmpeg,
} from "./ffmpeg"; } from "./ffmpeg";
import {
properties as propertiesGraphicsmagick,
convert as convertGraphicsmagick,
} from "./graphicsmagick";
const properties: { const properties: {
[key: string]: { [key: string]: {
properties: { properties: {
@ -48,9 +53,13 @@ const properties: {
properties: propertiesPandoc, properties: propertiesPandoc,
converter: convertPandoc, converter: convertPandoc,
}, },
graphicsmagick: {
properties: propertiesGraphicsmagick,
converter: convertGraphicsmagick,
},
ffmpeg: { ffmpeg: {
properties: propertiesFfmpeg, properties: propertiesFFmpeg,
converter: convertFfmpeg, converter: convertFFmpeg,
}, },
}; };
@ -86,6 +95,7 @@ export async function mainConverter(
for (const key in converterObj.properties.from) { for (const key in converterObj.properties.from) {
if ( if (
// HOW??
converterObj.properties.from[key].includes(fileType) && converterObj.properties.from[key].includes(fileType) &&
converterObj.properties.to[key].includes(convertTo) converterObj.properties.to[key].includes(convertTo)
) { ) {
@ -122,7 +132,7 @@ export async function mainConverter(
} }
} }
const possibleConversions: { [key: string]: { [key: string]: string[] } } = {}; const possibleTargets: { [key: string]: { [key: string]: string[] } } = {};
for (const converterName in properties) { for (const converterName in properties) {
const converterProperties = properties[converterName]?.properties; const converterProperties = properties[converterName]?.properties;
@ -137,33 +147,47 @@ for (const converterName in properties) {
} }
for (const extension of converterProperties.from[key] ?? []) { for (const extension of converterProperties.from[key] ?? []) {
if (!possibleConversions[extension]) { if (!possibleTargets[extension]) {
possibleConversions[extension] = {}; possibleTargets[extension] = {};
} }
possibleConversions[extension][converterName] =
possibleTargets[extension][converterName] =
converterProperties.to[key] || []; converterProperties.to[key] || [];
} }
} }
} }
// // save all possible conversions to a file export const getPossibleTargets = (
// import fs from "fs";
// import path from "path";
// import { FormatEnum } from "sharp";
// fs.writeFileSync(
// path.join(__dirname, ".", "possibleConversions.json"),
// JSON.stringify(possibleConversions),
// );
export const getPossibleConversions = (
from: string, from: string,
): { [key: string]: string[] } => { ): { [key: string]: string[] } => {
const fromClean = normalizeFiletype(from); const fromClean = normalizeFiletype(from);
return possibleConversions[fromClean] || {}; return possibleTargets[fromClean] || {};
}; };
const allTargets: { [key: string]: string[]} = {}; const possibleInputs: string[] = [];
for (const converterName in properties) {
const converterProperties = properties[converterName]?.properties;
if (!converterProperties) {
continue;
}
for (const key in converterProperties.from) {
for (const extension of converterProperties.from[key] ?? []) {
if (!possibleInputs.includes(extension)) {
possibleInputs.push(extension);
}
}
}
}
possibleInputs.sort();
export const getPossibleInputs = () => {
return possibleInputs;
};
const allTargets: { [key: string]: string[] } = {};
for (const converterName in properties) { for (const converterName in properties) {
const converterProperties = properties[converterName]?.properties; const converterProperties = properties[converterName]?.properties;
@ -184,3 +208,50 @@ for (const converterName in properties) {
export const getAllTargets = () => { export const getAllTargets = () => {
return allTargets; return allTargets;
}; };
const allInputs: { [key: string]: string[] } = {};
for (const converterName in properties) {
const converterProperties = properties[converterName]?.properties;
if (!converterProperties) {
continue;
}
for (const key in converterProperties.from) {
if (allInputs[converterName]) {
allInputs[converterName].push(...converterProperties.from[key]);
} else {
allInputs[converterName] = converterProperties.from[key];
}
}
}
export const getAllInputs = (converter: string) => {
return allInputs[converter] || [];
};
// // count the number of unique formats
// const uniqueFormats = new Set();
// for (const converterName in properties) {
// const converterProperties = properties[converterName]?.properties;
// if (!converterProperties) {
// continue;
// }
// for (const key in converterProperties.from) {
// for (const extension of converterProperties.from[key] ?? []) {
// uniqueFormats.add(extension);
// }
// }
// for (const key in converterProperties.to) {
// for (const extension of converterProperties.to[key] ?? []) {
// uniqueFormats.add(extension);
// }
// }
// }
// // print the number of unique Inputs and Outputs
// console.log(`Unique Formats: ${uniqueFormats.size}`);

File diff suppressed because one or more lines are too long

View file

@ -10,8 +10,10 @@ import { BaseHtml } from "./components/base";
import { Header } from "./components/header"; import { Header } from "./components/header";
import { import {
mainConverter, mainConverter,
getPossibleConversions, getPossibleTargets,
getPossibleInputs,
getAllTargets, getAllTargets,
getAllInputs,
} from "./converters/main"; } from "./converters/main";
import { normalizeFiletype } from "./helpers/normalizeFiletype"; import { normalizeFiletype } from "./helpers/normalizeFiletype";
@ -19,7 +21,7 @@ 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 accountRegistration = const ACCOUNT_REGISTRATION =
process.env.ACCOUNT_REGISTRATION === "true" || false; process.env.ACCOUNT_REGISTRATION === "true" || false;
// fileNames: fileNames, // fileNames: fileNames,
@ -50,6 +52,8 @@ CREATE TABLE IF NOT EXISTS jobs (
FOREIGN KEY (user_id) REFERENCES users(id) FOREIGN KEY (user_id) REFERENCES users(id)
);`); );`);
let FIRST_RUN = db.query("SELECT * FROM users").get() === null || false;
interface IUser { interface IUser {
id: number; id: number;
email: string; email: string;
@ -94,7 +98,54 @@ const app = new Elysia()
prefix: "/", prefix: "/",
}), }),
) )
.get("/register", () => { .get("/setup", ({ redirect }) => {
if (!FIRST_RUN) {
return redirect("/login");
}
return (
<BaseHtml title="ConvertX | Setup">
<main class="container">
<h1>Welcome to ConvertX</h1>
<article>
<header>Create your account</header>
<form method="post" action="/register">
<fieldset>
<label>
Email/Username
<input
type="email"
name="email"
placeholder="Email"
required
/>
</label>
<label>
Password
<input
type="password"
name="password"
placeholder="Password"
required
/>
</label>
</fieldset>
<input type="submit" value="Create account" />
</form>
<footer>
Report any issues on{" "}
<a href="https://github.com/C4illin/ConvertX">GitHub</a>.
</footer>
</article>
</main>
</BaseHtml>
);
})
.get("/register", ({ redirect }) => {
if (!ACCOUNT_REGISTRATION) {
return redirect("/login");
}
return ( return (
<BaseHtml title="ConvertX | Register"> <BaseHtml title="ConvertX | Register">
<Header /> <Header />
@ -130,7 +181,15 @@ const app = new Elysia()
}) })
.post( .post(
"/register", "/register",
async ({ body, set, jwt, cookie: { auth } }) => { async ({ body, set, redirect, jwt, cookie: { auth } }) => {
if (!ACCOUNT_REGISTRATION && !FIRST_RUN) {
return redirect("/login");
}
if (FIRST_RUN) {
FIRST_RUN = false;
}
const existingUser = await db const existingUser = await db
.query("SELECT * FROM users WHERE email = ?") .query("SELECT * FROM users WHERE email = ?")
.get(body.email); .get(body.email);
@ -171,20 +230,18 @@ const app = new Elysia()
sameSite: "strict", sameSite: "strict",
}); });
// redirect to home redirect("/");
set.status = 302;
set.headers = {
Location: "/",
};
}, },
{ 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 } }) => {
console.log("login handler"); if (FIRST_RUN) {
return redirect("/setup");
}
// if already logged in, redirect to home // if already logged in, redirect to home
if (auth?.value) { if (auth?.value) {
const user = await jwt.verify(auth.value); const user = await jwt.verify(auth.value);
console.log(user);
if (user) { if (user) {
return redirect("/"); return redirect("/");
@ -296,6 +353,10 @@ const app = new Elysia()
return redirect("/login"); return redirect("/login");
}) })
.get("/", async ({ jwt, redirect, cookie: { auth, jobId } }) => { .get("/", async ({ jwt, redirect, cookie: { auth, jobId } }) => {
if (FIRST_RUN) {
return redirect("/setup");
}
if (!auth?.value) { if (!auth?.value) {
return redirect("/login"); return redirect("/login");
} }
@ -347,8 +408,20 @@ const app = new Elysia()
<main class="container"> <main class="container">
<article> <article>
<h1>Convert</h1> <h1>Convert</h1>
<table id="file-list" /> <div style={{ maxHeight: "50vh", overflowY: "auto" }}>
<table id="file-list" class="striped" />
</div>
<input type="file" name="file" multiple /> <input type="file" name="file" multiple />
{/* <label for="convert_from">Convert from</label> */}
<select name="convert_from" aria-label="Convert from" required>
<option selected disabled value="">
Convert from
</option>
{getPossibleInputs().map((input) => (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<option>{input}</option>
))}
</select>
</article> </article>
<form method="post" action="/convert"> <form method="post" action="/convert">
<input type="hidden" name="file_names" id="file_names" /> <input type="hidden" name="file_names" id="file_names" />
@ -378,13 +451,13 @@ const app = new Elysia()
.post( .post(
"/conversions", "/conversions",
({ body }) => { ({ body }) => {
console.log(body);
return ( return (
<select name="convert_to" aria-label="Convert to" required> <select name="convert_to" aria-label="Convert to" required>
<option selected disabled value=""> <option selected disabled value="">
Convert to Convert to
</option> </option>
{Object.entries(getPossibleConversions(body.fileType)).map(([converter, targets]) => ( {Object.entries(getPossibleTargets(body.fileType)).map(
([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) => (
@ -392,7 +465,8 @@ const app = new Elysia()
<option value={`${target},${converter}`}>{target}</option> <option value={`${target},${converter}`}>{target}</option>
))} ))}
</optgroup> </optgroup>
))} ),
)}
</select> </select>
); );
}, },
@ -513,7 +587,9 @@ const app = new Elysia()
); );
} }
const convertTo = normalizeFiletype(body.convert_to.split(",")[0] as string); 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[];
@ -532,16 +608,25 @@ const app = new Elysia()
); );
// Start the conversion process in the background // Start the conversion process in the background
Promise.all(fileNames.map(async (fileName) => { Promise.all(
fileNames.map(async (fileName) => {
const filePath = `${userUploadsDir}${fileName}`; const filePath = `${userUploadsDir}${fileName}`;
const fileTypeOrig = fileName.split(".").pop() as string; const fileTypeOrig = fileName.split(".").pop() as string;
const fileType = normalizeFiletype(fileTypeOrig); const fileType = normalizeFiletype(fileTypeOrig);
const newFileName = fileName.replace(fileTypeOrig, convertTo); const newFileName = fileName.replace(fileTypeOrig, convertTo);
const targetPath = `${userOutputDir}${newFileName}`; const targetPath = `${userOutputDir}${newFileName}`;
await mainConverter(filePath, fileType, convertTo, targetPath, {}, converterName); await mainConverter(
filePath,
fileType,
convertTo,
targetPath,
{},
converterName,
);
query.run(jobId.value, fileName, newFileName); query.run(jobId.value, fileName, newFileName);
})) }),
)
.then(() => { .then(() => {
// All conversions are done, update the job status to 'completed' // All conversions are done, update the job status to 'completed'
db.run( db.run(
@ -550,7 +635,7 @@ const app = new Elysia()
); );
}) })
.catch((error) => { .catch((error) => {
console.error('Error in conversion process:', error); console.error("Error in conversion process:", error);
}); });
// Redirect the client immediately // Redirect the client immediately
@ -564,20 +649,16 @@ const app = new Elysia()
}, },
) )
.get("/test", async ({ jwt, redirect, cookie: { auth } }) => { .get("/test", async ({ jwt, redirect, cookie: { auth } }) => {
console.log("results page");
if (!auth?.value) { if (!auth?.value) {
console.log("no auth value");
return redirect("/login"); return redirect("/login");
} }
const user = await jwt.verify(auth.value); const user = await jwt.verify(auth.value);
if (!user) { if (!user) {
console.log("no user");
return redirect("/login"); return redirect("/login");
} }
const userJobs = db let userJobs = db
.query("SELECT * FROM jobs WHERE user_id = ?") .query("SELECT * FROM jobs WHERE user_id = ?")
.all(user.id) as IJobs[]; .all(user.id) as IJobs[];
@ -589,6 +670,9 @@ const app = new Elysia()
job.finished_files = files.length; job.finished_files = files.length;
} }
// filter out jobs with no files
userJobs = userJobs.filter((job) => job.num_files > 0);
return ( return (
<BaseHtml title="ConvertX | Results"> <BaseHtml title="ConvertX | Results">
<Header loggedIn /> <Header loggedIn />
@ -741,6 +825,62 @@ const app = new Elysia()
return Bun.file(filePath); return Bun.file(filePath);
}, },
) )
.get("/converters", async ({ params, jwt, redirect, cookie: { auth } }) => {
if (!auth?.value) {
return redirect("/login");
}
const user = await jwt.verify(auth.value);
if (!user) {
return redirect("/login");
}
return (
<BaseHtml title="ConvertX | Converters">
<Header loggedIn />
<main class="container">
<article>
<h1>Converters</h1>
<table>
<thead>
<tr>
<th>Converter</th>
<th>From (Count)</th>
<th>To (Count)</th>
</tr>
</thead>
<tbody>
{Object.entries(getAllTargets()).map(([converter, targets]) => {
const inputs = getAllInputs(converter);
return (
<tr key={converter}>
<td>{converter}</td>
<td>
Count: {inputs.length}
<ul>
{inputs.map((input, index) => (
<li key={index}>{input}</li>
))}
</ul>
</td>
<td>
Count: {targets.length}
<ul>
{targets.map((target, index) => (
<li key={index}>{target}</li>
))}
</ul>
</td>
</tr>
);
})}
</tbody>
</table>
</article>
</main>
</BaseHtml>
);
})
.get( .get(
"/zip/:userId/:jobId", "/zip/:userId/:jobId",
async ({ params, jwt, redirect, cookie: { auth } }) => { async ({ params, jwt, redirect, cookie: { auth } }) => {

View file

@ -5,6 +5,8 @@ let fileType;
const selectElem = document.querySelector("select[name='convert_to']"); const selectElem = document.querySelector("select[name='convert_to']");
const convertFromSelect = document.querySelector("select[name='convert_from']");
// Add a 'change' event listener to the file input element // Add a 'change' event listener to the file input element
fileInput.addEventListener("change", (e) => { fileInput.addEventListener("change", (e) => {
console.log(e.target.files); console.log(e.target.files);
@ -20,8 +22,8 @@ fileInput.addEventListener("change", (e) => {
const row = document.createElement("tr"); const row = document.createElement("tr");
row.innerHTML = ` row.innerHTML = `
<td>${file.name}</td> <td>${file.name}</td>
<td>${(file.size / 1024 / 1024).toFixed(2)} MB</td> <td>${(file.size / 1024).toFixed(2)} kB</td>
<td><button class="secondary" onclick="deleteRow(this)">x</button></td> <td><a class="secondary" onclick="deleteRow(this)" style="cursor: pointer">Remove</a></td>
`; `;
if (!fileType) { if (!fileType) {
@ -30,6 +32,15 @@ fileInput.addEventListener("change", (e) => {
fileInput.setAttribute("accept", `.${fileType}`); fileInput.setAttribute("accept", `.${fileType}`);
setTitle(); setTitle();
// choose the option that matches the file type
for (const option of convertFromSelect.children) {
console.log(option.value);
if (option.value === fileType) {
option.selected = true;
}
}
fetch("/conversions", { fetch("/conversions", {
method: "POST", method: "POST",
body: JSON.stringify({ fileType: fileType }), body: JSON.stringify({ fileType: fileType }),
@ -57,7 +68,7 @@ fileInput.addEventListener("change", (e) => {
const setTitle = () => { const setTitle = () => {
const title = document.querySelector("h1"); const title = document.querySelector("h1");
title.textContent = `Convert ${fileType ? `.${fileType}` : "___"}`; title.textContent = `Convert ${fileType ? `.${fileType}` : ""}`;
}; };
// Add a onclick for the delete button // Add a onclick for the delete button