Skip to content

Commit

Permalink
[romdata] change to modal...
Browse files Browse the repository at this point in the history
* 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: reactjs/react-modal#1054
  • Loading branch information
tonywoode committed Jan 26, 2025
1 parent 0cd9c12 commit 2769679
Show file tree
Hide file tree
Showing 4 changed files with 222 additions and 48 deletions.
235 changes: 189 additions & 46 deletions app/root.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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?)
Expand Down Expand Up @@ -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 = () => (
Expand Down Expand Up @@ -153,13 +175,56 @@ export default function App() {
let match = matches.find(match => 'romdata' in match.data)
const [isSplitLoaded, setIsSplitLoaded] = useState(false)
const actionData = useActionData<typeof action>()
const navigation = useNavigation()
const [modalState, setModalState] = useState<ModalState>({ 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
useEffect(() => {
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 (
<html lang="en">
<head>
Expand All @@ -173,15 +238,93 @@ export default function App() {
<div id="root"></div> {/* Set the app element for react-modal */}
<div className="flex flex-row">
{menu()}
<Form method="post">
<button className="box-border border-2 border-gray-500 px-2 m-3">Convert Original QP Romdata</button>
{actionData && (
<span className={`ml-2 ${actionData.success ? 'text-green-600' : 'text-red-600'}`}>
{actionData.message}
</span>
)}
</Form>
<button
onClick={() => setModalState({ isOpen: true })}
className="box-border border-2 border-gray-500 px-2 m-3"
>
Convert Original QP Romdata
</button>
</div>
<Modal
isOpen={modalState.isOpen}
onRequestClose={() => {
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"
>
<div className="w-[32rem]">
<h2 className="text-xl font-bold mb-4">Converting QuickPlay Data</h2>

{modalState.isConverting ? (
<div className="flex items-center mb-4">
<div className="animate-spin mr-3 h-5 w-5 border-2 border-gray-500 border-t-transparent rounded-full"></div>
<p>Processing...</p>
</div>
) : (
<>
{!modalState.selectedPath ? (
<>
{modalState.result?.success ? (
<div className="mb-4">
<p className="text-green-600 mb-2">{modalState.result.message}</p>
</div>
) : (
<>
<Form method="post">
<input type="hidden" name="intent" value="selectDirectory" />
<button className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
Select QuickPlay Folder
</button>
</Form>
{modalState.error && <p className="mt-4 text-red-600">{modalState.error}</p>}
</>
)}
</>
) : (
<>
<p className="mb-4 text-sm text-gray-600">Selected: {modalState.selectedPath}</p>
<div className="flex gap-2">
{actionData?.existingData ? (
<>
<Form method="post">
<input type="hidden" name="sourcePath" value={modalState.selectedPath} />
<input type="hidden" name="action" value="backup" />
<button className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
Backup & Continue
</button>
</Form>
<Form method="post">
<input type="hidden" name="sourcePath" value={modalState.selectedPath} />
<input type="hidden" name="action" value="overwrite" />
<button className="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600">
Overwrite
</button>
</Form>
</>
) : (
<Form method="post">
<input type="hidden" name="sourcePath" value={modalState.selectedPath} />
<button className="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">
Convert
</button>
</Form>
)}
</div>
</>
)}
<button
onClick={() => setModalState({ isOpen: false })}
className="mt-4 bg-gray-500 text-white px-4 py-2 rounded hover:bg-gray-600"
>
Close
</button>
</>
)}
</div>
</Modal>
{isSplitLoaded && (
<>
<Split sizes={[18, 82]} className="flex overflow-hidden" style={{ height: 'calc(100vh - 7em)' }}>
Expand Down
15 changes: 13 additions & 2 deletions app/utils/convertQuickPlayData.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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,
Expand Down
19 changes: 19 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit 2769679

Please sign in to comment.