diff --git a/backend/app.go b/backend/app.go index 6700a5c..b70a94e 100644 --- a/backend/app.go +++ b/backend/app.go @@ -4,6 +4,7 @@ import ( "context" "runtime" + "github.com/cabaalexander/save-manager/backend/utils" rt "github.com/wailsapp/wails/v2/pkg/runtime" ) @@ -27,34 +28,12 @@ func (a *App) ToggleFullScreen() { } } -func (a *App) OpenDirectoryDialog() string { - path, err := rt.OpenDirectoryDialog(a.ctx, a.getDefaultFileOrDirPath()) - if err != nil { - return "" - } - return path -} - -func (a *App) OpenFileDialog() string { - path, err := rt.OpenFileDialog(a.ctx, a.getDefaultFileOrDirPath()) - if err != nil { - return "" - } - return path +func (a *App) OpenDialogDirApp() (string, error) { + return utils.OpenDialogDir(a.ctx, a.Settings.DefaultSavePath, false) } -func (a *App) getDefaultFileOrDirPath() rt.OpenDialogOptions { - defaultPath := a.Settings.DefaultSavePath - if a.Settings.JsonSettings.DefaultSavePathIsFile { - return rt.OpenDialogOptions{ - DefaultFilename: defaultPath, - Title: "Select File", - } - } - return rt.OpenDialogOptions{ - DefaultDirectory: defaultPath, - Title: "Select Directory", - } +func (a *App) OpenDialogFileApp() (string, error) { + return utils.OpenDialogFile(a.ctx, a.Settings.DefaultSavePath, true) } func (a *App) GetOS() string { diff --git a/backend/game.go b/backend/game.go index 86b602f..34f106b 100644 --- a/backend/game.go +++ b/backend/game.go @@ -12,9 +12,10 @@ import ( ) type GameSingle struct { - ID uuid.UUID - Name string - SavePath string + ID uuid.UUID + Name string + SavePath string + SavePathIsFile bool } type JsonGame struct { @@ -39,9 +40,9 @@ func (g *Game) Startup(ctx context.Context) { g.JsonGame = *game } -func (g *Game) AddGame(name string, savePath string) uuid.UUID { +func (g *Game) AddGame(name, savePath string, isFile bool) uuid.UUID { id := uuid.New() - game := GameSingle{ID: id, Name: name, SavePath: savePath} + game := GameSingle{ID: id, Name: name, SavePath: savePath, SavePathIsFile: isFile} g.JsonGame.Data = append(g.JsonGame.Data, game) g.updateJson() CreateGameDir(id) @@ -81,10 +82,10 @@ func (g *Game) FindGame(id uuid.UUID) GameSingle { return GameSingle{} } -func (g *Game) OpenGameDir(gameID uuid.UUID) { +func (g *Game) BrowseGameDir(gameID uuid.UUID) { for _, game := range g.JsonGame.Data { if game.ID == gameID { - utils.OpenPath(g.ctx, game.SavePath) + utils.BrowsePath(g.ctx, game.SavePath) } } } @@ -99,6 +100,7 @@ func (g *Game) UpdateGame(props GameSingle) { if props.SavePath != "" { gm.SavePath = props.SavePath } + gm.SavePathIsFile = props.SavePathIsFile } gameList = append(gameList, gm) } @@ -106,6 +108,16 @@ func (g *Game) UpdateGame(props GameSingle) { g.updateJson() } +func (g *Game) OpenDialogDirGame(gameID uuid.UUID) (string, error) { + game := g.FindGame(gameID) + return utils.OpenDialogDir(g.ctx, game.SavePath, game.SavePathIsFile) +} + +func (g *Game) OpenDialogFileGame(gameID uuid.UUID) (string, error) { + game := g.FindGame(gameID) + return utils.OpenDialogFile(g.ctx, game.SavePath, game.SavePathIsFile) +} + func (g *Game) removeGameDir(gameID uuid.UUID) error { gameDir, err := GetGameDir(gameID) if err != nil { diff --git a/backend/save-quicksave.go b/backend/save-quicksave.go index 8aedb45..6d64193 100644 --- a/backend/save-quicksave.go +++ b/backend/save-quicksave.go @@ -57,7 +57,7 @@ func (s *Save) OpenQuickSaveDir(gameID uuid.UUID) error { if err != nil { return err } - if err = utils.OpenPath(s.ctx, quickSaveDir); err != nil { + if err = utils.BrowsePath(s.ctx, quickSaveDir); err != nil { return err } return nil diff --git a/backend/save.go b/backend/save.go index 029126a..54833c3 100644 --- a/backend/save.go +++ b/backend/save.go @@ -113,7 +113,7 @@ func (s *Save) OpenSaveDir(saveID, gameID uuid.UUID) error { if err != nil { return err } - utils.OpenPath(s.ctx, saveDir) + utils.BrowsePath(s.ctx, saveDir) return nil } diff --git a/backend/settings.go b/backend/settings.go index 03b142a..bd76972 100644 --- a/backend/settings.go +++ b/backend/settings.go @@ -64,8 +64,8 @@ func (s *Settings) ReadSettings() (*JsonSettings, error) { return settingsJson, nil } -func (s *Settings) SetDefaultSavePath(text string, isFile bool) { - s.JsonSettings.DefaultSavePath = text +func (s *Settings) SetDefaultSavePath(path string, isFile bool) { + s.JsonSettings.DefaultSavePath = path s.JsonSettings.DefaultSavePathIsFile = isFile s.updateJson() } diff --git a/backend/utils/utils.go b/backend/utils/utils.go index 2f1e3bd..5ff884f 100644 --- a/backend/utils/utils.go +++ b/backend/utils/utils.go @@ -7,6 +7,7 @@ import ( "io/fs" "os" "path" + "path/filepath" rt "github.com/wailsapp/wails/v2/pkg/runtime" ) @@ -158,7 +159,14 @@ type StartAble interface { Startup(ctx context.Context) } -func OpenPath(ctx context.Context, path string) error { +func BrowsePath(ctx context.Context, path string) error { + pathIsFile, err := isPathFile(path) + if err != nil { + return err + } + if pathIsFile { + path = filepath.Dir(path) + } rt.BrowserOpenURL(ctx, path) return nil } @@ -170,3 +178,54 @@ func StartApps(l []StartAble) func(ctx context.Context) { } } } + +func OpenDialogFile(ctx context.Context, path string, isFile bool) (string, error) { + path, err := rt.OpenFileDialog(ctx, getFileOrDirOptions(path, isFile)) + if err != nil { + return "", err + } + return path, nil +} + +func OpenDialogDir(ctx context.Context, path string, isFile bool) (string, error) { + pathIsFile, err := isPathFile(path) + if err != nil { + return "", err + } + // if path is a file get parent dir (so this opens a directory efectively) + if pathIsFile { + path = filepath.Dir(path) + } + path, err = rt.OpenDirectoryDialog( + ctx, + getFileOrDirOptions(path, isFile), + ) + if err != nil { + return "", err + } + return path, nil +} + +func getFileOrDirOptions(path string, isFile bool) rt.OpenDialogOptions { + if isFile { + return rt.OpenDialogOptions{ + DefaultFilename: path, + Title: "Select File", + } + } + return rt.OpenDialogOptions{ + DefaultDirectory: path, + Title: "Select Directory", + } +} + +func isPathFile(path string) (bool, error) { + fileInfo, err := os.Stat(path) + if err != nil { + return false, err + } + if fileInfo.IsDir() { + return false, nil + } + return true, nil +} diff --git a/frontend/src/components/AddGame.tsx b/frontend/src/components/AddGame.tsx index a7efb31..65b0847 100644 --- a/frontend/src/components/AddGame.tsx +++ b/frontend/src/components/AddGame.tsx @@ -38,6 +38,7 @@ const AddGame = () => { const gameID = (await addGame({ Name: data.Name, SavePath: data.SavePath, + SavePathIsFile: data.SavePathIsFile, })) as string; navigate(`/game/${gameID}`); }} diff --git a/frontend/src/components/DialogGameForm.tsx b/frontend/src/components/DialogGameForm.tsx index 98778fd..8ab56a1 100644 --- a/frontend/src/components/DialogGameForm.tsx +++ b/frontend/src/components/DialogGameForm.tsx @@ -9,7 +9,6 @@ import { Input, Typography, } from "@material-tailwind/react"; -import { OpenDirectoryDialog, OpenFileDialog } from "@wailsjs/go/backend/App"; import clsx from "clsx"; import { type Dispatch, @@ -21,12 +20,22 @@ import { import { type SubmitHandler, useForm } from "react-hook-form"; import { FaDirections } from "react-icons/fa"; import { FaX } from "react-icons/fa6"; +import { OpenDialogDirApp, OpenDialogFileApp } from "@wailsjs/go/backend/App"; +import { + OpenDialogDirGame, + OpenDialogFileGame, +} from "@wailsjs/go/backend/Game"; import { type GameSingle } from "@/hooks/useGame"; import useSettings from "@/hooks/useSettings"; const TRANSITION_TIMEOUT = 300; -type FormInputs = Pick; +type FormInputs = Pick; + +type DialogGameFormDefaultValues = { + formInputs: Partial; + gameID: string; +}; type DialogGameFormProps = { open: boolean; @@ -34,15 +43,11 @@ type DialogGameFormProps = { title: string; bodyTitle?: string; required?: Partial>; - defaultValues?: Partial>; + defaultValues?: Partial; submit: (data: FormInputs) => unknown; }; const DialogGameForm: FC = (props) => { - const { querySettings } = useSettings(); - const [isFileDialog, setIsFileDialog] = useState( - querySettings.data?.DefaultSavePathIsFile ?? false, - ); const { register, handleSubmit, @@ -53,19 +58,42 @@ const DialogGameForm: FC = (props) => { formState: { errors: formErrors }, } = useForm(); const { defaultValues } = props; + const { querySettings } = useSettings(); + + const defaultIsFile = + defaultValues?.formInputs?.SavePathIsFile ?? + querySettings.data?.DefaultSavePathIsFile ?? + false; + + const [isFileDialog, setIsFileDialog] = useState(defaultIsFile); const onSubmit: SubmitHandler = (data) => props.submit(data); const handlePath = async () => { - const path = isFileDialog - ? await OpenFileDialog() - : await OpenDirectoryDialog(); + let path = ""; + + if (defaultValues?.formInputs?.SavePath === "") { + path = isFileDialog + ? await OpenDialogFileApp() + : await OpenDialogDirApp(); + } else { + path = isFileDialog + ? await OpenDialogFileGame(defaultValues?.gameID) + : await OpenDialogDirGame(defaultValues?.gameID); + } + if (path === "") return; + setValue("SavePath", path); clearErrors("SavePath"); setFocus("Name"); }; + const handleIsFile = () => { + setIsFileDialog(!isFileDialog); + setValue("SavePathIsFile", !isFileDialog); + }; + useEffect(() => { if (props.open) { setTimeout(() => { @@ -75,13 +103,17 @@ const DialogGameForm: FC = (props) => { }); useEffect(() => { - if (defaultValues) { - Object.entries(defaultValues).forEach(([key, value]) => { + if (defaultValues?.formInputs) { + Object.entries(defaultValues.formInputs).forEach(([key, value]) => { setValue(key as keyof FormInputs, value); }); } }, [defaultValues, setValue]); + useEffect(() => { + setIsFileDialog(defaultIsFile); + }, [defaultIsFile]); + const toggleDialog = () => { props.handler(!props.open); setTimeout(() => { @@ -151,11 +183,15 @@ const DialogGameForm: FC = (props) => { { - setIsFileDialog(!isFileDialog); - }} + onChange={handleIsFile} label="File" /> + @@ -166,7 +202,8 @@ const DialogGameForm: FC = (props) => { @@ -177,7 +214,13 @@ const DialogGameForm: FC = (props) => { DialogGameForm.defaultProps = { bodyTitle: "", required: { Name: false, SavePath: false }, - defaultValues: { Name: "", SavePath: "" }, + defaultValues: { + formInputs: { + Name: "", + SavePath: "", + }, + gameID: "", + }, }; export default DialogGameForm; diff --git a/frontend/src/hooks/useGame.tsx b/frontend/src/hooks/useGame.tsx index 02d85b1..f1154e9 100644 --- a/frontend/src/hooks/useGame.tsx +++ b/frontend/src/hooks/useGame.tsx @@ -12,6 +12,7 @@ export type GameSingle = { ID: string; Name: string; SavePath: string; + SavePathIsFile: boolean; }; export type Game = { @@ -57,8 +58,8 @@ const useGame = (props?: Partial) => { const { mutateAsync: addGame } = useMutation({ onSuccess: invalidateGamesQuery, - mutationFn: (g: Pick) => - AddGame(g.Name, g.SavePath), + mutationFn: (g: Pick) => + AddGame(g.Name, g.SavePath, g.SavePathIsFile), }); const { mutateAsync: updateGame } = useMutation({ @@ -67,6 +68,7 @@ const useGame = (props?: Partial) => { }); return { + invalidateGamesQuery, queryGame, removeGame, addGame, diff --git a/frontend/src/pages/Game.tsx b/frontend/src/pages/Game.tsx index a795e07..dc122d1 100644 --- a/frontend/src/pages/Game.tsx +++ b/frontend/src/pages/Game.tsx @@ -13,9 +13,9 @@ import { type SubmitHandler, useForm } from "react-hook-form"; import clsx from "clsx"; import { FaFolderOpen, FaPencil, FaTrash, FaUpload } from "react-icons/fa6"; import { OpenQuickSaveDir, OpenSaveDir } from "@wailsjs/go/backend/Save"; -import { OpenGameDir } from "@wailsjs/go/backend/Game"; import { toast } from "react-toastify"; import { useCallback, useState } from "react"; +import { BrowseGameDir } from "@wailsjs/go/backend/Game"; import useGame, { type GameSingle } from "@/hooks/useGame"; import useMenuMiddleItem from "@/hooks/useMenuMiddleItem"; import LightningSave from "@/components/LightningSave"; @@ -46,7 +46,7 @@ const Game = () => { } = useSave({ GameID: gameID, }); - const { queryGame, updateGame } = useGame({ + const { queryGame, updateGame, invalidateGamesQuery } = useGame({ queryKey: "game", queryArgs: { ID: gameID }, }); @@ -79,7 +79,7 @@ const Game = () => { const handleOpenSaveDirectory = (saveID: string) => OpenSaveDir(saveID, gameID); - const handleOpenGameDirectory = () => OpenGameDir(gameID); + const handleOpenGameDirectory = () => BrowseGameDir(gameID); const handleQuickSave = useCallback(async () => { await addQuickSave({ GameID: gameID }); @@ -163,7 +163,10 @@ const Game = () => {