diff --git a/backend/game.go b/backend/game.go index 34f106b..e74ff7c 100644 --- a/backend/game.go +++ b/backend/game.go @@ -2,6 +2,7 @@ package backend import ( "context" + "errors" "fmt" "os" "path" @@ -50,7 +51,7 @@ func (g *Game) AddGame(name, savePath string, isFile bool) uuid.UUID { return id } -func (g *Game) RemoveGame(id uuid.UUID) { +func (g *Game) RemoveGame(id uuid.UUID) error { newList := []GameSingle{} for _, game := range g.JsonGame.Data { if game.ID == id { @@ -58,10 +59,15 @@ func (g *Game) RemoveGame(id uuid.UUID) { } newList = append(newList, game) } + if len(newList) == len(g.JsonGame.Data) { + errorMsg := fmt.Sprintf("no element deleted: %v", id) + return errors.New(errorMsg) + } g.JsonGame.Data = newList g.updateJson() g.removeGameDir(id) g.logf("Deleted: %v", id) + return nil } func (g *Game) ReadGames() (*JsonGame, error) { diff --git a/backend/save.go b/backend/save.go index 54833c3..58a7983 100644 --- a/backend/save.go +++ b/backend/save.go @@ -66,6 +66,14 @@ func (s *Save) LoadSave(saveID, gameID uuid.UUID) error { return nil } +func (s *Save) OverwriteSave(saveID, gameID uuid.UUID) error { + err := s.copyGameContent(gameID, saveID) + if err != nil { + return err + } + return nil +} + func (s *Save) ReadSaves() (*JsonSave, error) { saveJson, err := utils.ReadConfigFrom[JsonSave](s.filename) return saveJson, err @@ -134,9 +142,6 @@ func (s *Save) copySaveContent(saveID, gameID uuid.UUID) error { if err != nil { return err } - if err = os.RemoveAll(gamePath); err != nil { - return err - } if err = utils.CopyDir(savePath, gamePath); err != nil { return err } diff --git a/backend/utils/copy-dir.go b/backend/utils/copy-dir.go index 6b79a01..50e5ff8 100644 --- a/backend/utils/copy-dir.go +++ b/backend/utils/copy-dir.go @@ -1,9 +1,15 @@ package utils import ( + "os" + cp "github.com/otiai10/copy" ) func CopyDir(src, dst string) error { + // hard copy (remove target dir and then copy) + if err := os.RemoveAll(dst); err != nil { + return err + } return cp.Copy(src, dst) } diff --git a/frontend/src/components/QuickSaveChip.tsx b/frontend/src/components/QuickSaveChip.tsx new file mode 100644 index 0000000..7f6126b --- /dev/null +++ b/frontend/src/components/QuickSaveChip.tsx @@ -0,0 +1,55 @@ +import { Button, Chip } from "@material-tailwind/react"; +import clsx from "clsx"; +import { FaFolderOpen } from "react-icons/fa6"; +import WithTooltip from "@/components/WithTooltip"; + +type QuickSaveChipProps = { + enabled: boolean; + onClose: () => void; + onOpen: () => void; +}; + +const QuickSaveChip = (props: QuickSaveChipProps) => { + return ( +
+ + Quick Save + + + + +
+ } + onClose={() => props.onClose()} + icon={ + + } + /> + + ); +}; + +export default QuickSaveChip; diff --git a/frontend/src/components/Save.tsx b/frontend/src/components/Save.tsx new file mode 100644 index 0000000..3eda6de --- /dev/null +++ b/frontend/src/components/Save.tsx @@ -0,0 +1,77 @@ +import { Button, Typography } from "@material-tailwind/react"; +import { FaDownload, FaFolderOpen, FaTrash, FaUpload } from "react-icons/fa6"; +import { type SaveSingle } from "@/hooks/useSave"; +import WithTooltip from "@/components/WithTooltip"; + +type SavePropsCallback = () => Promise | void; + +type SaveProps = { + data: SaveSingle; + onLoad: SavePropsCallback; + onSave: SavePropsCallback; + onDelete: SavePropsCallback; + onOpenDirectory: SavePropsCallback; +}; + +const Save = (props: SaveProps) => { + const formatDate = (dateString: string) => { + const date = new Date(dateString); + const intlDate = new Intl.DateTimeFormat("en-US").format(date); + return intlDate; + }; + + return ( +
+
+
+ + + + + + + +
+ + {props.data.Name} +
+ + + {formatDate(props.data.CreatedAt)} + + + + + + + + + +
+ ); +}; + +export default Save; diff --git a/frontend/src/components/WithTooltip.tsx b/frontend/src/components/WithTooltip.tsx new file mode 100644 index 0000000..f8f5809 --- /dev/null +++ b/frontend/src/components/WithTooltip.tsx @@ -0,0 +1,26 @@ +import { Tooltip, Typography } from "@material-tailwind/react"; +import { type ReactElement } from "react"; + +type WithTooltipProps = { + children: ReactElement; + content: string; + placement: string; +}; + +const WithTooltip = (props: WithTooltipProps) => { + return ( + + {props.content} + + } + placement={props.placement} + className="border border-blue-gray-50 bg-white shadow-xl shadow-black/10" + > + {props.children} + + ); +}; + +export default WithTooltip; diff --git a/frontend/src/hooks/useSave.tsx b/frontend/src/hooks/useSave.tsx index ee81880..102c5b8 100644 --- a/frontend/src/hooks/useSave.tsx +++ b/frontend/src/hooks/useSave.tsx @@ -6,6 +6,7 @@ import { GetSaves, LoadQuickSave, LoadSave, + OverwriteSave, RemoveQuickSave, RemoveSave, } from "@wailsjs/go/backend/Save"; @@ -70,6 +71,11 @@ const useSave = (props?: Pick) => { mutationFn: (s: Pick) => LoadQuickSave(s.GameID), }); + const { mutateAsync: overwriteSave } = useMutation({ + mutationFn: (s: Pick) => + OverwriteSave(s.ID, s.GameID), + }); + return { querySaves, queryQuickSave, @@ -79,6 +85,7 @@ const useSave = (props?: Pick) => { removeQuickSave, loadSave, loadQuickSave, + overwriteSave, }; }; diff --git a/frontend/src/pages/Game.tsx b/frontend/src/pages/Game.tsx index dc122d1..7f28282 100644 --- a/frontend/src/pages/Game.tsx +++ b/frontend/src/pages/Game.tsx @@ -4,14 +4,13 @@ import { Card, CardBody, CardHeader, - Chip, Input, Typography, } from "@material-tailwind/react"; import { useParams } from "react-router-dom"; import { type SubmitHandler, useForm } from "react-hook-form"; import clsx from "clsx"; -import { FaFolderOpen, FaPencil, FaTrash, FaUpload } from "react-icons/fa6"; +import { FaFolderOpen, FaPencil } from "react-icons/fa6"; import { OpenQuickSaveDir, OpenSaveDir } from "@wailsjs/go/backend/Save"; import { toast } from "react-toastify"; import { useCallback, useState } from "react"; @@ -22,6 +21,9 @@ import LightningSave from "@/components/LightningSave"; import useSave from "@/hooks/useSave"; import useEvents from "@/hooks/useEvents"; import DialogGameForm from "@/components/DialogGameForm"; +import Save from "@/components/Save"; +import WithTooltip from "@/components/WithTooltip"; +import QuickSaveChip from "@/components/QuickSaveChip"; type GameQueryParams = { id: string; @@ -43,6 +45,7 @@ const Game = () => { removeQuickSave, loadSave, loadQuickSave, + overwriteSave, } = useSave({ GameID: gameID, }); @@ -73,6 +76,11 @@ const Game = () => { toast.info("Loaded"); }; + const handleOverwrite = async (saveID: string) => { + await overwriteSave({ ID: saveID, GameID: gameID }); + toast.info("Saved"); + }; + const handleDelete = (saveID: string) => removeSave({ ID: saveID, GameID: gameID }); @@ -93,56 +101,9 @@ const Game = () => { const handleOpenQuickSaveDirectory = () => OpenQuickSaveDir(gameID); - const formatDate = (dateString: string) => { - const date = new Date(dateString); - const intlDate = new Intl.DateTimeFormat("en-US").format(date); - return intlDate; - }; - useEvents({ type: "quickSave", cb: handleQuickSave }); useEvents({ type: "quickLoad", cb: handleQuickLoad }); - const chipValue = ( -
- Quick Save - - -
- ); - - const getQuickSaveChip = () => ( -
- removeQuickSave({ GameID: gameID })} - icon={ - - } - /> -
- ); - useMenuMiddleItem( , ); @@ -154,24 +115,28 @@ const Game = () => {
- - - + + + + + + +
@@ -184,7 +149,11 @@ const Game = () => {
Manage your saves 👾 - {getQuickSaveChip()} + removeQuickSave({ GameID: gameID })} + onOpen={() => handleOpenQuickSaveDirectory()} + />
{
) : ( querySaves.data?.map((save) => ( -
-
- - {save.Name} -
- - - {formatDate(save.CreatedAt)} - - - - - -
+ handleLoad(save.ID)} + onSave={() => handleOverwrite(save.ID)} + onDelete={() => handleDelete(save.ID)} + onOpenDirectory={() => handleOpenSaveDirectory(save.ID)} + /> )) )} +