emoemu 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +77 -0
- package/.node-version +1 -0
- package/CLAUDE.md +435 -0
- package/README.md +404 -0
- package/TODO.md +655 -0
- package/dist/index.cjs +25108 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +25085 -0
- package/docs/building-libretro-cores-arm-mac.md +237 -0
- package/docs/config-file-format.md +488 -0
- package/docs/cores-trd.md +425 -0
- package/docs/headless-hardware-rendering-trd.md +676 -0
- package/docs/libretro-cores-trd.md +997 -0
- package/docs/mupen64-software-rendering-trd.md +751 -0
- package/docs/n64-support-trd.md +306 -0
- package/docs/native-rendering-trd.md +540 -0
- package/docs/native-ui-rendering-trd.md +1195 -0
- package/docs/netplay-trd.md +665 -0
- package/docs/retroarch-netplay-docs.md +277 -0
- package/docs/save-state-format.md +666 -0
- package/eslint.config.js +111 -0
- package/icon/icon.png +0 -0
- package/icon/icon.pxd +0 -0
- package/package.json +63 -0
- package/pnpm-workspace.yaml +10 -0
- package/src/Emulator/consts.ts +14 -0
- package/src/Emulator/index.ts +2496 -0
- package/src/Emulator/saveState/index.ts +155 -0
- package/src/Emulator/screenshot/index.ts +160 -0
- package/src/Emulator/terminalDimensions/index.ts +79 -0
- package/src/Emulator/types.ts +83 -0
- package/src/cli/commands/consts.ts +10 -0
- package/src/cli/commands/index.ts +462 -0
- package/src/cli/parseArgs/consts.ts +17 -0
- package/src/cli/parseArgs/index.ts +457 -0
- package/src/cli/parseArgs/types.ts +61 -0
- package/src/cli/runEmulator/index.ts +406 -0
- package/src/cli/runEmulator/types.ts +7 -0
- package/src/consts.ts +19 -0
- package/src/core/button/consts.ts +35 -0
- package/src/core/button/index.ts +123 -0
- package/src/core/core.ts +300 -0
- package/src/core/index.ts +19 -0
- package/src/cores/libretro/api/index.ts +334 -0
- package/src/cores/libretro/api/types.ts +148 -0
- package/src/cores/libretro/callbacks/consts.ts +41 -0
- package/src/cores/libretro/callbacks/index.ts +456 -0
- package/src/cores/libretro/consts.ts +45 -0
- package/src/cores/libretro/coreOptions/consts.ts +36 -0
- package/src/cores/libretro/coreOptions/index.ts +222 -0
- package/src/cores/libretro/environment/consts.ts +118 -0
- package/src/cores/libretro/environment/index.ts +1095 -0
- package/src/cores/libretro/index.ts +937 -0
- package/src/cores/libretro/loader/index.ts +496 -0
- package/src/cores/libretro/pixelFormat/consts.ts +43 -0
- package/src/cores/libretro/pixelFormat/index.ts +397 -0
- package/src/cores/libretro/types.ts +339 -0
- package/src/frontend/AudioManager/index.ts +420 -0
- package/src/frontend/SettingsManager/index.ts +250 -0
- package/src/frontend/config/index.ts +608 -0
- package/src/frontend/config/tests.ts +354 -0
- package/src/frontend/config/types.ts +36 -0
- package/src/frontend/consts.ts +114 -0
- package/src/frontend/coreBuilder/index.ts +644 -0
- package/src/frontend/coreBuilder/types.ts +15 -0
- package/src/frontend/coreDownloader/index.ts +620 -0
- package/src/frontend/coreDownloader/types.ts +17 -0
- package/src/frontend/corePreferences/index.ts +69 -0
- package/src/frontend/coreRegistry/index.ts +276 -0
- package/src/frontend/directoryCache/index.ts +75 -0
- package/src/frontend/index.ts +79 -0
- package/src/frontend/notifications/consts.ts +14 -0
- package/src/frontend/notifications/index.ts +250 -0
- package/src/frontend/playlist/consts.ts +168 -0
- package/src/frontend/playlist/index.ts +899 -0
- package/src/frontend/playlist/labelFormatter/consts.ts +15 -0
- package/src/frontend/playlist/labelFormatter/index.ts +155 -0
- package/src/frontend/playlist/labelFormatter/tests.ts +153 -0
- package/src/frontend/playlist/reader/index.ts +559 -0
- package/src/frontend/playlist/sync/index.ts +511 -0
- package/src/frontend/playlist/systemLookup/index.ts +233 -0
- package/src/frontend/playlist/utils/index.ts +50 -0
- package/src/frontend/romScanner/consts.ts +348 -0
- package/src/frontend/romScanner/index.ts +1957 -0
- package/src/frontend/saveServices/consts.ts +2 -0
- package/src/frontend/saveServices/index.ts +313 -0
- package/src/frontend/serviceProvider/index.ts +108 -0
- package/src/frontend/serviceProvider/types.ts +13 -0
- package/src/index.ts +428 -0
- package/src/input/Controller/consts.ts +50 -0
- package/src/input/Controller/index.ts +81 -0
- package/src/input/GamepadManager/consts.ts +22 -0
- package/src/input/GamepadManager/index.ts +418 -0
- package/src/input/InputManager/consts.ts +86 -0
- package/src/input/InputManager/index.ts +593 -0
- package/src/input/InputMapper/consts.ts +33 -0
- package/src/input/InputMapper/index.ts +436 -0
- package/src/input/consts.ts +410 -0
- package/src/input/gamepadProfiles/index.ts +1100 -0
- package/src/input/index.ts +38 -0
- package/src/input/inputUtils/index.ts +31 -0
- package/src/netplay/FrameBuffer/consts.ts +2 -0
- package/src/netplay/FrameBuffer/index.ts +364 -0
- package/src/netplay/FrameBuffer/tests.ts +286 -0
- package/src/netplay/InputBuffer/consts.ts +7 -0
- package/src/netplay/InputBuffer/index.ts +347 -0
- package/src/netplay/InputBuffer/tests.ts +166 -0
- package/src/netplay/NetplayClient/index.ts +976 -0
- package/src/netplay/NetplayConnection/index.ts +536 -0
- package/src/netplay/NetplayDiscovery/consts.ts +41 -0
- package/src/netplay/NetplayDiscovery/index.ts +525 -0
- package/src/netplay/NetplayServer/index.ts +1407 -0
- package/src/netplay/SyncManager/index.ts +984 -0
- package/src/netplay/SyncManager/tests.ts +419 -0
- package/src/netplay/consts.ts +371 -0
- package/src/netplay/crc32/consts.ts +14 -0
- package/src/netplay/crc32/index.ts +97 -0
- package/src/netplay/crc32/tests.ts +40 -0
- package/src/netplay/index.ts +41 -0
- package/src/netplay/netplayLogger/consts.ts +30 -0
- package/src/netplay/netplayLogger/index.ts +345 -0
- package/src/netplay/protocol/consts.ts +86 -0
- package/src/netplay/protocol/index.ts +1280 -0
- package/src/netplay/protocol/tests.ts +606 -0
- package/src/netplay/protocol/types.ts +20 -0
- package/src/netplay/types.ts +395 -0
- package/src/rendering/KittyRenderer/index.ts +616 -0
- package/src/rendering/NativeRenderer/index.ts +279 -0
- package/src/rendering/NativeRenderer/tests.ts +133 -0
- package/src/rendering/TerminalRenderer/index.ts +770 -0
- package/src/rendering/consts.ts +401 -0
- package/src/rendering/fonts/CozetteVector.ttf +0 -0
- package/src/rendering/index.ts +26 -0
- package/src/rendering/nativeUi/NativeWindowManager/index.ts +158 -0
- package/src/rendering/nativeUi/NativeWindowManager/tests.ts +81 -0
- package/src/rendering/nativeUi/consts.ts +6 -0
- package/src/rendering/nativeUi/index.ts +20 -0
- package/src/rendering/postProcessing/consts.ts +38 -0
- package/src/rendering/postProcessing/index.ts +923 -0
- package/src/rendering/shared/ansi/consts.ts +12 -0
- package/src/rendering/shared/ansi/index.ts +104 -0
- package/src/rendering/shared/consts.ts +2 -0
- package/src/rendering/shared/fitToTerminal/index.ts +67 -0
- package/src/ui/AddRomsPrompt/consts.ts +13 -0
- package/src/ui/AddRomsPrompt/index.tsx +781 -0
- package/src/ui/App/consts.ts +2 -0
- package/src/ui/App/index.tsx +456 -0
- package/src/ui/AppCapabilities/index.tsx +67 -0
- package/src/ui/ConfigContext/index.tsx +56 -0
- package/src/ui/CoreManager/consts.ts +11 -0
- package/src/ui/CoreManager/index.tsx +779 -0
- package/src/ui/CoreSelector/consts.ts +2 -0
- package/src/ui/CoreSelector/index.tsx +251 -0
- package/src/ui/DialogContainer/index.tsx +42 -0
- package/src/ui/DialogOptionsList/index.tsx +61 -0
- package/src/ui/DuplicateCrcPrompt/consts.ts +5 -0
- package/src/ui/DuplicateCrcPrompt/index.tsx +146 -0
- package/src/ui/GamepadContext/consts.ts +15 -0
- package/src/ui/GamepadContext/index.tsx +295 -0
- package/src/ui/NativeDialog/index.tsx +120 -0
- package/src/ui/NetplayDisconnectedDialog/index.tsx +93 -0
- package/src/ui/NetplayPauseMenu/consts.ts +2 -0
- package/src/ui/NetplayPauseMenu/index.tsx +97 -0
- package/src/ui/RomBrowser/NetplayPanel/consts.ts +24 -0
- package/src/ui/RomBrowser/NetplayPanel/index.tsx +520 -0
- package/src/ui/RomBrowser/SettingsPanel/index.tsx +478 -0
- package/src/ui/RomBrowser/consts.ts +61 -0
- package/src/ui/RomBrowser/index.tsx +1164 -0
- package/src/ui/RomBrowser/settingsConfig/index.ts +320 -0
- package/src/ui/RomBrowser/types.ts +67 -0
- package/src/ui/SaveStateDialog/consts.ts +2 -0
- package/src/ui/SaveStateDialog/index.tsx +225 -0
- package/src/ui/WarningDialog/index.tsx +113 -0
- package/src/ui/consts.ts +27 -0
- package/src/ui/hooks/useClearTerminal/consts.ts +2 -0
- package/src/ui/hooks/useClearTerminal/index.ts +37 -0
- package/src/ui/hooks/useDialogNavigation/index.ts +99 -0
- package/src/ui/hooks/useGamepad/consts.ts +21 -0
- package/src/ui/hooks/useGamepad/index.ts +194 -0
- package/src/ui/index.ts +27 -0
- package/src/utils/buffer/consts.ts +17 -0
- package/src/utils/buffer/index.ts +129 -0
- package/src/utils/color/consts.ts +58 -0
- package/src/utils/color/index.ts +183 -0
- package/src/utils/compression/consts.ts +50 -0
- package/src/utils/compression/index.ts +101 -0
- package/src/utils/consts.ts +2 -0
- package/src/utils/crc32/consts.ts +22 -0
- package/src/utils/crc32/index.ts +83 -0
- package/src/utils/ensureDirectory/index.ts +10 -0
- package/src/utils/format/consts.ts +8 -0
- package/src/utils/format/index.ts +53 -0
- package/src/utils/getErrorMessage/index.ts +10 -0
- package/src/utils/index.ts +113 -0
- package/src/utils/ini/index.ts +200 -0
- package/src/utils/kitty/consts.ts +13 -0
- package/src/utils/kitty/index.ts +181 -0
- package/src/utils/logger/consts.ts +35 -0
- package/src/utils/logger/index.ts +217 -0
- package/src/utils/paths/consts.ts +18 -0
- package/src/utils/paths/index.ts +151 -0
- package/src/utils/png/consts.ts +34 -0
- package/src/utils/png/index.ts +131 -0
- package/src/utils/readJsonFile/index.ts +16 -0
- package/src/utils/rotateLogFile/index.ts +44 -0
- package/src/utils/safeClose/index.ts +10 -0
- package/src/utils/terminal/consts.ts +8 -0
- package/src/utils/terminal/index.ts +102 -0
- package/src/utils/thumbnailRenderer/consts.ts +2 -0
- package/src/utils/thumbnailRenderer/index.ts +147 -0
- package/src/utils/typedError/index.ts +26 -0
- package/tsconfig.json +31 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,781 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Add ROMs Prompt Component
|
|
3
|
+
*
|
|
4
|
+
* Shared component for adding ROMs to the library. Used both when no playlists
|
|
5
|
+
* exist (initial setup) and from the ROM browser's "Add ROMs" action.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState, useCallback, useRef, useMemo, useEffect } from 'react';
|
|
9
|
+
import { Box, Text, useInput, useApp } from 'ink';
|
|
10
|
+
import { Spinner, ProgressBar, TextInput } from '@inkjs/ui';
|
|
11
|
+
import { readdirSync, statSync } from 'fs';
|
|
12
|
+
import { resolve, dirname, basename, join } from 'path';
|
|
13
|
+
import { ScanCancelledError, validateRomFile } from '../../frontend/romScanner';
|
|
14
|
+
import { getSupportedSystems, getSupportedExtensions } from '../../frontend/coreRegistry';
|
|
15
|
+
import type { ScanProgress } from '../../frontend/romScanner';
|
|
16
|
+
import { useGamepadContext } from '../GamepadContext';
|
|
17
|
+
import {
|
|
18
|
+
findPlaylistsForDirectory,
|
|
19
|
+
generatePlaylistsBySystem,
|
|
20
|
+
buildPlaylistIndex,
|
|
21
|
+
normalizePath,
|
|
22
|
+
analyzePlaylistSync,
|
|
23
|
+
syncPlaylists,
|
|
24
|
+
resolvePath,
|
|
25
|
+
} from '../../frontend/playlist';
|
|
26
|
+
import type { PlaylistInfo, DuplicateDecision } from '../../frontend/playlist';
|
|
27
|
+
import { showDuplicateCrcPrompt } from '../DuplicateCrcPrompt';
|
|
28
|
+
import {
|
|
29
|
+
PROGRESS_FULL,
|
|
30
|
+
PROGRESS_MULTIPLIER,
|
|
31
|
+
MAX_PATH_SUGGESTIONS,
|
|
32
|
+
MENU_DIRECTORY,
|
|
33
|
+
MENU_IMPORT,
|
|
34
|
+
MENU_EXIT,
|
|
35
|
+
} from './consts';
|
|
36
|
+
import { getErrorMessage } from '../../utils/getErrorMessage';
|
|
37
|
+
|
|
38
|
+
export * from './consts';
|
|
39
|
+
|
|
40
|
+
export interface AddRomsPromptProps {
|
|
41
|
+
directory: string;
|
|
42
|
+
playlistDirectory: string;
|
|
43
|
+
scanDepth: number;
|
|
44
|
+
onPlaylistGenerated: (playlists: PlaylistInfo[]) => void;
|
|
45
|
+
onExit: () => void;
|
|
46
|
+
/** If true, automatically start importing without showing the prompt UI */
|
|
47
|
+
autoImport?: boolean;
|
|
48
|
+
/** If true, exit the entire app when cancel is pressed (default: false) */
|
|
49
|
+
exitAppOnCancel?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** System breakdown for import results */
|
|
53
|
+
interface SystemCount {
|
|
54
|
+
system: string;
|
|
55
|
+
count: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get path suggestions based on current input path (directories and ROM files)
|
|
60
|
+
*/
|
|
61
|
+
const getPathSuggestions = (inputPath: string): string[] => {
|
|
62
|
+
const supportedExtensions = new Set(getSupportedExtensions());
|
|
63
|
+
|
|
64
|
+
const isRomFile = (entry: string): boolean => {
|
|
65
|
+
const ext = '.' + entry.split('.').pop()?.toLowerCase();
|
|
66
|
+
return supportedExtensions.has(ext);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const isValidEntry = (fullPath: string): boolean => {
|
|
70
|
+
try {
|
|
71
|
+
const stat = statSync(fullPath);
|
|
72
|
+
if (stat.isDirectory()) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
// Only suggest ROM files
|
|
76
|
+
return stat.isFile() && isRomFile(fullPath);
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const resolvedPath = resolve(inputPath);
|
|
84
|
+
|
|
85
|
+
// Check if the input path itself is a directory
|
|
86
|
+
try {
|
|
87
|
+
const stat = statSync(resolvedPath);
|
|
88
|
+
if (stat.isDirectory()) {
|
|
89
|
+
// List contents of this directory (directories first, then ROM files)
|
|
90
|
+
const entries = readdirSync(resolvedPath);
|
|
91
|
+
const dirs: string[] = [];
|
|
92
|
+
const files: string[] = [];
|
|
93
|
+
|
|
94
|
+
for (const entry of entries) {
|
|
95
|
+
if (entry.startsWith('.')) {continue;} // Hide hidden entries
|
|
96
|
+
const fullPath = join(resolvedPath, entry);
|
|
97
|
+
try {
|
|
98
|
+
const entryStat = statSync(fullPath);
|
|
99
|
+
if (entryStat.isDirectory()) {
|
|
100
|
+
dirs.push(fullPath);
|
|
101
|
+
} else if (entryStat.isFile() && isRomFile(entry)) {
|
|
102
|
+
files.push(fullPath);
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
// Skip inaccessible entries
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Return directories first, then ROM files
|
|
110
|
+
return [...dirs, ...files].slice(0, MAX_PATH_SUGGESTIONS);
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
// Path doesn't exist as-is, try parent directory
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Get the parent directory and filter by basename prefix
|
|
117
|
+
const parentDir = dirname(resolvedPath);
|
|
118
|
+
const prefix = basename(resolvedPath).toLowerCase();
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const entries = readdirSync(parentDir);
|
|
122
|
+
const dirs: string[] = [];
|
|
123
|
+
const files: string[] = [];
|
|
124
|
+
|
|
125
|
+
for (const entry of entries) {
|
|
126
|
+
if (entry.startsWith('.')) {continue;} // Hide hidden entries
|
|
127
|
+
if (!entry.toLowerCase().startsWith(prefix)) {continue;}
|
|
128
|
+
const fullPath = join(parentDir, entry);
|
|
129
|
+
if (isValidEntry(fullPath)) {
|
|
130
|
+
try {
|
|
131
|
+
const stat = statSync(fullPath);
|
|
132
|
+
if (stat.isDirectory()) {
|
|
133
|
+
dirs.push(fullPath);
|
|
134
|
+
} else {
|
|
135
|
+
files.push(fullPath);
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
// Skip
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return [...dirs, ...files].slice(0, MAX_PATH_SUGGESTIONS);
|
|
144
|
+
} catch {
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
} catch {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/** Result of the import operation */
|
|
153
|
+
interface ImportResult {
|
|
154
|
+
totalFiles: number;
|
|
155
|
+
romsFound: number;
|
|
156
|
+
romsAdded: number;
|
|
157
|
+
alreadyInLibrary: number;
|
|
158
|
+
filesSkipped: number;
|
|
159
|
+
/** Number of playlist entries removed (files no longer exist) */
|
|
160
|
+
removed: number;
|
|
161
|
+
/** Number of entries updated (moved ROMs with path changes) */
|
|
162
|
+
moved: number;
|
|
163
|
+
/** Number of duplicate entries where path was updated */
|
|
164
|
+
duplicatesUpdated: number;
|
|
165
|
+
/** Number of duplicate entries that were skipped */
|
|
166
|
+
duplicatesSkipped: number;
|
|
167
|
+
playlists: PlaylistInfo[];
|
|
168
|
+
systems: SystemCount[];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export const AddRomsPrompt = ({
|
|
172
|
+
directory,
|
|
173
|
+
playlistDirectory,
|
|
174
|
+
scanDepth,
|
|
175
|
+
onPlaylistGenerated,
|
|
176
|
+
onExit,
|
|
177
|
+
autoImport = false,
|
|
178
|
+
exitAppOnCancel = false,
|
|
179
|
+
}: AddRomsPromptProps) => {
|
|
180
|
+
const { exit } = useApp();
|
|
181
|
+
const [selectedPath, setSelectedDirectory] = useState(directory);
|
|
182
|
+
const [inputKey, setInputKey] = useState(0); // Key to force TextInput re-render
|
|
183
|
+
const [hasTyped, setHasTyped] = useState(false); // Track if user has typed in current session
|
|
184
|
+
const [selectedIndex, setSelectedIndex] = useState(MENU_IMPORT); // Default to Add to Library
|
|
185
|
+
const [isGenerating, setIsGenerating] = useState(false);
|
|
186
|
+
const [progress, setProgress] = useState<ScanProgress | null>(null);
|
|
187
|
+
const [importResult, setImportResult] = useState<ImportResult | null>(null);
|
|
188
|
+
const [error, setError] = useState<string | null>(null);
|
|
189
|
+
const abortControllerRef = useRef<AbortController | null>(null);
|
|
190
|
+
const progressRef = useRef<ScanProgress | null>(null);
|
|
191
|
+
|
|
192
|
+
// Get path suggestions based on current input (only if user has typed)
|
|
193
|
+
const pathSuggestions = useMemo(
|
|
194
|
+
() => hasTyped ? getPathSuggestions(selectedPath) : [],
|
|
195
|
+
[selectedPath, hasTyped]
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
// Handle directory input changes
|
|
199
|
+
const handlePathChange = useCallback((value: string) => {
|
|
200
|
+
setSelectedDirectory(value);
|
|
201
|
+
setHasTyped(true);
|
|
202
|
+
}, []);
|
|
203
|
+
|
|
204
|
+
const isEditingPath = selectedIndex === MENU_DIRECTORY;
|
|
205
|
+
|
|
206
|
+
const cancelImport = useCallback(() => {
|
|
207
|
+
if (abortControllerRef.current) {
|
|
208
|
+
abortControllerRef.current.abort();
|
|
209
|
+
abortControllerRef.current = null;
|
|
210
|
+
}
|
|
211
|
+
}, []);
|
|
212
|
+
|
|
213
|
+
const handleGenerate = useCallback(async () => {
|
|
214
|
+
const targetPath = resolve(selectedPath);
|
|
215
|
+
|
|
216
|
+
// Check if path exists
|
|
217
|
+
let pathStat: ReturnType<typeof statSync>;
|
|
218
|
+
try {
|
|
219
|
+
pathStat = statSync(targetPath);
|
|
220
|
+
} catch {
|
|
221
|
+
setError('Path does not exist');
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
setIsGenerating(true);
|
|
226
|
+
setError(null);
|
|
227
|
+
setProgress(null);
|
|
228
|
+
setImportResult(null);
|
|
229
|
+
|
|
230
|
+
// Build playlist index to check for existing ROMs
|
|
231
|
+
const playlistIndex = buildPlaylistIndex(playlistDirectory);
|
|
232
|
+
|
|
233
|
+
// Handle single file
|
|
234
|
+
if (pathStat.isFile()) {
|
|
235
|
+
const result = validateRomFile(targetPath);
|
|
236
|
+
if (!result.valid) {
|
|
237
|
+
setError(result.message);
|
|
238
|
+
setIsGenerating(false);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const rom = result.rom;
|
|
243
|
+
|
|
244
|
+
// Check if ROM is already in library
|
|
245
|
+
const normalizedPath = normalizePath(targetPath);
|
|
246
|
+
if (playlistIndex.has(normalizedPath)) {
|
|
247
|
+
// ROM already in library - show success with 0 added
|
|
248
|
+
const playlists = findPlaylistsForDirectory(dirname(targetPath), playlistDirectory);
|
|
249
|
+
setImportResult({
|
|
250
|
+
totalFiles: 1,
|
|
251
|
+
romsFound: 1,
|
|
252
|
+
romsAdded: 0,
|
|
253
|
+
alreadyInLibrary: 1,
|
|
254
|
+
filesSkipped: 0,
|
|
255
|
+
removed: 0,
|
|
256
|
+
moved: 0,
|
|
257
|
+
duplicatesUpdated: 0,
|
|
258
|
+
duplicatesSkipped: 0,
|
|
259
|
+
playlists,
|
|
260
|
+
systems: [{ system: rom.system, count: 1 }],
|
|
261
|
+
});
|
|
262
|
+
setIsGenerating(false);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Add single ROM to playlist
|
|
267
|
+
const results = generatePlaylistsBySystem([rom], playlistDirectory);
|
|
268
|
+
|
|
269
|
+
const failures = results.filter(r => !r.success);
|
|
270
|
+
if (failures.length > 0) {
|
|
271
|
+
setError(`Failed to add ROM to library: ${failures[0].error}`);
|
|
272
|
+
setIsGenerating(false);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Find playlists for the ROM's directory
|
|
277
|
+
const playlists = findPlaylistsForDirectory(dirname(targetPath), playlistDirectory);
|
|
278
|
+
|
|
279
|
+
setImportResult({
|
|
280
|
+
totalFiles: 1,
|
|
281
|
+
romsFound: 1,
|
|
282
|
+
romsAdded: 1,
|
|
283
|
+
alreadyInLibrary: 0,
|
|
284
|
+
filesSkipped: 0,
|
|
285
|
+
removed: 0,
|
|
286
|
+
moved: 0,
|
|
287
|
+
duplicatesUpdated: 0,
|
|
288
|
+
duplicatesSkipped: 0,
|
|
289
|
+
playlists,
|
|
290
|
+
systems: [{ system: rom.system, count: 1 }],
|
|
291
|
+
});
|
|
292
|
+
setIsGenerating(false);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Handle directory
|
|
297
|
+
if (!pathStat.isDirectory()) {
|
|
298
|
+
setError('Path is not a file or directory');
|
|
299
|
+
setIsGenerating(false);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Create new abort controller for this scan
|
|
304
|
+
abortControllerRef.current = new AbortController();
|
|
305
|
+
const { signal } = abortControllerRef.current;
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
// Analyze the directory for sync needs (new ROMs, missing entries, moved files)
|
|
309
|
+
const analysis = await analyzePlaylistSync(
|
|
310
|
+
targetPath,
|
|
311
|
+
playlistDirectory,
|
|
312
|
+
scanDepth,
|
|
313
|
+
(scanProgress) => {
|
|
314
|
+
progressRef.current = scanProgress;
|
|
315
|
+
setProgress(scanProgress);
|
|
316
|
+
},
|
|
317
|
+
signal
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
// Calculate totals from ref (state may not be updated yet due to React batching)
|
|
321
|
+
const finalProgress = progressRef.current;
|
|
322
|
+
const totalFiles = finalProgress?.total ?? 0;
|
|
323
|
+
const romsFound = finalProgress?.romsFound ?? 0;
|
|
324
|
+
const alreadyInLibrary = romsFound - analysis.newRoms.length - analysis.movedRoms.length;
|
|
325
|
+
|
|
326
|
+
// Check if there are no ROMs at all and nothing in playlists to clean up
|
|
327
|
+
if (romsFound === 0 && analysis.missingEntries.length === 0 && analysis.newRoms.length === 0) {
|
|
328
|
+
setError('No supported ROMs found in this directory');
|
|
329
|
+
setIsGenerating(false);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// If nothing needs to sync, show "all in library" message
|
|
334
|
+
if (!analysis.needsSync) {
|
|
335
|
+
const playlists = findPlaylistsForDirectory(targetPath, playlistDirectory);
|
|
336
|
+
|
|
337
|
+
// For "all in library" case, we don't show system breakdown since we only have counts
|
|
338
|
+
const systems: SystemCount[] = [];
|
|
339
|
+
|
|
340
|
+
setImportResult({
|
|
341
|
+
totalFiles,
|
|
342
|
+
romsFound,
|
|
343
|
+
romsAdded: 0,
|
|
344
|
+
alreadyInLibrary,
|
|
345
|
+
filesSkipped: totalFiles - romsFound,
|
|
346
|
+
removed: 0,
|
|
347
|
+
moved: 0,
|
|
348
|
+
duplicatesUpdated: 0,
|
|
349
|
+
duplicatesSkipped: 0,
|
|
350
|
+
playlists,
|
|
351
|
+
systems,
|
|
352
|
+
});
|
|
353
|
+
setIsGenerating(false);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Handle duplicate CRC ROMs by prompting the user
|
|
358
|
+
const duplicateDecisions: DuplicateDecision[] = [];
|
|
359
|
+
if (analysis.duplicateCrcRoms.length > 0) {
|
|
360
|
+
for (const duplicate of analysis.duplicateCrcRoms) {
|
|
361
|
+
const existingPath = resolvePath(
|
|
362
|
+
duplicate.existingEntry.entry.path,
|
|
363
|
+
dirname(duplicate.existingEntry.playlistPath)
|
|
364
|
+
);
|
|
365
|
+
const choice = await showDuplicateCrcPrompt({
|
|
366
|
+
newPath: duplicate.newRom.path,
|
|
367
|
+
existingPath,
|
|
368
|
+
label: duplicate.existingEntry.entry.label,
|
|
369
|
+
crc32: duplicate.crc32,
|
|
370
|
+
});
|
|
371
|
+
duplicateDecisions.push({ duplicate, choice });
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Apply the sync changes (add new, remove missing, update moved, handle duplicates)
|
|
376
|
+
const syncResult = syncPlaylists(analysis, targetPath, playlistDirectory, {}, duplicateDecisions);
|
|
377
|
+
|
|
378
|
+
if (!syncResult.success && syncResult.errors.length > 0) {
|
|
379
|
+
setError(`Sync completed with errors: ${syncResult.errors[0]}`);
|
|
380
|
+
// Continue to show results even with errors
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Find the updated playlists
|
|
384
|
+
const playlists = findPlaylistsForDirectory(targetPath, playlistDirectory);
|
|
385
|
+
|
|
386
|
+
// Count new ROMs by system
|
|
387
|
+
const systemMap = new Map<string, number>();
|
|
388
|
+
for (const rom of analysis.newRoms) {
|
|
389
|
+
const count = systemMap.get(rom.system) ?? 0;
|
|
390
|
+
systemMap.set(rom.system, count + 1);
|
|
391
|
+
}
|
|
392
|
+
const systems: SystemCount[] = Array.from(systemMap.entries())
|
|
393
|
+
.map(([system, count]) => ({ system, count }))
|
|
394
|
+
.sort((a, b) => b.count - a.count);
|
|
395
|
+
|
|
396
|
+
// Show completion summary
|
|
397
|
+
setImportResult({
|
|
398
|
+
totalFiles,
|
|
399
|
+
romsFound,
|
|
400
|
+
romsAdded: syncResult.added,
|
|
401
|
+
alreadyInLibrary,
|
|
402
|
+
filesSkipped: totalFiles - romsFound,
|
|
403
|
+
removed: syncResult.removed,
|
|
404
|
+
moved: syncResult.moved,
|
|
405
|
+
duplicatesUpdated: syncResult.duplicatesUpdated,
|
|
406
|
+
duplicatesSkipped: syncResult.duplicatesSkipped,
|
|
407
|
+
playlists,
|
|
408
|
+
systems,
|
|
409
|
+
});
|
|
410
|
+
setIsGenerating(false);
|
|
411
|
+
} catch (err) {
|
|
412
|
+
// If cancelled, just return to prompt without error
|
|
413
|
+
if (err instanceof ScanCancelledError) {
|
|
414
|
+
setIsGenerating(false);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
setError(getErrorMessage(err));
|
|
418
|
+
setIsGenerating(false);
|
|
419
|
+
}
|
|
420
|
+
}, [selectedPath, scanDepth, playlistDirectory]);
|
|
421
|
+
|
|
422
|
+
// Auto-trigger import when autoImport is true (CLI path provided)
|
|
423
|
+
useEffect(() => {
|
|
424
|
+
if (autoImport && !isGenerating && !importResult && !error) {
|
|
425
|
+
void handleGenerate();
|
|
426
|
+
}
|
|
427
|
+
}, [autoImport, handleGenerate, isGenerating, importResult, error]);
|
|
428
|
+
|
|
429
|
+
// Auto-continue when autoImport is true and import completes (skip summary screen)
|
|
430
|
+
useEffect(() => {
|
|
431
|
+
if (autoImport && importResult) {
|
|
432
|
+
onPlaylistGenerated(importResult.playlists);
|
|
433
|
+
exit(); // Close the Ink app so importDirectory can continue
|
|
434
|
+
}
|
|
435
|
+
}, [autoImport, importResult, onPlaylistGenerated, exit]);
|
|
436
|
+
|
|
437
|
+
useInput((input, key) => {
|
|
438
|
+
// Allow cancellation during import
|
|
439
|
+
if (isGenerating) {
|
|
440
|
+
if (key.escape) {
|
|
441
|
+
cancelImport();
|
|
442
|
+
}
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Handle completion screen - any key to continue
|
|
447
|
+
if (importResult) {
|
|
448
|
+
onPlaylistGenerated(importResult.playlists);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// When editing path, only handle navigation keys
|
|
453
|
+
if (isEditingPath) {
|
|
454
|
+
// Tab or Right arrow accepts the first suggestion
|
|
455
|
+
if (key.tab || key.rightArrow) {
|
|
456
|
+
if (pathSuggestions.length > 0) {
|
|
457
|
+
setSelectedDirectory(pathSuggestions[0]);
|
|
458
|
+
setInputKey(prev => prev + 1); // Force TextInput re-render
|
|
459
|
+
}
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
// Enter accepts the user-typed value (not autocomplete) and moves to Import
|
|
463
|
+
if (key.return) {
|
|
464
|
+
setSelectedIndex(MENU_IMPORT);
|
|
465
|
+
setHasTyped(false);
|
|
466
|
+
setInputKey(prev => prev + 1); // Force TextInput re-render to clear suggestion highlight
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (key.downArrow) {
|
|
470
|
+
setSelectedIndex(MENU_IMPORT);
|
|
471
|
+
setHasTyped(false);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (key.escape) {
|
|
475
|
+
setSelectedIndex(MENU_IMPORT);
|
|
476
|
+
setHasTyped(false);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
// Let TextInput handle all other input
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (key.escape) {
|
|
484
|
+
onExit();
|
|
485
|
+
if (exitAppOnCancel) {
|
|
486
|
+
exit();
|
|
487
|
+
}
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (key.upArrow) {
|
|
492
|
+
setSelectedIndex(prev => Math.max(MENU_DIRECTORY, prev - 1));
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (key.downArrow) {
|
|
497
|
+
setSelectedIndex(prev => Math.min(MENU_EXIT, prev + 1));
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (key.return || input === ' ') {
|
|
502
|
+
if (selectedIndex === MENU_IMPORT) {
|
|
503
|
+
void handleGenerate();
|
|
504
|
+
} else if (selectedIndex === MENU_EXIT) {
|
|
505
|
+
onExit();
|
|
506
|
+
if (exitAppOnCancel) {
|
|
507
|
+
exit();
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
useGamepadContext({
|
|
514
|
+
onUp: () => {
|
|
515
|
+
if (isGenerating || importResult || isEditingPath) {return;}
|
|
516
|
+
setSelectedIndex(prev => Math.max(MENU_DIRECTORY, prev - 1));
|
|
517
|
+
},
|
|
518
|
+
onDown: () => {
|
|
519
|
+
if (isGenerating || importResult) {return;}
|
|
520
|
+
if (isEditingPath) {
|
|
521
|
+
setSelectedIndex(MENU_IMPORT);
|
|
522
|
+
setHasTyped(false);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
setSelectedIndex(prev => Math.min(MENU_EXIT, prev + 1));
|
|
526
|
+
},
|
|
527
|
+
onConfirm: () => {
|
|
528
|
+
if (isGenerating || isEditingPath) {return;}
|
|
529
|
+
if (importResult) {
|
|
530
|
+
onPlaylistGenerated(importResult.playlists);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
if (selectedIndex === MENU_IMPORT) {
|
|
534
|
+
void handleGenerate();
|
|
535
|
+
} else if (selectedIndex === MENU_EXIT) {
|
|
536
|
+
onExit();
|
|
537
|
+
if (exitAppOnCancel) {
|
|
538
|
+
exit();
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
},
|
|
542
|
+
onCancel: () => {
|
|
543
|
+
// Allow cancellation during import
|
|
544
|
+
if (isGenerating) {
|
|
545
|
+
cancelImport();
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
// Exit path editing mode
|
|
549
|
+
if (isEditingPath) {
|
|
550
|
+
setSelectedIndex(MENU_IMPORT);
|
|
551
|
+
setHasTyped(false);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
if (importResult) {
|
|
555
|
+
onPlaylistGenerated(importResult.playlists);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
onExit();
|
|
559
|
+
if (exitAppOnCancel) {
|
|
560
|
+
exit();
|
|
561
|
+
}
|
|
562
|
+
},
|
|
563
|
+
onStart: () => {
|
|
564
|
+
if (importResult) {
|
|
565
|
+
onPlaylistGenerated(importResult.playlists);
|
|
566
|
+
}
|
|
567
|
+
},
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
if (isGenerating) {
|
|
571
|
+
if (progress) {
|
|
572
|
+
// Show determinate progress when total is known, indeterminate otherwise
|
|
573
|
+
const totalCount = progress.total;
|
|
574
|
+
const hasTotalCount = totalCount !== undefined && totalCount > 0;
|
|
575
|
+
const progressPercent = hasTotalCount
|
|
576
|
+
? Math.round((progress.processed / totalCount) * PROGRESS_MULTIPLIER)
|
|
577
|
+
: undefined;
|
|
578
|
+
|
|
579
|
+
return (
|
|
580
|
+
<Box flexDirection="column" padding={1}>
|
|
581
|
+
<Box marginBottom={1}>
|
|
582
|
+
<Text color="cyan">Looking for ROMs...</Text>
|
|
583
|
+
</Box>
|
|
584
|
+
{progressPercent !== undefined && (
|
|
585
|
+
<Box marginBottom={1}>
|
|
586
|
+
<ProgressBar value={progressPercent} />
|
|
587
|
+
</Box>
|
|
588
|
+
)}
|
|
589
|
+
<Box marginBottom={1}>
|
|
590
|
+
<Text color="gray">
|
|
591
|
+
{hasTotalCount
|
|
592
|
+
? `${progress.processed} of ${totalCount} files checked`
|
|
593
|
+
: `${progress.processed} files checked`}
|
|
594
|
+
{progress.romsFound > 0 && <Text color="green"> ({progress.romsFound} ROMs found)</Text>}
|
|
595
|
+
</Text>
|
|
596
|
+
</Box>
|
|
597
|
+
<Box marginBottom={1}>
|
|
598
|
+
<Text color="gray" dimColor wrap="truncate-end">
|
|
599
|
+
{progress.currentFile}
|
|
600
|
+
</Text>
|
|
601
|
+
</Box>
|
|
602
|
+
<Box>
|
|
603
|
+
<Text color="gray" dimColor>
|
|
604
|
+
Press <Text color="yellow">ESC</Text> to cancel
|
|
605
|
+
</Text>
|
|
606
|
+
</Box>
|
|
607
|
+
</Box>
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
return (
|
|
611
|
+
<Box padding={1}>
|
|
612
|
+
<Spinner label="Preparing..." />
|
|
613
|
+
</Box>
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Show completion summary (skip when autoImport - useEffect will auto-continue)
|
|
618
|
+
if (importResult) {
|
|
619
|
+
// When autoImport is true, skip the summary - the useEffect will call onPlaylistGenerated
|
|
620
|
+
if (autoImport) {
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const noChanges = importResult.romsAdded === 0 && importResult.removed === 0 && importResult.moved === 0 && importResult.duplicatesUpdated === 0;
|
|
625
|
+
const allAlreadyInLibrary = noChanges && importResult.alreadyInLibrary > 0;
|
|
626
|
+
const someAlreadyInLibrary = importResult.romsAdded > 0 && importResult.alreadyInLibrary > 0;
|
|
627
|
+
const hasChanges = importResult.romsAdded > 0 || importResult.removed > 0 || importResult.moved > 0 || importResult.duplicatesUpdated > 0;
|
|
628
|
+
|
|
629
|
+
return (
|
|
630
|
+
<Box flexDirection="column" padding={1}>
|
|
631
|
+
<Box marginBottom={1}>
|
|
632
|
+
{allAlreadyInLibrary ? (
|
|
633
|
+
<Text bold color="cyan">{'\u2714'} All Games in Library</Text>
|
|
634
|
+
) : (
|
|
635
|
+
<Text bold color="green">{'\u2714'} Sync Complete</Text>
|
|
636
|
+
)}
|
|
637
|
+
</Box>
|
|
638
|
+
<Box marginBottom={1}>
|
|
639
|
+
<ProgressBar value={PROGRESS_FULL} />
|
|
640
|
+
</Box>
|
|
641
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
642
|
+
{allAlreadyInLibrary ? (
|
|
643
|
+
<Text color="white">
|
|
644
|
+
All <Text color="cyan" bold>{importResult.alreadyInLibrary}</Text> ROM{importResult.alreadyInLibrary !== 1 ? 's' : ''} already in your library
|
|
645
|
+
</Text>
|
|
646
|
+
) : (
|
|
647
|
+
<>
|
|
648
|
+
{importResult.romsAdded > 0 && (
|
|
649
|
+
<Text color="white">
|
|
650
|
+
<Text color="green" bold>{importResult.romsAdded}</Text> ROM{importResult.romsAdded !== 1 ? 's' : ''} detected
|
|
651
|
+
</Text>
|
|
652
|
+
)}
|
|
653
|
+
{importResult.removed > 0 && (
|
|
654
|
+
<Text color="white">
|
|
655
|
+
<Text color="yellow" bold>{importResult.removed}</Text> missing ROM{importResult.removed !== 1 ? 's' : ''} removed from library
|
|
656
|
+
</Text>
|
|
657
|
+
)}
|
|
658
|
+
{importResult.moved > 0 && (
|
|
659
|
+
<Text color="white">
|
|
660
|
+
<Text color="blue" bold>{importResult.moved}</Text> ROM{importResult.moved !== 1 ? 's' : ''} moved (paths updated)
|
|
661
|
+
</Text>
|
|
662
|
+
)}
|
|
663
|
+
{importResult.duplicatesUpdated > 0 && (
|
|
664
|
+
<Text color="white">
|
|
665
|
+
<Text color="magenta" bold>{importResult.duplicatesUpdated}</Text> duplicate ROM{importResult.duplicatesUpdated !== 1 ? 's' : ''} updated to new path
|
|
666
|
+
</Text>
|
|
667
|
+
)}
|
|
668
|
+
{importResult.duplicatesSkipped > 0 && (
|
|
669
|
+
<Text color="gray">
|
|
670
|
+
{importResult.duplicatesSkipped} duplicate{importResult.duplicatesSkipped !== 1 ? 's' : ''} skipped (kept existing)
|
|
671
|
+
</Text>
|
|
672
|
+
)}
|
|
673
|
+
{someAlreadyInLibrary && (
|
|
674
|
+
<Text color="gray">
|
|
675
|
+
{importResult.alreadyInLibrary} already in library (skipped)
|
|
676
|
+
</Text>
|
|
677
|
+
)}
|
|
678
|
+
</>
|
|
679
|
+
)}
|
|
680
|
+
{importResult.filesSkipped > 0 && (
|
|
681
|
+
<Text color="gray">
|
|
682
|
+
{importResult.filesSkipped} file{importResult.filesSkipped !== 1 ? 's' : ''} skipped (not recognized as ROMs)
|
|
683
|
+
</Text>
|
|
684
|
+
)}
|
|
685
|
+
</Box>
|
|
686
|
+
{importResult.systems.length > 0 && hasChanges && (
|
|
687
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
688
|
+
<Text color="white" bold>ROMs found for:</Text>
|
|
689
|
+
{importResult.systems.map(({ system, count }) => (
|
|
690
|
+
<Text key={system} color="gray">
|
|
691
|
+
{' '}<Text color="cyan">{system}</Text>: {count} ROM{count !== 1 ? 's' : ''}
|
|
692
|
+
</Text>
|
|
693
|
+
))}
|
|
694
|
+
</Box>
|
|
695
|
+
)}
|
|
696
|
+
<Box marginTop={1}>
|
|
697
|
+
<Text color="gray" dimColor>
|
|
698
|
+
Press any key to continue
|
|
699
|
+
</Text>
|
|
700
|
+
</Box>
|
|
701
|
+
</Box>
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
return (
|
|
706
|
+
<Box flexDirection="column" padding={1}>
|
|
707
|
+
<Box marginBottom={1}>
|
|
708
|
+
<Text bold color="cyan">{'\u{1F4C2}'} Add ROMs</Text>
|
|
709
|
+
</Box>
|
|
710
|
+
|
|
711
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
712
|
+
{/* Path input */}
|
|
713
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
714
|
+
<Box>
|
|
715
|
+
<Text color={isEditingPath ? 'cyan' : 'white'} bold={isEditingPath}>
|
|
716
|
+
{isEditingPath ? '\u25B6 ' : ' '}Path:{' '}
|
|
717
|
+
</Text>
|
|
718
|
+
<TextInput
|
|
719
|
+
key={inputKey}
|
|
720
|
+
defaultValue={selectedPath}
|
|
721
|
+
suggestions={pathSuggestions}
|
|
722
|
+
onChange={handlePathChange}
|
|
723
|
+
isDisabled={!isEditingPath}
|
|
724
|
+
/>
|
|
725
|
+
</Box>
|
|
726
|
+
{isEditingPath && (
|
|
727
|
+
<Text color="gray" dimColor> Path to ROM file or directory</Text>
|
|
728
|
+
)}
|
|
729
|
+
</Box>
|
|
730
|
+
|
|
731
|
+
{/* Add to Library option */}
|
|
732
|
+
<Box flexDirection="column">
|
|
733
|
+
<Text
|
|
734
|
+
color={selectedIndex === MENU_IMPORT ? 'cyan' : 'white'}
|
|
735
|
+
bold={selectedIndex === MENU_IMPORT}
|
|
736
|
+
>
|
|
737
|
+
{selectedIndex === MENU_IMPORT ? '\u25B6 ' : ' '}Add to Library
|
|
738
|
+
</Text>
|
|
739
|
+
{selectedIndex === MENU_IMPORT && (
|
|
740
|
+
<Text color="gray" dimColor> Your ROM files will stay where they are</Text>
|
|
741
|
+
)}
|
|
742
|
+
</Box>
|
|
743
|
+
|
|
744
|
+
{/* Exit option */}
|
|
745
|
+
<Box flexDirection="column">
|
|
746
|
+
<Text
|
|
747
|
+
color={selectedIndex === MENU_EXIT ? 'red' : 'red'}
|
|
748
|
+
bold={selectedIndex === MENU_EXIT}
|
|
749
|
+
>
|
|
750
|
+
{selectedIndex === MENU_EXIT ? '\u25B6 ' : ' '}Cancel
|
|
751
|
+
</Text>
|
|
752
|
+
{selectedIndex === MENU_EXIT && (
|
|
753
|
+
<Text color="gray" dimColor> Return to ROM browser</Text>
|
|
754
|
+
)}
|
|
755
|
+
</Box>
|
|
756
|
+
</Box>
|
|
757
|
+
|
|
758
|
+
<Box marginBottom={1}>
|
|
759
|
+
<Text color="gray" dimColor>
|
|
760
|
+
Supported: {getSupportedSystems().join(', ')}
|
|
761
|
+
</Text>
|
|
762
|
+
</Box>
|
|
763
|
+
|
|
764
|
+
{error && (
|
|
765
|
+
<Box marginBottom={1}>
|
|
766
|
+
<Text color="red">{'\u2717'} {error}</Text>
|
|
767
|
+
</Box>
|
|
768
|
+
)}
|
|
769
|
+
|
|
770
|
+
<Box marginTop={1}>
|
|
771
|
+
<Text color="gray" dimColor>
|
|
772
|
+
{isEditingPath
|
|
773
|
+
? 'Tab/\u2192: Autocomplete \u23CE: Confirm \u2193/ESC: Menu'
|
|
774
|
+
: '\u2191\u2193: Navigate \u23CE/A: Select ESC/B: Cancel'}
|
|
775
|
+
</Text>
|
|
776
|
+
</Box>
|
|
777
|
+
</Box>
|
|
778
|
+
);
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
export default AddRomsPrompt;
|