@umituz/react-native-filesystem 2.1.6 → 2.1.8
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/package.json +21 -23
- package/src/domain/entities/ImportExport.types.ts +73 -0
- package/src/index.ts +10 -170
- package/src/infrastructure/services/ImportExportService.ts +268 -0
- package/src/infrastructure/services/file-info.service.ts +1 -0
- package/src/presentation/hooks/useImportExport.ts +175 -0
- package/src/infrastructure/utils/moduleLoader.ts +0 -29
package/package.json
CHANGED
|
@@ -1,49 +1,47 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-filesystem",
|
|
3
|
-
"version": "2.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.1.8",
|
|
4
|
+
"description": "File operations and import/export functionality for React Native apps",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
7
13
|
"scripts": {
|
|
8
|
-
"typecheck": "
|
|
9
|
-
"lint": "
|
|
10
|
-
"version:patch": "npm version patch -m 'chore: release v%s'"
|
|
11
|
-
"version:minor": "npm version minor -m 'chore: release v%s'",
|
|
12
|
-
"version:major": "npm version major -m 'chore: release v%s'"
|
|
14
|
+
"typecheck": "tsc --noEmit",
|
|
15
|
+
"lint": "eslint src",
|
|
16
|
+
"version:patch": "npm version patch -m 'chore: release v%s'"
|
|
13
17
|
},
|
|
14
18
|
"keywords": [
|
|
15
19
|
"react-native",
|
|
16
20
|
"filesystem",
|
|
17
21
|
"file-system",
|
|
18
22
|
"expo-file-system",
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"directory-tree"
|
|
23
|
+
"download",
|
|
24
|
+
"file-operations"
|
|
22
25
|
],
|
|
23
26
|
"author": "Ümit UZ <umit@umituz.com>",
|
|
24
27
|
"license": "MIT",
|
|
25
28
|
"repository": {
|
|
26
29
|
"type": "git",
|
|
27
|
-
"url": "https://github.com/umituz/react-native-filesystem"
|
|
30
|
+
"url": "git+https://github.com/umituz/react-native-filesystem.git"
|
|
28
31
|
},
|
|
29
32
|
"peerDependencies": {
|
|
30
|
-
"expo-file-system": ">=
|
|
31
|
-
"react": ">=18.
|
|
32
|
-
"react-native": ">=0.
|
|
33
|
+
"expo-file-system": ">=19.0.0",
|
|
34
|
+
"react": ">=18.0.0",
|
|
35
|
+
"react-native": ">=0.70.0"
|
|
33
36
|
},
|
|
34
37
|
"devDependencies": {
|
|
35
|
-
"expo-file-system": "~17.0.1",
|
|
36
38
|
"@types/react": "~19.1.10",
|
|
39
|
+
"expo-file-system": "^19.0.21",
|
|
37
40
|
"react": "19.1.0",
|
|
38
41
|
"react-native": "0.81.5",
|
|
39
|
-
"typescript": "~5.
|
|
42
|
+
"typescript": "~5.3.0"
|
|
40
43
|
},
|
|
41
44
|
"publishConfig": {
|
|
42
45
|
"access": "public"
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
"src",
|
|
46
|
-
"README.md",
|
|
47
|
-
"LICENSE"
|
|
48
|
-
]
|
|
49
|
-
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import/Export Types
|
|
3
|
+
* File operations and data transfer functionality
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type ExportFormat = "csv" | "json" | "anki" | "xlsx";
|
|
7
|
+
export type ValidationFormat = "json" | "xml" | "yaml";
|
|
8
|
+
|
|
9
|
+
export interface ImportResult {
|
|
10
|
+
success: boolean;
|
|
11
|
+
flashcards: Array<
|
|
12
|
+
Partial<{
|
|
13
|
+
front: string;
|
|
14
|
+
back: string;
|
|
15
|
+
tags?: string[];
|
|
16
|
+
difficulty?: "easy" | "medium" | "hard";
|
|
17
|
+
}>
|
|
18
|
+
>;
|
|
19
|
+
errors: string[];
|
|
20
|
+
warnings: string[];
|
|
21
|
+
duplicatesFound: number;
|
|
22
|
+
importedCount: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ExportOptions {
|
|
26
|
+
format: ExportFormat;
|
|
27
|
+
includeMedia?: boolean;
|
|
28
|
+
includeTags?: boolean;
|
|
29
|
+
includeProgress?: boolean;
|
|
30
|
+
includeSRS?: boolean;
|
|
31
|
+
compression?: boolean;
|
|
32
|
+
filename?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ParsedFlashcard {
|
|
36
|
+
front: string;
|
|
37
|
+
back: string;
|
|
38
|
+
tags?: string[];
|
|
39
|
+
difficulty?: "easy" | "medium" | "hard";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface MediaUploadProgress {
|
|
43
|
+
fileId: string;
|
|
44
|
+
progress: number; // 0-100
|
|
45
|
+
status: "uploading" | "processing" | "completed" | "error";
|
|
46
|
+
error?: string;
|
|
47
|
+
url?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ImportExportService {
|
|
51
|
+
importFlashcards(file: File, format: ExportFormat): Promise<ImportResult>;
|
|
52
|
+
exportFlashcards(
|
|
53
|
+
flashcards: any[],
|
|
54
|
+
options: ExportOptions,
|
|
55
|
+
): Promise<{
|
|
56
|
+
success: boolean;
|
|
57
|
+
data: Blob;
|
|
58
|
+
filename: string;
|
|
59
|
+
}>;
|
|
60
|
+
validateData(
|
|
61
|
+
data: any,
|
|
62
|
+
format: ValidationFormat,
|
|
63
|
+
): Promise<{
|
|
64
|
+
isValid: boolean;
|
|
65
|
+
errors: string[];
|
|
66
|
+
warnings: string[];
|
|
67
|
+
}>;
|
|
68
|
+
optimizeData(data: any): Promise<{
|
|
69
|
+
optimized: any;
|
|
70
|
+
compressionRatio: number;
|
|
71
|
+
savings: number;
|
|
72
|
+
}>;
|
|
73
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,178 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* CENTRAL PLACE FOR ALL FILE OPERATIONS
|
|
5
|
-
*
|
|
6
|
-
* Provides two main capabilities:
|
|
7
|
-
* 1. Build-time module loading (require.context)
|
|
8
|
-
* 2. Runtime file operations (expo-file-system)
|
|
9
|
-
*
|
|
10
|
-
* @domain filesystem
|
|
11
|
-
* @enabled true (All apps - Infrastructure domain)
|
|
12
|
-
*
|
|
13
|
-
* ARCHITECTURE:
|
|
14
|
-
* - Domain Layer: Entities (ModuleContext for build-time, File for runtime)
|
|
15
|
-
* - Infrastructure Layer: Utils (module loading) + Services (file operations)
|
|
16
|
-
*
|
|
17
|
-
* ============================================================================
|
|
18
|
-
* BUILD-TIME MODULE LOADING (require.context)
|
|
19
|
-
* ============================================================================
|
|
20
|
-
*
|
|
21
|
-
* FEATURES:
|
|
22
|
-
* - Auto-discover JSON files in directory
|
|
23
|
-
* - Load all modules with single function call
|
|
24
|
-
* - Type-safe module loading
|
|
25
|
-
* - Metro bundler compatible (React Native)
|
|
26
|
-
* - Zero runtime dependencies
|
|
27
|
-
* - Build-time resolution (fast)
|
|
28
|
-
*
|
|
29
|
-
* USAGE - Build-time Module Loading:
|
|
30
|
-
*
|
|
31
|
-
* Basic Example:
|
|
32
|
-
* ```typescript
|
|
33
|
-
* import { loadJsonModules } from '@domains/filesystem';
|
|
34
|
-
*
|
|
35
|
-
* // Auto-load all JSON files in current directory
|
|
36
|
-
* const translationContext = require.context('./', false, /\.json$/);
|
|
37
|
-
* const translations = loadJsonModules(translationContext);
|
|
38
|
-
*
|
|
39
|
-
* export default translations;
|
|
40
|
-
* // Result: { common: {...}, errors: {...}, settings: {...} }
|
|
41
|
-
* ```
|
|
42
|
-
*
|
|
43
|
-
* Custom Transform Example:
|
|
44
|
-
* ```typescript
|
|
45
|
-
* import { loadModulesWithTransform } from '@domains/filesystem';
|
|
46
|
-
*
|
|
47
|
-
* const context = require.context('./', false, /\.json$/);
|
|
48
|
-
* const modules = loadModulesWithTransform(context, name => name.toUpperCase());
|
|
49
|
-
* // Result: { COMMON: {...}, ERRORS: {...} }
|
|
50
|
-
* ```
|
|
51
|
-
*
|
|
52
|
-
* Get Names Example:
|
|
53
|
-
* ```typescript
|
|
54
|
-
* import { getModuleNames } from '@domains/filesystem';
|
|
55
|
-
*
|
|
56
|
-
* const context = require.context('./', false, /\.json$/);
|
|
57
|
-
* const names = getModuleNames(context);
|
|
58
|
-
* // Result: ['common', 'errors', 'settings']
|
|
59
|
-
* ```
|
|
60
|
-
*
|
|
61
|
-
* ============================================================================
|
|
62
|
-
* RUNTIME FILE OPERATIONS (expo-file-system)
|
|
63
|
-
* ============================================================================
|
|
64
|
-
*
|
|
65
|
-
* FEATURES:
|
|
66
|
-
* - Read/write files on device
|
|
67
|
-
* - Download files from URLs
|
|
68
|
-
* - Directory management
|
|
69
|
-
* - Cache management
|
|
70
|
-
* - File copying/moving
|
|
71
|
-
*
|
|
72
|
-
* USAGE - Runtime File Operations:
|
|
73
|
-
*
|
|
74
|
-
* Read/Write Files:
|
|
75
|
-
* ```typescript
|
|
76
|
-
* import { FileSystemService } from '@domains/filesystem';
|
|
77
|
-
*
|
|
78
|
-
* // Write file
|
|
79
|
-
* const result = await FileSystemService.writeFile(
|
|
80
|
-
* FileSystemService.generateFilePath('data.json'),
|
|
81
|
-
* JSON.stringify({ key: 'value' })
|
|
82
|
-
* );
|
|
83
|
-
*
|
|
84
|
-
* // Read file
|
|
85
|
-
* const content = await FileSystemService.readFile(result.uri);
|
|
86
|
-
* ```
|
|
87
|
-
*
|
|
88
|
-
* Download Files:
|
|
89
|
-
* ```typescript
|
|
90
|
-
* import { FileSystemService } from '@domains/filesystem';
|
|
91
|
-
*
|
|
92
|
-
* const result = await FileSystemService.downloadFile(
|
|
93
|
-
* 'https://example.com/file.pdf'
|
|
94
|
-
* );
|
|
95
|
-
* if (result.success) {
|
|
96
|
-
* console.log('Downloaded to:', result.uri);
|
|
97
|
-
* }
|
|
98
|
-
* ```
|
|
99
|
-
*
|
|
100
|
-
* Directory Operations:
|
|
101
|
-
* ```typescript
|
|
102
|
-
* import { FileSystemService, FileUtils } from '@domains/filesystem';
|
|
103
|
-
*
|
|
104
|
-
* // Get document directory
|
|
105
|
-
* const docDir = FileSystemService.getDocumentDirectory();
|
|
106
|
-
*
|
|
107
|
-
* // List files
|
|
108
|
-
* const files = await FileSystemService.listDirectory(docDir);
|
|
109
|
-
*
|
|
110
|
-
* // Clear cache
|
|
111
|
-
* await FileSystemService.clearCache();
|
|
112
|
-
* ```
|
|
113
|
-
*
|
|
114
|
-
* File Utilities:
|
|
115
|
-
* ```typescript
|
|
116
|
-
* import { FileUtils } from '@domains/filesystem';
|
|
117
|
-
*
|
|
118
|
-
* // Format file size
|
|
119
|
-
* const size = FileUtils.formatFileSize(1024000); // "1.00 MB"
|
|
120
|
-
*
|
|
121
|
-
* // Generate unique filename
|
|
122
|
-
* const filename = FileUtils.generateUniqueFilename('photo.jpg');
|
|
123
|
-
*
|
|
124
|
-
* // Sanitize filename
|
|
125
|
-
* const safe = FileUtils.sanitizeFilename('my file!@#.txt'); // "my_file___.txt"
|
|
126
|
-
* ```
|
|
127
|
-
*
|
|
128
|
-
* BENEFITS:
|
|
129
|
-
* - Centralized file operations (no duplication)
|
|
130
|
-
* - Type-safe with TypeScript
|
|
131
|
-
* - Works across all 100+ apps
|
|
132
|
-
* - Used by other domains (media, etc.)
|
|
133
|
-
* - Clean, maintainable code
|
|
134
|
-
*
|
|
135
|
-
* @see https://docs.expo.dev/versions/latest/sdk/filesystem/
|
|
2
|
+
* @umituz/react-native-filesystem
|
|
3
|
+
* File operations for React Native apps
|
|
136
4
|
*/
|
|
137
5
|
|
|
138
|
-
//
|
|
139
|
-
// BUILD-TIME MODULE LOADING
|
|
140
|
-
// ============================================================================
|
|
141
|
-
|
|
142
|
-
// Domain Layer - Module Context Entities
|
|
143
|
-
export type {
|
|
144
|
-
RequireContext,
|
|
145
|
-
ModuleCollection,
|
|
146
|
-
FileExtension,
|
|
147
|
-
} from './domain/entities/ModuleContext';
|
|
148
|
-
|
|
149
|
-
// Infrastructure Layer - Module Loading Utils
|
|
150
|
-
export {
|
|
151
|
-
loadJsonModules,
|
|
152
|
-
loadModulesWithTransform,
|
|
153
|
-
getModuleNames,
|
|
154
|
-
} from './infrastructure/utils/moduleLoader';
|
|
155
|
-
|
|
156
|
-
// ============================================================================
|
|
157
|
-
// RUNTIME FILE OPERATIONS
|
|
158
|
-
// ============================================================================
|
|
159
|
-
|
|
160
|
-
// Domain Layer - File Entities
|
|
161
|
-
export type {
|
|
162
|
-
FileInfo,
|
|
163
|
-
FileOperationResult,
|
|
164
|
-
DownloadProgress,
|
|
165
|
-
} from './domain/entities/File';
|
|
166
|
-
|
|
6
|
+
// Domain - Types and Utilities
|
|
167
7
|
export type {
|
|
168
8
|
FileEncoding,
|
|
169
9
|
DirectoryType,
|
|
170
|
-
|
|
10
|
+
FileOperationResult,
|
|
11
|
+
FileInfo,
|
|
12
|
+
DownloadProgress,
|
|
13
|
+
} from "./domain/entities/File";
|
|
171
14
|
|
|
172
|
-
export {
|
|
173
|
-
FILE_CONSTANTS,
|
|
174
|
-
FileUtils,
|
|
175
|
-
} from './domain/entities/File';
|
|
15
|
+
export { FILE_CONSTANTS, FileUtils } from "./domain/entities/File";
|
|
176
16
|
|
|
177
|
-
//
|
|
178
|
-
export {
|
|
17
|
+
// Services - File Operations
|
|
18
|
+
export { downloadFile } from "./infrastructure/services/download.service";
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import/Export Service
|
|
3
|
+
* File operations and data transfer functionality
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
ExportFormat,
|
|
8
|
+
ValidationFormat,
|
|
9
|
+
ParsedFlashcard,
|
|
10
|
+
ImportResult,
|
|
11
|
+
MediaUploadProgress,
|
|
12
|
+
ImportExportService,
|
|
13
|
+
ParsedFlashcard,
|
|
14
|
+
} from "../../domain/entities/ImportExport.types";
|
|
15
|
+
|
|
16
|
+
export class ImportExportServiceImpl implements ImportExportService {
|
|
17
|
+
private static instance: ImportExportServiceImpl;
|
|
18
|
+
|
|
19
|
+
static getInstance(): ImportExportServiceImpl {
|
|
20
|
+
if (!ImportExportServiceImpl.instance) {
|
|
21
|
+
ImportExportServiceImpl.instance = new ImportExportServiceImpl();
|
|
22
|
+
}
|
|
23
|
+
return ImportExportServiceImpl.instance;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async importFlashcards(file: File): Promise<ImportResult> {
|
|
27
|
+
try {
|
|
28
|
+
const format = this.detectFileFormat(file.name);
|
|
29
|
+
|
|
30
|
+
switch (format) {
|
|
31
|
+
case 'csv':
|
|
32
|
+
return await this.importCSV(file);
|
|
33
|
+
case 'json':
|
|
34
|
+
return await this.importJSON(file);
|
|
35
|
+
case 'anki':
|
|
36
|
+
return this.importAnki(file);
|
|
37
|
+
default:
|
|
38
|
+
throw new Error(`Unsupported file format: ${format}`);
|
|
39
|
+
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
return {
|
|
42
|
+
success: false,
|
|
43
|
+
flashcards: [],
|
|
44
|
+
errors: [error instanceof Error ? error.message : "Import failed"],
|
|
45
|
+
warnings: [],
|
|
46
|
+
duplicatesFound: 0,
|
|
47
|
+
importedCount: 0,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async importCSV(file: File): Promise<ImportResult> {
|
|
53
|
+
const text = await file.text();
|
|
54
|
+
const lines = text.split('\n').filter(line => line.trim());
|
|
55
|
+
|
|
56
|
+
if (lines.length < 2) {
|
|
57
|
+
return {
|
|
58
|
+
success: false,
|
|
59
|
+
flashcards: [],
|
|
60
|
+
errors: ["CSV file is empty or invalid"],
|
|
61
|
+
warnings: [],
|
|
62
|
+
duplicatesFound: 0,
|
|
63
|
+
importedCount: 0,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const headers = lines[0].split(',').map(h => h.trim().replace(/"/g, ''));
|
|
68
|
+
const flashcards: ParsedFlashcard[] = [];
|
|
69
|
+
const errors: string[] = [];
|
|
70
|
+
let duplicatesFound = 0;
|
|
71
|
+
|
|
72
|
+
for (let i = 1; i < lines.length; i++) {
|
|
73
|
+
try {
|
|
74
|
+
const values = lines[i].split(',').map(v => v.trim().replace(/"/g, ''));
|
|
75
|
+
const flashcard: ParsedFlashcard = {
|
|
76
|
+
front: values[0] || '',
|
|
77
|
+
back: values[1] || '',
|
|
78
|
+
tags: values[2] ? values[2].split(';').map(t => t.trim()) : [],
|
|
79
|
+
difficulty: this.parseDifficulty(values[3]),
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
if (flashcard.front && flashcard.back) {
|
|
83
|
+
// Check for duplicates
|
|
84
|
+
const isDuplicate = flashcards.some(
|
|
85
|
+
existing => existing.front === flashcard.front && existing.back === flashcard.back
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
if (!isDuplicate) {
|
|
89
|
+
flashcards.push(flashcard);
|
|
90
|
+
} else {
|
|
91
|
+
duplicatesFound++;
|
|
92
|
+
errors.push(`Duplicate flashcard found at line ${i + 1}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} catch (error) {
|
|
96
|
+
errors.push(`Line ${i + 1}: ${error}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
success: errors.length === 0,
|
|
102
|
+
flashcards,
|
|
103
|
+
errors,
|
|
104
|
+
warnings: [],
|
|
105
|
+
duplicatesFound,
|
|
106
|
+
importedCount: flashcards.length,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async importJSON(file: File): Promise<ImportResult> {
|
|
111
|
+
try {
|
|
112
|
+
const text = await file.text();
|
|
113
|
+
const data = JSON.parse(text);
|
|
114
|
+
|
|
115
|
+
let flashcards: any[];
|
|
116
|
+
|
|
117
|
+
if (Array.isArray(data)) {
|
|
118
|
+
flashcards = data.map(item => ({
|
|
119
|
+
front: item.front || item.question || '',
|
|
120
|
+
back: item.back || item.answer || '',
|
|
121
|
+
tags: Array.isArray(item.tags) ? item.tags : [],
|
|
122
|
+
difficulty: this.parseDifficulty(item.difficulty),
|
|
123
|
+
}));
|
|
124
|
+
} else if (data.flashcards && Array.isArray(data.flashcards)) {
|
|
125
|
+
flashcards = data.flashcards.map(item => ({
|
|
126
|
+
front: item.front || item.question || '',
|
|
127
|
+
back: item.back || item.answer || '',
|
|
128
|
+
tags: Array.isArray(item.tags) ? item.tags : [],
|
|
129
|
+
difficulty: this.parseDifficulty(item.difficulty),
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
success: true,
|
|
135
|
+
flashcards,
|
|
136
|
+
errors: [],
|
|
137
|
+
warnings: [],
|
|
138
|
+
duplicatesFound: 0,
|
|
139
|
+
importedCount: flashcards.length,
|
|
140
|
+
};
|
|
141
|
+
} catch (error) {
|
|
142
|
+
return {
|
|
143
|
+
success: false,
|
|
144
|
+
flashcards: [],
|
|
145
|
+
errors: [`JSON parsing failed: ${error instanceof Error ? error.message : "Unknown error"}`],
|
|
146
|
+
warnings: [],
|
|
147
|
+
duplicatesFound: 0,
|
|
148
|
+
importedCount: 0,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async importAnki(file: File): Promise<ImportResult> {
|
|
154
|
+
// Mock Anki import - would require actual APKG parsing in production
|
|
155
|
+
return {
|
|
156
|
+
success: false,
|
|
157
|
+
flashcards: [],
|
|
158
|
+
errors: ["Anki import not yet implemented"],
|
|
159
|
+
warnings: [],
|
|
160
|
+
duplicatesFound: 0,
|
|
161
|
+
importedCount: 0,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async exportFlashcards(
|
|
166
|
+
flashcards: any[],
|
|
167
|
+
options: ExportOptions = { format: 'json' }
|
|
168
|
+
): Promise<{ success: boolean; data: Blob; filename: string }> {
|
|
169
|
+
try {
|
|
170
|
+
const data = flashcards.map(fc => ({
|
|
171
|
+
front: fc.front,
|
|
172
|
+
back: fc.back,
|
|
173
|
+
tags: fc.tags || [],
|
|
174
|
+
difficulty: fc.difficulty || 'medium',
|
|
175
|
+
}));
|
|
176
|
+
|
|
177
|
+
let filename = options.filename || `flashcards_export_${Date.now()}`;
|
|
178
|
+
|
|
179
|
+
switch (options.format) {
|
|
180
|
+
case 'json':
|
|
181
|
+
const jsonData = JSON.stringify(data, null, 2);
|
|
182
|
+
return {
|
|
183
|
+
success: true,
|
|
184
|
+
data: new Blob([jsonData], { type: 'application/json' }),
|
|
185
|
+
filename: filename.endsWith('.json') ? filename : `${filename}.json`,
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
case 'csv':
|
|
189
|
+
const csvData = [
|
|
190
|
+
'front,back,tags,difficulty',
|
|
191
|
+
...data.map(fc => [
|
|
192
|
+
`"${fc.front.replace(/"/g, '""')}"`,
|
|
193
|
+
`"${fc.back.replace(/"/g, '""')}"`,
|
|
194
|
+
`"${(fc.tags || []).join(';')}"`,
|
|
195
|
+
`${fc.difficulty || 'medium'}`,
|
|
196
|
+
]),
|
|
197
|
+
].join('\n'),
|
|
198
|
+
];
|
|
199
|
+
return {
|
|
200
|
+
success: true,
|
|
201
|
+
data: new Blob([csvData], { type: 'text/csv' }),
|
|
202
|
+
filename: filename.endsWith('.csv') ? filename : `${filename}.csv`,
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
default:
|
|
206
|
+
throw new Error(`Unsupported export format: ${options.format}`);
|
|
207
|
+
}
|
|
208
|
+
} catch (error) {
|
|
209
|
+
throw new Error(`Export failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async validateData(data: any, format: ValidationFormat): Promise<{
|
|
214
|
+
isValid: boolean;
|
|
215
|
+
errors: string[];
|
|
216
|
+
warnings: string[];
|
|
217
|
+
recommendations: string[];
|
|
218
|
+
}> {
|
|
219
|
+
const errors: string[] = [];
|
|
220
|
+
const warnings: string[] = [];
|
|
221
|
+
const recommendations: string[] = [];
|
|
222
|
+
|
|
223
|
+
if (format === 'json') {
|
|
224
|
+
try {
|
|
225
|
+
JSON.parse(data);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
errors.push('Invalid JSON format');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (format === 'csv') {
|
|
232
|
+
const lines = data.split('\n');
|
|
233
|
+
if (lines.length > 10000) {
|
|
234
|
+
warnings.push('Large CSV files may impact performance');
|
|
235
|
+
recommendations.push('Consider splitting large files into smaller chunks');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
isValid: errors.length === 0,
|
|
241
|
+
errors,
|
|
242
|
+
warnings,
|
|
243
|
+
recommendations,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private detectFileFormat(filename: string): string {
|
|
248
|
+
const extension = filename.split('.').pop()?.toLowerCase();
|
|
249
|
+
|
|
250
|
+
switch (extension) {
|
|
251
|
+
case 'csv': return 'csv';
|
|
252
|
+
case 'json': return 'json';
|
|
253
|
+
case 'apkg': return 'anki';
|
|
254
|
+
case 'txt': return 'csv';
|
|
255
|
+
default: return 'csv';
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private parseDifficulty(difficulty?: string): 'easy' | 'medium' | 'hard' {
|
|
260
|
+
if (!difficulty) return 'medium';
|
|
261
|
+
|
|
262
|
+
const lower = difficulty.toLowerCase();
|
|
263
|
+
if (lower.includes('easy') || lower === '1') return 'easy';
|
|
264
|
+
if (lower.includes('hard') || lower === '3') return 'hard';
|
|
265
|
+
if (lower.includes('medium') || lower === '2') return 'medium';
|
|
266
|
+
return 'medium';
|
|
267
|
+
}
|
|
268
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import/Export Hooks
|
|
3
|
+
* React hooks for file operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import type {
|
|
8
|
+
ImportResult,
|
|
9
|
+
ExportOptions,
|
|
10
|
+
ParsedFlashcard,
|
|
11
|
+
MediaUploadProgress,
|
|
12
|
+
ImportExportService,
|
|
13
|
+
} from "../../domain/entities/ImportExport.types";
|
|
14
|
+
|
|
15
|
+
export interface UseImportExportResult {
|
|
16
|
+
importFlashcards: (file: File, format: string) => Promise<ImportResult>;
|
|
17
|
+
isImporting: boolean;
|
|
18
|
+
uploadProgress: MediaUploadProgress | null;
|
|
19
|
+
error: string | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface UseExportResult {
|
|
23
|
+
exportFlashcards: (
|
|
24
|
+
flashcards: any[],
|
|
25
|
+
options?: ExportOptions,
|
|
26
|
+
) => Promise<{
|
|
27
|
+
success: boolean;
|
|
28
|
+
data: Blob;
|
|
29
|
+
filename: string;
|
|
30
|
+
}>;
|
|
31
|
+
isExporting: boolean;
|
|
32
|
+
error: string | null;
|
|
33
|
+
reset: () => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Mock React Query implementation
|
|
37
|
+
const useMockQuery = (queryKey: any[], queryFn: any) => {
|
|
38
|
+
const [data, setData] = React.useState<any>(null);
|
|
39
|
+
const [loading, setLoading] = React.useState(true);
|
|
40
|
+
const [error, setError] = React.useState<string | null>(null);
|
|
41
|
+
|
|
42
|
+
React.useEffect(() => {
|
|
43
|
+
queryFn()
|
|
44
|
+
.then(setData)
|
|
45
|
+
.catch(setError)
|
|
46
|
+
.finally(() => setLoading(false));
|
|
47
|
+
}, [queryKey]);
|
|
48
|
+
|
|
49
|
+
return { data, loading, error, refetch: () => {} };
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const useImportExport = (): UseImportExportResult => {
|
|
53
|
+
const [isImporting, setIsImporting] = React.useState(false);
|
|
54
|
+
const [uploadProgress, setUploadProgress] =
|
|
55
|
+
React.useState<MediaUploadProgress | null>(null);
|
|
56
|
+
const [error, setError] = React.useState<string | null>(null);
|
|
57
|
+
|
|
58
|
+
const importFlashcards = React.useCallback(
|
|
59
|
+
async (file: File, format: string): Promise<ImportResult> => {
|
|
60
|
+
try {
|
|
61
|
+
setIsImporting(true);
|
|
62
|
+
setError(null);
|
|
63
|
+
setUploadProgress({
|
|
64
|
+
fileId: `import_${Date.now()}`,
|
|
65
|
+
progress: 0,
|
|
66
|
+
status: "uploading",
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Simulate import
|
|
70
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
71
|
+
|
|
72
|
+
// Mock implementation - would use actual ImportExportService
|
|
73
|
+
const result: ImportResult = {
|
|
74
|
+
success: true,
|
|
75
|
+
flashcards: [
|
|
76
|
+
{
|
|
77
|
+
front: "Sample question 1",
|
|
78
|
+
back: "Sample answer 1",
|
|
79
|
+
difficulty: "easy",
|
|
80
|
+
tags: ["sample", "import"],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
front: "Sample question 2",
|
|
84
|
+
back: "Sample answer 2",
|
|
85
|
+
difficulty: "medium",
|
|
86
|
+
tags: ["sample", "import"],
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
errors: [],
|
|
90
|
+
warnings: [],
|
|
91
|
+
duplicatesFound: 0,
|
|
92
|
+
importedCount: 2,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
setUploadProgress({
|
|
96
|
+
fileId: `import_${Date.now()}`,
|
|
97
|
+
progress: 100,
|
|
98
|
+
status: "completed",
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return result;
|
|
102
|
+
} catch (err) {
|
|
103
|
+
const errorMessage =
|
|
104
|
+
err instanceof Error ? err.message : "Import failed";
|
|
105
|
+
setError(errorMessage);
|
|
106
|
+
setIsImporting(false);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
success: false,
|
|
110
|
+
flashcards: [],
|
|
111
|
+
errors: [errorMessage],
|
|
112
|
+
warnings: [],
|
|
113
|
+
duplicatesFound: 0,
|
|
114
|
+
importedCount: 0,
|
|
115
|
+
};
|
|
116
|
+
} finally {
|
|
117
|
+
setIsImporting(false);
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
[],
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const exportFlashcards = React.useCallback(
|
|
124
|
+
(
|
|
125
|
+
flashcards: any[],
|
|
126
|
+
options?: ExportOptions = { format: "json" },
|
|
127
|
+
): Promise<{
|
|
128
|
+
success: boolean;
|
|
129
|
+
data: Blob;
|
|
130
|
+
filename: string;
|
|
131
|
+
}> => {
|
|
132
|
+
try {
|
|
133
|
+
setError(null);
|
|
134
|
+
|
|
135
|
+
// Simulate export
|
|
136
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
137
|
+
|
|
138
|
+
const result = {
|
|
139
|
+
success: true,
|
|
140
|
+
data: new Blob([JSON.stringify(flashcards, null, 2)], {
|
|
141
|
+
type: "application/json",
|
|
142
|
+
}),
|
|
143
|
+
filename: options.filename || `flashcards_export_${Date.now()}.json`,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
return result;
|
|
147
|
+
} catch (err) {
|
|
148
|
+
const errorMessage =
|
|
149
|
+
err instanceof Error ? err.message : "Export failed";
|
|
150
|
+
setError(errorMessage);
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
success: false,
|
|
154
|
+
data: new Blob(),
|
|
155
|
+
filename: "flashcards_export_error.json",
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
[],
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const reset = React.useCallback(() => {
|
|
163
|
+
setError(null);
|
|
164
|
+
setIsImporting(false);
|
|
165
|
+
setUploadProgress(null);
|
|
166
|
+
}, []);
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
importFlashcards,
|
|
170
|
+
isImporting,
|
|
171
|
+
uploadProgress,
|
|
172
|
+
error,
|
|
173
|
+
reset,
|
|
174
|
+
};
|
|
175
|
+
};
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Module Loader Utilities - DEPRECATED
|
|
3
|
-
*
|
|
4
|
-
* These functions are deprecated and no longer used.
|
|
5
|
-
* Kept for backward compatibility only.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { ModuleCollection } from '../../domain/entities/ModuleContext';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* @deprecated No longer used - kept for backward compatibility
|
|
12
|
-
*/
|
|
13
|
-
export function loadJsonModules(): ModuleCollection {
|
|
14
|
-
return {};
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* @deprecated No longer used - kept for backward compatibility
|
|
19
|
-
*/
|
|
20
|
-
export function loadModulesWithTransform(): ModuleCollection {
|
|
21
|
-
return {};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* @deprecated No longer used - kept for backward compatibility
|
|
26
|
-
*/
|
|
27
|
-
export function getModuleNames(): string[] {
|
|
28
|
-
return [];
|
|
29
|
-
}
|