diff --git a/example/lib/main.dart b/example/lib/main.dart index eadeb96b..909b4434 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -8,11 +8,10 @@ import 'src/screens/import/import.dart'; import 'src/screens/initialisation_error/initialisation_error.dart'; import 'src/screens/main/main.dart'; import 'src/screens/main/secondary_view/contents/home/components/stores_list/state/export_selection_provider.dart'; -import 'src/screens/old/configure_download/configure_download.dart'; -import 'src/screens/old/download/download.dart'; import 'src/screens/store_editor/store_editor.dart'; import 'src/shared/misc/shared_preferences.dart'; import 'src/shared/state/download_configuration_provider.dart'; +import 'src/shared/state/download_provider.dart'; import 'src/shared/state/general_provider.dart'; import 'src/shared/state/region_selection_provider.dart'; @@ -63,22 +62,6 @@ class _AppContainer extends StatelessWidget { fullscreenDialog: true, ), ), - ConfigureDownloadPopup.route: ( - std: null, - custom: (context, settings) => MaterialPageRoute( - builder: (context) => const ConfigureDownloadPopup(), - settings: settings, - fullscreenDialog: true, - ), - ), - DownloadPopup.route: ( - std: null, - custom: (context, settings) => MaterialPageRoute( - builder: (context) => const DownloadPopup(), - settings: settings, - fullscreenDialog: true, - ), - ), }; @override @@ -119,7 +102,9 @@ class _AppContainer extends StatelessWidget { ), ChangeNotifierProvider( create: (_) => DownloadConfigurationProvider(), - //lazy: true, + ), + ChangeNotifierProvider( + create: (_) => DownloadingProvider(), ), ], child: MaterialApp( diff --git a/example/lib/src/screens/initialisation_error/initialisation_error.dart b/example/lib/src/screens/initialisation_error/initialisation_error.dart index cd8c6238..cb579264 100644 --- a/example/lib/src/screens/initialisation_error/initialisation_error.dart +++ b/example/lib/src/screens/initialisation_error/initialisation_error.dart @@ -78,7 +78,7 @@ class InitialisationError extends StatelessWidget { await dir.delete(recursive: true); } on FileSystemException { showFailure(); - rethrow; + return; } runApp(const SizedBox.shrink()); // Destroy current app diff --git a/example/lib/src/screens/main/map_view/components/download_progress/download_progress_masker.dart b/example/lib/src/screens/main/map_view/components/download_progress/download_progress_masker.dart index a6c456d5..dfab651a 100644 --- a/example/lib/src/screens/main/map_view/components/download_progress/download_progress_masker.dart +++ b/example/lib/src/screens/main/map_view/components/download_progress/download_progress_masker.dart @@ -39,8 +39,11 @@ class _DownloadProgressMaskerState extends State { child: GreyscaleMasker( mapCamera: MapCamera.of(context), tileCoordinatesStream: dps - .where((e) => !e.latestTileEvent.isRepeat) - .map((e) => e.latestTileEvent.coordinates), + .where( + (e) => + e.latestTileEvent != null && !e.latestTileEvent!.isRepeat, + ) + .map((e) => e.latestTileEvent!.coordinates), minZoom: widget.minZoom, maxZoom: widget.maxZoom, tileSize: widget.tileSize, diff --git a/example/lib/src/screens/main/map_view/components/region_selection/region_shape.dart b/example/lib/src/screens/main/map_view/components/region_selection/region_shape.dart index 1d7d888c..a5b93e3c 100644 --- a/example/lib/src/screens/main/map_view/components/region_selection/region_shape.dart +++ b/example/lib/src/screens/main/map_view/components/region_selection/region_shape.dart @@ -68,7 +68,7 @@ class RegionShape extends StatelessWidget { color: Theme.of(context) .colorScheme .surface - .withValues(alpha: 0.5), + .withAlpha(255 ~/ 2), ), ], ), @@ -88,7 +88,7 @@ class RegionShape extends StatelessWidget { bounds.southEast, bounds.southWest, ], - color: color.toColor().withValues(alpha: 0.7), + color: color.toColor().withAlpha(255 ~/ 2), ), ], ), @@ -98,7 +98,7 @@ class RegionShape extends StatelessWidget { point: center, radius: radius * 1000, useRadiusInMeter: true, - color: color.toColor().withValues(alpha: 0.7), + color: color.toColor().withAlpha(255 ~/ 2), ), ], ), @@ -108,7 +108,7 @@ class RegionShape extends StatelessWidget { .map( (o) => Polygon( points: o, - color: color.toColor().withValues(alpha: 0.7), + color: color.toColor().withAlpha(255 ~/ 2), ), ) .toList(growable: false), @@ -117,7 +117,7 @@ class RegionShape extends StatelessWidget { polygons: [ Polygon( points: outline, - color: color.toColor().withValues(alpha: 0.7), + color: color.toColor().withAlpha(255 ~/ 2), ), ], ), diff --git a/example/lib/src/screens/main/secondary_view/contents/download_configuration/components/config_options/config_options.dart b/example/lib/src/screens/main/secondary_view/contents/download_configuration/components/config_options/config_options.dart index a731cab3..409f8f9f 100644 --- a/example/lib/src/screens/main/secondary_view/contents/download_configuration/components/config_options/config_options.dart +++ b/example/lib/src/screens/main/secondary_view/contents/download_configuration/components/config_options/config_options.dart @@ -27,6 +27,12 @@ class _ConfigOptionsState extends State { context.select((p) => p.rateLimit); final maxBufferLength = context .select((p) => p.maxBufferLength); + final skipExistingTiles = + context.select( + (p) => p.skipExistingTiles, + ); + final skipSeaTiles = context + .select((p) => p.skipSeaTiles); return SingleChildScrollView( child: Column( @@ -151,7 +157,12 @@ class _ConfigOptionsState extends State { const SizedBox(width: 12), const Text('Skip Existing Tiles'), const Spacer(), - Switch.adaptive(value: true, onChanged: (value) {}), + Switch.adaptive( + value: skipExistingTiles, + onChanged: (v) => context + .read() + .skipExistingTiles = v, + ), ], ), Row( @@ -162,7 +173,12 @@ class _ConfigOptionsState extends State { const SizedBox(width: 12), const Text('Skip Sea Tiles'), const Spacer(), - Switch.adaptive(value: true, onChanged: (value) {}), + Switch.adaptive( + value: skipSeaTiles, + onChanged: (v) => context + .read() + .skipSeaTiles = v, + ), ], ), ], diff --git a/example/lib/src/screens/main/secondary_view/contents/download_configuration/components/confirmation_panel/confirmation_panel.dart b/example/lib/src/screens/main/secondary_view/contents/download_configuration/components/confirmation_panel/confirmation_panel.dart index 4f4f7cd5..0da0ca0b 100644 --- a/example/lib/src/screens/main/secondary_view/contents/download_configuration/components/confirmation_panel/confirmation_panel.dart +++ b/example/lib/src/screens/main/secondary_view/contents/download_configuration/components/confirmation_panel/confirmation_panel.dart @@ -4,7 +4,9 @@ import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; +import '../../../../../../../shared/misc/store_metadata_keys.dart'; import '../../../../../../../shared/state/download_configuration_provider.dart'; +import '../../../../../../../shared/state/download_provider.dart'; import '../../../../../../../shared/state/region_selection_provider.dart'; class ConfirmationPanel extends StatefulWidget { @@ -15,16 +17,23 @@ class ConfirmationPanel extends StatefulWidget { } class _ConfirmationPanelState extends State { - DownloadableRegion? _prevDownloadableRegion; + DownloadableRegion? _prevTileCountableRegion; late Future _tileCount; - void _updateTileCount() { - _tileCount = const FMTCStore('').download.check(_prevDownloadableRegion!); - setState(() {}); - } + bool _loadingDownloader = false; @override Widget build(BuildContext context) { + final regions = context + .select>( + (p) => p.constructedRegions, + ) + .keys + .toList(growable: false); + final minZoom = + context.select((p) => p.minZoom); + final maxZoom = + context.select((p) => p.maxZoom); final startTile = context.select((p) => p.startTile); final endTile = @@ -35,31 +44,21 @@ class _ConfirmationPanelState extends State { ) != null; - // Not suitable for download! - final downloadableRegion = MultiRegion( - context - .select>( - (p) => p.constructedRegions, - ) - .keys - .toList(growable: false), - ).toDownloadable( - minZoom: - context.select((p) => p.minZoom), - maxZoom: - context.select((p) => p.maxZoom), + final tileCountableRegion = MultiRegion(regions).toDownloadable( + minZoom: minZoom, + maxZoom: maxZoom, start: startTile, end: endTile, options: TileLayer(), ); - if (_prevDownloadableRegion == null || - downloadableRegion.originalRegion != - _prevDownloadableRegion!.originalRegion || - downloadableRegion.minZoom != _prevDownloadableRegion!.minZoom || - downloadableRegion.maxZoom != _prevDownloadableRegion!.maxZoom || - downloadableRegion.start != _prevDownloadableRegion!.start || - downloadableRegion.end != _prevDownloadableRegion!.end) { - _prevDownloadableRegion = downloadableRegion; + if (_prevTileCountableRegion == null || + tileCountableRegion.originalRegion != + _prevTileCountableRegion!.originalRegion || + tileCountableRegion.minZoom != _prevTileCountableRegion!.minZoom || + tileCountableRegion.maxZoom != _prevTileCountableRegion!.maxZoom || + tileCountableRegion.start != _prevTileCountableRegion!.start || + tileCountableRegion.end != _prevTileCountableRegion!.end) { + _prevTileCountableRegion = tileCountableRegion; _updateTileCount(); } @@ -179,13 +178,71 @@ class _ConfirmationPanelState extends State { height: 46, width: double.infinity, child: FilledButton.icon( - onPressed: !hasSelectedStoreName ? null : () {}, - label: const Text('Start Download'), - icon: const Icon(Icons.download), + onPressed: !hasSelectedStoreName || _loadingDownloader + ? null + : _startDownload, + label: _loadingDownloader + ? const SizedBox.square( + dimension: 24, + child: CircularProgressIndicator.adaptive(), + ) + : const Text('Start Download'), + icon: _loadingDownloader ? null : const Icon(Icons.download), ), ), ], ), ); } + + void _updateTileCount() { + _tileCount = const FMTCStore('').download.check(_prevTileCountableRegion!); + setState(() {}); + } + + Future _startDownload() async { + setState(() => _loadingDownloader = true); + + final downloadingProvider = context.read(); + final regionSelection = context.read(); + final downloadConfiguration = context.read(); + + final store = FMTCStore(downloadConfiguration.selectedStoreName!); + final urlTemplate = + (await store.metadata.read)[StoreMetadataKeys.urlTemplate.key]; + + if (!mounted) return; + + final downloadableRegion = MultiRegion( + regionSelection.constructedRegions.keys.toList(growable: false), + ).toDownloadable( + minZoom: downloadConfiguration.minZoom, + maxZoom: downloadConfiguration.maxZoom, + start: downloadConfiguration.startTile, + end: downloadConfiguration.endTile, + options: TileLayer( + urlTemplate: urlTemplate, + userAgentPackageName: 'dev.jaffaketchup.fmtc.demo', + ), + ); + + final downloadStream = store.download.startForeground( + region: downloadableRegion, + parallelThreads: downloadConfiguration.parallelThreads, + maxBufferLength: downloadConfiguration.maxBufferLength, + skipExistingTiles: downloadConfiguration.skipExistingTiles, + skipSeaTiles: downloadConfiguration.skipSeaTiles, + rateLimit: downloadConfiguration.rateLimit, + ); + + downloadingProvider.assignDownload( + storeName: downloadConfiguration.selectedStoreName!, + downloadableRegion: downloadableRegion, + stream: downloadStream, + ); + + // The downloading view is switched to by `assignDownload`, when the first + // event is recieved from the stream (indicating the preparation is + // complete and the download is starting). + } } diff --git a/example/lib/src/screens/main/secondary_view/contents/downloading/components/confirm_cancellation_dialog.dart b/example/lib/src/screens/main/secondary_view/contents/downloading/components/confirm_cancellation_dialog.dart new file mode 100644 index 00000000..228e3e98 --- /dev/null +++ b/example/lib/src/screens/main/secondary_view/contents/downloading/components/confirm_cancellation_dialog.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../../../../../../shared/state/download_provider.dart'; + +class ConfirmCancellationDialog extends StatefulWidget { + const ConfirmCancellationDialog({super.key}); + + @override + State createState() => + _ConfirmCancellationDialogState(); +} + +class _ConfirmCancellationDialogState extends State { + bool _isCancelling = false; + + @override + Widget build(BuildContext context) => AlertDialog.adaptive( + icon: const Icon(Icons.cancel), + title: const Text('Cancel download?'), + content: const Text('Any tiles already downloaded will not be removed'), + actions: _isCancelling + ? [const CircularProgressIndicator.adaptive()] + : [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Continue download'), + ), + FilledButton( + onPressed: () async { + setState(() => _isCancelling = true); + await context.read().cancel(); + if (context.mounted) Navigator.of(context).pop(true); + }, + child: const Text('Cancel download'), + ), + ], + ); +} diff --git a/example/lib/src/screens/main/secondary_view/contents/downloading/components/statistics/components/progress/colors.dart b/example/lib/src/screens/main/secondary_view/contents/downloading/components/statistics/components/progress/colors.dart new file mode 100644 index 00000000..b5fa8d42 --- /dev/null +++ b/example/lib/src/screens/main/secondary_view/contents/downloading/components/statistics/components/progress/colors.dart @@ -0,0 +1,8 @@ +import 'package:flutter/material.dart'; + +class DownloadingProgressIndicatorColors { + static final pendingColor = Colors.grey[350]!; + static const failedColor = Colors.red; + static const skippedColor = Colors.orange; + static const successfulColor = Colors.green; +} diff --git a/example/lib/src/screens/main/secondary_view/contents/downloading/components/statistics/components/progress/indicator_bars.dart b/example/lib/src/screens/main/secondary_view/contents/downloading/components/statistics/components/progress/indicator_bars.dart new file mode 100644 index 00000000..82e9f4ae --- /dev/null +++ b/example/lib/src/screens/main/secondary_view/contents/downloading/components/statistics/components/progress/indicator_bars.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../../../../../../../../../shared/state/download_provider.dart'; +import 'colors.dart'; + +class ProgressIndicatorBars extends StatelessWidget { + const ProgressIndicatorBars({super.key}); + + static const double _barHeight = 14; + + @override + Widget build(BuildContext context) { + final successful = context.select( + (p) => p.latestEvent.successfulTiles / p.latestEvent.maxTiles, + ); + final skipped = context.select( + (p) => p.latestEvent.skippedTiles / p.latestEvent.maxTiles, + ); + final failed = context.select( + (p) => p.latestEvent.failedTiles / p.latestEvent.maxTiles, + ); + + return ClipRRect( + borderRadius: BorderRadius.circular(8), + child: IntrinsicHeight( + child: Stack( + children: [ + LinearProgressIndicator( + value: successful + skipped + failed, + backgroundColor: DownloadingProgressIndicatorColors.pendingColor, + color: DownloadingProgressIndicatorColors.failedColor, + minHeight: _barHeight, + ), + LinearProgressIndicator( + value: successful + skipped, + backgroundColor: Colors.transparent, + color: DownloadingProgressIndicatorColors.skippedColor, + minHeight: _barHeight, + ), + LinearProgressIndicator( + value: successful, + backgroundColor: Colors.transparent, + color: DownloadingProgressIndicatorColors.successfulColor, + minHeight: _barHeight, + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/src/screens/main/secondary_view/contents/downloading/components/statistics/components/progress/indicator_text.dart b/example/lib/src/screens/main/secondary_view/contents/downloading/components/statistics/components/progress/indicator_text.dart new file mode 100644 index 00000000..db8bf066 --- /dev/null +++ b/example/lib/src/screens/main/secondary_view/contents/downloading/components/statistics/components/progress/indicator_text.dart @@ -0,0 +1,180 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../../../../../../../../../shared/misc/exts/size_formatter.dart'; +import '../../../../../../../../../shared/state/download_provider.dart'; +import 'colors.dart'; + +class ProgressIndicatorText extends StatefulWidget { + const ProgressIndicatorText({super.key}); + + @override + State createState() => _ProgressIndicatorTextState(); +} + +class _ProgressIndicatorTextState extends State { + late final Timer _rawPercentAlternator; + bool _usePercentages = false; + + @override + void initState() { + super.initState(); + _rawPercentAlternator = Timer.periodic( + const Duration(seconds: 2), + (_) => setState(() => _usePercentages = !_usePercentages), + ); + } + + @override + void dispose() { + _rawPercentAlternator.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final cachedTilesCount = context.select( + (p) => p.latestEvent.cachedTiles - p.latestEvent.bufferedTiles, + ); + final cachedTilesSize = context.select( + (p) => p.latestEvent.cachedSize - p.latestEvent.bufferedSize, + ) * + 1024; + + final bufferedTilesCount = context + .select((p) => p.latestEvent.bufferedTiles); + final bufferedTilesSize = context.select( + (p) => p.latestEvent.bufferedSize, + ) * + 1024; + + final skippedExistingTilesCount = context + .select((p) => p.skippedExistingTileCount); + final skippedExistingTilesSize = context + .select((p) => p.skippedExistingTileSize); + + final skippedSeaTilesCount = + context.select((p) => p.skippedSeaTileCount); + final skippedSeaTilesSize = + context.select((p) => p.skippedSeaTileSize); + + final failedTilesCount = context + .select((p) => p.latestEvent.failedTiles); + + final pendingTilesCount = context + .select((p) => p.latestEvent.remainingTiles); + + final maxTilesCount = + context.select((p) => p.latestEvent.maxTiles); + + return Column( + children: [ + _TextRow( + color: DownloadingProgressIndicatorColors.successfulColor, + type: 'Successful', + statistic: _usePercentages + ? '${(((cachedTilesCount + bufferedTilesCount) / maxTilesCount) * 100).toStringAsFixed(1)}% ' + : '${cachedTilesCount + bufferedTilesCount} tiles (${(cachedTilesSize + bufferedTilesSize).asReadableSize})', + ), + const SizedBox(height: 4), + _TextRow( + type: 'Cached', + statistic: _usePercentages + ? '${((cachedTilesCount / maxTilesCount) * 100).toStringAsFixed(1)}% ' + : '$cachedTilesCount tiles (${cachedTilesSize.asReadableSize})', + ), + const SizedBox(height: 4), + _TextRow( + type: 'Buffered', + statistic: _usePercentages + ? '${((bufferedTilesCount / maxTilesCount) * 100).toStringAsFixed(1)}%' + : '$bufferedTilesCount tiles (${bufferedTilesSize.asReadableSize})', + ), + const SizedBox(height: 4), + _TextRow( + color: DownloadingProgressIndicatorColors.skippedColor, + type: 'Skipped', + statistic: _usePercentages + ? '${(((skippedSeaTilesCount + skippedExistingTilesCount) / maxTilesCount) * 100).toStringAsFixed(1)}%' + : '${skippedSeaTilesCount + skippedExistingTilesCount} tiles (${(skippedSeaTilesSize + skippedExistingTilesSize).asReadableSize})', + ), + const SizedBox(height: 4), + _TextRow( + type: 'Existing', + statistic: _usePercentages + ? '${((skippedExistingTilesCount / maxTilesCount) * 100).toStringAsFixed(1)}%' + : '$skippedExistingTilesCount tiles (${skippedExistingTilesSize.asReadableSize})', + ), + const SizedBox(height: 4), + _TextRow( + type: 'Sea Tiles', + statistic: _usePercentages + ? '${((skippedSeaTilesCount / maxTilesCount) * 100).toStringAsFixed(1)}%' + : '$skippedSeaTilesCount tiles (${skippedSeaTilesSize.asReadableSize})', + ), + const SizedBox(height: 4), + _TextRow( + color: DownloadingProgressIndicatorColors.failedColor, + type: 'Failed', + statistic: _usePercentages + ? '${((failedTilesCount / maxTilesCount) * 100).toStringAsFixed(1)}%' + : '$failedTilesCount tiles', + ), + const SizedBox(height: 4), + _TextRow( + color: DownloadingProgressIndicatorColors.pendingColor, + type: 'Pending', + statistic: _usePercentages + ? '${((pendingTilesCount / maxTilesCount) * 100).toStringAsFixed(1)}%' + : '$pendingTilesCount/$maxTilesCount tiles', + ), + ], + ); + } +} + +class _TextRow extends StatelessWidget { + const _TextRow({ + this.color, + required this.type, + required this.statistic, + }); + + final Color? color; + final String type; + final String statistic; + + @override + Widget build(BuildContext context) => Row( + children: [ + if (color case final color?) + Container( + height: 16, + width: 16, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(8), + ), + ) + else + const SizedBox(width: 28), + const SizedBox(width: 8), + Text( + type, + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + fontStyle: + color == null ? FontStyle.italic : FontStyle.normal, + ), + ), + const Spacer(), + Text( + statistic, + style: TextStyle( + fontStyle: color == null ? FontStyle.italic : FontStyle.normal, + ), + ), + ], + ); +} diff --git a/example/lib/src/screens/main/secondary_view/contents/downloading/components/statistics/components/timing/timing.dart b/example/lib/src/screens/main/secondary_view/contents/downloading/components/statistics/components/timing/timing.dart new file mode 100644 index 00000000..24ff71bb --- /dev/null +++ b/example/lib/src/screens/main/secondary_view/contents/downloading/components/statistics/components/timing/timing.dart @@ -0,0 +1,115 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../../../../../../../../../shared/state/download_configuration_provider.dart'; +import '../../../../../../../../../shared/state/download_provider.dart'; + +class TimingStats extends StatefulWidget { + const TimingStats({super.key}); + + @override + State createState() => _TimingStatsState(); +} + +class _TimingStatsState extends State { + @override + Widget build(BuildContext context) { + final estRemainingDuration = context.select( + (p) => p.latestEvent.estRemainingDuration, + ); + + return Column( + children: [ + Row( + children: [ + const Icon(Icons.timer_outlined, size: 32), + const SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _formatDuration( + context.select( + (p) => p.latestEvent.elapsedDuration, + ), + ), + style: Theme.of(context).textTheme.titleLarge, + ), + const Text('duration elapsed'), + ], + ), + const Spacer(), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Row( + children: [ + Text( + context + .select( + (p) => p.latestEvent.tilesPerSecond, + ) + .toStringAsFixed(0), + style: Theme.of(context).textTheme.titleLarge, + ), + if (context.select( + (p) => p.latestEvent.tilesPerSecond, + ) >= + context.select( + (p) => p.rateLimit, + ) - + 2) + Padding( + padding: const EdgeInsets.only(left: 8), + child: Icon( + Icons.publish, + color: Colors.orange[700], + ), + ), + ], + ), + const Text('tiles per second'), + ], + ), + const SizedBox(width: 8), + const Icon(Icons.speed, size: 32), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + const Icon(Icons.timelapse, size: 32), + const SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + switch (estRemainingDuration) { + <= Duration.zero => 'almost done', + < const Duration(minutes: 1) => '< 1 min', + < const Duration(minutes: 60) => + 'about ${estRemainingDuration.inMinutes} mins', + _ => 'about ${estRemainingDuration.inHours} hours', + }, + style: Theme.of(context).textTheme.titleLarge, + ), + const Text('est. duration remaining'), + ], + ), + ], + ), + ], + ); + } + + String _formatDuration( + Duration duration, { + bool showSeconds = true, + }) { + final hours = duration.inHours.toString().padLeft(2, '0'); + final minutes = duration.inMinutes.remainder(60).toString().padLeft(2, '0'); + final seconds = duration.inSeconds.remainder(60).toString().padLeft(2, '0'); + + return '${hours}h ${minutes}m${showSeconds ? ' ${seconds}s' : ''}'; + } +} diff --git a/example/lib/src/screens/main/secondary_view/contents/downloading/components/statistics/statistics.dart b/example/lib/src/screens/main/secondary_view/contents/downloading/components/statistics/statistics.dart new file mode 100644 index 00000000..9bbadcb1 --- /dev/null +++ b/example/lib/src/screens/main/secondary_view/contents/downloading/components/statistics/statistics.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../../../../../../../shared/state/download_provider.dart'; +import '../../../../../../../shared/state/region_selection_provider.dart'; +import 'components/progress/indicator_bars.dart'; +import 'components/progress/indicator_text.dart'; +import 'components/timing/timing.dart'; + +class DownloadStatistics extends StatelessWidget { + const DownloadStatistics({super.key}); + + @override + Widget build(BuildContext context) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (context.select( + (p) => p.latestEvent.isComplete, + )) + IntrinsicHeight( + child: Row( + children: [ + Text( + 'Downloading complete', + style: Theme.of(context).textTheme.headlineMedium, + ), + const Spacer(), + SizedBox( + height: double.infinity, + child: FilledButton.icon( + onPressed: () { + context.read() + ..isDownloadSetupPanelVisible = false + ..clearConstructedRegions() + ..clearCoordinates(); + context.read().reset(); + }, + label: const Text('Reset'), + icon: const Icon(Icons.done_all), + ), + ), + ], + ), + ) + else if (context.select((p) => p.isPaused)) + Row( + children: [ + Text( + 'Downloading paused', + style: Theme.of(context).textTheme.headlineMedium, + ), + const Spacer(), + const Icon(Icons.pause_circle, size: 36), + ], + ) + else + Row( + children: [ + Text( + 'Downloading map', + style: Theme.of(context).textTheme.headlineMedium, + ), + const Spacer(), + const Padding( + padding: EdgeInsets.all(2), + child: SizedBox.square( + dimension: 32, + child: CircularProgressIndicator.adaptive(), + ), + ), + ], + ), + const SizedBox(height: 24), + const TimingStats(), + const SizedBox(height: 24), + const ProgressIndicatorBars(), + const SizedBox(height: 16), + const ProgressIndicatorText(), + ], + ); +} diff --git a/example/lib/src/screens/main/secondary_view/contents/downloading/downloading_view_bottom_sheet.dart b/example/lib/src/screens/main/secondary_view/contents/downloading/downloading_view_bottom_sheet.dart new file mode 100644 index 00000000..e69de29b diff --git a/example/lib/src/screens/main/secondary_view/contents/downloading/downloading_view_side.dart b/example/lib/src/screens/main/secondary_view/contents/downloading/downloading_view_side.dart new file mode 100644 index 00000000..976cd5c3 --- /dev/null +++ b/example/lib/src/screens/main/secondary_view/contents/downloading/downloading_view_side.dart @@ -0,0 +1,110 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../../../../../shared/state/download_provider.dart'; +import '../../../../../shared/state/region_selection_provider.dart'; +import '../../layouts/side/components/panel.dart'; +import 'components/confirm_cancellation_dialog.dart'; +import 'components/statistics/statistics.dart'; + +class DownloadingViewSide extends StatefulWidget { + const DownloadingViewSide({ + super.key, + }); + + @override + State createState() => _DownloadingViewSideState(); +} + +class _DownloadingViewSideState extends State { + bool _isPausing = false; + + @override + Widget build(BuildContext context) => Column( + children: [ + IntrinsicHeight( + child: Row( + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(99), + color: Theme.of(context).colorScheme.surface, + ), + padding: const EdgeInsets.all(4), + child: IconButton( + onPressed: () async { + if (context + .read() + .latestEvent + .isComplete) { + context.read() + ..isDownloadSetupPanelVisible = false + ..clearConstructedRegions() + ..clearCoordinates(); + context.read().reset(); + return; + } + + await showDialog( + context: context, + builder: (context) => const ConfirmCancellationDialog(), + ); + }, + icon: const Icon(Icons.cancel), + tooltip: 'Cancel Download', + ), + ), + const SizedBox(width: 12), + if (context.select( + (p) => !p.latestEvent.isComplete, + )) + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(99), + color: Theme.of(context).colorScheme.surface, + ), + padding: const EdgeInsets.all(4), + child: _isPausing + ? const AspectRatio( + aspectRatio: 1, + child: Center( + child: SizedBox.square( + dimension: 24, + child: CircularProgressIndicator.adaptive(), + ), + ), + ) + : context.select( + (p) => p.isPaused, + ) + ? IconButton( + onPressed: () => context + .read() + .resume(), + icon: const Icon(Icons.play_arrow), + tooltip: 'Resume Download', + ) + : IconButton( + onPressed: () async { + setState(() => _isPausing = true); + await context + .read() + .pause(); + setState(() => _isPausing = false); + }, + icon: const Icon(Icons.pause), + tooltip: 'Pause Download', + ), + ), + ], + ), + ), + const SizedBox(height: 16), + const Expanded( + child: SideViewPanel( + child: SingleChildScrollView(child: DownloadStatistics()), + ), + ), + const SizedBox(height: 16), + ], + ); +} diff --git a/example/lib/src/screens/main/secondary_view/layouts/side/side.dart b/example/lib/src/screens/main/secondary_view/layouts/side/side.dart index f93a6985..60efc8cc 100644 --- a/example/lib/src/screens/main/secondary_view/layouts/side/side.dart +++ b/example/lib/src/screens/main/secondary_view/layouts/side/side.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import '../../../../../shared/state/download_provider.dart'; import '../../../../../shared/state/region_selection_provider.dart'; import '../../contents/download_configuration/download_configuration_view_side.dart'; +import '../../contents/downloading/downloading_view_side.dart'; import '../../contents/home/home_view_side.dart'; import '../../contents/region_selection/region_selection_view_side.dart'; @@ -44,11 +46,13 @@ class SecondaryViewSide extends StatelessWidget { ), child: switch (selectedTab) { 0 => HomeViewSide(constraints: constraints), - 1 => context.select( - (p) => p.isDownloadSetupPanelVisible, - ) - ? const DownloadConfigurationViewSide() - : const RegionSelectionViewSide(), + 1 => context.select((p) => p.isFocused) + ? const DownloadingViewSide() + : context.select( + (p) => p.isDownloadSetupPanelVisible, + ) + ? const DownloadConfigurationViewSide() + : const RegionSelectionViewSide(), _ => Placeholder(key: ValueKey(selectedTab)), }, ), diff --git a/example/lib/src/screens/old/configure_download/components/numerical_input_row.dart b/example/lib/src/screens/old/configure_download/components/numerical_input_row.dart deleted file mode 100644 index 237d165e..00000000 --- a/example/lib/src/screens/old/configure_download/components/numerical_input_row.dart +++ /dev/null @@ -1,137 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:provider/provider.dart'; - -import '../../../../shared/state/download_configuration_provider.dart'; - -class NumericalInputRow extends StatefulWidget { - const NumericalInputRow({ - super.key, - required this.label, - required this.suffixText, - required this.value, - required this.min, - required this.max, - this.maxEligibleTilesPreview, - required this.onChanged, - }); - - final String label; - final String suffixText; - final int Function(DownloadConfigurationProvider provider) value; - final int min; - final int? max; - final int? maxEligibleTilesPreview; - final void Function(DownloadConfigurationProvider provider, int value) - onChanged; - - @override - State createState() => _NumericalInputRowState(); -} - -class _NumericalInputRowState extends State { - TextEditingController? tec; - - @override - Widget build(BuildContext context) => - Selector( - selector: (context, provider) => widget.value(provider), - builder: (context, currentValue, _) { - tec ??= TextEditingController(text: currentValue.toString()); - - return Row( - children: [ - Text(widget.label), - const Spacer(), - if (widget.maxEligibleTilesPreview != null) ...[ - IconButton( - icon: const Icon(Icons.visibility), - disabledColor: Colors.green, - tooltip: currentValue > widget.maxEligibleTilesPreview! - ? 'Tap to enable following download live' - : 'Eligible to follow download live', - onPressed: currentValue > widget.maxEligibleTilesPreview! - ? () { - widget.onChanged( - context.read(), - widget.maxEligibleTilesPreview!, - ); - tec!.text = widget.maxEligibleTilesPreview.toString(); - } - : null, - ), - const SizedBox(width: 8), - ], - if (widget.max != null) ...[ - Tooltip( - message: currentValue == widget.max - ? 'Limited in the example app' - : '', - child: Icon( - Icons.lock, - color: currentValue == widget.max - ? Colors.amber - : Colors.white.withValues(alpha: 0.2), - ), - ), - const SizedBox(width: 16), - ], - IntrinsicWidth( - child: TextFormField( - controller: tec, - textAlign: TextAlign.end, - keyboardType: TextInputType.number, - decoration: InputDecoration( - isDense: true, - counterText: '', - suffixText: ' ${widget.suffixText}', - ), - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - _NumericalRangeFormatter( - min: widget.min, - max: widget.max ?? double.maxFinite.toInt(), - ), - ], - onChanged: (newVal) => widget.onChanged( - context.read(), - int.tryParse(newVal) ?? currentValue, - ), - ), - ), - ], - ); - }, - ); -} - -class _NumericalRangeFormatter extends TextInputFormatter { - const _NumericalRangeFormatter({required this.min, required this.max}); - final int min; - final int max; - - @override - TextEditingValue formatEditUpdate( - TextEditingValue oldValue, - TextEditingValue newValue, - ) { - if (newValue.text.isEmpty) return newValue; - - final int parsed = int.parse(newValue.text); - - if (parsed < min) { - return TextEditingValue.empty.copyWith( - text: min.toString(), - selection: TextSelection.collapsed(offset: min.toString().length), - ); - } - if (parsed > max) { - return TextEditingValue.empty.copyWith( - text: max.toString(), - selection: TextSelection.collapsed(offset: max.toString().length), - ); - } - - return newValue; - } -} diff --git a/example/lib/src/screens/old/configure_download/components/options_pane.dart b/example/lib/src/screens/old/configure_download/components/options_pane.dart deleted file mode 100644 index 8aa9cd08..00000000 --- a/example/lib/src/screens/old/configure_download/components/options_pane.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../../../../shared/misc/exts/interleave.dart'; - -class OptionsPane extends StatelessWidget { - const OptionsPane({ - super.key, - required this.label, - required this.children, - this.interPadding = 8, - }); - - final String label; - final Iterable children; - final double interPadding; - - @override - Widget build(BuildContext context) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(left: 14), - child: Text(label), - ), - const SizedBox.square(dimension: 4), - DecoratedBox( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(12), - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), - child: children.singleOrNull ?? - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: children - .interleave(SizedBox.square(dimension: interPadding)) - .toList(), - ), - ), - ), - ], - ); -} diff --git a/example/lib/src/screens/old/configure_download/components/region_information.dart b/example/lib/src/screens/old/configure_download/components/region_information.dart deleted file mode 100644 index 06a75de6..00000000 --- a/example/lib/src/screens/old/configure_download/components/region_information.dart +++ /dev/null @@ -1,286 +0,0 @@ -import 'dart:math'; - -import 'package:collection/collection.dart'; -import 'package:dart_earcut/dart_earcut.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; -import 'package:intl/intl.dart'; -import 'package:latlong2/latlong.dart'; - -class RegionInformation extends StatefulWidget { - const RegionInformation({ - super.key, - required this.region, - required this.maxTiles, - }); - - final DownloadableRegion region; - final int? maxTiles; - - @override - State createState() => _RegionInformationState(); -} - -class _RegionInformationState extends State { - final distance = const Distance(roundResult: false).distance; - - @override - Widget build(BuildContext context) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ...widget.region.when( - rectangle: (rectangleRegion) { - final rectangle = rectangleRegion.originalRegion; - - return [ - const Text('TOTAL AREA'), - Text( - '${(distance(rectangle.bounds.northWest, rectangle.bounds.northEast) * distance(rectangle.bounds.northEast, rectangle.bounds.southEast) / 1000000).toStringAsFixed(3)} km²', - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 24, - ), - ), - const SizedBox(height: 10), - const Text('APPROX. NORTH WEST'), - Text( - '${rectangle.bounds.northWest.latitude.toStringAsFixed(3)}, ${rectangle.bounds.northWest.longitude.toStringAsFixed(3)}', - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 24, - ), - ), - const SizedBox(height: 10), - const Text('APPROX. SOUTH EAST'), - Text( - '${rectangle.bounds.southEast.latitude.toStringAsFixed(3)}, ${rectangle.bounds.southEast.longitude.toStringAsFixed(3)}', - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 24, - ), - ), - ]; - }, - circle: (circleRegion) { - final circle = circleRegion.originalRegion; - - return [ - const Text('TOTAL AREA'), - Text( - '${(pi * pow(circle.radius, 2)).toStringAsFixed(3)} km²', - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 24, - ), - ), - const SizedBox(height: 10), - const Text('RADIUS'), - Text( - '${circle.radius.toStringAsFixed(2)} km', - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 24, - ), - ), - const SizedBox(height: 10), - const Text('APPROX. CENTER'), - Text( - '${circle.center.latitude.toStringAsFixed(3)}, ${circle.center.longitude.toStringAsFixed(3)}', - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 24, - ), - ), - ]; - }, - line: (lineRegion) { - final line = lineRegion.originalRegion; - - double totalDistance = 0; - - for (int i = 0; i < line.line.length - 1; i++) { - totalDistance += - distance(line.line[i], line.line[i + 1]); - } - - return [ - const Text('LINE LENGTH'), - Text( - '${(totalDistance / 1000).toStringAsFixed(3)} km', - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 24, - ), - ), - const SizedBox(height: 10), - const Text('FIRST COORD'), - Text( - '${line.line[0].latitude.toStringAsFixed(3)}, ${line.line[0].longitude.toStringAsFixed(3)}', - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 24, - ), - ), - const SizedBox(height: 10), - const Text('LAST COORD'), - Text( - '${line.line.last.latitude.toStringAsFixed(3)}, ${line.line.last.longitude.toStringAsFixed(3)}', - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 24, - ), - ), - ]; - }, - customPolygon: (customPolygonRegion) { - final customPolygon = customPolygonRegion.originalRegion; - - double area = 0; - - for (final triangle in Earcut.triangulateFromPoints( - customPolygon.outline - .map(const Epsg3857().projection.project), - ).map(customPolygon.outline.elementAt).slices(3)) { - final a = distance(triangle[0], triangle[1]); - final b = distance(triangle[1], triangle[2]); - final c = distance(triangle[2], triangle[0]); - - area += 0.25 * - sqrt( - 4 * a * a * b * b - pow(a * a + b * b - c * c, 2), - ); - } - - return [ - const Text('TOTAL AREA'), - Text( - '${(area / 1000000).toStringAsFixed(3)} km²', - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 24, - ), - ), - ]; - }, - multi: (_) => throw UnsupportedError( - '`MultiRegion` is not supported in the example app', - ), - ), - ], - ), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - const Text('ZOOM LEVELS'), - Text( - '${widget.region.minZoom} - ${widget.region.maxZoom}', - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 24, - ), - ), - const SizedBox(height: 10), - const Text('TOTAL TILES'), - if (widget.maxTiles case final maxTiles?) - Text( - NumberFormat('###,###').format(maxTiles), - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 24, - ), - ) - else - Padding( - padding: const EdgeInsets.only(top: 4), - child: SizedBox( - height: 36, - width: 36, - child: Center( - child: SizedBox( - height: 28, - width: 28, - child: CircularProgressIndicator( - color: Theme.of(context).colorScheme.secondary, - ), - ), - ), - ), - ), - const SizedBox(height: 10), - const Text('TILES RANGE'), - if (widget.region.start == 1 && widget.region.end == null) - const Text( - '*', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 24, - ), - ) - else - Text( - '${NumberFormat('###,###').format(widget.region.start)} - ${widget.region.end != null ? NumberFormat('###,###').format(widget.region.end) : '*'}', - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 24, - ), - ), - ], - ), - ], - ), - Padding( - padding: const EdgeInsets.all(8), - child: DecoratedBox( - decoration: BoxDecoration( - color: Colors.amber, - borderRadius: BorderRadius.circular(16), - ), - child: Row( - children: [ - const Padding( - padding: EdgeInsets.symmetric(horizontal: 8), - child: Icon(Icons.warning_amber, size: 28), - ), - Expanded( - child: DecoratedBox( - decoration: BoxDecoration( - color: Colors.amber[200], - borderRadius: BorderRadius.circular(16), - ), - child: const Padding( - padding: EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "You must abide by your tile server's Terms of " - 'Service when bulk downloading.', - style: TextStyle(fontWeight: FontWeight.bold), - ), - Text( - 'Many servers will ' - 'forbid or heavily restrict this action, as it ' - 'places extra strain on resources. Be respectful, ' - 'and note that you use this functionality at your ' - 'own risk.', - ), - ], - ), - ), - ), - ), - ], - ), - ), - ), - ], - ); -} diff --git a/example/lib/src/screens/old/configure_download/components/start_download_button.dart b/example/lib/src/screens/old/configure_download/components/start_download_button.dart deleted file mode 100644 index 847c49ce..00000000 --- a/example/lib/src/screens/old/configure_download/components/start_download_button.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; -import 'package:provider/provider.dart'; - -import '../../../../shared/misc/store_metadata_keys.dart'; -import '../../../../shared/state/download_configuration_provider.dart'; -import '../../download/download.dart'; - -class StartDownloadButton extends StatelessWidget { - const StartDownloadButton({ - super.key, - required this.region, - required this.maxTiles, - }); - - final DownloadableRegion region; - final int? maxTiles; - - @override - Widget build(BuildContext context) => - Selector( - selector: (context, provider) => provider.selectedStoreName, - builder: (context, selectedStore, child) { - final enabled = selectedStore != null && maxTiles != null; - - return IgnorePointer( - ignoring: !enabled, - child: AnimatedOpacity( - opacity: enabled ? 1 : 0, - duration: const Duration(milliseconds: 200), - curve: Curves.easeInOut, - child: child, - ), - ); - }, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - FloatingActionButton.extended( - onPressed: () async { - final configureDownloadProvider = - context.read(); - - final selectedStore = - FMTCStore(configureDownloadProvider.selectedStoreName!); - - if (!await selectedStore.manage.ready && context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Selected store no longer exists'), - ), - ); - return; - } - - final urlTemplate = (await selectedStore - .metadata.read)[StoreMetadataKeys.urlTemplate.key]!; - - if (!context.mounted) return; - - unawaited( - Navigator.of(context).popAndPushNamed( - DownloadPopup.route, - arguments: ( - downloadProgress: selectedStore.download.startForeground( - region: region.originalRegion.toDownloadable( - minZoom: region.minZoom, - maxZoom: region.maxZoom, - start: region.start, - end: region.end, - options: TileLayer( - urlTemplate: urlTemplate, - userAgentPackageName: 'dev.jaffaketchup.fmtc.demo', - ), - ), - parallelThreads: - configureDownloadProvider.parallelThreads, - maxBufferLength: - configureDownloadProvider.maxBufferLength, - skipExistingTiles: - configureDownloadProvider.skipExistingTiles, - skipSeaTiles: configureDownloadProvider.skipSeaTiles, - rateLimit: configureDownloadProvider.rateLimit, - ), - maxTiles: maxTiles! - ), - ), - ); - }, - label: const Text('Start Download'), - icon: const Icon(Icons.save), - ), - ], - ), - ); -} diff --git a/example/lib/src/screens/old/configure_download/components/store_selector.dart b/example/lib/src/screens/old/configure_download/components/store_selector.dart deleted file mode 100644 index 559fc3a0..00000000 --- a/example/lib/src/screens/old/configure_download/components/store_selector.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; -import 'package:provider/provider.dart'; - -import '../../../../shared/state/download_configuration_provider.dart'; - -class StoreSelector extends StatefulWidget { - const StoreSelector({super.key}); - - @override - State createState() => _StoreSelectorState(); -} - -class _StoreSelectorState extends State { - @override - Widget build(BuildContext context) => Row( - children: [ - const Text('Store'), - const Spacer(), - IntrinsicWidth( - child: Selector( - selector: (context, provider) => provider.selectedStoreName, - builder: (context, selectedStore, _) => - FutureBuilder>( - future: FMTCRoot.stats.storesAvailable, - builder: (context, snapshot) { - final items = snapshot.data - ?.map( - (e) => DropdownMenuItem( - value: e.storeName, - child: Text(e.storeName), - ), - ) - .toList(); - final text = snapshot.data == null - ? 'Loading...' - : snapshot.data!.isEmpty - ? 'None Available' - : 'None Selected'; - - return DropdownButton( - items: items, - onChanged: (store) => context - .read() - .selectedStoreName = store, - value: selectedStore, - hint: Text(text), - padding: const EdgeInsets.only(left: 12), - ); - }, - ), - ), - ), - ], - ); -} diff --git a/example/lib/src/screens/old/configure_download/configure_download.dart b/example/lib/src/screens/old/configure_download/configure_download.dart deleted file mode 100644 index caafbd34..00000000 --- a/example/lib/src/screens/old/configure_download/configure_download.dart +++ /dev/null @@ -1,147 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; -import 'package:provider/provider.dart'; - -import '../../../shared/misc/exts/interleave.dart'; -import '../../../shared/state/region_selection_provider.dart'; -import 'components/numerical_input_row.dart'; -import 'components/options_pane.dart'; -import 'components/region_information.dart'; -import 'components/start_download_button.dart'; -import 'components/store_selector.dart'; -import '../../../shared/state/download_configuration_provider.dart'; - -class ConfigureDownloadPopup extends StatefulWidget { - const ConfigureDownloadPopup({super.key}); - - static const String route = '/download/configure'; - - @override - State createState() => _ConfigureDownloadPopupState(); -} - -class _ConfigureDownloadPopupState extends State { - DownloadableRegion? region; - int? maxTiles; - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - - final provider = context.read(); - /*const FMTCStore('') - .download - .check( - region ??= provider.region!.toDownloadable( - minZoom: provider.minZoom, - maxZoom: provider.maxZoom, - start: provider.startTile, - end: provider.endTile, - options: TileLayer(), - ), - ) - .then((v) => setState(() => maxTiles = v));*/ - } - - @override - Widget build(BuildContext context) => Scaffold( - appBar: AppBar(title: const Text('Configure Bulk Download')), - floatingActionButton: StartDownloadButton( - region: region!, - maxTiles: maxTiles, - ), - body: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox.shrink(), - RegionInformation( - region: region!, - maxTiles: maxTiles, - ), - const Divider(thickness: 2, height: 8), - const OptionsPane( - label: 'STORE DIRECTORY', - children: [StoreSelector()], - ), - OptionsPane( - label: 'PERFORMANCE FACTORS', - children: [ - NumericalInputRow( - label: 'Parallel Threads', - suffixText: 'threads', - value: (provider) => provider.parallelThreads, - min: 1, - max: 10, - onChanged: (provider, value) => - provider.parallelThreads = value, - ), - NumericalInputRow( - label: 'Rate Limit', - suffixText: 'max. tps', - value: (provider) => provider.rateLimit, - min: 1, - max: 300, - maxEligibleTilesPreview: 20, - onChanged: (provider, value) => - provider.rateLimit = value, - ), - NumericalInputRow( - label: 'Tile Buffer Length', - suffixText: 'max. tiles', - value: (provider) => provider.maxBufferLength, - min: 0, - max: null, - onChanged: (provider, value) => - provider.maxBufferLength = value, - ), - ], - ), - OptionsPane( - label: 'SKIP TILES', - children: [ - Row( - children: [ - const Text('Skip Existing Tiles'), - const Spacer(), - Switch.adaptive( - value: context - .select( - (provider) => provider.skipExistingTiles, - ), - onChanged: (val) => context - .read() - .skipExistingTiles = val, - activeColor: Theme.of(context).colorScheme.primary, - ), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('Skip Sea Tiles'), - const Spacer(), - Switch.adaptive( - value: context - .select( - (provider) => provider.skipSeaTiles, - ), - onChanged: (val) => context - .read() - .skipSeaTiles = val, - activeColor: Theme.of(context).colorScheme.primary, - ), - ], - ), - ], - ), - const SizedBox(height: 72), - ].interleave(const SizedBox.square(dimension: 16)).toList(), - ), - ), - ), - ); -} diff --git a/example/lib/src/screens/old/download/components/confirm_cancellation_dialog.dart b/example/lib/src/screens/old/download/components/confirm_cancellation_dialog.dart deleted file mode 100644 index 9114acf5..00000000 --- a/example/lib/src/screens/old/download/components/confirm_cancellation_dialog.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; -import 'package:provider/provider.dart'; - -import '../../../../shared/state/download_configuration_provider.dart'; - -class ConfirmCancellationDialog extends StatefulWidget { - const ConfirmCancellationDialog({super.key}); - - @override - State createState() => - _ConfirmCancellationDialogState(); -} - -class _ConfirmCancellationDialogState extends State { - bool isCancelling = false; - - @override - Widget build(BuildContext context) => AlertDialog.adaptive( - icon: const Icon(Icons.cancel), - title: const Text('Cancel download?'), - content: const Text('Any tiles already downloaded will not be removed'), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: const Text('Continue download'), - ), - if (isCancelling) - const CircularProgressIndicator.adaptive() - else - FilledButton( - onPressed: () async { - setState(() => isCancelling = true); - await FMTCStore( - context - .read() - .selectedStoreName!, - ).download.cancel(); - if (context.mounted) Navigator.of(context).pop(true); - }, - child: const Text('Cancel download'), - ), - ], - ); -} diff --git a/example/lib/src/screens/old/download/components/main_statistics.dart b/example/lib/src/screens/old/download/components/main_statistics.dart deleted file mode 100644 index b615db27..00000000 --- a/example/lib/src/screens/old/download/components/main_statistics.dart +++ /dev/null @@ -1,138 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; -import 'package:provider/provider.dart'; - -import '../../../../shared/state/download_configuration_provider.dart'; -import 'stat_display.dart'; - -class MainStatistics extends StatefulWidget { - const MainStatistics({ - super.key, - required this.download, - required this.maxTiles, - }); - - final DownloadProgress? download; - final int maxTiles; - - @override - State createState() => _MainStatisticsState(); -} - -class _MainStatisticsState extends State { - @override - Widget build(BuildContext context) => IntrinsicWidth( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - RepaintBoundary( - child: Text( - '${widget.download?.attemptedTiles ?? 0}/${widget.maxTiles} (${widget.download?.percentageProgress.toStringAsFixed(2) ?? 0}%)', - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - ), - const SizedBox(height: 16), - StatDisplay( - statistic: - '${widget.download?.elapsedDuration.toString().split('.')[0] ?? '0:00:00'} ' - '/ ${widget.download?.estTotalDuration.toString().split('.')[0] ?? '0:00:00'}', - description: 'elapsed / estimated total duration', - ), - StatDisplay( - statistic: widget.download?.estRemainingDuration - .toString() - .split('.')[0] ?? - '0:00:00', - description: 'estimated remaining duration', - ), - RepaintBoundary( - child: Column( - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - widget.download?.tilesPerSecond.toStringAsFixed(2) ?? - '...', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: - widget.download?.isTPSArtificiallyCapped ?? false - ? Colors.amber - : null, - ), - ), - if (widget.download?.isTPSArtificiallyCapped ?? - false) ...[ - const SizedBox(width: 8), - const Icon(Icons.lock_clock, color: Colors.amber), - ], - ], - ), - Text( - 'approx. tiles per second', - style: TextStyle( - fontSize: 16, - color: widget.download?.isTPSArtificiallyCapped ?? false - ? Colors.amber - : null, - ), - ), - ], - ), - ), - const SizedBox(height: 24), - if (!(widget.download?.isComplete ?? false)) - RepaintBoundary( - child: Selector( - selector: (context, provider) => provider.selectedStoreName, - builder: (context, selectedStoreName, _) { - final selectedStore = FMTCStore(selectedStoreName!); - - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton.outlined( - onPressed: () async { - if (selectedStore.download.isPaused()) { - selectedStore.download.resume(); - } else { - await selectedStore.download.pause(); - } - setState(() {}); - }, - icon: Icon( - selectedStore.download.isPaused() - ? Icons.play_arrow - : Icons.pause, - ), - ), - const SizedBox(width: 12), - IconButton.outlined( - onPressed: () => selectedStore.download.cancel(), - icon: const Icon(Icons.cancel), - ), - ], - ); - }, - ), - ), - if (widget.download?.isComplete ?? false) - SizedBox( - width: double.infinity, - child: OutlinedButton( - onPressed: () => Navigator.of(context).pop(), - child: const Padding( - padding: EdgeInsets.symmetric(vertical: 8), - child: Text('Exit'), - ), - ), - ), - ], - ), - ); -} diff --git a/example/lib/src/screens/old/download/components/multi_linear_progress_indicator.dart b/example/lib/src/screens/old/download/components/multi_linear_progress_indicator.dart deleted file mode 100644 index 1881fc65..00000000 --- a/example/lib/src/screens/old/download/components/multi_linear_progress_indicator.dart +++ /dev/null @@ -1,88 +0,0 @@ -import 'package:flutter/material.dart'; - -typedef IndividualProgress = ({num value, Color color, Widget? child}); - -class MulitLinearProgressIndicator extends StatefulWidget { - const MulitLinearProgressIndicator({ - super.key, - required this.progresses, - this.maxValue = 1, - this.backgroundChild, - this.height = 24, - this.radius, - this.childAlignment = Alignment.centerRight, - this.animationDuration = const Duration(milliseconds: 500), - }); - - final List progresses; - final num maxValue; - final Widget? backgroundChild; - final double height; - final BorderRadiusGeometry? radius; - final AlignmentGeometry childAlignment; - final Duration animationDuration; - - @override - State createState() => - _MulitLinearProgressIndicatorState(); -} - -class _MulitLinearProgressIndicatorState - extends State { - @override - Widget build(BuildContext context) => RepaintBoundary( - child: LayoutBuilder( - builder: (context, constraints) => ClipRRect( - borderRadius: - widget.radius ?? BorderRadius.circular(widget.height / 2), - child: SizedBox( - height: widget.height, - width: constraints.maxWidth, - child: Stack( - children: [ - Positioned.fill( - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: widget.radius ?? - BorderRadius.circular(widget.height / 2), - border: Border.all( - color: Theme.of(context).colorScheme.onSurface, - ), - ), - padding: EdgeInsets.symmetric( - vertical: 2, - horizontal: widget.height / 2, - ), - alignment: widget.childAlignment, - child: widget.backgroundChild, - ), - ), - ...widget.progresses.map( - (e) => AnimatedPositioned( - height: widget.height, - left: 0, - width: (constraints.maxWidth / widget.maxValue) * e.value, - duration: widget.animationDuration, - child: Container( - decoration: BoxDecoration( - color: e.color, - borderRadius: widget.radius ?? - BorderRadius.circular(widget.height / 2), - ), - padding: EdgeInsets.symmetric( - vertical: 2, - horizontal: widget.height / 2, - ), - alignment: widget.childAlignment, - child: e.child, - ), - ), - ), - ], - ), - ), - ), - ), - ); -} diff --git a/example/lib/src/screens/old/download/components/stat_display.dart b/example/lib/src/screens/old/download/components/stat_display.dart deleted file mode 100644 index 3592c850..00000000 --- a/example/lib/src/screens/old/download/components/stat_display.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:flutter/material.dart'; - -class StatDisplay extends StatelessWidget { - const StatDisplay({ - super.key, - required this.statistic, - required this.description, - }); - - final String statistic; - final String description; - - @override - Widget build(BuildContext context) => RepaintBoundary( - child: Column( - children: [ - Text( - statistic, - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.center, - ), - Text( - description, - style: const TextStyle(fontSize: 16), - textAlign: TextAlign.center, - ), - ], - ), - ); -} diff --git a/example/lib/src/screens/old/download/components/stats_table.dart b/example/lib/src/screens/old/download/components/stats_table.dart deleted file mode 100644 index 09d8bd12..00000000 --- a/example/lib/src/screens/old/download/components/stats_table.dart +++ /dev/null @@ -1,86 +0,0 @@ -part of '../download.dart'; - -class _StatsTable extends StatelessWidget { - const _StatsTable({ - required this.download, - }); - - final DownloadProgress? download; - - @override - Widget build(BuildContext context) => Table( - defaultVerticalAlignment: TableCellVerticalAlignment.middle, - children: [ - TableRow( - children: [ - StatDisplay( - statistic: - '${(download?.cachedTiles ?? 0) - (download?.bufferedTiles ?? 0)} + ${download?.bufferedTiles ?? 0}', - description: 'cached + buffered tiles', - ), - StatDisplay( - statistic: - '${(((download?.cachedSize ?? 0) - (download?.bufferedSize ?? 0)) * 1024).asReadableSize} + ${((download?.bufferedSize ?? 0) * 1024).asReadableSize}', - description: 'cached + buffered size', - ), - ], - ), - TableRow( - children: [ - StatDisplay( - statistic: - '${download?.skippedTiles ?? 0} (${(download?.skippedTiles ?? 0) == 0 ? 0 : (100 - (((download?.cachedTiles ?? 0) - (download?.skippedTiles ?? 0)) / (download?.cachedTiles ?? 0)) * 100).clamp(double.minPositive, 100).toStringAsFixed(1)}%)', - description: 'skipped tiles (% saving)', - ), - StatDisplay( - statistic: - '${((download?.skippedSize ?? 0) * 1024).asReadableSize} (${(download?.skippedTiles ?? 0) == 0 ? 0 : (100 - (((download?.cachedSize ?? 0) - (download?.skippedSize ?? 0)) / (download?.cachedSize ?? 0)) * 100).clamp(double.minPositive, 100).toStringAsFixed(1)}%)', - description: 'skipped size (% saving)', - ), - ], - ), - TableRow( - children: [ - RepaintBoundary( - child: Column( - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - download?.failedTiles.toString() ?? '0', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: (download?.failedTiles ?? 0) == 0 - ? null - : Colors.red, - ), - ), - if ((download?.failedTiles ?? 0) != 0) ...[ - const SizedBox(width: 8), - const Icon( - Icons.warning_amber, - color: Colors.red, - ), - ], - ], - ), - Text( - 'failed tiles', - style: TextStyle( - fontSize: 16, - color: (download?.failedTiles ?? 0) == 0 - ? null - : Colors.red, - ), - ), - ], - ), - ), - const SizedBox.shrink(), - ], - ), - ], - ); -} diff --git a/example/lib/src/screens/old/download/download.dart b/example/lib/src/screens/old/download/download.dart deleted file mode 100644 index cafc78e5..00000000 --- a/example/lib/src/screens/old/download/download.dart +++ /dev/null @@ -1,343 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; - -import '../../../shared/misc/exts/size_formatter.dart'; -import 'components/confirm_cancellation_dialog.dart'; -import 'components/main_statistics.dart'; -import 'components/multi_linear_progress_indicator.dart'; -import 'components/stat_display.dart'; - -part 'components/stats_table.dart'; - -class DownloadPopup extends StatefulWidget { - const DownloadPopup({super.key}); - - static const String route = '/download/progress'; - - @override - State createState() => _DownloadPopupState(); -} - -class _DownloadPopupState extends State { - bool isInitialised = false; - - late final Stream downloadProgress; - late final StreamSubscription dpSubscription; - late final int maxTiles; - - final failedTiles = []; - final skippedTiles = []; - bool isCompleteCanPop = false; - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - - if (!isInitialised) { - final arguments = ModalRoute.of(context)!.settings.arguments! as ({ - Stream downloadProgress, - int maxTiles - }); - downloadProgress = arguments.downloadProgress.asBroadcastStream(); - dpSubscription = downloadProgress.listen((progress) { - if (progress.latestTileEvent.isRepeat) return; - if (progress.latestTileEvent.result.category == - TileEventResultCategory.failed) { - failedTiles.add(progress.latestTileEvent); - } - if (progress.latestTileEvent.result.category == - TileEventResultCategory.skipped) { - skippedTiles.add(progress.latestTileEvent); - } - isCompleteCanPop = progress.isComplete; - }); - maxTiles = arguments.maxTiles; - } - - isInitialised = true; - } - - @override - void dispose() { - dpSubscription.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) => PopScope( - canPop: isCompleteCanPop, - onPopInvokedWithResult: (didPop, result) async { - if (!didPop && - await showDialog( - context: context, - builder: (context) => const ConfirmCancellationDialog(), - ) as bool && - context.mounted) Navigator.of(context).pop(); - }, - child: Scaffold( - appBar: AppBar( - title: const Text('Downloading Region'), - ), - body: Padding( - padding: const EdgeInsets.all(12), - child: StreamBuilder( - stream: downloadProgress, - builder: (context, snapshot) { - final download = snapshot.data; - - return LayoutBuilder( - builder: (context, constraints) { - final isWide = constraints.maxWidth > 800; - - return SingleChildScrollView( - child: Column( - children: [ - IntrinsicHeight( - child: Flex( - direction: - isWide ? Axis.horizontal : Axis.vertical, - children: [ - Expanded( - child: Wrap( - alignment: WrapAlignment.center, - runAlignment: WrapAlignment.center, - crossAxisAlignment: - WrapCrossAlignment.center, - spacing: 32, - runSpacing: 28, - children: [ - RepaintBoundary( - child: SizedBox.square( - dimension: isWide ? 216 : 196, - child: ClipRRect( - borderRadius: - BorderRadius.circular(16), - child: download?.latestTileEvent - .tileImage != - null - ? Image.memory( - download!.latestTileEvent - .tileImage!, - gaplessPlayback: true, - ) - : const Center( - child: - CircularProgressIndicator - .adaptive(), - ), - ), - ), - ), - MainStatistics( - download: download, - maxTiles: maxTiles, - ), - ], - ), - ), - const SizedBox.square(dimension: 16), - if (isWide) - const VerticalDivider() - else - const Divider(), - const SizedBox.square(dimension: 16), - if (isWide) - Expanded( - child: _StatsTable(download: download), - ) - else - _StatsTable(download: download), - ], - ), - ), - const SizedBox(height: 30), - MulitLinearProgressIndicator( - maxValue: maxTiles, - backgroundChild: Text( - '${download?.remainingTiles ?? 0}', - style: const TextStyle(color: Colors.white), - ), - progresses: [ - ( - value: (download?.cachedTiles ?? 0) + - (download?.skippedTiles ?? 0) + - (download?.failedTiles ?? 0), - color: Colors.red, - child: Text( - '${download?.failedTiles ?? 0}', - style: const TextStyle(color: Colors.black), - ) - ), - ( - value: (download?.cachedTiles ?? 0) + - (download?.skippedTiles ?? 0), - color: Colors.yellow, - child: Text( - '${download?.skippedTiles ?? 0}', - style: const TextStyle(color: Colors.black), - ) - ), - ( - value: download?.cachedTiles ?? 0, - color: Colors.green[300]!, - child: Text( - '${download?.bufferedTiles ?? 0}', - style: const TextStyle(color: Colors.black), - ) - ), - ( - value: (download?.cachedTiles ?? 0) - - (download?.bufferedTiles ?? 0), - color: Colors.green, - child: Text( - '${(download?.cachedTiles ?? 0) - (download?.bufferedTiles ?? 0)}', - style: const TextStyle(color: Colors.black), - ) - ), - ], - ), - const SizedBox(height: 32), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const RotatedBox( - quarterTurns: 3, - child: Text( - 'FAILED TILES', - ), - ), - Expanded( - child: RepaintBoundary( - child: failedTiles.isEmpty - ? const Padding( - padding: EdgeInsets.symmetric( - horizontal: 2, - ), - child: Column( - children: [ - Icon(Icons.task_alt, size: 48), - SizedBox(height: 10), - Text( - 'Any failed tiles will appear here', - textAlign: TextAlign.center, - ), - ], - ), - ) - : ListView.builder( - reverse: true, - addRepaintBoundaries: false, - itemCount: failedTiles.length, - shrinkWrap: true, - itemBuilder: (context, index) => - ListTile( - leading: Icon( - switch ( - failedTiles[index].result) { - TileEventResult - .noConnectionDuringFetch => - Icons.wifi_off, - TileEventResult - .unknownFetchException => - Icons.error, - TileEventResult - .negativeFetchResponse => - Icons.reply, - _ => Icons.abc, - }, - ), - title: Text(failedTiles[index].url), - subtitle: Text( - switch ( - failedTiles[index].result) { - TileEventResult - .noConnectionDuringFetch => - 'Failed to establish a connection to the network', - TileEventResult - .unknownFetchException => - 'There was an unknown error when trying to download this tile, of type ${failedTiles[index].fetchError.runtimeType}', - TileEventResult - .negativeFetchResponse => - 'The tile server responded with an HTTP status code of ${failedTiles[index].fetchResponse!.statusCode} (${failedTiles[index].fetchResponse!.reasonPhrase})', - _ => throw Error(), - }, - ), - ), - ), - ), - ), - const SizedBox(width: 8), - const RotatedBox( - quarterTurns: 3, - child: Text( - 'SKIPPED TILES', - ), - ), - Expanded( - child: RepaintBoundary( - child: skippedTiles.isEmpty - ? const Padding( - padding: EdgeInsets.symmetric( - horizontal: 2, - ), - child: Column( - children: [ - Icon(Icons.task_alt, size: 48), - SizedBox(height: 10), - Text( - 'Any skipped tiles will appear here', - textAlign: TextAlign.center, - ), - ], - ), - ) - : ListView.builder( - reverse: true, - addRepaintBoundaries: false, - itemCount: skippedTiles.length, - shrinkWrap: true, - itemBuilder: (context, index) => - ListTile( - leading: Icon( - switch ( - skippedTiles[index].result) { - TileEventResult - .alreadyExisting => - Icons.disabled_visible, - TileEventResult.isSeaTile => - Icons.water_drop, - _ => Icons.abc, - }, - ), - title: - Text(skippedTiles[index].url), - subtitle: Text( - switch ( - skippedTiles[index].result) { - TileEventResult - .alreadyExisting => - 'Tile already exists', - TileEventResult.isSeaTile => - 'Tile is a sea tile', - _ => throw Error(), - }, - ), - ), - ), - ), - ), - ], - ), - ], - ), - ); - }, - ); - }, - ), - ), - ), - ); -} diff --git a/example/lib/src/shared/state/download_provider.dart b/example/lib/src/shared/state/download_provider.dart new file mode 100644 index 00000000..ba6e2d8c --- /dev/null +++ b/example/lib/src/shared/state/download_provider.dart @@ -0,0 +1,94 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; + +class DownloadingProvider extends ChangeNotifier { + bool _isFocused = false; + bool get isFocused => _isFocused; + + bool _isPaused = false; + bool get isPaused => _isPaused; + + DownloadableRegion? _downloadableRegion; + DownloadableRegion get downloadableRegion => + _downloadableRegion ?? (throw _notReadyError); + + DownloadProgress? _latestEvent; + DownloadProgress get latestEvent => _latestEvent ?? (throw _notReadyError); + + late int _skippedSeaTileCount; + int get skippedSeaTileCount => _skippedSeaTileCount; + + late int _skippedSeaTileSize; + int get skippedSeaTileSize => _skippedSeaTileSize; + + late int _skippedExistingTileCount; + int get skippedExistingTileCount => _skippedExistingTileCount; + + late int _skippedExistingTileSize; + int get skippedExistingTileSize => _skippedExistingTileSize; + + late String _storeName; + late StreamSubscription _streamSub; + + void assignDownload({ + required String storeName, + required DownloadableRegion downloadableRegion, + required Stream stream, + }) { + _storeName = storeName; + _downloadableRegion = downloadableRegion; + + _skippedExistingTileCount = 0; + _skippedSeaTileCount = 0; + _skippedExistingTileSize = 0; + _skippedSeaTileSize = 0; + + _streamSub = stream.listen( + (progress) { + if (progress.attemptedTiles == 0) _isFocused = true; + _latestEvent = progress; + + final latestTile = progress.latestTileEvent; + + if (latestTile != null && !latestTile.isRepeat) { + if (latestTile.result == TileEventResult.alreadyExisting) { + _skippedExistingTileCount++; + _skippedExistingTileSize += latestTile.tileImage!.lengthInBytes; + } + if (latestTile.result == TileEventResult.isSeaTile) { + _skippedSeaTileCount++; + _skippedSeaTileSize += latestTile.tileImage!.lengthInBytes; + } + } + + notifyListeners(); + }, + onDone: () => _streamSub.cancel(), + ); + } + + Future pause() async { + await FMTCStore(_storeName).download.pause(); + _isPaused = true; + notifyListeners(); + } + + void resume() { + FMTCStore(_storeName).download.resume(); + _isPaused = false; + notifyListeners(); + } + + Future cancel() => FMTCStore(_storeName).download.cancel(); + + void reset() { + _isFocused = false; + notifyListeners(); + } + + StateError get _notReadyError => StateError( + 'Unsafe to retrieve information before a download has been assigned.', + ); +} diff --git a/lib/src/bulk_download/external/download_progress.dart b/lib/src/bulk_download/external/download_progress.dart index 54f0e15c..2fb6f16d 100644 --- a/lib/src/bulk_download/external/download_progress.dart +++ b/lib/src/bulk_download/external/download_progress.dart @@ -9,7 +9,7 @@ part of '../../../flutter_map_tile_caching.dart'; @immutable class DownloadProgress { const DownloadProgress.__({ - required TileEvent? latestTileEvent, + required this.latestTileEvent, required this.cachedTiles, required this.cachedSize, required this.bufferedTiles, @@ -22,7 +22,7 @@ class DownloadProgress { required this.tilesPerSecond, required this.isTPSArtificiallyCapped, required this.isComplete, - }) : _latestTileEvent = latestTileEvent; + }); factory DownloadProgress._initial({required int maxTiles}) => DownloadProgress.__( @@ -44,8 +44,7 @@ class DownloadProgress { /// The result of the latest attempted tile /// /// {@macro fmtc.tileevent.extraConsiderations} - TileEvent get latestTileEvent => _latestTileEvent!; - final TileEvent? _latestTileEvent; + final TileEvent? latestTileEvent; /// The number of new tiles successfully downloaded and in the tile buffer or /// cached @@ -201,7 +200,7 @@ class DownloadProgress { required int? rateLimit, }) => DownloadProgress.__( - latestTileEvent: latestTileEvent._copyWithRepeat(), + latestTileEvent: latestTileEvent?._copyWithRepeat(), cachedTiles: cachedTiles, cachedSize: cachedSize, bufferedTiles: bufferedTiles, @@ -228,7 +227,7 @@ class DownloadProgress { }) { final isNewTile = newTileEvent != null; return DownloadProgress.__( - latestTileEvent: newTileEvent ?? latestTileEvent._copyWithRepeat(), + latestTileEvent: newTileEvent, cachedTiles: isNewTile && newTileEvent.result.category == TileEventResultCategory.cached ? cachedTiles + 1 @@ -264,7 +263,7 @@ class DownloadProgress { bool operator ==(Object other) => identical(this, other) || (other is DownloadProgress && - _latestTileEvent == other._latestTileEvent && + latestTileEvent == other.latestTileEvent && cachedTiles == other.cachedTiles && cachedSize == other.cachedSize && bufferedTiles == other.bufferedTiles && @@ -280,7 +279,7 @@ class DownloadProgress { @override int get hashCode => Object.hashAllUnordered([ - _latestTileEvent, + latestTileEvent, cachedTiles, cachedSize, bufferedTiles, diff --git a/lib/src/bulk_download/internal/manager.dart b/lib/src/bulk_download/internal/manager.dart index 66246d53..610e2a37 100644 --- a/lib/src/bulk_download/internal/manager.dart +++ b/lib/src/bulk_download/internal/manager.dart @@ -216,6 +216,9 @@ Future _downloadManager( // Now it's safe, start accepting communications from the root send(rootReceivePort.sendPort); + // Send an initial progress report to indicate the start of the download + send(initialDownloadProgress); + // Start download threads & wait for download to complete/cancelled downloadDuration.start(); await Future.wait( diff --git a/lib/src/bulk_download/internal/tile_loops/count.dart b/lib/src/bulk_download/internal/tile_loops/count.dart index db20503a..77a71a04 100644 --- a/lib/src/bulk_download/internal/tile_loops/count.dart +++ b/lib/src/bulk_download/internal/tile_loops/count.dart @@ -6,10 +6,6 @@ part of 'shared.dart'; /// A set of methods for each type of [BaseRegion] that counts the number of /// tiles within the specified [DownloadableRegion] /// -/// Each method should handle a [DownloadableRegion] with a specific generic type -/// [BaseRegion]. If a method is passed a non-compatible region, it is expected -/// to throw a `CastError`. -/// /// These methods should be run within seperate isolates, as they do heavy, /// potentially lengthy computation. They do not perform multiple-communication, /// and so only require simple Isolate protocols such as [Isolate.run]. diff --git a/lib/src/bulk_download/internal/tile_loops/generate.dart b/lib/src/bulk_download/internal/tile_loops/generate.dart index 832e2f5f..9253cb68 100644 --- a/lib/src/bulk_download/internal/tile_loops/generate.dart +++ b/lib/src/bulk_download/internal/tile_loops/generate.dart @@ -6,10 +6,6 @@ part of 'shared.dart'; /// A set of methods for each type of [BaseRegion] that generates the coordinates /// of every tile within the specified [DownloadableRegion] /// -/// Each method should handle a [DownloadableRegion] with a specific generic type -/// [BaseRegion]. If a method is passed a non-compatible region, it is expected -/// to throw a `CastError`. -/// /// These methods must be run within seperate isolates, as they do heavy, /// potentially lengthy computation. They do perform multiple-communication, /// sending a new coordinate after they recieve a request message only. They will