Line data Source code
1 : import 'package:flutter/foundation.dart';
2 :
3 : /// Typed result helpers for built-in workers.
4 : ///
5 : /// Each class wraps the raw `resultData: Map<String, dynamic>?` returned by
6 : /// [TaskEvent] and exposes typed, named fields. This eliminates runtime
7 : /// `as` casts and makes result handling refactor-safe.
8 : ///
9 : /// ## Usage
10 : ///
11 : /// ```dart
12 : /// NativeWorkManager.events.listen((event) {
13 : /// if (!event.success) return;
14 : ///
15 : /// switch (event.workerClassName) {
16 : /// case 'HttpDownloadWorker':
17 : /// final r = DownloadResult.from(event.resultData);
18 : /// print('Saved to ${r?.filePath} (${r?.fileSize} bytes)');
19 : /// case 'HttpUploadWorker':
20 : /// final r = UploadResult.from(event.resultData);
21 : /// print('Uploaded ${r?.fileCount} files, ${r?.uploadedSize} bytes');
22 : /// case 'CryptoWorker':
23 : /// final r = CryptoResult.from(event.resultData);
24 : /// print('Hash: ${r?.hash}');
25 : /// }
26 : /// });
27 : /// ```
28 :
29 : // ── Download ─────────────────────────────────────────────────────────────────
30 :
31 : /// Result data from [HttpDownloadWorker] and [ParallelHttpDownloadWorker].
32 : @immutable
33 : class DownloadResult {
34 1 : const DownloadResult({
35 : required this.filePath,
36 : required this.fileName,
37 : required this.fileSize,
38 : this.contentType,
39 : this.finalUrl,
40 : this.serverSuggestedName,
41 : this.skipped = false,
42 : });
43 :
44 : /// Absolute path of the saved file.
45 : final String filePath;
46 :
47 : /// Filename (last segment of [filePath]).
48 : final String fileName;
49 :
50 : /// File size in bytes.
51 : final int fileSize;
52 :
53 : /// MIME type from the `Content-Type` response header, if present.
54 : final String? contentType;
55 :
56 : /// Final URL after any redirects.
57 : final String? finalUrl;
58 :
59 : /// Filename suggested by the server's `Content-Disposition` header.
60 : final String? serverSuggestedName;
61 :
62 : /// `true` when the download was skipped because the file already existed
63 : /// and `skipExisting` or `onDuplicate: skip` was set.
64 : final bool skipped;
65 :
66 : /// Parse from a raw [TaskEvent.resultData] map. Returns `null` if [data] is
67 : /// `null` or missing required fields.
68 1 : static DownloadResult? from(Map<String, dynamic>? data) {
69 : if (data == null) return null;
70 1 : final fp = data['filePath'] as String?;
71 1 : final fn = data['fileName'] as String?;
72 1 : final fs = data['fileSize'];
73 : if (fp == null || fn == null || fs == null) return null;
74 1 : return DownloadResult(
75 : filePath: fp,
76 : fileName: fn,
77 1 : fileSize: (fs as num).toInt(),
78 1 : contentType: data['contentType'] as String?,
79 1 : finalUrl: data['finalUrl'] as String?,
80 1 : serverSuggestedName: data['serverSuggestedName'] as String?,
81 1 : skipped: (data['skipped'] as bool?) ?? false,
82 : );
83 : }
84 :
85 1 : @override
86 : String toString() =>
87 4 : 'DownloadResult(filePath: $filePath, fileSize: $fileSize, skipped: $skipped)';
88 : }
89 :
90 : // ── Parallel download ─────────────────────────────────────────────────────────
91 :
92 : /// Per-file outcome inside [ParallelDownloadResult.files].
93 : @immutable
94 : class DownloadFileOutcome {
95 1 : const DownloadFileOutcome({
96 : required this.url,
97 : required this.success,
98 : this.filePath,
99 : this.fileName,
100 : this.fileSize,
101 : this.error,
102 : });
103 :
104 : final String url;
105 : final bool success;
106 : final String? filePath;
107 : final String? fileName;
108 : final int? fileSize;
109 : final String? error;
110 :
111 1 : static DownloadFileOutcome _from(Map<String, dynamic> m) =>
112 1 : DownloadFileOutcome(
113 1 : url: (m['url'] as String?) ?? '',
114 1 : success: (m['success'] as bool?) ?? false,
115 1 : filePath: m['filePath'] as String?,
116 1 : fileName: m['fileName'] as String?,
117 2 : fileSize: (m['fileSize'] as num?)?.toInt(),
118 1 : error: m['error'] as String?,
119 : );
120 : }
121 :
122 : /// Result data from [ParallelHttpDownloadWorker].
123 : @immutable
124 : class ParallelDownloadResult {
125 1 : const ParallelDownloadResult({
126 : required this.downloadedCount,
127 : required this.failedCount,
128 : required this.totalBytes,
129 : required this.files,
130 : });
131 :
132 : final int downloadedCount;
133 : final int failedCount;
134 : final int totalBytes;
135 : final List<DownloadFileOutcome> files;
136 :
137 1 : static ParallelDownloadResult? from(Map<String, dynamic>? data) {
138 : if (data == null) return null;
139 1 : final rawFiles = data['fileResults'] as List?;
140 1 : return ParallelDownloadResult(
141 2 : downloadedCount: (data['downloadedCount'] as num?)?.toInt() ?? 0,
142 2 : failedCount: (data['failedCount'] as num?)?.toInt() ?? 0,
143 2 : totalBytes: (data['totalBytes'] as num?)?.toInt() ?? 0,
144 : files: rawFiles
145 1 : ?.whereType<Map>()
146 2 : .map((e) =>
147 2 : DownloadFileOutcome._from(Map<String, dynamic>.from(e)))
148 1 : .toList() ??
149 : const [],
150 : );
151 : }
152 : }
153 :
154 : // ── Upload ────────────────────────────────────────────────────────────────────
155 :
156 : /// Result data from [HttpUploadWorker].
157 : @immutable
158 : class UploadResult {
159 1 : const UploadResult({
160 : required this.statusCode,
161 : required this.uploadedSize,
162 : required this.fileCount,
163 : this.responseBody,
164 : });
165 :
166 : final int statusCode;
167 :
168 : /// Total bytes sent.
169 : final int uploadedSize;
170 :
171 : /// Number of files included in the upload.
172 : final int fileCount;
173 :
174 : /// Raw response body from the server, if any.
175 : final String? responseBody;
176 :
177 1 : static UploadResult? from(Map<String, dynamic>? data) {
178 : if (data == null) return null;
179 1 : return UploadResult(
180 2 : statusCode: (data['statusCode'] as num?)?.toInt() ?? 0,
181 2 : uploadedSize: (data['uploadedSize'] as num?)?.toInt() ?? 0,
182 2 : fileCount: (data['fileCount'] as num?)?.toInt() ?? 0,
183 1 : responseBody: data['responseBody'] as String?,
184 : );
185 : }
186 : }
187 :
188 : // ── Parallel upload ───────────────────────────────────────────────────────────
189 :
190 : /// Per-file outcome inside [ParallelUploadResult.files].
191 : @immutable
192 : class UploadFileOutcome {
193 1 : const UploadFileOutcome({
194 : required this.fileName,
195 : required this.filePath,
196 : required this.fileSize,
197 : required this.success,
198 : this.statusCode,
199 : this.responseBody,
200 : this.error,
201 : });
202 :
203 : final String fileName;
204 : final String filePath;
205 : final int fileSize;
206 : final bool success;
207 : final int? statusCode;
208 : final String? responseBody;
209 : final String? error;
210 :
211 2 : static UploadFileOutcome _from(Map<String, dynamic> m) => UploadFileOutcome(
212 1 : fileName: (m['fileName'] as String?) ?? '',
213 1 : filePath: (m['filePath'] as String?) ?? '',
214 2 : fileSize: (m['fileSize'] as num?)?.toInt() ?? 0,
215 1 : success: (m['success'] as bool?) ?? false,
216 2 : statusCode: (m['statusCode'] as num?)?.toInt(),
217 1 : responseBody: m['responseBody'] as String?,
218 1 : error: m['error'] as String?,
219 : );
220 : }
221 :
222 : /// Result data from [ParallelHttpUploadWorker].
223 : @immutable
224 : class ParallelUploadResult {
225 1 : const ParallelUploadResult({
226 : required this.uploadedCount,
227 : required this.failedCount,
228 : required this.totalBytes,
229 : required this.files,
230 : });
231 :
232 : final int uploadedCount;
233 : final int failedCount;
234 : final int totalBytes;
235 : final List<UploadFileOutcome> files;
236 :
237 1 : static ParallelUploadResult? from(Map<String, dynamic>? data) {
238 : if (data == null) return null;
239 1 : final rawFiles = data['fileResults'] as List?;
240 1 : return ParallelUploadResult(
241 2 : uploadedCount: (data['uploadedCount'] as num?)?.toInt() ?? 0,
242 2 : failedCount: (data['failedCount'] as num?)?.toInt() ?? 0,
243 2 : totalBytes: (data['totalBytes'] as num?)?.toInt() ?? 0,
244 : files: rawFiles
245 1 : ?.whereType<Map>()
246 4 : .map((e) => UploadFileOutcome._from(Map<String, dynamic>.from(e)))
247 1 : .toList() ??
248 : const [],
249 : );
250 : }
251 : }
252 :
253 : // ── HTTP request ──────────────────────────────────────────────────────────────
254 :
255 : /// Result data from [HttpRequestWorker].
256 : @immutable
257 : class HttpRequestResult {
258 1 : const HttpRequestResult({
259 : required this.statusCode,
260 : required this.body,
261 : required this.contentLength,
262 : });
263 :
264 : final int statusCode;
265 : final String body;
266 : final int contentLength;
267 :
268 1 : static HttpRequestResult? from(Map<String, dynamic>? data) {
269 : if (data == null) return null;
270 1 : return HttpRequestResult(
271 2 : statusCode: (data['statusCode'] as num?)?.toInt() ?? 0,
272 1 : body: (data['body'] as String?) ?? '',
273 2 : contentLength: (data['contentLength'] as num?)?.toInt() ?? 0,
274 : );
275 : }
276 : }
277 :
278 : // ── Crypto ────────────────────────────────────────────────────────────────────
279 :
280 : /// Result data from [CryptoHashWorker], [CryptoEncryptWorker], or [CryptoDecryptWorker] operations.
281 : @immutable
282 : class CryptoResult {
283 1 : const CryptoResult({
284 : this.hash,
285 : this.algorithm,
286 : this.outputPath,
287 : this.fileSize,
288 : this.operation,
289 : });
290 :
291 : /// Hex-encoded hash digest (for `hash` operations).
292 : final String? hash;
293 :
294 : /// Hash algorithm used (e.g. `'SHA-256'`).
295 : final String? algorithm;
296 :
297 : /// Output file path (for encrypt/decrypt operations).
298 : final String? outputPath;
299 :
300 : /// Output file size in bytes.
301 : final int? fileSize;
302 :
303 : /// Operation performed: `'hash'`, `'encrypt'`, or `'decrypt'`.
304 : final String? operation;
305 :
306 1 : static CryptoResult? from(Map<String, dynamic>? data) {
307 : if (data == null) return null;
308 1 : return CryptoResult(
309 1 : hash: data['hash'] as String?,
310 1 : algorithm: data['algorithm'] as String?,
311 1 : outputPath: data['outputPath'] as String?,
312 2 : fileSize: (data['fileSize'] as num?)?.toInt(),
313 1 : operation: data['operation'] as String?,
314 : );
315 : }
316 : }
317 :
318 : // ── File compression / decompression ─────────────────────────────────────────
319 :
320 : /// Result data from [FileCompressionWorker].
321 : @immutable
322 : class CompressionResult {
323 1 : const CompressionResult({
324 : required this.outputPath,
325 : required this.fileCount,
326 : required this.totalSize,
327 : required this.compressedSize,
328 : });
329 :
330 : final String outputPath;
331 : final int fileCount;
332 : final int totalSize;
333 : final int compressedSize;
334 :
335 1 : double get compressionRatio =>
336 5 : totalSize > 0 ? compressedSize / totalSize : 1.0;
337 :
338 1 : static CompressionResult? from(Map<String, dynamic>? data) {
339 : if (data == null) return null;
340 1 : final op = data['outputPath'] as String?;
341 : if (op == null) return null;
342 1 : return CompressionResult(
343 : outputPath: op,
344 2 : fileCount: (data['fileCount'] as num?)?.toInt() ?? 0,
345 2 : totalSize: (data['totalSize'] as num?)?.toInt() ?? 0,
346 2 : compressedSize: (data['compressedSize'] as num?)?.toInt() ?? 0,
347 : );
348 : }
349 : }
350 :
351 : /// Result data from [FileDecompressionWorker].
352 : @immutable
353 : class DecompressionResult {
354 1 : const DecompressionResult({
355 : required this.outputPath,
356 : required this.extractedCount,
357 : required this.totalSize,
358 : });
359 :
360 : final String outputPath;
361 : final int extractedCount;
362 : final int totalSize;
363 :
364 1 : static DecompressionResult? from(Map<String, dynamic>? data) {
365 : if (data == null) return null;
366 1 : final op = data['outputPath'] as String?;
367 : if (op == null) return null;
368 1 : return DecompressionResult(
369 : outputPath: op,
370 2 : extractedCount: (data['extractedCount'] as num?)?.toInt() ?? 0,
371 2 : totalSize: (data['totalSize'] as num?)?.toInt() ?? 0,
372 : );
373 : }
374 : }
375 :
376 : // ── Image processing ──────────────────────────────────────────────────────────
377 :
378 : /// Result data from [ImageProcessWorker].
379 : @immutable
380 : class ImageProcessResult {
381 1 : const ImageProcessResult({
382 : required this.outputPath,
383 : required this.width,
384 : required this.height,
385 : required this.fileSize,
386 : this.format,
387 : });
388 :
389 : final String outputPath;
390 : final int width;
391 : final int height;
392 : final int fileSize;
393 :
394 : /// Output image format (e.g. `'jpeg'`, `'png'`, `'webp'`).
395 : final String? format;
396 :
397 1 : static ImageProcessResult? from(Map<String, dynamic>? data) {
398 : if (data == null) return null;
399 1 : final op = data['outputPath'] as String?;
400 : if (op == null) return null;
401 1 : return ImageProcessResult(
402 : outputPath: op,
403 2 : width: (data['width'] as num?)?.toInt() ?? 0,
404 2 : height: (data['height'] as num?)?.toInt() ?? 0,
405 2 : fileSize: (data['fileSize'] as num?)?.toInt() ?? 0,
406 1 : format: data['format'] as String?,
407 : );
408 : }
409 : }
410 :
411 : // ── File system ───────────────────────────────────────────────────────────────
412 :
413 : /// Result data from file system workers ([FileSystemCopyWorker], [FileSystemMoveWorker], etc.).
414 : @immutable
415 : class FileSystemResult {
416 1 : const FileSystemResult({
417 : required this.operation,
418 : this.sourcePath,
419 : this.destinationPath,
420 : this.entries,
421 : this.count,
422 : });
423 :
424 : /// Operation performed: `'copy'`, `'move'`, `'delete'`, `'list'`, `'mkdir'`.
425 : final String operation;
426 : final String? sourcePath;
427 : final String? destinationPath;
428 :
429 : /// For `'list'` operations: list of file/directory paths.
430 : final List<String>? entries;
431 :
432 : /// For `'delete'` or batch operations: number of items affected.
433 : final int? count;
434 :
435 1 : static FileSystemResult? from(Map<String, dynamic>? data) {
436 : if (data == null) return null;
437 1 : final op = data['operation'] as String?;
438 : if (op == null) return null;
439 1 : final rawEntries = data['entries'] as List?;
440 1 : return FileSystemResult(
441 : operation: op,
442 1 : sourcePath: data['sourcePath'] as String?,
443 1 : destinationPath: data['destinationPath'] as String?,
444 3 : entries: rawEntries?.map((e) => e as String).toList(),
445 2 : count: (data['count'] as num?)?.toInt(),
446 : );
447 : }
448 : }
|