Skip to content

Commit cf4f80c

Browse files
authored
Merge pull request #15 from LibreSplit/file
2 parents 90364b6 + efa2e08 commit cf4f80c

File tree

4 files changed

+170
-43
lines changed

4 files changed

+170
-43
lines changed

src/app/converter.tsx

Lines changed: 56 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
1-
import React, { useState } from "react";
1+
import { useState } from "react";
22

3+
import AppFileSelect from "@/components/libresplit/AppFileSelect";
4+
import { AppSplitPreview } from "@/components/libresplit/AppSplitPreview";
35
import init, { convert } from "@libresplit/libresplit-converter";
46
import wasmUrl from "@libresplit/libresplit-converter/libresplit_converter_bg.wasm?url";
57

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

10-
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
11-
const file = event.target.files?.[0] || null;
13+
const handleSelectChange = async (files: File | File[] | null) => {
14+
const file = Array.isArray(files) ? (files[0] ?? null) : files;
1215
setSelectedFile(file);
16+
setResult(null);
17+
setFileText(null);
18+
19+
if (file) {
20+
const text = await file.text();
21+
setFileText(text);
22+
}
1323
};
1424

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

2131
try {
2232
const text = await selectedFile.text();
23-
2433
await init(wasmUrl);
25-
2634
const converted = convert(text);
27-
2835
setResult(converted);
2936
} catch (error) {
3037
console.error("Error processing file: ", error);
@@ -36,55 +43,61 @@ export function Converter() {
3643
if (!result || !selectedFile) return;
3744

3845
const fileName = selectedFile.name.replace(/\.[^/.]+$/, ".json");
39-
4046
const blob = new Blob([result], { type: "application/json" });
4147
const url = URL.createObjectURL(blob);
4248

4349
const link = document.createElement("a");
4450
link.href = url;
4551
link.download = fileName;
4652
link.click();
47-
4853
URL.revokeObjectURL(url);
4954
};
5055

5156
return (
52-
<div className="flex min-h-screen items-center justify-center bg-linear-to-tr from-gray-700 to-sky-900 p-6">
53-
<div className="w-full max-w-lg space-y-6 rounded-lg bg-gray-800 p-6 shadow-lg">
54-
<h1 className="text-center text-2xl font-bold text-white">
55-
LibreSplit Converter
56-
</h1>
57-
<div className="space-y-4">
58-
<input
59-
type="file"
60-
accept=".lss"
61-
onChange={handleFileChange}
62-
className="block w-full rounded-md border border-white px-3 py-2 text-white focus:ring focus:ring-indigo-500 focus:outline-none"
63-
/>
64-
<button
65-
onClick={handleSubmit}
66-
disabled={!selectedFile}
67-
className={
68-
'${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'
69-
}
70-
>
71-
Convert
72-
</button>
57+
<div className="flex h-[calc(100vh-64px-24px)] flex-col space-y-4 overflow-hidden">
58+
<div className="shrink-0 px-[100px]">
59+
<AppFileSelect
60+
label="Select LiveSplit file:"
61+
value={selectedFile}
62+
onChange={handleSelectChange}
63+
multiple={false}
64+
filters={[{ name: "LiveSplit (.lss)", extensions: ["lss", "xml"] }]}
65+
/>
66+
</div>
67+
68+
<div className="flex shrink-0 items-center justify-center gap-2">
69+
<button
70+
onClick={handleSubmit}
71+
className="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
72+
>
73+
Convert
74+
</button>
75+
<button
76+
onClick={handleDownload}
77+
disabled={!result}
78+
className="rounded bg-gray-200 px-4 py-2 text-black disabled:opacity-50"
79+
>
80+
Download Splits
81+
</button>
82+
</div>
83+
84+
<div className="min-h-0 flex-1">
85+
<div className="flex h-full min-h-0 w-full items-stretch justify-center gap-4">
86+
{fileText && (
87+
<div className="flex min-h-0 flex-1 flex-col">
88+
<span className="mb-2 text-center font-semibold">LiveSplit:</span>
89+
<AppSplitPreview text={fileText} className="h-full flex-1" />
90+
</div>
91+
)}
92+
{result && (
93+
<div className="flex min-h-0 flex-1 flex-col">
94+
<span className="mb-2 text-center font-semibold">
95+
LibreSplit:
96+
</span>
97+
<AppSplitPreview text={result} className="h-full flex-1" />
98+
</div>
99+
)}
73100
</div>
74-
{result && (
75-
<div className="space-y-4">
76-
<p className="text-center font-medium text-green-600">
77-
Conversion successful! Click the button below to download your
78-
LibreSplit file.
79-
</p>
80-
<button
81-
onClick={handleDownload}
82-
className="w-full rounded-md bg-green-600 px-4 py-2 font-semibold text-white hover:bg-green-700"
83-
>
84-
Download
85-
</button>
86-
</div>
87-
)}
88101
</div>
89102
</div>
90103
);
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { useMemo, useRef } from "react";
2+
3+
import { Button } from "../ui/button";
4+
import { Input } from "../ui/input";
5+
6+
interface AppFileSelectProps {
7+
label?: string;
8+
value: File | File[] | null;
9+
onChange: (files: File | File[] | null) => void;
10+
multiple?: boolean;
11+
filters?: { name: string; extensions: string[] }[];
12+
}
13+
14+
export default function AppFileSelect({
15+
label = "Select file:",
16+
value,
17+
onChange,
18+
multiple = false,
19+
filters,
20+
}: AppFileSelectProps) {
21+
const inputRef = useRef<HTMLInputElement | null>(null);
22+
23+
const display = useMemo(() => {
24+
if (!value) return "No file chosen.";
25+
if (Array.isArray(value)) {
26+
return value.map((f) => f.name).join(", ");
27+
}
28+
return value.name;
29+
}, [value]);
30+
31+
const handlePick = () => {
32+
inputRef.current?.click();
33+
};
34+
35+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
36+
const files = e.target.files;
37+
if (!files || files.length === 0) {
38+
onChange(null);
39+
return;
40+
}
41+
42+
if (multiple) {
43+
onChange(Array.from(files));
44+
} else {
45+
onChange(files[0]);
46+
}
47+
};
48+
49+
const accept = filters
50+
? filters.flatMap((f) => f.extensions.map((ext) => `.${ext}`)).join(",")
51+
: undefined;
52+
53+
return (
54+
<div className="space-y-2 px-8">
55+
<span>{label}</span>
56+
<div className="flex items-center">
57+
<Input className="flex-1 rounded-r-none" value={display} readOnly />
58+
<Button
59+
type="button"
60+
onClick={handlePick}
61+
className="rounded-l-none bg-gray-200 text-black hover:bg-blue-200"
62+
>
63+
Open
64+
</Button>
65+
</div>
66+
<input
67+
ref={inputRef}
68+
type="file"
69+
className="hidden"
70+
multiple={multiple}
71+
accept={accept}
72+
onChange={handleChange}
73+
/>
74+
</div>
75+
);
76+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
interface AppSplitPreviewProps {
2+
text: string;
3+
className?: string;
4+
}
5+
6+
export function AppSplitPreview({ text, className }: AppSplitPreviewProps) {
7+
return (
8+
<textarea
9+
readOnly
10+
value={text}
11+
wrap="off"
12+
spellCheck={false}
13+
aria-readonly="true"
14+
className={`h-full w-full resize-none ${className ?? ""}`}
15+
/>
16+
);
17+
}

src/components/ui/input.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import * as React from "react";
2+
3+
import { cn } from "@/lib/utils";
4+
5+
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6+
return (
7+
<input
8+
type={type}
9+
data-slot="input"
10+
className={cn(
11+
"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",
12+
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
13+
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
14+
className,
15+
)}
16+
{...props}
17+
/>
18+
);
19+
}
20+
21+
export { Input };

0 commit comments

Comments
 (0)