Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
99 changes: 56 additions & 43 deletions src/app/converter.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
import React, { useState } from "react";
import { useState } from "react";

import AppFileSelect from "@/components/libresplit/AppFileSelect";
import { AppSplitPreview } from "@/components/libresplit/AppSplitPreview";
import init, { convert } from "@libresplit/libresplit-converter";
import wasmUrl from "@libresplit/libresplit-converter/libresplit_converter_bg.wasm?url";

export function Converter() {
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [fileText, setFileText] = useState<string | null>(null);
const [result, setResult] = useState<string | null>(null);

const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0] || null;
const handleSelectChange = async (files: File | File[] | null) => {
const file = Array.isArray(files) ? (files[0] ?? null) : files;
setSelectedFile(file);
setResult(null);
setFileText(null);

if (file) {
const text = await file.text();
setFileText(text);
}
};

const handleSubmit = async () => {
Expand All @@ -20,11 +30,8 @@ export function Converter() {

try {
const text = await selectedFile.text();

await init(wasmUrl);

const converted = convert(text);

setResult(converted);
} catch (error) {
console.error("Error processing file: ", error);
Expand All @@ -36,55 +43,61 @@ export function Converter() {
if (!result || !selectedFile) return;

const fileName = selectedFile.name.replace(/\.[^/.]+$/, ".json");

const blob = new Blob([result], { type: "application/json" });
const url = URL.createObjectURL(blob);

const link = document.createElement("a");
link.href = url;
link.download = fileName;
link.click();

URL.revokeObjectURL(url);
};

return (
<div className="flex min-h-screen items-center justify-center bg-linear-to-tr from-gray-700 to-sky-900 p-6">
<div className="w-full max-w-lg space-y-6 rounded-lg bg-gray-800 p-6 shadow-lg">
<h1 className="text-center text-2xl font-bold text-white">
LibreSplit Converter
</h1>
<div className="space-y-4">
<input
type="file"
accept=".lss"
onChange={handleFileChange}
className="block w-full rounded-md border border-white px-3 py-2 text-white focus:ring focus:ring-indigo-500 focus:outline-none"
/>
<button
onClick={handleSubmit}
disabled={!selectedFile}
className={
'${selectedFile ? "bg-indigo-600 hover:bg-indigo-700" : "bg-gray-400 cursor-not-allowed"} w-full rounded-md px-4 py-2 font-semibold text-white'
}
>
Convert
</button>
<div className="flex h-[calc(100vh-64px-24px)] flex-col space-y-4 overflow-hidden">
<div className="shrink-0 px-[100px]">
<AppFileSelect
label="Select LiveSplit file:"
value={selectedFile}
onChange={handleSelectChange}
multiple={false}
filters={[{ name: "LiveSplit (.lss)", extensions: ["lss", "xml"] }]}
/>
</div>

<div className="flex shrink-0 items-center justify-center gap-2">
<button
onClick={handleSubmit}
className="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
>
Convert
</button>
<button
onClick={handleDownload}
disabled={!result}
className="rounded bg-gray-200 px-4 py-2 text-black disabled:opacity-50"
>
Download Splits
</button>
</div>

<div className="min-h-0 flex-1">
<div className="flex h-full min-h-0 w-full items-stretch justify-center gap-4">
{fileText && (
<div className="flex min-h-0 flex-1 flex-col">
<span className="mb-2 text-center font-semibold">LiveSplit:</span>
<AppSplitPreview text={fileText} className="h-full flex-1" />
</div>
)}
{result && (
<div className="flex min-h-0 flex-1 flex-col">
<span className="mb-2 text-center font-semibold">
LibreSplit:
</span>
<AppSplitPreview text={result} className="h-full flex-1" />
</div>
)}
</div>
{result && (
<div className="space-y-4">
<p className="text-center font-medium text-green-600">
Conversion successful! Click the button below to download your
LibreSplit file.
</p>
<button
onClick={handleDownload}
className="w-full rounded-md bg-green-600 px-4 py-2 font-semibold text-white hover:bg-green-700"
>
Download
</button>
</div>
)}
</div>
</div>
);
Expand Down
76 changes: 76 additions & 0 deletions src/components/libresplit/AppFileSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { useMemo, useRef } from "react";

import { Button } from "../ui/button";
import { Input } from "../ui/input";

interface AppFileSelectProps {
label?: string;
value: File | File[] | null;
onChange: (files: File | File[] | null) => void;
multiple?: boolean;
filters?: { name: string; extensions: string[] }[];
}

export default function AppFileSelect({
label = "Select file:",
value,
onChange,
multiple = false,
filters,
}: AppFileSelectProps) {
const inputRef = useRef<HTMLInputElement | null>(null);

const display = useMemo(() => {
if (!value) return "No file chosen.";
if (Array.isArray(value)) {
return value.map((f) => f.name).join(", ");
}
return value.name;
}, [value]);

const handlePick = () => {
inputRef.current?.click();
};

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (!files || files.length === 0) {
onChange(null);
return;
}

if (multiple) {
onChange(Array.from(files));
} else {
onChange(files[0]);
}
};

const accept = filters
? filters.flatMap((f) => f.extensions.map((ext) => `.${ext}`)).join(",")
: undefined;

return (
<div className="space-y-2 px-8">
<span>{label}</span>
<div className="flex items-center">
<Input className="flex-1 rounded-r-none" value={display} readOnly />
<Button
type="button"
onClick={handlePick}
className="rounded-l-none bg-gray-200 text-black hover:bg-blue-200"
>
Open
</Button>
</div>
<input
ref={inputRef}
type="file"
className="hidden"
multiple={multiple}
accept={accept}
onChange={handleChange}
/>
</div>
);
}
17 changes: 17 additions & 0 deletions src/components/libresplit/AppSplitPreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
interface AppSplitPreviewProps {
text: string;
className?: string;
}

export function AppSplitPreview({ text, className }: AppSplitPreviewProps) {
return (
<textarea
readOnly
value={text}
wrap="off"
spellCheck={false}
aria-readonly="true"
className={`h-full w-full resize-none ${className ?? ""}`}
/>
);
}
21 changes: 21 additions & 0 deletions src/components/ui/input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import * as React from "react";

import { cn } from "@/lib/utils";

function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30",
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
className,
)}
{...props}
/>
);
}

export { Input };