Skip to content

Commit ffb158b

Browse files
committed
Light/dark theme
1 parent 1327bbf commit ffb158b

8 files changed

Lines changed: 330 additions & 341 deletions

File tree

uuid_tool/components.json

Lines changed: 0 additions & 20 deletions
This file was deleted.

uuid_tool/index.html

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
<head>
44
<meta charset="UTF-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Map Hasher</title>
7+
<meta name="description" content="Extract map hashes from binary files and PNG images" />
8+
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
69

7-
<meta property="og:title" content="EvMod website" />
8-
<meta property="og:description" content="hashcode tool" />
10+
<meta property="og:title" content="Map Hash Extractor" />
11+
<meta property="og:description" content="Extract map hashes from binary files and PNG images" />
912
<meta property="og:type" content="website" />
1013
</head>
1114

uuid_tool/public/favicon.svg

Lines changed: 3 additions & 0 deletions
Loading

uuid_tool/src/App.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import Index from "./pages/Index";
1+
import Index from "./Index";
22

33
const App = () => <Index />;
44

5-
export default App;
5+
export default App;

uuid_tool/src/Index.tsx

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
import { useState, useCallback } from "react";
2+
import { md5 } from "@/lib/md5";
3+
import { useTheme } from "@/hooks/useTheme";
4+
5+
const MAP_COLORS: number[] = [
6+
0xff000000, 0xff000000, 0xff000000, 0xff000000,
7+
-10912473, -9594576, -8408520, -12362211, -5331853, -2766452, -530013, -8225962, -7566196, -5526613,
8+
-3684409, -9868951, -4980736, -2359296, -65536, -7929856, -9408332, -7697700, -6250241, -11250553, -9079435, -7303024, -5789785, -10987432,
9+
-16754944, -16750080, -16745472, -16760576, -4934476, -2302756, -1, -7895161, -9210239, -7499618, -5986120, -11118495, -9810890, -8233406, -6853299,
10+
-11585240, -11579569, -10461088, -9408400, -12895429, -13816396, -13158436, -12566273, -14605945, -10202062, -8690114, -7375032, -11845850,
11+
-4935252, -2303533, -779, -7895679, -6792924, -4559572, -2588877, -9288933, -8571496, -6733382, -5092136, -10606478, -12030824, -10976070,
12+
-10053160, -13217422, -6184668, -3816148, -1710797, -8816357, -10907631, -9588715, -8401895, -12358643, -5613196, -3117682, -884827, -8371369,
13+
-13290187, -12500671, -11776948, -14145496, -9671572, -8092540, -6710887, -11447983, -13280916, -12489340, -11763815, -14138543, -10933123,
14+
-9619815, -8437838, -12377762, -14404227, -13876839, -13415246, -14997410, -12045020, -10993364, -10073037, -13228005, -12035804, -10982100,
15+
-10059981, -13221093, -9690076, -8115156, -6737101, -11461861, -15658735, -15395563, -15132391, -15921907, -5199818, -2634430, -332211, -8094168,
16+
-12543338, -11551561, -10691627, -13601936, -13346124, -12620068, -11894529, -14204025, -16738008, -16729294, -16721606, -16748002, -10798046,
17+
-9483734, -8301007, -12309223, -11599616, -10485504, -9436672, -12910336, -7111567, -4941686, -3034719, -9544363, -9422567, -7780833, -6335964,
18+
-11261165, -9880244, -8369315, -6989972, -11653575, -11580319, -10461833, -9409398, -12895927, -8168167, -6262241, -4553436, -10336749, -12037595,
19+
-10984403, -9997003, -13222628, -9423305, -7716285, -6271666, -11261911, -14148584, -13556962, -13031133, -14805742, -10532027, -9151404, -7902366,
20+
-12109773, -12763072, -11841713, -11051940, -13750224, -11128002, -9879989, -8763048, -12573138, -13292736, -12503729, -11780516, -14147536,
21+
-13294824, -12506338, -11783645, -14149102, -13289187, -12499420, -11775446, -14144746, -10212832, -8768729, -7455698, -11854056, -15069429,
22+
-14740979, -14346736, -15529208, -8052446, -6084310, -4378575, -10217191, -9950140, -8440237, -7061663, -11656909, -12578540, -11594471, -10741475,
23+
-13628145, -15771554, -15569805, -15303034, -16039354, -14130078, -13469064, -12939636, -14791862, -12837077, -11918027, -11129794, -13822176,
24+
-15827107, -15623310, -15420283, -16097466, -12171706, -11119018, -10197916, -13355980, -6784153, -4548994, -2576493, -9282483, -10914455, -9596799,
25+
-8411242, -12363697
26+
];
27+
28+
const MAP_COLORS_REVERSE = new Map<number, number>();
29+
MAP_COLORS.forEach((color, idx) => MAP_COLORS_REVERSE.set(color, idx));
30+
MAP_COLORS_REVERSE.set(0, 0);
31+
MAP_COLORS_REVERSE.set(0xff000000 | 0, 0);
32+
33+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
34+
35+
const bytesToHex = (bytes: Uint8Array): string =>
36+
Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
37+
38+
const formatUUID = (hex: string) =>
39+
`${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
40+
41+
const uuidsToBytes = (uuidList: string[]): Uint8Array => {
42+
const bytes = new Uint8Array(uuidList.length * 16);
43+
uuidList.forEach((uuid, i) => {
44+
const hex = uuid.replace(/-/g, "");
45+
for (let j = 0; j < 16; j++)
46+
bytes[i * 16 + j] = parseInt(hex.slice(j * 2, j * 2 + 2), 16);
47+
});
48+
return bytes;
49+
};
50+
51+
const parseUUIDs = (buffer: ArrayBuffer): string[] => {
52+
const bytes = new Uint8Array(buffer);
53+
const result: string[] = [];
54+
for (let i = 0; i + 16 <= bytes.length; i += 16)
55+
result.push(formatUUID(bytesToHex(bytes.slice(i, i + 16))));
56+
return result;
57+
};
58+
59+
const nameUUIDFromBytes = (bytes: Uint8Array): string => {
60+
const hash = md5(bytes);
61+
hash[6] = (hash[6] & 0x0f) | 0x30;
62+
hash[8] = (hash[8] & 0x3f) | 0x80;
63+
return formatUUID(bytesToHex(hash));
64+
};
65+
66+
const setLockedBit = (uuid: string): string => {
67+
const [first, ...rest] = uuid.split("-");
68+
const byte0 = parseInt(first.slice(0, 2), 16) | 0x01;
69+
return byte0.toString(16).padStart(2, "0") + first.slice(2) + "-" + rest.join("-");
70+
};
71+
72+
const parseImageUUIDs = (img: HTMLImageElement): string[] => {
73+
const canvas = document.createElement("canvas");
74+
canvas.width = img.width;
75+
canvas.height = img.height;
76+
const ctx = canvas.getContext("2d")!;
77+
ctx.drawImage(img, 0, 0);
78+
79+
if (img.width % 128 || img.height % 128)
80+
throw new Error(`Dimensions must be divisible by 128. Got ${img.width}×${img.height}`);
81+
82+
const results: string[] = [];
83+
for (let by = 0; by < img.height / 128; by++) {
84+
for (let bx = 0; bx < img.width / 128; bx++) {
85+
const { data } = ctx.getImageData(bx * 128, by * 128, 128, 128);
86+
const colors = new Uint8Array(128 * 128);
87+
for (let i = 0; i < 128 * 128; i++) {
88+
const off = i * 4;
89+
const argb = ((data[off + 3] << 24) | (data[off] << 16) | (data[off + 1] << 8) | data[off + 2]) | 0;
90+
const colorByte = MAP_COLORS_REVERSE.get(argb);
91+
if (colorByte === undefined) {
92+
const x = bx * 128 + (i % 128), y = by * 128 + ((i / 128) | 0);
93+
throw new Error(`Unsupported color at (${x}, ${y}): ARGB=${argb}`);
94+
}
95+
colors[i] = colorByte;
96+
}
97+
results.push(setLockedBit(nameUUIDFromBytes(colors)));
98+
}
99+
}
100+
return results;
101+
};
102+
103+
export default function Index() {
104+
const { resolved, toggle } = useTheme();
105+
const [uuids, setUuids] = useState<string[]>([]);
106+
const [copied, setCopied] = useState(false);
107+
const [isDragging, setIsDragging] = useState(false);
108+
const [activeTab, setActiveTab] = useState<"binary" | "image">("binary");
109+
const [error, setError] = useState<string | null>(null);
110+
const [isEditing, setIsEditing] = useState(false);
111+
const [editText, setEditText] = useState("");
112+
113+
const handleFile = useCallback((file: File, mode: "binary" | "image") => {
114+
setError(null);
115+
if (mode === "binary") {
116+
const reader = new FileReader();
117+
reader.onload = (e) => {
118+
const parsed = parseUUIDs(e.target!.result as ArrayBuffer);
119+
setUuids(parsed);
120+
if (!parsed.length) setError("No UUIDs found. File may be empty or invalid.");
121+
};
122+
reader.readAsArrayBuffer(file);
123+
} else {
124+
const img = new Image();
125+
img.onload = () => {
126+
try {
127+
const parsed = parseImageUUIDs(img);
128+
setUuids(parsed);
129+
if (!parsed.length) setError("No UUIDs found.");
130+
} catch (err) {
131+
setError(err instanceof Error ? err.message : "Unknown error");
132+
}
133+
};
134+
img.src = URL.createObjectURL(file);
135+
}
136+
}, []);
137+
138+
const handleDrop = useCallback((e: React.DragEvent) => {
139+
e.preventDefault();
140+
setIsDragging(false);
141+
const file = e.dataTransfer.files[0];
142+
if (file) handleFile(file, activeTab);
143+
}, [activeTab, handleFile]);
144+
145+
const handleInput = (e: React.ChangeEvent<HTMLInputElement>) => {
146+
const file = e.target.files?.[0];
147+
if (file) handleFile(file, activeTab);
148+
};
149+
150+
const handleEdit = () => { setEditText(uuids.join("\n")); setIsEditing(true); };
151+
152+
const handleSave = () => {
153+
const lines = editText.split("\n").map(l => l.trim()).filter(Boolean);
154+
const bytes = uuidsToBytes(lines);
155+
const a = document.createElement("a");
156+
a.href = URL.createObjectURL(new Blob([bytes.buffer as ArrayBuffer]));
157+
a.download = "uuids.bin";
158+
a.click();
159+
URL.revokeObjectURL(a.href);
160+
setUuids(lines);
161+
setIsEditing(false);
162+
setError(null);
163+
};
164+
165+
const copyAll = async () => {
166+
await navigator.clipboard.writeText(uuids.join("\n"));
167+
setCopied(true);
168+
setTimeout(() => setCopied(false), 2000);
169+
};
170+
171+
const editLines = isEditing
172+
? editText.split("\n").map(l => l.trim().toLowerCase()).filter(Boolean)
173+
: [];
174+
const invalidCount = editLines.filter(l => !UUID_RE.test(l)).length;
175+
const uniqueCount = isEditing
176+
? new Set(editLines).size
177+
: new Set(uuids.map(u => u.toLowerCase())).size;
178+
179+
const saveDisabled = (() => {
180+
if (!isEditing) return true;
181+
if (invalidCount > 0) return true;
182+
const editSet = new Set(editLines);
183+
const origSet = new Set(uuids.map(u => u.toLowerCase()));
184+
return editSet.size === origSet.size && [...editSet].every(u => origSet.has(u));
185+
})();
186+
187+
return (
188+
<div className="min-h-screen p-6 md:p-12">
189+
<div className="mx-auto max-w-2xl">
190+
<div className="mb-6 flex items-center justify-between">
191+
<h1 className="text-2xl font-semibold">Map Hash Extractor</h1>
192+
<button
193+
onClick={toggle}
194+
className="rounded p-2 hover:bg-muted transition-colors"
195+
aria-label="Toggle theme"
196+
>
197+
{resolved === "dark" ? "☀️" : "🌙"}
198+
</button>
199+
</div>
200+
201+
<div className="mb-4 flex gap-2">
202+
{(["binary", "image"] as const).map(tab => (
203+
<button
204+
key={tab}
205+
onClick={() => setActiveTab(tab)}
206+
className={`px-4 py-2 rounded border transition-colors ${
207+
activeTab === tab ? "bg-accent text-white border-accent" : "border-border hover:bg-muted"
208+
}`}
209+
>
210+
{tab === "binary" ? "Binary File" : "PNG Image"}
211+
</button>
212+
))}
213+
</div>
214+
215+
<div
216+
className={`border-2 border-dashed rounded-lg p-8 text-center transition-colors ${
217+
isDragging ? "border-accent bg-accent/10" : "border-border hover:border-muted-foreground"
218+
}`}
219+
onDragOver={(e) => { e.preventDefault(); setIsDragging(true); }}
220+
onDragLeave={() => setIsDragging(false)}
221+
onDrop={handleDrop}
222+
>
223+
<p className="mb-2 text-muted-foreground">
224+
{activeTab === "binary" ? "Drop binary file here" : "Drop PNG image here"}
225+
</p>
226+
{activeTab === "image" && (
227+
<p className="mb-4 text-xs text-muted-foreground">Dimensions must be 128×128 multiples. Lossy formats unsupported.</p>
228+
)}
229+
<label className="inline-block cursor-pointer px-4 py-2 rounded bg-muted hover:bg-border transition-colors">
230+
<input type="file" className="hidden" onChange={handleInput} accept={activeTab === "image" ? "image/png" : "*"} key={activeTab} />
231+
Select {activeTab === "binary" ? "File" : "PNG"}
232+
</label>
233+
</div>
234+
235+
{error && <p className="mt-4 text-red-500 text-sm">{error}</p>}
236+
237+
{uuids.length > 0 && (
238+
<div className="mt-6 border border-border rounded-lg p-4">
239+
<div className="mb-3 flex items-center justify-between">
240+
<span className="text-sm text-muted-foreground">
241+
{isEditing && invalidCount > 0
242+
? <span className="text-red-500">{invalidCount} invalid UUID{invalidCount !== 1 && "s"}</span>
243+
: `${uniqueCount} UUID${uniqueCount !== 1 ? "s" : ""}`}
244+
</span>
245+
<div className="flex gap-2">
246+
<button onClick={copyAll} className="px-3 py-1 text-sm rounded hover:bg-muted transition-colors">
247+
{copied ? "✓ Copied" : "Copy All"}
248+
</button>
249+
{isEditing ? (
250+
<button
251+
onClick={handleSave}
252+
disabled={saveDisabled}
253+
className={`px-3 py-1 text-sm rounded transition-colors ${
254+
saveDisabled ? "bg-muted text-muted-foreground cursor-not-allowed" : "bg-accent text-white hover:bg-accent/80"
255+
}`}
256+
>
257+
Save
258+
</button>
259+
) : (
260+
<button onClick={handleEdit} className="px-3 py-1 text-sm rounded hover:bg-muted transition-colors">Edit</button>
261+
)}
262+
</div>
263+
</div>
264+
<div className="max-h-96 overflow-auto rounded bg-muted p-4">
265+
{isEditing ? (
266+
<textarea
267+
value={editText}
268+
onChange={(e) => setEditText(e.target.value)}
269+
className="w-full h-64 font-mono text-sm bg-transparent border-none outline-none resize-none"
270+
/>
271+
) : (
272+
<pre className="font-mono text-sm whitespace-pre-wrap break-all">{uuids.join("\n")}</pre>
273+
)}
274+
</div>
275+
</div>
276+
)}
277+
</div>
278+
</div>
279+
);
280+
}

uuid_tool/src/hooks/useTheme.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { useState, useEffect } from "react";
2+
3+
type Theme = "light" | "dark" | "system";
4+
5+
const getSystemTheme = () =>
6+
matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
7+
8+
export function useTheme() {
9+
const [theme, setTheme] = useState<Theme>(
10+
() => (localStorage.getItem("theme") as Theme) ?? "system"
11+
);
12+
13+
const resolved = theme === "system" ? getSystemTheme() : theme;
14+
15+
useEffect(() => {
16+
const root = document.documentElement;
17+
root.classList.toggle("dark", resolved === "dark");
18+
19+
if (theme === "system") localStorage.removeItem("theme");
20+
else localStorage.setItem("theme", theme);
21+
22+
if (theme !== "system") return;
23+
const mq = matchMedia("(prefers-color-scheme: dark)");
24+
const onChange = () => setTheme((t) => (t === "system" ? "system" : t));
25+
mq.addEventListener("change", onChange);
26+
return () => mq.removeEventListener("change", onChange);
27+
}, [theme, resolved]);
28+
29+
const toggle = () =>
30+
setTheme(resolved === "dark" ? "light" : "dark");
31+
32+
return { theme, resolved, toggle } as const;
33+
}

uuid_tool/src/index.css

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,13 @@
1111
--accent: 220 70% 50%;
1212
}
1313

14-
@media (prefers-color-scheme: dark) {
15-
:root {
16-
--background: 220 20% 8%;
17-
--foreground: 220 15% 95%;
18-
--muted: 220 15% 16%;
19-
--muted-foreground: 220 10% 55%;
20-
--border: 220 15% 20%;
21-
--accent: 220 70% 55%;
22-
}
14+
.dark {
15+
--background: 220 20% 8%;
16+
--foreground: 220 15% 95%;
17+
--muted: 220 15% 16%;
18+
--muted-foreground: 220 10% 55%;
19+
--border: 220 15% 20%;
20+
--accent: 220 70% 55%;
2321
}
2422

2523
body {

0 commit comments

Comments
 (0)