1
0
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:
Eric Windmill
2025-02-12 18:08:01 -05:00
committed by GitHub
parent d62c784789
commit 719fd72c38
685 changed files with 76244 additions and 53721 deletions

View File

@@ -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;
}

View File

@@ -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),
),
);
}

View File

@@ -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),
),
],

View File

@@ -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,
};
}

View File

@@ -60,7 +60,7 @@ class ActivityEntry extends StatelessWidget {
key: ValueKey('${activity.ref}-checkbox'),
value: selected,
onChanged: onChanged,
)
),
],
),
);

View File

@@ -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);
}

View File

@@ -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,
)),
),
),
);

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;

View File

@@ -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],
),
),
);

View File

@@ -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,
),
),
));
);
}
}
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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

View File

@@ -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,

View File

@@ -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;

View File

@@ -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,

View File

@@ -46,9 +46,7 @@ class ErrorIndicator extends StatelessWidget {
),
),
),
const SizedBox(
height: 10,
),
const SizedBox(height: 10),
FilledButton(
onPressed: onPressed,
style: const ButtonStyle(

View File

@@ -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;

View File

@@ -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,
};
}

View File

@@ -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;

View File

@@ -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({

View File

@@ -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);
}

View File

@@ -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,
),

View File

@@ -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(

View File

@@ -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;
}

View File

@@ -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),
],
),
);

View File

@@ -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),
);
}
}

View File

@@ -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;
}

View File

@@ -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),
);
},

View File

@@ -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 {
);
}
},
)
),
],
),
),

View File

@@ -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),
),
],
),

View File

@@ -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;

View File

@@ -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,
),
),
));
);
}
}
}