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 dropZone = document.getElementById("dropzone");
|
||||
const convertButton = document.querySelector("input[type='submit']");
|
||||
const supportedSourcesEl = document.getElementById("supported-sources");
|
||||
|
||||
const fileNames = [];
|
||||
let fileType;
|
||||
let pendingFiles = 0;
|
||||
let formatSelected = false;
|
||||
let latestSourcesRequestKey = null;
|
||||
|
||||
dropZone.addEventListener("dragover", (e) => {
|
||||
e.preventDefault();
|
||||
|
|
@ -71,6 +74,44 @@ function handleFile(file) {
|
|||
|
||||
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 convertToInput = document.querySelector("input[name='convert_to_search']");
|
||||
const convertToPopup = document.querySelector(".convert_to_popup");
|
||||
|
|
@ -117,6 +158,7 @@ const updateSearchBar = () => {
|
|||
convertButton.disabled = false;
|
||||
}
|
||||
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
|
||||
convertButton.disabled = true;
|
||||
formatSelected = false;
|
||||
latestSourcesRequestKey = null;
|
||||
if (supportedSourcesEl) supportedSourcesEl.textContent = "";
|
||||
});
|
||||
|
||||
convertToInput.addEventListener("blur", (e) => {
|
||||
|
|
|
|||
|
|
@ -282,6 +282,44 @@ export const getPossibleTargets = (from: string): Record<string, string[]> => {
|
|||
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[] = [];
|
||||
for (const converterName in properties) {
|
||||
const converterProperties = properties[converterName]?.properties;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { root } from "./pages/root";
|
|||
import { upload } from "./pages/upload";
|
||||
import { user } from "./pages/user";
|
||||
import { healthcheck } from "./pages/healthcheck";
|
||||
import { convertSources } from "./pages/convertSources";
|
||||
|
||||
export const uploadsDir = "./data/uploads/";
|
||||
export const outputDir = "./data/output/";
|
||||
|
|
@ -50,6 +51,7 @@ const app = new Elysia({
|
|||
.use(listConverters)
|
||||
.use(chooseConverter)
|
||||
.use(healthcheck)
|
||||
.use(convertSources)
|
||||
.onError(({ error, code, request }) => {
|
||||
if (code === "NOT_FOUND") {
|
||||
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"
|
||||
disabled
|
||||
/>
|
||||
<div id="supported-sources" class="my-2 text-sm text-neutral-400"></div>
|
||||
</form>
|
||||
</main>
|
||||
<script src="script.js" defer />
|
||||
|
|
|
|||
Loading…
Reference in New Issue