313 lines
8.9 KiB
Dart
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),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|