diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..98cf289a --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,28 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "example", + "cwd": "flutter_cache_manager/example", + "request": "launch", + "type": "dart" + }, + { + "name": "example (profile mode)", + "cwd": "flutter_cache_manager/example", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "example (release mode)", + "cwd": "flutter_cache_manager/example", + "request": "launch", + "type": "dart", + "flutterMode": "release" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..bffaa747 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "multiCommand.commands": [ + { + "command": "multiCommand.dartSave", + "interval": 100, + "sequence": [ + "workbench.action.files.save" + ] + } + ], +} \ No newline at end of file diff --git a/flutter_cache_manager/analysis_options.yaml b/flutter_cache_manager/analysis_options.yaml index f9b30346..801d885f 100644 --- a/flutter_cache_manager/analysis_options.yaml +++ b/flutter_cache_manager/analysis_options.yaml @@ -1 +1,4 @@ +analyzer: + exclude: + - build/** include: package:flutter_lints/flutter.yaml diff --git a/flutter_cache_manager/example/analysis_options.yaml b/flutter_cache_manager/example/analysis_options.yaml index f9b30346..743e05ad 100644 --- a/flutter_cache_manager/example/analysis_options.yaml +++ b/flutter_cache_manager/example/analysis_options.yaml @@ -1 +1,10 @@ +analyzer: + exclude: + - build/** + - android/** + - ios/** + - web/** + - windows/** + - macos/** + - linux/** include: package:flutter_lints/flutter.yaml diff --git a/flutter_cache_manager/example/lib/main.dart b/flutter_cache_manager/example/lib/main.dart index ee34ca00..e5e6fc29 100644 --- a/flutter_cache_manager/example/lib/main.dart +++ b/flutter_cache_manager/example/lib/main.dart @@ -17,7 +17,9 @@ void main() { CacheManager.logLevel = CacheManagerLogLevel.verbose; } -const url = 'https://picsum.photos/200/300'; +const url = + // 'https://images.pexels.com/photos/1624496/pexels-photo-1624496.jpeg'; + 'https://images.pexels.com/photos/1037992/pexels-photo-1037992.jpeg'; /// Example [Widget] showing the functionalities of flutter_cache_manager class CacheManagerPage extends StatefulWidget { diff --git a/flutter_cache_manager/example/lib/plugin_example/progress_indicator.dart b/flutter_cache_manager/example/lib/plugin_example/progress_indicator.dart index 549ff670..c3710222 100644 --- a/flutter_cache_manager/example/lib/plugin_example/progress_indicator.dart +++ b/flutter_cache_manager/example/lib/plugin_example/progress_indicator.dart @@ -17,7 +17,7 @@ class ProgressIndicator extends StatelessWidget { SizedBox( width: 50, height: 50, - child: CircularProgressIndicator.adaptive( + child: CircularProgressIndicator( value: progress?.progress, ), ), diff --git a/flutter_cache_manager/example/macos/Flutter/GeneratedPluginRegistrant.swift b/flutter_cache_manager/example/macos/Flutter/GeneratedPluginRegistrant.swift index 645e27da..368554e0 100644 --- a/flutter_cache_manager/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/flutter_cache_manager/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,10 +5,12 @@ import FlutterMacOS import Foundation +import path_provider_foundation import sqflite_darwin import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/flutter_cache_manager/lib/src/result/interlaced_progress.dart b/flutter_cache_manager/lib/src/result/interlaced_progress.dart new file mode 100644 index 00000000..3c85ce12 --- /dev/null +++ b/flutter_cache_manager/lib/src/result/interlaced_progress.dart @@ -0,0 +1,10 @@ +import 'dart:typed_data'; + +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; + +class InterlacedProgress extends DownloadProgress { + InterlacedProgress( + super.originalUrl, super.totalSize, super.downloaded, this.data); + + final Uint8List data; +} diff --git a/flutter_cache_manager/lib/src/result/result.dart b/flutter_cache_manager/lib/src/result/result.dart index fba37a7f..60dc84c3 100644 --- a/flutter_cache_manager/lib/src/result/result.dart +++ b/flutter_cache_manager/lib/src/result/result.dart @@ -1,3 +1,4 @@ export 'download_progress.dart'; export 'file_info.dart'; export 'file_response.dart'; +export 'interlaced_progress.dart'; diff --git a/flutter_cache_manager/lib/src/web/interlaced/interlaced_transformer.dart b/flutter_cache_manager/lib/src/web/interlaced/interlaced_transformer.dart new file mode 100644 index 00000000..8e127c72 --- /dev/null +++ b/flutter_cache_manager/lib/src/web/interlaced/interlaced_transformer.dart @@ -0,0 +1,116 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:flutter_cache_manager/src/web/interlaced/progressive_jpeg_decoder.dart'; + +class InterlacedData { + final Uint8List data; + InterlacedData(this.data); +} + +class InterlacedConverter extends Converter, InterlacedData> { + const InterlacedConverter(); + + @override + InterlacedData convert(List input) => + InterlacedData(Uint8List.fromList(input)); + + @override + Sink startChunkedConversion(Sink sink) => + InterlacedByteConversionSink(sink); +} + +/// Represents a decoder check function and its corresponding decoder constructor +class DecoderCheck { + final bool? Function(BytesBuilder) check; + final InterlacedDecoder Function(BytesBuilder) createDecoder; + + const DecoderCheck({ + required this.check, + required this.createDecoder, + }); +} + +class InterlacedByteConversionSink implements ChunkedConversionSink { + final Sink _output; + + // Buffer to accumulate chunks + BytesBuilder? _buffer = BytesBuilder(); + + InterlacedDecoder? _decoder; + + static final _decoderChecks = [ + DecoderCheck( + check: ProgressiveJPEGDecoder.isProgressiveJPEG, + createDecoder: (buffer) => ProgressiveJPEGDecoder(buffer), + ), + ]; + + InterlacedByteConversionSink(this._output); + + @override + void add(List chunk) { + // Ensure buffer is not null (should not happen in normal flow) + final buffer = _buffer; + if (buffer == null) { + throw StateError('Sink has been closed and cannot accept new data.'); + } + + _decoder ??= resolveDecoder(); + + if (_decoder == null) { + return _buffer!.add(chunk); + } + + final interlacedData = _decoder?.addChunk(chunk); + if (interlacedData != null) { + _output.add(interlacedData); + } + } + + @override + void close() { + _buffer?.clear(); + _buffer = null; + _decoder = null; + _output.close(); + } + + InterlacedDecoder? resolveDecoder() { + // Try each decoder check + for (final decoderCheck in _decoderChecks) { + final result = decoderCheck.check(_buffer!); + if (result == true) { + return decoderCheck.createDecoder(_buffer!); + } + } + + // Check if all decoders returned false + if (_decoderChecks.every( + (check) => check.check(_buffer!) == false, + )) { + return DumbDecoder(_buffer!); + } + + return null; + } +} + +class DumbDecoder extends InterlacedDecoder { + DumbDecoder(super.buffer); + + @override + InterlacedData? addChunk(List chunk) { + buffer.add(chunk); + return null; + } +} + +// Base class for interlaced format decoders +abstract class InterlacedDecoder { + final BytesBuilder buffer; + + InterlacedDecoder(this.buffer); + + InterlacedData? addChunk(List chunk); +} diff --git a/flutter_cache_manager/lib/src/web/interlaced/progressive_jpeg_decoder.dart b/flutter_cache_manager/lib/src/web/interlaced/progressive_jpeg_decoder.dart new file mode 100644 index 00000000..200a5a6d --- /dev/null +++ b/flutter_cache_manager/lib/src/web/interlaced/progressive_jpeg_decoder.dart @@ -0,0 +1,78 @@ +import 'dart:typed_data'; + +import 'package:flutter_cache_manager/src/web/interlaced/interlaced_transformer.dart'; + +// Decoder for progressive JPEG images +class ProgressiveJPEGDecoder extends InterlacedDecoder { + /// Returns true if the buffer is a progressive JPEG image + /// Returns false if the buffer is not a progressive JPEG image + /// Returns null if the buffer is not enough to determine if it is a progressive JPEG image + static bool? isProgressiveJPEG(BytesBuilder? buffer) { + if (buffer == null) return null; + + final data = buffer.toBytes(); + + if (data.length < 4) return null; + + // Check for the SOI (Start of Image) + if (data[0] != 0xFF || data[1] != 0xD8) { + return false; + } + + // Check for the first SOF marker + for (int i = 2; i < data.length - 1; i++) { + if (data[i] == 0xFF && data[i + 1] >= 0xC0 && data[i + 1] <= 0xCF) { + return data[i + 1] == 0xC2; + } + } + + return null; + } + + // List of valid offsets + final List _validOffsets = []; + + ProgressiveJPEGDecoder(super.buffer); + + @override + InterlacedData? addChunk(List chunk) { + // Calculate startOffset before adding the new chunk + int startOffset = + (buffer.length - 1).clamp(0, buffer.length); // Ensure valid bounds + + // Add the new chunk to the buffer + buffer.add(chunk); + + _updateOffsets(buffer.toBytes(), startOffset); + + return _getBestData(); + } + + InterlacedData? _getBestData() { + // Get the best valid data using the latest valid offset + if (_validOffsets.isNotEmpty) { + final data = + Uint8List.sublistView(buffer.toBytes(), 0, _validOffsets.last); + + data[data.length - 1] = 0xd9; // Fix the last byte as EOI + return InterlacedData(data); + } + + return null; + } + + void _updateOffsets(Uint8List data, int startOffset) { + // Iterate through data starting from the adjusted offset + for (int i = startOffset; i < data.length - 1; i++) { + if (data[i] == 0xFF && _isMarker(data[i + 1])) { + // Add offset of the marker's end + _validOffsets.add(i + 2); + } + } + } + + bool _isMarker(int byte) { + // Check for JPEG markers: 0xDA (Start of Scan) or 0xD9 (End of Image) + return byte == 0xDA || byte == 0xD9; + } +} diff --git a/flutter_cache_manager/lib/src/web/web_helper.dart b/flutter_cache_manager/lib/src/web/web_helper.dart index d5b77146..8bb32cc4 100644 --- a/flutter_cache_manager/lib/src/web/web_helper.dart +++ b/flutter_cache_manager/lib/src/web/web_helper.dart @@ -6,6 +6,7 @@ import 'package:clock/clock.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_cache_manager/src/cache_store.dart'; +import 'package:flutter_cache_manager/src/web/interlaced/interlaced_transformer.dart'; import 'package:flutter_cache_manager/src/web/queue_item.dart'; import 'package:rxdart/rxdart.dart'; import 'package:uuid/uuid.dart'; @@ -145,14 +146,37 @@ class WebHelper { var newCacheObject = _setDataFromHeaders(cacheObject, response); if (statusCodesNewFile.contains(response.statusCode)) { var savedBytes = 0; - await for (final progress in _saveFile(newCacheObject, response)) { - savedBytes = progress; - yield DownloadProgress( - cacheObject.url, - response.contentLength, - progress, - ); + final chunkStream = + _saveFile(newCacheObject, response).asBroadcastStream(); + + final stream = MergeStream([ + chunkStream, + chunkStream + .transform(const InterlacedConverter()) + .distinctUnique( + equals: (a, b) => a.data.length == b.data.length, + hashCode: (e) => e.data.length, + ), + ]); + + await for (final progress in stream) { + if (progress is InterlacedData) { + yield InterlacedProgress( + cacheObject.url, + response.contentLength, + progress.data.length, + progress.data, + ); + } else if (progress is List) { + savedBytes += progress.length; + yield DownloadProgress( + cacheObject.url, + response.contentLength, + savedBytes, + ); + } } + newCacheObject = newCacheObject.copyWith(length: savedBytes); } @@ -195,8 +219,11 @@ class WebHelper { ); } - Stream _saveFile(CacheObject cacheObject, FileServiceResponse response) { - final receivedBytesResultController = StreamController(); + Stream> _saveFile( + CacheObject cacheObject, + FileServiceResponse response, + ) { + final receivedBytesResultController = StreamController>(); _saveFileAndPostUpdates( receivedBytesResultController, cacheObject, @@ -206,22 +233,18 @@ class WebHelper { } Future _saveFileAndPostUpdates( - StreamController receivedBytesResultController, + StreamController> receivedBytesResultController, CacheObject cacheObject, FileServiceResponse response, ) async { final file = await _store.fileSystem.createFile(cacheObject.relativePath); try { - var receivedBytes = 0; final sink = file.openWrite(); - await response.content - .map((s) { - receivedBytes += s.length; - receivedBytesResultController.add(receivedBytes); - return s; - }) - .pipe(sink); + await response.content.map((s) { + receivedBytesResultController.add(s); + return s; + }).pipe(sink); } on Object catch (e, stacktrace) { receivedBytesResultController.addError(e, stacktrace); } diff --git a/flutter_cache_manager/pubspec.yaml b/flutter_cache_manager/pubspec.yaml index 3f0c9ce3..17d12c24 100644 --- a/flutter_cache_manager/pubspec.yaml +++ b/flutter_cache_manager/pubspec.yaml @@ -6,7 +6,7 @@ topics: - cache - cache-manager environment: - sdk: '>=3.8.0 <4.0.0' + sdk: ">=3.8.0 <4.0.0" dependencies: clock: ^1.1.2 diff --git a/flutter_cache_manager_firebase/analysis_options.yaml b/flutter_cache_manager_firebase/analysis_options.yaml index f9b30346..801d885f 100644 --- a/flutter_cache_manager_firebase/analysis_options.yaml +++ b/flutter_cache_manager_firebase/analysis_options.yaml @@ -1 +1,4 @@ +analyzer: + exclude: + - build/** include: package:flutter_lints/flutter.yaml