mirror of
https://github.com/flutter/samples.git
synced 2025-11-08 13:58:47 +00:00
Compass app (#2446)
This commit is contained in:
104
compass_app/app/lib/ui/booking/widgets/booking_body.dart
Normal file
104
compass_app/app/lib/ui/booking/widgets/booking_body.dart
Normal file
@@ -0,0 +1,104 @@
|
||||
// Copyright 2024 The Flutter team. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../domain/models/activity/activity.dart';
|
||||
import '../../../utils/image_error_listener.dart';
|
||||
import '../../core/themes/dimens.dart';
|
||||
import '../view_models/booking_viewmodel.dart';
|
||||
import 'booking_header.dart';
|
||||
|
||||
class BookingBody extends StatelessWidget {
|
||||
const BookingBody({
|
||||
super.key,
|
||||
required this.viewModel,
|
||||
});
|
||||
|
||||
final BookingViewModel viewModel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListenableBuilder(
|
||||
listenable: viewModel,
|
||||
builder: (context, _) {
|
||||
final booking = viewModel.booking;
|
||||
if (booking == null) return const SizedBox();
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(child: BookingHeader(booking: booking)),
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
final activity = booking.activity[index];
|
||||
return _Activity(activity: activity);
|
||||
},
|
||||
childCount: booking.activity.length,
|
||||
),
|
||||
),
|
||||
const SliverToBoxAdapter(child: SizedBox(height: 200)),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Activity extends StatelessWidget {
|
||||
const _Activity({
|
||||
required this.activity,
|
||||
});
|
||||
|
||||
final Activity activity;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: Dimens.paddingVertical,
|
||||
left: Dimens.of(context).paddingScreenHorizontal,
|
||||
right: Dimens.of(context).paddingScreenHorizontal,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: activity.imageUrl,
|
||||
height: 80,
|
||||
width: 80,
|
||||
errorListener: imageErrorListener,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
activity.timeOfDay.name.toUpperCase(),
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
),
|
||||
Text(
|
||||
activity.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
Text(
|
||||
activity.description,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
191
compass_app/app/lib/ui/booking/widgets/booking_header.dart
Normal file
191
compass_app/app/lib/ui/booking/widgets/booking_header.dart
Normal file
@@ -0,0 +1,191 @@
|
||||
// Copyright 2024 The Flutter team. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../domain/models/booking/booking.dart';
|
||||
import '../../../utils/image_error_listener.dart';
|
||||
import '../../core/localization/applocalization.dart';
|
||||
import '../../core/themes/colors.dart';
|
||||
import '../../core/themes/dimens.dart';
|
||||
import '../../core/ui/date_format_start_end.dart';
|
||||
import '../../core/ui/home_button.dart';
|
||||
import '../../core/ui/tag_chip.dart';
|
||||
|
||||
class BookingHeader extends StatelessWidget {
|
||||
const BookingHeader({
|
||||
super.key,
|
||||
required this.booking,
|
||||
});
|
||||
|
||||
final Booking booking;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_Top(booking: booking),
|
||||
Padding(
|
||||
padding: Dimens.of(context).edgeInsetsScreenHorizontal,
|
||||
child: Text(
|
||||
booking.destination.knownFor,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: Dimens.paddingVertical),
|
||||
_Tags(booking: booking),
|
||||
const SizedBox(height: Dimens.paddingVertical),
|
||||
Padding(
|
||||
padding: Dimens.of(context).edgeInsetsScreenHorizontal,
|
||||
child: Text(
|
||||
AppLocalization.of(context).yourChosenActivities,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Top extends StatelessWidget {
|
||||
const _Top({
|
||||
required this.booking,
|
||||
});
|
||||
|
||||
final Booking booking;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 260,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
_HeaderImage(booking: booking),
|
||||
const _Gradient(),
|
||||
_Headline(booking: booking),
|
||||
Positioned(
|
||||
right: Dimens.of(context).paddingScreenHorizontal,
|
||||
top: Dimens.of(context).paddingScreenVertical,
|
||||
child: const SafeArea(
|
||||
top: true,
|
||||
child: HomeButton(blur: true),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Tags extends StatelessWidget {
|
||||
const _Tags({
|
||||
required this.booking,
|
||||
});
|
||||
|
||||
final Booking booking;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final brightness = Theme.of(context).brightness;
|
||||
final chipColor = switch (brightness) {
|
||||
Brightness.dark => AppColors.whiteTransparent,
|
||||
Brightness.light => AppColors.blackTransparent,
|
||||
};
|
||||
return Padding(
|
||||
padding: Dimens.of(context).edgeInsetsScreenHorizontal,
|
||||
child: Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
children: booking.destination.tags
|
||||
.map(
|
||||
(tag) => TagChip(
|
||||
tag: tag,
|
||||
fontSize: 16,
|
||||
height: 32,
|
||||
chipColor: chipColor,
|
||||
onChipColor: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Headline extends StatelessWidget {
|
||||
const _Headline({
|
||||
required this.booking,
|
||||
});
|
||||
|
||||
final Booking booking;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Align(
|
||||
alignment: AlignmentDirectional.bottomStart,
|
||||
child: Padding(
|
||||
padding: Dimens.of(context).edgeInsetsScreenSymmetric,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
booking.destination.name,
|
||||
style: Theme.of(context).textTheme.headlineLarge,
|
||||
),
|
||||
Text(
|
||||
dateFormatStartEnd(
|
||||
DateTimeRange(
|
||||
start: booking.startDate,
|
||||
end: booking.endDate,
|
||||
),
|
||||
),
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HeaderImage extends StatelessWidget {
|
||||
const _HeaderImage({
|
||||
required this.booking,
|
||||
});
|
||||
|
||||
final Booking booking;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CachedNetworkImage(
|
||||
fit: BoxFit.fitWidth,
|
||||
imageUrl: booking.destination.imageUrl,
|
||||
errorListener: imageErrorListener,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Gradient extends StatelessWidget {
|
||||
const _Gradient();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
Theme.of(context).colorScheme.surface,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
115
compass_app/app/lib/ui/booking/widgets/booking_screen.dart
Normal file
115
compass_app/app/lib/ui/booking/widgets/booking_screen.dart
Normal file
@@ -0,0 +1,115 @@
|
||||
// Copyright 2024 The Flutter team. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../../routing/routes.dart';
|
||||
import '../../core/localization/applocalization.dart';
|
||||
import '../../core/ui/error_indicator.dart';
|
||||
import '../view_models/booking_viewmodel.dart';
|
||||
import 'booking_body.dart';
|
||||
|
||||
class BookingScreen extends StatefulWidget {
|
||||
const BookingScreen({
|
||||
super.key,
|
||||
required this.viewModel,
|
||||
});
|
||||
|
||||
final BookingViewModel viewModel;
|
||||
|
||||
@override
|
||||
State<BookingScreen> createState() => _BookingScreenState();
|
||||
}
|
||||
|
||||
class _BookingScreenState extends State<BookingScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.viewModel.shareBooking.addListener(_listener);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.viewModel.shareBooking.removeListener(_listener);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, r) {
|
||||
// Back navigation always goes to home
|
||||
if (!didPop) context.go(Routes.home);
|
||||
},
|
||||
child: Scaffold(
|
||||
floatingActionButton: ListenableBuilder(
|
||||
listenable: widget.viewModel,
|
||||
builder: (context, _) => FloatingActionButton.extended(
|
||||
// Workaround for https://github.com/flutter/flutter/issues/115358#issuecomment-2117157419
|
||||
heroTag: null,
|
||||
key: const ValueKey('share-button'),
|
||||
onPressed: widget.viewModel.booking != null
|
||||
? widget.viewModel.shareBooking.execute
|
||||
: null,
|
||||
label: Text(AppLocalization.of(context).shareTrip),
|
||||
icon: const Icon(Icons.share_outlined),
|
||||
),
|
||||
),
|
||||
body: ListenableBuilder(
|
||||
// Listen to changes in both commands
|
||||
listenable: Listenable.merge([
|
||||
widget.viewModel.createBooking,
|
||||
widget.viewModel.loadBooking,
|
||||
]),
|
||||
builder: (context, child) {
|
||||
// If either command is running, show progress indicator
|
||||
if (widget.viewModel.createBooking.running ||
|
||||
widget.viewModel.loadBooking.running) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
// If fails to create booking, tap to try again
|
||||
if (widget.viewModel.createBooking.error) {
|
||||
return Center(
|
||||
child: ErrorIndicator(
|
||||
title: AppLocalization.of(context).errorWhileLoadingBooking,
|
||||
label: AppLocalization.of(context).tryAgain,
|
||||
onPressed: widget.viewModel.createBooking.execute,
|
||||
),
|
||||
);
|
||||
}
|
||||
// If existing booking fails to load, tap to go /home
|
||||
if (widget.viewModel.loadBooking.error) {
|
||||
return Center(
|
||||
child: ErrorIndicator(
|
||||
title: AppLocalization.of(context).errorWhileLoadingBooking,
|
||||
label: AppLocalization.of(context).close,
|
||||
onPressed: () => context.go(Routes.home),
|
||||
),
|
||||
);
|
||||
}
|
||||
return child!;
|
||||
},
|
||||
child: BookingBody(viewModel: widget.viewModel),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _listener() {
|
||||
if (widget.viewModel.shareBooking.error) {
|
||||
widget.viewModel.shareBooking.clearResult();
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(AppLocalization.of(context).errorWhileSharing),
|
||||
action: SnackBarAction(
|
||||
label: AppLocalization.of(context).tryAgain,
|
||||
onPressed: widget.viewModel.shareBooking.execute,
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user