mirror of
https://github.com/trafficlunar/tomodachi-share.git
synced 2026-03-28 11:13:16 +00:00
feat: abandon webp
This commit is contained in:
parent
44be36b501
commit
22fb3a2e30
18 changed files with 34 additions and 39 deletions
|
|
@ -15,7 +15,7 @@ const nextConfig: NextConfig = {
|
||||||
pathname: "/tutorial/**",
|
pathname: "/tutorial/**",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
pathname: "/guest.webp",
|
pathname: "/guest.png",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
|
|
|
||||||
BIN
public/guest.png
Normal file
BIN
public/guest.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 KiB |
|
|
@ -49,7 +49,7 @@ export async function PATCH(request: NextRequest) {
|
||||||
if (!image) {
|
if (!image) {
|
||||||
await prisma.user.update({
|
await prisma.user.update({
|
||||||
where: { id: Number(session.user.id) },
|
where: { id: Number(session.user.id) },
|
||||||
data: { image: `/guest.webp`, imageUpdatedAt: new Date() },
|
data: { image: `/guest.png`, imageUpdatedAt: new Date() },
|
||||||
});
|
});
|
||||||
|
|
||||||
return rateLimit.sendResponse({ success: true });
|
return rateLimit.sendResponse({ success: true });
|
||||||
|
|
@ -64,10 +64,10 @@ export async function PATCH(request: NextRequest) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const buffer = Buffer.from(await image.arrayBuffer());
|
const buffer = Buffer.from(await image.arrayBuffer());
|
||||||
const webpBuffer = await sharp(buffer, { animated: true }).resize({ width: 128, height: 128 }).webp({ quality: 85 }).toBuffer();
|
const pngBuffer = await sharp(buffer, { animated: true }).resize({ width: 128, height: 128 }).png({ quality: 85 }).toBuffer();
|
||||||
const fileLocation = path.join(uploadsDirectory, `${session.user.id}.webp`);
|
const fileLocation = path.join(uploadsDirectory, `${session.user.id}.png`);
|
||||||
|
|
||||||
await fs.writeFile(fileLocation, webpBuffer);
|
await fs.writeFile(fileLocation, pngBuffer);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error uploading profile picture:", error);
|
console.error("Error uploading profile picture:", error);
|
||||||
Sentry.captureException(error, { extra: { stage: "upload-profile-picture" } });
|
Sentry.captureException(error, { extra: { stage: "upload-profile-picture" } });
|
||||||
|
|
|
||||||
|
|
@ -126,10 +126,10 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
images.map(async (image, index) => {
|
images.map(async (image, index) => {
|
||||||
const buffer = Buffer.from(await image.arrayBuffer());
|
const buffer = Buffer.from(await image.arrayBuffer());
|
||||||
const webpBuffer = await sharp(buffer).webp({ quality: 85 }).toBuffer();
|
const pngBuffer = await sharp(buffer).png({ quality: 85 }).toBuffer();
|
||||||
const fileLocation = path.join(miiUploadsDirectory, `image${index}.webp`);
|
const fileLocation = path.join(miiUploadsDirectory, `image${index}.png`);
|
||||||
|
|
||||||
await fs.writeFile(fileLocation, webpBuffer);
|
await fs.writeFile(fileLocation, pngBuffer);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -148,10 +148,10 @@ export async function POST(request: NextRequest) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Compress and store
|
// Compress and store
|
||||||
const studioWebpBuffer = await sharp(studioBuffer).webp({ quality: 85 }).toBuffer();
|
const studioPngBuffer = await sharp(studioBuffer).png({ quality: 85 }).toBuffer();
|
||||||
const studioFileLocation = path.join(miiUploadsDirectory, "mii.webp");
|
const studioFileLocation = path.join(miiUploadsDirectory, "mii.png");
|
||||||
|
|
||||||
await fs.writeFile(studioFileLocation, studioWebpBuffer);
|
await fs.writeFile(studioFileLocation, studioPngBuffer);
|
||||||
|
|
||||||
// Generate a new QR code for aesthetic reasons
|
// Generate a new QR code for aesthetic reasons
|
||||||
const byteString = String.fromCharCode(...qrBytes);
|
const byteString = String.fromCharCode(...qrBytes);
|
||||||
|
|
@ -165,10 +165,10 @@ export async function POST(request: NextRequest) {
|
||||||
const codeBuffer = Buffer.from(codeBase64, "base64");
|
const codeBuffer = Buffer.from(codeBase64, "base64");
|
||||||
|
|
||||||
// Compress and store
|
// Compress and store
|
||||||
const codeWebpBuffer = await sharp(codeBuffer).webp({ quality: 85 }).toBuffer();
|
const codePngBuffer = await sharp(codeBuffer).png({ quality: 85 }).toBuffer();
|
||||||
const codeFileLocation = path.join(miiUploadsDirectory, "qr-code.webp");
|
const codeFileLocation = path.join(miiUploadsDirectory, "qr-code.png");
|
||||||
|
|
||||||
await fs.writeFile(codeFileLocation, codeWebpBuffer);
|
await fs.writeFile(codeFileLocation, codePngBuffer);
|
||||||
await generateMetadataImage(miiRecord, session.user.name!);
|
await generateMetadataImage(miiRecord, session.user.name!);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Clean up if something went wrong
|
// Clean up if something went wrong
|
||||||
|
|
@ -184,10 +184,10 @@ export async function POST(request: NextRequest) {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
images.map(async (image, index) => {
|
images.map(async (image, index) => {
|
||||||
const buffer = Buffer.from(await image.arrayBuffer());
|
const buffer = Buffer.from(await image.arrayBuffer());
|
||||||
const webpBuffer = await sharp(buffer).webp({ quality: 85 }).toBuffer();
|
const pngBuffer = await sharp(buffer).png({ quality: 85 }).toBuffer();
|
||||||
const fileLocation = path.join(miiUploadsDirectory, `image${index}.webp`);
|
const fileLocation = path.join(miiUploadsDirectory, `image${index}.png`);
|
||||||
|
|
||||||
await fs.writeFile(fileLocation, webpBuffer);
|
await fs.writeFile(fileLocation, pngBuffer);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||||
if (!searchParamsParsed.success) return rateLimit.sendResponse({ error: searchParamsParsed.error.issues[0].message }, 400);
|
if (!searchParamsParsed.success) return rateLimit.sendResponse({ error: searchParamsParsed.error.issues[0].message }, 400);
|
||||||
const { type: imageType } = searchParamsParsed.data;
|
const { type: imageType } = searchParamsParsed.data;
|
||||||
|
|
||||||
const fileExtension = imageType === "metadata" ? ".png" : ".webp";
|
const filePath = path.join(process.cwd(), "uploads", "mii", miiId.toString(), `${imageType}.png`);
|
||||||
const filePath = path.join(process.cwd(), "uploads", "mii", miiId.toString(), `${imageType}${fileExtension}`);
|
|
||||||
|
|
||||||
let buffer: Buffer | undefined;
|
let buffer: Buffer | undefined;
|
||||||
// Only find Mii if image type is 'metadata'
|
// Only find Mii if image type is 'metadata'
|
||||||
|
|
@ -109,7 +108,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||||
}
|
}
|
||||||
|
|
||||||
return rateLimit.sendResponse(buffer, 200, {
|
return rateLimit.sendResponse(buffer, 200, {
|
||||||
"Content-Type": "image/webp",
|
"Content-Type": "image/png",
|
||||||
"X-Robots-Tag": "noindex, noimageindex, nofollow",
|
"X-Robots-Tag": "noindex, noimageindex, nofollow",
|
||||||
"Cache-Control": "no-store",
|
"Cache-Control": "no-store",
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||||
type: "profile",
|
type: "profile",
|
||||||
title: `${user.name} (@${user.username}) - TomodachiShare`,
|
title: `${user.name} (@${user.username}) - TomodachiShare`,
|
||||||
description: `View ${user.name}'s profile on TomodachiShare. Creator of ${user._count.miis} Miis. Member since ${joinDate}.`,
|
description: `View ${user.name}'s profile on TomodachiShare. Creator of ${user._count.miis} Miis. Member since ${joinDate}.`,
|
||||||
images: [user.image ?? "/guest.webp"],
|
images: [user.image ?? "/guest.png"],
|
||||||
username: user.username,
|
username: user.username,
|
||||||
firstName: user.name,
|
firstName: user.name,
|
||||||
},
|
},
|
||||||
|
|
@ -55,7 +55,7 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||||
card: "summary",
|
card: "summary",
|
||||||
title: `${user.name} (@${user.username}) - TomodachiShare`,
|
title: `${user.name} (@${user.username}) - TomodachiShare`,
|
||||||
description: `View ${user.name}'s profile on TomodachiShare. Creator of ${user._count.miis} Miis. Member since ${joinDate}.`,
|
description: `View ${user.name}'s profile on TomodachiShare. Creator of ${user._count.miis} Miis. Member since ${joinDate}.`,
|
||||||
images: [user.image ?? "/guest.webp"],
|
images: [user.image ?? "/guest.png"],
|
||||||
creator: user.username!,
|
creator: user.username!,
|
||||||
},
|
},
|
||||||
alternates: {
|
alternates: {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||||
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.issues[0].message }, 400);
|
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.issues[0].message }, 400);
|
||||||
const userId = parsed.data;
|
const userId = parsed.data;
|
||||||
|
|
||||||
const filePath = path.join(process.cwd(), "uploads", "user", `${userId}.webp`);
|
const filePath = path.join(process.cwd(), "uploads", "user", `${userId}.png`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const buffer = await fs.readFile(filePath);
|
const buffer = await fs.readFile(filePath);
|
||||||
|
|
|
||||||
|
|
@ -63,7 +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 src={linkedProfile.image || "/guest.webp"} width={24} height={24} className="bg-white rounded-lg border-r border-orange-400" />
|
<ProfilePicture src={linkedProfile.image || "/guest.png"} width={24} height={24} className="bg-white rounded-lg border-r border-orange-400" />
|
||||||
{linkedProfile.name}
|
{linkedProfile.name}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ export default function Dropzone({ onDrop, options, children }: Props) {
|
||||||
onDrop: handleDrop,
|
onDrop: handleDrop,
|
||||||
maxFiles: 3,
|
maxFiles: 3,
|
||||||
accept: {
|
accept: {
|
||||||
"image/*": [".png", ".jpg", ".jpeg", ".bmp", ".webp", ".heic"],
|
"image/*": [".png", ".jpg", ".jpeg", ".bmp", ".png", ".heic"],
|
||||||
},
|
},
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ export default async function ProfileInformation({ userId, page }: Props) {
|
||||||
<div className="flex w-full gap-4 overflow-x-scroll">
|
<div className="flex w-full gap-4 overflow-x-scroll">
|
||||||
{/* Profile picture */}
|
{/* Profile picture */}
|
||||||
<Link href={`/profile/${user.id}`} className="size-28 aspect-square">
|
<Link href={`/profile/${user.id}`} className="size-28 aspect-square">
|
||||||
<ProfilePicture src={user.image ?? "/guest.webp"} className="rounded-full bg-white border-2 border-orange-400 shadow max-md:self-center" />
|
<ProfilePicture src={user.image ?? "/guest.png"} className="rounded-full bg-white border-2 border-orange-400 shadow max-md:self-center" />
|
||||||
</Link>
|
</Link>
|
||||||
{/* User information */}
|
{/* User information */}
|
||||||
<div className="flex flex-col w-full relative py-3">
|
<div className="flex flex-col w-full relative py-3">
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ export default async function ProfileOverview() {
|
||||||
<li title="Your profile">
|
<li title="Your profile">
|
||||||
<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">
|
<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">
|
||||||
<Image
|
<Image
|
||||||
src={session?.user?.image ?? "/guest.webp"}
|
src={session?.user?.image ?? "/guest.png"}
|
||||||
alt="profile picture"
|
alt="profile picture"
|
||||||
width={40}
|
width={40}
|
||||||
height={40}
|
height={40}
|
||||||
|
|
|
||||||
|
|
@ -7,5 +7,5 @@ export default function ProfilePicture(props: Partial<ImageProps>) {
|
||||||
const { src, ...rest } = props;
|
const { src, ...rest } = props;
|
||||||
const [imgSrc, setImgSrc] = useState(src);
|
const [imgSrc, setImgSrc] = useState(src);
|
||||||
|
|
||||||
return <Image width={128} height={128} {...rest} src={imgSrc || "/guest.webp"} alt={"profile picture"} onError={() => setImgSrc("/guest.webp")} />;
|
return <Image width={128} height={128} {...rest} src={imgSrc || "/guest.png"} alt={"profile picture"} onError={() => setImgSrc("/guest.png")} />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ export default function ProfilePictureSettings() {
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Image
|
<Image
|
||||||
src={newPicture ? URL.createObjectURL(newPicture) : "/guest.webp"}
|
src={newPicture ? URL.createObjectURL(newPicture) : "/guest.png"}
|
||||||
alt="new profile picture"
|
alt="new profile picture"
|
||||||
width={96}
|
width={96}
|
||||||
height={96}
|
height={96}
|
||||||
|
|
@ -93,7 +93,7 @@ export default function ProfilePictureSettings() {
|
||||||
<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">
|
||||||
<p className="font-semibold mb-2">New profile picture:</p>
|
<p className="font-semibold mb-2">New profile picture:</p>
|
||||||
<Image
|
<Image
|
||||||
src={newPicture ? URL.createObjectURL(newPicture) : "/guest.webp"}
|
src={newPicture ? URL.createObjectURL(newPicture) : "/guest.png"}
|
||||||
alt="new profile picture"
|
alt="new profile picture"
|
||||||
width={128}
|
width={128}
|
||||||
height={128}
|
height={128}
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ export default function ReportUserForm({ user }: Props) {
|
||||||
<hr className="border-zinc-300" />
|
<hr className="border-zinc-300" />
|
||||||
|
|
||||||
<div className="bg-orange-100 rounded-xl border-2 border-orange-400 flex p-4 gap-4">
|
<div className="bg-orange-100 rounded-xl border-2 border-orange-400 flex p-4 gap-4">
|
||||||
<ProfilePicture src={user.image ?? "/guest.webp"} width={96} height={96} className="aspect-square rounded-full border-2 border-orange-400" />
|
<ProfilePicture src={user.image ?? "/guest.png"} width={96} height={96} className="aspect-square rounded-full border-2 border-orange-400" />
|
||||||
<div className="flex flex-col justify-center">
|
<div className="flex flex-col justify-center">
|
||||||
<p className="text-xl font-bold overflow-hidden text-ellipsis">{user.name}</p>
|
<p className="text-xl font-bold overflow-hidden text-ellipsis">{user.name}</p>
|
||||||
<p className="text-sm font-bold overflow-hidden text-ellipsis">@{user.username}</p>
|
<p className="text-sm font-bold overflow-hidden text-ellipsis">@{user.username}</p>
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ export default function EditForm({ mii, likes }: Props) {
|
||||||
const response = await fetch(path);
|
const response = await fetch(path);
|
||||||
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}.png`, { type: "image/png" }), { path });
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -130,16 +130,14 @@ export async function generateMetadataImage(mii: Mii, author: string): Promise<{
|
||||||
|
|
||||||
// Load assets concurrently
|
// Load assets concurrently
|
||||||
const [miiImage, qrCodeImage, fonts] = await Promise.all([
|
const [miiImage, qrCodeImage, fonts] = await Promise.all([
|
||||||
// Read and convert the .webp images to .png (because satori doesn't support it)
|
// Read and convert the images to data URI
|
||||||
fs.readFile(path.join(miiUploadsDirectory, "mii.webp")).then((buffer) =>
|
fs.readFile(path.join(miiUploadsDirectory, "mii.png")).then((buffer) =>
|
||||||
sharp(buffer)
|
sharp(buffer)
|
||||||
.png()
|
|
||||||
.toBuffer()
|
.toBuffer()
|
||||||
.then((pngBuffer) => `data:image/png;base64,${pngBuffer.toString("base64")}`),
|
.then((pngBuffer) => `data:image/png;base64,${pngBuffer.toString("base64")}`),
|
||||||
),
|
),
|
||||||
fs.readFile(path.join(miiUploadsDirectory, "qr-code.webp")).then((buffer) =>
|
fs.readFile(path.join(miiUploadsDirectory, "qr-code.png")).then((buffer) =>
|
||||||
sharp(buffer)
|
sharp(buffer)
|
||||||
.png()
|
|
||||||
.toBuffer()
|
.toBuffer()
|
||||||
.then((pngBuffer) => `data:image/png;base64,${pngBuffer.toString("base64")}`),
|
.then((pngBuffer) => `data:image/png;base64,${pngBuffer.toString("base64")}`),
|
||||||
),
|
),
|
||||||
|
|
@ -209,8 +207,6 @@ export async function generateMetadataImage(mii: Mii, author: string): Promise<{
|
||||||
|
|
||||||
// Store the file
|
// Store the file
|
||||||
try {
|
try {
|
||||||
// I tried using .webp here but the quality looked awful
|
|
||||||
// but it actually might be well-liked due to the hatred of .webp
|
|
||||||
const fileLocation = path.join(miiUploadsDirectory, "metadata.png");
|
const fileLocation = path.join(miiUploadsDirectory, "metadata.png");
|
||||||
await fs.writeFile(fileLocation, buffer);
|
await fs.writeFile(fileLocation, buffer);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue