@fgv/ts-res-browser 1.0.0 → 5.0.0-2
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/.rush/temp/{5fc90bc7c2ccf812114ea099111568e5429be2a2.tar.log → 587277bfc395406ad444e989b6c309b9698f534c.tar.log} +2 -2
- package/.rush/temp/chunked-rush-logs/ts-res-browser.build.chunks.jsonl +12 -12
- package/.rush/temp/operation/build/all.log +12 -12
- package/.rush/temp/operation/build/log-chunks.jsonl +12 -12
- package/.rush/temp/operation/build/state.json +1 -1
- package/.rush/temp/shrinkwrap-deps.json +11 -3
- package/README.md +85 -15
- package/cli.js +45 -0
- package/dist/bundle.js +1 -1
- package/package.json +12 -7
- package/rush-logs/ts-res-browser.build.cache.log +1 -1
- package/rush-logs/ts-res-browser.build.log +12 -12
- package/src/App.tsx +122 -1
- package/src/components/common/FileImporter.tsx +10 -5
- package/src/components/layout/Sidebar.tsx +8 -1
- package/src/components/tools/ConfigurationTool.tsx +4 -1
- package/src/components/tools/FilterTool.tsx +106 -19
- package/src/components/tools/ImportTool.tsx +3 -0
- package/src/components/tools/ZipLoader.tsx +302 -0
- package/src/hooks/useAppState.ts +4 -0
- package/src/hooks/useResourceManager.ts +52 -1
- package/src/hooks/useUrlParams.ts +56 -0
- package/src/types/app.ts +9 -1
- package/src/utils/fileImport.ts +7 -5
- package/src/utils/fileTreeConverter.ts +160 -0
- package/src/utils/urlParams.ts +238 -0
- package/src/utils/zip/browserZipFileTreeAccessors.ts +385 -0
- package/webpack.config.js +7 -1
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
ArchiveBoxIcon,
|
|
4
|
+
FolderOpenIcon,
|
|
5
|
+
ExclamationTriangleIcon,
|
|
6
|
+
CheckCircleIcon,
|
|
7
|
+
ArrowRightIcon
|
|
8
|
+
} from '@heroicons/react/24/outline';
|
|
9
|
+
import { Message } from '../../types/app';
|
|
10
|
+
import { UseResourceManagerReturn } from '../../hooks/useResourceManager';
|
|
11
|
+
import { BrowserZipFileTreeAccessors } from '../../utils/zip/browserZipFileTreeAccessors';
|
|
12
|
+
import { FileTree } from '@fgv/ts-utils';
|
|
13
|
+
|
|
14
|
+
interface ZipLoaderProps {
|
|
15
|
+
onMessage: (type: Message['type'], message: string) => void;
|
|
16
|
+
resourceManager: UseResourceManagerReturn;
|
|
17
|
+
zipFile?: string;
|
|
18
|
+
zipPath?: string;
|
|
19
|
+
onLoadComplete?: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ZipManifest {
|
|
23
|
+
timestamp: string;
|
|
24
|
+
input?: {
|
|
25
|
+
type: 'file' | 'directory';
|
|
26
|
+
originalPath: string;
|
|
27
|
+
archivePath: string;
|
|
28
|
+
};
|
|
29
|
+
config?: {
|
|
30
|
+
type: 'file';
|
|
31
|
+
originalPath: string;
|
|
32
|
+
archivePath: string;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const ZipLoader: React.FC<ZipLoaderProps> = ({
|
|
37
|
+
onMessage,
|
|
38
|
+
resourceManager,
|
|
39
|
+
zipFile,
|
|
40
|
+
zipPath,
|
|
41
|
+
onLoadComplete
|
|
42
|
+
}) => {
|
|
43
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
44
|
+
const [loadingStep, setLoadingStep] = useState<string>('');
|
|
45
|
+
const [zipFound, setZipFound] = useState<boolean | null>(null);
|
|
46
|
+
const [manifest, setManifest] = useState<ZipManifest | null>(null);
|
|
47
|
+
const [autoloadAttempted, setAutoloadAttempted] = useState(false);
|
|
48
|
+
|
|
49
|
+
// Auto-load ZIP if we have a specific file reference
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if ((zipFile || zipPath) && !autoloadAttempted) {
|
|
52
|
+
setAutoloadAttempted(true);
|
|
53
|
+
handleLoadZip();
|
|
54
|
+
}
|
|
55
|
+
}, [zipFile, zipPath, autoloadAttempted]);
|
|
56
|
+
|
|
57
|
+
const handleLoadZip = async () => {
|
|
58
|
+
if (!zipFile && !zipPath) {
|
|
59
|
+
onMessage('error', 'No ZIP file specified');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
setIsLoading(true);
|
|
64
|
+
setLoadingStep('Opening file picker...');
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
// Try to load the ZIP file using File System Access API
|
|
68
|
+
let fileHandle: FileSystemFileHandle | null = null;
|
|
69
|
+
|
|
70
|
+
if ('showOpenFilePicker' in window) {
|
|
71
|
+
try {
|
|
72
|
+
const [handle] = await (window as any).showOpenFilePicker({
|
|
73
|
+
types: [
|
|
74
|
+
{
|
|
75
|
+
description: 'ZIP Archives',
|
|
76
|
+
accept: { 'application/zip': ['.zip'] }
|
|
77
|
+
}
|
|
78
|
+
],
|
|
79
|
+
startIn: 'downloads',
|
|
80
|
+
multiple: false
|
|
81
|
+
});
|
|
82
|
+
fileHandle = handle;
|
|
83
|
+
setZipFound(true);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if ((error as DOMException).name !== 'AbortError') {
|
|
86
|
+
onMessage('warning', 'Could not open file picker. Please select the ZIP file manually.');
|
|
87
|
+
}
|
|
88
|
+
setZipFound(false);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
onMessage('error', 'File System Access API not supported. Please use a modern browser.');
|
|
93
|
+
setZipFound(false);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!fileHandle) {
|
|
98
|
+
setZipFound(false);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
setLoadingStep('Reading ZIP archive...');
|
|
103
|
+
const file = await fileHandle.getFile();
|
|
104
|
+
|
|
105
|
+
// Create ZIP FileTree
|
|
106
|
+
const zipAccessorsResult = await BrowserZipFileTreeAccessors.fromFile(file);
|
|
107
|
+
if (zipAccessorsResult.isFailure()) {
|
|
108
|
+
onMessage('error', `Failed to read ZIP: ${zipAccessorsResult.message}`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const zipAccessors = zipAccessorsResult.value;
|
|
113
|
+
const fileTree = FileTree.FileTree.create(zipAccessors);
|
|
114
|
+
if (fileTree.isFailure()) {
|
|
115
|
+
onMessage('error', `Failed to create file tree: ${fileTree.message}`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
setLoadingStep('Reading manifest...');
|
|
120
|
+
|
|
121
|
+
// Try to read manifest
|
|
122
|
+
const manifestResult = fileTree.value.getFile('/manifest.json');
|
|
123
|
+
if (manifestResult.isSuccess()) {
|
|
124
|
+
const manifestContents = manifestResult.value.getContents();
|
|
125
|
+
if (manifestContents.isSuccess()) {
|
|
126
|
+
const parsedManifest = manifestContents.value as ZipManifest;
|
|
127
|
+
setManifest(parsedManifest);
|
|
128
|
+
onMessage('info', `ZIP created: ${new Date(parsedManifest.timestamp).toLocaleString()}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Process resources from the ZIP
|
|
133
|
+
setLoadingStep('Processing resources...');
|
|
134
|
+
|
|
135
|
+
// Look for input directory/files
|
|
136
|
+
let inputProcessed = false;
|
|
137
|
+
if (manifest?.input) {
|
|
138
|
+
const inputPath = `/${manifest.input.archivePath}`;
|
|
139
|
+
const inputResult = fileTree.value.getItem(inputPath);
|
|
140
|
+
|
|
141
|
+
if (inputResult.isSuccess()) {
|
|
142
|
+
const inputItem = inputResult.value;
|
|
143
|
+
|
|
144
|
+
if (inputItem.type === 'directory') {
|
|
145
|
+
// Process directory
|
|
146
|
+
await resourceManager.actions.processFileTree(fileTree.value, inputPath);
|
|
147
|
+
inputProcessed = true;
|
|
148
|
+
} else if (inputItem.type === 'file') {
|
|
149
|
+
// Process single file
|
|
150
|
+
await resourceManager.actions.processFileTree(fileTree.value, inputPath);
|
|
151
|
+
inputProcessed = true;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Fallback: look for common input patterns
|
|
157
|
+
if (!inputProcessed) {
|
|
158
|
+
const commonInputPaths = ['/input', '/resources', '/data'];
|
|
159
|
+
for (const inputPath of commonInputPaths) {
|
|
160
|
+
const inputResult = fileTree.value.getItem(inputPath);
|
|
161
|
+
if (inputResult.isSuccess() && inputResult.value.type === 'directory') {
|
|
162
|
+
await resourceManager.actions.processFileTree(fileTree.value, inputPath);
|
|
163
|
+
inputProcessed = true;
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Load configuration if present
|
|
170
|
+
if (manifest?.config) {
|
|
171
|
+
const configPath = `/${manifest.config.archivePath}`;
|
|
172
|
+
const configResult = fileTree.value.getFile(configPath);
|
|
173
|
+
|
|
174
|
+
if (configResult.isSuccess()) {
|
|
175
|
+
const configContents = configResult.value.getContents();
|
|
176
|
+
if (configContents.isSuccess()) {
|
|
177
|
+
resourceManager.actions.applyConfiguration(configContents.value as any);
|
|
178
|
+
onMessage('success', 'Configuration loaded from ZIP');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (inputProcessed) {
|
|
184
|
+
onMessage('success', 'Resources loaded successfully from ZIP!');
|
|
185
|
+
onLoadComplete?.();
|
|
186
|
+
} else {
|
|
187
|
+
onMessage('warning', 'ZIP loaded but no resources found. Check ZIP contents.');
|
|
188
|
+
}
|
|
189
|
+
} catch (error) {
|
|
190
|
+
onMessage('error', `Failed to load ZIP: ${error instanceof Error ? error.message : String(error)}`);
|
|
191
|
+
setZipFound(false);
|
|
192
|
+
} finally {
|
|
193
|
+
setIsLoading(false);
|
|
194
|
+
setLoadingStep('');
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const expectedFileName = zipFile || 'ts-res-bundle-*.zip';
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<div className="p-6">
|
|
202
|
+
<div className="flex items-center space-x-3 mb-6">
|
|
203
|
+
<ArchiveBoxIcon className="h-8 w-8 text-purple-600" />
|
|
204
|
+
<h2 className="text-2xl font-bold text-gray-900">Load ZIP Archive</h2>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<div className="max-w-2xl">
|
|
208
|
+
{/* ZIP File Info */}
|
|
209
|
+
<div className="bg-blue-50 rounded-lg p-4 mb-6">
|
|
210
|
+
<div className="flex items-start space-x-3">
|
|
211
|
+
<ArchiveBoxIcon className="h-5 w-5 text-blue-600 mt-0.5" />
|
|
212
|
+
<div className="text-sm text-blue-800">
|
|
213
|
+
<p className="font-medium">ZIP Archive Ready</p>
|
|
214
|
+
{zipPath && (
|
|
215
|
+
<p className="mt-1">
|
|
216
|
+
<span className="font-medium">Location:</span> {zipPath}
|
|
217
|
+
</p>
|
|
218
|
+
)}
|
|
219
|
+
{zipFile && (
|
|
220
|
+
<p className="mt-1">
|
|
221
|
+
<span className="font-medium">File:</span> {zipFile}
|
|
222
|
+
</p>
|
|
223
|
+
)}
|
|
224
|
+
<p className="mt-2">
|
|
225
|
+
The CLI has created a ZIP archive containing your resources and configuration. Click below to
|
|
226
|
+
load it directly into the browser.
|
|
227
|
+
</p>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
{/* Load Button */}
|
|
233
|
+
<div className="space-y-4">
|
|
234
|
+
<button
|
|
235
|
+
onClick={handleLoadZip}
|
|
236
|
+
disabled={isLoading}
|
|
237
|
+
className="w-full flex items-center justify-center space-x-2 px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
238
|
+
>
|
|
239
|
+
{isLoading ? (
|
|
240
|
+
<>
|
|
241
|
+
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
|
|
242
|
+
<span>{loadingStep || 'Loading...'}</span>
|
|
243
|
+
</>
|
|
244
|
+
) : (
|
|
245
|
+
<>
|
|
246
|
+
<FolderOpenIcon className="w-5 h-5" />
|
|
247
|
+
<span>Load ZIP Archive</span>
|
|
248
|
+
<ArrowRightIcon className="w-5 h-5" />
|
|
249
|
+
</>
|
|
250
|
+
)}
|
|
251
|
+
</button>
|
|
252
|
+
|
|
253
|
+
{/* Instructions */}
|
|
254
|
+
<div className="bg-gray-50 rounded-lg p-4">
|
|
255
|
+
<h3 className="text-sm font-medium text-gray-900 mb-2">Instructions:</h3>
|
|
256
|
+
<ol className="text-sm text-gray-600 space-y-1 list-decimal list-inside">
|
|
257
|
+
<li>Click "Load ZIP Archive" above</li>
|
|
258
|
+
<li>
|
|
259
|
+
Select the ZIP file: <code className="bg-gray-200 px-1 rounded">{expectedFileName}</code>
|
|
260
|
+
</li>
|
|
261
|
+
<li>The browser will automatically process your resources and configuration</li>
|
|
262
|
+
</ol>
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
{/* Manifest Info */}
|
|
266
|
+
{manifest && (
|
|
267
|
+
<div className="bg-green-50 rounded-lg p-4">
|
|
268
|
+
<div className="flex items-start space-x-3">
|
|
269
|
+
<CheckCircleIcon className="h-5 w-5 text-green-600 mt-0.5" />
|
|
270
|
+
<div className="text-sm text-green-800">
|
|
271
|
+
<p className="font-medium">ZIP Archive Contents</p>
|
|
272
|
+
<div className="mt-2 space-y-1">
|
|
273
|
+
{manifest.input && <p>📁 Resources: {manifest.input.originalPath}</p>}
|
|
274
|
+
{manifest.config && <p>⚙️ Configuration: {manifest.config.originalPath}</p>}
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
)}
|
|
280
|
+
|
|
281
|
+
{/* Error State */}
|
|
282
|
+
{zipFound === false && (
|
|
283
|
+
<div className="bg-yellow-50 rounded-lg p-4">
|
|
284
|
+
<div className="flex items-start space-x-3">
|
|
285
|
+
<ExclamationTriangleIcon className="h-5 w-5 text-yellow-600 mt-0.5" />
|
|
286
|
+
<div className="text-sm text-yellow-800">
|
|
287
|
+
<p className="font-medium">ZIP File Not Found</p>
|
|
288
|
+
<p className="mt-1">
|
|
289
|
+
Could not locate or open the ZIP file. Please make sure it exists in your Downloads folder
|
|
290
|
+
and try again.
|
|
291
|
+
</p>
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
)}
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
);
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
export default ZipLoader;
|
package/src/hooks/useAppState.ts
CHANGED
|
@@ -18,6 +18,9 @@ export const useAppState = (): { state: AppState; actions: AppActions } => {
|
|
|
18
18
|
setState((prev) => ({ ...prev, selectedTool: tool }));
|
|
19
19
|
}, []);
|
|
20
20
|
|
|
21
|
+
// Alias for setSelectedTool to match the interface
|
|
22
|
+
const setActiveTool = setSelectedTool;
|
|
23
|
+
|
|
21
24
|
const addMessage = useCallback((type: Message['type'], message: string) => {
|
|
22
25
|
const newMessage: Message = {
|
|
23
26
|
id: Date.now().toString(),
|
|
@@ -94,6 +97,7 @@ export const useAppState = (): { state: AppState; actions: AppActions } => {
|
|
|
94
97
|
|
|
95
98
|
const actions: AppActions = {
|
|
96
99
|
setSelectedTool,
|
|
100
|
+
setActiveTool,
|
|
97
101
|
addMessage,
|
|
98
102
|
clearMessages,
|
|
99
103
|
updateFilterEnabled,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, useCallback } from 'react';
|
|
2
|
-
import { Result, succeed, fail } from '@fgv/ts-utils';
|
|
2
|
+
import { Result, succeed, fail, FileTree } from '@fgv/ts-utils';
|
|
3
3
|
import {
|
|
4
4
|
ProcessedResources,
|
|
5
5
|
processImportedDirectory,
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
createSimpleContext
|
|
8
8
|
} from '../utils/tsResIntegration';
|
|
9
9
|
import { ImportedDirectory, ImportedFile } from '../utils/fileImport';
|
|
10
|
+
import { FileTreeConverter } from '../utils/fileTreeConverter';
|
|
10
11
|
import { Config } from '@fgv/ts-res';
|
|
11
12
|
|
|
12
13
|
export interface ResourceManagerState {
|
|
@@ -22,6 +23,7 @@ export interface UseResourceManagerReturn {
|
|
|
22
23
|
actions: {
|
|
23
24
|
processDirectory: (directory: ImportedDirectory) => Promise<void>;
|
|
24
25
|
processFiles: (files: ImportedFile[]) => Promise<void>;
|
|
26
|
+
processFileTree: (fileTree: FileTree.FileTree, rootPath?: string) => Promise<void>;
|
|
25
27
|
clearError: () => void;
|
|
26
28
|
reset: () => void;
|
|
27
29
|
resolveResource: (resourceId: string, context?: Record<string, string>) => Promise<Result<any>>;
|
|
@@ -216,6 +218,54 @@ export const useResourceManager = (): UseResourceManagerReturn => {
|
|
|
216
218
|
[state.processedResources]
|
|
217
219
|
);
|
|
218
220
|
|
|
221
|
+
const processFileTree = useCallback(
|
|
222
|
+
async (fileTree: FileTree.FileTree, rootPath: string = '/') => {
|
|
223
|
+
console.log('=== STARTING FILETREE PROCESSING ===');
|
|
224
|
+
console.log('Root path:', rootPath);
|
|
225
|
+
|
|
226
|
+
setState((prev) => ({ ...prev, isProcessing: true, error: null }));
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
console.log('1. Converting FileTree to ImportedDirectory/Files...');
|
|
230
|
+
|
|
231
|
+
const convertResult = FileTreeConverter.convertPath(fileTree, rootPath);
|
|
232
|
+
if (convertResult.isFailure()) {
|
|
233
|
+
setState((prev) => ({
|
|
234
|
+
...prev,
|
|
235
|
+
isProcessing: false,
|
|
236
|
+
error: `Failed to convert FileTree: ${convertResult.message}`
|
|
237
|
+
}));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const converted = convertResult.value;
|
|
242
|
+
console.log('2. FileTree converted successfully');
|
|
243
|
+
|
|
244
|
+
// Process based on whether we got a directory or files
|
|
245
|
+
if (Array.isArray(converted)) {
|
|
246
|
+
// Got files
|
|
247
|
+
console.log('3. Processing as files, count:', converted.length);
|
|
248
|
+
await processFiles(converted);
|
|
249
|
+
} else {
|
|
250
|
+
// Got directory
|
|
251
|
+
console.log('3. Processing as directory:', converted.name);
|
|
252
|
+
await processDirectory(converted);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
console.log('4. processFileTree completed successfully');
|
|
256
|
+
} catch (error) {
|
|
257
|
+
console.error('=== FILETREE PROCESSING ERROR ===');
|
|
258
|
+
console.error('Error:', error);
|
|
259
|
+
setState((prev) => ({
|
|
260
|
+
...prev,
|
|
261
|
+
isProcessing: false,
|
|
262
|
+
error: `Unexpected error: ${error instanceof Error ? error.message : String(error)}`
|
|
263
|
+
}));
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
[processFiles, processDirectory]
|
|
267
|
+
);
|
|
268
|
+
|
|
219
269
|
const applyConfiguration = useCallback((config: Config.Model.ISystemConfiguration) => {
|
|
220
270
|
console.log('Applying configuration:', config.name || 'Unnamed configuration');
|
|
221
271
|
setState((prev) => ({
|
|
@@ -233,6 +283,7 @@ export const useResourceManager = (): UseResourceManagerReturn => {
|
|
|
233
283
|
actions: {
|
|
234
284
|
processDirectory,
|
|
235
285
|
processFiles,
|
|
286
|
+
processFileTree,
|
|
236
287
|
clearError,
|
|
237
288
|
reset,
|
|
238
289
|
resolveResource,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025 Erik Fortune
|
|
3
|
+
*
|
|
4
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
* in the Software without restriction, including without limitation the rights
|
|
7
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
* furnished to do so, subject to the following conditions:
|
|
10
|
+
*
|
|
11
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
* copies or substantial portions of the Software.
|
|
13
|
+
*
|
|
14
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
* SOFTWARE.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { useEffect, useState } from 'react';
|
|
24
|
+
import { parseUrlParameters, IUrlConfigOptions } from '../utils/urlParams';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Hook to parse and provide URL parameters for initial app configuration
|
|
28
|
+
*/
|
|
29
|
+
export function useUrlParams() {
|
|
30
|
+
const [urlParams, setUrlParams] = useState<IUrlConfigOptions>({});
|
|
31
|
+
const [hasUrlParams, setHasUrlParams] = useState(false);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const params = parseUrlParameters();
|
|
35
|
+
setUrlParams(params);
|
|
36
|
+
|
|
37
|
+
// Check if any meaningful parameters were provided
|
|
38
|
+
const hasParams = !!(
|
|
39
|
+
params.input ||
|
|
40
|
+
params.config ||
|
|
41
|
+
params.contextFilter ||
|
|
42
|
+
params.qualifierDefaults ||
|
|
43
|
+
params.resourceTypes ||
|
|
44
|
+
params.maxDistance !== undefined ||
|
|
45
|
+
params.reduceQualifiers ||
|
|
46
|
+
params.interactive
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
setHasUrlParams(hasParams);
|
|
50
|
+
}, []);
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
urlParams,
|
|
54
|
+
hasUrlParams
|
|
55
|
+
};
|
|
56
|
+
}
|
package/src/types/app.ts
CHANGED
|
@@ -6,7 +6,14 @@ export interface Message {
|
|
|
6
6
|
timestamp: Date;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
export type Tool =
|
|
9
|
+
export type Tool =
|
|
10
|
+
| 'import'
|
|
11
|
+
| 'source'
|
|
12
|
+
| 'filter'
|
|
13
|
+
| 'compiled'
|
|
14
|
+
| 'resolution'
|
|
15
|
+
| 'configuration'
|
|
16
|
+
| 'zip-loader';
|
|
10
17
|
|
|
11
18
|
export interface FilterState {
|
|
12
19
|
enabled: boolean;
|
|
@@ -24,6 +31,7 @@ export interface AppState {
|
|
|
24
31
|
|
|
25
32
|
export interface AppActions {
|
|
26
33
|
setSelectedTool: (tool: Tool, force?: boolean) => void;
|
|
34
|
+
setActiveTool: (tool: Tool, force?: boolean) => void; // Alias for setSelectedTool
|
|
27
35
|
addMessage: (type: Message['type'], message: string) => void;
|
|
28
36
|
clearMessages: () => void;
|
|
29
37
|
updateFilterEnabled: (enabled: boolean) => void;
|
package/src/utils/fileImport.ts
CHANGED
|
@@ -18,6 +18,7 @@ export interface FileImportOptions {
|
|
|
18
18
|
acceptedTypes?: string[];
|
|
19
19
|
multiple?: boolean;
|
|
20
20
|
includeDirectories?: boolean;
|
|
21
|
+
startIn?: string | FileSystemHandle;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
/**
|
|
@@ -34,7 +35,7 @@ export class ModernFileImporter {
|
|
|
34
35
|
/**
|
|
35
36
|
* Pick a directory using File System Access API
|
|
36
37
|
*/
|
|
37
|
-
async pickDirectory(): Promise<Result<ImportedDirectory>> {
|
|
38
|
+
async pickDirectory(options: FileImportOptions = {}): Promise<Result<ImportedDirectory>> {
|
|
38
39
|
try {
|
|
39
40
|
if (!isFileSystemAccessSupported()) {
|
|
40
41
|
return fail('File System Access API not supported');
|
|
@@ -42,7 +43,7 @@ export class ModernFileImporter {
|
|
|
42
43
|
|
|
43
44
|
const directoryHandle = await window.showDirectoryPicker({
|
|
44
45
|
mode: 'read',
|
|
45
|
-
startIn: 'documents'
|
|
46
|
+
startIn: options.startIn || 'documents'
|
|
46
47
|
});
|
|
47
48
|
|
|
48
49
|
const importedDirectory = await this.processDirectoryHandle(directoryHandle);
|
|
@@ -76,7 +77,8 @@ export class ModernFileImporter {
|
|
|
76
77
|
}
|
|
77
78
|
]
|
|
78
79
|
: undefined,
|
|
79
|
-
excludeAcceptAllOption: false
|
|
80
|
+
excludeAcceptAllOption: false,
|
|
81
|
+
startIn: options.startIn || 'documents'
|
|
80
82
|
});
|
|
81
83
|
|
|
82
84
|
const importedFiles: ImportedFile[] = [];
|
|
@@ -303,9 +305,9 @@ export class FileImporter {
|
|
|
303
305
|
/**
|
|
304
306
|
* Pick a directory using the best available method
|
|
305
307
|
*/
|
|
306
|
-
async pickDirectory(): Promise<Result<ImportedDirectory>> {
|
|
308
|
+
async pickDirectory(options: FileImportOptions = {}): Promise<Result<ImportedDirectory>> {
|
|
307
309
|
if (isFileSystemAccessSupported()) {
|
|
308
|
-
return this.modernImporter.pickDirectory();
|
|
310
|
+
return this.modernImporter.pickDirectory(options);
|
|
309
311
|
} else {
|
|
310
312
|
return this.fallbackImporter.pickDirectory();
|
|
311
313
|
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025 Erik Fortune
|
|
3
|
+
*
|
|
4
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
* in the Software without restriction, including without limitation the rights
|
|
7
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
* furnished to do so, subject to the following conditions:
|
|
10
|
+
*
|
|
11
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
* copies or substantial portions of the Software.
|
|
13
|
+
*
|
|
14
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
* SOFTWARE.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { FileTree, Result, succeed, fail } from '@fgv/ts-utils';
|
|
24
|
+
import { ImportedDirectory, ImportedFile } from './fileImport';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Converts a FileTree item (directory or file) to ImportedDirectory/ImportedFile format
|
|
28
|
+
* for compatibility with existing resource processing pipeline.
|
|
29
|
+
*/
|
|
30
|
+
export class FileTreeConverter {
|
|
31
|
+
/**
|
|
32
|
+
* Converts a FileTree directory to ImportedDirectory format.
|
|
33
|
+
*/
|
|
34
|
+
public static convertDirectory(
|
|
35
|
+
fileTree: FileTree.FileTree,
|
|
36
|
+
directoryPath: string
|
|
37
|
+
): Result<ImportedDirectory> {
|
|
38
|
+
try {
|
|
39
|
+
const itemResult = fileTree.getItem(directoryPath);
|
|
40
|
+
if (itemResult.isFailure()) {
|
|
41
|
+
return fail(`Directory not found: ${directoryPath}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const item = itemResult.value;
|
|
45
|
+
if (item.type !== 'directory') {
|
|
46
|
+
return fail(`Path is not a directory: ${directoryPath}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const childrenResult = item.getChildren();
|
|
50
|
+
if (childrenResult.isFailure()) {
|
|
51
|
+
return fail(`Failed to get directory children: ${childrenResult.message}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const files: ImportedFile[] = [];
|
|
55
|
+
const directories: ImportedDirectory[] = [];
|
|
56
|
+
|
|
57
|
+
for (const child of childrenResult.value) {
|
|
58
|
+
if (child.type === 'file') {
|
|
59
|
+
const fileResult = this.convertFile(child);
|
|
60
|
+
if (fileResult.isSuccess()) {
|
|
61
|
+
files.push(fileResult.value);
|
|
62
|
+
}
|
|
63
|
+
} else if (child.type === 'directory') {
|
|
64
|
+
const dirResult = this.convertDirectory(fileTree, child.absolutePath);
|
|
65
|
+
if (dirResult.isSuccess()) {
|
|
66
|
+
directories.push(dirResult.value);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const importedDirectory: ImportedDirectory = {
|
|
72
|
+
name: item.name,
|
|
73
|
+
path: item.absolutePath,
|
|
74
|
+
files,
|
|
75
|
+
directories
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return succeed(importedDirectory);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return fail(`Failed to convert directory: ${error instanceof Error ? error.message : String(error)}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Converts a FileTree file item to ImportedFile format.
|
|
86
|
+
*/
|
|
87
|
+
public static convertFile(fileItem: FileTree.IFileTreeFileItem): Result<ImportedFile> {
|
|
88
|
+
try {
|
|
89
|
+
const contentsResult = fileItem.getRawContents();
|
|
90
|
+
if (contentsResult.isFailure()) {
|
|
91
|
+
return fail(`Failed to read file contents: ${contentsResult.message}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const importedFile: ImportedFile = {
|
|
95
|
+
name: fileItem.name,
|
|
96
|
+
path: fileItem.absolutePath,
|
|
97
|
+
content: contentsResult.value
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return succeed(importedFile);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
return fail(`Failed to convert file: ${error instanceof Error ? error.message : String(error)}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Converts multiple FileTree files to ImportedFile format.
|
|
108
|
+
*/
|
|
109
|
+
public static convertFiles(fileItems: FileTree.IFileTreeFileItem[]): Result<ImportedFile[]> {
|
|
110
|
+
try {
|
|
111
|
+
const importedFiles: ImportedFile[] = [];
|
|
112
|
+
|
|
113
|
+
for (const fileItem of fileItems) {
|
|
114
|
+
const fileResult = this.convertFile(fileItem);
|
|
115
|
+
if (fileResult.isSuccess()) {
|
|
116
|
+
importedFiles.push(fileResult.value);
|
|
117
|
+
} else {
|
|
118
|
+
return fail(`Failed to convert file ${fileItem.name}: ${fileResult.message}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return succeed(importedFiles);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
return fail(`Failed to convert files: ${error instanceof Error ? error.message : String(error)}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Converts a FileTree path to ImportedDirectory or ImportedFile format,
|
|
130
|
+
* automatically detecting the type.
|
|
131
|
+
*/
|
|
132
|
+
public static convertPath(
|
|
133
|
+
fileTree: FileTree.FileTree,
|
|
134
|
+
path: string
|
|
135
|
+
): Result<ImportedDirectory | ImportedFile[]> {
|
|
136
|
+
try {
|
|
137
|
+
const itemResult = fileTree.getItem(path);
|
|
138
|
+
if (itemResult.isFailure()) {
|
|
139
|
+
return fail(`Path not found: ${path}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const item = itemResult.value;
|
|
143
|
+
|
|
144
|
+
if (item.type === 'directory') {
|
|
145
|
+
return this.convertDirectory(fileTree, path);
|
|
146
|
+
} else if (item.type === 'file') {
|
|
147
|
+
const fileResult = this.convertFile(item);
|
|
148
|
+
if (fileResult.isSuccess()) {
|
|
149
|
+
return succeed([fileResult.value]);
|
|
150
|
+
} else {
|
|
151
|
+
return fileResult;
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
return fail(`Unknown item type: ${path}`);
|
|
155
|
+
}
|
|
156
|
+
} catch (error) {
|
|
157
|
+
return fail(`Failed to convert path: ${error instanceof Error ? error.message : String(error)}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|