From 2769679e55f5250ab8ce3a137bad4968bb435d33 Mon Sep 17 00:00:00 2001 From: tony woode Date: Sun, 26 Jan 2025 18:41:39 +0000 Subject: [PATCH] [romdata] change to modal... * which obv doesn't belong in root.tsx * this better displays progress and choices * also installed react-modal types in dev, but note potential for issues later: https://github.com/reactjs/react-modal/issues/1054 --- app/root.tsx | 235 ++++++++++++++++++----- app/utils/convertQuickPlayData.server.ts | 15 +- package-lock.json | 19 ++ package.json | 1 + 4 files changed, 222 insertions(+), 48 deletions(-) diff --git a/app/root.tsx b/app/root.tsx index b3d6163..b8c1dc2 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -1,12 +1,13 @@ import { cssBundleHref } from '@remix-run/css-bundle' import { json, type LinksFunction, type MetaFunction } from '@remix-run/node' -import { Form, Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration, useLoaderData, useMatches, useActionData } from '@remix-run/react' // prettier-ignore +import { Form, Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration, useLoaderData, useMatches, useActionData, useNavigation } from '@remix-run/react' // prettier-ignore import { useState, useEffect, useRef } from 'react' import electron from '~/electron.server' import reactTabsStyles from 'react-tabs/style/react-tabs.css' import { Menu, MenuItem, MenuButton, SubMenu, MenuDivider } from '@szhsin/react-menu' import { Tree } from 'react-arborist' import Split from 'react-split' +import Modal from 'react-modal' // import { Resizable, ResizableBox } from 'react-resizable' import tailwindStyles from '~/styles/tailwind.css' @@ -17,7 +18,7 @@ import reactMenuTransitionStyles from '@szhsin/react-menu/dist/transitions/slide import { scanFolder } from '~/makeSidebarData.server' import { Node } from '~/components/Node' -import { convertQuickPlayData } from './utils/convertQuickPlayData.server' +import { convertQuickPlayData, validateQuickPlayDirectory } from './utils/convertQuickPlayData.server' //configure and export logging per-domain feature //todo: user-enablable - split out to json/global flag?) @@ -53,53 +54,74 @@ export async function loader() { return json({ folderData, userDataPath: electron.app.getPath('userData') }) } -export const action = async () => { - const result = await electron.dialog.showOpenDialog({ - message: "Select original QuickPlay install folder (should contain 'Data' and 'Dats' dirs)", - properties: ['openDirectory'] - }) +type ModalState = { + isOpen: boolean + selectedPath?: string + isConverting?: boolean + error?: string + result?: { + success: boolean + message: string + } +} + +export const action = async ({ request }: { request: Request }) => { + const formData = await request.formData() + const intent = formData.get('intent') + + if (intent === 'selectDirectory') { + const result = await electron.dialog.showOpenDialog({ + message: "Select original QuickPlay install folder (should contain 'Data' and 'Dats' dirs)", + properties: ['openDirectory'] + }) + + if (result.canceled || !result.filePaths.length) { + return json({ success: false, message: 'No folder selected' }) + } + + const sourcePath = result.filePaths[0] - if (!result.canceled && result.filePaths.length > 0) { try { - const convertedFiles = await convertQuickPlayData(result.filePaths[0], 'data') + await validateQuickPlayDirectory(sourcePath) return json({ success: true, - message: `Successfully converted ${convertedFiles} romdata files` + path: sourcePath, + message: `Selected: ${sourcePath}` }) } catch (error) { - if (error.message === 'EXISTING_DATA') { - const choice = await electron.dialog.showMessageBox({ - type: 'warning', - message: 'Existing data found in destination', - detail: 'How would you like to proceed?', - buttons: ['Cancel', 'Backup existing and continue', 'Continue (overwrite)'], - defaultId: 0, - cancelId: 0 - }) - - if (choice.response === 0) { - return json({ success: false, message: 'Operation cancelled' }) - } - - const backupChoice = choice.response === 1 ? 'backup' : 'overwrite' - const convertedFiles = await convertQuickPlayData(result.filePaths[0], 'data', backupChoice) - - if (!convertedFiles) { - return json({ success: false, message: 'Operation cancelled' }) - } + return json({ + success: false, + error: error.message + }) + } + } - const message = backupChoice === 'backup' - ? `Successfully converted ${convertedFiles} romdata files (backup created)` - : `Successfully converted ${convertedFiles} romdata files` + const sourcePath = formData.get('sourcePath') + const action = formData.get('action') - return json({ success: true, message }) - } + if (!sourcePath) { + return json({ success: false, message: 'No path selected' }) + } - return json({ success: false, message: error.message }, { status: 400 }) + try { + const convertedFiles = await convertQuickPlayData(sourcePath.toString(), 'data', action?.toString() as BackupChoice) + return json({ + success: true, + path: sourcePath, + message: `Successfully converted ${convertedFiles} romdata files`, + complete: true // Add this flag to indicate completion + }) + } catch (error) { + if (error.message === 'EXISTING_DATA') { + return json({ + success: false, + path: sourcePath, + message: 'EXISTING_DATA', + existingData: true + }) } + return json({ success: false, message: error.message }) } - - return json({ success: false, message: 'No folder selected' }) } const menu = () => ( @@ -153,6 +175,10 @@ export default function App() { let match = matches.find(match => 'romdata' in match.data) const [isSplitLoaded, setIsSplitLoaded] = useState(false) const actionData = useActionData() + const navigation = useNavigation() + const [modalState, setModalState] = useState({ isOpen: false }) + + useEffect(() => Modal.setAppElement('#root'), []) // Set the app element for react-modal (else it complains in console about aria) // sets isSplitLoaded after the initial render, to avoid flash of tabs while grid's rendering //TODO: this is causing delay, using react-split-grid might be better https://github.com/nathancahill/split // but see this after trying, which will cause console error https://github.com/nathancahill/split/issues/573 @@ -160,6 +186,45 @@ export default function App() { setIsSplitLoaded(true) }, []) + // Effect to track form submission state + useEffect(() => { + if (navigation.state === 'submitting') { + setModalState(prev => ({ ...prev, isConverting: true })) + } + }, [navigation.state]) + + // Effect to handle action response + useEffect(() => { + if (actionData) { + setModalState(prev => ({ + ...prev, + isConverting: false, + result: { + success: actionData.success, + message: actionData.message + } + })) + } + }, [actionData]) + + // Update modal state based on form submission and response + useEffect(() => { + if (navigation.state === 'submitting') { + setModalState(prev => ({ ...prev, isConverting: true, error: undefined })) + } else if (actionData) { + setModalState(prev => ({ + ...prev, + isConverting: false, + selectedPath: actionData.complete ? undefined : actionData.path, // Reset path on completion + error: actionData.error, + result: { + success: actionData.success, + message: actionData.message + } + })) + } + }, [navigation.state, actionData]) + return ( @@ -173,15 +238,93 @@ export default function App() {
{/* Set the app element for react-modal */}
{menu()} -
- - {actionData && ( - - {actionData.message} - - )} -
+
+ { + if (!modalState.isConverting) { + setModalState({ isOpen: false }) + } + }} + className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-white p-6 rounded-lg shadow-lg" + overlayClassName="fixed inset-0 bg-black/50" + > +
+

Converting QuickPlay Data

+ + {modalState.isConverting ? ( +
+
+

Processing...

+
+ ) : ( + <> + {!modalState.selectedPath ? ( + <> + {modalState.result?.success ? ( +
+

{modalState.result.message}

+
+ ) : ( + <> +
+ + +
+ {modalState.error &&

{modalState.error}

} + + )} + + ) : ( + <> +

Selected: {modalState.selectedPath}

+
+ {actionData?.existingData ? ( + <> +
+ + + +
+
+ + + +
+ + ) : ( +
+ + +
+ )} +
+ + )} + + + )} +
+
{isSplitLoaded && ( <> diff --git a/app/utils/convertQuickPlayData.server.ts b/app/utils/convertQuickPlayData.server.ts index a1935b1..15917b6 100644 --- a/app/utils/convertQuickPlayData.server.ts +++ b/app/utils/convertQuickPlayData.server.ts @@ -8,16 +8,26 @@ import { BackupChoice, handleExistingData } from './safeDirectoryOps.server' // we'll then set the appropriate gamesDrive prefix for the platform, this is optional const gamesDirPathPrefix = 'F:' -export async function convertQuickPlayData(sourcePath: string, destinationPath: string = 'data', choice?: BackupChoice) { +export async function validateQuickPlayDirectory(sourcePath: string): Promise<{ dataFolderPath: string }> { if (!fs.existsSync(sourcePath)) { throw new Error('Source path does not exist') } const dataFolderPath = path.join(sourcePath, 'data') if (!fs.existsSync(dataFolderPath)) { - throw new Error('No QuickPlay data folder found in selected directory') + throw new Error('Selected folder must contain a "data" directory') } + return { dataFolderPath } +} + +export async function convertQuickPlayData( + sourcePath: string, + destinationPath: string = 'data', + choice?: BackupChoice +) { + const { dataFolderPath } = await validateQuickPlayDirectory(sourcePath) + if (fs.existsSync(destinationPath) && fs.readdirSync(destinationPath).length > 0) { if (!choice) { throw new Error('EXISTING_DATA') @@ -29,6 +39,7 @@ export async function convertQuickPlayData(sourcePath: string, destinationPath: fs.mkdirSync(destinationPath, { recursive: true }) return processDirectory(dataFolderPath, destinationPath) } + /** * takes a directory of QuickPlay Frontend's data (preferably QuickPlay Frontend's data folder), * walks the folder tree, creating a mirror directory tree at dest, and converts all romdata.dat files to romdata.json at dest, diff --git a/package-lock.json b/package-lock.json index 1dc99f0..4812a57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,7 @@ "@remix-run/serve": "^2.1.0", "@types/react": "^18.2.31", "@types/react-dom": "^18.2.14", + "@types/react-modal": "^3.16.3", "@types/react-resizable": "^3.0.6", "cross-env": "^7.0.3", "electron": "^27.0.2", @@ -2590,6 +2591,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-modal": { + "version": "3.16.3", + "resolved": "https://registry.npmjs.org/@types/react-modal/-/react-modal-3.16.3.tgz", + "integrity": "sha512-xXuGavyEGaFQDgBv4UVm8/ZsG+qxeQ7f77yNrW3n+1J6XAstUy5rYHeIHPh1KzsGc6IkCIdu6lQ2xWzu1jBTLg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-resizable": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/@types/react-resizable/-/react-resizable-3.0.6.tgz", @@ -16879,6 +16889,15 @@ "@types/react": "*" } }, + "@types/react-modal": { + "version": "3.16.3", + "resolved": "https://registry.npmjs.org/@types/react-modal/-/react-modal-3.16.3.tgz", + "integrity": "sha512-xXuGavyEGaFQDgBv4UVm8/ZsG+qxeQ7f77yNrW3n+1J6XAstUy5rYHeIHPh1KzsGc6IkCIdu6lQ2xWzu1jBTLg==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/react-resizable": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/@types/react-resizable/-/react-resizable-3.0.6.tgz", diff --git a/package.json b/package.json index ddc521c..6111a3e 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "@remix-run/serve": "^2.1.0", "@types/react": "^18.2.31", "@types/react-dom": "^18.2.14", + "@types/react-modal": "^3.16.3", "@types/react-resizable": "^3.0.6", "cross-env": "^7.0.3", "electron": "^27.0.2",