Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion next.config.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
if (process.env.NODE_ENV === "development") {
if (
process.env.NODE_ENV === "development" &&
process.env.STARDEW_APP_LOCAL_ONLY !== "1"
) {
const { initOpenNextCloudflareForDev } = require("@opennextjs/cloudflare");
initOpenNextCloudflareForDev();
}

/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
output: "standalone",
typescript: { ignoreBuildErrors: true },
eslint: { ignoreDuringBuilds: true },
rewrites: async () => {
Expand Down
57 changes: 57 additions & 0 deletions scripts/node-readlink-compat.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const fs = require("node:fs");

function shouldNormalizeReadlinkError(error, path) {
if (!error || error.code !== "EISDIR") return false;

try {
return !fs.lstatSync(path).isSymbolicLink();
} catch {
return false;
}
}

function normalizeReadlinkError(error) {
error.code = "EINVAL";
error.message = error.message.replace("EISDIR", "EINVAL");
return error;
}

const originalReadlinkSync = fs.readlinkSync.bind(fs);
fs.readlinkSync = function readlinkSyncCompat(path, options) {
try {
return originalReadlinkSync(path, options);
} catch (error) {
if (shouldNormalizeReadlinkError(error, path)) {
throw normalizeReadlinkError(error);
}
throw error;
}
};

const originalReadlink = fs.readlink.bind(fs);
fs.readlink = function readlinkCompat(path, options, callback) {
if (typeof options === "function") {
callback = options;
options = undefined;
}

return originalReadlink(path, options, (error, result) => {
if (error && shouldNormalizeReadlinkError(error, path)) {
callback(normalizeReadlinkError(error));
return;
}
callback(error, result);
});
};

const originalPromisesReadlink = fs.promises.readlink.bind(fs.promises);
fs.promises.readlink = async function promisesReadlinkCompat(path, options) {
try {
return await originalPromisesReadlink(path, options);
} catch (error) {
if (shouldNormalizeReadlinkError(error, path)) {
throw normalizeReadlinkError(error);
}
throw error;
}
};
76 changes: 42 additions & 34 deletions src/components/dialogs/upload-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from "@/components/ui/dialog";
import { PlayersContext } from "@/contexts/players-context";
import { parseSaveFile } from "@/lib/file";
import { readBestSaveFileText } from "@/lib/save-loader";
import { useContext, useState } from "react";
import Dropzone from "react-dropzone";
import { toast } from "sonner";
Expand All @@ -21,7 +22,7 @@ interface Props {
interface InstructionsDialogProps {
open: boolean;
setOpen: (open: boolean) => void;
platform: "Mac" | "Windows" | "Linux" | "Switch";
platform: "Mac" | "Windows" | "Linux" | "Switch" | "PlayStation";
}

const InstructionsDialog = ({
Expand Down Expand Up @@ -78,6 +79,16 @@ const InstructionsDialog = ({
"We apologize for any inconvenience this may cause.",
],
};
case "PlayStation":
return {
title: "PS4 and PS Vita Save Files",
path: "",
steps: [
"Export the save with Apollo Save Tool, Save Wizard, VitaShell Open Decrypted, psvpfstools, or an equivalent tool.",
"If the export gives you farm folders, upload the farm-named payload such as Farmer_123456789 or Farmmc_355939291.",
"The app can read plain XML and zlib-compressed PlayStation payloads. SaveGameInfo and pfsSKKey metadata are not complete saves by themselves.",
],
};
}
};

Expand Down Expand Up @@ -131,50 +142,36 @@ export const UploadDialog = ({ open, setOpen }: Props) => {
const { activePlayer, uploadPlayers } = useContext(PlayersContext);
const [instructionsOpen, setInstructionsOpen] = useState(false);
const [selectedPlatform, setSelectedPlatform] = useState<
"Mac" | "Windows" | "Linux" | "Switch"
"Mac" | "Windows" | "Linux" | "Switch" | "PlayStation"
>("Mac");

const handleChange = (file: File) => {
const handleChange = (files: File[]) => {
setOpen(false);

if (typeof file === "undefined" || !file) return;
if (!files.length) return;

if (file.type !== "") {
if (files.length === 1 && files[0].type !== "") {
toast.error("Invalid file type", {
description: "Please upload a Stardew Valley save file.",
});
return;
}

const reader = new FileReader();

let uploadPromise;

reader.onloadstart = () => {
uploadPromise = new Promise((resolve, reject) => {
reader.onload = async function (event) {
try {
const players = parseSaveFile(event.target?.result as string);
await uploadPlayers(players);
resolve("Your save file was successfully uploaded!");
} catch (err) {
reject(err instanceof Error ? err.message : "Unknown error.");
}
};
});

// Start the loading toast
toast.promise(uploadPromise, {
loading: "Uploading your save file...",
success: (data) => `${data}`,
error: (err) => `There was an error parsing your save file:\n${err}`,
});

// Reset the input
uploadPromise = null;
};
const uploadPromise = (async () => {
const { text } = await readBestSaveFileText(files);
const players = parseSaveFile(text);
await uploadPlayers(players);
return "Your save file was successfully uploaded!";
})();

reader.readAsText(file);
toast.promise(uploadPromise, {
loading: "Uploading your save file...",
success: (data) => `${data}`,
error: (err) =>
`There was an error parsing your save file:\n${
err instanceof Error ? err.message : "Unknown error."
}`,
});
};

return (
Expand All @@ -187,9 +184,10 @@ export const UploadDialog = ({ open, setOpen }: Props) => {
<DialogDescription>
<Dropzone
onDrop={(acceptedFiles) => {
handleChange(acceptedFiles[0]);
handleChange(acceptedFiles);
}}
useFsAccessApi={false}
multiple
>
{({ getRootProps, getInputProps }) => (
<>
Expand Down Expand Up @@ -258,6 +256,16 @@ export const UploadDialog = ({ open, setOpen }: Props) => {
>
Nintendo Switch
</Button>
<Button
variant={"secondary"}
onClick={() => {
setSelectedPlatform("PlayStation");
setInstructionsOpen(true);
}}
className="w-full"
>
PS4 / PS Vita
</Button>
</div>
</div>
</DialogContent>
Expand Down
51 changes: 20 additions & 31 deletions src/components/sheets/mobile-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { Button } from "@/components/ui/button";
import { Drawer, DrawerContent } from "@/components/ui/drawer";
import { fetchJson } from "@/lib/fetch";
import { parseSaveFile } from "@/lib/file";
import { readBestSaveFileText } from "@/lib/save-loader";
import { toast } from "sonner";
import { ScrollArea } from "../ui/scroll-area";

Expand Down Expand Up @@ -66,48 +67,35 @@ export const MobileNav = ({
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
e.preventDefault();

const file = e.target!.files![0];
const files = e.target.files;
const file = files?.[0];

setIsOpen(false);

if (typeof file === "undefined" || !file) return;
if (!files?.length || typeof file === "undefined" || !file) return;

if (file.type !== "") {
if (files.length === 1 && file.type !== "") {
toast.error("Invalid file type", {
description: "Please upload a Stardew Valley save file.",
});
return;
}

const reader = new FileReader();
const uploadPromise = (async () => {
const { text } = await readBestSaveFileText(files);
const players = parseSaveFile(text);
await uploadPlayers(players);
return "Your save file was successfully uploaded!";
})();

let uploadPromise;

reader.onloadstart = () => {
uploadPromise = new Promise((resolve, reject) => {
reader.onload = async function (event) {
try {
const players = parseSaveFile(event.target?.result as string);
await uploadPlayers(players);
resolve("Your save file was successfully uploaded!");
} catch (err) {
reject(err instanceof Error ? err.message : "Unknown error.");
}
};
});

// Start the loading toast
toast.promise(uploadPromise, {
loading: "Uploading your save file...",
success: (data) => `${data}`,
error: (err) => `There was an error parsing your save file:\n${err}`,
});

// Reset the input
uploadPromise = null;
};

reader.readAsText(file);
toast.promise(uploadPromise, {
loading: "Uploading your save file...",
success: (data) => `${data}`,
error: (err) =>
`There was an error parsing your save file:\n${
err instanceof Error ? err.message : "Unknown error."
}`,
});
};

return (
Expand Down Expand Up @@ -145,6 +133,7 @@ export const MobileNav = ({
type="file"
ref={inputRef}
className="hidden"
multiple
onChange={(e: ChangeEvent<HTMLInputElement>) =>
handleChange(e)
}
Expand Down
2 changes: 2 additions & 0 deletions src/components/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
IconAward,
IconBook,
IconBox,
IconArrowsExchange,
IconBrandDiscord,
IconBrandGithub,
IconBuildingWarehouse,
Expand Down Expand Up @@ -44,6 +45,7 @@ export const miscNavigation = [
{ name: "Bundles", href: "/bundles", icon: IconBox },
{ name: "Secret Notes", href: "/notes", icon: IconNote },
{ name: "Rarecrows", href: "/rarecrows", icon: IconCarrot },
{ name: "Save Converter", href: "/converter", icon: IconArrowsExchange },
{ name: "Account Settings", href: "/account", icon: IconSettings },
];

Expand Down
Loading