mirror of
https://github.com/trafficlunar/tomodachi-share.git
synced 2026-05-13 21:27:46 +00:00
Compare commits
2 commits
1ec0b73712
...
5b498430c8
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b498430c8 | |||
| 79e6adff64 |
13 changed files with 967 additions and 1009 deletions
18
package.json
18
package.json
|
|
@ -16,20 +16,20 @@
|
||||||
"@bprogress/next": "^3.2.12",
|
"@bprogress/next": "^3.2.12",
|
||||||
"@hello-pangea/dnd": "^18.0.1",
|
"@hello-pangea/dnd": "^18.0.1",
|
||||||
"@prisma/client": "^6.19.2",
|
"@prisma/client": "^6.19.2",
|
||||||
"@sentry/nextjs": "^10.46.0",
|
"@sentry/nextjs": "^10.48.0",
|
||||||
"bit-buffer": "^0.3.0",
|
"bit-buffer": "^0.3.0",
|
||||||
"canvas-confetti": "^1.9.4",
|
"canvas-confetti": "^1.9.4",
|
||||||
"charinfo-ex": "^0.0.2",
|
"charinfo-ex": "^0.0.5",
|
||||||
"dayjs": "^1.11.20",
|
"dayjs": "^1.11.20",
|
||||||
"downshift": "^9.3.2",
|
"downshift": "^9.3.2",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"file-type": "^22.0.0",
|
"file-type": "^22.0.1",
|
||||||
"jsqr": "^1.4.0",
|
"jsqr": "^1.4.0",
|
||||||
"next": "16.2.1",
|
"next": "16.2.3",
|
||||||
"next-auth": "5.0.0-beta.30",
|
"next-auth": "5.0.0-beta.30",
|
||||||
"qrcode-generator": "^2.0.4",
|
"qrcode-generator": "^2.0.4",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.5",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.5",
|
||||||
"react-dropzone": "^15.0.0",
|
"react-dropzone": "^15.0.0",
|
||||||
"react-image-crop": "^11.0.10",
|
"react-image-crop": "^11.0.10",
|
||||||
"redis": "^5.11.0",
|
"redis": "^5.11.0",
|
||||||
|
|
@ -45,13 +45,13 @@
|
||||||
"@iconify/react": "^6.0.2",
|
"@iconify/react": "^6.0.2",
|
||||||
"@tailwindcss/postcss": "^4.2.2",
|
"@tailwindcss/postcss": "^4.2.2",
|
||||||
"@types/canvas-confetti": "^1.9.0",
|
"@types/canvas-confetti": "^1.9.0",
|
||||||
"@types/node": "^25.5.0",
|
"@types/node": "^25.6.0",
|
||||||
"@types/react": "^19.2.14",
|
"@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": "^10.1.0",
|
"eslint": "^10.2.0",
|
||||||
"eslint-config-next": "16.2.1",
|
"eslint-config-next": "16.2.3",
|
||||||
"prisma": "^6.19.2",
|
"prisma": "^6.19.2",
|
||||||
"schema-dts": "^2.0.0",
|
"schema-dts": "^2.0.0",
|
||||||
"tailwindcss": "^4.2.2",
|
"tailwindcss": "^4.2.2",
|
||||||
|
|
|
||||||
1535
pnpm-lock.yaml
1535
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -1,2 +1,2 @@
|
||||||
-- AlterTable
|
-- AlterTable
|
||||||
ALTER TABLE "miis" ADD COLUMN "miiData" JSONB;
|
ALTER TABLE "miis" ADD COLUMN "miiData" BYTEA;
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ model Mii {
|
||||||
gender MiiGender?
|
gender MiiGender?
|
||||||
makeup MiiMakeup?
|
makeup MiiMakeup?
|
||||||
|
|
||||||
miiData Json?
|
miiData Bytes?
|
||||||
|
|
||||||
firstName String?
|
firstName String?
|
||||||
lastName String?
|
lastName String?
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,10 @@ const submitSchema = z.object({
|
||||||
way: z.enum(["savedata", "manual"]).optional(),
|
way: z.enum(["savedata", "manual"]).optional(),
|
||||||
|
|
||||||
// Save data way
|
// Save data way
|
||||||
// TODO: miiData
|
miiDataFile: z
|
||||||
|
.instanceof(File)
|
||||||
|
.refine((blob) => blob.size < 1024 * 30, "File too large") // TODO: actual size
|
||||||
|
.optional(),
|
||||||
|
|
||||||
// Manual way
|
// Manual way
|
||||||
miiFeaturesImage: z.union([z.instanceof(File), z.any()]).optional(),
|
miiFeaturesImage: z.union([z.instanceof(File), z.any()]).optional(),
|
||||||
|
|
@ -109,6 +112,7 @@ export async function POST(request: NextRequest) {
|
||||||
youtubeId: formData.get("youtubeId"),
|
youtubeId: formData.get("youtubeId"),
|
||||||
|
|
||||||
way: formData.get("way"),
|
way: formData.get("way"),
|
||||||
|
miiDataFile: formData.get("miiDataFile"),
|
||||||
|
|
||||||
miiFeaturesImage: formData.get("miiFeaturesImage"),
|
miiFeaturesImage: formData.get("miiFeaturesImage"),
|
||||||
instructions: minifiedInstructions,
|
instructions: minifiedInstructions,
|
||||||
|
|
@ -146,6 +150,7 @@ export async function POST(request: NextRequest) {
|
||||||
miiPortraitImage,
|
miiPortraitImage,
|
||||||
miiFeaturesImage,
|
miiFeaturesImage,
|
||||||
way,
|
way,
|
||||||
|
miiDataFile,
|
||||||
youtubeId,
|
youtubeId,
|
||||||
image1,
|
image1,
|
||||||
image2,
|
image2,
|
||||||
|
|
@ -197,14 +202,13 @@ export async function POST(request: NextRequest) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const miiData: CharInfoEx | undefined =
|
const miiDataFileBuffer = miiDataFile ? await miiDataFile.arrayBuffer() : undefined;
|
||||||
way === "savedata" && formData.get("miiData") ? (JSON.parse(formData.get("miiData") as string) as CharInfoEx) : undefined;
|
const miiDataFileArray = miiDataFileBuffer ? new Uint8Array(miiDataFileBuffer) : undefined;
|
||||||
|
const miiData = miiDataFileBuffer ? CharInfoEx.FromShareMiiFileArrayBuffer(miiDataFileBuffer) : undefined;
|
||||||
|
|
||||||
if (way === "savedata" && !miiData) {
|
if (way === "savedata") {
|
||||||
return rateLimit.sendResponse({ error: "No mii data provided" }, 400);
|
if (!miiData) return rateLimit.sendResponse({ error: "No mii data provided" }, 400);
|
||||||
}
|
|
||||||
|
|
||||||
if (way === "savedata" && miiData) {
|
|
||||||
const instructions: Partial<SwitchMiiInstructions> = {
|
const instructions: Partial<SwitchMiiInstructions> = {
|
||||||
head: {
|
head: {
|
||||||
type: miiData.facelineType,
|
type: miiData.facelineType,
|
||||||
|
|
@ -224,21 +228,21 @@ export async function POST(request: NextRequest) {
|
||||||
eyebrows: {
|
eyebrows: {
|
||||||
type: miiData.eyebrowType,
|
type: miiData.eyebrowType,
|
||||||
color: miiData.eyebrowColor,
|
color: miiData.eyebrowColor,
|
||||||
height: miiData.eyebrowY,
|
height: miiData.eyebrowY - 10,
|
||||||
distance: miiData.eyebrowX,
|
distance: miiData.eyebrowX - 4,
|
||||||
rotation: miiData.eyebrowRotate,
|
rotation: miiData.eyebrowRotate - 6,
|
||||||
size: miiData.eyebrowScale,
|
size: miiData.eyebrowScale - 4,
|
||||||
stretch: miiData.eyebrowAspect,
|
stretch: miiData.eyebrowAspect - 3,
|
||||||
},
|
},
|
||||||
eyes: {
|
eyes: {
|
||||||
main: {
|
main: {
|
||||||
type: miiData.eyeType,
|
type: miiData.eyeType,
|
||||||
color: miiData.eyeColor,
|
color: miiData.eyeColor,
|
||||||
height: miiData.eyeY,
|
height: miiData.eyeY - 12,
|
||||||
distance: miiData.eyeX,
|
distance: miiData.eyeX - 2,
|
||||||
rotation: miiData.eyeRotate,
|
rotation: miiData.eyeRotate - 4,
|
||||||
size: miiData.eyeScale,
|
size: miiData.eyeScale - 4,
|
||||||
stretch: miiData.eyeAspect,
|
stretch: miiData.eyeAspect - 3,
|
||||||
},
|
},
|
||||||
eyelashesTop: {
|
eyelashesTop: {
|
||||||
type: miiData.eyelashUpperType,
|
type: miiData.eyelashUpperType,
|
||||||
|
|
@ -257,12 +261,12 @@ export async function POST(request: NextRequest) {
|
||||||
stretch: miiData.eyelashLowerAspect,
|
stretch: miiData.eyelashLowerAspect,
|
||||||
},
|
},
|
||||||
eyelidTop: {
|
eyelidTop: {
|
||||||
type: miiData.eyeLidUpperType,
|
type: miiData.eyelidUpperType,
|
||||||
height: miiData.eyeLidUpperY,
|
height: miiData.eyelidUpperY,
|
||||||
distance: miiData.eyeLidUpperX,
|
distance: miiData.eyelidUpperX,
|
||||||
rotation: miiData.eyeLidUpperRotate,
|
rotation: miiData.eyelidUpperRotate,
|
||||||
size: miiData.eyeLidUpperScale,
|
size: miiData.eyelidUpperScale,
|
||||||
stretch: miiData.eyeLidUpperAspect,
|
stretch: miiData.eyelidUpperAspect,
|
||||||
},
|
},
|
||||||
eyelidBottom: {
|
eyelidBottom: {
|
||||||
type: miiData.eyelidLowerType,
|
type: miiData.eyelidLowerType,
|
||||||
|
|
@ -287,47 +291,47 @@ export async function POST(request: NextRequest) {
|
||||||
},
|
},
|
||||||
nose: {
|
nose: {
|
||||||
type: miiData.noseType,
|
type: miiData.noseType,
|
||||||
height: miiData.noseY,
|
height: miiData.noseY - 9,
|
||||||
size: miiData.noseScale,
|
size: miiData.noseScale - 4,
|
||||||
},
|
},
|
||||||
lips: {
|
lips: {
|
||||||
type: miiData.mouthType,
|
type: miiData.mouthType,
|
||||||
color: miiData.mouthColor,
|
color: miiData.mouthColor,
|
||||||
height: miiData.mouthY,
|
height: miiData.mouthY - 13,
|
||||||
rotation: miiData.mouthRotate,
|
rotation: miiData.mouthRotate,
|
||||||
size: miiData.mouthScale,
|
size: miiData.mouthScale - 4,
|
||||||
stretch: miiData.mouthAspect,
|
stretch: miiData.mouthAspect - 3,
|
||||||
// uh oh, no lipstick
|
// uh oh, no lipstick
|
||||||
hasLipstick: false,
|
hasLipstick: false,
|
||||||
},
|
},
|
||||||
ears: {
|
ears: {
|
||||||
type: miiData.earType,
|
type: miiData.earType,
|
||||||
height: miiData.earY,
|
height: miiData.earY - 4,
|
||||||
size: miiData.earScale,
|
size: miiData.earScale - 2,
|
||||||
},
|
},
|
||||||
glasses: {
|
glasses: {
|
||||||
type: miiData.glassType1,
|
type: miiData.glassType1,
|
||||||
type2: miiData.glassType2,
|
type2: miiData.glassType2,
|
||||||
ringColor: miiData.glassColor1,
|
ringColor: miiData.glassColor1,
|
||||||
shadesColor: miiData.glassColor2,
|
shadesColor: miiData.glassColor2,
|
||||||
height: miiData.glassY,
|
height: miiData.glassY - 11,
|
||||||
size: miiData.glassScale,
|
size: miiData.glassScale - 4,
|
||||||
stretch: miiData.glassAspect,
|
stretch: miiData.glassAspect - 3,
|
||||||
},
|
},
|
||||||
other: {
|
other: {
|
||||||
wrinkles1: {
|
wrinkles1: {
|
||||||
type: miiData.wrinkleLower,
|
type: miiData.wrinkleLowerType,
|
||||||
height: miiData.wrinkleLowerY,
|
height: miiData.wrinkleLowerY - 15,
|
||||||
distance: miiData.wrinkleLowerX,
|
distance: miiData.wrinkleLowerX - 2,
|
||||||
size: miiData.wrinkleLowerScale,
|
size: miiData.wrinkleLowerScale - 6,
|
||||||
stretch: miiData.wrinkleLowerAspect,
|
stretch: miiData.wrinkleLowerAspect - 3,
|
||||||
},
|
},
|
||||||
wrinkles2: {
|
wrinkles2: {
|
||||||
type: miiData.wrinkleUpper,
|
type: miiData.wrinkleUpperType,
|
||||||
height: miiData.wrinkleUpperY,
|
height: miiData.wrinkleUpperY - 23,
|
||||||
distance: miiData.wrinkleUpperX,
|
distance: miiData.wrinkleUpperX - 7,
|
||||||
size: miiData.wrinkleUpperScale,
|
size: miiData.wrinkleUpperScale - 6,
|
||||||
stretch: miiData.wrinkleUpperAspect,
|
stretch: miiData.wrinkleUpperAspect - 3,
|
||||||
},
|
},
|
||||||
beard: {
|
beard: {
|
||||||
type: miiData.beardType,
|
type: miiData.beardType,
|
||||||
|
|
@ -336,11 +340,11 @@ export async function POST(request: NextRequest) {
|
||||||
moustache: {
|
moustache: {
|
||||||
type: miiData.mustacheType,
|
type: miiData.mustacheType,
|
||||||
color: miiData.mustacheColor,
|
color: miiData.mustacheColor,
|
||||||
height: miiData.mustacheY,
|
height: miiData.mustacheY - 10,
|
||||||
// uh oh, no flipped
|
// uh oh, no flipped
|
||||||
isFlipped: false,
|
isFlipped: false,
|
||||||
size: miiData.mustacheScale,
|
size: miiData.mustacheScale - 4,
|
||||||
stretch: miiData.mustacheAspect,
|
stretch: miiData.mustacheAspect - 3,
|
||||||
},
|
},
|
||||||
goatee: {
|
goatee: {
|
||||||
type: miiData.beardShortType,
|
type: miiData.beardShortType,
|
||||||
|
|
@ -348,25 +352,25 @@ export async function POST(request: NextRequest) {
|
||||||
},
|
},
|
||||||
mole: {
|
mole: {
|
||||||
type: miiData.moleX != 0,
|
type: miiData.moleX != 0,
|
||||||
height: miiData.moleY,
|
height: miiData.moleY - 20,
|
||||||
distance: miiData.moleX,
|
distance: miiData.moleX - 2,
|
||||||
size: miiData.moleScale,
|
size: miiData.moleScale - 4,
|
||||||
},
|
},
|
||||||
eyeShadow: {
|
eyeShadow: {
|
||||||
type: miiData.makeup0,
|
type: miiData.makeup0,
|
||||||
color: miiData.makeup0Color,
|
color: miiData.makeup0Color,
|
||||||
height: miiData.makeup0Y,
|
height: miiData.makeup0Y - 12,
|
||||||
distance: miiData.makeup0X,
|
distance: miiData.makeup0X - 1,
|
||||||
size: miiData.makeup0Scale,
|
size: miiData.makeup0Scale - 6,
|
||||||
stretch: miiData.makeup0Aspect,
|
stretch: miiData.makeup0Aspect - 3,
|
||||||
},
|
},
|
||||||
blush: {
|
blush: {
|
||||||
type: miiData.makeup1,
|
type: miiData.makeup1,
|
||||||
color: miiData.makeup1Color,
|
color: miiData.makeup1Color,
|
||||||
height: miiData.makeup1Y,
|
height: miiData.makeup1Y - 19,
|
||||||
distance: miiData.makeup1X,
|
distance: miiData.makeup1X - 6,
|
||||||
size: miiData.makeup1Scale,
|
size: miiData.makeup1Scale - 5,
|
||||||
stretch: miiData.makeup1Aspect,
|
stretch: miiData.makeup1Aspect - 3,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
height: miiData.height,
|
height: miiData.height,
|
||||||
|
|
@ -401,7 +405,7 @@ export async function POST(request: NextRequest) {
|
||||||
youtubeId,
|
youtubeId,
|
||||||
makeup: makeup ?? "PARTIAL",
|
makeup: makeup ?? "PARTIAL",
|
||||||
instructions: minifiedInstructions,
|
instructions: minifiedInstructions,
|
||||||
...(way === "savedata" && { miiData: miiData?.toJson() }),
|
...(way === "savedata" && { miiData: miiDataFileArray }),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -385,10 +385,14 @@ export default async function MiiPage({ params }: Props) {
|
||||||
{/* Instructions */}
|
{/* Instructions */}
|
||||||
{mii.platform === "SWITCH" && (
|
{mii.platform === "SWITCH" && (
|
||||||
<div className="bg-amber-50 border-2 border-amber-500 rounded-2xl shadow-lg p-4 flex flex-col gap-3 max-h-96 overflow-y-auto">
|
<div className="bg-amber-50 border-2 border-amber-500 rounded-2xl shadow-lg p-4 flex flex-col gap-3 max-h-96 overflow-y-auto">
|
||||||
<h2 className="text-xl font-semibold text-amber-700 flex items-center gap-2">
|
<div>
|
||||||
<Icon icon="fa7-solid:list" />
|
<h2 className="text-xl font-semibold text-amber-700 flex items-center gap-2">
|
||||||
Instructions
|
<Icon icon="fa7-solid:list" />
|
||||||
</h2>
|
Instructions
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p className="text-xs text-amber-800">All instructions are based off of the default Male Mii.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
{mii.youtubeId && (
|
{mii.youtubeId && (
|
||||||
<iframe
|
<iframe
|
||||||
|
|
@ -402,7 +406,7 @@ export default async function MiiPage({ params }: Props) {
|
||||||
></iframe>
|
></iframe>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<MiiInstructions instructions={mii.instructions as Partial<SwitchMiiInstructions>} />
|
<MiiInstructions instructions={mii.instructions as Partial<SwitchMiiInstructions>} isUsingSaveFile={mii.miiData !== null} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ export default function Description({ text, className }: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p className={`text-sm mt-2 bg-white/50 p-3 rounded-lg border border-orange-200 whitespace-break-spaces max-h-54 overflow-y-auto ${className}`}>
|
<p className={`text-sm mt-2 bg-white/50 p-3 rounded-lg border border-orange-200 whitespace-break-spaces max-h-54 overflow-y-auto ${className}`}>
|
||||||
{parts.map(async (part, index) => {
|
{parts.map((part, index) => {
|
||||||
try {
|
try {
|
||||||
// Check if it's a URL
|
// Check if it's a URL
|
||||||
if (!urlRegex.test(part)) throw new Error("Not a URL");
|
if (!urlRegex.test(part)) throw new Error("Not a URL");
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ export default function Dropzone({ type = "image", onDrop, options, children }:
|
||||||
const { getRootProps, getInputProps } = useDropzone({
|
const { getRootProps, getInputProps } = useDropzone({
|
||||||
onDrop: handleDrop,
|
onDrop: handleDrop,
|
||||||
maxFiles: 3,
|
maxFiles: 3,
|
||||||
accept: type === "image" ? { "image/*": [".png", ".jpg", ".jpeg", ".bmp", ".png", ".heic"] } : { "application/octet-stream": [".sav"] },
|
accept: type === "image" ? { "image/*": [".png", ".jpg", ".jpeg", ".bmp", ".png", ".heic"] } : { "application/octet-stream": [".ltd"] },
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,14 @@ import React from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
import DatingPreferencesViewer from "./dating-preferences";
|
import DatingPreferencesViewer from "./dating-preferences";
|
||||||
import VoiceViewer from "./voice-viewer";
|
|
||||||
import PersonalityViewer from "./personality-viewer";
|
import PersonalityViewer from "./personality-viewer";
|
||||||
|
|
||||||
import { SwitchMiiInstructions } from "@/types";
|
import { SwitchMiiInstructions } from "@/types";
|
||||||
import { COLORS } from "@/lib/switch";
|
import { COLOR_MAP, COLORS } from "@/lib/switch";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
instructions: Partial<SwitchMiiInstructions>;
|
instructions: Partial<SwitchMiiInstructions>;
|
||||||
|
isUsingSaveFile: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SectionProps {
|
interface SectionProps {
|
||||||
|
|
@ -19,6 +19,7 @@ interface SectionProps {
|
||||||
pad?: number; // Number of digits to pad with zeroes
|
pad?: number; // Number of digits to pad with zeroes
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
isSubSection?: boolean;
|
isSubSection?: boolean;
|
||||||
|
isUsingSaveFile: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ORDINAL_SUFFIXES: Record<string, string> = {
|
const ORDINAL_SUFFIXES: Record<string, string> = {
|
||||||
|
|
@ -46,29 +47,35 @@ function GridPosition({ index, cols = 5 }: { index: number; cols?: number }) {
|
||||||
return `${row}${rowSuffix} row, ${col}${colSuffix} column`;
|
return `${row}${rowSuffix} row, ${col}${colSuffix} column`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ColorPosition({ color }: { color: number | undefined | null }) {
|
function ColorPosition({ color, isUsingSaveFile }: { color: number | undefined | null; isUsingSaveFile: boolean }) {
|
||||||
if (color === undefined || color === null) return null;
|
if (color === undefined || color === null) return null;
|
||||||
if (color <= 7) {
|
|
||||||
|
const index = isUsingSaveFile ? COLOR_MAP[color] : color;
|
||||||
|
if (index === undefined) return null;
|
||||||
|
|
||||||
|
console.log(index, color, COLORS[color]);
|
||||||
|
|
||||||
|
if (index <= 7) {
|
||||||
return (
|
return (
|
||||||
<span className="flex items-center">
|
<span className="flex items-center">
|
||||||
<div className="size-5 rounded mr-1.5 shrink-0" style={{ backgroundColor: `#${COLORS[color]}` }}></div>
|
<div className="size-5 rounded mr-1.5 shrink-0" style={{ backgroundColor: `#${COLORS[index]}` }}></div>
|
||||||
Color menu on left, <GridPosition index={color} cols={1} />
|
Color menu on left, <GridPosition index={index} cols={1} />
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (color >= 108) {
|
if (index >= 108) {
|
||||||
return (
|
return (
|
||||||
<span className="flex items-center">
|
<span className="flex items-center">
|
||||||
<div className="size-5 rounded mr-1.5 shrink-0" style={{ backgroundColor: `#${COLORS[color]}` }}></div>
|
<div className="size-5 rounded mr-1.5 shrink-0" style={{ backgroundColor: `#${COLORS[index]}` }}></div>
|
||||||
Outside color menu, <GridPosition index={color - 108} cols={2} />
|
Outside color menu, <GridPosition index={index - 108} cols={2} />
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="flex items-center">
|
<span className="flex items-center">
|
||||||
<div className="size-5 rounded mr-1.5 shrink-0" style={{ backgroundColor: `#${COLORS[color]}` }}></div>
|
<div className="size-5 rounded mr-1.5 shrink-0" style={{ backgroundColor: `#${COLORS[index]}` }}></div>
|
||||||
Color menu on right, <GridPosition index={color - 8} cols={10} />
|
Color menu on right, <GridPosition index={index - 8} cols={10} />
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -87,7 +94,7 @@ function TableCell({ label, children }: TableCellProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Section({ name, iconTemplate, pad = 2, instructions, children, isSubSection }: SectionProps) {
|
function Section({ name, iconTemplate, pad = 2, instructions, children, isSubSection, isUsingSaveFile }: SectionProps) {
|
||||||
if (typeof instructions !== "object" || !instructions) return null;
|
if (typeof instructions !== "object" || !instructions) return null;
|
||||||
|
|
||||||
const type = "type" in instructions ? instructions.type : undefined;
|
const type = "type" in instructions ? instructions.type : undefined;
|
||||||
|
|
@ -102,7 +109,7 @@ function Section({ name, iconTemplate, pad = 2, instructions, children, isSubSec
|
||||||
const isBooleanType = typeof type === "boolean";
|
const isBooleanType = typeof type === "boolean";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`p-3 w-max ${isSubSection ? "not-first:mt-2 pt-0!" : "border-l-4 border-amber-400 bg-amber-100/50 rounded-r-lg py-2.5 mb-4"}`}>
|
<div className={`p-3 w-max ${isSubSection ? "not-first:mt-2 pt-0!" : "border-l-4 border-amber-400 bg-amber-100/50 rounded-r-lg py-2.5"}`}>
|
||||||
<h3 className={`font-semibold text-amber-800 mb-1 ${isSubSection ? "text-lg" : "text-xl"}`}>{name}</h3>
|
<h3 className={`font-semibold text-amber-800 mb-1 ${isSubSection ? "text-lg" : "text-xl"}`}>{name}</h3>
|
||||||
|
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
|
|
@ -117,7 +124,7 @@ function Section({ name, iconTemplate, pad = 2, instructions, children, isSubSec
|
||||||
|
|
||||||
{not(color) && (
|
{not(color) && (
|
||||||
<TableCell label="Color">
|
<TableCell label="Color">
|
||||||
<ColorPosition color={color} />
|
<ColorPosition color={color} isUsingSaveFile={isUsingSaveFile} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
{not(height) && <TableCell label="Height">{numberValue(height, 0)}</TableCell>}
|
{not(height) && <TableCell label="Height">{numberValue(height, 0)}</TableCell>}
|
||||||
|
|
@ -133,27 +140,27 @@ function Section({ name, iconTemplate, pad = 2, instructions, children, isSubSec
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MiiInstructions({ instructions }: Props) {
|
export default function MiiInstructions({ instructions, isUsingSaveFile }: Props) {
|
||||||
if (Object.keys(instructions).length === 0) return null;
|
if (Object.keys(instructions).length === 0) return null;
|
||||||
const { head, hair, eyebrows, eyes, nose, lips, ears, glasses, other, height, weight, birthday, datingPreferences, voice, personality } = instructions;
|
const { head, hair, eyebrows, eyes, nose, lips, ears, glasses, other, height, weight, birthday, datingPreferences, voice, personality } = instructions;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{head && (
|
{head && (
|
||||||
<Section name="Head" iconTemplate="Faceline%s_Uit" instructions={head}>
|
<Section name="Head" iconTemplate="Faceline%s_Uit" instructions={head} isUsingSaveFile={isUsingSaveFile}>
|
||||||
{not(head.skinColor) && (
|
{not(head.skinColor) && (
|
||||||
<TableCell label="Skin Color">
|
<TableCell label="Skin Color">
|
||||||
<ColorPosition color={head.skinColor} />
|
<ColorPosition color={head.skinColor} isUsingSaveFile={isUsingSaveFile} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
</Section>
|
</Section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{hair && (
|
{hair && (
|
||||||
<Section name="Hair" iconTemplate="HairAll%s_Uit" pad={3} instructions={hair}>
|
<Section name="Hair" iconTemplate="HairAll%s_Uit" pad={3} instructions={hair} isUsingSaveFile={isUsingSaveFile}>
|
||||||
{not(hair.set) && (
|
{not(hair.set) && (
|
||||||
<TableCell label="Set">
|
<TableCell label="Set">
|
||||||
<Image src={`/icons/MiiEditor_Face_HairAll${String(hair.set).padStart(3, "0")}_Uit.png`} width={64} height={64} alt="icon" />
|
<Image src={`/icons/MiiEditor_Face_Hair${String(hair.set).padStart(3, "0")}_Uit.png`} width={64} height={64} alt="icon" />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
{not(hair.bangs) && (
|
{not(hair.bangs) && (
|
||||||
|
|
@ -168,12 +175,12 @@ export default function MiiInstructions({ instructions }: Props) {
|
||||||
)}
|
)}
|
||||||
{not(hair.subColor) && (
|
{not(hair.subColor) && (
|
||||||
<TableCell label="Sub Color">
|
<TableCell label="Sub Color">
|
||||||
<ColorPosition color={hair.subColor} />
|
<ColorPosition color={hair.subColor} isUsingSaveFile={isUsingSaveFile} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
{not(hair.subColor2) && (
|
{not(hair.subColor2) && (
|
||||||
<TableCell label="Sub Color (Back)">
|
<TableCell label="Sub Color (Back)">
|
||||||
<ColorPosition color={hair.subColor2} />
|
<ColorPosition color={hair.subColor2} isUsingSaveFile={isUsingSaveFile} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
{not(hair.style) && <TableCell label="Tying Style">{hair.style}</TableCell>}
|
{not(hair.style) && <TableCell label="Tying Style">{hair.style}</TableCell>}
|
||||||
|
|
@ -181,64 +188,64 @@ export default function MiiInstructions({ instructions }: Props) {
|
||||||
</Section>
|
</Section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{eyebrows && <Section name="Eyebrows" iconTemplate="Eyebrow%s_Uit" instructions={eyebrows}></Section>}
|
{eyebrows && <Section name="Eyebrows" iconTemplate="Eyebrow%s_Uit" instructions={eyebrows} isUsingSaveFile={isUsingSaveFile} />}
|
||||||
|
|
||||||
{eyes && (
|
{eyes && (
|
||||||
<Section name="Eyes" instructions={eyes}>
|
<Section name="Eyes" instructions={eyes} isUsingSaveFile={isUsingSaveFile}>
|
||||||
<Section isSubSection name="Main" iconTemplate="Eye%s_Uit" pad={3} instructions={eyes.main} />
|
<Section isSubSection name="Main" iconTemplate="Eye%s_Uit" pad={3} instructions={eyes.main} isUsingSaveFile={isUsingSaveFile} />
|
||||||
<Section isSubSection name="Eyelashes (Top)" iconTemplate="Eyelash%s_Uit" instructions={eyes.eyelashesTop} />
|
<Section isSubSection name="Eyelashes (Top)" iconTemplate="Eyelash%s_Uit" instructions={eyes.eyelashesTop} isUsingSaveFile={isUsingSaveFile} />
|
||||||
<Section isSubSection name="Eyelashes (Bottom)" iconTemplate="EyelashLower%s_Uit" instructions={eyes.eyelashesBottom} />
|
<Section
|
||||||
<Section isSubSection name="Eyelid (Top)" iconTemplate="EyelidUpper%s_Uit" instructions={eyes.eyelidTop} />
|
isSubSection
|
||||||
<Section isSubSection name="Eyelid (Bottom)" iconTemplate="EyelidLower%s_Uit" instructions={eyes.eyelidBottom} />
|
name="Eyelashes (Bottom)"
|
||||||
<Section isSubSection name="Eyeliner" instructions={eyes.eyeliner} />
|
iconTemplate="EyelashLower%s_Uit"
|
||||||
<Section isSubSection name="Pupil" iconTemplate="EyeHighlight%s_Uit" instructions={eyes.pupil} />
|
instructions={eyes.eyelashesBottom}
|
||||||
|
isUsingSaveFile={isUsingSaveFile}
|
||||||
|
/>
|
||||||
|
<Section isSubSection name="Eyelid (Top)" iconTemplate="EyelidUpper%s_Uit" instructions={eyes.eyelidTop} isUsingSaveFile={isUsingSaveFile} />
|
||||||
|
<Section isSubSection name="Eyelid (Bottom)" iconTemplate="EyelidLower%s_Uit" instructions={eyes.eyelidBottom} isUsingSaveFile={isUsingSaveFile} />
|
||||||
|
<Section isSubSection name="Eyeliner" instructions={eyes.eyeliner} isUsingSaveFile={isUsingSaveFile} />
|
||||||
|
<Section isSubSection name="Pupil" iconTemplate="EyeHighlight%s_Uit" instructions={eyes.pupil} isUsingSaveFile={isUsingSaveFile} />
|
||||||
</Section>
|
</Section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{nose && <Section name="Nose" iconTemplate="Nose%s_Uit" instructions={nose}></Section>}
|
{nose && <Section name="Nose" iconTemplate="Nose%s_Uit" instructions={nose} isUsingSaveFile={isUsingSaveFile} />}
|
||||||
|
|
||||||
{lips && (
|
{lips && (
|
||||||
<Section name="Lips" iconTemplate="Mouth%s_Uit" pad={3} instructions={lips}>
|
<Section name="Lips" iconTemplate="Mouth%s_Uit" pad={3} instructions={lips} isUsingSaveFile={isUsingSaveFile}>
|
||||||
{not(lips.hasLipstick) && <TableCell label="Lipstick">{lips.hasLipstick ? "Yes" : "No"}</TableCell>}
|
{not(lips.hasLipstick) && <TableCell label="Lipstick">{lips.hasLipstick ? "Yes" : "No"}</TableCell>}
|
||||||
</Section>
|
</Section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{ears && <Section name="Ears" iconTemplate="Ear%s_Uit" instructions={ears}></Section>}
|
{ears && <Section name="Ears" iconTemplate="Ear%s_Uit" instructions={ears} isUsingSaveFile={isUsingSaveFile} />}
|
||||||
|
|
||||||
{glasses && (
|
{glasses && (
|
||||||
<Section name="Glasses" iconTemplate="Glass%scolor_Uit" instructions={glasses}>
|
<Section name="Glasses" iconTemplate="Glass%scolor_Uit" instructions={glasses} isUsingSaveFile={isUsingSaveFile}>
|
||||||
{not(glasses.type2) && <TableCell label="Type 2">{glasses.type2}</TableCell>}
|
{not(glasses.type2) && <TableCell label="Type 2">{glasses.type2}</TableCell>}
|
||||||
{not(glasses.ringColor) && (
|
{not(glasses.ringColor) && (
|
||||||
<TableCell label="Ring Color">
|
<TableCell label="Ring Color">
|
||||||
<ColorPosition color={glasses.ringColor} />
|
<ColorPosition color={glasses.ringColor} isUsingSaveFile={isUsingSaveFile} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
{not(glasses.shadesColor) && (
|
{not(glasses.shadesColor) && (
|
||||||
<TableCell label="Shades Color">
|
<TableCell label="Shades Color">
|
||||||
<ColorPosition color={glasses.shadesColor} />
|
<ColorPosition color={glasses.shadesColor} isUsingSaveFile={isUsingSaveFile} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
</Section>
|
</Section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{other && (
|
{other && (
|
||||||
<Section name="Other" instructions={other}>
|
<Section name="Other" instructions={other} isUsingSaveFile={isUsingSaveFile}>
|
||||||
<Section isSubSection name="Wrinkles 1" iconTemplate="WrinkleLower%s_Uit" instructions={other.wrinkles1} />
|
<Section isSubSection name="Wrinkles 1" iconTemplate="WrinkleLower%s_Uit" instructions={other.wrinkles1} isUsingSaveFile={isUsingSaveFile} />
|
||||||
<Section isSubSection name="Wrinkles 2" iconTemplate="WrinkleUpper%s_Uit" instructions={other.wrinkles2} />
|
<Section isSubSection name="Wrinkles 2" iconTemplate="WrinkleUpper%s_Uit" instructions={other.wrinkles2} isUsingSaveFile={isUsingSaveFile} />
|
||||||
<Section isSubSection name="Beard" iconTemplate="Beard%s_Uit" instructions={other.beard} />
|
<Section isSubSection name="Beard" iconTemplate="Beard%s_Uit" instructions={other.beard} isUsingSaveFile={isUsingSaveFile} />
|
||||||
<Section isSubSection name="Moustache" iconTemplate="Mustache%s_Uit" instructions={other.moustache}>
|
<Section isSubSection name="Moustache" iconTemplate="Mustache%s_Uit" instructions={other.moustache} isUsingSaveFile={isUsingSaveFile}>
|
||||||
{other.moustache && other.moustache.isFlipped !== undefined && <TableCell label="Flipped">{other.moustache.isFlipped ? "Yes" : "No"}</TableCell>}
|
{other.moustache && other.moustache.isFlipped !== undefined && <TableCell label="Flipped">{other.moustache.isFlipped ? "Yes" : "No"}</TableCell>}
|
||||||
</Section>
|
</Section>
|
||||||
<Section isSubSection name="Goatee" iconTemplate="BeardShort%s_Uit" instructions={other.goatee} />
|
<Section isSubSection name="Goatee" iconTemplate="BeardShort%s_Uit" instructions={other.goatee} isUsingSaveFile={isUsingSaveFile} />
|
||||||
<Section isSubSection name="Mole" instructions={other.mole}>
|
<Section isSubSection name="Mole" instructions={other.mole} isUsingSaveFile={isUsingSaveFile} />
|
||||||
{other.mole?.type && (
|
<Section isSubSection name="Eye Shadow" iconTemplate="MakeUpper%s_Uit" instructions={other.eyeShadow} isUsingSaveFile={isUsingSaveFile} />
|
||||||
<TableCell label="Icon">
|
<Section isSubSection name="Blush" iconTemplate="MakeLower%s_Uit" instructions={other.blush} isUsingSaveFile={isUsingSaveFile} />
|
||||||
<Image src={`/icons/MiiEditor_Face_Mole00_Uit.png`} width={64} height={64} alt="icon" />
|
|
||||||
</TableCell>
|
|
||||||
)}
|
|
||||||
</Section>
|
|
||||||
<Section isSubSection name="Eye Shadow" iconTemplate="MakeUpper%s_Uit" instructions={other.eyeShadow} />
|
|
||||||
<Section isSubSection name="Blush" iconTemplate="MakeLower%s_Uit" instructions={other.blush} />
|
|
||||||
</Section>
|
</Section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { Icon } from "@iconify/react";
|
||||||
interface Props {
|
interface Props {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
image: string | undefined;
|
image: string | File | undefined;
|
||||||
setImage: (value: string | undefined) => void;
|
setImage: (value: string | undefined) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,9 +61,7 @@ export default function SubmitForm({ inQueueMiisCount }: Props) {
|
||||||
const [makeup, setMakeup] = useState<MiiMakeup>("PARTIAL");
|
const [makeup, setMakeup] = useState<MiiMakeup>("PARTIAL");
|
||||||
|
|
||||||
const [way, setWay] = useState<"savedata" | "manual" | null>(null);
|
const [way, setWay] = useState<"savedata" | "manual" | null>(null);
|
||||||
const [miiSaveFileBytes, setMiiSaveFileBytes] = useState<ArrayBufferLike | undefined>();
|
const [miiDataFile, setMiiDataFile] = useState<File | undefined>();
|
||||||
const [miis, setMiis] = useState<CharInfoEx[]>([]);
|
|
||||||
const [selectedMiiIndex, setSelectedMiiIndex] = useState(0);
|
|
||||||
|
|
||||||
const [youtubeId, setYouTubeId] = useState("");
|
const [youtubeId, setYouTubeId] = useState("");
|
||||||
const instructions = useRef<SwitchMiiInstructions>(defaultInstructions);
|
const instructions = useRef<SwitchMiiInstructions>(defaultInstructions);
|
||||||
|
|
@ -114,7 +112,11 @@ export default function SubmitForm({ inQueueMiisCount }: Props) {
|
||||||
formData.append("miiPortraitImage", portraitBlob);
|
formData.append("miiPortraitImage", portraitBlob);
|
||||||
formData.append("way", way);
|
formData.append("way", way);
|
||||||
if (way === "savedata") {
|
if (way === "savedata") {
|
||||||
formData.append("miiData", JSON.stringify(miis[selectedMiiIndex]));
|
if (!miiDataFile) {
|
||||||
|
setError("Failed to find Mii data file, did you upload one?");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
formData.append("miiDataFile", miiDataFile);
|
||||||
} else {
|
} else {
|
||||||
const featuresResponse = await fetch(miiFeaturesUri!);
|
const featuresResponse = await fetch(miiFeaturesUri!);
|
||||||
if (!featuresResponse.ok) {
|
if (!featuresResponse.ok) {
|
||||||
|
|
@ -150,19 +152,7 @@ export default function SubmitForm({ inQueueMiisCount }: Props) {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (platform === "SWITCH") {
|
if (platform !== "THREE_DS") return;
|
||||||
if (!miiSaveFileBytes) return;
|
|
||||||
const miis: CharInfoEx[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < 70; i++) {
|
|
||||||
const data = CharInfoEx.FromSaveFileArrayBuffer(miiSaveFileBytes, i);
|
|
||||||
if (data.name === "") continue;
|
|
||||||
miis.push(data);
|
|
||||||
}
|
|
||||||
setMiis(miis);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (qrBytesRaw.length == 0) return;
|
if (qrBytesRaw.length == 0) return;
|
||||||
const qrBytes = new Uint8Array(qrBytesRaw);
|
const qrBytes = new Uint8Array(qrBytesRaw);
|
||||||
|
|
||||||
|
|
@ -199,7 +189,7 @@ export default function SubmitForm({ inQueueMiisCount }: Props) {
|
||||||
};
|
};
|
||||||
|
|
||||||
preview();
|
preview();
|
||||||
}, [miiSaveFileBytes, qrBytesRaw, platform]);
|
}, [qrBytesRaw, platform]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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">
|
||||||
|
|
@ -233,7 +223,7 @@ export default function SubmitForm({ inQueueMiisCount }: Props) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="max-w-2xl">
|
<div className="max-w-2xl w-full">
|
||||||
{inQueueMiisCount !== 0 && (
|
{inQueueMiisCount !== 0 && (
|
||||||
<div className="bg-zinc-50 border-2 border-zinc-400 rounded-2xl shadow-lg p-4 flex items-start gap-3 text-zinc-600 mb-4">
|
<div className="bg-zinc-50 border-2 border-zinc-400 rounded-2xl shadow-lg p-4 flex items-start gap-3 text-zinc-600 mb-4">
|
||||||
<Icon icon="material-symbols:timer" className="text-2xl shrink-0" />
|
<Icon icon="material-symbols:timer" className="text-2xl shrink-0" />
|
||||||
|
|
@ -443,7 +433,7 @@ export default function SubmitForm({ inQueueMiisCount }: Props) {
|
||||||
type="button"
|
type="button"
|
||||||
className={`flex flex-col justify-center items-center rounded-xl p-4 shadow-md border-2 cursor-pointer text-center text-sm transition hover:scale-[1.03] ${way === "savedata" ? "bg-cyan-100 border-cyan-600" : "bg-zinc-50 border-zinc-300 hover:bg-cyan-100 hover:border-cyan-600"}`}
|
className={`flex flex-col justify-center items-center rounded-xl p-4 shadow-md border-2 cursor-pointer text-center text-sm transition hover:scale-[1.03] ${way === "savedata" ? "bg-cyan-100 border-cyan-600" : "bg-zinc-50 border-zinc-300 hover:bg-cyan-100 hover:border-cyan-600"}`}
|
||||||
>
|
>
|
||||||
Save Data (no makeup yet)
|
.ltd file
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|
@ -455,6 +445,8 @@ export default function SubmitForm({ inQueueMiisCount }: Props) {
|
||||||
Manual
|
Manual
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs text-zinc-400 text-center mt-2">Click on a way to see tutorials for them</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* (Switch Only) Mii Screenshots */}
|
{/* (Switch Only) Mii Screenshots */}
|
||||||
|
|
@ -483,7 +475,7 @@ export default function SubmitForm({ inQueueMiisCount }: Props) {
|
||||||
className="size-20 object-cover rounded-xl border-2 border-orange-300 shrink-0 opacity-70"
|
className="size-20 object-cover rounded-xl border-2 border-orange-300 shrink-0 opacity-70"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<SwitchFileUpload text="a screenshot of your Mii here" image={miiPortraitUri} setImage={setMiiPortraitUri} forceCrop />
|
<SwitchFileUpload text="a screenshot of your Mii here" file={miiPortraitUri} setImage={setMiiPortraitUri} forceCrop />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -505,7 +497,7 @@ export default function SubmitForm({ inQueueMiisCount }: Props) {
|
||||||
className="size-20 object-cover rounded-xl border-2 border-orange-300 shrink-0 opacity-70"
|
className="size-20 object-cover rounded-xl border-2 border-orange-300 shrink-0 opacity-70"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<SwitchFileUpload text="a screenshot of your Mii's features here" image={miiFeaturesUri} setImage={setMiiFeaturesUri} />
|
<SwitchFileUpload text="a screenshot of your Mii's features here" file={miiFeaturesUri} setImage={setMiiFeaturesUri} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -551,6 +543,8 @@ export default function SubmitForm({ inQueueMiisCount }: Props) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col items-center gap-2">
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
<SwitchFileUpload type="file" text="your Mii's .ltd file" file={miiDataFile} setFile={setMiiDataFile} />
|
||||||
|
|
||||||
{/* YouTube */}
|
{/* YouTube */}
|
||||||
<div className="w-full grid grid-cols-3 items-center">
|
<div className="w-full grid grid-cols-3 items-center">
|
||||||
<label htmlFor="youtube" className="font-semibold">
|
<label htmlFor="youtube" className="font-semibold">
|
||||||
|
|
@ -571,27 +565,6 @@ export default function SubmitForm({ inQueueMiisCount }: Props) {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SwitchFileUpload type="file" text="your Mii.sav file" setFileBytes={setMiiSaveFileBytes} />
|
|
||||||
|
|
||||||
<p className="text-sm text-center">Choose the Mii you want to submit:</p>
|
|
||||||
|
|
||||||
<div className="relative bg-orange-100 border-2 border-orange-300 rounded-lg max-w-md w-full p-1 flex flex-col h-48 overflow-x-auto">
|
|
||||||
{miis?.length === 0 ? (
|
|
||||||
<p className="absolute left-1/2 -translate-x-1/2 top-1/2 -translate-y-1/2 text-center text-sm">No miis found, upload a save file!</p>
|
|
||||||
) : (
|
|
||||||
miis?.map((mii, index) => (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
key={index}
|
|
||||||
onClick={() => setSelectedMiiIndex(index)}
|
|
||||||
className={`w-full cursor-pointer text-left px-1.5 py-0.5 rounded-md transition-colors duration-75 ${selectedMiiIndex === index ? "bg-orange-300" : "hover:bg-orange-200"}`}
|
|
||||||
>
|
|
||||||
{mii.name}
|
|
||||||
</button>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { FileWithPath } from "react-dropzone";
|
import { FileWithPath } from "react-dropzone";
|
||||||
import { Icon } from "@iconify/react";
|
import { Icon } from "@iconify/react";
|
||||||
import Dropzone from "../dropzone";
|
import Dropzone from "../dropzone";
|
||||||
|
|
@ -11,43 +11,37 @@ interface Props {
|
||||||
text: string;
|
text: string;
|
||||||
type?: "file" | "image";
|
type?: "file" | "image";
|
||||||
forceCrop?: boolean;
|
forceCrop?: boolean;
|
||||||
image?: string | undefined;
|
file?: string | File | undefined;
|
||||||
setFileBytes?: (value: ArrayBufferLike | undefined) => void;
|
setFile?: (value: File | undefined) => void;
|
||||||
setImage?: (value: string | undefined) => void;
|
setImage?: (value: string | undefined) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SwitchFileUpload({ text, type = "image", forceCrop, image, setFileBytes, setImage }: Props) {
|
export default function SwitchFileUpload({ text, type = "image", forceCrop, file, setFile, setImage }: Props) {
|
||||||
const [isCameraOpen, setIsCameraOpen] = useState(false);
|
const [isCameraOpen, setIsCameraOpen] = useState(false);
|
||||||
const [isCropOpen, setIsCropOpen] = useState(false);
|
const [isCropOpen, setIsCropOpen] = useState(false);
|
||||||
|
|
||||||
const handleDrop = useCallback(
|
const handleDrop = useCallback(
|
||||||
(acceptedFiles: FileWithPath[]) => {
|
(acceptedFiles: FileWithPath[]) => {
|
||||||
const file = acceptedFiles[0];
|
const file = acceptedFiles[0];
|
||||||
const reader = new FileReader();
|
if (type === "file") {
|
||||||
reader.onload = async (event) => {
|
setFile!(file);
|
||||||
const result = event.target!.result;
|
} else {
|
||||||
|
const reader = new FileReader();
|
||||||
if (type === "file") {
|
reader.onload = (event) => {
|
||||||
const buffer = result as ArrayBuffer;
|
setImage!(event.target!.result as string);
|
||||||
setFileBytes!(buffer);
|
|
||||||
} else {
|
|
||||||
// for images
|
|
||||||
setImage!(result as string);
|
|
||||||
if (forceCrop) setIsCropOpen(true);
|
if (forceCrop) setIsCropOpen(true);
|
||||||
}
|
};
|
||||||
};
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
if (type === "file") reader.readAsArrayBuffer(file);
|
|
||||||
else reader.readAsDataURL(file);
|
|
||||||
},
|
},
|
||||||
[setFileBytes, setImage],
|
[setFile, setImage],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-md w-full flex flex-col items-center gap-2">
|
<div className="max-w-md w-full flex flex-col items-center gap-2">
|
||||||
<Dropzone type={type} onDrop={handleDrop} options={{ maxFiles: 1 }}>
|
<Dropzone type={type} onDrop={handleDrop} options={{ maxFiles: 1 }}>
|
||||||
<p className="text-center text-sm">
|
<p className="text-center text-sm">
|
||||||
{!image ? (
|
{!file ? (
|
||||||
<>
|
<>
|
||||||
Drag and drop {text}
|
Drag and drop {text}
|
||||||
<br />
|
<br />
|
||||||
|
|
@ -82,7 +76,7 @@ export default function SwitchFileUpload({ text, type = "image", forceCrop, imag
|
||||||
if (forceCrop) setIsCropOpen(true);
|
if (forceCrop) setIsCropOpen(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ImageEditorPortrait isOpen={isCropOpen} setIsOpen={setIsCropOpen} image={image} setImage={setImage!} />
|
<ImageEditorPortrait isOpen={isCropOpen} setIsOpen={setIsCropOpen} image={file} setImage={setImage!} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -28,37 +28,31 @@ export function minifyInstructions(instructions: Partial<SwitchMiiInstructions>)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultInstructions: SwitchMiiInstructions = {
|
export const defaultInstructions: SwitchMiiInstructions = {
|
||||||
head: { skinColor: null },
|
head: { type: null, skinColor: null },
|
||||||
hair: {
|
hair: { set: null, bangs: null, back: null, color: null, subColor: null, subColor2: null, style: null, isFlipped: false },
|
||||||
color: null,
|
eyebrows: { type: null, color: null, height: null, distance: null, rotation: null, size: null, stretch: null },
|
||||||
subColor: null,
|
|
||||||
subColor2: null,
|
|
||||||
style: null,
|
|
||||||
isFlipped: false,
|
|
||||||
},
|
|
||||||
eyebrows: { color: null, height: null, distance: null, rotation: null, size: null, stretch: null },
|
|
||||||
eyes: {
|
eyes: {
|
||||||
main: { color: null, height: null, distance: null, rotation: null, size: null, stretch: null },
|
main: { type: null, color: null, height: null, distance: null, rotation: null, size: null, stretch: null },
|
||||||
eyelashesTop: { height: null, distance: null, rotation: null, size: null, stretch: null },
|
eyelashesTop: { type: null, height: null, distance: null, rotation: null, size: null, stretch: null },
|
||||||
eyelashesBottom: { height: null, distance: null, rotation: null, size: null, stretch: null },
|
eyelashesBottom: { type: null, height: null, distance: null, rotation: null, size: null, stretch: null },
|
||||||
eyelidTop: { height: null, distance: null, rotation: null, size: null, stretch: null },
|
eyelidTop: { type: null, height: null, distance: null, rotation: null, size: null, stretch: null },
|
||||||
eyelidBottom: { height: null, distance: null, rotation: null, size: null, stretch: null },
|
eyelidBottom: { type: null, height: null, distance: null, rotation: null, size: null, stretch: null },
|
||||||
eyeliner: { color: null },
|
eyeliner: { type: false, color: null },
|
||||||
pupil: { height: null, distance: null, rotation: null, size: null, stretch: null },
|
pupil: { type: null, height: null, distance: null, rotation: null, size: null, stretch: null },
|
||||||
},
|
},
|
||||||
nose: { height: null, size: null },
|
nose: { type: null, height: null, size: null },
|
||||||
lips: { color: null, height: null, rotation: null, size: null, stretch: null, hasLipstick: false },
|
lips: { type: null, color: null, height: null, rotation: null, size: null, stretch: null, hasLipstick: false },
|
||||||
ears: { height: null, size: null },
|
ears: { type: null, height: null, size: null },
|
||||||
glasses: { ringColor: null, shadesColor: null, height: null, size: null, stretch: null },
|
glasses: { type: null, type2: null, ringColor: null, shadesColor: null, height: null, size: null, stretch: null },
|
||||||
other: {
|
other: {
|
||||||
wrinkles1: { height: null, distance: null, size: null, stretch: null },
|
wrinkles1: { type: null, height: null, distance: null, size: null, stretch: null },
|
||||||
wrinkles2: { height: null, distance: null, size: null, stretch: null },
|
wrinkles2: { type: null, height: null, distance: null, size: null, stretch: null },
|
||||||
beard: { color: null },
|
beard: { type: null, color: null },
|
||||||
moustache: { color: null, height: null, isFlipped: false, size: null, stretch: null },
|
moustache: { type: null, color: null, height: null, isFlipped: false, size: null, stretch: null },
|
||||||
goatee: { color: null },
|
goatee: { type: null, color: null },
|
||||||
mole: { color: null, height: null, distance: null, size: null },
|
mole: { type: false, height: null, distance: null, size: null },
|
||||||
eyeShadow: { color: null, height: null, distance: null, size: null, stretch: null },
|
eyeShadow: { type: null, color: null, height: null, distance: null, size: null, stretch: null },
|
||||||
blush: { color: null, height: null, distance: null, size: null, stretch: null },
|
blush: { type: null, color: null, height: null, distance: null, size: null, stretch: null },
|
||||||
},
|
},
|
||||||
height: null,
|
height: null,
|
||||||
weight: null,
|
weight: null,
|
||||||
|
|
@ -238,3 +232,26 @@ export const COLORS: string[] = [
|
||||||
"86E1B0",
|
"86E1B0",
|
||||||
"6E44B0",
|
"6E44B0",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const COLOR_MAP: number[] = [
|
||||||
|
// Row 1
|
||||||
|
88, 99, 107, 97, 48, 101, 90, 29, 98, 68,
|
||||||
|
// Row 2
|
||||||
|
89, 91, 74, 92, 100, 87, 94, 49, 58, 67,
|
||||||
|
// Row 3
|
||||||
|
57, 47, 80, 69, 96, 37, 27, 17, 106, 76,
|
||||||
|
// Row 4
|
||||||
|
86, 77, 66, 56, 46, 36, 26, 16, 105, 95,
|
||||||
|
// Row 5
|
||||||
|
85, 75, 65, 45, 55, 35, 25, 15, 104, 84,
|
||||||
|
// Row 6
|
||||||
|
64, 44, 24, 54, 34, 14, 103, 93, 83, 73,
|
||||||
|
// Row 7
|
||||||
|
63, 53, 43, 23, 13, 102, 72, 82, 62, 52,
|
||||||
|
// Row 8
|
||||||
|
42, 33, 32, 22, 12, 81, 71, 51, 61, 41,
|
||||||
|
// Row 9
|
||||||
|
31, 21, 11, 79, 19, 30, 40, 20, 10, 59,
|
||||||
|
// Row 10
|
||||||
|
60, 39, 70, 50, 9, 78, 38, 28, 18, 8,
|
||||||
|
];
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue