@umituz/react-native-filesystem 2.1.10 → 2.1.11
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 +1 -1
- package/src/index.ts +24 -1
- package/src/infrastructure/services/download.service.ts +173 -4
- package/src/domain/entities/ImportExport.types.ts +0 -73
- package/src/domain/entities/ModuleContext.ts +0 -31
- package/src/infrastructure/services/ImportExportService.ts +0 -268
- package/src/presentation/hooks/useImportExport.ts +0 -175
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -14,6 +14,29 @@ export type {
|
|
|
14
14
|
|
|
15
15
|
export { FILE_CONSTANTS, FileUtils } from "./domain/entities/File";
|
|
16
16
|
|
|
17
|
+
// Services - Download Operations
|
|
18
|
+
export type {
|
|
19
|
+
DownloadProgressCallback,
|
|
20
|
+
DownloadWithProgressResult,
|
|
21
|
+
} from "./infrastructure/services/download.service";
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
downloadFile,
|
|
25
|
+
downloadFileWithProgress,
|
|
26
|
+
isUrlCached,
|
|
27
|
+
getCachedFileUri,
|
|
28
|
+
deleteCachedFile,
|
|
29
|
+
} from "./infrastructure/services/download.service";
|
|
30
|
+
|
|
31
|
+
// Services - Directory Operations
|
|
32
|
+
export {
|
|
33
|
+
getCacheDirectory,
|
|
34
|
+
getDocumentDirectory,
|
|
35
|
+
createDirectory,
|
|
36
|
+
} from "./infrastructure/services/directory.service";
|
|
37
|
+
|
|
17
38
|
// Services - File Operations
|
|
18
|
-
export { downloadFile } from "./infrastructure/services/download.service";
|
|
19
39
|
export { FileSystemService } from "./infrastructure/services/FileSystemService";
|
|
40
|
+
export { fileExists, getFileInfo } from "./infrastructure/services/file-info.service";
|
|
41
|
+
export { deleteFile } from "./infrastructure/services/file-manager.service";
|
|
42
|
+
export { clearCache } from "./infrastructure/services/cache.service";
|
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Download Service
|
|
3
|
-
* Single Responsibility: Download files from URLs
|
|
3
|
+
* Single Responsibility: Download files from URLs with progress tracking
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { File, Paths } from "expo-file-system";
|
|
7
|
-
import type { FileOperationResult } from "../../domain/entities/File";
|
|
6
|
+
import { File, Paths, Directory } from "expo-file-system";
|
|
7
|
+
import type { FileOperationResult, DownloadProgress } from "../../domain/entities/File";
|
|
8
8
|
import { FileUtils } from "../../domain/entities/File";
|
|
9
9
|
|
|
10
|
+
/** Progress callback type */
|
|
11
|
+
export type DownloadProgressCallback = (progress: DownloadProgress) => void;
|
|
12
|
+
|
|
13
|
+
/** Download with progress result */
|
|
14
|
+
export interface DownloadWithProgressResult extends FileOperationResult {
|
|
15
|
+
fromCache?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
10
18
|
/**
|
|
11
|
-
* Download file from URL
|
|
19
|
+
* Download file from URL (simple version without progress)
|
|
12
20
|
*/
|
|
13
21
|
export async function downloadFile(
|
|
14
22
|
url: string,
|
|
@@ -36,3 +44,164 @@ export async function downloadFile(
|
|
|
36
44
|
}
|
|
37
45
|
}
|
|
38
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Generate hash from URL for cache filename
|
|
49
|
+
*/
|
|
50
|
+
function hashUrl(url: string): string {
|
|
51
|
+
let hash = 0;
|
|
52
|
+
for (let i = 0; i < url.length; i++) {
|
|
53
|
+
const char = url.charCodeAt(i);
|
|
54
|
+
hash = (hash << 5) - hash + char;
|
|
55
|
+
hash = hash & hash;
|
|
56
|
+
}
|
|
57
|
+
return Math.abs(hash).toString(36);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get extension from URL
|
|
62
|
+
*/
|
|
63
|
+
function getExtensionFromUrl(url: string): string {
|
|
64
|
+
const urlPath = url.split("?")[0];
|
|
65
|
+
const ext = urlPath.split(".").pop()?.toLowerCase() || "mp4";
|
|
66
|
+
const validExts = ["mp4", "mov", "m4v", "webm", "jpg", "png", "pdf"];
|
|
67
|
+
return validExts.includes(ext) ? ext : "mp4";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Download file with progress tracking (uses expo/fetch for progress)
|
|
72
|
+
* Falls back to simple download if progress not needed
|
|
73
|
+
*/
|
|
74
|
+
export async function downloadFileWithProgress(
|
|
75
|
+
url: string,
|
|
76
|
+
cacheDir: string,
|
|
77
|
+
onProgress?: DownloadProgressCallback,
|
|
78
|
+
): Promise<DownloadWithProgressResult> {
|
|
79
|
+
try {
|
|
80
|
+
// Ensure cache directory exists
|
|
81
|
+
const dir = new Directory(cacheDir);
|
|
82
|
+
if (!dir.exists) {
|
|
83
|
+
dir.create({ intermediates: true, idempotent: true });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Generate filename from URL hash
|
|
87
|
+
const hash = hashUrl(url);
|
|
88
|
+
const ext = getExtensionFromUrl(url);
|
|
89
|
+
const filename = `cached_${hash}.${ext}`;
|
|
90
|
+
const destUri = FileUtils.joinPaths(cacheDir, filename);
|
|
91
|
+
|
|
92
|
+
// Check if already cached
|
|
93
|
+
const cachedFile = new File(destUri);
|
|
94
|
+
if (cachedFile.exists && (cachedFile.size ?? 0) > 0) {
|
|
95
|
+
return { success: true, uri: destUri, fromCache: true };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Download using fetch with progress (expo/fetch)
|
|
99
|
+
const response = await fetch(url);
|
|
100
|
+
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const contentLength = response.headers.get("content-length");
|
|
106
|
+
const totalBytes = contentLength ? parseInt(contentLength, 10) : 0;
|
|
107
|
+
|
|
108
|
+
if (!response.body) {
|
|
109
|
+
// Fallback: no streaming support, use simple download
|
|
110
|
+
const result = await downloadFile(url, destUri);
|
|
111
|
+
return { ...result, fromCache: false };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Stream download with progress
|
|
115
|
+
const reader = response.body.getReader();
|
|
116
|
+
const chunks: Uint8Array[] = [];
|
|
117
|
+
let receivedBytes = 0;
|
|
118
|
+
|
|
119
|
+
while (true) {
|
|
120
|
+
const { done, value } = await reader.read();
|
|
121
|
+
if (done) break;
|
|
122
|
+
|
|
123
|
+
chunks.push(value);
|
|
124
|
+
receivedBytes += value.length;
|
|
125
|
+
|
|
126
|
+
if (onProgress) {
|
|
127
|
+
onProgress({
|
|
128
|
+
totalBytesWritten: receivedBytes,
|
|
129
|
+
totalBytesExpectedToWrite: totalBytes || receivedBytes,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Combine chunks and write to file
|
|
135
|
+
const allChunks = new Uint8Array(receivedBytes);
|
|
136
|
+
let position = 0;
|
|
137
|
+
for (const chunk of chunks) {
|
|
138
|
+
allChunks.set(chunk, position);
|
|
139
|
+
position += chunk.length;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Write bytes to file
|
|
143
|
+
const file = new File(destUri);
|
|
144
|
+
file.write(allChunks);
|
|
145
|
+
|
|
146
|
+
return { success: true, uri: destUri, fromCache: false };
|
|
147
|
+
} catch (error) {
|
|
148
|
+
return {
|
|
149
|
+
success: false,
|
|
150
|
+
error: error instanceof Error ? error.message : "Download failed",
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Check if URL is cached
|
|
157
|
+
*/
|
|
158
|
+
export function isUrlCached(url: string, cacheDir: string): boolean {
|
|
159
|
+
try {
|
|
160
|
+
const hash = hashUrl(url);
|
|
161
|
+
const ext = getExtensionFromUrl(url);
|
|
162
|
+
const filename = `cached_${hash}.${ext}`;
|
|
163
|
+
const destUri = FileUtils.joinPaths(cacheDir, filename);
|
|
164
|
+
const file = new File(destUri);
|
|
165
|
+
return file.exists && (file.size ?? 0) > 0;
|
|
166
|
+
} catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get cached file URI if exists
|
|
173
|
+
*/
|
|
174
|
+
export function getCachedFileUri(url: string, cacheDir: string): string | null {
|
|
175
|
+
try {
|
|
176
|
+
const hash = hashUrl(url);
|
|
177
|
+
const ext = getExtensionFromUrl(url);
|
|
178
|
+
const filename = `cached_${hash}.${ext}`;
|
|
179
|
+
const destUri = FileUtils.joinPaths(cacheDir, filename);
|
|
180
|
+
const file = new File(destUri);
|
|
181
|
+
if (file.exists && (file.size ?? 0) > 0) {
|
|
182
|
+
return destUri;
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Delete cached file
|
|
192
|
+
*/
|
|
193
|
+
export function deleteCachedFile(url: string, cacheDir: string): boolean {
|
|
194
|
+
try {
|
|
195
|
+
const hash = hashUrl(url);
|
|
196
|
+
const ext = getExtensionFromUrl(url);
|
|
197
|
+
const filename = `cached_${hash}.${ext}`;
|
|
198
|
+
const destUri = FileUtils.joinPaths(cacheDir, filename);
|
|
199
|
+
const file = new File(destUri);
|
|
200
|
+
if (file.exists) {
|
|
201
|
+
file.delete();
|
|
202
|
+
}
|
|
203
|
+
return true;
|
|
204
|
+
} catch {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -1,73 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Module Context Entity
|
|
3
|
-
* Domain layer types for build-time module loading
|
|
4
|
-
*
|
|
5
|
-
* DEPRECATED: These types are kept for backward compatibility only.
|
|
6
|
-
* The module loading functionality is no longer actively used.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* RequireFunction interface for Webpack/Metro require.context
|
|
11
|
-
* @deprecated No longer actively used
|
|
12
|
-
*/
|
|
13
|
-
export interface RequireContext {
|
|
14
|
-
keys(): string[];
|
|
15
|
-
(id: string): any;
|
|
16
|
-
<T>(id: string): T;
|
|
17
|
-
resolve(id: string): string;
|
|
18
|
-
id: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Collection of loaded modules
|
|
23
|
-
* @deprecated No longer actively used
|
|
24
|
-
*/
|
|
25
|
-
export type ModuleCollection = Record<string, any>;
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Supported file extensions for module loading
|
|
29
|
-
* @deprecated No longer actively used
|
|
30
|
-
*/
|
|
31
|
-
export type FileExtension = '.json' | '.js' | '.ts' | '.tsx';
|
|
@@ -1,268 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,175 +0,0 @@
|
|
|
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
|
-
};
|