reorganize and rename widgets
This commit is contained in:
parent
05d25869ef
commit
d5f29dc344
4 changed files with 568 additions and 547 deletions
83
lib/widgets/location/location_map.dart
Normal file
83
lib/widgets/location/location_map.dart
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import "package:flutter_map/flutter_map.dart";
|
||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
import "package:latlong2/latlong.dart";
|
||||
import "package:material_ui/material_ui.dart";
|
||||
import "package:nexus/helpers/launch_helper.dart";
|
||||
|
||||
class const LocationMap(
|
||||
final LatLng center, {
|
||||
final bool showMarker = true,
|
||||
final bool interactive = false,
|
||||
final double zoom = 15,
|
||||
final MapController? controller,
|
||||
final PositionCallback? onPositionChanged,
|
||||
super.key,
|
||||
}) extends ConsumerWidget {
|
||||
/// The marker's icon, which points at the location from its bottom.
|
||||
static Widget markerOf(BuildContext context) => Icon(
|
||||
Icons.location_pin,
|
||||
size: 40,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) => FlutterMap(
|
||||
mapController: controller,
|
||||
options: MapOptions(
|
||||
initialCenter: center,
|
||||
initialZoom: zoom,
|
||||
minZoom: 2,
|
||||
maxZoom: 19,
|
||||
onPositionChanged: onPositionChanged,
|
||||
interactionOptions: InteractionOptions(
|
||||
flags: interactive
|
||||
? InteractiveFlag.all & ~InteractiveFlag.rotate
|
||||
: InteractiveFlag.none,
|
||||
),
|
||||
),
|
||||
children: [
|
||||
TileLayer(
|
||||
urlTemplate: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||
// Required by OpenStreetMap's tile usage policy
|
||||
userAgentPackageName: "nexus.federated.nexus",
|
||||
),
|
||||
if (showMarker)
|
||||
MarkerLayer(
|
||||
markers: [
|
||||
Marker(
|
||||
point: center,
|
||||
width: 40,
|
||||
height: 40,
|
||||
alignment: .topCenter,
|
||||
child: markerOf(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
// Required by OpenStreetMap
|
||||
Align(
|
||||
alignment: .bottomRight,
|
||||
child: Material(
|
||||
color: Theme.of(context).colorScheme.surface.withValues(alpha: 0.8),
|
||||
child: InkWell(
|
||||
onTap: interactive
|
||||
? () => ref
|
||||
.read(LaunchHelper.provider)
|
||||
.launchUrl(
|
||||
Uri.https("www.openstreetmap.org", "/copyright"),
|
||||
)
|
||||
: null,
|
||||
child: Padding(
|
||||
padding: const .symmetric(horizontal: 4, vertical: 2),
|
||||
child: Text(
|
||||
"© OpenStreetMap",
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
maxLines: 1,
|
||||
overflow: .ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
68
lib/widgets/location/location_message.dart
Normal file
68
lib/widgets/location/location_message.dart
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import "package:material_ui/material_ui.dart";
|
||||
import "package:material_adaptive_sheets/material_adaptive_sheets.dart";
|
||||
import "package:nexus/models/content/message.dart";
|
||||
import "package:nexus/models/location.dart";
|
||||
import "package:nexus/widgets/location/location_map.dart";
|
||||
import "package:nexus/widgets/location/location_viewer_sheet.dart";
|
||||
|
||||
class const LocationMessage(final LocationMessageContent content, {super.key})
|
||||
extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final point = content.point;
|
||||
final description = content.description;
|
||||
|
||||
if (point == null) {
|
||||
return Wrap(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Text(
|
||||
"Invalid location:",
|
||||
style: TextStyle(color: theme.colorScheme.error),
|
||||
),
|
||||
Text(content.body),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: .start,
|
||||
spacing: 8,
|
||||
children: [
|
||||
if (description != null)
|
||||
Text(description, style: theme.textTheme.titleSmall),
|
||||
ConstrainedBox(
|
||||
constraints: const .new(maxWidth: 400),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 3 / 2,
|
||||
child: ClipRRect(
|
||||
borderRadius: .circular(12),
|
||||
child: Stack(
|
||||
children: [
|
||||
LocationMap(point),
|
||||
Positioned.fill(
|
||||
child: Material(
|
||||
type: .transparency,
|
||||
child: InkWell(
|
||||
onTap: () => context.showAdaptiveSheet(
|
||||
enableDrag: false,
|
||||
(_) => LocationViewerSheet(
|
||||
point,
|
||||
type: content.assetType,
|
||||
description: description,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(point.formatted, style: theme.textTheme.labelSmall),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
157
lib/widgets/location/location_picker_sheet.dart
Normal file
157
lib/widgets/location/location_picker_sheet.dart
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import "package:flutter_hooks/flutter_hooks.dart";
|
||||
import "package:flutter_map/flutter_map.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:latlong2/latlong.dart";
|
||||
import "package:material_ui/material_ui.dart";
|
||||
import "package:material_adaptive_sheets/material_adaptive_sheets.dart";
|
||||
import "package:nexus/helpers/current_location_helper.dart";
|
||||
import "package:nexus/models/content/message.dart";
|
||||
import "package:nexus/models/location.dart";
|
||||
import "package:nexus/widgets/location/location_map.dart";
|
||||
|
||||
/// Chooses a location to share, either the user's own or a pin dropped on the
|
||||
/// map.
|
||||
class const LocationPickerSheet({
|
||||
required final void Function(LocationMessageContent location) onPick,
|
||||
super.key,
|
||||
}) extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final mapController = useMemoized(MapController.new);
|
||||
useEffect(() => mapController.dispose, [mapController]);
|
||||
|
||||
// Not state, as it changes every frame while the map moves
|
||||
final center = useRef(const LatLng(0, 0));
|
||||
|
||||
// Until the map is moved or the user is located, it's an arbitrary spot
|
||||
final placed = useState(false);
|
||||
// Whether the map is on the user's location, and its accuracy in meters
|
||||
final own = useState<({double? accuracy})?>(null);
|
||||
final locating = useState(false);
|
||||
final error = useState<String?>(null);
|
||||
final descriptionController = useTextEditingController();
|
||||
|
||||
Future<void> locate({bool askPermission = true}) async {
|
||||
locating.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
final found = await ref
|
||||
.read(CurrentLocationHelper.provider)
|
||||
.get(askPermission: askPermission);
|
||||
// Doesn't move away from a spot the user chose while it was finding them
|
||||
if (found == null ||
|
||||
!context.mounted ||
|
||||
(!askPermission && placed.value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final (point, accuracy) = found;
|
||||
center.value = point;
|
||||
mapController.move(point, 16);
|
||||
own.value = (accuracy: accuracy);
|
||||
placed.value = true;
|
||||
} catch (caught) {
|
||||
if (context.mounted && askPermission) error.value = caught.toString();
|
||||
} finally {
|
||||
if (context.mounted) locating.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Starts at the user's location if they've already allowed it, except on
|
||||
// Linux, where checking would ask
|
||||
useEffect(() {
|
||||
locate(askPermission: false);
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
void share() {
|
||||
final description = descriptionController.text.trim();
|
||||
final self = own.value != null;
|
||||
final geoUri = GeoUri.fromLatLng(
|
||||
center.value,
|
||||
uncertainty: own.value?.accuracy,
|
||||
);
|
||||
|
||||
onPick(
|
||||
.new(
|
||||
body:
|
||||
"${description.isEmpty ? (self ? "My location" : "Location") : description}: $geoUri",
|
||||
geoUri: geoUri,
|
||||
location: .new(
|
||||
uri: geoUri,
|
||||
description: description.isEmpty ? null : description,
|
||||
),
|
||||
asset: .new(type: self ? .self : .pin),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SheetLayout(
|
||||
title: const Text("Share Location"),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
child: const Text("Cancel"),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: placed.value ? share : null,
|
||||
child: const Text("Share"),
|
||||
),
|
||||
],
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 300,
|
||||
child: ClipRRect(
|
||||
borderRadius: .circular(12),
|
||||
child: Stack(
|
||||
children: [
|
||||
LocationMap(
|
||||
center.value,
|
||||
zoom: 2,
|
||||
interactive: true,
|
||||
showMarker: false,
|
||||
controller: mapController,
|
||||
onPositionChanged: (camera, hasGesture) {
|
||||
center.value = camera.center;
|
||||
if (hasGesture) {
|
||||
own.value = null;
|
||||
placed.value = true;
|
||||
}
|
||||
},
|
||||
),
|
||||
IgnorePointer(
|
||||
child: Center(
|
||||
child: Transform.translate(
|
||||
offset: const .new(0, -20),
|
||||
child: LocationMap.markerOf(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
onPressed: locating.value ? null : locate,
|
||||
icon: locating.value
|
||||
? const SizedBox.square(
|
||||
dimension: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.my_location),
|
||||
label: const Text("Use current location"),
|
||||
),
|
||||
if (error.value case final error?)
|
||||
Text(
|
||||
error,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
TextField(
|
||||
controller: descriptionController,
|
||||
textCapitalization: .sentences,
|
||||
decoration: const .new(labelText: "Description (optional)"),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
43
lib/widgets/location/location_viewer_sheet.dart
Normal file
43
lib/widgets/location/location_viewer_sheet.dart
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
import "package:latlong2/latlong.dart";
|
||||
import "package:material_ui/material_ui.dart";
|
||||
import "package:material_adaptive_sheets/material_adaptive_sheets.dart";
|
||||
import "package:nexus/helpers/launch_helper.dart";
|
||||
import "package:nexus/models/location.dart";
|
||||
import "package:nexus/widgets/location/location_map.dart";
|
||||
|
||||
class const LocationViewerSheet(
|
||||
final LatLng point, {
|
||||
final LocationAssetType type = .self,
|
||||
final String? description,
|
||||
super.key,
|
||||
}) extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) => SheetLayout(
|
||||
title: Text(
|
||||
description ?? (type == .self ? "Shared Location" : "Location"),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => ref
|
||||
.read(LaunchHelper.provider)
|
||||
.launchUrl(GeoUri.openStreetMap(point)),
|
||||
child: const Text("Open in OpenStreetMap"),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
child: const Text("Close"),
|
||||
),
|
||||
],
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 400,
|
||||
child: ClipRRect(
|
||||
borderRadius: .circular(12),
|
||||
child: LocationMap(point, interactive: true),
|
||||
),
|
||||
),
|
||||
SelectableText(point.formatted),
|
||||
],
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue