186 lines
7.0 KiB
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;
|
|
}
|