tv: fixed map
This commit is contained in:
parent
a70881c902
commit
eda8baeda2
6 changed files with 349 additions and 272 deletions
104
lib/widgets/map/address_row.dart
Normal file
104
lib/widgets/map/address_row.dart
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
import 'package:aves/model/entry/extensions/location.dart';
|
||||||
|
import 'package:aves/model/settings/enums/coordinate_format.dart';
|
||||||
|
import 'package:aves/model/settings/settings.dart';
|
||||||
|
import 'package:aves/services/common/services.dart';
|
||||||
|
import 'package:aves/services/geocoding_service.dart';
|
||||||
|
import 'package:aves/theme/icons.dart';
|
||||||
|
import 'package:aves/theme/styles.dart';
|
||||||
|
import 'package:aves/theme/text.dart';
|
||||||
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import 'info_row.dart';
|
||||||
|
|
||||||
|
class MapAddressRow extends StatefulWidget {
|
||||||
|
final AvesEntry? entry;
|
||||||
|
|
||||||
|
const MapAddressRow({
|
||||||
|
super.key,
|
||||||
|
required this.entry,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MapAddressRow> createState() => _MapAddressRowState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MapAddressRowState extends State<MapAddressRow> {
|
||||||
|
final ValueNotifier<String?> _addressLineNotifier = ValueNotifier(null);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_updateAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant MapAddressRow oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (oldWidget.entry != widget.entry) {
|
||||||
|
_updateAddress();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
alignment: AlignmentDirectional.centerStart,
|
||||||
|
// addresses can include non-latin scripts with inconsistent line height,
|
||||||
|
// which is especially an issue for relayout/painting of heavy Google map,
|
||||||
|
// so we give extra height to give breathing room to the text and stabilize layout
|
||||||
|
height: Theme.of(context).textTheme.bodyMedium!.fontSize! * context.select<MediaQueryData, double>((mq) => mq.textScaleFactor) * 2,
|
||||||
|
child: ValueListenableBuilder<String?>(
|
||||||
|
valueListenable: _addressLineNotifier,
|
||||||
|
builder: (context, addressLine, child) {
|
||||||
|
final entry = widget.entry;
|
||||||
|
final location = addressLine ??
|
||||||
|
(entry == null
|
||||||
|
? AText.valueNotAvailable
|
||||||
|
: entry.hasAddress
|
||||||
|
? entry.shortAddress
|
||||||
|
: settings.coordinateFormat.format(context.l10n, entry.latLng!));
|
||||||
|
return Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
WidgetSpan(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: MapInfoRow.iconPadding),
|
||||||
|
child: Icon(AIcons.location, size: MapInfoRow.getIconSize(context)),
|
||||||
|
),
|
||||||
|
alignment: PlaceholderAlignment.middle,
|
||||||
|
),
|
||||||
|
TextSpan(text: location),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
strutStyle: AStyles.overflowStrut,
|
||||||
|
softWrap: false,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
maxLines: 1,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _updateAddress() async {
|
||||||
|
final entry = widget.entry;
|
||||||
|
final addressLine = await _getAddressLine(entry);
|
||||||
|
if (mounted && entry == widget.entry) {
|
||||||
|
_addressLineNotifier.value = addressLine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> _getAddressLine(AvesEntry? entry) async {
|
||||||
|
if (entry != null && await availability.canLocatePlaces) {
|
||||||
|
final addresses = await GeocodingService.getAddress(entry.latLng!, settings.appliedLocale);
|
||||||
|
if (addresses.isNotEmpty) {
|
||||||
|
final address = addresses.first;
|
||||||
|
return address.addressLine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
45
lib/widgets/map/date_row.dart
Normal file
45
lib/widgets/map/date_row.dart
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
import 'package:aves/theme/format.dart';
|
||||||
|
import 'package:aves/theme/icons.dart';
|
||||||
|
import 'package:aves/theme/styles.dart';
|
||||||
|
import 'package:aves/theme/text.dart';
|
||||||
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
|
import 'package:aves/widgets/map/info_row.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class MapDateRow extends StatelessWidget {
|
||||||
|
final AvesEntry? entry;
|
||||||
|
|
||||||
|
const MapDateRow({
|
||||||
|
super.key,
|
||||||
|
required this.entry,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final locale = context.l10n.localeName;
|
||||||
|
final use24hour = context.select<MediaQueryData, bool>((v) => v.alwaysUse24HourFormat);
|
||||||
|
|
||||||
|
final date = entry?.bestDate;
|
||||||
|
final dateText = date != null ? formatDateTime(date, locale, use24hour) : AText.valueNotAvailable;
|
||||||
|
return Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
WidgetSpan(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: MapInfoRow.iconPadding),
|
||||||
|
child: Icon(AIcons.date, size: MapInfoRow.getIconSize(context)),
|
||||||
|
),
|
||||||
|
alignment: PlaceholderAlignment.middle,
|
||||||
|
),
|
||||||
|
TextSpan(text: dateText),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
strutStyle: AStyles.overflowStrut,
|
||||||
|
softWrap: false,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
maxLines: 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
63
lib/widgets/map/info_row.dart
Normal file
63
lib/widgets/map/info_row.dart
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
import 'package:aves/widgets/map/address_row.dart';
|
||||||
|
import 'package:aves/widgets/map/date_row.dart';
|
||||||
|
import 'package:aves_map/aves_map.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class MapInfoRow extends StatelessWidget {
|
||||||
|
final ValueNotifier<AvesEntry?> entryNotifier;
|
||||||
|
|
||||||
|
static const double iconPadding = 8.0;
|
||||||
|
static const double _interRowPadding = 2.0;
|
||||||
|
|
||||||
|
const MapInfoRow({
|
||||||
|
super.key,
|
||||||
|
required this.entryNotifier,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final orientation = context.select<MediaQueryData, Orientation>((v) => v.orientation);
|
||||||
|
|
||||||
|
return ValueListenableBuilder<AvesEntry?>(
|
||||||
|
valueListenable: entryNotifier,
|
||||||
|
builder: (context, entry, child) {
|
||||||
|
final content = orientation == Orientation.portrait
|
||||||
|
? [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
MapAddressRow(entry: entry),
|
||||||
|
const SizedBox(height: _interRowPadding),
|
||||||
|
MapDateRow(entry: entry),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
MapDateRow(entry: entry),
|
||||||
|
Expanded(
|
||||||
|
child: MapAddressRow(entry: entry),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
return Opacity(
|
||||||
|
opacity: entry != null ? 1 : 0,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const SizedBox(width: iconPadding),
|
||||||
|
const DotMarker(),
|
||||||
|
...content,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static double getIconSize(BuildContext context) => 16.0 * context.select<MediaQueryData, double>((mq) => mq.textScaleFactor);
|
||||||
|
}
|
|
@ -1,192 +0,0 @@
|
||||||
import 'package:aves/model/entry/entry.dart';
|
|
||||||
import 'package:aves/model/entry/extensions/location.dart';
|
|
||||||
import 'package:aves/model/settings/enums/coordinate_format.dart';
|
|
||||||
import 'package:aves/model/settings/settings.dart';
|
|
||||||
import 'package:aves/services/common/services.dart';
|
|
||||||
import 'package:aves/services/geocoding_service.dart';
|
|
||||||
import 'package:aves/theme/format.dart';
|
|
||||||
import 'package:aves/theme/icons.dart';
|
|
||||||
import 'package:aves/theme/styles.dart';
|
|
||||||
import 'package:aves/theme/text.dart';
|
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
|
||||||
import 'package:aves_map/aves_map.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
class MapInfoRow extends StatelessWidget {
|
|
||||||
final ValueNotifier<AvesEntry?> entryNotifier;
|
|
||||||
|
|
||||||
static const double iconPadding = 8.0;
|
|
||||||
static const double _interRowPadding = 2.0;
|
|
||||||
|
|
||||||
const MapInfoRow({
|
|
||||||
super.key,
|
|
||||||
required this.entryNotifier,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final orientation = context.select<MediaQueryData, Orientation>((v) => v.orientation);
|
|
||||||
|
|
||||||
return ValueListenableBuilder<AvesEntry?>(
|
|
||||||
valueListenable: entryNotifier,
|
|
||||||
builder: (context, entry, child) {
|
|
||||||
final content = orientation == Orientation.portrait
|
|
||||||
? [
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
_AddressRow(entry: entry),
|
|
||||||
const SizedBox(height: _interRowPadding),
|
|
||||||
_DateRow(entry: entry),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
: [
|
|
||||||
_DateRow(entry: entry),
|
|
||||||
Expanded(
|
|
||||||
child: _AddressRow(entry: entry),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
return Opacity(
|
|
||||||
opacity: entry != null ? 1 : 0,
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const SizedBox(width: iconPadding),
|
|
||||||
const DotMarker(),
|
|
||||||
...content,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static double getIconSize(BuildContext context) => 16.0 * context.select<MediaQueryData, double>((mq) => mq.textScaleFactor);
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AddressRow extends StatefulWidget {
|
|
||||||
final AvesEntry? entry;
|
|
||||||
|
|
||||||
const _AddressRow({
|
|
||||||
required this.entry,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_AddressRow> createState() => _AddressRowState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AddressRowState extends State<_AddressRow> {
|
|
||||||
final ValueNotifier<String?> _addressLineNotifier = ValueNotifier(null);
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_updateAddress();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(covariant _AddressRow oldWidget) {
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
if (oldWidget.entry != widget.entry) {
|
|
||||||
_updateAddress();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final entry = widget.entry;
|
|
||||||
return Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const SizedBox(width: MapInfoRow.iconPadding),
|
|
||||||
Icon(AIcons.location, size: MapInfoRow.getIconSize(context)),
|
|
||||||
const SizedBox(width: MapInfoRow.iconPadding),
|
|
||||||
Expanded(
|
|
||||||
child: Container(
|
|
||||||
alignment: AlignmentDirectional.centerStart,
|
|
||||||
// addresses can include non-latin scripts with inconsistent line height,
|
|
||||||
// which is especially an issue for relayout/painting of heavy Google map,
|
|
||||||
// so we give extra height to give breathing room to the text and stabilize layout
|
|
||||||
height: Theme.of(context).textTheme.bodyMedium!.fontSize! * context.select<MediaQueryData, double>((mq) => mq.textScaleFactor) * 2,
|
|
||||||
child: ValueListenableBuilder<String?>(
|
|
||||||
valueListenable: _addressLineNotifier,
|
|
||||||
builder: (context, addressLine, child) {
|
|
||||||
final location = addressLine ??
|
|
||||||
(entry == null
|
|
||||||
? AText.valueNotAvailable
|
|
||||||
: entry.hasAddress
|
|
||||||
? entry.shortAddress
|
|
||||||
: settings.coordinateFormat.format(context.l10n, entry.latLng!));
|
|
||||||
return Text(
|
|
||||||
location,
|
|
||||||
strutStyle: AStyles.overflowStrut,
|
|
||||||
softWrap: false,
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
maxLines: 1,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _updateAddress() async {
|
|
||||||
final entry = widget.entry;
|
|
||||||
final addressLine = await _getAddressLine(entry);
|
|
||||||
if (mounted && entry == widget.entry) {
|
|
||||||
_addressLineNotifier.value = addressLine;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<String?> _getAddressLine(AvesEntry? entry) async {
|
|
||||||
if (entry != null && await availability.canLocatePlaces) {
|
|
||||||
final addresses = await GeocodingService.getAddress(entry.latLng!, settings.appliedLocale);
|
|
||||||
if (addresses.isNotEmpty) {
|
|
||||||
final address = addresses.first;
|
|
||||||
return address.addressLine;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DateRow extends StatelessWidget {
|
|
||||||
final AvesEntry? entry;
|
|
||||||
|
|
||||||
const _DateRow({
|
|
||||||
required this.entry,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final locale = context.l10n.localeName;
|
|
||||||
final use24hour = context.select<MediaQueryData, bool>((v) => v.alwaysUse24HourFormat);
|
|
||||||
|
|
||||||
final date = entry?.bestDate;
|
|
||||||
final dateText = date != null ? formatDateTime(date, locale, use24hour) : AText.valueNotAvailable;
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
const SizedBox(width: MapInfoRow.iconPadding),
|
|
||||||
Icon(AIcons.date, size: MapInfoRow.getIconSize(context)),
|
|
||||||
const SizedBox(width: MapInfoRow.iconPadding),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
dateText,
|
|
||||||
strutStyle: AStyles.overflowStrut,
|
|
||||||
softWrap: false,
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
maxLines: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -15,7 +15,6 @@ import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/model/source/tag.dart';
|
import 'package:aves/model/source/tag.dart';
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/utils/debouncer.dart';
|
|
||||||
import 'package:aves/widgets/collection/collection_page.dart';
|
import 'package:aves/widgets/collection/collection_page.dart';
|
||||||
import 'package:aves/widgets/collection/entry_set_action_delegate.dart';
|
import 'package:aves/widgets/collection/entry_set_action_delegate.dart';
|
||||||
import 'package:aves/widgets/common/basic/font_size_icon_theme.dart';
|
import 'package:aves/widgets/common/basic/font_size_icon_theme.dart';
|
||||||
|
@ -25,14 +24,12 @@ import 'package:aves/widgets/common/basic/scaffold.dart';
|
||||||
import 'package:aves/widgets/common/behaviour/routes.dart';
|
import 'package:aves/widgets/common/behaviour/routes.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/identity/buttons/captioned_button.dart';
|
import 'package:aves/widgets/common/identity/buttons/captioned_button.dart';
|
||||||
import 'package:aves/widgets/common/identity/empty.dart';
|
|
||||||
import 'package:aves/widgets/common/map/geo_map.dart';
|
import 'package:aves/widgets/common/map/geo_map.dart';
|
||||||
import 'package:aves/widgets/common/map/map_action_delegate.dart';
|
import 'package:aves/widgets/common/map/map_action_delegate.dart';
|
||||||
import 'package:aves/widgets/common/providers/highlight_info_provider.dart';
|
import 'package:aves/widgets/common/providers/highlight_info_provider.dart';
|
||||||
import 'package:aves/widgets/common/providers/map_theme_provider.dart';
|
import 'package:aves/widgets/common/providers/map_theme_provider.dart';
|
||||||
import 'package:aves/widgets/common/thumbnail/scroller.dart';
|
|
||||||
import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart';
|
import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart';
|
||||||
import 'package:aves/widgets/map/map_info_row.dart';
|
import 'package:aves/widgets/map/scroller.dart';
|
||||||
import 'package:aves/widgets/viewer/controls/notifications.dart';
|
import 'package:aves/widgets/viewer/controls/notifications.dart';
|
||||||
import 'package:aves/widgets/viewer/entry_viewer_page.dart';
|
import 'package:aves/widgets/viewer/entry_viewer_page.dart';
|
||||||
import 'package:aves_map/aves_map.dart';
|
import 'package:aves_map/aves_map.dart';
|
||||||
|
@ -101,9 +98,8 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
|
||||||
final ValueNotifier<int?> _selectedIndexNotifier = ValueNotifier(0);
|
final ValueNotifier<int?> _selectedIndexNotifier = ValueNotifier(0);
|
||||||
final ValueNotifier<CollectionLens?> _regionCollectionNotifier = ValueNotifier(null);
|
final ValueNotifier<CollectionLens?> _regionCollectionNotifier = ValueNotifier(null);
|
||||||
final ValueNotifier<LatLng?> _dotLocationNotifier = ValueNotifier(null);
|
final ValueNotifier<LatLng?> _dotLocationNotifier = ValueNotifier(null);
|
||||||
final ValueNotifier<AvesEntry?> _dotEntryNotifier = ValueNotifier(null), _infoEntryNotifier = ValueNotifier(null);
|
final ValueNotifier<AvesEntry?> _dotEntryNotifier = ValueNotifier(null);
|
||||||
final ValueNotifier<double> _overlayOpacityNotifier = ValueNotifier(1);
|
final ValueNotifier<double> _overlayOpacityNotifier = ValueNotifier(1);
|
||||||
final Debouncer _infoDebouncer = Debouncer(delay: Durations.mapInfoDebounceDelay);
|
|
||||||
final ValueNotifier<bool> _overlayVisible = ValueNotifier(true);
|
final ValueNotifier<bool> _overlayVisible = ValueNotifier(true);
|
||||||
late AnimationController _overlayAnimationController;
|
late AnimationController _overlayAnimationController;
|
||||||
late Animation<double> _overlayScale, _scrollerSize;
|
late Animation<double> _overlayScale, _scrollerSize;
|
||||||
|
@ -125,8 +121,6 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_dotEntryNotifier.addListener(_onSelectedEntryChanged);
|
|
||||||
|
|
||||||
_overlayAnimationController = AnimationController(
|
_overlayAnimationController = AnimationController(
|
||||||
duration: context.read<DurationsData>().viewerOverlayAnimation,
|
duration: context.read<DurationsData>().viewerOverlayAnimation,
|
||||||
vsync: this,
|
vsync: this,
|
||||||
|
@ -166,7 +160,6 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
|
||||||
..forEach((sub) => sub.cancel())
|
..forEach((sub) => sub.cancel())
|
||||||
..clear();
|
..clear();
|
||||||
_dotEntryNotifier.value?.metadataChangeNotifier.removeListener(_onMarkerEntryMetadataChanged);
|
_dotEntryNotifier.value?.metadataChangeNotifier.removeListener(_onMarkerEntryMetadataChanged);
|
||||||
_dotEntryNotifier.removeListener(_onSelectedEntryChanged);
|
|
||||||
_overlayAnimationController.dispose();
|
_overlayAnimationController.dispose();
|
||||||
_overlayVisible.removeListener(_onOverlayVisibleChanged);
|
_overlayVisible.removeListener(_onOverlayVisibleChanged);
|
||||||
_mapController.dispose();
|
_mapController.dispose();
|
||||||
|
@ -229,8 +222,13 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
const Divider(height: 0),
|
const Divider(height: 0),
|
||||||
_buildOverlayController(),
|
_buildOverlayControls(),
|
||||||
_buildScroller(),
|
MapEntryScroller(
|
||||||
|
regionCollectionNotifier: _regionCollectionNotifier,
|
||||||
|
dotEntryNotifier: _dotEntryNotifier,
|
||||||
|
selectedIndexNotifier: _selectedIndexNotifier,
|
||||||
|
onTap: (index) => _goToViewer(_getRegionEntry(index)),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -300,7 +298,7 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
|
||||||
return child;
|
return child;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildOverlayController() {
|
Widget _buildOverlayControls() {
|
||||||
if (widget.overlayEntry == null) return const SizedBox();
|
if (widget.overlayEntry == null) return const SizedBox();
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
|
@ -326,63 +324,6 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildScroller() {
|
|
||||||
return Stack(
|
|
||||||
children: [
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
SafeArea(
|
|
||||||
top: false,
|
|
||||||
bottom: false,
|
|
||||||
child: MapInfoRow(entryNotifier: _infoEntryNotifier),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Selector<MediaQueryData, double>(
|
|
||||||
selector: (context, mq) => mq.size.width,
|
|
||||||
builder: (context, mqWidth, child) => ValueListenableBuilder<CollectionLens?>(
|
|
||||||
valueListenable: _regionCollectionNotifier,
|
|
||||||
builder: (context, regionCollection, child) {
|
|
||||||
return AnimatedBuilder(
|
|
||||||
// update when entries are added/removed
|
|
||||||
animation: regionCollection ?? ChangeNotifier(),
|
|
||||||
builder: (context, child) {
|
|
||||||
final regionEntries = regionCollection?.sortedEntries ?? [];
|
|
||||||
return ThumbnailScroller(
|
|
||||||
availableWidth: mqWidth,
|
|
||||||
entryCount: regionEntries.length,
|
|
||||||
entryBuilder: (index) => index < regionEntries.length ? regionEntries[index] : null,
|
|
||||||
indexNotifier: _selectedIndexNotifier,
|
|
||||||
onTap: _onThumbnailTap,
|
|
||||||
heroTagger: (entry) => Object.hashAll([regionCollection?.id, entry.id]),
|
|
||||||
highlightable: true,
|
|
||||||
showLocation: false,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Positioned.fill(
|
|
||||||
child: ValueListenableBuilder<CollectionLens?>(
|
|
||||||
valueListenable: _regionCollectionNotifier,
|
|
||||||
builder: (context, regionCollection, child) {
|
|
||||||
return regionCollection != null && regionCollection.isEmpty
|
|
||||||
? EmptyContent(
|
|
||||||
text: context.l10n.mapEmptyRegion,
|
|
||||||
alignment: Alignment.center,
|
|
||||||
fontSize: 18,
|
|
||||||
)
|
|
||||||
: const SizedBox();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onIdle(ZoomedBounds bounds) {
|
void _onIdle(ZoomedBounds bounds) {
|
||||||
_regionFilter = CoordinateFilter(bounds.sw, bounds.ne);
|
_regionFilter = CoordinateFilter(bounds.sw, bounds.ne);
|
||||||
_updateRegionCollection();
|
_updateRegionCollection();
|
||||||
|
@ -432,8 +373,6 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onThumbnailTap(int index) => _goToViewer(_getRegionEntry(index));
|
|
||||||
|
|
||||||
void _onThumbnailIndexChanged() => _onEntrySelected(_getRegionEntry(_selectedIndexNotifier.value));
|
void _onThumbnailIndexChanged() => _onEntrySelected(_getRegionEntry(_selectedIndexNotifier.value));
|
||||||
|
|
||||||
void _onEntrySelected(AvesEntry? selectedEntry) {
|
void _onEntrySelected(AvesEntry? selectedEntry) {
|
||||||
|
@ -447,15 +386,6 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
|
||||||
_dotLocationNotifier.value = _dotEntryNotifier.value?.latLng;
|
_dotLocationNotifier.value = _dotEntryNotifier.value?.latLng;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSelectedEntryChanged() {
|
|
||||||
final selectedEntry = _dotEntryNotifier.value;
|
|
||||||
if (_infoEntryNotifier.value == null || selectedEntry == null) {
|
|
||||||
_infoEntryNotifier.value = selectedEntry;
|
|
||||||
} else {
|
|
||||||
_infoDebouncer(() => _infoEntryNotifier.value = selectedEntry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _goToViewer(AvesEntry? initialEntry) {
|
void _goToViewer(AvesEntry? initialEntry) {
|
||||||
if (initialEntry == null) return;
|
if (initialEntry == null) return;
|
||||||
|
|
||||||
|
|
127
lib/widgets/map/scroller.dart
Normal file
127
lib/widgets/map/scroller.dart
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
|
import 'package:aves/theme/durations.dart';
|
||||||
|
import 'package:aves/utils/debouncer.dart';
|
||||||
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
|
import 'package:aves/widgets/common/identity/empty.dart';
|
||||||
|
import 'package:aves/widgets/common/thumbnail/scroller.dart';
|
||||||
|
import 'package:aves/widgets/map/info_row.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class MapEntryScroller extends StatefulWidget {
|
||||||
|
final ValueNotifier<CollectionLens?> regionCollectionNotifier;
|
||||||
|
final ValueNotifier<AvesEntry?> dotEntryNotifier;
|
||||||
|
final ValueNotifier<int?> selectedIndexNotifier;
|
||||||
|
final void Function(int index) onTap;
|
||||||
|
|
||||||
|
const MapEntryScroller({
|
||||||
|
super.key,
|
||||||
|
required this.regionCollectionNotifier,
|
||||||
|
required this.dotEntryNotifier,
|
||||||
|
required this.selectedIndexNotifier,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MapEntryScroller> createState() => _MapEntryScrollerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MapEntryScrollerState extends State<MapEntryScroller> {
|
||||||
|
final ValueNotifier<AvesEntry?> _infoEntryNotifier = ValueNotifier(null);
|
||||||
|
final Debouncer _infoDebouncer = Debouncer(delay: Durations.mapInfoDebounceDelay);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_registerWidget(widget);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant MapEntryScroller oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
_unregisterWidget(oldWidget);
|
||||||
|
_registerWidget(widget);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_unregisterWidget(widget);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _registerWidget(MapEntryScroller widget) {
|
||||||
|
widget.dotEntryNotifier.addListener(_onSelectedEntryChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _unregisterWidget(MapEntryScroller widget) {
|
||||||
|
widget.dotEntryNotifier.removeListener(_onSelectedEntryChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SafeArea(
|
||||||
|
top: false,
|
||||||
|
bottom: false,
|
||||||
|
child: MapInfoRow(entryNotifier: _infoEntryNotifier),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Selector<MediaQueryData, double>(
|
||||||
|
selector: (context, mq) => mq.size.width,
|
||||||
|
builder: (context, mqWidth, child) => ValueListenableBuilder<CollectionLens?>(
|
||||||
|
valueListenable: widget.regionCollectionNotifier,
|
||||||
|
builder: (context, regionCollection, child) {
|
||||||
|
return AnimatedBuilder(
|
||||||
|
// update when entries are added/removed
|
||||||
|
animation: regionCollection ?? ChangeNotifier(),
|
||||||
|
builder: (context, child) {
|
||||||
|
final regionEntries = regionCollection?.sortedEntries ?? [];
|
||||||
|
return ThumbnailScroller(
|
||||||
|
availableWidth: mqWidth,
|
||||||
|
entryCount: regionEntries.length,
|
||||||
|
entryBuilder: (index) => index < regionEntries.length ? regionEntries[index] : null,
|
||||||
|
indexNotifier: widget.selectedIndexNotifier,
|
||||||
|
onTap: widget.onTap,
|
||||||
|
heroTagger: (entry) => Object.hashAll([regionCollection?.id, entry.id]),
|
||||||
|
highlightable: true,
|
||||||
|
showLocation: false,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Positioned.fill(
|
||||||
|
child: ValueListenableBuilder<CollectionLens?>(
|
||||||
|
valueListenable: widget.regionCollectionNotifier,
|
||||||
|
builder: (context, regionCollection, child) {
|
||||||
|
return regionCollection != null && regionCollection.isEmpty
|
||||||
|
? EmptyContent(
|
||||||
|
text: context.l10n.mapEmptyRegion,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
fontSize: 18,
|
||||||
|
)
|
||||||
|
: const SizedBox();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSelectedEntryChanged() {
|
||||||
|
final selectedEntry = widget.dotEntryNotifier.value;
|
||||||
|
if (_infoEntryNotifier.value == null || selectedEntry == null) {
|
||||||
|
_infoEntryNotifier.value = selectedEntry;
|
||||||
|
} else {
|
||||||
|
_infoDebouncer(() => _infoEntryNotifier.value = selectedEntry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue