Files
anyway/frontend/lib/presentation/widgets/trip_details_panel.dart

186 lines
7.0 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:map_launcher/map_launcher.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:anyway/domain/entities/landmark.dart';
import 'package:anyway/domain/entities/trip.dart';
import 'package:anyway/presentation/utils/trip_location_utils.dart';
import 'package:anyway/presentation/widgets/trip_details/trip_hero_header.dart';
import 'package:anyway/presentation/widgets/trip_details/trip_hero_meta_card.dart';
import 'package:anyway/presentation/widgets/trip_details/trip_landmark_card.dart';
import 'package:anyway/presentation/widgets/trip_details/trip_step_between_landmarks.dart';
class TripDetailsPanel extends StatefulWidget {
const TripDetailsPanel({super.key, required this.trip, required this.controller, this.onLandmarkUpdated});
final Trip trip;
final ScrollController controller;
final VoidCallback? onLandmarkUpdated;
@override
State<TripDetailsPanel> createState() => _TripDetailsPanelState();
}
class _TripDetailsPanelState extends State<TripDetailsPanel> {
late Future<TripLocaleInfo> _localeFuture;
@override
void initState() {
super.initState();
_localeFuture = TripLocationUtils.resolveLocaleInfo(widget.trip);
}
@override
void didUpdateWidget(covariant TripDetailsPanel oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.trip.uuid != widget.trip.uuid) {
_localeFuture = TripLocationUtils.resolveLocaleInfo(widget.trip);
}
}
@override
Widget build(BuildContext context) {
final landmarks = widget.trip.landmarks;
if (landmarks.isEmpty) {
return const Center(child: Text('No landmarks in this trip yet.'));
}
return CustomScrollView(
controller: widget.controller,
slivers: [
SliverToBoxAdapter(
child: Column(
children: [
const SizedBox(height: 10),
Container(
width: 42,
height: 5,
decoration: BoxDecoration(color: Colors.grey.shade300, borderRadius: BorderRadius.circular(12)),
),
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: FutureBuilder<TripLocaleInfo>(
future: _localeFuture,
builder: (context, snapshot) {
final info = snapshot.data;
final isLoading = snapshot.connectionState == ConnectionState.waiting;
final subtitle = isLoading ? 'Pinpointing your destination...' : 'Here is a recommended route for your trip in ${info?.cityName ?? 'your area'}.';
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TripHeroHeader(localeInfo: info),
const SizedBox(height: 10),
TripHeroMetaCard(
subtitle: subtitle,
totalStops: widget.trip.landmarks.length,
totalMinutes: widget.trip.totalTimeMinutes,
countryName: info?.countryName,
isLoading: isLoading,
),
],
);
},
),
),
const SizedBox(height: 14),
],
),
),
SliverPadding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 32),
sliver: SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
final landmark = landmarks[index];
return Column(
children: [
TripLandmarkCard(
landmark: landmark,
position: index + 1,
onToggleVisited: () => _toggleVisited(landmark),
onOpenWebsite: landmark.websiteUrl == null ? null : () => _openWebsite(landmark.websiteUrl!),
),
if (index < landmarks.length - 1)
TripStepBetweenLandmarks(current: landmark, next: landmarks[index + 1], onRequestDirections: () => _showNavigationOptions(context, landmark, landmarks[index + 1])),
],
);
}, childCount: landmarks.length),
),
),
],
);
}
void _toggleVisited(Landmark landmark) {
setState(() => landmark.isVisited = !landmark.isVisited);
widget.onLandmarkUpdated?.call();
}
Future<void> _openWebsite(String url) async {
final uri = Uri.tryParse(url);
if (uri == null) return;
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
Future<void> _showNavigationOptions(BuildContext context, Landmark current, Landmark next) async {
if (!_hasValidLocation(current) || !_hasValidLocation(next)) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Coordinates unavailable for directions.')));
return;
}
List<AvailableMap> availableMaps = [];
try {
availableMaps = await MapLauncher.installedMaps;
} catch (error) {
debugPrint('Unable to load maps: $error');
}
if (!context.mounted) return;
if (availableMaps.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('No navigation apps detected.')));
return;
}
final origin = Coords(current.location[0], current.location[1]);
final destination = Coords(next.location[0], next.location[1]);
await showModalBottomSheet<void>(
context: context,
builder: (sheetContext) {
return SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(24, 24, 24, 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Navigate to ${next.name}', style: Theme.of(sheetContext).textTheme.titleMedium),
const SizedBox(height: 4),
Text('Choose your preferred maps app.', style: Theme.of(sheetContext).textTheme.bodySmall),
],
),
),
for (final map in availableMaps)
ListTile(
leading: ClipRRect(borderRadius: BorderRadius.circular(8), child: SvgPicture.asset(map.icon, height: 30, width: 30)),
title: Text(map.mapName),
onTap: () async {
await map.showDirections(origin: origin, originTitle: current.name, destination: destination, destinationTitle: next.name, directionsMode: DirectionsMode.walking);
if (sheetContext.mounted) Navigator.of(sheetContext).pop();
},
),
],
),
);
},
);
}
bool _hasValidLocation(Landmark landmark) => landmark.location.length >= 2;
}