mirror of
https://github.com/timokz/flutter-vienna-hackathon-25.git
synced 2025-11-08 19:04:20 +01:00
add filters to grid view
This commit is contained in:
parent
8bde15320b
commit
2b84f749ae
10 changed files with 473 additions and 136 deletions
|
|
@ -0,0 +1,28 @@
|
|||
import 'dart:math' as math;
|
||||
|
||||
class LocationFilter {
|
||||
final double centerLat;
|
||||
final double centerLon;
|
||||
final double radiusMeters;
|
||||
const LocationFilter({
|
||||
required this.centerLat,
|
||||
required this.centerLon,
|
||||
required this.radiusMeters,
|
||||
});
|
||||
|
||||
bool contains(double lat, double lon) =>
|
||||
_haversineMeters(centerLat, centerLon, lat, lon) <= radiusMeters;
|
||||
|
||||
static double _haversineMeters(
|
||||
double lat1, double lon1, double lat2, double lon2) {
|
||||
const R = 6371000.0;
|
||||
final dLat = (lat2 - lat1) * (math.pi / 180.0);
|
||||
final dLon = (lon2 - lon1) * (math.pi / 180.0);
|
||||
final a = math.sin(dLat / 2) * math.sin(dLat / 2) +
|
||||
math.cos(lat1 * (math.pi / 180.0)) *
|
||||
math.cos(lat2 * (math.pi / 180.0)) *
|
||||
math.sin(dLon / 2) *
|
||||
math.sin(dLon / 2);
|
||||
return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'location_filter.dart';
|
||||
|
||||
class FilterController extends ValueNotifier<LocationFilter?> {
|
||||
FilterController([super.initial]);
|
||||
void clear() => value = null;
|
||||
void setLocation(double lat, double lon, double radiusMeters) {
|
||||
value = LocationFilter(
|
||||
centerLat: lat, centerLon: lon, radiusMeters: radiusMeters);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ import 'package:location/location.dart';
|
|||
import 'package:wien_talks_client/wien_talks_client.dart';
|
||||
import 'package:wien_talks_flutter/widgets/get_location_widget.dart';
|
||||
import 'package:wien_talks_flutter/helper/funmap_mgr.dart';
|
||||
import 'package:wien_talks_flutter/mapfile_widget.dart';
|
||||
import 'package:wien_talks_flutter/widgets/mapfile_widget.dart';
|
||||
import 'package:wien_talks_flutter/widgets/news_input_form.dart';
|
||||
import 'package:wien_talks_flutter/widgets/screen_widget.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:wien_talks_flutter/helper/funmap_mgr.dart';
|
||||
import 'package:wien_talks_flutter/screens/show_latest_news_widget.dart';
|
||||
import 'package:wien_talks_flutter/screens/latest_quotes_screen.dart';
|
||||
import 'package:wien_talks_flutter/widgets/intro_text_widget.dart';
|
||||
import 'package:wien_talks_flutter/widgets/screen_widget.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,221 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import 'package:wien_talks_client/wien_talks_client.dart';
|
||||
import 'package:wien_talks_flutter/helper/funmap_mgr.dart';
|
||||
import 'package:wien_talks_flutter/helper/location_filter.dart';
|
||||
import 'package:wien_talks_flutter/helper/location_util.dart';
|
||||
import 'package:wien_talks_flutter/helper/time_util.dart';
|
||||
import 'package:wien_talks_flutter/widgets/filter_chips_bar.dart';
|
||||
import 'package:wien_talks_flutter/widgets/filter_overlay.dart';
|
||||
import 'package:wien_talks_flutter/widgets/flamboyant_quote_card.dart';
|
||||
|
||||
class LatestQuotesScreen extends StatefulWidget {
|
||||
const LatestQuotesScreen({super.key});
|
||||
|
||||
@override
|
||||
State<LatestQuotesScreen> createState() => _LatestQuotesScreenState();
|
||||
}
|
||||
|
||||
class _LatestQuotesScreenState extends State<LatestQuotesScreen> {
|
||||
final List<Quote> _quotes = [];
|
||||
StreamSubscription<Quote>? _sub;
|
||||
LocationFilter? _locationFilter;
|
||||
String _sort = 'new';
|
||||
bool _today = false;
|
||||
bool _nearby = false;
|
||||
Object? _error;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_connectStream();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_sub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _connectStream() {
|
||||
_sub?.cancel();
|
||||
_sub = FunmapMgr().client.quote.streamAllQuotes(limit: 50).listen(
|
||||
(q) => setState(() => _upsert(q)),
|
||||
onError: (e) => setState(() => _error = e),
|
||||
onDone: () => Future.delayed(const Duration(seconds: 2), () {
|
||||
if (mounted) _connectStream();
|
||||
}),
|
||||
cancelOnError: false,
|
||||
);
|
||||
}
|
||||
|
||||
void _upsert(Quote q) {
|
||||
final i = _quotes.indexWhere((x) => x.id == q.id);
|
||||
if (i >= 0) {
|
||||
_quotes[i] = q;
|
||||
} else {
|
||||
_quotes.add(q);
|
||||
}
|
||||
_applyFilters();
|
||||
}
|
||||
|
||||
Future<void> _vote(Quote quote, bool up) async {
|
||||
final idx = _quotes.indexWhere((q) => q.id == quote.id);
|
||||
if (idx < 0) return;
|
||||
|
||||
final original = _quotes[idx];
|
||||
final updated = original.copyWith(
|
||||
upvotes: up ? original.upvotes + 1 : original.upvotes,
|
||||
downvotes: up ? original.downvotes : original.downvotes + 1,
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_quotes[idx] = updated;
|
||||
});
|
||||
|
||||
try {
|
||||
await FunmapMgr().client.quote.updateQuote(updated);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() => _quotes[idx] = original);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Vote failed: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
List<Quote> get _view {
|
||||
final now = DateTime.now();
|
||||
|
||||
bool isToday(DateTime t) {
|
||||
final lt = t.toLocal();
|
||||
final ln = now.toLocal();
|
||||
return lt.year == ln.year && lt.month == ln.month && lt.day == ln.day;
|
||||
}
|
||||
|
||||
Iterable<Quote> it = _quotes;
|
||||
|
||||
if (_today) {
|
||||
it = it.where((q) => isToday(q.createdAt));
|
||||
}
|
||||
if (_nearby && _locationFilter != null) {
|
||||
final f = _locationFilter!;
|
||||
it = it.where((q) => f.contains(q.lat, q.long));
|
||||
}
|
||||
|
||||
final list = it.toList()
|
||||
..sort((a, b) {
|
||||
if (_sort == 'top') {
|
||||
final as = (a.upvotes - a.downvotes);
|
||||
final bs = (b.upvotes - b.downvotes);
|
||||
final cmp = bs.compareTo(as);
|
||||
if (cmp != 0) return cmp;
|
||||
}
|
||||
return b.createdAt.compareTo(a.createdAt);
|
||||
});
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
Future<void> _pickLocationFilter() async {
|
||||
final picked = await showModalBottomSheet<LocationFilter>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (_) => LocationFilterSheet(current: _locationFilter),
|
||||
);
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
_locationFilter = picked;
|
||||
_nearby = true;
|
||||
});
|
||||
_applyFilters();
|
||||
}
|
||||
}
|
||||
|
||||
void _applyFilters() => setState(() {});
|
||||
|
||||
void _clearFilters() {
|
||||
setState(() {
|
||||
_today = false;
|
||||
_nearby = false;
|
||||
_locationFilter = null;
|
||||
});
|
||||
_applyFilters();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_quotes.isEmpty && _error == null) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (_error != null && _quotes.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text('Error: $_error'),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (_quotes.isEmpty) {
|
||||
return const Center(child: Text('Nix da. Sag halt was'));
|
||||
}
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
pinned: false,
|
||||
floating: false,
|
||||
toolbarHeight: 56,
|
||||
titleSpacing: 12,
|
||||
title: FilterChipsBar(
|
||||
sort: _sort,
|
||||
today: _today,
|
||||
nearby: _nearby,
|
||||
onSortChanged: (s) {
|
||||
setState(() => _sort = s);
|
||||
_applyFilters();
|
||||
},
|
||||
onTodayChanged: (v) {
|
||||
setState(() => _today = v);
|
||||
_applyFilters();
|
||||
},
|
||||
onNearbyPressed: _pickLocationFilter,
|
||||
onClear: (_today || _nearby) ? _clearFilters : null,
|
||||
),
|
||||
elevation: 4,
|
||||
),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 12),
|
||||
sliver: SliverMasonryGrid.count(
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
childCount: _view.length,
|
||||
itemBuilder: (context, i) {
|
||||
final q = _view[i];
|
||||
final author = (q.authorName ?? '').trim();
|
||||
final meta = [
|
||||
if (author.isNotEmpty) author,
|
||||
timeAgo(q.createdAt),
|
||||
].join(' · ');
|
||||
return FlamboyantQuoteCard(
|
||||
quote: q,
|
||||
meta: meta,
|
||||
onVoteUp: () async {
|
||||
await _vote(q, true);
|
||||
_applyFilters();
|
||||
},
|
||||
onVoteDown: () async {
|
||||
await _vote(q, false);
|
||||
_applyFilters();
|
||||
},
|
||||
staticMapUrlBuilder: gStaticMap,
|
||||
);
|
||||
},
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:wien_talks_flutter/screens/show_latest_news_widget.dart';
|
||||
import 'package:wien_talks_flutter/screens/latest_quotes_screen.dart';
|
||||
import 'package:wien_talks_flutter/widgets/heading_text.dart';
|
||||
import 'package:wien_talks_flutter/widgets/screen_widget.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,133 +0,0 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import 'package:wien_talks_client/wien_talks_client.dart';
|
||||
import 'package:wien_talks_flutter/helper/funmap_mgr.dart';
|
||||
import 'package:wien_talks_flutter/helper/location_util.dart';
|
||||
import 'package:wien_talks_flutter/helper/time_util.dart';
|
||||
import 'package:wien_talks_flutter/widgets/flamboyant_quote_card.dart';
|
||||
|
||||
class LatestQuotesScreen extends StatefulWidget {
|
||||
const LatestQuotesScreen({super.key});
|
||||
|
||||
@override
|
||||
State<LatestQuotesScreen> createState() => _LatestQuotesScreenState();
|
||||
}
|
||||
|
||||
class _LatestQuotesScreenState extends State<LatestQuotesScreen> {
|
||||
final List<Quote> _quotes = [];
|
||||
StreamSubscription<Quote>? _sub;
|
||||
|
||||
Object? _error;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_connectStream();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_sub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _connectStream() {
|
||||
_sub?.cancel();
|
||||
_sub = FunmapMgr().client.quote.streamAllQuotes(limit: 50).listen(
|
||||
(q) => setState(() => _upsert(q)),
|
||||
onError: (e) => setState(() => _error = e),
|
||||
onDone: () => Future.delayed(const Duration(seconds: 2), () {
|
||||
if (mounted) _connectStream();
|
||||
}),
|
||||
cancelOnError: false,
|
||||
);
|
||||
}
|
||||
|
||||
void _upsert(Quote q) {
|
||||
final i = _quotes.indexWhere((x) => x.id == q.id);
|
||||
if (i >= 0) {
|
||||
_quotes[i] = q;
|
||||
} else {
|
||||
_quotes.add(q);
|
||||
}
|
||||
_quotes.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
}
|
||||
|
||||
void _sortDesc() {
|
||||
_quotes.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
}
|
||||
|
||||
Future<void> _vote(Quote quote, bool up) async {
|
||||
final idx = _quotes.indexWhere((q) => q.id == quote.id);
|
||||
if (idx < 0) return;
|
||||
|
||||
final original = _quotes[idx];
|
||||
final updated = original.copyWith(
|
||||
upvotes: up ? original.upvotes + 1 : original.upvotes,
|
||||
downvotes: up ? original.downvotes : original.downvotes + 1,
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_quotes[idx] = updated;
|
||||
_sortDesc();
|
||||
});
|
||||
|
||||
try {
|
||||
await FunmapMgr().client.quote.updateQuote(updated);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() => _quotes[idx] = original);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Vote failed: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_quotes.isEmpty && _error == null) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (_error != null && _quotes.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text('Error: $_error'),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (_quotes.isEmpty) {
|
||||
return const Center(child: Text('Nix da. Sag halt was'));
|
||||
}
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return MasonryGridView.count(
|
||||
cacheExtent: 20,
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 12),
|
||||
itemCount: _quotes.length,
|
||||
itemBuilder: (context, i) {
|
||||
final q = _quotes[i];
|
||||
final author = (q.authorName ?? '').trim();
|
||||
final meta = [
|
||||
if (author.isNotEmpty) author,
|
||||
timeAgo(q.createdAt),
|
||||
].join(' · ');
|
||||
|
||||
return FlamboyantQuoteCard(
|
||||
quote: q,
|
||||
meta: meta,
|
||||
onVoteUp: () => _vote(q, true),
|
||||
onVoteDown: () => _vote(q, false),
|
||||
staticMapUrlBuilder: gStaticMap);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:wien_talks_client/wien_talks_client.dart';
|
||||
|
||||
class FilterChipsBar extends StatelessWidget {
|
||||
const FilterChipsBar({
|
||||
super.key,
|
||||
required this.sort,
|
||||
required this.today,
|
||||
required this.nearby,
|
||||
required this.onSortChanged,
|
||||
required this.onTodayChanged,
|
||||
required this.onNearbyPressed,
|
||||
this.onClear,
|
||||
});
|
||||
|
||||
final String sort;
|
||||
final bool today;
|
||||
final bool nearby;
|
||||
|
||||
final ValueChanged<String> onSortChanged;
|
||||
final ValueChanged<bool> onTodayChanged;
|
||||
final VoidCallback onNearbyPressed;
|
||||
final VoidCallback? onClear;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints.tightFor(height: 40),
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: [
|
||||
ChoiceChip(
|
||||
label: const Text('New'),
|
||||
selected: sort == 'new',
|
||||
onSelected: (_) => onSortChanged('new'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ChoiceChip(
|
||||
label: const Text('Loveed'),
|
||||
selected: sort == 'top',
|
||||
onSelected: (_) => onSortChanged('top'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
FilterChip(
|
||||
label: const Text('Today'),
|
||||
selected: today,
|
||||
onSelected: (v) => onTodayChanged(v),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
FilterChip(
|
||||
label: Text('Close by'),
|
||||
selected: nearby,
|
||||
onSelected: (_) => onNearbyPressed(),
|
||||
),
|
||||
if (onClear != null && (today || nearby)) ...[
|
||||
const SizedBox(width: 8),
|
||||
ActionChip(
|
||||
label: const Text('Clear'),
|
||||
onPressed: onClear,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
142
wien_talks/wien_talks_flutter/lib/widgets/filter_overlay.dart
Normal file
142
wien_talks/wien_talks_flutter/lib/widgets/filter_overlay.dart
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:wien_talks_flutter/helper/location_filter.dart';
|
||||
|
||||
class LocationFilterSheet extends StatefulWidget {
|
||||
const LocationFilterSheet({super.key, this.current});
|
||||
final LocationFilter? current;
|
||||
|
||||
@override
|
||||
State<LocationFilterSheet> createState() => _LocationFilterSheetState();
|
||||
}
|
||||
|
||||
class _LocationFilterSheetState extends State<LocationFilterSheet> {
|
||||
final _latCtrl = TextEditingController();
|
||||
final _lonCtrl = TextEditingController();
|
||||
|
||||
double? _lat;
|
||||
double? _lon;
|
||||
double _radius = 1000;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final c = widget.current;
|
||||
_lat = c?.centerLat;
|
||||
_lon = c?.centerLon;
|
||||
_radius = c?.radiusMeters ?? _radius;
|
||||
_latCtrl.text = _lat?.toStringAsFixed(6) ?? '';
|
||||
_lonCtrl.text = _lon?.toStringAsFixed(6) ?? '';
|
||||
_latCtrl.addListener(() => _lat = double.tryParse(_latCtrl.text));
|
||||
_lonCtrl.addListener(() => _lon = double.tryParse(_lonCtrl.text));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_latCtrl.dispose();
|
||||
_lonCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool get _coordsValid {
|
||||
final lat = _lat, lon = _lon;
|
||||
return lat != null &&
|
||||
lon != null &&
|
||||
lat >= -90 &&
|
||||
lat <= 90 &&
|
||||
lon >= -180 &&
|
||||
lon <= 180;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
|
||||
|
||||
return SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.fromLTRB(16, 16, 16, 16 + bottomInset),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('Location filter',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
//todo(timo) fix decimal point display
|
||||
TextField(
|
||||
controller: _latCtrl,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Center latitude',
|
||||
errorText: (_lat == null || (_lat! >= -90 && _lat! <= 90))
|
||||
? null
|
||||
: ' between −90 and 90',
|
||||
),
|
||||
keyboardType: const TextInputType.numberWithOptions(
|
||||
signed: true, decimal: true),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: _lonCtrl,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Center longitude',
|
||||
errorText: (_lon == null || (_lon! >= -180 && _lon! <= 180))
|
||||
? null
|
||||
: ' between −180 and 180',
|
||||
),
|
||||
keyboardType: const TextInputType.numberWithOptions(
|
||||
signed: true, decimal: true),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
const Text('Radius'),
|
||||
Expanded(
|
||||
child: Slider(
|
||||
value: _radius,
|
||||
min: 200,
|
||||
max: 5000,
|
||||
divisions: 24,
|
||||
label:
|
||||
'${(_radius / 1000).toStringAsFixed(_radius < 1000 ? 1 : 0)} km',
|
||||
onChanged: (v) => setState(() => _radius = v),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 72,
|
||||
child: Text('${(_radius / 1000).toStringAsFixed(1)} km'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () =>
|
||||
Navigator.pop<LocationFilter?>(context, null),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: FilledButton(
|
||||
onPressed: _coordsValid
|
||||
? () => Navigator.pop<LocationFilter>(
|
||||
context,
|
||||
LocationFilter(
|
||||
centerLat: _lat!,
|
||||
centerLon: _lon!,
|
||||
radiusMeters: _radius,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
child: const Text('Apply'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue