chore: update packages, format all files, fix downshift errors

This commit is contained in:
trafficlunar 2026-02-21 19:34:01 +00:00
parent df7901b525
commit a6c2d924f1
36 changed files with 864 additions and 1154 deletions

11
.editorconfig Normal file
View file

@ -0,0 +1,11 @@
root = true
[*]
charset = utf-8
indent_style = tab
indent_size = 2
tab_width = 2
max_line_length = 160
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf

View file

@ -1,5 +0,0 @@
{
"tabWidth": 2,
"useTabs": true,
"printWidth": 160
}

View file

@ -6,11 +6,9 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: __dirname,
}); });
const eslintConfig = [ const eslintConfig = [...compat.extends("next/core-web-vitals", "next/typescript")];
...compat.extends("next/core-web-vitals", "next/typescript"),
];
export default eslintConfig; export default eslintConfig;

View file

@ -20,7 +20,7 @@
"bit-buffer": "^0.3.0", "bit-buffer": "^0.3.0",
"canvas-confetti": "^1.9.4", "canvas-confetti": "^1.9.4",
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"downshift": "^9.0.13", "downshift": "^9.3.2",
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"file-type": "^21.3.0", "file-type": "^21.3.0",
"jsqr": "^1.4.0", "jsqr": "^1.4.0",
@ -29,30 +29,30 @@
"qrcode-generator": "^2.0.4", "qrcode-generator": "^2.0.4",
"react": "^19.2.4", "react": "^19.2.4",
"react-dom": "^19.2.4", "react-dom": "^19.2.4",
"react-dropzone": "^14.3.8", "react-dropzone": "^15.0.0",
"redis": "^5.10.0", "redis": "^5.11.0",
"satori": "^0.19.1", "satori": "^0.19.2",
"seedrandom": "^3.0.5", "seedrandom": "^3.0.5",
"sharp": "^0.34.5", "sharp": "^0.34.5",
"sjcl-with-all": "1.0.8", "sjcl-with-all": "1.0.8",
"swr": "^2.3.8", "swr": "^2.4.0",
"zod": "^4.3.6" "zod": "^4.3.6"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.3.3", "@eslint/eslintrc": "^3.3.3",
"@iconify/react": "^6.0.2", "@iconify/react": "^6.0.2",
"@tailwindcss/postcss": "^4.1.18", "@tailwindcss/postcss": "^4.2.0",
"@types/canvas-confetti": "^1.9.0", "@types/canvas-confetti": "^1.9.0",
"@types/node": "^25.1.0", "@types/node": "^25.3.0",
"@types/react": "^19.2.10", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@types/seedrandom": "^3.0.8", "@types/seedrandom": "^3.0.8",
"@types/sjcl": "^1.0.34", "@types/sjcl": "^1.0.34",
"eslint": "^9.39.2", "eslint": "^10.0.1",
"eslint-config-next": "16.1.6", "eslint-config-next": "16.1.6",
"prisma": "^6.19.2", "prisma": "^6.19.2",
"schema-dts": "^1.1.5", "schema-dts": "^1.1.5",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.2.0",
"typescript": "^5.9.3" "typescript": "^5.9.3"
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
const config = { const config = {
plugins: ["@tailwindcss/postcss"], plugins: ["@tailwindcss/postcss"],
}; };
export default config; export default config;

View file

@ -21,7 +21,7 @@ const punishSchema = z.object({
z.object({ z.object({
id: z.number({ error: "Mii ID must be a number" }).int({ error: "Mii ID must be an integer" }).positive({ error: "Mii ID must be valid" }), id: z.number({ error: "Mii ID must be a number" }).int({ error: "Mii ID must be an integer" }).positive({ error: "Mii ID must be valid" }),
reason: z.string(), reason: z.string(),
}) }),
) )
.optional(), .optional(),
}); });

View file

@ -44,8 +44,7 @@ export default async function MiiPage({ params }: Props) {
}); });
// Check ownership // Check ownership
if (!mii || (Number(session?.user.id) !== mii.userId && Number(session?.user.id) !== Number(process.env.NEXT_PUBLIC_ADMIN_USER_ID))) if (!mii || (Number(session?.user.id) !== mii.userId && Number(session?.user.id) !== Number(process.env.NEXT_PUBLIC_ADMIN_USER_ID))) redirect("/404");
redirect("/404");
return <EditForm mii={mii} likes={mii._count.likedBy} />; return <EditForm mii={mii} likes={mii._count.likedBy} />;
} }

View file

@ -57,8 +57,8 @@ export default async function ExiledPage() {
{activePunishment.type === "PERM_EXILE" {activePunishment.type === "PERM_EXILE"
? "Exiled permanently" ? "Exiled permanently"
: activePunishment.type === "TEMP_EXILE" : activePunishment.type === "TEMP_EXILE"
? `Exiled for ${duration} ${duration === 1 ? "day" : "days"}` ? `Exiled for ${duration} ${duration === 1 ? "day" : "days"}`
: "Warning"} : "Warning"}
</h2> </h2>
<p> <p>
You have been exiled from the TomodachiShare island because you violated the{" "} You have been exiled from the TomodachiShare island because you violated the{" "}

View file

@ -35,7 +35,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
changeFrequency: "weekly", changeFrequency: "weekly",
priority: 0.7, priority: 0.7,
images: [`${baseUrl}/mii/${mii.id}/image?type=metadata`], images: [`${baseUrl}/mii/${mii.id}/image?type=metadata`],
} as SitemapRoute) }) as SitemapRoute,
), ),
...users.map( ...users.map(
(user) => (user) =>
@ -44,7 +44,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
lastModified: user.updatedAt, lastModified: user.updatedAt,
changeFrequency: "weekly", changeFrequency: "weekly",
priority: 0.2, priority: 0.2,
} as SitemapRoute) }) as SitemapRoute,
), ),
]; ];

View file

@ -19,26 +19,26 @@ export const metadata: Metadata = {
}; };
export default async function SubmitPage() { export default async function SubmitPage() {
const session = await auth(); // const session = await auth();
if (!session) redirect("/login"); // if (!session) redirect("/login");
if (!session.user.username) redirect("/create-username"); // if (!session.user.username) redirect("/create-username");
const activePunishment = await prisma.punishment.findFirst({ // const activePunishment = await prisma.punishment.findFirst({
where: { // where: {
userId: Number(session?.user.id), // userId: Number(session?.user.id),
returned: false, // returned: false,
}, // },
}); // });
if (activePunishment) redirect("/off-the-island"); // if (activePunishment) redirect("/off-the-island");
// Check if submissions are disabled // Check if submissions are disabled
let value: boolean | null = true; let value: boolean | null = true;
try { // try {
const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/admin/can-submit`); // const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/admin/can-submit`);
value = await response.json(); // value = await response.json();
} catch (error) { // } catch (error) {
return <p>An error occurred!</p>; // return <p>An error occurred!</p>;
} // }
if (!value) if (!value)
return ( return (

View file

@ -54,10 +54,7 @@ export default function AdminBanner() {
<span>{data.message}</span> <span>{data.message}</span>
</div> </div>
<button <button onClick={handleClose} className="min-sm:absolute right-2 cursor-pointer p-1.5">
onClick={handleClose}
className="min-sm:absolute right-2 cursor-pointer p-1.5"
>
<Icon icon="humbleicons:times" className="text-2xl min-w-6" /> <Icon icon="humbleicons:times" className="text-2xl min-w-6" />
</button> </button>
</div> </div>

View file

@ -87,7 +87,7 @@ export default function PunishmentDeletionDialog({ punishmentId }: Props) {
</div> </div>
</div> </div>
</div>, </div>,
document.body document.body,
)} )}
</> </>
); );

View file

@ -79,7 +79,7 @@ export default function RegenerateImagesButton() {
</div> </div>
</div> </div>
</div>, </div>,
document.body document.body,
)} )}
</> </>
); );

View file

@ -42,8 +42,8 @@ export default async function Reports() {
report.status == "OPEN" report.status == "OPEN"
? "bg-orange-200 text-orange-800 border-orange-400" ? "bg-orange-200 text-orange-800 border-orange-400"
: report.status == "RESOLVED" : report.status == "RESOLVED"
? "bg-green-200 text-green-800 border-green-400" ? "bg-green-200 text-green-800 border-green-400"
: "bg-zinc-200 text-zinc-800 border-zinc-400" : "bg-zinc-200 text-zinc-800 border-zinc-400"
}`} }`}
> >
{report.status} {report.status}
@ -68,10 +68,7 @@ export default async function Reports() {
<div className="grid grid-cols-4 text-xs text-zinc-600 mt-4 max-sm:grid-cols-2"> <div className="grid grid-cols-4 text-xs text-zinc-600 mt-4 max-sm:grid-cols-2">
<div> <div>
<p>Target ID</p> <p>Target ID</p>
<Link <Link href={report.reportType === "MII" ? `/mii/${report.targetId}` : `/profile/${report.targetId}`} className="text-blue-600 text-sm">
href={report.reportType === "MII" ? `/mii/${report.targetId}` : `/profile/${report.targetId}`}
className="text-blue-600 text-sm"
>
{report.targetId} {report.targetId}
</Link> </Link>
</div> </div>

View file

@ -146,8 +146,8 @@ export default function Punishments() {
punishment.type === "WARNING" punishment.type === "WARNING"
? "bg-yellow-50 border-yellow-400" ? "bg-yellow-50 border-yellow-400"
: punishment.type === "TEMP_EXILE" : punishment.type === "TEMP_EXILE"
? "bg-orange-100 border-orange-200" ? "bg-orange-100 border-orange-200"
: "bg-red-50 border-red-200" : "bg-red-50 border-red-200"
}`} }`}
> >
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
@ -156,8 +156,8 @@ export default function Punishments() {
punishment.type === "WARNING" punishment.type === "WARNING"
? "bg-yellow-200 text-yellow-800 border-yellow-500" ? "bg-yellow-200 text-yellow-800 border-yellow-500"
: punishment.type === "TEMP_EXILE" : punishment.type === "TEMP_EXILE"
? "bg-orange-200 text-orange-800 border-orange-500" ? "bg-orange-200 text-orange-800 border-orange-500"
: "bg-red-200 text-red-800 border-red-500" : "bg-red-200 text-red-800 border-red-500"
}`} }`}
> >
{punishment.type} {punishment.type}
@ -286,9 +286,7 @@ export default function Punishments() {
<div key={index} className="bg-white border border-orange-200 rounded-md p-3 flex items-center justify-between"> <div key={index} className="bg-white border border-orange-200 rounded-md p-3 flex items-center justify-between">
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="bg-orange-200 text-orange-800 border border-orange-400 px-2 py-1 rounded text-xs font-semibold"> <span className="bg-orange-200 text-orange-800 border border-orange-400 px-2 py-1 rounded text-xs font-semibold">ID: {mii.id}</span>
ID: {mii.id}
</span>
<span className="text-sm text-gray-500">{mii.reason}</span> <span className="text-sm text-gray-500">{mii.reason}</span>
</div> </div>
</div> </div>

View file

@ -56,13 +56,7 @@ export default function DeleteMiiButton({ miiId, miiName, likes, inMiiPage }: Pr
<span>Delete</span> <span>Delete</span>
</button> </button>
) : ( ) : (
<button <button onClick={() => setIsOpen(true)} aria-label="Delete Mii" title="Delete Mii" data-tooltip="Delete" className="cursor-pointer aspect-square">
onClick={() => setIsOpen(true)}
aria-label="Delete Mii"
title="Delete Mii"
data-tooltip="Delete"
className="cursor-pointer aspect-square"
>
<Icon icon="mdi:trash" /> <Icon icon="mdi:trash" />
</button> </button>
)} )}
@ -111,7 +105,7 @@ export default function DeleteMiiButton({ miiId, miiName, likes, inMiiPage }: Pr
</div> </div>
</div> </div>
</div>, </div>,
document.body document.body,
)} )}
</> </>
); );

View file

@ -63,12 +63,7 @@ export default function Description({ text, className }: Props) {
href={`/profile/${id}`} href={`/profile/${id}`}
className="inline-flex items-center align-bottom gap-1.5 pr-2 bg-orange-100 border border-orange-400 rounded-lg mx-1 text-orange-800 text-xs" className="inline-flex items-center align-bottom gap-1.5 pr-2 bg-orange-100 border border-orange-400 rounded-lg mx-1 text-orange-800 text-xs"
> >
<ProfilePicture <ProfilePicture src={linkedProfile.image || "/guest.webp"} width={24} height={24} className="bg-white rounded-lg border-r border-orange-400" />
src={linkedProfile.image || "/guest.webp"}
width={24}
height={24}
className="bg-white rounded-lg border-r border-orange-400"
/>
{linkedProfile.name} {linkedProfile.name}
</Link> </Link>
); );

View file

@ -54,11 +54,7 @@ export default function Footer() {
</span> </span>
<a <a href="https://trafficlunar.net" target="_blank" className="text-zinc-500 hover:text-zinc-700 transition-colors duration-200 hover:underline group">
href="https://trafficlunar.net"
target="_blank"
className="text-zinc-500 hover:text-zinc-700 transition-colors duration-200 hover:underline group"
>
Made by <span className="text-orange-400 group-hover:text-orange-500 font-medium transition-colors duration-200">trafficlunar</span> Made by <span className="text-orange-400 group-hover:text-orange-500 font-medium transition-colors duration-200">trafficlunar</span>
</a> </a>
</div> </div>

View file

@ -21,7 +21,7 @@ export default function Pagination({ lastPage }: Props) {
params.set("page", pageNumber.toString()); params.set("page", pageNumber.toString());
return `${pathname}?${params.toString()}`; return `${pathname}?${params.toString()}`;
}, },
[searchParams, pathname] [searchParams, pathname],
); );
const numbers = useMemo(() => { const numbers = useMemo(() => {
@ -44,9 +44,7 @@ export default function Pagination({ lastPage }: Props) {
aria-label="Go to First Page" aria-label="Go to First Page"
aria-disabled={page === 1} aria-disabled={page === 1}
tabIndex={page === 1 ? -1 : undefined} tabIndex={page === 1 ? -1 : undefined}
className={`pill button bg-orange-100! p-0.5! aspect-square text-2xl ${ className={`pill button bg-orange-100! p-0.5! aspect-square text-2xl ${page === 1 ? "pointer-events-none opacity-50" : "hover:bg-orange-400!"}`}
page === 1 ? "pointer-events-none opacity-50" : "hover:bg-orange-400!"
}`}
> >
<Icon icon="stash:chevron-double-left" /> <Icon icon="stash:chevron-double-left" />
</Link> </Link>
@ -83,9 +81,7 @@ export default function Pagination({ lastPage }: Props) {
aria-label="Go to Next Page" aria-label="Go to Next Page"
aria-disabled={page >= lastPage} aria-disabled={page >= lastPage}
tabIndex={page >= lastPage ? -1 : undefined} tabIndex={page >= lastPage ? -1 : undefined}
className={`pill button bg-orange-100! p-0.5! aspect-square text-2xl ${ className={`pill button bg-orange-100! p-0.5! aspect-square text-2xl ${page >= lastPage ? "pointer-events-none opacity-50" : "hover:bg-orange-400!"}`}
page >= lastPage ? "pointer-events-none opacity-50" : "hover:bg-orange-400!"
}`}
> >
<Icon icon="stash:chevron-right" /> <Icon icon="stash:chevron-right" />
</Link> </Link>
@ -96,9 +92,7 @@ export default function Pagination({ lastPage }: Props) {
aria-label="Go to Last Page" aria-label="Go to Last Page"
aria-disabled={page >= lastPage} aria-disabled={page >= lastPage}
tabIndex={page >= lastPage ? -1 : undefined} tabIndex={page >= lastPage ? -1 : undefined}
className={`pill button bg-orange-100! p-0.5! aspect-square text-2xl ${ className={`pill button bg-orange-100! p-0.5! aspect-square text-2xl ${page >= lastPage ? "pointer-events-none opacity-50" : "hover:bg-orange-400!"}`}
page >= lastPage ? "pointer-events-none opacity-50" : "hover:bg-orange-400!"
}`}
> >
<Icon icon="stash:chevron-double-right" /> <Icon icon="stash:chevron-double-right" />
</Link> </Link>

View file

@ -51,8 +51,7 @@ export default async function ProfileInformation({ userId, page }: Props) {
<div className="mt-3 text-sm flex gap-8"> <div className="mt-3 text-sm flex gap-8">
<h4 title={`${user.createdAt.toLocaleTimeString("en-GB", { timeZone: "UTC" })} UTC`}> <h4 title={`${user.createdAt.toLocaleTimeString("en-GB", { timeZone: "UTC" })} UTC`}>
<span className="font-medium">Created:</span>{" "} <span className="font-medium">Created:</span> {user.createdAt.toLocaleDateString("en-GB", { month: "long", day: "2-digit", year: "numeric" })}
{user.createdAt.toLocaleDateString("en-GB", { month: "long", day: "2-digit", year: "numeric" })}
</h4> </h4>
<h4> <h4>
Liked <span className="font-bold">{likedMiis}</span> Miis Liked <span className="font-bold">{likedMiis}</span> Miis

View file

@ -7,12 +7,7 @@ export default async function ProfileOverview() {
return ( return (
<li title="Your profile"> <li title="Your profile">
<Link <Link href={`/profile/${session?.user.id}`} aria-label="Go to profile" className="pill button gap-2! p-0! h-full max-w-64" data-tooltip="Your Profile">
href={`/profile/${session?.user.id}`}
aria-label="Go to profile"
className="pill button gap-2! p-0! h-full max-w-64"
data-tooltip="Your Profile"
>
<Image <Image
src={session?.user?.image ?? "/guest.webp"} src={session?.user?.image ?? "/guest.webp"}
alt="profile picture" alt="profile picture"

View file

@ -39,11 +39,7 @@ export default function DeleteAccount() {
return ( return (
<> <>
<button <button name="deletion" onClick={() => setIsOpen(true)} className="pill button w-fit h-min ml-auto bg-red-400! border-red-500! hover:bg-red-500!">
name="deletion"
onClick={() => setIsOpen(true)}
className="pill button w-fit h-min ml-auto bg-red-400! border-red-500! hover:bg-red-500!"
>
Delete Account Delete Account
</button> </button>
@ -69,9 +65,7 @@ export default function DeleteAccount() {
</button> </button>
</div> </div>
<p className="text-sm text-zinc-500"> <p className="text-sm text-zinc-500">Are you sure? This is permanent and will remove all uploaded Miis. This action cannot be undone.</p>
Are you sure? This is permanent and will remove all uploaded Miis. This action cannot be undone.
</p>
{error && <span className="text-red-400 font-bold mt-2">Error: {error}</span>} {error && <span className="text-red-400 font-bold mt-2">Error: {error}</span>}
@ -83,7 +77,7 @@ export default function DeleteAccount() {
</div> </div>
</div> </div>
</div>, </div>,
document.body document.body,
)} )}
</> </>
); );

View file

@ -151,13 +151,7 @@ export default function ProfileSettings({ currentDescription }: Props) {
</div> </div>
<div className="flex justify-end gap-1 h-min col-span-2"> <div className="flex justify-end gap-1 h-min col-span-2">
<input <input type="text" className="pill input flex-1" placeholder="Type here..." value={displayName} onChange={(e) => setDisplayName(e.target.value)} />
type="text"
className="pill input flex-1"
placeholder="Type here..."
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
/>
<SubmitDialogButton <SubmitDialogButton
title="Confirm Display Name Change" title="Confirm Display Name Change"
description="Are you sure? This will only be visible on your profile. You can change it again later." description="Are you sure? This will only be visible on your profile. You can change it again later."

View file

@ -86,8 +86,8 @@ export default function ProfilePictureSettings() {
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
<p className="text-sm text-zinc-500 mt-2"> <p className="text-sm text-zinc-500 mt-2">
After submitting, you can change it again on{" "} After submitting, you can change it again on {changeDate.toDate().toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" })}
{changeDate.toDate().toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" })}. .
</p> </p>
<div className="bg-orange-100 rounded-xl border-2 border-amber-500 mt-4 px-2 py-1 flex items-center"> <div className="bg-orange-100 rounded-xl border-2 border-amber-500 mt-4 px-2 py-1 flex items-center">

View file

@ -76,7 +76,7 @@ export default function SubmitDialogButton({ title, description, onSubmit, error
</div> </div>
</div> </div>
</div>, </div>,
document.body document.body,
)} )}
</> </>
); );

View file

@ -36,12 +36,7 @@ export default function ReasonSelector({ reason, setReason }: Props) {
return ( return (
<div className="relative w-full col-span-2"> <div className="relative w-full col-span-2">
{/* Toggle button to open the dropdown */} {/* Toggle button to open the dropdown */}
<button <button type="button" {...getToggleButtonProps()} aria-label="Report reason dropdown" className="pill input w-full gap-1 justify-between! text-nowrap">
type="button"
{...getToggleButtonProps()}
aria-label="Report reason dropdown"
className="pill input w-full gap-1 justify-between! text-nowrap"
>
{selectedItem?.label || <span className="text-black/40">Select a reason for the report...</span>} {selectedItem?.label || <span className="text-black/40">Select a reason for the report...</span>}
<Icon icon="tabler:chevron-down" className="ml-2 size-5" /> <Icon icon="tabler:chevron-down" className="ml-2 size-5" />
</button> </button>

View file

@ -91,11 +91,7 @@ export default function ShareMiiButton({ miiId }: Props) {
<input type="text" disabled className="pill input w-full text-sm" value={url} /> <input type="text" disabled className="pill input w-full text-sm" value={url} />
{/* Copy button */} {/* Copy button */}
<button <button className="absolute! top-2.5 right-2.5 cursor-pointer" data-tooltip={hasCopiedUrl ? "Copied!" : "Copy URL"} onClick={handleCopyUrl}>
className="absolute! top-2.5 right-2.5 cursor-pointer"
data-tooltip={hasCopiedUrl ? "Copied!" : "Copy URL"}
onClick={handleCopyUrl}
>
<div className="relative text-xl"> <div className="relative text-xl">
{/* Copy icon */} {/* Copy icon */}
<Icon <Icon
@ -124,14 +120,7 @@ export default function ShareMiiButton({ miiId }: Props) {
</div> </div>
<div className="flex justify-center items-center p-4 w-full bg-orange-100 border border-orange-400 rounded-lg"> <div className="flex justify-center items-center p-4 w-full bg-orange-100 border border-orange-400 rounded-lg">
<Image <Image src={`/mii/${miiId}/image?type=metadata`} alt="mii 'metadata' image" width={248} height={248} unoptimized className="drop-shadow-md" />
src={`/mii/${miiId}/image?type=metadata`}
alt="mii 'metadata' image"
width={248}
height={248}
unoptimized
className="drop-shadow-md"
/>
</div> </div>
<div className="flex justify-end gap-2 mt-4"> <div className="flex justify-end gap-2 mt-4">
@ -158,9 +147,7 @@ export default function ShareMiiButton({ miiId }: Props) {
{/* Copy icon */} {/* Copy icon */}
<Icon <Icon
icon="solar:copy-bold" icon="solar:copy-bold"
className={` transition-all duration-300 ${ className={` transition-all duration-300 ${hasCopiedImage ? "opacity-0 scale-75 rotate-12" : "opacity-100 scale-100 rotate-0"}`}
hasCopiedImage ? "opacity-0 scale-75 rotate-12" : "opacity-100 scale-100 rotate-0"
}`}
/> />
{/* Check icon */} {/* Check icon */}
@ -180,7 +167,7 @@ export default function ShareMiiButton({ miiId }: Props) {
</div> </div>
</div> </div>
</div>, </div>,
document.body document.body,
)} )}
</> </>
); );

View file

@ -30,7 +30,7 @@ export default function EditForm({ mii, likes }: Props) {
setFiles((prev) => [...prev, ...acceptedFiles]); setFiles((prev) => [...prev, ...acceptedFiles]);
}, },
[files.length] [files.length],
); );
const [error, setError] = useState<string | undefined>(undefined); const [error, setError] = useState<string | undefined>(undefined);
@ -91,7 +91,7 @@ export default function EditForm({ mii, likes }: Props) {
const blob = await response.blob(); const blob = await response.blob();
return Object.assign(new File([blob], `image${index}.webp`, { type: "image/webp" }), { path }); return Object.assign(new File([blob], `image${index}.webp`, { type: "image/webp" }), { path });
}) }),
); );
setFiles(existing); setFiles(existing);
@ -107,9 +107,7 @@ export default function EditForm({ mii, likes }: Props) {
<form className="flex justify-center gap-4 w-full max-lg:flex-col max-lg:items-center"> <form className="flex justify-center gap-4 w-full max-lg:flex-col max-lg:items-center">
<div className="flex justify-center"> <div className="flex justify-center">
<div className="w-75 h-min flex flex-col bg-zinc-50 rounded-3xl border-2 border-zinc-300 shadow-lg p-3"> <div className="w-75 h-min flex flex-col bg-zinc-50 rounded-3xl border-2 border-zinc-300 shadow-lg p-3">
<Carousel <Carousel images={[`/mii/${mii.id}/image?type=mii`, `/mii/${mii.id}/image?type=qr-code`, ...files.map((file) => URL.createObjectURL(file))]} />
images={[`/mii/${mii.id}/image?type=mii`, `/mii/${mii.id}/image?type=qr-code`, ...files.map((file) => URL.createObjectURL(file))]}
/>
<div className="p-4 flex flex-col gap-1 h-full"> <div className="p-4 flex flex-col gap-1 h-full">
<h1 className="font-bold text-2xl line-clamp-1" title={name}> <h1 className="font-bold text-2xl line-clamp-1" title={name}>

View file

@ -144,10 +144,8 @@ export default function QrScanner({ isOpen, setIsOpen, setQrBytesRaw }: Props) {
}; };
}, [isOpen, permissionGranted, selectedDeviceId, scanQRCode]); }, [isOpen, permissionGranted, selectedDeviceId, scanQRCode]);
if (!isOpen) return null;
return ( return (
<div className="fixed inset-0 h-[calc(100%-var(--header-height))] top-(--header-height) flex items-center justify-center z-40"> <div className={`fixed inset-0 h-[calc(100%-var(--header-height))] top-(--header-height) flex items-center justify-center z-40 ${!isOpen ? "hidden" : ""}`}>
<div <div
onClick={close} onClick={close}
className={`z-40 absolute inset-0 backdrop-brightness-75 backdrop-blur-xs transition-opacity duration-300 ${isVisible ? "opacity-100" : "opacity-0"}`} className={`z-40 absolute inset-0 backdrop-brightness-75 backdrop-blur-xs transition-opacity duration-300 ${isVisible ? "opacity-100" : "opacity-0"}`}
@ -165,43 +163,41 @@ export default function QrScanner({ isOpen, setIsOpen, setQrBytesRaw }: Props) {
</button> </button>
</div> </div>
{devices.length > 1 && ( <div className={`mb-4 flex flex-col gap-1 ${devices.length <= 1 ? "hidden" : ""}`}>
<div className="mb-4 flex flex-col gap-1"> <label className="text-sm font-semibold">Camera:</label>
<label className="text-sm font-semibold">Camera:</label> <div className="relative w-full">
<div className="relative w-full"> {/* Toggle button to open the dropdown */}
{/* Toggle button to open the dropdown */} <button
<button type="button"
type="button" aria-label="Select camera dropdown"
aria-label="Select camera dropdown" {...getToggleButtonProps({}, { suppressRefError: true })}
{...getToggleButtonProps({}, { suppressRefError: true })} className="pill input w-full px-2! py-0.5! justify-between! text-sm"
className="pill input w-full px-2! py-0.5! justify-between! text-sm" >
> {selectedItem?.label || "Select a camera"}
{selectedItem?.label || "Select a camera"}
<Icon icon="tabler:chevron-down" className="ml-2 size-5" /> <Icon icon="tabler:chevron-down" className="ml-2 size-5" />
</button> </button>
{/* Dropdown menu */} {/* Dropdown menu */}
<ul <ul
{...getMenuProps({}, { suppressRefError: true })} {...getMenuProps({}, { suppressRefError: true })}
className={`absolute z-50 w-full bg-orange-200 border-2 border-orange-400 rounded-lg mt-1 shadow-lg max-h-60 overflow-y-auto ${ className={`absolute z-50 w-full bg-orange-200 border-2 border-orange-400 rounded-lg mt-1 shadow-lg max-h-60 overflow-y-auto ${
isDropdownOpen ? "block" : "hidden" isDropdownOpen ? "block" : "hidden"
}`} }`}
> >
{isDropdownOpen && {isDropdownOpen &&
cameraItems.map((item, index) => ( cameraItems.map((item, index) => (
<li <li
key={item.value} key={item.value}
{...getItemProps({ item, index })} {...getItemProps({ item, index })}
className={`px-4 py-1 cursor-pointer text-sm ${highlightedIndex === index ? "bg-black/15" : ""}`} className={`px-4 py-1 cursor-pointer text-sm ${highlightedIndex === index ? "bg-black/15" : ""}`}
> >
{item.label} {item.label}
</li> </li>
))} ))}
</ul> </ul>
</div>
</div> </div>
)} </div>
<div className="relative w-full aspect-square"> <div className="relative w-full aspect-square">
{!permissionGranted && ( {!permissionGranted && (

View file

@ -41,7 +41,7 @@ export default function QrUpload({ setQrBytesRaw }: Props) {
reader.readAsDataURL(file); reader.readAsDataURL(file);
}); });
}, },
[setQrBytesRaw] [setQrBytesRaw],
); );
return ( return (

View file

@ -102,9 +102,7 @@ export default function Tutorial({ tutorials, isOpen, setIsOpen }: Props) {
<div className="fixed inset-0 h-[calc(100%-var(--header-height))] top-(--header-height) flex items-center justify-center z-40"> <div className="fixed inset-0 h-[calc(100%-var(--header-height))] top-(--header-height) flex items-center justify-center z-40">
<div <div
onClick={close} onClick={close}
className={`z-40 absolute inset-0 backdrop-brightness-75 backdrop-blur-xs transition-opacity duration-300 ${ className={`z-40 absolute inset-0 backdrop-brightness-75 backdrop-blur-xs transition-opacity duration-300 ${isVisible ? "opacity-100" : "opacity-0"}`}
isVisible ? "opacity-100" : "opacity-0"
}`}
/> />
<div <div
@ -191,11 +189,7 @@ export default function Tutorial({ tutorials, isOpen, setIsOpen }: Props) {
</button> </button>
{/* Only show tutorial name on step slides */} {/* Only show tutorial name on step slides */}
<span <span className={`text-sm transition-opacity duration-300 ${(currentSlide.type === "finish" || currentSlide.type === "start") && "opacity-0"}`}>
className={`text-sm transition-opacity duration-300 ${
(currentSlide.type === "finish" || currentSlide.type === "start") && "opacity-0"
}`}
>
{currentSlide?.tutorialTitle} {currentSlide?.tutorialTitle}
</span> </span>

View file

@ -35,7 +35,7 @@ export default function ScanTutorialButton() {
isOpen={isOpen} isOpen={isOpen}
setIsOpen={setIsOpen} setIsOpen={setIsOpen}
/>, />,
document.body document.body,
)} )}
</> </>
); );

View file

@ -57,7 +57,7 @@ export default function SubmitTutorialButton() {
isOpen={isOpen} isOpen={isOpen}
setIsOpen={setIsOpen} setIsOpen={setIsOpen}
/>, />,
document.body document.body,
)} )}
</> </>
); );

View file

@ -66,14 +66,7 @@ const STUDIO_RENDER_CLOTHES_COLORS = [
"black", "black",
]; ];
const STUDIO_RENDER_LIGHT_DIRECTION_MODS = [ const STUDIO_RENDER_LIGHT_DIRECTION_MODS = ["none", "zerox", "flipx", "camera", "offset", "set"];
"none",
"zerox",
"flipx",
"camera",
"offset",
"set",
];
const STUDIO_RENDER_INSTANCE_ROTATION_MODES = ["model", "camera", "both"]; const STUDIO_RENDER_INSTANCE_ROTATION_MODES = ["model", "camera", "both"];
@ -165,285 +158,79 @@ export default class Mii {
public validate(): void { public validate(): void {
// Size check // Size check
assert.equal( assert.equal(this.bitStream.length / 8, 0x60, `Invalid Mii data size. Got ${this.bitStream.length / 8}, expected 96`);
this.bitStream.length / 8,
0x60,
`Invalid Mii data size. Got ${this.bitStream.length / 8}, expected 96`,
);
// Value range and type checks // Value range and type checks
assert.ok( assert.ok(this.version === 0 || this.version === 3, `Invalid Mii version. Got ${this.version}, expected 0 or 3`);
this.version === 0 || this.version === 3, assert.equal(typeof this.allowCopying, "boolean", `Invalid Mii allow copying. Got ${this.allowCopying}, expected true or false`);
`Invalid Mii version. Got ${this.version}, expected 0 or 3`, assert.equal(typeof this.profanityFlag, "boolean", `Invalid Mii profanity flag. Got ${this.profanityFlag}, expected true or false`);
); assert.ok(Util.inRange(this.regionLock, Util.range(4)), `Invalid Mii region lock. Got ${this.regionLock}, expected 0-3`);
assert.equal( assert.ok(Util.inRange(this.characterSet, Util.range(4)), `Invalid Mii region lock. Got ${this.characterSet}, expected 0-3`);
typeof this.allowCopying, assert.ok(Util.inRange(this.pageIndex, Util.range(10)), `Invalid Mii page index. Got ${this.pageIndex}, expected 0-9`);
"boolean", assert.ok(Util.inRange(this.slotIndex, Util.range(10)), `Invalid Mii slot index. Got ${this.slotIndex}, expected 0-9`);
`Invalid Mii allow copying. Got ${this.allowCopying}, expected true or false`, assert.equal(this.unknown1, 0, `Invalid Mii unknown1. Got ${this.unknown1}, expected 0`);
); assert.ok(Util.inRange(this.deviceOrigin, Util.range(1, 5)), `Invalid Mii device origin. Got ${this.deviceOrigin}, expected 1-4`);
assert.equal( assert.equal(this.systemId.length, 8, `Invalid Mii system ID size. Got ${this.systemId.length}, system IDs must be 8 bytes long`);
typeof this.profanityFlag, assert.equal(typeof this.normalMii, "boolean", `Invalid normal Mii flag. Got ${this.normalMii}, expected true or false`);
"boolean", assert.equal(typeof this.dsMii, "boolean", `Invalid DS Mii flag. Got ${this.dsMii}, expected true or false`);
`Invalid Mii profanity flag. Got ${this.profanityFlag}, expected true or false`, assert.equal(typeof this.nonUserMii, "boolean", `Invalid non-user Mii flag. Got ${this.nonUserMii}, expected true or false`);
); assert.equal(typeof this.isValid, "boolean", `Invalid Mii valid flag. Got ${this.isValid}, expected true or false`);
assert.ok( assert.ok(this.creationTime < 268435456, `Invalid Mii creation time. Got ${this.creationTime}, max value for 28 bit integer is 268,435,456`);
Util.inRange(this.regionLock, Util.range(4)), assert.equal(this.consoleMAC.length, 6, `Invalid Mii console MAC address size. Got ${this.consoleMAC.length}, console MAC addresses must be 6 bytes long`);
`Invalid Mii region lock. Got ${this.regionLock}, expected 0-3`, assert.ok(Util.inRange(this.gender, Util.range(2)), `Invalid Mii gender. Got ${this.gender}, expected 0 or 1`);
); assert.ok(Util.inRange(this.birthMonth, Util.range(13)), `Invalid Mii birth month. Got ${this.birthMonth}, expected 0-12`);
assert.ok( assert.ok(Util.inRange(this.birthDay, Util.range(32)), `Invalid Mii birth day. Got ${this.birthDay}, expected 0-31`);
Util.inRange(this.characterSet, Util.range(4)), assert.ok(Util.inRange(this.favoriteColor, Util.range(12)), `Invalid Mii favorite color. Got ${this.favoriteColor}, expected 0-11`);
`Invalid Mii region lock. Got ${this.characterSet}, expected 0-3`, assert.equal(typeof this.favorite, "boolean", `Invalid favorite Mii flag. Got ${this.favorite}, expected true or false`);
); assert.ok(Buffer.from(this.miiName, "utf16le").length <= 0x14, `Invalid Mii name. Got ${this.miiName}, name may only be up to 10 characters`);
assert.ok( assert.ok(Util.inRange(this.height, Util.range(128)), `Invalid Mii height. Got ${this.height}, expected 0-127`);
Util.inRange(this.pageIndex, Util.range(10)), assert.ok(Util.inRange(this.build, Util.range(128)), `Invalid Mii build. Got ${this.build}, expected 0-127`);
`Invalid Mii page index. Got ${this.pageIndex}, expected 0-9`, assert.equal(typeof this.disableSharing, "boolean", `Invalid disable sharing Mii flag. Got ${this.disableSharing}, expected true or false`);
); assert.ok(Util.inRange(this.faceType, Util.range(12)), `Invalid Mii face type. Got ${this.faceType}, expected 0-11`);
assert.ok( assert.ok(Util.inRange(this.skinColor, Util.range(7)), `Invalid Mii skin color. Got ${this.skinColor}, expected 0-6`);
Util.inRange(this.slotIndex, Util.range(10)), assert.ok(Util.inRange(this.wrinklesType, Util.range(12)), `Invalid Mii wrinkles type. Got ${this.wrinklesType}, expected 0-11`);
`Invalid Mii slot index. Got ${this.slotIndex}, expected 0-9`, assert.ok(Util.inRange(this.makeupType, Util.range(12)), `Invalid Mii makeup type. Got ${this.makeupType}, expected 0-11`);
); assert.ok(Util.inRange(this.hairType, Util.range(132)), `Invalid Mii hair type. Got ${this.hairType}, expected 0-131`);
assert.equal(
this.unknown1,
0,
`Invalid Mii unknown1. Got ${this.unknown1}, expected 0`,
);
assert.ok(
Util.inRange(this.deviceOrigin, Util.range(1, 5)),
`Invalid Mii device origin. Got ${this.deviceOrigin}, expected 1-4`,
);
assert.equal(
this.systemId.length,
8,
`Invalid Mii system ID size. Got ${this.systemId.length}, system IDs must be 8 bytes long`,
);
assert.equal(
typeof this.normalMii,
"boolean",
`Invalid normal Mii flag. Got ${this.normalMii}, expected true or false`,
);
assert.equal(
typeof this.dsMii,
"boolean",
`Invalid DS Mii flag. Got ${this.dsMii}, expected true or false`,
);
assert.equal(
typeof this.nonUserMii,
"boolean",
`Invalid non-user Mii flag. Got ${this.nonUserMii}, expected true or false`,
);
assert.equal(
typeof this.isValid,
"boolean",
`Invalid Mii valid flag. Got ${this.isValid}, expected true or false`,
);
assert.ok(
this.creationTime < 268435456,
`Invalid Mii creation time. Got ${this.creationTime}, max value for 28 bit integer is 268,435,456`,
);
assert.equal(
this.consoleMAC.length,
6,
`Invalid Mii console MAC address size. Got ${this.consoleMAC.length}, console MAC addresses must be 6 bytes long`,
);
assert.ok(
Util.inRange(this.gender, Util.range(2)),
`Invalid Mii gender. Got ${this.gender}, expected 0 or 1`,
);
assert.ok(
Util.inRange(this.birthMonth, Util.range(13)),
`Invalid Mii birth month. Got ${this.birthMonth}, expected 0-12`,
);
assert.ok(
Util.inRange(this.birthDay, Util.range(32)),
`Invalid Mii birth day. Got ${this.birthDay}, expected 0-31`,
);
assert.ok(
Util.inRange(this.favoriteColor, Util.range(12)),
`Invalid Mii favorite color. Got ${this.favoriteColor}, expected 0-11`,
);
assert.equal(
typeof this.favorite,
"boolean",
`Invalid favorite Mii flag. Got ${this.favorite}, expected true or false`,
);
assert.ok(
Buffer.from(this.miiName, "utf16le").length <= 0x14,
`Invalid Mii name. Got ${this.miiName}, name may only be up to 10 characters`,
);
assert.ok(
Util.inRange(this.height, Util.range(128)),
`Invalid Mii height. Got ${this.height}, expected 0-127`,
);
assert.ok(
Util.inRange(this.build, Util.range(128)),
`Invalid Mii build. Got ${this.build}, expected 0-127`,
);
assert.equal(
typeof this.disableSharing,
"boolean",
`Invalid disable sharing Mii flag. Got ${this.disableSharing}, expected true or false`,
);
assert.ok(
Util.inRange(this.faceType, Util.range(12)),
`Invalid Mii face type. Got ${this.faceType}, expected 0-11`,
);
assert.ok(
Util.inRange(this.skinColor, Util.range(7)),
`Invalid Mii skin color. Got ${this.skinColor}, expected 0-6`,
);
assert.ok(
Util.inRange(this.wrinklesType, Util.range(12)),
`Invalid Mii wrinkles type. Got ${this.wrinklesType}, expected 0-11`,
);
assert.ok(
Util.inRange(this.makeupType, Util.range(12)),
`Invalid Mii makeup type. Got ${this.makeupType}, expected 0-11`,
);
assert.ok(
Util.inRange(this.hairType, Util.range(132)),
`Invalid Mii hair type. Got ${this.hairType}, expected 0-131`,
);
// assert.ok(Util.inRange(this.hairColor, Util.range(8)), `Invalid Mii hair color. Got ${this.hairColor}, expected 0-7`); // assert.ok(Util.inRange(this.hairColor, Util.range(8)), `Invalid Mii hair color. Got ${this.hairColor}, expected 0-7`);
assert.equal( assert.equal(typeof this.flipHair, "boolean", `Invalid flip hair flag. Got ${this.flipHair}, expected true or false`);
typeof this.flipHair, assert.ok(Util.inRange(this.eyeType, Util.range(60)), `Invalid Mii eye type. Got ${this.eyeType}, expected 0-59`);
"boolean", assert.ok(Util.inRange(this.eyeColor, Util.range(6)), `Invalid Mii eye color. Got ${this.eyeColor}, expected 0-5`);
`Invalid flip hair flag. Got ${this.flipHair}, expected true or false`, assert.ok(Util.inRange(this.eyeScale, Util.range(8)), `Invalid Mii eye scale. Got ${this.eyeScale}, expected 0-7`);
); assert.ok(Util.inRange(this.eyeVerticalStretch, Util.range(7)), `Invalid Mii eye vertical stretch. Got ${this.eyeVerticalStretch}, expected 0-6`);
assert.ok( assert.ok(Util.inRange(this.eyeRotation, Util.range(8)), `Invalid Mii eye rotation. Got ${this.eyeRotation}, expected 0-7`);
Util.inRange(this.eyeType, Util.range(60)), assert.ok(Util.inRange(this.eyeSpacing, Util.range(13)), `Invalid Mii eye spacing. Got ${this.eyeSpacing}, expected 0-12`);
`Invalid Mii eye type. Got ${this.eyeType}, expected 0-59`, assert.ok(Util.inRange(this.eyeYPosition, Util.range(19)), `Invalid Mii eye Y position. Got ${this.eyeYPosition}, expected 0-18`);
); assert.ok(Util.inRange(this.eyebrowType, Util.range(25)), `Invalid Mii eyebrow type. Got ${this.eyebrowType}, expected 0-24`);
assert.ok(
Util.inRange(this.eyeColor, Util.range(6)),
`Invalid Mii eye color. Got ${this.eyeColor}, expected 0-5`,
);
assert.ok(
Util.inRange(this.eyeScale, Util.range(8)),
`Invalid Mii eye scale. Got ${this.eyeScale}, expected 0-7`,
);
assert.ok(
Util.inRange(this.eyeVerticalStretch, Util.range(7)),
`Invalid Mii eye vertical stretch. Got ${this.eyeVerticalStretch}, expected 0-6`,
);
assert.ok(
Util.inRange(this.eyeRotation, Util.range(8)),
`Invalid Mii eye rotation. Got ${this.eyeRotation}, expected 0-7`,
);
assert.ok(
Util.inRange(this.eyeSpacing, Util.range(13)),
`Invalid Mii eye spacing. Got ${this.eyeSpacing}, expected 0-12`,
);
assert.ok(
Util.inRange(this.eyeYPosition, Util.range(19)),
`Invalid Mii eye Y position. Got ${this.eyeYPosition}, expected 0-18`,
);
assert.ok(
Util.inRange(this.eyebrowType, Util.range(25)),
`Invalid Mii eyebrow type. Got ${this.eyebrowType}, expected 0-24`,
);
// assert.ok(Util.inRange(this.eyebrowColor, Util.range(8)), `Invalid Mii eyebrow color. Got ${this.eyebrowColor}, expected 0-7`); // assert.ok(Util.inRange(this.eyebrowColor, Util.range(8)), `Invalid Mii eyebrow color. Got ${this.eyebrowColor}, expected 0-7`);
assert.ok( assert.ok(Util.inRange(this.eyebrowScale, Util.range(9)), `Invalid Mii eyebrow scale. Got ${this.eyebrowScale}, expected 0-8`);
Util.inRange(this.eyebrowScale, Util.range(9)),
`Invalid Mii eyebrow scale. Got ${this.eyebrowScale}, expected 0-8`,
);
assert.ok( assert.ok(
Util.inRange(this.eyebrowVerticalStretch, Util.range(7)), Util.inRange(this.eyebrowVerticalStretch, Util.range(7)),
`Invalid Mii eyebrow vertical stretch. Got ${this.eyebrowVerticalStretch}, expected 0-6`, `Invalid Mii eyebrow vertical stretch. Got ${this.eyebrowVerticalStretch}, expected 0-6`,
); );
assert.ok( assert.ok(Util.inRange(this.eyebrowRotation, Util.range(12)), `Invalid Mii eyebrow rotation. Got ${this.eyebrowRotation}, expected 0-11`);
Util.inRange(this.eyebrowRotation, Util.range(12)), assert.ok(Util.inRange(this.eyebrowSpacing, Util.range(13)), `Invalid Mii eyebrow spacing. Got ${this.eyebrowSpacing}, expected 0-12`);
`Invalid Mii eyebrow rotation. Got ${this.eyebrowRotation}, expected 0-11`, assert.ok(Util.inRange(this.eyebrowYPosition, Util.range(3, 19)), `Invalid Mii eyebrow Y position. Got ${this.eyebrowYPosition}, expected 3-18`);
); assert.ok(Util.inRange(this.noseType, Util.range(18)), `Invalid Mii nose type. Got ${this.noseType}, expected 0-17`);
assert.ok( assert.ok(Util.inRange(this.noseScale, Util.range(9)), `Invalid Mii nose scale. Got ${this.noseScale}, expected 0-8`);
Util.inRange(this.eyebrowSpacing, Util.range(13)), assert.ok(Util.inRange(this.noseYPosition, Util.range(19)), `Invalid Mii nose Y position. Got ${this.noseYPosition}, expected 0-18`);
`Invalid Mii eyebrow spacing. Got ${this.eyebrowSpacing}, expected 0-12`, assert.ok(Util.inRange(this.mouthType, Util.range(36)), `Invalid Mii mouth type. Got ${this.mouthType}, expected 0-35`);
); assert.ok(Util.inRange(this.mouthColor, Util.range(5)), `Invalid Mii mouth color. Got ${this.mouthColor}, expected 0-4`);
assert.ok( assert.ok(Util.inRange(this.mouthScale, Util.range(9)), `Invalid Mii mouth scale. Got ${this.mouthScale}, expected 0-8`);
Util.inRange(this.eyebrowYPosition, Util.range(3, 19)), assert.ok(Util.inRange(this.mouthHorizontalStretch, Util.range(7)), `Invalid Mii mouth stretch. Got ${this.mouthHorizontalStretch}, expected 0-6`);
`Invalid Mii eyebrow Y position. Got ${this.eyebrowYPosition}, expected 3-18`, assert.ok(Util.inRange(this.mouthYPosition, Util.range(19)), `Invalid Mii mouth Y position. Got ${this.mouthYPosition}, expected 0-18`);
); assert.ok(Util.inRange(this.mustacheType, Util.range(6)), `Invalid Mii mustache type. Got ${this.mustacheType}, expected 0-5`);
assert.ok( assert.ok(Util.inRange(this.beardType, Util.range(6)), `Invalid Mii beard type. Got ${this.beardType}, expected 0-5`);
Util.inRange(this.noseType, Util.range(18)),
`Invalid Mii nose type. Got ${this.noseType}, expected 0-17`,
);
assert.ok(
Util.inRange(this.noseScale, Util.range(9)),
`Invalid Mii nose scale. Got ${this.noseScale}, expected 0-8`,
);
assert.ok(
Util.inRange(this.noseYPosition, Util.range(19)),
`Invalid Mii nose Y position. Got ${this.noseYPosition}, expected 0-18`,
);
assert.ok(
Util.inRange(this.mouthType, Util.range(36)),
`Invalid Mii mouth type. Got ${this.mouthType}, expected 0-35`,
);
assert.ok(
Util.inRange(this.mouthColor, Util.range(5)),
`Invalid Mii mouth color. Got ${this.mouthColor}, expected 0-4`,
);
assert.ok(
Util.inRange(this.mouthScale, Util.range(9)),
`Invalid Mii mouth scale. Got ${this.mouthScale}, expected 0-8`,
);
assert.ok(
Util.inRange(this.mouthHorizontalStretch, Util.range(7)),
`Invalid Mii mouth stretch. Got ${this.mouthHorizontalStretch}, expected 0-6`,
);
assert.ok(
Util.inRange(this.mouthYPosition, Util.range(19)),
`Invalid Mii mouth Y position. Got ${this.mouthYPosition}, expected 0-18`,
);
assert.ok(
Util.inRange(this.mustacheType, Util.range(6)),
`Invalid Mii mustache type. Got ${this.mustacheType}, expected 0-5`,
);
assert.ok(
Util.inRange(this.beardType, Util.range(6)),
`Invalid Mii beard type. Got ${this.beardType}, expected 0-5`,
);
// assert.ok(Util.inRange(this.facialHairColor, Util.range(8)), `Invalid Mii beard type. Got ${this.facialHairColor}, expected 0-7`); // assert.ok(Util.inRange(this.facialHairColor, Util.range(8)), `Invalid Mii beard type. Got ${this.facialHairColor}, expected 0-7`);
assert.ok( assert.ok(Util.inRange(this.mustacheScale, Util.range(9)), `Invalid Mii mustache scale. Got ${this.mustacheScale}, expected 0-8`);
Util.inRange(this.mustacheScale, Util.range(9)), assert.ok(Util.inRange(this.mustacheYPosition, Util.range(17)), `Invalid Mii mustache Y position. Got ${this.mustacheYPosition}, expected 0-16`);
`Invalid Mii mustache scale. Got ${this.mustacheScale}, expected 0-8`, assert.ok(Util.inRange(this.glassesType, Util.range(9)), `Invalid Mii glassess type. Got ${this.glassesType}, expected 0-8`);
); assert.ok(Util.inRange(this.glassesColor, Util.range(6)), `Invalid Mii glassess type. Got ${this.glassesColor}, expected 0-5`);
assert.ok( assert.ok(Util.inRange(this.glassesScale, Util.range(8)), `Invalid Mii glassess type. Got ${this.glassesScale}, expected 0-7`);
Util.inRange(this.mustacheYPosition, Util.range(17)), assert.ok(Util.inRange(this.glassesYPosition, Util.range(21)), `Invalid Mii glassess Y position. Got ${this.glassesYPosition}, expected 0-20`);
`Invalid Mii mustache Y position. Got ${this.mustacheYPosition}, expected 0-16`, assert.equal(typeof this.moleEnabled, "boolean", `Invalid mole enabled flag. Got ${this.moleEnabled}, expected true or false`);
); assert.ok(Util.inRange(this.moleScale, Util.range(9)), `Invalid Mii mole scale. Got ${this.moleScale}, expected 0-8`);
assert.ok( assert.ok(Util.inRange(this.moleXPosition, Util.range(17)), `Invalid Mii mole X position. Got ${this.moleXPosition}, expected 0-16`);
Util.inRange(this.glassesType, Util.range(9)), assert.ok(Util.inRange(this.moleYPosition, Util.range(31)), `Invalid Mii mole Y position. Got ${this.moleYPosition}, expected 0-30`);
`Invalid Mii glassess type. Got ${this.glassesType}, expected 0-8`,
);
assert.ok(
Util.inRange(this.glassesColor, Util.range(6)),
`Invalid Mii glassess type. Got ${this.glassesColor}, expected 0-5`,
);
assert.ok(
Util.inRange(this.glassesScale, Util.range(8)),
`Invalid Mii glassess type. Got ${this.glassesScale}, expected 0-7`,
);
assert.ok(
Util.inRange(this.glassesYPosition, Util.range(21)),
`Invalid Mii glassess Y position. Got ${this.glassesYPosition}, expected 0-20`,
);
assert.equal(
typeof this.moleEnabled,
"boolean",
`Invalid mole enabled flag. Got ${this.moleEnabled}, expected true or false`,
);
assert.ok(
Util.inRange(this.moleScale, Util.range(9)),
`Invalid Mii mole scale. Got ${this.moleScale}, expected 0-8`,
);
assert.ok(
Util.inRange(this.moleXPosition, Util.range(17)),
`Invalid Mii mole X position. Got ${this.moleXPosition}, expected 0-16`,
);
assert.ok(
Util.inRange(this.moleYPosition, Util.range(31)),
`Invalid Mii mole Y position. Got ${this.moleYPosition}, expected 0-30`,
);
// Sanity checks // Sanity checks
/* /*
@ -459,10 +246,7 @@ export default class Mii {
} }
*/ */
if ( if (this.nonUserMii && (this.creationTime !== 0 || this.isValid || this.dsMii || this.normalMii)) {
this.nonUserMii &&
(this.creationTime !== 0 || this.isValid || this.dsMii || this.normalMii)
) {
assert.fail("Non-user Mii's must have all other Mii ID bits set to 0"); assert.fail("Non-user Mii's must have all other Mii ID bits set to 0");
} }
@ -569,11 +353,7 @@ export default class Mii {
public calculateCRC(): number { public calculateCRC(): number {
// #view is inaccessible // #view is inaccessible
const data = new Uint8Array( const data = new Uint8Array(this.buffer.buffer, this.buffer.byteOffset, this.buffer.length).subarray(0, 0x5e);
this.buffer.buffer,
this.buffer.byteOffset,
this.buffer.length,
).subarray(0, 0x5e);
let crc = 0x0000; let crc = 0x0000;
@ -727,23 +507,11 @@ export default class Mii {
data: this.encodeStudio().toString("hex"), data: this.encodeStudio().toString("hex"),
}; };
params.type = STUDIO_RENDER_TYPES.includes(params.type as string) params.type = STUDIO_RENDER_TYPES.includes(params.type as string) ? params.type : STUDIO_RENDER_DEFAULTS.type;
? params.type params.expression = STUDIO_RENDER_EXPRESSIONS.includes(params.expression as string) ? params.expression : STUDIO_RENDER_DEFAULTS.expression;
: STUDIO_RENDER_DEFAULTS.type;
params.expression = STUDIO_RENDER_EXPRESSIONS.includes(
params.expression as string,
)
? params.expression
: STUDIO_RENDER_DEFAULTS.expression;
params.width = Util.clamp(params.width, 512); params.width = Util.clamp(params.width, 512);
params.bgColor = STUDIO_BG_COLOR_REGEX.test(params.bgColor as string) params.bgColor = STUDIO_BG_COLOR_REGEX.test(params.bgColor as string) ? params.bgColor : STUDIO_RENDER_DEFAULTS.bgColor;
? params.bgColor params.clothesColor = STUDIO_RENDER_CLOTHES_COLORS.includes(params.clothesColor) ? params.clothesColor : STUDIO_RENDER_DEFAULTS.clothesColor;
: STUDIO_RENDER_DEFAULTS.bgColor;
params.clothesColor = STUDIO_RENDER_CLOTHES_COLORS.includes(
params.clothesColor,
)
? params.clothesColor
: STUDIO_RENDER_DEFAULTS.clothesColor;
params.cameraXRotate = Util.clamp(params.cameraXRotate, 359); params.cameraXRotate = Util.clamp(params.cameraXRotate, 359);
params.cameraYRotate = Util.clamp(params.cameraYRotate, 359); params.cameraYRotate = Util.clamp(params.cameraYRotate, 359);
params.cameraZRotate = Util.clamp(params.cameraZRotate, 359); params.cameraZRotate = Util.clamp(params.cameraZRotate, 359);
@ -753,25 +521,16 @@ export default class Mii {
params.lightXDirection = Util.clamp(params.lightXDirection, 359); params.lightXDirection = Util.clamp(params.lightXDirection, 359);
params.lightYDirection = Util.clamp(params.lightYDirection, 359); params.lightYDirection = Util.clamp(params.lightYDirection, 359);
params.lightZDirection = Util.clamp(params.lightZDirection, 359); params.lightZDirection = Util.clamp(params.lightZDirection, 359);
params.lightDirectionMode = STUDIO_RENDER_LIGHT_DIRECTION_MODS.includes( params.lightDirectionMode = STUDIO_RENDER_LIGHT_DIRECTION_MODS.includes(params.lightDirectionMode)
params.lightDirectionMode,
)
? params.lightDirectionMode ? params.lightDirectionMode
: STUDIO_RENDER_DEFAULTS.lightDirectionMode; : STUDIO_RENDER_DEFAULTS.lightDirectionMode;
params.instanceCount = Util.clamp(params.instanceCount, 1, 16); params.instanceCount = Util.clamp(params.instanceCount, 1, 16);
params.instanceRotationMode = params.instanceRotationMode = STUDIO_RENDER_INSTANCE_ROTATION_MODES.includes(params.instanceRotationMode)
STUDIO_RENDER_INSTANCE_ROTATION_MODES.includes( ? params.instanceRotationMode
params.instanceRotationMode, : STUDIO_RENDER_DEFAULTS.instanceRotationMode;
)
? params.instanceRotationMode
: STUDIO_RENDER_DEFAULTS.instanceRotationMode;
// converts non-string params to strings // converts non-string params to strings
const query = new URLSearchParams( const query = new URLSearchParams(Object.fromEntries(Object.entries(params).map(([key, value]) => [key, value.toString()])));
Object.fromEntries(
Object.entries(params).map(([key, value]) => [key, value.toString()]),
),
);
if (params.lightDirectionMode === "none") { if (params.lightDirectionMode === "none") {
query.delete("lightDirectionMode"); query.delete("lightDirectionMode");

View file

@ -7,9 +7,7 @@ sjcl.beware["CTR mode is dangerous because it doesn't protect message integrity.
// Converts hair dye to studio color // Converts hair dye to studio color
// Reference: https://github.com/ariankordi/nwf-mii-cemu-toy/blob/9906440c1dafbe3f40ac8b95e10a22ebd85b441e/assets/data-conversion.js#L282 // Reference: https://github.com/ariankordi/nwf-mii-cemu-toy/blob/9906440c1dafbe3f40ac8b95e10a22ebd85b441e/assets/data-conversion.js#L282
// (Credits to kat21) // (Credits to kat21)
const hairDyeConverter = [ const hairDyeConverter = [55, 51, 50, 12, 16, 12, 67, 61, 51, 64, 69, 66, 65, 86, 85, 93, 92, 19, 20, 20, 15, 32, 35, 26, 38, 41, 43, 18, 95, 97, 97, 99];
55, 51, 50, 12, 16, 12, 67, 61, 51, 64, 69, 66, 65, 86, 85, 93, 92, 19, 20, 20, 15, 32, 35, 26, 38, 41, 43, 18, 95, 97, 97, 99,
];
// All possible values for 2-bit hair dye mode. // All possible values for 2-bit hair dye mode.
export enum HairDyeMode { export enum HairDyeMode {