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/theme/durations.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/entry_set_action_delegate.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/extensions/build_context.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/map_action_delegate.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/thumbnail/scroller.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/entry_viewer_page.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<CollectionLens?> _regionCollectionNotifier = 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 Debouncer _infoDebouncer = Debouncer(delay: Durations.mapInfoDebounceDelay);
|
||||
final ValueNotifier<bool> _overlayVisible = ValueNotifier(true);
|
||||
late AnimationController _overlayAnimationController;
|
||||
late Animation<double> _overlayScale, _scrollerSize;
|
||||
|
@ -125,8 +121,6 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
|
|||
});
|
||||
}
|
||||
|
||||
_dotEntryNotifier.addListener(_onSelectedEntryChanged);
|
||||
|
||||
_overlayAnimationController = AnimationController(
|
||||
duration: context.read<DurationsData>().viewerOverlayAnimation,
|
||||
vsync: this,
|
||||
|
@ -166,7 +160,6 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
|
|||
..forEach((sub) => sub.cancel())
|
||||
..clear();
|
||||
_dotEntryNotifier.value?.metadataChangeNotifier.removeListener(_onMarkerEntryMetadataChanged);
|
||||
_dotEntryNotifier.removeListener(_onSelectedEntryChanged);
|
||||
_overlayAnimationController.dispose();
|
||||
_overlayVisible.removeListener(_onOverlayVisibleChanged);
|
||||
_mapController.dispose();
|
||||
|
@ -229,8 +222,13 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
|
|||
children: [
|
||||
const SizedBox(height: 8),
|
||||
const Divider(height: 0),
|
||||
_buildOverlayController(),
|
||||
_buildScroller(),
|
||||
_buildOverlayControls(),
|
||||
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;
|
||||
}
|
||||
|
||||
Widget _buildOverlayController() {
|
||||
Widget _buildOverlayControls() {
|
||||
if (widget.overlayEntry == null) return const SizedBox();
|
||||
|
||||
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) {
|
||||
_regionFilter = CoordinateFilter(bounds.sw, bounds.ne);
|
||||
_updateRegionCollection();
|
||||
|
@ -432,8 +373,6 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
|
|||
return null;
|
||||
}
|
||||
|
||||
void _onThumbnailTap(int index) => _goToViewer(_getRegionEntry(index));
|
||||
|
||||
void _onThumbnailIndexChanged() => _onEntrySelected(_getRegionEntry(_selectedIndexNotifier.value));
|
||||
|
||||
void _onEntrySelected(AvesEntry? selectedEntry) {
|
||||
|
@ -447,15 +386,6 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
|
|||
_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) {
|
||||
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