Merge branch 'develop'
This commit is contained in:
commit
8b4ca6fe83
21 changed files with 1158 additions and 234 deletions
|
@ -1,4 +1,6 @@
|
||||||
// run `scripts/update_flutter_version.sh` to update with the content of `flutter --version --machine`
|
// 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 = {
|
const Map<String, String> version = {
|
||||||
"channel": "unknown",
|
"channel": "unknown",
|
||||||
"dartSdkVersion": "unknown",
|
"dartSdkVersion": "unknown",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
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/foundation.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
@ -15,12 +16,14 @@ class Settings {
|
||||||
Settings._private();
|
Settings._private();
|
||||||
|
|
||||||
// preferences
|
// preferences
|
||||||
|
static const catalogTimeZoneKey = 'catalog_time_zone';
|
||||||
static const collectionGroupFactorKey = 'collection_group_factor';
|
static const collectionGroupFactorKey = 'collection_group_factor';
|
||||||
static const collectionSortFactorKey = 'collection_sort_factor';
|
static const collectionSortFactorKey = 'collection_sort_factor';
|
||||||
static const collectionTileExtentKey = 'collection_tile_extent';
|
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 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 {
|
Future<void> init() async {
|
||||||
_prefs = await SharedPreferences.getInstance();
|
_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) ?? '';
|
String get catalogTimeZone => _prefs.getString(catalogTimeZoneKey) ?? '';
|
||||||
|
|
||||||
set catalogTimeZone(String newValue) => setAndNotify(catalogTimeZoneKey, newValue);
|
set catalogTimeZone(String newValue) => setAndNotify(catalogTimeZoneKey, newValue);
|
||||||
|
@ -70,10 +69,22 @@ class Settings {
|
||||||
|
|
||||||
set collectionTileExtent(double newValue) => setAndNotify(collectionTileExtentKey, newValue);
|
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);
|
bool get hasAcceptedTerms => getBoolOrDefault(hasAcceptedTermsKey, false);
|
||||||
|
|
||||||
set hasAcceptedTerms(bool newValue) => setAndNotify(hasAcceptedTermsKey, newValue);
|
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
|
// convenience methods
|
||||||
|
|
||||||
// ignore: avoid_positional_boolean_parameters
|
// 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -65,7 +65,7 @@ class GridThumbnail extends StatelessWidget {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLongPress: () {
|
onLongPress: () {
|
||||||
if (AvesApp.mode == AppMode.main && collection.isBrowsing) {
|
if (AvesApp.mode == AppMode.main) {
|
||||||
collection.toggleSelection(entry);
|
collection.toggleSelection(entry);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -3,9 +3,7 @@ import 'dart:ui';
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/filters/favourite.dart';
|
import 'package:aves/model/filters/favourite.dart';
|
||||||
import 'package:aves/model/filters/filters.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/mime.dart';
|
||||||
import 'package:aves/model/filters/tag.dart';
|
|
||||||
import 'package:aves/model/mime_types.dart';
|
import 'package:aves/model/mime_types.dart';
|
||||||
import 'package:aves/model/settings.dart';
|
import 'package:aves/model/settings.dart';
|
||||||
import 'package:aves/model/source/album.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/utils/android_file_utils.dart';
|
||||||
import 'package:aves/widgets/about/about_page.dart';
|
import 'package:aves/widgets/about/about_page.dart';
|
||||||
import 'package:aves/widgets/album/collection_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/aves_logo.dart';
|
||||||
import 'package:aves/widgets/common/icons.dart';
|
import 'package:aves/widgets/common/icons.dart';
|
||||||
import 'package:aves/widgets/debug_page.dart';
|
import 'package:aves/widgets/debug_page.dart';
|
||||||
import 'package:aves/widgets/filter_grid_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/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
@ -93,13 +91,22 @@ class _AppDrawerState extends State<AppDrawer> {
|
||||||
title: 'Favourites',
|
title: 'Favourites',
|
||||||
filter: FavouriteFilter(),
|
filter: FavouriteFilter(),
|
||||||
);
|
);
|
||||||
|
final settingsEntry = SafeArea(
|
||||||
|
top: false,
|
||||||
|
bottom: false,
|
||||||
|
child: ListTile(
|
||||||
|
leading: Icon(AIcons.settings),
|
||||||
|
title: Text('Preferences'),
|
||||||
|
onTap: () => _goTo((_) => SettingsPage()),
|
||||||
|
),
|
||||||
|
);
|
||||||
final aboutEntry = SafeArea(
|
final aboutEntry = SafeArea(
|
||||||
top: false,
|
top: false,
|
||||||
bottom: false,
|
bottom: false,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: Icon(AIcons.info),
|
leading: Icon(AIcons.info),
|
||||||
title: Text('About'),
|
title: Text('About'),
|
||||||
onTap: () => _goToAbout(context),
|
onTap: () => _goTo((_) => AboutPage()),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -114,6 +121,7 @@ class _AppDrawerState extends State<AppDrawer> {
|
||||||
_buildCountrySection(),
|
_buildCountrySection(),
|
||||||
_buildTagSection(),
|
_buildTagSection(),
|
||||||
Divider(),
|
Divider(),
|
||||||
|
settingsEntry,
|
||||||
aboutEntry,
|
aboutEntry,
|
||||||
if (kDebugMode) ...[
|
if (kDebugMode) ...[
|
||||||
Divider(),
|
Divider(),
|
||||||
|
@ -123,7 +131,7 @@ class _AppDrawerState extends State<AppDrawer> {
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: Icon(AIcons.debug),
|
leading: Icon(AIcons.debug),
|
||||||
title: Text('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.pop(context);
|
||||||
Navigator.push(
|
Navigator.push(context, MaterialPageRoute(builder: builder));
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
51
lib/widgets/common/action_delegates/map_style_dialog.dart
Normal file
51
lib/widgets/common/action_delegates/map_style_dialog.dart
Normal 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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
|
@ -35,9 +35,7 @@ class AvesDialog extends AlertDialog {
|
||||||
actions: actions,
|
actions: actions,
|
||||||
actionsPadding: EdgeInsets.symmetric(horizontal: 8),
|
actionsPadding: EdgeInsets.symmetric(horizontal: 8),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.all(
|
borderRadius: BorderRadius.circular(24),
|
||||||
Radius.circular(24),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ class BlurredRRect extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ClipRRect(
|
return ClipRRect(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(borderRadius)),
|
borderRadius: BorderRadius.circular(borderRadius),
|
||||||
child: BackdropFilter(
|
child: BackdropFilter(
|
||||||
filter: ImageFilter.blur(sigmaX: 4, sigmaY: 4),
|
filter: ImageFilter.blur(sigmaX: 4, sigmaY: 4),
|
||||||
child: child,
|
child: child,
|
||||||
|
|
|
@ -19,6 +19,7 @@ class AIcons {
|
||||||
static const IconData location = OMIcons.place;
|
static const IconData location = OMIcons.place;
|
||||||
static const IconData shooting = OMIcons.camera;
|
static const IconData shooting = OMIcons.camera;
|
||||||
static const IconData removableStorage = OMIcons.sdStorage;
|
static const IconData removableStorage = OMIcons.sdStorage;
|
||||||
|
static const IconData settings = OMIcons.settings;
|
||||||
static const IconData text = OMIcons.formatQuote;
|
static const IconData text = OMIcons.formatQuote;
|
||||||
static const IconData tag = OMIcons.localOffer;
|
static const IconData tag = OMIcons.localOffer;
|
||||||
|
|
||||||
|
@ -45,6 +46,7 @@ class AIcons {
|
||||||
static const IconData share = OMIcons.share;
|
static const IconData share = OMIcons.share;
|
||||||
static const IconData sort = OMIcons.sort;
|
static const IconData sort = OMIcons.sort;
|
||||||
static const IconData stats = OMIcons.pieChart;
|
static const IconData stats = OMIcons.pieChart;
|
||||||
|
static const IconData style = OMIcons.palette;
|
||||||
static const IconData zoomIn = OMIcons.add;
|
static const IconData zoomIn = OMIcons.add;
|
||||||
static const IconData zoomOut = OMIcons.remove;
|
static const IconData zoomOut = OMIcons.remove;
|
||||||
|
|
||||||
|
@ -136,9 +138,7 @@ class OverlayIcon extends StatelessWidget {
|
||||||
padding: text != null ? EdgeInsets.only(right: size / 4) : null,
|
padding: text != null ? EdgeInsets.only(right: size / 4) : null,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Color(0xBB000000),
|
color: Color(0xBB000000),
|
||||||
borderRadius: BorderRadius.all(
|
borderRadius: BorderRadius.circular(size),
|
||||||
Radius.circular(size),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
child: text == null
|
child: text == null
|
||||||
? iconChild
|
? iconChild
|
||||||
|
|
|
@ -12,9 +12,7 @@ ScrollThumbBuilder avesScrollThumbBuilder({
|
||||||
final scrollThumb = Container(
|
final scrollThumb = Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.black26,
|
color: Colors.black26,
|
||||||
borderRadius: BorderRadius.all(
|
borderRadius: BorderRadius.circular(12.0),
|
||||||
Radius.circular(12.0),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
height: height,
|
height: height,
|
||||||
margin: EdgeInsets.only(right: .5),
|
margin: EdgeInsets.only(right: .5),
|
||||||
|
@ -24,9 +22,7 @@ ScrollThumbBuilder avesScrollThumbBuilder({
|
||||||
width: 20.0,
|
width: 20.0,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
borderRadius: BorderRadius.all(
|
borderRadius: BorderRadius.circular(12.0),
|
||||||
Radius.circular(12.0),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
clipper: ArrowClipper(),
|
clipper: ArrowClipper(),
|
||||||
|
|
|
@ -2,13 +2,19 @@ import 'dart:ui';
|
||||||
|
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/filters/filters.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/image_entry.dart';
|
||||||
import 'package:aves/model/settings.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_lens.dart';
|
||||||
import 'package:aves/model/source/collection_source.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/android_file_utils.dart';
|
||||||
import 'package:aves/utils/durations.dart';
|
import 'package:aves/utils/durations.dart';
|
||||||
import 'package:aves/widgets/album/collection_page.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/raster.dart';
|
||||||
import 'package:aves/widgets/album/thumbnail/vector.dart';
|
import 'package:aves/widgets/album/thumbnail/vector.dart';
|
||||||
import 'package:aves/widgets/app_drawer.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:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||||
import 'package:provider/provider.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 {
|
class FilterNavigationPage extends StatelessWidget {
|
||||||
final CollectionSource source;
|
final CollectionSource source;
|
||||||
final String title;
|
final String title;
|
||||||
|
@ -48,7 +123,12 @@ class FilterNavigationPage extends StatelessWidget {
|
||||||
),
|
),
|
||||||
filterEntries: filterEntries,
|
filterEntries: filterEntries,
|
||||||
filterBuilder: filterBuilder,
|
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(
|
onPressed: (filter) => Navigator.pushAndRemoveUntil(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
|
|
|
@ -2,13 +2,14 @@ import 'package:aves/model/filters/location.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/model/settings.dart';
|
import 'package:aves/model/settings.dart';
|
||||||
import 'package:aves/model/source/collection_lens.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/utils/geo_utils.dart';
|
||||||
import 'package:aves/widgets/common/aves_filter_chip.dart';
|
import 'package:aves/widgets/common/aves_filter_chip.dart';
|
||||||
import 'package:aves/widgets/common/icons.dart';
|
import 'package:aves/widgets/common/icons.dart';
|
||||||
import 'package:aves/widgets/fullscreen/info/info_page.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:flutter/material.dart';
|
||||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
|
||||||
|
|
||||||
class LocationSection extends StatefulWidget {
|
class LocationSection extends StatefulWidget {
|
||||||
final CollectionLens collection;
|
final CollectionLens collection;
|
||||||
|
@ -94,14 +95,24 @@ class _LocationSectionState extends State<LocationSection> {
|
||||||
padding: EdgeInsets.only(bottom: 8),
|
padding: EdgeInsets.only(bottom: 8),
|
||||||
child: SectionRow(AIcons.location),
|
child: SectionRow(AIcons.location),
|
||||||
),
|
),
|
||||||
ImageMap(
|
NotificationListener(
|
||||||
markerId: entry.uri ?? entry.path,
|
onNotification: (notification) {
|
||||||
latLng: LatLng(
|
if (notification is MapStyleChangedNotification) setState(() {});
|
||||||
entry.latLng.item1,
|
return false;
|
||||||
entry.latLng.item2,
|
},
|
||||||
),
|
child: settings.infoMapStyle == EntryMapStyle.google
|
||||||
geoUri: entry.geoUri,
|
? EntryGoogleMap(
|
||||||
initialZoom: settings.infoMapZoom,
|
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)
|
if (location.isNotEmpty)
|
||||||
Padding(
|
Padding(
|
||||||
|
@ -133,113 +144,22 @@ class _LocationSectionState extends State<LocationSection> {
|
||||||
void _handleChange() => setState(() {});
|
void _handleChange() => setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
class ImageMap extends StatefulWidget {
|
// browse providers at https://leaflet-extras.github.io/leaflet-providers/preview/
|
||||||
final String markerId;
|
enum EntryMapStyle { google, osmHot, stamenToner, stamenWatercolor }
|
||||||
final LatLng latLng;
|
|
||||||
final String geoUri;
|
|
||||||
final double initialZoom;
|
|
||||||
|
|
||||||
const ImageMap({
|
extension ExtraEntryMapStyle on EntryMapStyle {
|
||||||
Key key,
|
String get name {
|
||||||
this.markerId,
|
switch (this) {
|
||||||
this.latLng,
|
case EntryMapStyle.google:
|
||||||
this.geoUri,
|
return 'Google Maps';
|
||||||
this.initialZoom,
|
case EntryMapStyle.osmHot:
|
||||||
}) : super(key: key);
|
return 'Humanitarian OpenStreetMap';
|
||||||
|
case EntryMapStyle.stamenToner:
|
||||||
@override
|
return 'Stamen Toner';
|
||||||
State<StatefulWidget> createState() => ImageMapState();
|
case EntryMapStyle.stamenWatercolor:
|
||||||
}
|
return 'Stamen Watercolor';
|
||||||
|
default:
|
||||||
class ImageMapState extends State<ImageMap> with AutomaticKeepAliveClientMixin {
|
return toString();
|
||||||
GoogleMapController _controller;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(ImageMap oldWidget) {
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
if (widget.latLng != oldWidget.latLng && _controller != null) {
|
|
||||||
_controller.moveCamera(CameraUpdate.newLatLng(widget.latLng));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@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;
|
|
||||||
}
|
}
|
||||||
|
|
112
lib/widgets/fullscreen/info/maps/buttons.dart
Normal file
112
lib/widgets/fullscreen/info/maps/buttons.dart
Normal 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 {}
|
109
lib/widgets/fullscreen/info/maps/google_map.dart
Normal file
109
lib/widgets/fullscreen/info/maps/google_map.dart
Normal 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;
|
||||||
|
}
|
218
lib/widgets/fullscreen/info/maps/leaflet_map.dart
Normal file
218
lib/widgets/fullscreen/info/maps/leaflet_map.dart
Normal 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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
140
lib/widgets/fullscreen/info/maps/scale_layer.dart
Normal file
140
lib/widgets/fullscreen/info/maps/scale_layer.dart
Normal 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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
126
lib/widgets/fullscreen/info/maps/scalebar_utils.dart
Normal file
126
lib/widgets/fullscreen/info/maps/scalebar_utils.dart
Normal 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);
|
||||||
|
}
|
|
@ -182,9 +182,7 @@ class VideoControlOverlayState extends State<VideoControlOverlay> with SingleTic
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: FullscreenOverlay.backgroundColor,
|
color: FullscreenOverlay.backgroundColor,
|
||||||
border: FullscreenOverlay.buildBorder(context),
|
border: FullscreenOverlay.buildBorder(context),
|
||||||
borderRadius: BorderRadius.all(
|
borderRadius: BorderRadius.circular(progressBarBorderRadius),
|
||||||
Radius.circular(progressBarBorderRadius),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
key: _progressBarKey,
|
key: _progressBarKey,
|
||||||
|
|
|
@ -8,6 +8,7 @@ import 'package:aves/utils/android_file_utils.dart';
|
||||||
import 'package:aves/widgets/album/collection_page.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/data_providers/media_store_collection_provider.dart';
|
||||||
import 'package:aves/widgets/common/icons.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:aves/widgets/fullscreen/fullscreen_page.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
@ -101,11 +102,17 @@ class _HomePageState extends State<HomePage> {
|
||||||
return SingleFullscreenPage(entry: _viewerEntry);
|
return SingleFullscreenPage(entry: _viewerEntry);
|
||||||
}
|
}
|
||||||
if (_mediaStore != null) {
|
if (_mediaStore != null) {
|
||||||
return CollectionPage(CollectionLens(
|
switch (settings.launchPage) {
|
||||||
source: _mediaStore,
|
case LaunchPage.albums:
|
||||||
groupFactor: settings.collectionGroupFactor,
|
return AlbumListPage(source: _mediaStore);
|
||||||
sortFactor: settings.collectionSortFactor,
|
break;
|
||||||
));
|
case LaunchPage.collection:
|
||||||
|
return CollectionPage(CollectionLens(
|
||||||
|
source: _mediaStore,
|
||||||
|
groupFactor: settings.collectionGroupFactor,
|
||||||
|
sortFactor: settings.collectionSortFactor,
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return SizedBox.shrink();
|
return SizedBox.shrink();
|
||||||
});
|
});
|
||||||
|
|
65
lib/widgets/settings_page.dart
Normal file
65
lib/widgets/settings_page.dart
Normal 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(() {});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
150
pubspec.lock
150
pubspec.lock
|
@ -1,6 +1,13 @@
|
||||||
# Generated by pub
|
# Generated by pub
|
||||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
packages:
|
packages:
|
||||||
|
ansicolor:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: ansicolor
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.2"
|
||||||
archive:
|
archive:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -36,6 +43,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
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:
|
characters:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -78,6 +92,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.14.13"
|
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:
|
convert:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -150,6 +171,13 @@ packages:
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
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:
|
flutter_ijkplayer:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -159,6 +187,20 @@ packages:
|
||||||
url: "git://github.com/deckerst/flutter_ijkplayer.git"
|
url: "git://github.com/deckerst/flutter_ijkplayer.git"
|
||||||
source: git
|
source: git
|
||||||
version: "0.3.7"
|
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:
|
flutter_markdown:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -217,14 +259,28 @@ packages:
|
||||||
name: google_maps_flutter
|
name: google_maps_flutter
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.29"
|
version: "0.5.30"
|
||||||
google_maps_flutter_platform_interface:
|
google_maps_flutter_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: google_maps_flutter_platform_interface
|
name: google_maps_flutter_platform_interface
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
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:
|
image:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -246,6 +302,20 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.2"
|
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:
|
logging:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -274,6 +344,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.8"
|
version: "1.1.8"
|
||||||
|
mgrs_dart:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: mgrs_dart
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.1"
|
||||||
nested:
|
nested:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -323,6 +400,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.4"
|
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:
|
path_provider_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -330,6 +414,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.0.1+2"
|
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:
|
path_provider_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -343,7 +434,7 @@ packages:
|
||||||
name: pdf
|
name: pdf
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.0"
|
version: "1.10.1"
|
||||||
pedantic:
|
pedantic:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -409,13 +500,20 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.2"
|
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:
|
printing:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: printing
|
name: printing
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.5.0"
|
version: "3.6.0"
|
||||||
process:
|
process:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -423,6 +521,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.13"
|
version: "3.0.13"
|
||||||
|
proj4dart:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: proj4dart
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.5"
|
||||||
provider:
|
provider:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -451,6 +556,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.3"
|
version: "2.1.3"
|
||||||
|
rxdart:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: rxdart
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.24.1"
|
||||||
screen:
|
screen:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -575,6 +687,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.17"
|
version: "0.2.17"
|
||||||
|
transparent_image:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: transparent_image
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
tuple:
|
tuple:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -589,6 +708,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
version: "1.2.0"
|
||||||
|
unicode:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: unicode
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.4"
|
||||||
url_launcher:
|
url_launcher:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -623,7 +749,7 @@ packages:
|
||||||
name: url_launcher_web
|
name: url_launcher_web
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.2"
|
version: "0.1.2+1"
|
||||||
utf:
|
utf:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -638,6 +764,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.2.0"
|
||||||
|
validate:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: validate
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.7.0"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -645,6 +778,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.8"
|
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:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -49,6 +49,7 @@ dependencies:
|
||||||
# path: ../flutter_ijkplayer
|
# path: ../flutter_ijkplayer
|
||||||
git:
|
git:
|
||||||
url: git://github.com/deckerst/flutter_ijkplayer.git
|
url: git://github.com/deckerst/flutter_ijkplayer.git
|
||||||
|
flutter_map:
|
||||||
flutter_markdown:
|
flutter_markdown:
|
||||||
flutter_native_timezone:
|
flutter_native_timezone:
|
||||||
flutter_staggered_animations:
|
flutter_staggered_animations:
|
||||||
|
@ -56,6 +57,7 @@ dependencies:
|
||||||
geocoder:
|
geocoder:
|
||||||
google_maps_flutter:
|
google_maps_flutter:
|
||||||
intl:
|
intl:
|
||||||
|
latlong: # for flutter_map
|
||||||
outline_material_icons:
|
outline_material_icons:
|
||||||
package_info:
|
package_info:
|
||||||
palette_generator:
|
palette_generator:
|
||||||
|
|
Loading…
Reference in a new issue