diff --git a/lib/flutter_version.dart b/lib/flutter_version.dart index bb9c69953..cee1bcb93 100644 --- a/lib/flutter_version.dart +++ b/lib/flutter_version.dart @@ -1,4 +1,6 @@ // run `scripts/update_flutter_version.sh` to update with the content of `flutter --version --machine` +// note on static analysis: the output of the script above yields double quotes, just like the example below +// ignore_for_file: prefer_single_quotes const Map version = { "channel": "unknown", "dartSdkVersion": "unknown", diff --git a/lib/model/settings.dart b/lib/model/settings.dart index 7de40ad4c..f634b4279 100644 --- a/lib/model/settings.dart +++ b/lib/model/settings.dart @@ -1,4 +1,5 @@ import 'package:aves/model/source/collection_lens.dart'; +import 'package:aves/widgets/fullscreen/info/location_section.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -15,12 +16,14 @@ class Settings { Settings._private(); // preferences + static const catalogTimeZoneKey = 'catalog_time_zone'; static const collectionGroupFactorKey = 'collection_group_factor'; static const collectionSortFactorKey = 'collection_sort_factor'; static const collectionTileExtentKey = 'collection_tile_extent'; - static const infoMapZoomKey = 'info_map_zoom'; - static const catalogTimeZoneKey = 'catalog_time_zone'; static const hasAcceptedTermsKey = 'has_accepted_terms'; + static const infoMapStyleKey = 'info_map_style'; + static const infoMapZoomKey = 'info_map_zoom'; + static const launchPageKey = 'launch_page'; Future init() async { _prefs = await SharedPreferences.getInstance(); @@ -50,10 +53,6 @@ class Settings { } } - double get infoMapZoom => _prefs.getDouble(infoMapZoomKey) ?? 12; - - set infoMapZoom(double newValue) => setAndNotify(infoMapZoomKey, newValue); - String get catalogTimeZone => _prefs.getString(catalogTimeZoneKey) ?? ''; set catalogTimeZone(String newValue) => setAndNotify(catalogTimeZoneKey, newValue); @@ -70,10 +69,22 @@ class Settings { set collectionTileExtent(double newValue) => setAndNotify(collectionTileExtentKey, newValue); + EntryMapStyle get infoMapStyle => getEnumOrDefault(infoMapStyleKey, EntryMapStyle.stamenWatercolor, EntryMapStyle.values); + + set infoMapStyle(EntryMapStyle newValue) => setAndNotify(infoMapStyleKey, newValue.toString()); + + double get infoMapZoom => _prefs.getDouble(infoMapZoomKey) ?? 12; + + set infoMapZoom(double newValue) => setAndNotify(infoMapZoomKey, newValue); + bool get hasAcceptedTerms => getBoolOrDefault(hasAcceptedTermsKey, false); set hasAcceptedTerms(bool newValue) => setAndNotify(hasAcceptedTermsKey, newValue); + LaunchPage get launchPage => getEnumOrDefault(launchPageKey, LaunchPage.collection, LaunchPage.values); + + set launchPage(LaunchPage newValue) => setAndNotify(launchPageKey, newValue.toString()); + // convenience methods // ignore: avoid_positional_boolean_parameters @@ -118,3 +129,18 @@ class Settings { } } } + +enum LaunchPage { collection, albums } + +extension ExtraLaunchPage on LaunchPage { + String get name { + switch (this) { + case LaunchPage.collection: + return 'All Media'; + case LaunchPage.albums: + return 'Albums'; + default: + return toString(); + } + } +} diff --git a/lib/widgets/album/grid/list_sliver.dart b/lib/widgets/album/grid/list_sliver.dart index 97fff2258..dc8845a33 100644 --- a/lib/widgets/album/grid/list_sliver.dart +++ b/lib/widgets/album/grid/list_sliver.dart @@ -65,7 +65,7 @@ class GridThumbnail extends StatelessWidget { } }, onLongPress: () { - if (AvesApp.mode == AppMode.main && collection.isBrowsing) { + if (AvesApp.mode == AppMode.main) { collection.toggleSelection(entry); } }, diff --git a/lib/widgets/app_drawer.dart b/lib/widgets/app_drawer.dart index e9eac9d66..29bde3213 100644 --- a/lib/widgets/app_drawer.dart +++ b/lib/widgets/app_drawer.dart @@ -3,9 +3,7 @@ import 'dart:ui'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/filters/location.dart'; import 'package:aves/model/filters/mime.dart'; -import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/mime_types.dart'; import 'package:aves/model/settings.dart'; import 'package:aves/model/source/album.dart'; @@ -16,11 +14,11 @@ import 'package:aves/model/source/tag.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/about/about_page.dart'; import 'package:aves/widgets/album/collection_page.dart'; -import 'package:aves/widgets/album/empty.dart'; import 'package:aves/widgets/common/aves_logo.dart'; import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/debug_page.dart'; import 'package:aves/widgets/filter_grid_page.dart'; +import 'package:aves/widgets/settings_page.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -93,13 +91,22 @@ class _AppDrawerState extends State { title: 'Favourites', filter: FavouriteFilter(), ); + final settingsEntry = SafeArea( + top: false, + bottom: false, + child: ListTile( + leading: Icon(AIcons.settings), + title: Text('Preferences'), + onTap: () => _goTo((_) => SettingsPage()), + ), + ); final aboutEntry = SafeArea( top: false, bottom: false, child: ListTile( leading: Icon(AIcons.info), title: Text('About'), - onTap: () => _goToAbout(context), + onTap: () => _goTo((_) => AboutPage()), ), ); @@ -114,6 +121,7 @@ class _AppDrawerState extends State { _buildCountrySection(), _buildTagSection(), Divider(), + settingsEntry, aboutEntry, if (kDebugMode) ...[ Divider(), @@ -123,7 +131,7 @@ class _AppDrawerState extends State { child: ListTile( leading: Icon(AIcons.debug), title: Text('Debug'), - onTap: () => _goToDebug(context), + onTap: () => _goTo((_) => DebugPage(source: source)), ), ), ], @@ -204,7 +212,7 @@ class _AppDrawerState extends State { ), ); }), - onTap: () => _goToAlbums(context), + onTap: () => _goTo((_) => AlbumListPage(source: source)), ), ); } @@ -226,7 +234,7 @@ class _AppDrawerState extends State { ), ); }), - onTap: () => _goToCountries(context), + onTap: () => _goTo((_) => CountryListPage(source: source)), ), ); } @@ -248,88 +256,14 @@ class _AppDrawerState extends State { ), ); }), - onTap: () => _goToTags(context), + onTap: () => _goTo((_) => TagListPage(source: source)), ), ); } - void _goToAlbums(BuildContext context) { + void _goTo(WidgetBuilder builder) { Navigator.pop(context); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => FilterNavigationPage( - source: source, - title: 'Albums', - filterEntries: source.getAlbumEntries(), - filterBuilder: (s) => AlbumFilter(s, source.getUniqueAlbumName(s)), - emptyBuilder: () => EmptyContent( - icon: AIcons.album, - text: 'No albums', - ), - ), - ), - ); - } - - void _goToCountries(BuildContext context) { - Navigator.pop(context); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => FilterNavigationPage( - source: source, - title: 'Countries', - filterEntries: source.getCountryEntries(), - filterBuilder: (s) => LocationFilter(LocationLevel.country, s), - emptyBuilder: () => EmptyContent( - icon: AIcons.location, - text: 'No countries', - ), - ), - ), - ); - } - - void _goToTags(BuildContext context) { - Navigator.pop(context); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => FilterNavigationPage( - source: source, - title: 'Tags', - filterEntries: source.getTagEntries(), - filterBuilder: (s) => TagFilter(s), - emptyBuilder: () => EmptyContent( - icon: AIcons.tag, - text: 'No tags', - ), - ), - ), - ); - } - - void _goToAbout(BuildContext context) { - Navigator.pop(context); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => AboutPage(), - ), - ); - } - - void _goToDebug(BuildContext context) { - Navigator.pop(context); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => DebugPage( - source: source, - ), - ), - ); + Navigator.push(context, MaterialPageRoute(builder: builder)); } } diff --git a/lib/widgets/common/action_delegates/map_style_dialog.dart b/lib/widgets/common/action_delegates/map_style_dialog.dart new file mode 100644 index 000000000..a9b0fd3d2 --- /dev/null +++ b/lib/widgets/common/action_delegates/map_style_dialog.dart @@ -0,0 +1,51 @@ +import 'package:aves/model/settings.dart'; +import 'package:aves/widgets/fullscreen/info/location_section.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +import '../dialog.dart'; + +class MapStyleDialog extends StatefulWidget { + @override + _MapStyleDialogState createState() => _MapStyleDialogState(); +} + +class _MapStyleDialogState extends State { + EntryMapStyle _selectedStyle; + + @override + void initState() { + super.initState(); + _selectedStyle = settings.infoMapStyle; + } + + @override + Widget build(BuildContext context) { + return AvesDialog( + title: 'Map Style', + scrollableContent: EntryMapStyle.values.map((style) => _buildRadioListTile(style, style.name)).toList(), + actions: [ + FlatButton( + onPressed: () => Navigator.pop(context), + child: Text('Cancel'.toUpperCase()), + ), + FlatButton( + onPressed: () => Navigator.pop(context, _selectedStyle), + child: Text('Apply'.toUpperCase()), + ), + ], + ); + } + + Widget _buildRadioListTile(EntryMapStyle style, String title) => RadioListTile( + value: style, + groupValue: _selectedStyle, + onChanged: (style) => setState(() => _selectedStyle = style), + title: Text( + title, + softWrap: false, + overflow: TextOverflow.fade, + maxLines: 1, + ), + ); +} diff --git a/lib/widgets/common/dialog.dart b/lib/widgets/common/dialog.dart index 01b919004..2681b9101 100644 --- a/lib/widgets/common/dialog.dart +++ b/lib/widgets/common/dialog.dart @@ -35,9 +35,7 @@ class AvesDialog extends AlertDialog { actions: actions, actionsPadding: EdgeInsets.symmetric(horizontal: 8), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.all( - Radius.circular(24), - ), + borderRadius: BorderRadius.circular(24), ), ); } diff --git a/lib/widgets/common/fx/blurred.dart b/lib/widgets/common/fx/blurred.dart index 04e7e75aa..e21af0e29 100644 --- a/lib/widgets/common/fx/blurred.dart +++ b/lib/widgets/common/fx/blurred.dart @@ -27,7 +27,7 @@ class BlurredRRect extends StatelessWidget { @override Widget build(BuildContext context) { return ClipRRect( - borderRadius: BorderRadius.all(Radius.circular(borderRadius)), + borderRadius: BorderRadius.circular(borderRadius), child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 4, sigmaY: 4), child: child, diff --git a/lib/widgets/common/icons.dart b/lib/widgets/common/icons.dart index 1c10cdcaf..43e68b3bc 100644 --- a/lib/widgets/common/icons.dart +++ b/lib/widgets/common/icons.dart @@ -19,6 +19,7 @@ class AIcons { static const IconData location = OMIcons.place; static const IconData shooting = OMIcons.camera; static const IconData removableStorage = OMIcons.sdStorage; + static const IconData settings = OMIcons.settings; static const IconData text = OMIcons.formatQuote; static const IconData tag = OMIcons.localOffer; @@ -45,6 +46,7 @@ class AIcons { static const IconData share = OMIcons.share; static const IconData sort = OMIcons.sort; static const IconData stats = OMIcons.pieChart; + static const IconData style = OMIcons.palette; static const IconData zoomIn = OMIcons.add; static const IconData zoomOut = OMIcons.remove; @@ -136,9 +138,7 @@ class OverlayIcon extends StatelessWidget { padding: text != null ? EdgeInsets.only(right: size / 4) : null, decoration: BoxDecoration( color: Color(0xBB000000), - borderRadius: BorderRadius.all( - Radius.circular(size), - ), + borderRadius: BorderRadius.circular(size), ), child: text == null ? iconChild diff --git a/lib/widgets/common/scroll_thumb.dart b/lib/widgets/common/scroll_thumb.dart index c2434f326..de907a8b3 100644 --- a/lib/widgets/common/scroll_thumb.dart +++ b/lib/widgets/common/scroll_thumb.dart @@ -12,9 +12,7 @@ ScrollThumbBuilder avesScrollThumbBuilder({ final scrollThumb = Container( decoration: BoxDecoration( color: Colors.black26, - borderRadius: BorderRadius.all( - Radius.circular(12.0), - ), + borderRadius: BorderRadius.circular(12.0), ), height: height, margin: EdgeInsets.only(right: .5), @@ -24,9 +22,7 @@ ScrollThumbBuilder avesScrollThumbBuilder({ width: 20.0, decoration: BoxDecoration( color: backgroundColor, - borderRadius: BorderRadius.all( - Radius.circular(12.0), - ), + borderRadius: BorderRadius.circular(12.0), ), ), clipper: ArrowClipper(), diff --git a/lib/widgets/filter_grid_page.dart b/lib/widgets/filter_grid_page.dart index 437f1e9c1..2862d2043 100644 --- a/lib/widgets/filter_grid_page.dart +++ b/lib/widgets/filter_grid_page.dart @@ -2,13 +2,19 @@ import 'dart:ui'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; +import 'package:aves/model/filters/location.dart'; +import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/settings.dart'; +import 'package:aves/model/source/album.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/model/source/location.dart'; +import 'package:aves/model/source/tag.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/durations.dart'; import 'package:aves/widgets/album/collection_page.dart'; +import 'package:aves/widgets/album/empty.dart'; import 'package:aves/widgets/album/thumbnail/raster.dart'; import 'package:aves/widgets/album/thumbnail/vector.dart'; import 'package:aves/widgets/app_drawer.dart'; @@ -20,6 +26,75 @@ import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:provider/provider.dart'; +class AlbumListPage extends StatelessWidget { + final CollectionSource source; + + const AlbumListPage({@required this.source}); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: source.eventBus.on(), + builder: (context, snapshot) => FilterNavigationPage( + source: source, + title: 'Albums', + filterEntries: source.getAlbumEntries(), + filterBuilder: (s) => AlbumFilter(s, source.getUniqueAlbumName(s)), + emptyBuilder: () => EmptyContent( + icon: AIcons.album, + text: 'No albums', + ), + ), + ); + } +} + +class CountryListPage extends StatelessWidget { + final CollectionSource source; + + const CountryListPage({@required this.source}); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: source.eventBus.on(), + builder: (context, snapshot) => FilterNavigationPage( + source: source, + title: 'Countries', + filterEntries: source.getCountryEntries(), + filterBuilder: (s) => LocationFilter(LocationLevel.country, s), + emptyBuilder: () => EmptyContent( + icon: AIcons.location, + text: 'No countries', + ), + ), + ); + } +} + +class TagListPage extends StatelessWidget { + final CollectionSource source; + + const TagListPage({@required this.source}); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: source.eventBus.on(), + builder: (context, snapshot) => FilterNavigationPage( + source: source, + title: 'Tags', + filterEntries: source.getTagEntries(), + filterBuilder: (s) => TagFilter(s), + emptyBuilder: () => EmptyContent( + icon: AIcons.tag, + text: 'No tags', + ), + ), + ); + } +} + class FilterNavigationPage extends StatelessWidget { final CollectionSource source; final String title; @@ -48,7 +123,12 @@ class FilterNavigationPage extends StatelessWidget { ), filterEntries: filterEntries, filterBuilder: filterBuilder, - emptyBuilder: emptyBuilder, + emptyBuilder: () => ValueListenableBuilder( + valueListenable: source.stateNotifier, + builder: (context, sourceState, child) { + return sourceState != SourceState.loading && emptyBuilder != null ? emptyBuilder() : SizedBox.shrink(); + }, + ), onPressed: (filter) => Navigator.pushAndRemoveUntil( context, MaterialPageRoute( diff --git a/lib/widgets/fullscreen/info/location_section.dart b/lib/widgets/fullscreen/info/location_section.dart index 26b57c96f..23b7ada6b 100644 --- a/lib/widgets/fullscreen/info/location_section.dart +++ b/lib/widgets/fullscreen/info/location_section.dart @@ -2,13 +2,14 @@ import 'package:aves/model/filters/location.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; -import 'package:aves/services/android_app_service.dart'; import 'package:aves/utils/geo_utils.dart'; import 'package:aves/widgets/common/aves_filter_chip.dart'; import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/fullscreen/info/info_page.dart'; +import 'package:aves/widgets/fullscreen/info/maps/buttons.dart'; +import 'package:aves/widgets/fullscreen/info/maps/google_map.dart'; +import 'package:aves/widgets/fullscreen/info/maps/leaflet_map.dart'; import 'package:flutter/material.dart'; -import 'package:google_maps_flutter/google_maps_flutter.dart'; class LocationSection extends StatefulWidget { final CollectionLens collection; @@ -94,14 +95,24 @@ class _LocationSectionState extends State { padding: EdgeInsets.only(bottom: 8), child: SectionRow(AIcons.location), ), - ImageMap( - markerId: entry.uri ?? entry.path, - latLng: LatLng( - entry.latLng.item1, - entry.latLng.item2, - ), - geoUri: entry.geoUri, - initialZoom: settings.infoMapZoom, + NotificationListener( + onNotification: (notification) { + if (notification is MapStyleChangedNotification) setState(() {}); + return false; + }, + child: settings.infoMapStyle == EntryMapStyle.google + ? EntryGoogleMap( + markerId: entry.uri ?? entry.path, + latLng: entry.latLng, + geoUri: entry.geoUri, + initialZoom: settings.infoMapZoom, + ) + : EntryLeafletMap( + latLng: entry.latLng, + geoUri: entry.geoUri, + initialZoom: settings.infoMapZoom, + style: settings.infoMapStyle, + ), ), if (location.isNotEmpty) Padding( @@ -133,113 +144,22 @@ class _LocationSectionState extends State { void _handleChange() => setState(() {}); } -class ImageMap extends StatefulWidget { - final String markerId; - final LatLng latLng; - final String geoUri; - final double initialZoom; +// browse providers at https://leaflet-extras.github.io/leaflet-providers/preview/ +enum EntryMapStyle { google, osmHot, stamenToner, stamenWatercolor } - const ImageMap({ - Key key, - this.markerId, - this.latLng, - this.geoUri, - this.initialZoom, - }) : super(key: key); - - @override - State createState() => ImageMapState(); -} - -class ImageMapState extends State with AutomaticKeepAliveClientMixin { - GoogleMapController _controller; - - @override - void didUpdateWidget(ImageMap oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.latLng != oldWidget.latLng && _controller != null) { - _controller.moveCamera(CameraUpdate.newLatLng(widget.latLng)); +extension ExtraEntryMapStyle on EntryMapStyle { + String get name { + switch (this) { + case EntryMapStyle.google: + return 'Google Maps'; + case EntryMapStyle.osmHot: + return 'Humanitarian OpenStreetMap'; + case EntryMapStyle.stamenToner: + return 'Stamen Toner'; + case EntryMapStyle.stamenWatercolor: + return 'Stamen Watercolor'; + default: + return toString(); } } - - @override - Widget build(BuildContext context) { - super.build(context); - final accentHue = HSVColor.fromColor(Theme.of(context).accentColor).hue; - return Row( - children: [ - Expanded( - child: GestureDetector( - onScaleStart: (details) { - // absorb scale gesture here to prevent scrolling - // and triggering by mistake a move to the image page above - }, - child: ClipRRect( - borderRadius: BorderRadius.all( - Radius.circular(16), - ), - child: Container( - color: Colors.white70, - height: 200, - child: GoogleMap( - // GoogleMap init perf issue: https://github.com/flutter/flutter/issues/28493 - initialCameraPosition: CameraPosition( - target: widget.latLng, - zoom: widget.initialZoom, - ), - onMapCreated: (controller) => setState(() => _controller = controller), - rotateGesturesEnabled: false, - scrollGesturesEnabled: false, - zoomControlsEnabled: false, - zoomGesturesEnabled: false, - liteModeEnabled: false, - tiltGesturesEnabled: false, - myLocationEnabled: false, - myLocationButtonEnabled: false, - markers: { - Marker( - markerId: MarkerId(widget.markerId), - icon: BitmapDescriptor.defaultMarkerWithHue(accentHue), - position: widget.latLng, - ) - }, - ), - ), - ), - ), - ), - SizedBox(width: 8), - TooltipTheme( - data: TooltipTheme.of(context).copyWith( - preferBelow: false, - ), - child: Column(children: [ - IconButton( - icon: Icon(AIcons.zoomIn), - onPressed: _controller == null ? null : () => _zoomBy(1), - tooltip: 'Zoom in', - ), - IconButton( - icon: Icon(AIcons.zoomOut), - onPressed: _controller == null ? null : () => _zoomBy(-1), - tooltip: 'Zoom out', - ), - IconButton( - icon: Icon(AIcons.openInNew), - onPressed: () => AndroidAppService.openMap(widget.geoUri), - tooltip: 'Show on map...', - ), - ]), - ) - ], - ); - } - - void _zoomBy(double amount) { - settings.infoMapZoom += amount; - _controller.animateCamera(CameraUpdate.zoomBy(amount)); - } - - @override - bool get wantKeepAlive => true; } diff --git a/lib/widgets/fullscreen/info/maps/buttons.dart b/lib/widgets/fullscreen/info/maps/buttons.dart new file mode 100644 index 000000000..61e5a7929 --- /dev/null +++ b/lib/widgets/fullscreen/info/maps/buttons.dart @@ -0,0 +1,112 @@ +import 'package:aves/model/settings.dart'; +import 'package:aves/services/android_app_service.dart'; +import 'package:aves/widgets/common/action_delegates/map_style_dialog.dart'; +import 'package:aves/widgets/common/fx/blurred.dart'; +import 'package:aves/widgets/common/icons.dart'; +import 'package:aves/widgets/fullscreen/info/location_section.dart'; +import 'package:aves/widgets/fullscreen/overlay/common.dart'; +import 'package:flutter/material.dart'; + +class MapButtonPanel extends StatelessWidget { + final String geoUri; + final void Function(double amount) zoomBy; + + static const BorderRadius mapBorderRadius = BorderRadius.all(Radius.circular(24)); // to match button circles + static const double padding = 4; + + const MapButtonPanel({ + @required this.geoUri, + @required this.zoomBy, + }); + + @override + Widget build(BuildContext context) { + return Positioned.fill( + child: Align( + alignment: AlignmentDirectional.centerEnd, + child: Padding( + padding: EdgeInsets.all(padding), + child: TooltipTheme( + data: TooltipTheme.of(context).copyWith( + preferBelow: false, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + MapOverlayButton( + icon: AIcons.style, + onPressed: () async { + final style = await showDialog( + context: context, + builder: (context) => MapStyleDialog(), + ); + if (style != null) { + settings.infoMapStyle = style; + MapStyleChangedNotification().dispatch(context); + } + }, + tooltip: 'Style map...', + ), + SizedBox(height: padding), + MapOverlayButton( + icon: AIcons.openInNew, + onPressed: () => AndroidAppService.openMap(geoUri), + tooltip: 'Show on map...', + ), + Spacer(), + MapOverlayButton( + icon: AIcons.zoomIn, + onPressed: () => zoomBy(1), + tooltip: 'Zoom in', + ), + SizedBox(height: padding), + MapOverlayButton( + icon: AIcons.zoomOut, + onPressed: () => zoomBy(-1), + tooltip: 'Zoom out', + ), + ], + ), + ), + ), + ), + ); + } +} + +class MapOverlayButton extends StatelessWidget { + final IconData icon; + final String tooltip; + final VoidCallback onPressed; + + const MapOverlayButton({ + @required this.icon, + @required this.tooltip, + @required this.onPressed, + }); + + @override + Widget build(BuildContext context) { + return BlurredOval( + child: Material( + type: MaterialType.circle, + color: FullscreenOverlay.backgroundColor, + child: Ink( + decoration: BoxDecoration( + border: FullscreenOverlay.buildBorder(context), + shape: BoxShape.circle, + ), + child: IconButton( + iconSize: 20, + visualDensity: VisualDensity.compact, + icon: Icon(icon), + onPressed: onPressed, + tooltip: tooltip, + ), + ), + ), + ); + } +} + +class MapStyleChangedNotification extends Notification {} diff --git a/lib/widgets/fullscreen/info/maps/google_map.dart b/lib/widgets/fullscreen/info/maps/google_map.dart new file mode 100644 index 000000000..9971b95df --- /dev/null +++ b/lib/widgets/fullscreen/info/maps/google_map.dart @@ -0,0 +1,109 @@ +import 'package:aves/model/settings.dart'; +import 'package:aves/widgets/fullscreen/info/maps/buttons.dart'; +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:tuple/tuple.dart'; + +class EntryGoogleMap extends StatefulWidget { + final String markerId; + final LatLng latLng; + final String geoUri; + final double initialZoom; + + EntryGoogleMap({ + Key key, + this.markerId, + Tuple2 latLng, + this.geoUri, + this.initialZoom, + }) : latLng = LatLng(latLng.item1, latLng.item2), + super(key: key); + + @override + State createState() => EntryGoogleMapState(); +} + +class EntryGoogleMapState extends State with AutomaticKeepAliveClientMixin { + GoogleMapController _controller; + + @override + void didUpdateWidget(EntryGoogleMap oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.latLng != oldWidget.latLng && _controller != null) { + _controller.moveCamera(CameraUpdate.newLatLng(widget.latLng)); + } + } + + @override + void dispose() { + super.dispose(); + _controller?.dispose(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Stack( + children: [ + _buildMap(), + Positioned.fill( + child: Align( + alignment: AlignmentDirectional.centerEnd, + child: MapButtonPanel( + geoUri: widget.geoUri, + zoomBy: _zoomBy, + ), + ), + ) + ], + ); + } + + Widget _buildMap() { + final accentHue = HSVColor.fromColor(Theme.of(context).accentColor).hue; + return GestureDetector( + onScaleStart: (details) { + // absorb scale gesture here to prevent scrolling + // and triggering by mistake a move to the image page above + }, + child: ClipRRect( + borderRadius: MapButtonPanel.mapBorderRadius, + child: Container( + color: Colors.white70, + height: 200, + child: GoogleMap( + // GoogleMap init perf issue: https://github.com/flutter/flutter/issues/28493 + initialCameraPosition: CameraPosition( + target: widget.latLng, + zoom: widget.initialZoom, + ), + onMapCreated: (controller) => setState(() => _controller = controller), + rotateGesturesEnabled: false, + scrollGesturesEnabled: false, + zoomControlsEnabled: false, + zoomGesturesEnabled: false, + liteModeEnabled: false, + tiltGesturesEnabled: false, + myLocationEnabled: false, + myLocationButtonEnabled: false, + markers: { + Marker( + markerId: MarkerId(widget.markerId), + icon: BitmapDescriptor.defaultMarkerWithHue(accentHue), + position: widget.latLng, + ) + }, + ), + ), + ), + ); + } + + void _zoomBy(double amount) { + settings.infoMapZoom += amount; + _controller.animateCamera(CameraUpdate.zoomBy(amount)); + } + + @override + bool get wantKeepAlive => true; +} diff --git a/lib/widgets/fullscreen/info/maps/leaflet_map.dart b/lib/widgets/fullscreen/info/maps/leaflet_map.dart new file mode 100644 index 000000000..a75c05976 --- /dev/null +++ b/lib/widgets/fullscreen/info/maps/leaflet_map.dart @@ -0,0 +1,218 @@ +import 'package:aves/model/settings.dart'; +import 'package:aves/widgets/fullscreen/info/maps/buttons.dart'; +import 'package:aves/widgets/fullscreen/info/maps/scale_layer.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:latlong/latlong.dart'; +import 'package:tuple/tuple.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../location_section.dart'; + +class EntryLeafletMap extends StatefulWidget { + final LatLng latLng; + final String geoUri; + final double initialZoom; + final EntryMapStyle style; + + EntryLeafletMap({ + Key key, + Tuple2 latLng, + this.geoUri, + this.initialZoom, + this.style, + }) : latLng = LatLng(latLng.item1, latLng.item2), + super(key: key); + + @override + State createState() => EntryLeafletMapState(); +} + +class EntryLeafletMapState extends State with AutomaticKeepAliveClientMixin, TickerProviderStateMixin { + final MapController _mapController = MapController(); + + static const markerSize = 40.0; + + @override + void didUpdateWidget(EntryLeafletMap oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.latLng != oldWidget.latLng && _mapController != null) { + _mapController.move(widget.latLng, settings.infoMapZoom); + } + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + _buildMap(), + MapButtonPanel( + geoUri: widget.geoUri, + zoomBy: _zoomBy, + ), + ], + ), + _buildAttribution(), + ], + ); + } + + Widget _buildMap() { + return GestureDetector( + onScaleStart: (details) { + // absorb scale gesture here to prevent scrolling + // and triggering by mistake a move to the image page above + }, + child: ClipRRect( + borderRadius: MapButtonPanel.mapBorderRadius, + child: Container( + color: Colors.white70, + height: 200, + child: FlutterMap( + options: MapOptions( + center: widget.latLng, + zoom: widget.initialZoom, + interactive: false, + ), + children: [ + _buildMapLayer(), + ScaleLayerWidget( + options: ScaleLayerOptions(), + ), + MarkerLayerWidget( + options: MarkerLayerOptions( + markers: [ + Marker( + width: markerSize, + height: markerSize, + point: widget.latLng, + builder: (ctx) { + return Icon( + Icons.place, + size: markerSize, + color: Theme.of(context).accentColor, + ); + }, + anchorPos: AnchorPos.align(AnchorAlign.top), + ), + ], + ), + ), + ], + mapController: _mapController, + ), + ), + ), + ); + } + + Widget _buildMapLayer() { + switch (widget.style) { + case EntryMapStyle.osmHot: + return OSMHotLayer(); + case EntryMapStyle.stamenToner: + return StamenTonerLayer(); + case EntryMapStyle.stamenWatercolor: + return StamenWatercolorLayer(); + default: + return SizedBox.shrink(); + } + } + + Widget _buildAttribution() { + switch (widget.style) { + case EntryMapStyle.osmHot: + return _buildAttributionMarkdown('© [OpenStreetMap](https://www.openstreetmap.org/copyright) contributors, Tiles style by [Humanitarian OpenStreetMap Team](https://www.hotosm.org/) hosted by [OpenStreetMap France](https://openstreetmap.fr/)'); + case EntryMapStyle.stamenToner: + case EntryMapStyle.stamenWatercolor: + return _buildAttributionMarkdown('Map tiles by [Stamen Design](http://stamen.com), [CC BY 3.0](http://creativecommons.org/licenses/by/3.0) — Map data © [OpenStreetMap](https://www.openstreetmap.org/copyright) contributors'); + default: + return SizedBox.shrink(); + } + } + + Widget _buildAttributionMarkdown(String data) { + return Markdown( + data: data, + selectable: true, + styleSheet: MarkdownStyleSheet( + a: TextStyle(color: Theme.of(context).accentColor), + p: TextStyle(color: Colors.white70, fontSize: 13, fontFamily: 'Concourse'), + ), + onTapLink: (url) async { + if (await canLaunch(url)) { + await launch(url); + } + }, + padding: EdgeInsets.symmetric(vertical: 4, horizontal: 0), + shrinkWrap: true, + ); + } + + void _zoomBy(double amount) { + if (_mapController == null) return; + + final endZoom = (settings.infoMapZoom + amount).clamp(1.0, 16.0); + settings.infoMapZoom = endZoom; + + final zoomTween = Tween(begin: _mapController.zoom, end: endZoom); + final controller = AnimationController(duration: const Duration(milliseconds: 200), vsync: this); + final animation = CurvedAnimation(parent: controller, curve: Curves.fastOutSlowIn); + controller.addListener(() => _mapController.move(widget.latLng, zoomTween.evaluate(animation))); + animation.addStatusListener((status) { + if (status == AnimationStatus.completed) { + controller.dispose(); + } else if (status == AnimationStatus.dismissed) { + controller.dispose(); + } + }); + controller.forward(); + } + + @override + bool get wantKeepAlive => true; +} + +class OSMHotLayer extends StatelessWidget { + @override + Widget build(BuildContext context) { + return TileLayerWidget( + options: TileLayerOptions( + urlTemplate: 'https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', + subdomains: ['a', 'b', 'c'], + retinaMode: MediaQuery.of(context).devicePixelRatio > 1, + ), + ); + } +} + +class StamenTonerLayer extends StatelessWidget { + @override + Widget build(BuildContext context) { + return TileLayerWidget( + options: TileLayerOptions( + urlTemplate: 'https://stamen-tiles-{s}.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}{r}.png', + subdomains: ['a', 'b', 'c', 'd'], + retinaMode: MediaQuery.of(context).devicePixelRatio > 1, + ), + ); + } +} + +class StamenWatercolorLayer extends StatelessWidget { + @override + Widget build(BuildContext context) { + return TileLayerWidget( + options: TileLayerOptions( + urlTemplate: 'https://stamen-tiles-{s}.a.ssl.fastly.net/watercolor/{z}/{x}/{y}.jpg', + subdomains: ['a', 'b', 'c', 'd'], + retinaMode: MediaQuery.of(context).devicePixelRatio > 1, + ), + ); + } +} diff --git a/lib/widgets/fullscreen/info/maps/scale_layer.dart b/lib/widgets/fullscreen/info/maps/scale_layer.dart new file mode 100644 index 000000000..70bf1ad4a --- /dev/null +++ b/lib/widgets/fullscreen/info/maps/scale_layer.dart @@ -0,0 +1,140 @@ +import 'dart:math'; + +import 'package:aves/labs/outlined_text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/plugin_api.dart'; + +import 'scalebar_utils.dart' as util; + +class ScaleLayerOptions extends LayerOptions { + final Widget Function(double width, String distance) builder; + + ScaleLayerOptions({ + Key key, + this.builder = defaultBuilder, + rebuild, + }) : super(key: key, rebuild: rebuild); + + static Widget defaultBuilder(double width, String distance) { + return ScaleBar( + distance: distance, + width: width, + ); + } +} + +class ScaleLayerWidget extends StatelessWidget { + final ScaleLayerOptions options; + + ScaleLayerWidget({@required this.options}) : super(key: options.key); + + @override + Widget build(BuildContext context) { + final mapState = MapState.of(context); + return ScaleLayer(options, mapState, mapState.onMoved); + } +} + +class ScaleLayer extends StatelessWidget { + final ScaleLayerOptions scaleLayerOpts; + final MapState map; + final Stream stream; + final scale = [ + 25000000, + 15000000, + 8000000, + 4000000, + 2000000, + 1000000, + 500000, + 250000, + 100000, + 50000, + 25000, + 15000, + 8000, + 4000, + 2000, + 1000, + 500, + 250, + 100, + 50, + 25, + 10, + 5, + ]; + + ScaleLayer(this.scaleLayerOpts, this.map, this.stream) : super(key: scaleLayerOpts.key); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: stream, + builder: (context, snapshot) { + final center = map.center; + final latitude = center.latitude.abs(); + final level = map.zoom.round() + (latitude > 80 ? 4 : latitude > 60 ? 3 : 2); + final distance = scale[max(0, min(20, level))].toDouble(); + final start = map.project(center); + final targetPoint = util.calculateEndingGlobalCoordinates(center, 90, distance); + final end = map.project(targetPoint); + final displayDistance = distance > 999 ? '${(distance / 1000).toStringAsFixed(0)} km' : '${distance.toStringAsFixed(0)} m'; + final double width = (end.x - start.x); + + return scaleLayerOpts.builder(width, displayDistance); + }, + ); + } +} + +class ScaleBar extends StatelessWidget { + final String distance; + final double width; + + static const Color fillColor = Colors.black; + static const Color outlineColor = Colors.white; + static const double outlineWidth = .5; + static const double barThickness = 1; + + const ScaleBar({ + @required this.distance, + @required this.width, + }); + + @override + Widget build(BuildContext context) { + return Container( + alignment: AlignmentDirectional.bottomStart, + padding: EdgeInsets.symmetric(vertical: 8, horizontal: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + OutlinedText( + text: distance, + style: TextStyle( + color: fillColor, + fontSize: 11, + ), + outlineWidth: outlineWidth * 2, + outlineColor: outlineColor, + ), + Container( + height: barThickness + outlineWidth * 2, + width: width, + decoration: BoxDecoration( + color: fillColor, + border: Border.all( + color: outlineColor, + width: outlineWidth, + ), + borderRadius: BorderRadius.circular(8), + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/fullscreen/info/maps/scalebar_utils.dart b/lib/widgets/fullscreen/info/maps/scalebar_utils.dart new file mode 100644 index 000000000..df781009a --- /dev/null +++ b/lib/widgets/fullscreen/info/maps/scalebar_utils.dart @@ -0,0 +1,126 @@ +import 'dart:math'; + +import 'package:latlong/latlong.dart'; + +const double piOver180 = PI / 180.0; + +double toDegrees(double radians) { + return radians / piOver180; +} + +double toRadians(double degrees) { + return degrees * piOver180; +} + +LatLng calculateEndingGlobalCoordinates(LatLng start, double startBearing, double distance) { + var mSemiMajorAxis = 6378137.0; //WGS84 major axis + var mSemiMinorAxis = (1.0 - 1.0 / 298.257223563) * 6378137.0; + var mFlattening = 1.0 / 298.257223563; + // double mInverseFlattening = 298.257223563; + + var a = mSemiMajorAxis; + var b = mSemiMinorAxis; + var aSquared = a * a; + var bSquared = b * b; + var f = mFlattening; + var phi1 = toRadians(start.latitude); + var alpha1 = toRadians(startBearing); + var cosAlpha1 = cos(alpha1); + var sinAlpha1 = sin(alpha1); + var s = distance; + var tanU1 = (1.0 - f) * tan(phi1); + var cosU1 = 1.0 / sqrt(1.0 + tanU1 * tanU1); + var sinU1 = tanU1 * cosU1; + + // eq. 1 + var sigma1 = atan2(tanU1, cosAlpha1); + + // eq. 2 + var sinAlpha = cosU1 * sinAlpha1; + + var sin2Alpha = sinAlpha * sinAlpha; + var cos2Alpha = 1 - sin2Alpha; + var uSquared = cos2Alpha * (aSquared - bSquared) / bSquared; + + // eq. 3 + var A = 1 + (uSquared / 16384) * (4096 + uSquared * (-768 + uSquared * (320 - 175 * uSquared))); + + // eq. 4 + var B = (uSquared / 1024) * (256 + uSquared * (-128 + uSquared * (74 - 47 * uSquared))); + + // iterate until there is a negligible change in sigma + double deltaSigma; + var sOverbA = s / (b * A); + var sigma = sOverbA; + double sinSigma; + var prevSigma = sOverbA; + double sigmaM2; + double cosSigmaM2; + double cos2SigmaM2; + + for (;;) { + // eq. 5 + sigmaM2 = 2.0 * sigma1 + sigma; + cosSigmaM2 = cos(sigmaM2); + cos2SigmaM2 = cosSigmaM2 * cosSigmaM2; + sinSigma = sin(sigma); + var cosSignma = cos(sigma); + + // eq. 6 + deltaSigma = B * sinSigma * (cosSigmaM2 + (B / 4.0) * (cosSignma * (-1 + 2 * cos2SigmaM2) - (B / 6.0) * cosSigmaM2 * (-3 + 4 * sinSigma * sinSigma) * (-3 + 4 * cos2SigmaM2))); + + // eq. 7 + sigma = sOverbA + deltaSigma; + + // break after converging to tolerance + if ((sigma - prevSigma).abs() < 0.0000000000001) break; + + prevSigma = sigma; + } + + sigmaM2 = 2.0 * sigma1 + sigma; + cosSigmaM2 = cos(sigmaM2); + cos2SigmaM2 = cosSigmaM2 * cosSigmaM2; + + var cosSigma = cos(sigma); + sinSigma = sin(sigma); + + // eq. 8 + var phi2 = atan2(sinU1 * cosSigma + cosU1 * sinSigma * cosAlpha1, (1.0 - f) * sqrt(sin2Alpha + pow(sinU1 * sinSigma - cosU1 * cosSigma * cosAlpha1, 2.0))); + + // eq. 9 + // This fixes the pole crossing defect spotted by Matt Feemster. When a + // path passes a pole and essentially crosses a line of latitude twice - + // once in each direction - the longitude calculation got messed up. + // Using + // atan2 instead of atan fixes the defect. The change is in the next 3 + // lines. + // double tanLambda = sinSigma * sinAlpha1 / (cosU1 * cosSigma - sinU1 * + // sinSigma * cosAlpha1); + // double lambda = Math.atan(tanLambda); + var lambda = atan2(sinSigma * sinAlpha1, (cosU1 * cosSigma - sinU1 * sinSigma * cosAlpha1)); + + // eq. 10 + var C = (f / 16) * cos2Alpha * (4 + f * (4 - 3 * cos2Alpha)); + + // eq. 11 + var L = lambda - (1 - C) * f * sinAlpha * (sigma + C * sinSigma * (cosSigmaM2 + C * cosSigma * (-1 + 2 * cos2SigmaM2))); + + // eq. 12 + // double alpha2 = Math.atan2(sinAlpha, -sinU1 * sinSigma + cosU1 * + // cosSigma * cosAlpha1); + + // build result + var latitude = toDegrees(phi2); + var longitude = start.longitude + toDegrees(L); + + // if ((endBearing != null) && (endBearing.length > 0)) { + // endBearing[0] = toDegrees(alpha2); + // } + + latitude = latitude < -90 ? -90 : latitude; + latitude = latitude > 90 ? 90 : latitude; + longitude = longitude < -180 ? -180 : longitude; + longitude = longitude > 180 ? 180 : longitude; + return LatLng(latitude, longitude); +} diff --git a/lib/widgets/fullscreen/overlay/video.dart b/lib/widgets/fullscreen/overlay/video.dart index 7b7e93405..e8baef6ef 100644 --- a/lib/widgets/fullscreen/overlay/video.dart +++ b/lib/widgets/fullscreen/overlay/video.dart @@ -182,9 +182,7 @@ class VideoControlOverlayState extends State with SingleTic decoration: BoxDecoration( color: FullscreenOverlay.backgroundColor, border: FullscreenOverlay.buildBorder(context), - borderRadius: BorderRadius.all( - Radius.circular(progressBarBorderRadius), - ), + borderRadius: BorderRadius.circular(progressBarBorderRadius), ), child: Column( key: _progressBarKey, diff --git a/lib/widgets/home_page.dart b/lib/widgets/home_page.dart index 6365e0c1e..4ff3fd2d1 100644 --- a/lib/widgets/home_page.dart +++ b/lib/widgets/home_page.dart @@ -8,6 +8,7 @@ import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/album/collection_page.dart'; import 'package:aves/widgets/common/data_providers/media_store_collection_provider.dart'; import 'package:aves/widgets/common/icons.dart'; +import 'package:aves/widgets/filter_grid_page.dart'; import 'package:aves/widgets/fullscreen/fullscreen_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -101,11 +102,17 @@ class _HomePageState extends State { return SingleFullscreenPage(entry: _viewerEntry); } if (_mediaStore != null) { - return CollectionPage(CollectionLens( - source: _mediaStore, - groupFactor: settings.collectionGroupFactor, - sortFactor: settings.collectionSortFactor, - )); + switch (settings.launchPage) { + case LaunchPage.albums: + return AlbumListPage(source: _mediaStore); + break; + case LaunchPage.collection: + return CollectionPage(CollectionLens( + source: _mediaStore, + groupFactor: settings.collectionGroupFactor, + sortFactor: settings.collectionSortFactor, + )); + } } return SizedBox.shrink(); }); diff --git a/lib/widgets/settings_page.dart b/lib/widgets/settings_page.dart new file mode 100644 index 000000000..bd94b32f1 --- /dev/null +++ b/lib/widgets/settings_page.dart @@ -0,0 +1,65 @@ +import 'package:aves/model/settings.dart'; +import 'package:aves/utils/constants.dart'; +import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart'; +import 'package:flutter/material.dart'; + +class SettingsPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MediaQueryDataProvider( + child: DefaultTabController( + length: 4, + child: Scaffold( + appBar: AppBar( + title: Text('Preferences'), + ), + body: SafeArea( + child: ListView( + padding: EdgeInsets.all(16), + children: [ + Text('General', style: Constants.titleTextStyle), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Launch Page:'), + SizedBox(width: 8), + Flexible(child: LaunchPageSelector()), + ], + ), + ], + ), + ), + ), + ), + ); + } +} + +class LaunchPageSelector extends StatefulWidget { + @override + _LaunchPageSelectorState createState() => _LaunchPageSelectorState(); +} + +class _LaunchPageSelectorState extends State { + @override + Widget build(BuildContext context) { + return DropdownButton( + items: LaunchPage.values + .map((selected) => DropdownMenuItem( + value: selected, + child: Text( + selected.name, + softWrap: false, + overflow: TextOverflow.fade, + maxLines: 1, + ), + )) + .toList(), + value: settings.launchPage, + onChanged: (selected) { + settings.launchPage = selected; + setState(() {}); + }, + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 329e3324c..4866bb809 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,13 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + ansicolor: + dependency: transitive + description: + name: ansicolor + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" archive: dependency: transitive description: @@ -36,6 +43,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.0" + cached_network_image: + dependency: transitive + description: + name: cached_network_image + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0+1" characters: dependency: transitive description: @@ -78,6 +92,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.14.13" + console_log_handler: + dependency: transitive + description: + name: console_log_handler + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.6" convert: dependency: transitive description: @@ -150,6 +171,13 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.1" flutter_ijkplayer: dependency: "direct main" description: @@ -159,6 +187,20 @@ packages: url: "git://github.com/deckerst/flutter_ijkplayer.git" source: git version: "0.3.7" + flutter_image: + dependency: transitive + description: + name: flutter_image + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + flutter_map: + dependency: "direct main" + description: + name: flutter_map + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.1+1" flutter_markdown: dependency: "direct main" description: @@ -217,14 +259,28 @@ packages: name: google_maps_flutter url: "https://pub.dartlang.org" source: hosted - version: "0.5.29" + version: "0.5.30" google_maps_flutter_platform_interface: dependency: transitive description: name: google_maps_flutter_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "1.0.4" + http: + dependency: transitive + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.2" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.4" image: dependency: transitive description: @@ -246,6 +302,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.6.2" + latlong: + dependency: "direct main" + description: + name: latlong + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.1" + lists: + dependency: transitive + description: + name: lists + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.6" logging: dependency: transitive description: @@ -274,6 +344,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.8" + mgrs_dart: + dependency: transitive + description: + name: mgrs_dart + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" nested: dependency: transitive description: @@ -323,6 +400,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.1.4" + path_provider: + dependency: transitive + description: + name: path_provider + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.11" path_provider_linux: dependency: transitive description: @@ -330,6 +414,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.0.1+2" + path_provider_macos: + dependency: transitive + description: + name: path_provider_macos + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.4+3" path_provider_platform_interface: dependency: transitive description: @@ -343,7 +434,7 @@ packages: name: pdf url: "https://pub.dartlang.org" source: hosted - version: "1.10.0" + version: "1.10.1" pedantic: dependency: "direct main" description: @@ -409,13 +500,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.2" + positioned_tap_detector: + dependency: transitive + description: + name: positioned_tap_detector + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" printing: dependency: "direct main" description: name: printing url: "https://pub.dartlang.org" source: hosted - version: "3.5.0" + version: "3.6.0" process: dependency: transitive description: @@ -423,6 +521,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.13" + proj4dart: + dependency: transitive + description: + name: proj4dart + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" provider: dependency: "direct main" description: @@ -451,6 +556,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.3" + rxdart: + dependency: transitive + description: + name: rxdart + url: "https://pub.dartlang.org" + source: hosted + version: "0.24.1" screen: dependency: "direct main" description: @@ -575,6 +687,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.2.17" + transparent_image: + dependency: transitive + description: + name: transparent_image + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" tuple: dependency: "direct main" description: @@ -589,6 +708,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" + unicode: + dependency: transitive + description: + name: unicode + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.4" url_launcher: dependency: "direct main" description: @@ -623,7 +749,7 @@ packages: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "0.1.2" + version: "0.1.2+1" utf: dependency: transitive description: @@ -638,6 +764,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.2.0" + validate: + dependency: transitive + description: + name: validate + url: "https://pub.dartlang.org" + source: hosted + version: "1.7.0" vector_math: dependency: transitive description: @@ -645,6 +778,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.8" + wkt_parser: + dependency: transitive + description: + name: wkt_parser + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.7" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 7334ccaf8..5626daa28 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -49,6 +49,7 @@ dependencies: # path: ../flutter_ijkplayer git: url: git://github.com/deckerst/flutter_ijkplayer.git + flutter_map: flutter_markdown: flutter_native_timezone: flutter_staggered_animations: @@ -56,6 +57,7 @@ dependencies: geocoder: google_maps_flutter: intl: + latlong: # for flutter_map outline_material_icons: package_info: palette_generator: