mirror of
https://github.com/flutter/samples.git
synced 2025-11-08 13:58:47 +00:00
Flutter 3.29 beta (#2571)
This commit is contained in:
@@ -16,8 +16,8 @@ class ActivitiesViewModel extends ChangeNotifier {
|
||||
ActivitiesViewModel({
|
||||
required ActivityRepository activityRepository,
|
||||
required ItineraryConfigRepository itineraryConfigRepository,
|
||||
}) : _activityRepository = activityRepository,
|
||||
_itineraryConfigRepository = itineraryConfigRepository {
|
||||
}) : _activityRepository = activityRepository,
|
||||
_itineraryConfigRepository = itineraryConfigRepository {
|
||||
loadActivities = Command0(_loadActivities)..execute();
|
||||
saveActivities = Command0(_saveActivities);
|
||||
}
|
||||
@@ -48,10 +48,7 @@ class ActivitiesViewModel extends ChangeNotifier {
|
||||
final result = await _itineraryConfigRepository.getItineraryConfig();
|
||||
switch (result) {
|
||||
case Error<ItineraryConfig>():
|
||||
_log.warning(
|
||||
'Failed to load stored ItineraryConfig',
|
||||
result.error,
|
||||
);
|
||||
_log.warning('Failed to load stored ItineraryConfig', result.error);
|
||||
return result;
|
||||
case Ok<ItineraryConfig>():
|
||||
}
|
||||
@@ -64,28 +61,37 @@ class ActivitiesViewModel extends ChangeNotifier {
|
||||
|
||||
_selectedActivities.addAll(result.value.activities);
|
||||
|
||||
final resultActivities =
|
||||
await _activityRepository.getByDestination(destinationRef);
|
||||
final resultActivities = await _activityRepository.getByDestination(
|
||||
destinationRef,
|
||||
);
|
||||
switch (resultActivities) {
|
||||
case Ok():
|
||||
{
|
||||
_daytimeActivities = resultActivities.value
|
||||
.where((activity) => [
|
||||
TimeOfDay.any,
|
||||
TimeOfDay.morning,
|
||||
TimeOfDay.afternoon,
|
||||
].contains(activity.timeOfDay))
|
||||
.toList();
|
||||
_daytimeActivities =
|
||||
resultActivities.value
|
||||
.where(
|
||||
(activity) => [
|
||||
TimeOfDay.any,
|
||||
TimeOfDay.morning,
|
||||
TimeOfDay.afternoon,
|
||||
].contains(activity.timeOfDay),
|
||||
)
|
||||
.toList();
|
||||
|
||||
_eveningActivities = resultActivities.value
|
||||
.where((activity) => [
|
||||
TimeOfDay.evening,
|
||||
TimeOfDay.night,
|
||||
].contains(activity.timeOfDay))
|
||||
.toList();
|
||||
_eveningActivities =
|
||||
resultActivities.value
|
||||
.where(
|
||||
(activity) => [
|
||||
TimeOfDay.evening,
|
||||
TimeOfDay.night,
|
||||
].contains(activity.timeOfDay),
|
||||
)
|
||||
.toList();
|
||||
|
||||
_log.fine('Activities (daytime: ${_daytimeActivities.length}, '
|
||||
'evening: ${_eveningActivities.length}) loaded');
|
||||
_log.fine(
|
||||
'Activities (daytime: ${_daytimeActivities.length}, '
|
||||
'evening: ${_eveningActivities.length}) loaded',
|
||||
);
|
||||
}
|
||||
case Error():
|
||||
{
|
||||
@@ -100,8 +106,9 @@ class ActivitiesViewModel extends ChangeNotifier {
|
||||
/// Add [Activity] to selected list.
|
||||
void addActivity(String activityRef) {
|
||||
assert(
|
||||
(_daytimeActivities + _eveningActivities)
|
||||
.any((activity) => activity.ref == activityRef),
|
||||
(_daytimeActivities + _eveningActivities).any(
|
||||
(activity) => activity.ref == activityRef,
|
||||
),
|
||||
"Activity $activityRef not found",
|
||||
);
|
||||
_selectedActivities.add(activityRef);
|
||||
@@ -112,8 +119,9 @@ class ActivitiesViewModel extends ChangeNotifier {
|
||||
/// Remove [Activity] from selected list.
|
||||
void removeActivity(String activityRef) {
|
||||
assert(
|
||||
(_daytimeActivities + _eveningActivities)
|
||||
.any((activity) => activity.ref == activityRef),
|
||||
(_daytimeActivities + _eveningActivities).any(
|
||||
(activity) => activity.ref == activityRef,
|
||||
),
|
||||
"Activity $activityRef not found",
|
||||
);
|
||||
_selectedActivities.remove(activityRef);
|
||||
@@ -135,12 +143,10 @@ class ActivitiesViewModel extends ChangeNotifier {
|
||||
|
||||
final itineraryConfig = resultConfig.value;
|
||||
final result = await _itineraryConfigRepository.setItineraryConfig(
|
||||
itineraryConfig.copyWith(activities: _selectedActivities.toList()));
|
||||
itineraryConfig.copyWith(activities: _selectedActivities.toList()),
|
||||
);
|
||||
if (result is Error) {
|
||||
_log.warning(
|
||||
'Failed to store ItineraryConfig',
|
||||
result.error,
|
||||
);
|
||||
_log.warning('Failed to store ItineraryConfig', result.error);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -33,28 +33,24 @@ class ActivitiesList extends StatelessWidget {
|
||||
bottom: Dimens.paddingVertical,
|
||||
),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
final activity = list[index];
|
||||
return Padding(
|
||||
padding:
|
||||
EdgeInsets.only(bottom: index < list.length - 1 ? 20 : 0),
|
||||
child: ActivityEntry(
|
||||
key: ValueKey(activity.ref),
|
||||
activity: activity,
|
||||
selected: viewModel.selectedActivities.contains(activity.ref),
|
||||
onChanged: (value) {
|
||||
if (value!) {
|
||||
viewModel.addActivity(activity.ref);
|
||||
} else {
|
||||
viewModel.removeActivity(activity.ref);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
childCount: list.length,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
final activity = list[index];
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: index < list.length - 1 ? 20 : 0),
|
||||
child: ActivityEntry(
|
||||
key: ValueKey(activity.ref),
|
||||
activity: activity,
|
||||
selected: viewModel.selectedActivities.contains(activity.ref),
|
||||
onChanged: (value) {
|
||||
if (value!) {
|
||||
viewModel.addActivity(activity.ref);
|
||||
} else {
|
||||
viewModel.removeActivity(activity.ref);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}, childCount: list.length),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,10 +18,7 @@ import 'activity_time_of_day.dart';
|
||||
const String confirmButtonKey = 'confirm-button';
|
||||
|
||||
class ActivitiesScreen extends StatefulWidget {
|
||||
const ActivitiesScreen({
|
||||
super.key,
|
||||
required this.viewModel,
|
||||
});
|
||||
const ActivitiesScreen({super.key, required this.viewModel});
|
||||
|
||||
final ActivitiesViewModel viewModel;
|
||||
|
||||
@@ -68,13 +65,16 @@ class _ActivitiesScreenState extends State<ActivitiesScreen> {
|
||||
const ActivitiesHeader(),
|
||||
if (widget.viewModel.loadActivities.running)
|
||||
const Expanded(
|
||||
child: Center(child: CircularProgressIndicator())),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
if (widget.viewModel.loadActivities.error)
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: ErrorIndicator(
|
||||
title: AppLocalization.of(context)
|
||||
.errorWhileLoadingActivities,
|
||||
title:
|
||||
AppLocalization.of(
|
||||
context,
|
||||
).errorWhileLoadingActivities,
|
||||
label: AppLocalization.of(context).tryAgain,
|
||||
onPressed: widget.viewModel.loadActivities.execute,
|
||||
),
|
||||
@@ -91,9 +91,7 @@ class _ActivitiesScreenState extends State<ActivitiesScreen> {
|
||||
Expanded(
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
const SliverToBoxAdapter(
|
||||
child: ActivitiesHeader(),
|
||||
),
|
||||
const SliverToBoxAdapter(child: ActivitiesHeader()),
|
||||
ActivitiesTitle(
|
||||
viewModel: widget.viewModel,
|
||||
activityTimeOfDay: ActivityTimeOfDay.daytime,
|
||||
@@ -145,9 +143,7 @@ class _ActivitiesScreenState extends State<ActivitiesScreen> {
|
||||
}
|
||||
|
||||
class _BottomArea extends StatelessWidget {
|
||||
const _BottomArea({
|
||||
required this.viewModel,
|
||||
});
|
||||
const _BottomArea({required this.viewModel});
|
||||
|
||||
final ActivitiesViewModel viewModel;
|
||||
|
||||
@@ -168,15 +164,17 @@ class _BottomArea extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
AppLocalization.of(context)
|
||||
.selected(viewModel.selectedActivities.length),
|
||||
AppLocalization.of(
|
||||
context,
|
||||
).selected(viewModel.selectedActivities.length),
|
||||
style: Theme.of(context).textTheme.labelLarge,
|
||||
),
|
||||
FilledButton(
|
||||
key: const Key(confirmButtonKey),
|
||||
onPressed: viewModel.selectedActivities.isNotEmpty
|
||||
? viewModel.saveActivities.execute
|
||||
: null,
|
||||
onPressed:
|
||||
viewModel.selectedActivities.isNotEmpty
|
||||
? viewModel.saveActivities.execute
|
||||
: null,
|
||||
child: Text(AppLocalization.of(context).confirm),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -37,7 +37,7 @@ class ActivitiesTitle extends StatelessWidget {
|
||||
}
|
||||
|
||||
String _label(BuildContext context) => switch (activityTimeOfDay) {
|
||||
ActivityTimeOfDay.daytime => AppLocalization.of(context).daytime,
|
||||
ActivityTimeOfDay.evening => AppLocalization.of(context).evening,
|
||||
};
|
||||
ActivityTimeOfDay.daytime => AppLocalization.of(context).daytime,
|
||||
ActivityTimeOfDay.evening => AppLocalization.of(context).evening,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ class ActivityEntry extends StatelessWidget {
|
||||
key: ValueKey('${activity.ref}-checkbox'),
|
||||
value: selected,
|
||||
onChanged: onChanged,
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -9,9 +9,8 @@ import '../../../../utils/command.dart';
|
||||
import '../../../../utils/result.dart';
|
||||
|
||||
class LoginViewModel {
|
||||
LoginViewModel({
|
||||
required AuthRepository authRepository,
|
||||
}) : _authRepository = authRepository {
|
||||
LoginViewModel({required AuthRepository authRepository})
|
||||
: _authRepository = authRepository {
|
||||
login = Command1<void, (String email, String password)>(_login);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,10 +12,7 @@ import '../view_models/login_viewmodel.dart';
|
||||
import 'tilted_cards.dart';
|
||||
|
||||
class LoginScreen extends StatefulWidget {
|
||||
const LoginScreen({
|
||||
super.key,
|
||||
required this.viewModel,
|
||||
});
|
||||
const LoginScreen({super.key, required this.viewModel});
|
||||
|
||||
final LoginViewModel viewModel;
|
||||
|
||||
@@ -24,10 +21,12 @@ class LoginScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _LoginScreenState extends State<LoginScreen> {
|
||||
final TextEditingController _email =
|
||||
TextEditingController(text: 'email@example.com');
|
||||
final TextEditingController _password =
|
||||
TextEditingController(text: 'password');
|
||||
final TextEditingController _email = TextEditingController(
|
||||
text: 'email@example.com',
|
||||
);
|
||||
final TextEditingController _password = TextEditingController(
|
||||
text: 'password',
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -61,22 +60,19 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
TextField(
|
||||
controller: _email,
|
||||
),
|
||||
TextField(controller: _email),
|
||||
const SizedBox(height: Dimens.paddingVertical),
|
||||
TextField(
|
||||
controller: _password,
|
||||
obscureText: true,
|
||||
),
|
||||
TextField(controller: _password, obscureText: true),
|
||||
const SizedBox(height: Dimens.paddingVertical),
|
||||
ListenableBuilder(
|
||||
listenable: widget.viewModel.login,
|
||||
builder: (context, _) {
|
||||
return FilledButton(
|
||||
onPressed: () {
|
||||
widget.viewModel.login
|
||||
.execute((_email.value.text, _password.value.text));
|
||||
widget.viewModel.login.execute((
|
||||
_email.value.text,
|
||||
_password.value.text,
|
||||
));
|
||||
},
|
||||
child: Text(AppLocalization.of(context).login),
|
||||
);
|
||||
@@ -103,8 +99,11 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
content: Text(AppLocalization.of(context).errorWhileLogin),
|
||||
action: SnackBarAction(
|
||||
label: AppLocalization.of(context).tryAgain,
|
||||
onPressed: () => widget.viewModel.login
|
||||
.execute((_email.value.text, _password.value.text)),
|
||||
onPressed:
|
||||
() => widget.viewModel.login.execute((
|
||||
_email.value.text,
|
||||
_password.value.text,
|
||||
)),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -12,8 +12,8 @@ class LogoutViewModel {
|
||||
LogoutViewModel({
|
||||
required AuthRepository authRepository,
|
||||
required ItineraryConfigRepository itineraryConfigRepository,
|
||||
}) : _authLogoutRepository = authRepository,
|
||||
_itineraryConfigRepository = itineraryConfigRepository {
|
||||
}) : _authLogoutRepository = authRepository,
|
||||
_itineraryConfigRepository = itineraryConfigRepository {
|
||||
logout = Command0(_logout);
|
||||
}
|
||||
final AuthRepository _authLogoutRepository;
|
||||
@@ -25,8 +25,9 @@ class LogoutViewModel {
|
||||
switch (result) {
|
||||
case Ok<void>():
|
||||
// clear stored itinerary config
|
||||
return _itineraryConfigRepository
|
||||
.setItineraryConfig(const ItineraryConfig());
|
||||
return _itineraryConfigRepository.setItineraryConfig(
|
||||
const ItineraryConfig(),
|
||||
);
|
||||
case Error<void>():
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -9,10 +9,7 @@ import '../../../core/themes/colors.dart';
|
||||
import '../view_models/logout_viewmodel.dart';
|
||||
|
||||
class LogoutButton extends StatefulWidget {
|
||||
const LogoutButton({
|
||||
super.key,
|
||||
required this.viewModel,
|
||||
});
|
||||
const LogoutButton({super.key, required this.viewModel});
|
||||
|
||||
final LogoutViewModel viewModel;
|
||||
|
||||
|
||||
@@ -20,10 +20,10 @@ class BookingViewModel extends ChangeNotifier {
|
||||
required BookingShareUseCase shareBookingUseCase,
|
||||
required ItineraryConfigRepository itineraryConfigRepository,
|
||||
required BookingRepository bookingRepository,
|
||||
}) : _createUseCase = createBookingUseCase,
|
||||
_shareUseCase = shareBookingUseCase,
|
||||
_itineraryConfigRepository = itineraryConfigRepository,
|
||||
_bookingRepository = bookingRepository {
|
||||
}) : _createUseCase = createBookingUseCase,
|
||||
_shareUseCase = shareBookingUseCase,
|
||||
_itineraryConfigRepository = itineraryConfigRepository,
|
||||
_bookingRepository = bookingRepository {
|
||||
createBooking = Command0(_createBooking);
|
||||
shareBooking = Command0(() => _shareUseCase.shareBooking(_booking!));
|
||||
loadBooking = Command1(_load);
|
||||
|
||||
@@ -12,10 +12,7 @@ import '../view_models/booking_viewmodel.dart';
|
||||
import 'booking_header.dart';
|
||||
|
||||
class BookingBody extends StatelessWidget {
|
||||
const BookingBody({
|
||||
super.key,
|
||||
required this.viewModel,
|
||||
});
|
||||
const BookingBody({super.key, required this.viewModel});
|
||||
|
||||
final BookingViewModel viewModel;
|
||||
|
||||
@@ -30,13 +27,10 @@ class BookingBody extends StatelessWidget {
|
||||
slivers: [
|
||||
SliverToBoxAdapter(child: BookingHeader(booking: booking)),
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
final activity = booking.activity[index];
|
||||
return _Activity(activity: activity);
|
||||
},
|
||||
childCount: booking.activity.length,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
final activity = booking.activity[index];
|
||||
return _Activity(activity: activity);
|
||||
}, childCount: booking.activity.length),
|
||||
),
|
||||
const SliverToBoxAdapter(child: SizedBox(height: 200)),
|
||||
],
|
||||
@@ -47,9 +41,7 @@ class BookingBody extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _Activity extends StatelessWidget {
|
||||
const _Activity({
|
||||
required this.activity,
|
||||
});
|
||||
const _Activity({required this.activity});
|
||||
|
||||
final Activity activity;
|
||||
|
||||
|
||||
@@ -15,10 +15,7 @@ import '../../core/ui/home_button.dart';
|
||||
import '../../core/ui/tag_chip.dart';
|
||||
|
||||
class BookingHeader extends StatelessWidget {
|
||||
const BookingHeader({
|
||||
super.key,
|
||||
required this.booking,
|
||||
});
|
||||
const BookingHeader({super.key, required this.booking});
|
||||
|
||||
final Booking booking;
|
||||
|
||||
@@ -51,9 +48,7 @@ class BookingHeader extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _Top extends StatelessWidget {
|
||||
const _Top({
|
||||
required this.booking,
|
||||
});
|
||||
const _Top({required this.booking});
|
||||
|
||||
final Booking booking;
|
||||
|
||||
@@ -70,10 +65,7 @@ class _Top extends StatelessWidget {
|
||||
Positioned(
|
||||
right: Dimens.of(context).paddingScreenHorizontal,
|
||||
top: Dimens.of(context).paddingScreenVertical,
|
||||
child: const SafeArea(
|
||||
top: true,
|
||||
child: HomeButton(blur: true),
|
||||
),
|
||||
child: const SafeArea(top: true, child: HomeButton(blur: true)),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -82,9 +74,7 @@ class _Top extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _Tags extends StatelessWidget {
|
||||
const _Tags({
|
||||
required this.booking,
|
||||
});
|
||||
const _Tags({required this.booking});
|
||||
|
||||
final Booking booking;
|
||||
|
||||
@@ -100,26 +90,25 @@ class _Tags extends StatelessWidget {
|
||||
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(),
|
||||
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,
|
||||
});
|
||||
const _Headline({required this.booking});
|
||||
|
||||
final Booking booking;
|
||||
|
||||
@@ -139,10 +128,7 @@ class _Headline extends StatelessWidget {
|
||||
),
|
||||
Text(
|
||||
dateFormatStartEnd(
|
||||
DateTimeRange(
|
||||
start: booking.startDate,
|
||||
end: booking.endDate,
|
||||
),
|
||||
DateTimeRange(start: booking.startDate, end: booking.endDate),
|
||||
),
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
@@ -154,9 +140,7 @@ class _Headline extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _HeaderImage extends StatelessWidget {
|
||||
const _HeaderImage({
|
||||
required this.booking,
|
||||
});
|
||||
const _HeaderImage({required this.booking});
|
||||
|
||||
final Booking booking;
|
||||
|
||||
@@ -180,10 +164,7 @@ class _Gradient extends StatelessWidget {
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
Theme.of(context).colorScheme.surface,
|
||||
],
|
||||
colors: [Colors.transparent, Theme.of(context).colorScheme.surface],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -12,10 +12,7 @@ import '../view_models/booking_viewmodel.dart';
|
||||
import 'booking_body.dart';
|
||||
|
||||
class BookingScreen extends StatefulWidget {
|
||||
const BookingScreen({
|
||||
super.key,
|
||||
required this.viewModel,
|
||||
});
|
||||
const BookingScreen({super.key, required this.viewModel});
|
||||
|
||||
final BookingViewModel viewModel;
|
||||
|
||||
@@ -47,16 +44,18 @@ class _BookingScreenState extends State<BookingScreen> {
|
||||
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),
|
||||
),
|
||||
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
|
||||
@@ -68,9 +67,7 @@ class _BookingScreenState extends State<BookingScreen> {
|
||||
// If either command is running, show progress indicator
|
||||
if (widget.viewModel.createBooking.running ||
|
||||
widget.viewModel.loadBooking.running) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
// If fails to create booking, tap to try again
|
||||
if (widget.viewModel.createBooking.error) {
|
||||
@@ -103,13 +100,15 @@ class _BookingScreenState extends State<BookingScreen> {
|
||||
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,
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalization.of(context).errorWhileSharing),
|
||||
action: SnackBarAction(
|
||||
label: AppLocalization.of(context).tryAgain,
|
||||
onPressed: widget.viewModel.shareBooking.execute,
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Simple Localizations similar to
|
||||
/// https://docs.flutter.dev/ui/accessibility-and-internationalization/internationalization#an-alternative-class-for-the-apps-localized-resources
|
||||
class AppLocalization {
|
||||
static AppLocalization of(BuildContext context) {
|
||||
return Localizations.of(context, AppLocalization);
|
||||
|
||||
@@ -10,8 +10,9 @@ abstract final class AppColors {
|
||||
static const grey1 = Color(0xFFF2F2F2);
|
||||
static const grey2 = Color(0xFF4D4D4D);
|
||||
static const grey3 = Color(0xFFA4A4A4);
|
||||
static const whiteTransparent =
|
||||
Color(0x4DFFFFFF); // Figma rgba(255, 255, 255, 0.3)
|
||||
static const whiteTransparent = Color(
|
||||
0x4DFFFFFF,
|
||||
); // Figma rgba(255, 255, 255, 0.3)
|
||||
static const blackTransparent = Color(0x4D000000);
|
||||
static const red1 = Color(0xFFE74C3C);
|
||||
|
||||
|
||||
@@ -27,17 +27,20 @@ abstract final class Dimens {
|
||||
|
||||
/// Symmetric padding for screen edges
|
||||
EdgeInsets get edgeInsetsScreenSymmetric => EdgeInsets.symmetric(
|
||||
horizontal: paddingScreenHorizontal, vertical: paddingScreenVertical);
|
||||
horizontal: paddingScreenHorizontal,
|
||||
vertical: paddingScreenVertical,
|
||||
);
|
||||
|
||||
static const Dimens desktop = _DimensDesktop();
|
||||
static const Dimens mobile = _DimensMobile();
|
||||
|
||||
/// Get dimensions definition based on screen size
|
||||
factory Dimens.of(BuildContext context) =>
|
||||
switch (MediaQuery.sizeOf(context).width) {
|
||||
> 600 => desktop,
|
||||
_ => mobile,
|
||||
};
|
||||
factory Dimens.of(BuildContext context) => switch (MediaQuery.sizeOf(
|
||||
context,
|
||||
).width) {
|
||||
> 600 && < 840 => desktop,
|
||||
_ => mobile,
|
||||
};
|
||||
}
|
||||
|
||||
/// Mobile dimensions
|
||||
|
||||
@@ -9,26 +9,11 @@ import 'colors.dart';
|
||||
|
||||
abstract final class AppTheme {
|
||||
static const _textTheme = TextTheme(
|
||||
headlineLarge: TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
headlineSmall: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
titleMedium: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
bodyLarge: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
bodyMedium: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
headlineLarge: TextStyle(fontSize: 32, fontWeight: FontWeight.w500),
|
||||
headlineSmall: TextStyle(fontSize: 18, fontWeight: FontWeight.w400),
|
||||
titleMedium: TextStyle(fontSize: 18, fontWeight: FontWeight.w500),
|
||||
bodyLarge: TextStyle(fontSize: 18, fontWeight: FontWeight.w400),
|
||||
bodyMedium: TextStyle(fontSize: 16, fontWeight: FontWeight.w400),
|
||||
bodySmall: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
|
||||
@@ -10,11 +10,7 @@ import 'blur_filter.dart';
|
||||
|
||||
/// Custom back button to pop navigation.
|
||||
class CustomBackButton extends StatelessWidget {
|
||||
const CustomBackButton({
|
||||
super.key,
|
||||
this.onTap,
|
||||
this.blur = false,
|
||||
});
|
||||
const CustomBackButton({super.key, this.onTap, this.blur = false});
|
||||
|
||||
final bool blur;
|
||||
final GestureTapCallback? onTap;
|
||||
|
||||
@@ -28,9 +28,10 @@ class CustomCheckbox extends StatelessWidget {
|
||||
),
|
||||
child: Material(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
color: value
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Colors.transparent,
|
||||
color:
|
||||
value
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Colors.transparent,
|
||||
child: SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
|
||||
@@ -46,9 +46,7 @@ class ErrorIndicator extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
FilledButton(
|
||||
onPressed: onPressed,
|
||||
style: const ButtonStyle(
|
||||
|
||||
@@ -11,10 +11,7 @@ import 'blur_filter.dart';
|
||||
|
||||
/// Home button to navigate back to the '/' path.
|
||||
class HomeButton extends StatelessWidget {
|
||||
const HomeButton({
|
||||
super.key,
|
||||
this.blur = false,
|
||||
});
|
||||
const HomeButton({super.key, this.blur = false});
|
||||
|
||||
final bool blur;
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ import 'package:flutter/material.dart';
|
||||
class AppCustomScrollBehavior extends MaterialScrollBehavior {
|
||||
@override
|
||||
Set<PointerDeviceKind> get dragDevices => {
|
||||
PointerDeviceKind.touch,
|
||||
// Allow to drag with mouse on Regions carousel
|
||||
PointerDeviceKind.mouse,
|
||||
};
|
||||
PointerDeviceKind.touch,
|
||||
// Allow to drag with mouse on Regions carousel
|
||||
PointerDeviceKind.mouse,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,11 +16,7 @@ import 'home_button.dart';
|
||||
/// Displays a search bar with the current configuration.
|
||||
/// Includes [HomeButton] to navigate back to the '/' path.
|
||||
class AppSearchBar extends StatelessWidget {
|
||||
const AppSearchBar({
|
||||
super.key,
|
||||
this.config,
|
||||
this.onTap,
|
||||
});
|
||||
const AppSearchBar({super.key, this.config, this.onTap});
|
||||
|
||||
final ItineraryConfig? config;
|
||||
final GestureTapCallback? onTap;
|
||||
@@ -59,9 +55,7 @@ class AppSearchBar extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _QueryText extends StatelessWidget {
|
||||
const _QueryText({
|
||||
required this.config,
|
||||
});
|
||||
const _QueryText({required this.config});
|
||||
|
||||
final ItineraryConfig? config;
|
||||
|
||||
|
||||
@@ -34,7 +34,8 @@ class TagChip extends StatelessWidget {
|
||||
filter: ImageFilter.blur(sigmaX: 3, sigmaY: 3),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: chipColor ??
|
||||
color:
|
||||
chipColor ??
|
||||
Theme.of(context).extension<TagChipTheme>()?.chipColor ??
|
||||
AppColors.whiteTransparent,
|
||||
),
|
||||
@@ -48,10 +49,11 @@ class TagChip extends StatelessWidget {
|
||||
children: [
|
||||
Icon(
|
||||
_iconFrom(tag),
|
||||
color: onChipColor ??
|
||||
Theme.of(context)
|
||||
.extension<TagChipTheme>()
|
||||
?.onChipColor ??
|
||||
color:
|
||||
onChipColor ??
|
||||
Theme.of(
|
||||
context,
|
||||
).extension<TagChipTheme>()?.onChipColor ??
|
||||
Colors.white,
|
||||
size: fontSize,
|
||||
),
|
||||
@@ -98,25 +100,23 @@ class TagChip extends StatelessWidget {
|
||||
// Note: original Figma file uses Google Sans
|
||||
// which is not available on GoogleFonts
|
||||
TextStyle _textStyle(BuildContext context) => GoogleFonts.openSans(
|
||||
textStyle: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: fontSize,
|
||||
color: onChipColor ??
|
||||
Theme.of(context).extension<TagChipTheme>()?.onChipColor ??
|
||||
Colors.white,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
),
|
||||
);
|
||||
textStyle: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: fontSize,
|
||||
color:
|
||||
onChipColor ??
|
||||
Theme.of(context).extension<TagChipTheme>()?.onChipColor ??
|
||||
Colors.white,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class TagChipTheme extends ThemeExtension<TagChipTheme> {
|
||||
final Color chipColor;
|
||||
final Color onChipColor;
|
||||
|
||||
TagChipTheme({
|
||||
required this.chipColor,
|
||||
required this.onChipColor,
|
||||
});
|
||||
TagChipTheme({required this.chipColor, required this.onChipColor});
|
||||
|
||||
@override
|
||||
ThemeExtension<TagChipTheme> copyWith({
|
||||
|
||||
@@ -18,8 +18,8 @@ class HomeViewModel extends ChangeNotifier {
|
||||
HomeViewModel({
|
||||
required BookingRepository bookingRepository,
|
||||
required UserRepository userRepository,
|
||||
}) : _bookingRepository = bookingRepository,
|
||||
_userRepository = userRepository {
|
||||
}) : _bookingRepository = bookingRepository,
|
||||
_userRepository = userRepository {
|
||||
load = Command0(_load)..execute();
|
||||
deleteBooking = Command1(_deleteBooking);
|
||||
}
|
||||
|
||||
@@ -18,10 +18,7 @@ import 'home_title.dart';
|
||||
const String bookingButtonKey = 'booking-button';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
const HomeScreen({
|
||||
super.key,
|
||||
required this.viewModel,
|
||||
});
|
||||
const HomeScreen({super.key, required this.viewModel});
|
||||
|
||||
final HomeViewModel viewModel;
|
||||
|
||||
@@ -67,9 +64,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
listenable: widget.viewModel.load,
|
||||
builder: (context, child) {
|
||||
if (widget.viewModel.load.running) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (widget.viewModel.load.error) {
|
||||
@@ -98,27 +93,32 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
),
|
||||
SliverList.builder(
|
||||
itemCount: widget.viewModel.bookings.length,
|
||||
itemBuilder: (_, index) => _Booking(
|
||||
key: ValueKey(widget.viewModel.bookings[index].id),
|
||||
booking: widget.viewModel.bookings[index],
|
||||
onTap: () => context.push(Routes.bookingWithId(
|
||||
widget.viewModel.bookings[index].id)),
|
||||
confirmDismiss: (_) async {
|
||||
// wait for command to complete
|
||||
await widget.viewModel.deleteBooking.execute(
|
||||
widget.viewModel.bookings[index].id,
|
||||
);
|
||||
// if command completed successfully, return true
|
||||
if (widget.viewModel.deleteBooking.completed) {
|
||||
// removes the dismissable from the list
|
||||
return true;
|
||||
} else {
|
||||
// the dismissable stays in the list
|
||||
return false;
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
itemBuilder:
|
||||
(_, index) => _Booking(
|
||||
key: ValueKey(widget.viewModel.bookings[index].id),
|
||||
booking: widget.viewModel.bookings[index],
|
||||
onTap:
|
||||
() => context.push(
|
||||
Routes.bookingWithId(
|
||||
widget.viewModel.bookings[index].id,
|
||||
),
|
||||
),
|
||||
confirmDismiss: (_) async {
|
||||
// wait for command to complete
|
||||
await widget.viewModel.deleteBooking.execute(
|
||||
widget.viewModel.bookings[index].id,
|
||||
);
|
||||
// if command completed successfully, return true
|
||||
if (widget.viewModel.deleteBooking.completed) {
|
||||
// removes the dismissable from the list
|
||||
return true;
|
||||
} else {
|
||||
// the dismissable stays in the list
|
||||
return false;
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
@@ -132,9 +132,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
if (widget.viewModel.deleteBooking.completed) {
|
||||
widget.viewModel.deleteBooking.clearResult();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalization.of(context).bookingDeleted),
|
||||
),
|
||||
SnackBar(content: Text(AppLocalization.of(context).bookingDeleted)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -189,16 +187,10 @@ class _Booking extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
booking.name,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
Text(booking.name, style: Theme.of(context).textTheme.titleLarge),
|
||||
Text(
|
||||
dateFormatStartEnd(
|
||||
DateTimeRange(
|
||||
start: booking.startDate,
|
||||
end: booking.endDate,
|
||||
),
|
||||
DateTimeRange(start: booking.startDate, end: booking.endDate),
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
|
||||
@@ -13,10 +13,7 @@ import '../../core/themes/dimens.dart';
|
||||
import '../view_models/home_viewmodel.dart';
|
||||
|
||||
class HomeHeader extends StatelessWidget {
|
||||
const HomeHeader({
|
||||
super.key,
|
||||
required this.viewModel,
|
||||
});
|
||||
const HomeHeader({super.key, required this.viewModel});
|
||||
|
||||
final HomeViewModel viewModel;
|
||||
|
||||
@@ -49,18 +46,14 @@ class HomeHeader extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: Dimens.paddingVertical),
|
||||
_Title(
|
||||
text: AppLocalization.of(context).nameTrips(user.name),
|
||||
),
|
||||
_Title(text: AppLocalization.of(context).nameTrips(user.name)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Title extends StatelessWidget {
|
||||
const _Title({
|
||||
required this.text,
|
||||
});
|
||||
const _Title({required this.text});
|
||||
|
||||
final String text;
|
||||
|
||||
@@ -68,16 +61,12 @@ class _Title extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return ShaderMask(
|
||||
blendMode: BlendMode.srcIn,
|
||||
shaderCallback: (bounds) => RadialGradient(
|
||||
center: Alignment.bottomLeft,
|
||||
radius: 2,
|
||||
colors: [
|
||||
Colors.purple.shade700,
|
||||
Colors.purple.shade400,
|
||||
],
|
||||
).createShader(
|
||||
Rect.fromLTWH(0, 0, bounds.width, bounds.height),
|
||||
),
|
||||
shaderCallback:
|
||||
(bounds) => RadialGradient(
|
||||
center: Alignment.bottomLeft,
|
||||
radius: 2,
|
||||
colors: [Colors.purple.shade700, Colors.purple.shade400],
|
||||
).createShader(Rect.fromLTWH(0, 0, bounds.width, bounds.height)),
|
||||
child: Text(
|
||||
text,
|
||||
style: GoogleFonts.rubik(
|
||||
|
||||
@@ -18,8 +18,8 @@ class ResultsViewModel extends ChangeNotifier {
|
||||
ResultsViewModel({
|
||||
required DestinationRepository destinationRepository,
|
||||
required ItineraryConfigRepository itineraryConfigRepository,
|
||||
}) : _destinationRepository = destinationRepository,
|
||||
_itineraryConfigRepository = itineraryConfigRepository {
|
||||
}) : _destinationRepository = destinationRepository,
|
||||
_itineraryConfigRepository = itineraryConfigRepository {
|
||||
updateItineraryConfig = Command1<void, String>(_updateItineraryConfig);
|
||||
search = Command0(_search)..execute();
|
||||
}
|
||||
@@ -67,10 +67,13 @@ class ResultsViewModel extends ChangeNotifier {
|
||||
case Ok():
|
||||
{
|
||||
// If the result is Ok, update the list of destinations
|
||||
_destinations = result.value
|
||||
.where((destination) =>
|
||||
destination.continent == _itineraryConfig!.continent)
|
||||
.toList();
|
||||
_destinations =
|
||||
result.value
|
||||
.where(
|
||||
(destination) =>
|
||||
destination.continent == _itineraryConfig!.continent,
|
||||
)
|
||||
.toList();
|
||||
_log.fine('Destinations (${_destinations.length}) loaded');
|
||||
}
|
||||
case Error():
|
||||
@@ -99,16 +102,11 @@ class ResultsViewModel extends ChangeNotifier {
|
||||
}
|
||||
|
||||
final itineraryConfig = resultConfig.value;
|
||||
final result = await _itineraryConfigRepository
|
||||
.setItineraryConfig(itineraryConfig.copyWith(
|
||||
destination: destinationRef,
|
||||
activities: [],
|
||||
));
|
||||
final result = await _itineraryConfigRepository.setItineraryConfig(
|
||||
itineraryConfig.copyWith(destination: destinationRef, activities: []),
|
||||
);
|
||||
if (result is Error) {
|
||||
_log.warning(
|
||||
'Failed to store ItineraryConfig',
|
||||
result.error,
|
||||
);
|
||||
_log.warning('Failed to store ItineraryConfig', result.error);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -10,11 +10,7 @@ import '../../../utils/image_error_listener.dart';
|
||||
import '../../core/ui/tag_chip.dart';
|
||||
|
||||
class ResultCard extends StatelessWidget {
|
||||
const ResultCard({
|
||||
super.key,
|
||||
required this.destination,
|
||||
required this.onTap,
|
||||
});
|
||||
const ResultCard({super.key, required this.destination, required this.onTap});
|
||||
|
||||
final Destination destination;
|
||||
final GestureTapCallback onTap;
|
||||
@@ -40,13 +36,8 @@ class ResultCard extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
destination.name.toUpperCase(),
|
||||
style: _cardTitleStyle,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 6,
|
||||
),
|
||||
Text(destination.name.toUpperCase(), style: _cardTitleStyle),
|
||||
const SizedBox(height: 6),
|
||||
Wrap(
|
||||
spacing: 4.0,
|
||||
runSpacing: 4.0,
|
||||
@@ -61,9 +52,7 @@ class ResultCard extends StatelessWidget {
|
||||
Positioned.fill(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
),
|
||||
child: InkWell(onTap: onTap),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -80,10 +69,7 @@ final _cardTitleStyle = GoogleFonts.rubik(
|
||||
letterSpacing: 1,
|
||||
shadows: [
|
||||
// Helps to read the text a bit better
|
||||
Shadow(
|
||||
blurRadius: 3.0,
|
||||
color: Colors.black,
|
||||
)
|
||||
Shadow(blurRadius: 3.0, color: Colors.black),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -14,10 +14,7 @@ import '../view_models/results_viewmodel.dart';
|
||||
import 'result_card.dart';
|
||||
|
||||
class ResultsScreen extends StatefulWidget {
|
||||
const ResultsScreen({
|
||||
super.key,
|
||||
required this.viewModel,
|
||||
});
|
||||
const ResultsScreen({super.key, required this.viewModel});
|
||||
|
||||
final ResultsViewModel viewModel;
|
||||
|
||||
@@ -64,13 +61,16 @@ class _ResultsScreenState extends State<ResultsScreen> {
|
||||
_AppSearchBar(widget: widget),
|
||||
if (widget.viewModel.search.running)
|
||||
const Expanded(
|
||||
child: Center(child: CircularProgressIndicator())),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
if (widget.viewModel.search.error)
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: ErrorIndicator(
|
||||
title: AppLocalization.of(context)
|
||||
.errorWhileLoadingDestinations,
|
||||
title:
|
||||
AppLocalization.of(
|
||||
context,
|
||||
).errorWhileLoadingDestinations,
|
||||
label: AppLocalization.of(context).tryAgain,
|
||||
onPressed: widget.viewModel.search.execute,
|
||||
),
|
||||
@@ -86,9 +86,7 @@ class _ResultsScreenState extends State<ResultsScreen> {
|
||||
padding: Dimens.of(context).edgeInsetsScreenHorizontal,
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: _AppSearchBar(widget: widget),
|
||||
),
|
||||
SliverToBoxAdapter(child: _AppSearchBar(widget: widget)),
|
||||
_Grid(viewModel: widget.viewModel),
|
||||
],
|
||||
),
|
||||
@@ -118,9 +116,7 @@ class _ResultsScreenState extends State<ResultsScreen> {
|
||||
}
|
||||
|
||||
class _AppSearchBar extends StatelessWidget {
|
||||
const _AppSearchBar({
|
||||
required this.widget,
|
||||
});
|
||||
const _AppSearchBar({required this.widget});
|
||||
|
||||
final ResultsScreen widget;
|
||||
|
||||
@@ -147,9 +143,7 @@ class _AppSearchBar extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _Grid extends StatelessWidget {
|
||||
const _Grid({
|
||||
required this.viewModel,
|
||||
});
|
||||
const _Grid({required this.viewModel});
|
||||
|
||||
final ResultsViewModel viewModel;
|
||||
|
||||
@@ -162,19 +156,16 @@ class _Grid extends StatelessWidget {
|
||||
mainAxisSpacing: 8.0,
|
||||
childAspectRatio: 182 / 222,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
final destination = viewModel.destinations[index];
|
||||
return ResultCard(
|
||||
key: ValueKey(destination.ref),
|
||||
destination: destination,
|
||||
onTap: () {
|
||||
viewModel.updateItineraryConfig.execute(destination.ref);
|
||||
},
|
||||
);
|
||||
},
|
||||
childCount: viewModel.destinations.length,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
final destination = viewModel.destinations[index];
|
||||
return ResultCard(
|
||||
key: ValueKey(destination.ref),
|
||||
destination: destination,
|
||||
onTap: () {
|
||||
viewModel.updateItineraryConfig.execute(destination.ref);
|
||||
},
|
||||
);
|
||||
}, childCount: viewModel.destinations.length),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ class SearchFormViewModel extends ChangeNotifier {
|
||||
SearchFormViewModel({
|
||||
required ContinentRepository continentRepository,
|
||||
required ItineraryConfigRepository itineraryConfigRepository,
|
||||
}) : _continentRepository = continentRepository,
|
||||
_itineraryConfigRepository = itineraryConfigRepository {
|
||||
}) : _continentRepository = continentRepository,
|
||||
_itineraryConfigRepository = itineraryConfigRepository {
|
||||
updateItineraryConfig = Command0(_updateItineraryConfig);
|
||||
load = Command0(_load)..execute();
|
||||
}
|
||||
@@ -125,10 +125,7 @@ class SearchFormViewModel extends ChangeNotifier {
|
||||
_log.fine('ItineraryConfig loaded');
|
||||
notifyListeners();
|
||||
case Error<ItineraryConfig>():
|
||||
_log.warning(
|
||||
'Failed to load stored ItineraryConfig',
|
||||
result.error,
|
||||
);
|
||||
_log.warning('Failed to load stored ItineraryConfig', result.error);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -20,10 +20,7 @@ import '../view_models/search_form_viewmodel.dart';
|
||||
/// Users can tap one item to select it.
|
||||
/// Tapping again the same item will deselect it.
|
||||
class SearchFormContinent extends StatelessWidget {
|
||||
const SearchFormContinent({
|
||||
super.key,
|
||||
required this.viewModel,
|
||||
});
|
||||
const SearchFormContinent({super.key, required this.viewModel});
|
||||
|
||||
final SearchFormViewModel viewModel;
|
||||
|
||||
@@ -35,9 +32,7 @@ class SearchFormContinent extends StatelessWidget {
|
||||
listenable: viewModel.load,
|
||||
builder: (context, child) {
|
||||
if (viewModel.load.running) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (viewModel.load.error) {
|
||||
return Center(
|
||||
@@ -110,9 +105,7 @@ class _CarouselItem extends StatelessWidget {
|
||||
// NOTE: Getting "invalid image data" error for some of the images
|
||||
// e.g. https://rstr.in/google/tripedia/jlbgFDrSUVE
|
||||
return const DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.grey3,
|
||||
),
|
||||
decoration: BoxDecoration(color: AppColors.grey3),
|
||||
child: SizedBox(width: 140, height: 140),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -14,10 +14,7 @@ import '../view_models/search_form_viewmodel.dart';
|
||||
///
|
||||
/// Opens a date range picker dialog when tapped.
|
||||
class SearchFormDate extends StatelessWidget {
|
||||
const SearchFormDate({
|
||||
super.key,
|
||||
required this.viewModel,
|
||||
});
|
||||
const SearchFormDate({super.key, required this.viewModel});
|
||||
|
||||
final SearchFormViewModel viewModel;
|
||||
|
||||
@@ -71,7 +68,7 @@ class SearchFormDate extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -16,10 +16,7 @@ const String addGuestsKey = 'add-guests';
|
||||
/// Users can tap the Plus and Minus icons to increase or decrease
|
||||
/// the number of guests.
|
||||
class SearchFormGuests extends StatelessWidget {
|
||||
const SearchFormGuests({
|
||||
super.key,
|
||||
required this.viewModel,
|
||||
});
|
||||
const SearchFormGuests({super.key, required this.viewModel});
|
||||
|
||||
final SearchFormViewModel viewModel;
|
||||
|
||||
@@ -44,10 +41,7 @@ class SearchFormGuests extends StatelessWidget {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Who',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
Text('Who', style: Theme.of(context).textTheme.titleMedium),
|
||||
_QuantitySelector(viewModel),
|
||||
],
|
||||
),
|
||||
@@ -81,22 +75,21 @@ class _QuantitySelector extends StatelessWidget {
|
||||
),
|
||||
ListenableBuilder(
|
||||
listenable: viewModel,
|
||||
builder: (context, _) => Text(
|
||||
viewModel.guests.toString(),
|
||||
style: viewModel.guests == 0
|
||||
? Theme.of(context).inputDecorationTheme.hintStyle
|
||||
: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
builder:
|
||||
(context, _) => Text(
|
||||
viewModel.guests.toString(),
|
||||
style:
|
||||
viewModel.guests == 0
|
||||
? Theme.of(context).inputDecorationTheme.hintStyle
|
||||
: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
key: const ValueKey(addGuestsKey),
|
||||
onTap: () {
|
||||
viewModel.guests++;
|
||||
},
|
||||
child: const Icon(
|
||||
Icons.add_circle_outline,
|
||||
color: AppColors.grey3,
|
||||
),
|
||||
child: const Icon(Icons.add_circle_outline, color: AppColors.grey3),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -21,10 +21,7 @@ import 'search_form_submit.dart';
|
||||
/// Tapping on the submit button opens the [ResultsScreen] screen
|
||||
/// passing the search options as query parameters.
|
||||
class SearchFormScreen extends StatelessWidget {
|
||||
const SearchFormScreen({
|
||||
super.key,
|
||||
required this.viewModel,
|
||||
});
|
||||
const SearchFormScreen({super.key, required this.viewModel});
|
||||
|
||||
final SearchFormViewModel viewModel;
|
||||
|
||||
|
||||
@@ -19,10 +19,7 @@ const String searchFormSubmitButtonKey = 'submit-button';
|
||||
/// When tapped, it navigates to the [ResultsScreen]
|
||||
/// passing the search options as query parameters.
|
||||
class SearchFormSubmit extends StatefulWidget {
|
||||
const SearchFormSubmit({
|
||||
super.key,
|
||||
required this.viewModel,
|
||||
});
|
||||
const SearchFormSubmit({super.key, required this.viewModel});
|
||||
|
||||
final SearchFormViewModel viewModel;
|
||||
|
||||
@@ -63,16 +60,15 @@ class _SearchFormSubmitState extends State<SearchFormSubmit> {
|
||||
listenable: widget.viewModel,
|
||||
child: SizedBox(
|
||||
height: 52,
|
||||
child: Center(
|
||||
child: Text(AppLocalization.of(context).search),
|
||||
),
|
||||
child: Center(child: Text(AppLocalization.of(context).search)),
|
||||
),
|
||||
builder: (context, child) {
|
||||
return FilledButton(
|
||||
key: const ValueKey(searchFormSubmitButtonKey),
|
||||
onPressed: widget.viewModel.valid
|
||||
? widget.viewModel.updateItineraryConfig.execute
|
||||
: null,
|
||||
onPressed:
|
||||
widget.viewModel.valid
|
||||
? widget.viewModel.updateItineraryConfig.execute
|
||||
: null,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
@@ -88,13 +84,15 @@ class _SearchFormSubmitState extends State<SearchFormSubmit> {
|
||||
|
||||
if (widget.viewModel.updateItineraryConfig.error) {
|
||||
widget.viewModel.updateItineraryConfig.clearResult();
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(AppLocalization.of(context).errorWhileSavingItinerary),
|
||||
action: SnackBarAction(
|
||||
label: AppLocalization.of(context).tryAgain,
|
||||
onPressed: widget.viewModel.updateItineraryConfig.execute,
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalization.of(context).errorWhileSavingItinerary),
|
||||
action: SnackBarAction(
|
||||
label: AppLocalization.of(context).tryAgain,
|
||||
onPressed: widget.viewModel.updateItineraryConfig.execute,
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user