Merge aa39368427 into c8551d9548
This commit is contained in:
commit
45b94cdf41
|
|
@ -2,10 +2,13 @@ const webroot = document.querySelector("meta[name='webroot']").content;
|
||||||
const fileInput = document.querySelector('input[type="file"]');
|
const fileInput = document.querySelector('input[type="file"]');
|
||||||
const dropZone = document.getElementById("dropzone");
|
const dropZone = document.getElementById("dropzone");
|
||||||
const convertButton = document.querySelector("input[type='submit']");
|
const convertButton = document.querySelector("input[type='submit']");
|
||||||
|
const supportedSourcesEl = document.getElementById("supported-sources");
|
||||||
|
|
||||||
const fileNames = [];
|
const fileNames = [];
|
||||||
let fileType;
|
let fileType;
|
||||||
let pendingFiles = 0;
|
let pendingFiles = 0;
|
||||||
let formatSelected = false;
|
let formatSelected = false;
|
||||||
|
let latestSourcesRequestKey = null;
|
||||||
|
|
||||||
dropZone.addEventListener("dragover", (e) => {
|
dropZone.addEventListener("dragover", (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -71,6 +74,44 @@ function handleFile(file) {
|
||||||
|
|
||||||
const selectContainer = document.querySelector("form .select_container");
|
const selectContainer = document.querySelector("form .select_container");
|
||||||
|
|
||||||
|
// Fetches the supported source types for a given target extension +
|
||||||
|
// converter pair, and renders them above the Convert button. Each request
|
||||||
|
// is tagged with a key (extension::converter) so that a slower, stale
|
||||||
|
// response — from an earlier selection, even one with the same extension
|
||||||
|
// but a different converter — can never overwrite what the most recently
|
||||||
|
// selected target should show.
|
||||||
|
const showSupportedSources = (targetExtension, converterName) => {
|
||||||
|
if (!supportedSourcesEl) return;
|
||||||
|
|
||||||
|
const requestKey = `${targetExtension}::${converterName}`;
|
||||||
|
latestSourcesRequestKey = requestKey;
|
||||||
|
|
||||||
|
fetch(`${webroot}/convert-sources`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ to: targetExtension }),
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((sourcesByConverter) => {
|
||||||
|
if (latestSourcesRequestKey !== requestKey) return;
|
||||||
|
|
||||||
|
const sources = sourcesByConverter[converterName] || [];
|
||||||
|
const unique = [...new Set(sources)].sort();
|
||||||
|
|
||||||
|
if (unique.length === 0) {
|
||||||
|
supportedSourcesEl.textContent = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
supportedSourcesEl.textContent = `Supported source types: ${unique.join(", ")}`;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (latestSourcesRequestKey !== requestKey) return;
|
||||||
|
console.error(err);
|
||||||
|
supportedSourcesEl.textContent = "";
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const updateSearchBar = () => {
|
const updateSearchBar = () => {
|
||||||
const convertToInput = document.querySelector("input[name='convert_to_search']");
|
const convertToInput = document.querySelector("input[name='convert_to_search']");
|
||||||
const convertToPopup = document.querySelector(".convert_to_popup");
|
const convertToPopup = document.querySelector(".convert_to_popup");
|
||||||
|
|
@ -117,6 +158,7 @@ const updateSearchBar = () => {
|
||||||
convertButton.disabled = false;
|
convertButton.disabled = false;
|
||||||
}
|
}
|
||||||
showMatching("");
|
showMatching("");
|
||||||
|
showSupportedSources(target.dataset.target, target.dataset.converter);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -131,6 +173,8 @@ const updateSearchBar = () => {
|
||||||
// when the user clears the search bar using the 'x' button
|
// when the user clears the search bar using the 'x' button
|
||||||
convertButton.disabled = true;
|
convertButton.disabled = true;
|
||||||
formatSelected = false;
|
formatSelected = false;
|
||||||
|
latestSourcesRequestKey = null;
|
||||||
|
if (supportedSourcesEl) supportedSourcesEl.textContent = "";
|
||||||
});
|
});
|
||||||
|
|
||||||
convertToInput.addEventListener("blur", (e) => {
|
convertToInput.addEventListener("blur", (e) => {
|
||||||
|
|
|
||||||
|
|
@ -282,6 +282,44 @@ export const getPossibleTargets = (from: string): Record<string, string[]> => {
|
||||||
return possibleTargets[fromClean] || {};
|
return possibleTargets[fromClean] || {};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Reverse of possibleTargets: for a given raw output extension (as advertised
|
||||||
|
// in each converter's `to` list — the same value the frontend's target
|
||||||
|
// buttons carry in `data-target`), which input extensions (per converter)
|
||||||
|
// can produce it.
|
||||||
|
//
|
||||||
|
// NOTE: intentionally keyed by the RAW target string, not the
|
||||||
|
// normalizeOutputFiletype()-ed one. Several converters advertise distinct
|
||||||
|
// raw targets that normalize to the same output extension (e.g. "latex" and
|
||||||
|
// "tex" both normalize to "tex", "jpeg" normalizes to "jpg") — normalizing
|
||||||
|
// here would incorrectly merge/misalign those. The frontend always sends
|
||||||
|
// the raw `data-target` value, so the lookup key must match that exactly.
|
||||||
|
const possibleSources: Record<string, Record<string, string[]>> = {};
|
||||||
|
|
||||||
|
for (const converterName in properties) {
|
||||||
|
const converterProperties = properties[converterName]?.properties;
|
||||||
|
if (!converterProperties) continue;
|
||||||
|
|
||||||
|
for (const key in converterProperties.from) {
|
||||||
|
const fromList = converterProperties.from[key];
|
||||||
|
const toList = converterProperties.to[key];
|
||||||
|
|
||||||
|
if (!fromList || !toList) continue;
|
||||||
|
|
||||||
|
for (const target of toList) {
|
||||||
|
if (!possibleSources[target]) possibleSources[target] = {};
|
||||||
|
|
||||||
|
const existing = possibleSources[target][converterName];
|
||||||
|
possibleSources[target][converterName] = existing
|
||||||
|
? Array.from(new Set([...existing, ...fromList]))
|
||||||
|
: [...fromList];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getPossibleSources = (to: string): Record<string, string[]> => {
|
||||||
|
return possibleSources[to] || {};
|
||||||
|
};
|
||||||
|
|
||||||
const possibleInputs: string[] = [];
|
const possibleInputs: string[] = [];
|
||||||
for (const converterName in properties) {
|
for (const converterName in properties) {
|
||||||
const converterProperties = properties[converterName]?.properties;
|
const converterProperties = properties[converterName]?.properties;
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import { root } from "./pages/root";
|
||||||
import { upload } from "./pages/upload";
|
import { upload } from "./pages/upload";
|
||||||
import { user } from "./pages/user";
|
import { user } from "./pages/user";
|
||||||
import { healthcheck } from "./pages/healthcheck";
|
import { healthcheck } from "./pages/healthcheck";
|
||||||
|
import { convertSources } from "./pages/convertSources";
|
||||||
|
|
||||||
export const uploadsDir = "./data/uploads/";
|
export const uploadsDir = "./data/uploads/";
|
||||||
export const outputDir = "./data/output/";
|
export const outputDir = "./data/output/";
|
||||||
|
|
@ -50,6 +51,7 @@ const app = new Elysia({
|
||||||
.use(listConverters)
|
.use(listConverters)
|
||||||
.use(chooseConverter)
|
.use(chooseConverter)
|
||||||
.use(healthcheck)
|
.use(healthcheck)
|
||||||
|
.use(convertSources)
|
||||||
.onError(({ error, code, request }) => {
|
.onError(({ error, code, request }) => {
|
||||||
if (code === "NOT_FOUND") {
|
if (code === "NOT_FOUND") {
|
||||||
console.warn(`404: ${request.method} ${new URL(request.url).pathname}`);
|
console.warn(`404: ${request.method} ${new URL(request.url).pathname}`);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { Elysia, t } from "elysia";
|
||||||
|
import { getPossibleSources } from "../converters/main";
|
||||||
|
import { userService } from "./user";
|
||||||
|
|
||||||
|
// Place this next to (or inside the same file as) the existing
|
||||||
|
// `/conversions` route, so it shares whatever auth/db conventions
|
||||||
|
// that route already uses.
|
||||||
|
export const convertSources = new Elysia().use(userService).post(
|
||||||
|
"/convert-sources",
|
||||||
|
({ body }) => {
|
||||||
|
// Record<converterName, string[]>
|
||||||
|
return getPossibleSources(body.to);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
body: t.Object({
|
||||||
|
to: t.String(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
@ -234,6 +234,7 @@ export const root = new Elysia().use(userService).get(
|
||||||
value="Convert"
|
value="Convert"
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
|
<div id="supported-sources" class="my-2 text-sm text-neutral-400"></div>
|
||||||
</form>
|
</form>
|
||||||
</main>
|
</main>
|
||||||
<script src="script.js" defer />
|
<script src="script.js" defer />
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue