maps/mobile/lib/features/places/presentation/screens/place_detail_screen.dart
2026-03-30 09:22:16 +02:00

313 lines
8.9 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../data/places_repository.dart';
import '../../providers/places_provider.dart';
import '../widgets/poi_chip.dart';
class PlaceDetailScreen extends ConsumerStatefulWidget {
final String osmType;
final int osmId;
const PlaceDetailScreen({
super.key,
required this.osmType,
required this.osmId,
});
@override
ConsumerState<PlaceDetailScreen> createState() => _PlaceDetailScreenState();
}
class _PlaceDetailScreenState extends ConsumerState<PlaceDetailScreen> {
PlaceData? _place;
bool _isLoading = true;
String? _error;
bool _isFavorited = false;
int? _favoriteId;
@override
void initState() {
super.initState();
_loadPlace();
}
Future<void> _loadPlace() async {
try {
final repo = ref.read(placesRepositoryProvider);
final place = await repo.getPoiDetail(widget.osmType, widget.osmId);
final fav = await repo.isFavorited(widget.osmType, widget.osmId);
if (mounted) {
setState(() {
_place = place;
_isLoading = false;
_isFavorited = fav != null;
_favoriteId = fav?.id;
});
}
} catch (e) {
if (mounted) {
setState(() {
_isLoading = false;
_error = 'Could not load place details.';
});
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_place?.name ?? 'Place Details'),
),
body: _buildBody(context),
);
}
Widget _buildBody(BuildContext context) {
if (_isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (_error != null) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error_outline, size: 48),
const SizedBox(height: 8),
Text(_error!),
const SizedBox(height: 16),
FilledButton(
onPressed: () {
setState(() {
_isLoading = true;
_error = null;
});
_loadPlace();
},
child: const Text('Retry'),
),
],
),
);
}
final place = _place!;
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Name and category
Text(place.name, style: Theme.of(context).textTheme.headlineSmall),
const SizedBox(height: 8),
PoiChip(category: place.category),
const SizedBox(height: 16),
// Address
if (place.displayAddress.isNotEmpty) ...[
_InfoRow(
icon: Icons.location_on,
label: 'Address',
value: place.displayAddress,
),
const SizedBox(height: 12),
],
// Opening hours
if (place.openingHoursParsed != null) ...[
_InfoRow(
icon: Icons.access_time,
label: 'Hours',
value: _formatOpeningHours(place),
),
const SizedBox(height: 12),
] else if (place.openingHours != null) ...[
_InfoRow(
icon: Icons.access_time,
label: 'Hours',
value: place.openingHours!,
),
const SizedBox(height: 12),
],
// Phone
if (place.phone != null) ...[
_InfoRow(
icon: Icons.phone,
label: 'Phone',
value: place.phone!,
),
const SizedBox(height: 12),
],
// Website
if (place.website != null) ...[
_InfoRow(
icon: Icons.language,
label: 'Website',
value: place.website!,
),
const SizedBox(height: 12),
],
// Wheelchair accessibility
if (place.wheelchair != null) ...[
_InfoRow(
icon: Icons.accessible,
label: 'Wheelchair',
value: _wheelchairLabel(place.wheelchair!),
),
const SizedBox(height: 12),
],
const SizedBox(height: 24),
// Action buttons
Row(
children: [
Expanded(
child: FilledButton.icon(
onPressed: () {
context.push('/route', extra: {
'destLat': place.latitude,
'destLon': place.longitude,
'destName': place.name,
});
},
icon: const Icon(Icons.directions),
label: const Text('Directions'),
),
),
const SizedBox(width: 8),
Expanded(
child: _isFavorited
? FilledButton.tonalIcon(
onPressed: () async {
if (_favoriteId != null) {
await ref
.read(placesProvider.notifier)
.removeFromFavorites(_favoriteId!);
setState(() {
_isFavorited = false;
_favoriteId = null;
});
}
},
icon: const Icon(Icons.bookmark),
label: const Text('Saved'),
)
: OutlinedButton.icon(
onPressed: () async {
await ref
.read(placesProvider.notifier)
.addToFavorites(place);
final fav = await ref
.read(placesRepositoryProvider)
.isFavorited(place.osmType, place.osmId);
setState(() {
_isFavorited = true;
_favoriteId = fav?.id;
});
},
icon: const Icon(Icons.bookmark_add_outlined),
label: const Text('Save'),
),
),
],
),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: () {
final coords =
'${place.latitude.toStringAsFixed(6)},${place.longitude.toStringAsFixed(6)}';
Clipboard.setData(ClipboardData(text: coords));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Coordinates copied: $coords')),
);
},
icon: const Icon(Icons.share),
label: const Text('Share coordinates'),
),
),
],
),
);
}
String _formatOpeningHours(PlaceData place) {
final parsed = place.openingHoursParsed!;
final parts = <String>[];
final isOpen = parsed['is_open'] as bool?;
if (isOpen == true) {
parts.add('Open now');
} else if (isOpen == false) {
parts.add('Closed');
}
final today = parsed['today'] as String?;
if (today != null) {
parts.add('Today: $today');
}
final nextChange = parsed['next_change'] as String?;
if (nextChange != null) {
parts.add(nextChange);
}
return parts.join(' \u2022 ');
}
String _wheelchairLabel(String value) {
switch (value) {
case 'yes':
return 'Wheelchair accessible';
case 'limited':
return 'Limited wheelchair access';
case 'no':
return 'Not wheelchair accessible';
default:
return value;
}
}
}
class _InfoRow extends StatelessWidget {
final IconData icon;
final String label;
final String value;
const _InfoRow({
required this.icon,
required this.label,
required this.value,
});
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 20, color: Theme.of(context).colorScheme.primary),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: Theme.of(context).colorScheme.outline,
),
),
Text(value, style: Theme.of(context).textTheme.bodyMedium),
],
),
),
],
);
}
}