Merge branch 'develop'

This commit is contained in:
Thibault Deckers 2020-08-17 12:48:40 +09:00
commit 8b4ca6fe83
21 changed files with 1158 additions and 234 deletions

View file

@ -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<String, String> version = {
"channel": "unknown",
"dartSdkVersion": "unknown",

View file

@ -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<void> 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();
}
}
}

View file

@ -65,7 +65,7 @@ class GridThumbnail extends StatelessWidget {
}
},
onLongPress: () {
if (AvesApp.mode == AppMode.main && collection.isBrowsing) {
if (AvesApp.mode == AppMode.main) {
collection.toggleSelection(entry);
}
},

View file

@ -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<AppDrawer> {
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<AppDrawer> {
_buildCountrySection(),
_buildTagSection(),
Divider(),
settingsEntry,
aboutEntry,
if (kDebugMode) ...[
Divider(),
@ -123,7 +131,7 @@ class _AppDrawerState extends State<AppDrawer> {
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<AppDrawer> {
),
);
}),
onTap: () => _goToAlbums(context),
onTap: () => _goTo((_) => AlbumListPage(source: source)),
),
);
}
@ -226,7 +234,7 @@ class _AppDrawerState extends State<AppDrawer> {
),
);
}),
onTap: () => _goToCountries(context),
onTap: () => _goTo((_) => CountryListPage(source: source)),
),
);
}
@ -248,88 +256,14 @@ class _AppDrawerState extends State<AppDrawer> {
),
);
}),
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));
}
}

View file

@ -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<MapStyleDialog> {
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<EntryMapStyle>(
value: style,
groupValue: _selectedStyle,
onChanged: (style) => setState(() => _selectedStyle = style),
title: Text(
title,
softWrap: false,
overflow: TextOverflow.fade,
maxLines: 1,
),
);
}

View file

@ -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),
),
);
}

View file

@ -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,

View file

@ -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

View file

@ -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(),

View file

@ -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<AlbumsChangedEvent>(),
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<LocationsChangedEvent>(),
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<TagsChangedEvent>(),
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<SourceState>(
valueListenable: source.stateNotifier,
builder: (context, sourceState, child) {
return sourceState != SourceState.loading && emptyBuilder != null ? emptyBuilder() : SizedBox.shrink();
},
),
onPressed: (filter) => Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(

View file

@ -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<LocationSection> {
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<LocationSection> {
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<StatefulWidget> createState() => ImageMapState();
}
class ImageMapState extends State<ImageMap> 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;
}

View file

@ -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<EntryMapStyle>(
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 {}

View file

@ -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<double, double> latLng,
this.geoUri,
this.initialZoom,
}) : latLng = LatLng(latLng.item1, latLng.item2),
super(key: key);
@override
State<StatefulWidget> createState() => EntryGoogleMapState();
}
class EntryGoogleMapState extends State<EntryGoogleMap> 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;
}

View file

@ -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<double, double> latLng,
this.geoUri,
this.initialZoom,
this.style,
}) : latLng = LatLng(latLng.item1, latLng.item2),
super(key: key);
@override
State<StatefulWidget> createState() => EntryLeafletMapState();
}
class EntryLeafletMapState extends State<EntryLeafletMap> 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<double>(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,
),
);
}
}

View file

@ -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<Null> 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<Null>(
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),
),
),
],
),
);
}
}

View file

@ -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);
}

View file

@ -182,9 +182,7 @@ class VideoControlOverlayState extends State<VideoControlOverlay> 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,

View file

@ -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<HomePage> {
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();
});

View file

@ -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<LaunchPageSelector> {
@override
Widget build(BuildContext context) {
return DropdownButton<LaunchPage>(
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(() {});
},
);
}
}

View file

@ -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:

View file

@ -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: