feature: add download all file by file alongside the tar download

This commit is contained in:
C4illin 2025-10-07 18:50:31 +00:00
parent 27e9b7eb35
commit 48e5e8a71c
20 changed files with 79 additions and 45 deletions

View file

@ -1,5 +1,5 @@
{ {
"name": "ConvertX Development Environment", "name": "ConvertX",
"build": { "build": {
"dockerfile": "Dockerfile" "dockerfile": "Dockerfile"
}, },
@ -46,5 +46,8 @@
}, },
"postCreateCommand": "bun install", "postCreateCommand": "bun install",
"remoteUser": "root", "remoteUser": "root",
"mounts": ["source=${localWorkspaceFolder}/data,target=/app/data,type=bind"] "mounts": ["source=${localWorkspaceFolder}/data,target=/app/data,type=bind"],
"containerEnv": {
"JWT_SECRET": "jwt_secret_only_used_in_testing_for_easier_hot_reloading"
}
} }

View file

@ -22,3 +22,17 @@ const refreshData = () => {
}; };
refreshData(); refreshData();
window.downloadAll = function () {
// Get all download links
const downloadLinks = document.querySelectorAll("a[download]");
// Trigger download for each link
downloadLinks.forEach((link, index) => {
// We add a delay for each download to prevent them from starting at the same time
setTimeout(() => {
const event = new MouseEvent("click");
link.dispatchEvent(event);
}, index * 300);
});
};

1
reset.d.ts vendored
View file

@ -1 +0,0 @@
import "@total-typescript/ts-reset";

View file

@ -1,4 +1,3 @@
import { Html } from "@elysiajs/html";
import { version } from "../../package.json"; import { version } from "../../package.json";
export const BaseHtml = ({ export const BaseHtml = ({

View file

@ -1,5 +1,3 @@
import { Html } from "@kitajs/html";
export const Header = ({ export const Header = ({
loggedIn, loggedIn,
accountRegistration, accountRegistration,

18
src/icons/download.tsx Normal file
View file

@ -0,0 +1,18 @@
export function DownloadIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
class={`size-6`}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3"
/>
</svg>
);
}

19
src/icons/eye.tsx Normal file
View file

@ -0,0 +1,19 @@
export function EyeIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
class={`size-6`}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z"
/>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg>
);
}

View file

@ -1,4 +1,3 @@
import { Html } from "@elysiajs/html";
import Elysia, { t } from "elysia"; import Elysia, { t } from "elysia";
import { getPossibleTargets } from "../converters/main"; import { getPossibleTargets } from "../converters/main";
import { userService } from "./user"; import { userService } from "./user";

View file

@ -1,4 +1,3 @@
import { Html } from "@elysiajs/html";
import { Elysia } from "elysia"; import { Elysia } from "elysia";
import { BaseHtml } from "../components/base"; import { BaseHtml } from "../components/base";
import { Header } from "../components/header"; import { Header } from "../components/header";

View file

@ -1,4 +1,3 @@
import { Html } from "@elysiajs/html";
import Elysia from "elysia"; import Elysia from "elysia";
import { BaseHtml } from "../components/base"; import { BaseHtml } from "../components/base";
import { Header } from "../components/header"; import { Header } from "../components/header";

View file

@ -1,4 +1,3 @@
import { Html } from "@elysiajs/html";
import { JWTPayloadSpec } from "@elysiajs/jwt"; import { JWTPayloadSpec } from "@elysiajs/jwt";
import { Elysia } from "elysia"; import { Elysia } from "elysia";
import { BaseHtml } from "../components/base"; import { BaseHtml } from "../components/base";
@ -6,6 +5,8 @@ import { Header } from "../components/header";
import db from "../db/db"; import db from "../db/db";
import { Filename, Jobs } from "../db/types"; import { Filename, Jobs } from "../db/types";
import { ALLOW_UNAUTHENTICATED, WEBROOT } from "../helpers/env"; import { ALLOW_UNAUTHENTICATED, WEBROOT } from "../helpers/env";
import { DownloadIcon } from "../icons/download";
import { EyeIcon } from "../icons/eye";
import { userService } from "./user"; import { userService } from "./user";
function ResultsArticle({ function ResultsArticle({
@ -25,25 +26,24 @@ function ResultsArticle({
<article class="article"> <article class="article">
<div class="mb-4 flex items-center justify-between"> <div class="mb-4 flex items-center justify-between">
<h1 class="text-xl">Results</h1> <h1 class="text-xl">Results</h1>
<div> <div class="flex flex-row gap-4">
<a <a
style={files.length !== job.num_files ? "pointer-events: none;" : ""} style={files.length !== job.num_files ? "pointer-events: none;" : ""}
href={`${WEBROOT}/archive/${user.id}/${job.id}`} href={`${WEBROOT}/archive/${user.id}/${job.id}`}
download={`converted_files_${job.id}.tar`} download={`converted_files_${job.id}.tar`}
class="flex btn-primary flex-row gap-2 text-contrast"
{...(files.length !== job.num_files ? { disabled: true, "aria-busy": "true" } : "")}
> >
<button <DownloadIcon /> <p>Tar</p>
type="button"
class="float-right w-40 btn-primary"
{...(files.length !== job.num_files ? { disabled: true, "aria-busy": "true" } : "")}
>
{files.length === job.num_files ? "Download All" : "Converting..."}
</button>
</a> </a>
<button class="flex btn-primary flex-row gap-2 text-contrast" onclick="downloadAll()">
<DownloadIcon /> <p>All</p>
</button>
</div> </div>
</div> </div>
<progress <progress
max={job.num_files} max={job.num_files}
value={files.length} {...(files.length === job.num_files ? { value: files.length } : "")}
class={` class={`
mb-4 inline-block h-2 w-full appearance-none overflow-hidden rounded-full border-0 mb-4 inline-block h-2 w-full appearance-none overflow-hidden rounded-full border-0
bg-neutral-700 bg-none text-accent-500 accent-accent-500 bg-neutral-700 bg-none text-accent-500 accent-accent-500
@ -84,15 +84,7 @@ function ResultsArticle({
sm:px-4 sm:px-4
`} `}
> >
View Actions
</th>
<th
class={`
px-2 py-2
sm:px-4
`}
>
Download
</th> </th>
</tr> </tr>
</thead> </thead>
@ -103,7 +95,7 @@ function ResultsArticle({
{file.output_file_name} {file.output_file_name}
</td> </td>
<td safe>{file.status}</td> <td safe>{file.status}</td>
<td> <td class="flex flex-row gap-4">
<a <a
class={` class={`
text-accent-500 underline text-accent-500 underline
@ -111,10 +103,8 @@ function ResultsArticle({
`} `}
href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`} href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
> >
View <EyeIcon />
</a> </a>
</td>
<td>
<a <a
class={` class={`
text-accent-500 underline text-accent-500 underline
@ -123,7 +113,7 @@ function ResultsArticle({
href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`} href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
download={file.output_file_name} download={file.output_file_name}
> >
Download <DownloadIcon />
</a> </a>
</td> </td>
</tr> </tr>

View file

@ -1,5 +1,4 @@
import { randomInt } from "node:crypto"; import { randomInt } from "node:crypto";
import { Html } from "@elysiajs/html";
import { JWTPayloadSpec } from "@elysiajs/jwt"; import { JWTPayloadSpec } from "@elysiajs/jwt";
import { Elysia, t } from "elysia"; import { Elysia, t } from "elysia";
import { BaseHtml } from "../components/base"; import { BaseHtml } from "../components/base";

View file

@ -1,5 +1,4 @@
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import { Html } from "@elysiajs/html";
import { jwt } from "@elysiajs/jwt"; import { jwt } from "@elysiajs/jwt";
import { Elysia, t } from "elysia"; import { Elysia, t } from "elysia";
import { BaseHtml } from "../components/base"; import { BaseHtml } from "../components/base";

View file

@ -1,5 +1,5 @@
import type { ExecFileException } from "node:child_process";
import { beforeEach, expect, test } from "bun:test"; import { beforeEach, expect, test } from "bun:test";
import type { ExecFileException } from "node:child_process";
import { convert } from "../../src/converters/dvisvgm"; import { convert } from "../../src/converters/dvisvgm";
import { ExecFileFn } from "../../src/converters/types"; import { ExecFileFn } from "../../src/converters/types";
import { runCommonTests } from "./helpers/commonTests"; import { runCommonTests } from "./helpers/commonTests";

View file

@ -1,5 +1,5 @@
import type { ExecFileException } from "node:child_process";
import { expect } from "bun:test"; import { expect } from "bun:test";
import type { ExecFileException } from "node:child_process";
import { ConvertFnWithExecFile, ExecFileFn } from "../../../src/converters/types"; import { ConvertFnWithExecFile, ExecFileFn } from "../../../src/converters/types";
export async function runConvertSuccessTest(convertFn: ConvertFnWithExecFile) { export async function runConvertSuccessTest(convertFn: ConvertFnWithExecFile) {

View file

@ -1,5 +1,5 @@
import type { ExecFileException } from "node:child_process";
import { beforeEach, expect, test } from "bun:test"; import { beforeEach, expect, test } from "bun:test";
import type { ExecFileException } from "node:child_process";
import { convert } from "../../src/converters/imagemagick"; import { convert } from "../../src/converters/imagemagick";
import { ExecFileFn } from "../../src/converters/types"; import { ExecFileFn } from "../../src/converters/types";
import { runCommonTests } from "./helpers/commonTests"; import { runCommonTests } from "./helpers/commonTests";

View file

@ -1,5 +1,5 @@
import type { ExecFileException } from "node:child_process";
import { beforeEach, expect, test } from "bun:test"; import { beforeEach, expect, test } from "bun:test";
import type { ExecFileException } from "node:child_process";
import { convert } from "../../src/converters/libjxl"; import { convert } from "../../src/converters/libjxl";
import { ExecFileFn } from "../../src/converters/types"; import { ExecFileFn } from "../../src/converters/types";
import { runCommonTests } from "./helpers/commonTests"; import { runCommonTests } from "./helpers/commonTests";

View file

@ -1,5 +1,5 @@
import type { ExecFileException } from "node:child_process";
import { expect, test } from "bun:test"; import { expect, test } from "bun:test";
import type { ExecFileException } from "node:child_process";
import { convert } from "../../src/converters/msgconvert"; import { convert } from "../../src/converters/msgconvert";
import { ExecFileFn } from "../../src/converters/types"; import { ExecFileFn } from "../../src/converters/types";

View file

@ -1,5 +1,5 @@
import type { ExecFileException } from "node:child_process";
import { beforeEach, expect, test } from "bun:test"; import { beforeEach, expect, test } from "bun:test";
import type { ExecFileException } from "node:child_process";
import { ExecFileFn } from "../../src/converters/types"; import { ExecFileFn } from "../../src/converters/types";
import { convert } from "../../src/converters/vips"; import { convert } from "../../src/converters/vips";
import { runCommonTests } from "./helpers/commonTests"; import { runCommonTests } from "./helpers/commonTests";

View file

@ -12,9 +12,8 @@
"strict": true, "strict": true,
"downlevelIteration": true, "downlevelIteration": true,
"skipLibCheck": true, "skipLibCheck": true,
"jsx": "react", "jsx": "react-jsx",
"jsxFactory": "Html.createElement", "jsxImportSource": "@kitajs/html",
"jsxFragmentFactory": "Html.Fragment",
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"allowJs": true, "allowJs": true,
@ -30,5 +29,5 @@
"esModuleInterop": true "esModuleInterop": true
// "noImplicitReturns": true // "noImplicitReturns": true
}, },
"include": ["src", "tests", "package.json", "reset.d.ts"] "include": ["src", "tests", "package.json"]
} }