Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
98 commits
Select commit Hold shift + click to select a range
8021259
add wireframe
Jor02 Feb 16, 2026
670ec6d
Merge remote-tracking branch 'upstream/master'
Jor02 Feb 17, 2026
2a47105
Reorder script tag position in HTML
headquarter8302 Feb 18, 2026
12c2d01
Initial UI surface
headquarter8302 Feb 18, 2026
199718c
Merge branch 'p2r3:master' into master
headquarter8302 Feb 18, 2026
b75d001
Prepare for Conversion page
headquarter8302 Feb 18, 2026
b536d15
Use the proper image for the logo
headquarter8302 Feb 18, 2026
991125c
add dark/light theme toggle
Jor02 Feb 18, 2026
11006a4
Fix invalid tsconfig JSX setup
headquarter8302 Feb 19, 2026
ab86090
Resolve conflict
headquarter8302 Feb 19, 2026
3257146
Merge branch 'master' into master
headquarter8302 Feb 19, 2026
717b7a8
Separate as script
headquarter8302 Feb 19, 2026
9de9176
improve upload page styling
Jor02 Feb 19, 2026
67834dd
Merge remote-tracking branch 'origin/master'
Jor02 Feb 19, 2026
d20b55b
Further preparation of Conversion page
headquarter8302 Feb 19, 2026
1445e07
Reconcile overwritten commits
headquarter8302 Feb 19, 2026
5218885
add conversion page
Jor02 Feb 19, 2026
cc42054
Run through formatter + debug function
headquarter8302 Feb 20, 2026
dff5a41
Remove `UploadFiles` signal, redundant
headquarter8302 Feb 21, 2026
6e5f34c
Remove `UploadFiles` signal, redundant
headquarter8302 Feb 21, 2026
60010e4
Explicitly export and use `selectedFiles` from `main.ts`
headquarter8302 Feb 21, 2026
c05e30e
add missing footer to conversion page
Jor02 Feb 21, 2026
82b2701
split sidenav into separate component
Jor02 Feb 21, 2026
d7bc42e
add missing footer import
Jor02 Feb 21, 2026
cded3ae
split format card into separate component
Jor02 Feb 21, 2026
d53a0c3
fix incorrectly using color instead of background-color
Jor02 Feb 21, 2026
739fd96
split conversion page settings into separate component
Jor02 Feb 21, 2026
2d7ef2f
increased format category font size
Jor02 Feb 21, 2026
bb4f45a
move missing css classes into ConversionSettings.css
Jor02 Feb 21, 2026
97bc2b1
move missing css class into SideNav.css
Jor02 Feb 21, 2026
77f620b
remove redundant classes from Conversion.css
Jor02 Feb 21, 2026
2a2c066
split selected file info from conversion into separate component
Jor02 Feb 21, 2026
3922fd1
move classes to ConversionSettings.css
Jor02 Feb 21, 2026
09bc398
removed unused class name in Conversion.tsx
Jor02 Feb 21, 2026
a9628c6
changed accidental class to classname
Jor02 Feb 21, 2026
533f474
fix chain link icon not displaying correctly
Jor02 Feb 21, 2026
c0b2a09
Rearrange files + formatting run
headquarter8302 Feb 21, 2026
ecb711f
Cleanup
headquarter8302 Feb 21, 2026
03accea
CSS scaling fix
headquarter8302 Feb 21, 2026
180d9a7
Move data to module scope
headquarter8302 Feb 23, 2026
5998c72
Add Popup component
headquarter8302 Feb 24, 2026
70301ed
Use generated formats for UI format card source
headquarter8302 Feb 24, 2026
ef40166
Calling preventDefault on keydown is a bad idea
headquarter8302 Feb 25, 2026
5aed53d
Track "Advanced mode" state changes
headquarter8302 Feb 25, 2026
eb494a3
Add search & selection functionality on Conversion
headquarter8302 Feb 25, 2026
1111ac3
Merge branch 'master' into master
headquarter8302 Feb 26, 2026
8dd36a2
Style scrollbars to theme
headquarter8302 Feb 26, 2026
4a39ab8
Styling changes to Conversion page
headquarter8302 Feb 26, 2026
8ff5e50
Styling for mobile layout
headquarter8302 Feb 26, 2026
e4ede73
Add button actions to Popup
headquarter8302 Feb 26, 2026
cc48282
Move variable change to document root
headquarter8302 Feb 26, 2026
b9ceb80
Interpolate more colors with the primary accent
headquarter8302 Feb 26, 2026
5286f40
Add arbitrary Popup data input
headquarter8302 Feb 26, 2026
106f6ac
Fix escape-dismiss not setting proper state
headquarter8302 Feb 26, 2026
93ba29e
Replace Help placeholder with README excerpt
headquarter8302 Feb 26, 2026
be789bf
Pass the handler name to format cards
headquarter8302 Feb 26, 2026
72a7cca
Limit popup width
headquarter8302 Feb 26, 2026
92f593e
Add state for upload field disabling
headquarter8302 Feb 26, 2026
509087a
Remove unused type field
headquarter8302 Feb 26, 2026
453f906
Show selected file name
headquarter8302 Feb 26, 2026
3062522
only scroll format card section when on slim screens
Jor02 Feb 26, 2026
2e24ab5
scale sidenav with screen size on slim screens
Jor02 Feb 26, 2026
d547c51
replace logo svg with better quality svg
Jor02 Feb 26, 2026
824df84
made sidenav interactable
Jor02 Feb 26, 2026
31ecc51
removed trailing semicolon
Jor02 Feb 26, 2026
000acd4
moved content-wrapper to a separate element
Jor02 Feb 27, 2026
c1c9b1c
turned FormatCard into button to improve accessibility
Jor02 Feb 27, 2026
70ca649
Allow multiple file uploads and file deselection
headquarter8302 Feb 28, 2026
37042a0
Merge branch 'p2r3:master' into master
headquarter8302 Mar 1, 2026
990ebb6
Use proper variables for styling
headquarter8302 Mar 1, 2026
5d169da
Refactor components and part of the prefs system
headquarter8302 Mar 2, 2026
b4d5521
Refactor SelectedFiles to use a Record type and update FileInfoBadge …
headquarter8302 Mar 2, 2026
b51fd1d
Extract to `ComponentHeader`
headquarter8302 Mar 2, 2026
ade5d19
Create "compact" version of theme toggle and add to Conversion page
headquarter8302 Mar 2, 2026
4706078
Use proper style variable
headquarter8302 Mar 2, 2026
9873107
Run through formatter
headquarter8302 Mar 3, 2026
7e3980b
Run through formatter
headquarter8302 Mar 3, 2026
70c7dcd
This should be outside
headquarter8302 Mar 3, 2026
71cd8a1
Use a Map for format options
headquarter8302 Mar 3, 2026
cc13413
Use Mode signal instead of value
headquarter8302 Mar 3, 2026
c473000
Fix and optimize format searching
headquarter8302 Mar 3, 2026
e5546fd
Deprecate variable, use Signal
headquarter8302 Mar 3, 2026
64b17d9
Pass data to ConversionSidebar component
headquarter8302 Mar 3, 2026
a81bdcf
Improved color calculation
Jor02 Mar 5, 2026
20e73ee
Improved color calculation (again)
Jor02 Mar 5, 2026
66749ca
Added custom button component
Jor02 Mar 5, 2026
e091e35
Merge branch 'p2r3:master' into master
headquarter8302 Mar 6, 2026
f817021
improve some padding (#1)
neko782 Mar 6, 2026
6bad84a
Add Conversion page flowchart
headquarter8302 Mar 7, 2026
f11156e
Use `ConversionOptionsMap` on `Conversion.tsx`
headquarter8302 Mar 10, 2026
8477f4f
Use `ConversionOption`
headquarter8302 Mar 10, 2026
2e4897c
Further `ConversionOptionsMap` migration
headquarter8302 Mar 10, 2026
7261268
Update flowchart
headquarter8302 Mar 10, 2026
996f7ff
Properly cache search index
headquarter8302 Mar 11, 2026
887a76c
Final `ConversionOptionsMap` conversion
headquarter8302 Mar 11, 2026
0fdb74e
Ensure links open in a new tab
headquarter8302 Mar 11, 2026
14a46a4
Allow clicks on backdrop to dismiss dialog
headquarter8302 Mar 11, 2026
6b6a26f
1st attempt at filtering non-Advanced mode formats
headquarter8302 Mar 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions base.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
html,
body {
padding: 0;
font-family: sans-serif;
}

*,
*::after,
*::before {
margin: 0;
box-sizing: border-box
}

@media (prefers-reduced-motion: no-preference) {
html {
interpolate-size: allow-keywords;
}
}

input,
button,
textarea,
select {
font: inherit;
}

img {
font-size: 12px;
}
360 changes: 360 additions & 0 deletions docs/Conversion flowchart.drawio

Large diffs are not rendered by default.

Binary file added docs/Conversion flowchart.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/wireframe.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 8 additions & 42 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,54 +1,20 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<script type="module" src="./src/ui/scripts/theme-detection.ts"></script>
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="base.css">
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon">
<title>Convert to it!</title>
</head>
<body>

<input id="file-input" type="file">

<a id="commit-id" href="https://github.com/p2r3/convert"></a>

<div id="file-area">
<h2>Click to add your file</h2>
<p id="drop-hint-text">or drag and drop it here</p>
</div>
<div id="side-panel">
<button id="mode-button">Advanced mode</button>
</div>

<div id="format-containers">

<div id="from-container" class="format-container">
<h2>Convert from:</h2>
<input type="text" id="search-from" class="search" placeholder="Search">
<div id="from-list" class="format-list">

</div>
</div>

<div id="to-container" class="format-container">
<h2>Convert to:</h2>
<input type="text" id="search-to" class="search" placeholder="Search">
<div id="to-list" class="format-list">

</div>
</div>

</div>

<button id="convert-button" class="disabled">Convert</button>

<div id="popup-bg"></div>
<div id="popup">
<h2>Loading tools...</h2>
</div>

<script type="module" src="src/main.ts"></script>
<body>

</body>
</html>
<script type="module" src="src/main.new.ts"></script>
<script type="module" src="src/ui/index.tsx"></script>

</html>
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
}
},
"devDependencies": {
"@preact/preset-vite": "^2.10.3",
"@types/jszip": "^3.4.0",
"@types/opentype.js": "^1.3.9",
"electron": "^40.6.0",
Expand All @@ -59,6 +60,7 @@
"@ffmpeg/util": "^0.12.2",
"@flo-audio/reflo": "^0.1.2",
"@imagemagick/magick-wasm": "^0.0.37",
"@preact/signals": "^2.8.1",
"@sqlite.org/sqlite-wasm": "^3.51.2-build6",
"@stringsync/vexml": "^0.1.8",
"@toon-format/toon": "^2.1.0",
Expand All @@ -80,11 +82,13 @@
"papaparse": "^5.5.3",
"pdftoimg-js": "^0.2.5",
"pe-library": "^2.0.1",
"preact": "^10.28.3",
"svg-pathdata": "^8.0.0",
"three": "^0.182.0",
"three-bvh-csg": "^0.0.17",
"three-mesh-bvh": "^0.9.8",
"ts-flp": "^1.0.3",
"use-debounce": "^10.1.0",
"verovio": "^6.0.1",
"vexflow": "^5.0.0",
"vite-plugin-static-copy": "^3.1.6",
Expand Down
163 changes: 163 additions & 0 deletions src/main.new.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import type { FileFormat, FileData, FormatHandler, ConvertPathNode } from "./FormatHandler.js";
import normalizeMimeType from "./normalizeMimeType.js";
import handlers from "./handlers";
import { TraversionGraph } from "./TraversionGraph.js";
import { PopupData } from "./ui/index.js";
import { closePopup, openPopup } from "./ui/PopupStore.js";
import { signal } from "@preact/signals";
import { Mode, ModeEnum } from "./ui/ModeStore.js";

/** KV pairs of files */
type FileRecord = Record<`${string}-${string}`, File>

/** Map of available formats and its handler */
export type ConversionOptionsMap = Map<FileFormat, FormatHandler>;
/** A single conversion option, derived from `ConversionOptionsMap` */
export type ConversionOption = ConversionOptionsMap extends Map<infer K, infer V> ? [K, V] : never;

export const ConversionOptions: ConversionOptionsMap = new Map();

/**
* Files currently selected for conversion
*/
export const SelectedFiles = signal<FileRecord>({});

/**
* Handlers that support conversion from any formats
*/
export const ConversionsFromAnyInput: ConvertPathNode[] =
handlers
.filter(h => h.supportAnyInput && h.supportedFormats)
.flatMap(h => h.supportedFormats!
.filter(f => f.to)
.map(f => ({ handler: h, format: f })));

window.supportedFormatCache = new Map();
window.traversionGraph = new TraversionGraph();

window.printSupportedFormatCache = () => {
const entries = [];
for (const entry of window.supportedFormatCache)
entries.push(entry);
return JSON.stringify(entries, null, 2);
}

async function buildOptionList() {
ConversionOptions.clear();

for (const handler of handlers) {
if (!window.supportedFormatCache.has(handler.name)) {
console.warn(`Cache miss for formats of handler "${handler.name}"`);

try {
await handler.init();
} catch (_) { continue }

if (handler.supportedFormats) {
window.supportedFormatCache.set(handler.name, handler.supportedFormats);
console.info(`Updated supported format cache for "${handler.name}"`);
}
}

const supportedFormats = window.supportedFormatCache.get(handler.name);

if (!supportedFormats) {
console.warn(`Handler "${handler.name}" doesn't support any formats`);
continue
}

for (const format of supportedFormats) {
if (!format.mime) continue;
ConversionOptions.set(format, handler);
}
}

closePopup();
}

async function attemptConvertPath(files: FileData[], path: ConvertPathNode[]) {
PopupData.value = {
title: "Finding conversion route...",
text: `Trying ${path.map(c => c.format.format).join(" → ")}`
}
openPopup();

for (let i = 0; i < path.length - 1; i++) {
const handler = path[i + 1].handler;

try {
let supportedFormats = window.supportedFormatCache.get(handler.name);

if (!handler.ready) {
try {
await handler.init();
} catch (_) { return null; }

if (handler.supportedFormats) {
window.supportedFormatCache.set(handler.name, handler.supportedFormats);
supportedFormats = handler.supportedFormats;
}
}

if (!supportedFormats) throw `Handler "${handler.name}" doesn't support any formats.`;

const inputFormat = supportedFormats.find(c => c.mime === path[i].format.mime && c.from)!;

files = (
await Promise.all([
handler.doConvert(files, inputFormat, path[i + 1].format),
// Ensure that we wait long enough for the UI to update
new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))
])
)[0];

if (files.some(c => !c.bytes.length)) throw "Output is empty.";
} catch (e) {
console.log(path.map(c => c.format.format));
console.error(handler.name, `${path[i].format.format} → ${path[i + 1].format.format}`, e);

await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
return null;
}
}
}

window.tryConvertByTraversing = async function (
files: FileData[],
from: ConvertPathNode,
to: ConvertPathNode
) {
for await (const path of window.traversionGraph.searchPath(from, to, Mode.value === ModeEnum.Simple)) {
// Use exact output format if the target handler supports it
if (path.at(-1)?.handler === to.handler) {
path[path.length - 1] = to;
}
const attempt = await attemptConvertPath(files, path);
if (attempt) return attempt;
}
return null;
}

function downloadFile(bytes: Uint8Array, name: string, mime: string) {
const blob = new Blob([bytes as BlobPart], { type: mime });
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = name;
link.click();
}

try {
const cacheJSON = await fetch("cache.json")
.then(r => r.json());
window.supportedFormatCache = new Map(cacheJSON);
} catch (error) {
console.warn(
"Missing supported format precache.\n\n" +
"Consider saving the output of printSupportedFormatCache() to cache.json."
);
} finally {
await buildOptionList();
console.log("Built initial format list.");
}

console.debug(ConversionOptions);
6 changes: 3 additions & 3 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import handlers from "./handlers";
import { TraversionGraph } from "./TraversionGraph.js";

/** Files currently selected for conversion */
let selectedFiles: File[] = [];
export let selectedFiles: File[] = [];
/**
* Whether to use "simple" mode.
* - In **simple** mode, the input/output lists are grouped by file format.
Expand Down Expand Up @@ -316,10 +316,10 @@ ui.modeToggleButton.addEventListener("click", () => {
simpleMode = !simpleMode;
if (simpleMode) {
ui.modeToggleButton.textContent = "Advanced mode";
document.body.style.setProperty("--highlight-color", "#1C77FF");
document.body.style.setProperty("--primary", "#1C77FF");
} else {
ui.modeToggleButton.textContent = "Simple mode";
document.body.style.setProperty("--highlight-color", "#FF6F1C");
document.body.style.setProperty("--primary", "#FF6F1C");
}
buildOptionList();
});
Expand Down
40 changes: 40 additions & 0 deletions src/ui/ModeStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { signal } from "@preact/signals";

const STORAGE_KEY = "mode";

export enum ModeEnum {
Simple,
Advanced
}

export const enum ModeText {
Simple = "Simple mode",
Advanced = "Advanced mode"
}

function getInitialMode(): ModeEnum {
const stored = localStorage.getItem(STORAGE_KEY);
return (!!stored) ? parseInt(stored, 10) : ModeEnum.Simple;
}

export const Mode = signal<ModeEnum>(getInitialMode());

function applyMode(value: ModeEnum) {
if (value === ModeEnum.Simple) document.documentElement.style.setProperty("--primary", "#1C77FF");
if (value === ModeEnum.Advanced) document.documentElement.style.setProperty("--primary", "#FF6F1C");
}

Mode.subscribe((value) => {
localStorage.setItem(STORAGE_KEY, value.toString());
applyMode(value);
})

export function toggleMode() {
Mode.value = Mode.value === ModeEnum.Advanced
? ModeEnum.Simple
: ModeEnum.Advanced;
}

export function initMode() {
applyMode(Mode.value);
}
40 changes: 40 additions & 0 deletions src/ui/PopupStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { signal } from '@preact/signals'

export interface PopupDataContainer {
/** Title of the popup */
title?: string
/** The description text of the popup */
text?: string
/**
* Is the popup soft-dismissible?
*
* If this is false, the modal must be closed programmatically, or else it'll be stuck blocking input
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog#popover_api_html_attributes
*/
dismissible?: boolean
/** Text for the button. If this is undefined, the popup will hide the button */
buttonText?: string
/** The event handler for the button. If this is just `true`, clicking the button will close the modal */
buttonOnClick?: preact.MouseEventHandler<HTMLButtonElement> | true
/**
* Raw contents of the popup. Can be any arbitrary JSX data.
*
* If this is declared, properties `title` and `text` are ignored
*/
contents?: preact.JSX.Element
}

export const popupOpen = signal(false);

export const openPopup = () => (popupOpen.value = true);
export const closePopup = () => (popupOpen.value = false);
export const togglePopup = () => (popupOpen.value = !popupOpen.value);

// Manual overrides
// @ts-expect-error
window.openPopup = openPopup;
// @ts-expect-error
window.closePopup = closePopup;
// @ts-expect-error
window.togglePopup = togglePopup;
Loading