feat(todo): the map keeps its size, the search a screen of its own (ADR 0033, D32)
From the developer's trial of the picker. Below a medium width a sheet of fixed height sits under the map, whatever is around the pin, and is pulled up over it for the whole list, as Threema's; a landscape phone or a wider window has it as a pane beside the map. The search opens its own screen: name, what it is, where it lies and how far. A compass under the bar turns the map to north, and my position no longer zooms out. A pinch only zooms unless the fingers turn first, the pin's shadow lies on the map's middle, and a look around that failed says so, with Try again. It asks little: after the map rested, one at a time, nothing for a spot answered or by zooming, and nothing for 30 seconds after a failure, as komoot's Photon throttled a person moving the map about. Fixes the wrong map for the theme: OpenFreeMap's styles name themselves nothing and the tile cache goes by that name, so a theme got the other's tiles (seen on an emulator, both ways); each now has its own name. Whats-New: The location map keeps its size, the list of places pulls up over it, and search opens its own screen with kinds and distances. Whats-New-De: Die Ortskarte behält ihre Größe, die Ortsliste lässt sich über sie ziehen, und die Suche öffnet sich als eigene Seite mit Art und Entfernung. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
ce07fab193
commit
a4c1fe298d
11 changed files with 1663 additions and 241 deletions
|
|
@ -287,6 +287,11 @@
|
|||
"locationSearchFailed": "Die Suche hat nicht geklappt. Versuch es noch einmal.",
|
||||
"locationNoResults": "Nichts gefunden",
|
||||
"locationRecent": "Zuletzt verwendet",
|
||||
"locationNearbyFailed": "Die Umgebung ließ sich nicht abfragen",
|
||||
"locationTryAgain": "Nochmal versuchen",
|
||||
"locationPickHint": "Zoome hinein und schiebe die Karte auf den Ort",
|
||||
"locationNorth": "Nach Norden ausrichten",
|
||||
"locationKind": "{kind, select, city{Stadt} town{Stadt} village{Dorf} hamlet{Weiler} isolated_dwelling{Einzelsiedlung} locality{Flurname} suburb{Stadtteil} neighbourhood{Viertel} residential{Straße} house{Adresse} supermarket{Supermarkt} convenience{Laden} bakery{Bäckerei} butcher{Metzgerei} chemist{Drogerie} pharmacy{Apotheke} doctors{Arztpraxis} dentist{Zahnarztpraxis} hospital{Krankenhaus} restaurant{Restaurant} cafe{Café} fast_food{Imbiss} bar{Bar} pub{Kneipe} fuel{Tankstelle} bank{Bank} post_office{Post} school{Schule} kindergarten{Kindergarten} library{Bibliothek} station{Bahnhof} bus_stop{Bushaltestelle} hotel{Hotel} park{Park} other{}}",
|
||||
"locationNearby": "Oder einen Ort in der Nähe wählen",
|
||||
"locationZoomIn": "Vergrößern",
|
||||
"locationZoomOut": "Verkleinern",
|
||||
|
|
|
|||
|
|
@ -966,6 +966,31 @@
|
|||
"@locationNearby": {
|
||||
"description": "Above the places around the pin in the location picker."
|
||||
},
|
||||
"locationNearbyFailed": "Couldn't look up what's around here",
|
||||
"@locationNearbyFailed": {
|
||||
"description": "The location picker when asking what is around the pin failed."
|
||||
},
|
||||
"locationTryAgain": "Try again",
|
||||
"@locationTryAgain": {
|
||||
"description": "Beside the location picker's message that asking what is around the pin failed."
|
||||
},
|
||||
"locationPickHint": "Zoom in and move the map to a spot",
|
||||
"@locationPickHint": {
|
||||
"description": "The location picker's sheet while the map is too far out for a spot."
|
||||
},
|
||||
"locationNorth": "Point north",
|
||||
"@locationNorth": {
|
||||
"description": "The location picker's compass, turning the map back to north."
|
||||
},
|
||||
"locationKind": "{kind, select, city{City} town{Town} village{Village} hamlet{Hamlet} isolated_dwelling{Isolated dwelling} locality{Locality} suburb{District} neighbourhood{Neighbourhood} residential{Street} house{Address} supermarket{Supermarket} convenience{Shop} bakery{Bakery} butcher{Butcher} chemist{Chemist} pharmacy{Pharmacy} doctors{Doctor} dentist{Dentist} hospital{Hospital} restaurant{Restaurant} cafe{Café} fast_food{Fast food} bar{Bar} pub{Pub} fuel{Petrol station} bank{Bank} post_office{Post office} school{School} kindergarten{Nursery} library{Library} station{Station} bus_stop{Bus stop} hotel{Hotel} park{Park} other{}}",
|
||||
"@locationKind": {
|
||||
"description": "What a place found is, from OpenStreetMap's value; empty for any other.",
|
||||
"placeholders": {
|
||||
"kind": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"locationZoomIn": "Zoom in",
|
||||
"@locationZoomIn": {
|
||||
"description": "The location picker's button zooming the map in."
|
||||
|
|
|
|||
|
|
@ -1828,6 +1828,36 @@ abstract class AppLocalizations {
|
|||
/// **'Or choose a nearby place'**
|
||||
String get locationNearby;
|
||||
|
||||
/// The location picker when asking what is around the pin failed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Couldn\'t look up what\'s around here'**
|
||||
String get locationNearbyFailed;
|
||||
|
||||
/// Beside the location picker's message that asking what is around the pin failed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Try again'**
|
||||
String get locationTryAgain;
|
||||
|
||||
/// The location picker's sheet while the map is too far out for a spot.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Zoom in and move the map to a spot'**
|
||||
String get locationPickHint;
|
||||
|
||||
/// The location picker's compass, turning the map back to north.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Point north'**
|
||||
String get locationNorth;
|
||||
|
||||
/// What a place found is, from OpenStreetMap's value; empty for any other.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{kind, select, city{City} town{Town} village{Village} hamlet{Hamlet} isolated_dwelling{Isolated dwelling} locality{Locality} suburb{District} neighbourhood{Neighbourhood} residential{Street} house{Address} supermarket{Supermarket} convenience{Shop} bakery{Bakery} butcher{Butcher} chemist{Chemist} pharmacy{Pharmacy} doctors{Doctor} dentist{Dentist} hospital{Hospital} restaurant{Restaurant} cafe{Café} fast_food{Fast food} bar{Bar} pub{Pub} fuel{Petrol station} bank{Bank} post_office{Post office} school{School} kindergarten{Nursery} library{Library} station{Station} bus_stop{Bus stop} hotel{Hotel} park{Park} other{}}'**
|
||||
String locationKind(String kind);
|
||||
|
||||
/// The location picker's button zooming the map in.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -1129,6 +1129,64 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get locationNearby => 'Oder einen Ort in der Nähe wählen';
|
||||
|
||||
@override
|
||||
String get locationNearbyFailed => 'Die Umgebung ließ sich nicht abfragen';
|
||||
|
||||
@override
|
||||
String get locationTryAgain => 'Nochmal versuchen';
|
||||
|
||||
@override
|
||||
String get locationPickHint =>
|
||||
'Zoome hinein und schiebe die Karte auf den Ort';
|
||||
|
||||
@override
|
||||
String get locationNorth => 'Nach Norden ausrichten';
|
||||
|
||||
@override
|
||||
String locationKind(String kind) {
|
||||
String _temp0 = intl.Intl.selectLogic(
|
||||
kind,
|
||||
{
|
||||
'city': 'Stadt',
|
||||
'town': 'Stadt',
|
||||
'village': 'Dorf',
|
||||
'hamlet': 'Weiler',
|
||||
'isolated_dwelling': 'Einzelsiedlung',
|
||||
'locality': 'Flurname',
|
||||
'suburb': 'Stadtteil',
|
||||
'neighbourhood': 'Viertel',
|
||||
'residential': 'Straße',
|
||||
'house': 'Adresse',
|
||||
'supermarket': 'Supermarkt',
|
||||
'convenience': 'Laden',
|
||||
'bakery': 'Bäckerei',
|
||||
'butcher': 'Metzgerei',
|
||||
'chemist': 'Drogerie',
|
||||
'pharmacy': 'Apotheke',
|
||||
'doctors': 'Arztpraxis',
|
||||
'dentist': 'Zahnarztpraxis',
|
||||
'hospital': 'Krankenhaus',
|
||||
'restaurant': 'Restaurant',
|
||||
'cafe': 'Café',
|
||||
'fast_food': 'Imbiss',
|
||||
'bar': 'Bar',
|
||||
'pub': 'Kneipe',
|
||||
'fuel': 'Tankstelle',
|
||||
'bank': 'Bank',
|
||||
'post_office': 'Post',
|
||||
'school': 'Schule',
|
||||
'kindergarten': 'Kindergarten',
|
||||
'library': 'Bibliothek',
|
||||
'station': 'Bahnhof',
|
||||
'bus_stop': 'Bushaltestelle',
|
||||
'hotel': 'Hotel',
|
||||
'park': 'Park',
|
||||
'other': '',
|
||||
},
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get locationZoomIn => 'Vergrößern';
|
||||
|
||||
|
|
|
|||
|
|
@ -1128,6 +1128,63 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get locationNearby => 'Or choose a nearby place';
|
||||
|
||||
@override
|
||||
String get locationNearbyFailed => 'Couldn\'t look up what\'s around here';
|
||||
|
||||
@override
|
||||
String get locationTryAgain => 'Try again';
|
||||
|
||||
@override
|
||||
String get locationPickHint => 'Zoom in and move the map to a spot';
|
||||
|
||||
@override
|
||||
String get locationNorth => 'Point north';
|
||||
|
||||
@override
|
||||
String locationKind(String kind) {
|
||||
String _temp0 = intl.Intl.selectLogic(
|
||||
kind,
|
||||
{
|
||||
'city': 'City',
|
||||
'town': 'Town',
|
||||
'village': 'Village',
|
||||
'hamlet': 'Hamlet',
|
||||
'isolated_dwelling': 'Isolated dwelling',
|
||||
'locality': 'Locality',
|
||||
'suburb': 'District',
|
||||
'neighbourhood': 'Neighbourhood',
|
||||
'residential': 'Street',
|
||||
'house': 'Address',
|
||||
'supermarket': 'Supermarket',
|
||||
'convenience': 'Shop',
|
||||
'bakery': 'Bakery',
|
||||
'butcher': 'Butcher',
|
||||
'chemist': 'Chemist',
|
||||
'pharmacy': 'Pharmacy',
|
||||
'doctors': 'Doctor',
|
||||
'dentist': 'Dentist',
|
||||
'hospital': 'Hospital',
|
||||
'restaurant': 'Restaurant',
|
||||
'cafe': 'Café',
|
||||
'fast_food': 'Fast food',
|
||||
'bar': 'Bar',
|
||||
'pub': 'Pub',
|
||||
'fuel': 'Petrol station',
|
||||
'bank': 'Bank',
|
||||
'post_office': 'Post office',
|
||||
'school': 'School',
|
||||
'kindergarten': 'Nursery',
|
||||
'library': 'Library',
|
||||
'station': 'Station',
|
||||
'bus_stop': 'Bus stop',
|
||||
'hotel': 'Hotel',
|
||||
'park': 'Park',
|
||||
'other': '',
|
||||
},
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get locationZoomIn => 'Zoom in';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:halkyon_todo_client/halkyon_todo_client.dart' as protocol;
|
||||
import 'package:halkyon_todo_flutter/src/feature/location/data/location_reminder_gateway.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/location/model/task_location.dart';
|
||||
|
|
@ -101,6 +102,7 @@ class InstancePlaceSearch({
|
|||
address: place.detail ?? '',
|
||||
osmType: place.osmType,
|
||||
osmId: place.osmId,
|
||||
kind: place.osmValue ?? place.kind,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -140,6 +142,7 @@ TaskLocation? _place(Map<String, Object?> at, double lat, double lon) {
|
|||
}.where((part) => part.isNotEmpty).join(', '),
|
||||
osmType: at['osm_type'] as String?,
|
||||
osmId: at['osm_id'] as int?,
|
||||
kind: at['osm_value'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -158,6 +161,7 @@ class PublicPhotonGateway() implements PlaceSearchGateway {
|
|||
final response = await request.close();
|
||||
final body = await response.transform(utf8.decoder).join();
|
||||
if (response.statusCode != HttpStatus.ok) {
|
||||
debugPrint('Photon answered ${response.statusCode}');
|
||||
throw HttpException('Photon: ${response.statusCode}');
|
||||
}
|
||||
return placesOfPhoton(jsonDecode(body) as Map<String, Object?>);
|
||||
|
|
|
|||
|
|
@ -26,6 +26,10 @@ final class const TaskLocation({
|
|||
final String? osmType,
|
||||
final int? osmId,
|
||||
final LocationTrigger trigger = LocationTrigger.none,
|
||||
|
||||
/// What it is in OpenStreetMap (`supermarket`, `village`), for the search's
|
||||
/// list alone: kept nowhere, and no part of what tells two places apart.
|
||||
final String? kind,
|
||||
}) with Equatable {
|
||||
/// What tells it from any other place, whatever its reminder.
|
||||
String get key => '${lat.toStringAsFixed(5)},${lon.toStringAsFixed(5)}';
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -18,6 +18,30 @@ typedef MapTiles = Widget Function();
|
|||
/// while the style loads, nor where it can't be read.
|
||||
Widget vectorTiles() => const _VectorTiles();
|
||||
|
||||
/// The name of OpenFreeMap's style for a [dark] theme or a light one.
|
||||
String mapStyleName({required bool dark}) => dark ? 'dark' : 'liberty';
|
||||
|
||||
/// OpenFreeMap's style for a [dark] theme or a light one. Neither names
|
||||
/// itself, and vector_map_tiles keeps the tiles it draws on the device under
|
||||
/// the style's name (`default` for both): unnamed, a dark app gets the tiles
|
||||
/// it drew while light, so each is given its own.
|
||||
Future<Style> readMapStyle({required bool dark}) async {
|
||||
final name = mapStyleName(dark: dark);
|
||||
return namedMapStyle(
|
||||
await StyleReader(uri: 'https://tiles.openfreemap.org/styles/$name').read(),
|
||||
name,
|
||||
);
|
||||
}
|
||||
|
||||
/// [style] under [name].
|
||||
Style namedMapStyle(Style style, String name) => Style(
|
||||
theme: style.theme.copyWith(id: name),
|
||||
providers: style.providers,
|
||||
sprites: style.sprites,
|
||||
zoom: style.zoom,
|
||||
center: style.center,
|
||||
);
|
||||
|
||||
class const _VectorTiles() extends StatefulWidget {
|
||||
@override
|
||||
State<_VectorTiles> createState() => _VectorTilesState();
|
||||
|
|
@ -33,11 +57,7 @@ class _VectorTilesState() extends State<_VectorTiles> {
|
|||
final dark = Theme.of(context).brightness == Brightness.dark;
|
||||
if (dark != _dark) {
|
||||
_dark = dark;
|
||||
_style = StyleReader(
|
||||
uri:
|
||||
'https://tiles.openfreemap.org/styles/'
|
||||
'${dark ? 'dark' : 'liberty'}',
|
||||
).read();
|
||||
_style = readMapStyle(dark: dark);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,223 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:halkyon_todo_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/location/data/location_reminder_gateway.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/location/data/place_search_gateway.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/location/model/task_location.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// The key of the search's text field.
|
||||
const locationSearchKey = ValueKey('location-search');
|
||||
|
||||
/// What the search ended on: the place chosen, among all it found for what
|
||||
/// was typed.
|
||||
typedef PlaceSearched = ({
|
||||
TaskLocation picked,
|
||||
List<TaskLocation> found,
|
||||
String query,
|
||||
});
|
||||
|
||||
/// [metres] as a list says it, in [locale]'s digits: `850 m`, `1.2 km`,
|
||||
/// `178 km`.
|
||||
String distanceText(double metres, String locale) => switch (metres) {
|
||||
< 1000 => '${metres.round()} m',
|
||||
< 10000 => '${NumberFormat('0.0', locale).format(metres / 1000)} km',
|
||||
_ => '${NumberFormat('#,##0', locale).format(metres / 1000)} km',
|
||||
};
|
||||
|
||||
/// The search of the location picker (ADR 0033, D32), a screen of its own as
|
||||
/// Threema's, the keyboard leaving a map no room: [query] to start with,
|
||||
/// [recents] until something is typed, each place with what it is, where it
|
||||
/// lies, telling two EDEKAs apart, and how far it is from [from].
|
||||
class const PlaceSearchScreen({
|
||||
required final PlaceSearchGateway places,
|
||||
final LatLon? near,
|
||||
final LatLon? from,
|
||||
final List<TaskLocation> recents = const [],
|
||||
final String? query,
|
||||
super.key,
|
||||
}) extends StatefulWidget {
|
||||
@override
|
||||
State<PlaceSearchScreen> createState() => _PlaceSearchScreenState();
|
||||
}
|
||||
|
||||
class _PlaceSearchScreenState() extends State<PlaceSearchScreen> {
|
||||
late final _text = TextEditingController(text: widget.query);
|
||||
Timer? _typing;
|
||||
|
||||
/// Answers to anything but the latest search are dropped.
|
||||
var _asked = 0;
|
||||
var _searching = false;
|
||||
var _failed = false;
|
||||
List<TaskLocation>? _results;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.query?.trim().isNotEmpty ?? false) {
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => unawaited(_find(widget.query!)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_typing?.cancel();
|
||||
_text.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _typed(String text) {
|
||||
_typing?.cancel();
|
||||
if (text.trim().isEmpty) {
|
||||
_asked++;
|
||||
setState(() {
|
||||
_results = null;
|
||||
_searching = false;
|
||||
_failed = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
_typing = Timer(
|
||||
const Duration(milliseconds: 350),
|
||||
() => unawaited(_find(text)),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _find(String text) async {
|
||||
final asked = ++_asked;
|
||||
final language = Localizations.localeOf(context).languageCode;
|
||||
setState(() {
|
||||
_searching = true;
|
||||
_failed = false;
|
||||
});
|
||||
try {
|
||||
final found = await widget.places.search(
|
||||
text.trim(),
|
||||
near: widget.near,
|
||||
language: language,
|
||||
);
|
||||
if (mounted && asked == _asked) {
|
||||
setState(() {
|
||||
_results = found;
|
||||
_searching = false;
|
||||
});
|
||||
}
|
||||
} on Object {
|
||||
if (mounted && asked == _asked) {
|
||||
setState(() {
|
||||
_searching = false;
|
||||
_failed = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String? _distance(TaskLocation place, String locale) => switch (widget.from) {
|
||||
(:final lat, :final lon) => distanceText(
|
||||
const Distance().as(
|
||||
LengthUnit.Meter,
|
||||
LatLng(lat, lon),
|
||||
LatLng(place.lat, place.lon),
|
||||
),
|
||||
locale,
|
||||
),
|
||||
null => null,
|
||||
};
|
||||
|
||||
Widget _row(BuildContext context, TaskLocation place) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final locale = Localizations.localeOf(context).toString();
|
||||
final kind = switch (place.kind) {
|
||||
final kind? => l10n.locationKind(kind),
|
||||
null => '',
|
||||
};
|
||||
final about = [
|
||||
kind,
|
||||
place.address,
|
||||
].where((part) => part.isNotEmpty).join(' · ');
|
||||
final distance = _distance(place, locale);
|
||||
return ListTile(
|
||||
leading: const Icon(LucideIcons.mapPin),
|
||||
title: Text(place.name),
|
||||
subtitle: about.isEmpty ? null : Text(about),
|
||||
trailing: distance == null ? null : Text(distance),
|
||||
onTap: () => Navigator.of(context).pop<PlaceSearched>((
|
||||
picked: place,
|
||||
found: _results ?? const [],
|
||||
query: _text.text.trim(),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final results = _results;
|
||||
final places = results ?? widget.recents;
|
||||
final message = switch ((_failed, results)) {
|
||||
(true, _) => l10n.locationSearchFailed,
|
||||
(_, []) when !_searching => l10n.locationNoResults,
|
||||
_ => null,
|
||||
};
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: const BackButton(),
|
||||
titleSpacing: 0,
|
||||
title: TextField(
|
||||
key: locationSearchKey,
|
||||
controller: _text,
|
||||
autofocus: true,
|
||||
textInputAction: TextInputAction.search,
|
||||
textCapitalization: TextCapitalization.words,
|
||||
decoration: InputDecoration(
|
||||
hintText: l10n.locationSearchHint,
|
||||
border: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
filled: false,
|
||||
),
|
||||
onChanged: _typed,
|
||||
onSubmitted: (text) {
|
||||
_typing?.cancel();
|
||||
unawaited(_find(text));
|
||||
},
|
||||
),
|
||||
bottom: _searching
|
||||
? const PreferredSize(
|
||||
preferredSize: Size.fromHeight(2),
|
||||
child: LinearProgressIndicator(minHeight: 2),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
body: ListView(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.paddingOf(context).bottom),
|
||||
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
||||
children: [
|
||||
if (message != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(
|
||||
message,
|
||||
style: TextStyle(color: scheme.onSurfaceVariant),
|
||||
),
|
||||
),
|
||||
if (results == null && places.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||
child: Text(
|
||||
l10n.locationRecent,
|
||||
style: Theme.of(context).textTheme.labelLarge,
|
||||
),
|
||||
),
|
||||
for (final place in places) _row(context, place),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,9 @@ enum SidePaneKind() {
|
|||
|
||||
/// Today's Plan (D30).
|
||||
plan,
|
||||
|
||||
/// What the location picker says beside its map (D32).
|
||||
location,
|
||||
}
|
||||
|
||||
/// How wide the device keeps its side panes (ADR 0033, D33): a window's
|
||||
|
|
|
|||
Loading…
Reference in a new issue