1
0
mirror of https://github.com/flutter/samples.git synced 2026-04-24 07:51:04 +00:00

Compass app (#2446)

This commit is contained in:
Eric Windmill
2024-09-27 18:49:27 -04:00
committed by GitHub
parent fcf2552cda
commit 46b5a26b26
326 changed files with 53272 additions and 0 deletions

View File

@@ -0,0 +1,121 @@
// 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/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);
}
static const _strings = <String, String>{
'activities': 'Activities',
'addDates': 'Add Dates',
'bookingDeleted': 'Booking deleted',
'bookNewTrip': 'Book New Trip',
'close': 'Close',
'confirm': 'Confirm',
'daytime': 'Daytime',
'errorWhileDeletingBooking': 'Error while deleting booking',
'errorWhileLoadingActivities': 'Error while loading activities',
'errorWhileLoadingBooking': 'Error while loading booking',
'errorWhileLoadingContinents': 'Error while loading continents',
'errorWhileLoadingDestinations': 'Error while loading destinations',
'errorWhileLoadingHome': 'Error while loading home',
'errorWhileLogin': 'Error while trying to login',
'errorWhileLogout': 'Error while trying to logout',
'errorWhileSavingActivities': 'Error while saving activities',
'errorWhileSavingItinerary': 'Error while saving itinerary',
'errorWhileSharing': 'Error while sharing booking',
'evening': 'Evening',
'login': 'Login',
'nameTrips': '{name}\'s Trips',
'search': 'Search',
'searchDestination': 'Search destination',
'selected': '{1} selected',
'shareTrip': 'Share Trip',
'tryAgain': 'Try again',
'yourChosenActivities': 'Your chosen activities',
'when': 'When',
};
// If string for "label" does not exist, will show "[LABEL]"
static String _get(String label) =>
_strings[label] ?? '[${label.toUpperCase()}]';
String get activities => _get('activities');
String get addDates => _get('addDates');
String get confirm => _get('confirm');
String get daytime => _get('daytime');
String get errorWhileLoadingActivities => _get('errorWhileLoadingActivities');
String get errorWhileLoadingBooking => _get('errorWhileLoadingBooking');
String get errorWhileLoadingContinents => _get('errorWhileLoadingContinents');
String get errorWhileLoadingDestinations =>
_get('errorWhileLoadingDestinations');
String get errorWhileSavingActivities => _get('errorWhileSavingActivities');
String get errorWhileSavingItinerary => _get('errorWhileSavingItinerary');
String get evening => _get('evening');
String get search => _get('search');
String get searchDestination => _get('searchDestination');
String get shareTrip => _get('shareTrip');
String get tryAgain => _get('tryAgain');
String get yourChosenActivities => _get('yourChosenActivities');
String get when => _get('when');
String get errorWhileLogin => _get('errorWhileLogin');
String get login => _get('login');
String get errorWhileLogout => _get('errorWhileLogout');
String get close => _get('close');
String get errorWhileSharing => _get('errorWhileSharing');
String get bookNewTrip => _get('bookNewTrip');
String get errorWhileLoadingHome => _get('errorWhileLoadingHome');
String get bookingDeleted => _get('bookingDeleted');
String get errorWhileDeletingBooking => _get('errorWhileDeletingBooking');
String nameTrips(String name) => _get('nameTrips').replaceAll('{name}', name);
String selected(int value) =>
_get('selected').replaceAll('{1}', value.toString());
}
class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalization> {
@override
bool isSupported(Locale locale) => locale.languageCode == 'en';
@override
Future<AppLocalization> load(Locale locale) {
return SynchronousFuture(AppLocalization());
}
@override
bool shouldReload(covariant LocalizationsDelegate<AppLocalization> old) =>
false;
}

View File

@@ -0,0 +1,41 @@
// 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';
class AppColors {
static const black1 = Color(0xFF101010);
static const white1 = Color(0xFFFFF7FA);
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 blackTransparent = Color(0x4D000000);
static const red1 = Color(0xFFE74C3C);
static const lightColorScheme = ColorScheme(
brightness: Brightness.light,
primary: AppColors.black1,
onPrimary: AppColors.white1,
secondary: AppColors.black1,
onSecondary: AppColors.white1,
surface: Colors.white,
onSurface: AppColors.black1,
error: Colors.white,
onError: Colors.red,
);
static const darkColorScheme = ColorScheme(
brightness: Brightness.dark,
primary: AppColors.white1,
onPrimary: AppColors.black1,
secondary: AppColors.white1,
onSecondary: AppColors.black1,
surface: AppColors.black1,
onSurface: Colors.white,
error: Colors.black,
onError: AppColors.red1,
);
}

View File

@@ -0,0 +1,65 @@
// 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';
sealed class Dimens {
const Dimens();
/// General horizontal padding used to separate UI items
static const paddingHorizontal = 20.0;
/// General vertical padding used to separate UI items
static const paddingVertical = 24.0;
/// Horizontal padding for screen edges
abstract final double paddingScreenHorizontal;
/// Vertical padding for screen edges
abstract final double paddingScreenVertical;
/// Horizontal symmetric padding for screen edges
EdgeInsets get edgeInsetsScreenHorizontal =>
EdgeInsets.symmetric(horizontal: paddingScreenHorizontal);
/// Symmetric padding for screen edges
EdgeInsets get edgeInsetsScreenSymmetric => EdgeInsets.symmetric(
horizontal: paddingScreenHorizontal, vertical: paddingScreenVertical);
static final dimensDesktop = DimensDesktop();
static final dimensMobile = DimensMobile();
/// Get dimensions definition based on screen size
factory Dimens.of(BuildContext context) =>
switch (MediaQuery.sizeOf(context).width) {
> 600 => dimensDesktop,
_ => dimensMobile,
};
abstract final double profilePictureSize;
}
/// Mobile dimensions
class DimensMobile extends Dimens {
@override
double paddingScreenHorizontal = Dimens.paddingHorizontal;
@override
double paddingScreenVertical = Dimens.paddingVertical;
@override
double get profilePictureSize => 64.0;
}
/// Desktop/Web dimensions
class DimensDesktop extends Dimens {
@override
double paddingScreenHorizontal = 100.0;
@override
double paddingScreenVertical = 64.0;
@override
double get profilePictureSize => 128.0;
}

View File

@@ -0,0 +1,84 @@
// 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 'colors.dart';
import '../ui/tag_chip.dart';
import 'package:flutter/material.dart';
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,
),
bodySmall: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.grey3,
),
labelSmall: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w500,
color: AppColors.grey3,
),
labelLarge: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w400,
color: AppColors.grey3,
),
);
static const _inputDecorationTheme = InputDecorationTheme(
hintStyle: TextStyle(
// grey3 works for both light and dark themes
color: AppColors.grey3,
fontSize: 18.0,
fontWeight: FontWeight.w400,
),
);
static ThemeData lightTheme = ThemeData(
useMaterial3: true,
brightness: Brightness.light,
colorScheme: AppColors.lightColorScheme,
textTheme: _textTheme,
inputDecorationTheme: _inputDecorationTheme,
extensions: [
TagChipTheme(
chipColor: AppColors.whiteTransparent,
onChipColor: Colors.white,
),
],
);
static ThemeData darkTheme = ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorScheme: AppColors.darkColorScheme,
textTheme: _textTheme,
inputDecorationTheme: _inputDecorationTheme,
extensions: [
TagChipTheme(
chipColor: AppColors.blackTransparent,
onChipColor: Colors.white,
),
],
);
}

View File

@@ -0,0 +1,63 @@
// 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 '../themes/colors.dart';
import 'blur_filter.dart';
/// Custom back button to pop navigation.
class CustomBackButton extends StatelessWidget {
const CustomBackButton({
super.key,
this.onTap,
this.blur = false,
});
final bool blur;
final GestureTapCallback? onTap;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 40.0,
width: 40.0,
child: Stack(
children: [
if (blur)
ClipRect(
child: BackdropFilter(
filter: kBlurFilter,
child: const SizedBox(height: 40.0, width: 40.0),
),
),
DecoratedBox(
decoration: BoxDecoration(
border: Border.all(color: AppColors.grey1),
borderRadius: BorderRadius.circular(8.0),
),
child: InkWell(
borderRadius: BorderRadius.circular(8.0),
onTap: () {
if (onTap != null) {
onTap!();
} else {
context.pop();
}
},
child: Center(
child: Icon(
size: 24.0,
Icons.arrow_back,
color: Theme.of(context).colorScheme.onSurface,
),
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,7 @@
// 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 'dart:ui';
final kBlurFilter = ImageFilter.blur(sigmaX: 2, sigmaY: 2);

View File

@@ -0,0 +1,50 @@
// 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 '../themes/colors.dart';
class CustomCheckbox extends StatelessWidget {
const CustomCheckbox({
super.key,
required this.value,
required this.onChanged,
});
final bool value;
final ValueChanged<bool?> onChanged;
@override
Widget build(BuildContext context) {
return InkResponse(
radius: 24,
onTap: () => onChanged(!value),
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24),
border: Border.all(color: AppColors.grey3),
),
child: Material(
borderRadius: BorderRadius.circular(24),
color: value
? Theme.of(context).colorScheme.primary
: Colors.transparent,
child: SizedBox(
width: 24,
height: 24,
child: Visibility(
visible: value,
child: Icon(
Icons.check,
size: 14,
color: Theme.of(context).colorScheme.onPrimary,
),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,24 @@
// 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:intl/intl.dart';
final _dateFormatDay = DateFormat('d');
final _dateFormatDayMonth = DateFormat('d MMM');
String dateFormatStartEnd(DateTimeRange dateTimeRange) {
final start = dateTimeRange.start;
final end = dateTimeRange.end;
final dayMonthEnd = _dateFormatDayMonth.format(end);
if (start.month == end.month) {
final dayStart = _dateFormatDay.format(start);
return '$dayStart - $dayMonthEnd';
}
final dayMonthStart = _dateFormatDayMonth.format(start);
return '$dayMonthStart - $dayMonthEnd';
}

View File

@@ -0,0 +1,63 @@
// 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 '../themes/colors.dart';
class ErrorIndicator extends StatelessWidget {
const ErrorIndicator({
super.key,
required this.title,
required this.label,
required this.onPressed,
});
final String title;
final String label;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
IntrinsicWidth(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: Row(
children: [
Icon(
Icons.error_outline,
color: Theme.of(context).colorScheme.onError,
),
const SizedBox(width: 10),
Text(
title,
style: TextStyle(
color: Theme.of(context).colorScheme.onError,
),
),
],
),
),
),
),
const SizedBox(
height: 10,
),
FilledButton(
onPressed: onPressed,
style: const ButtonStyle(
backgroundColor: WidgetStatePropertyAll(AppColors.red1),
foregroundColor: WidgetStatePropertyAll(Colors.white),
),
child: Text(label),
),
],
);
}
}

View File

@@ -0,0 +1,60 @@
// 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 '../themes/colors.dart';
import 'blur_filter.dart';
/// Home button to navigate back to the '/' path.
class HomeButton extends StatelessWidget {
const HomeButton({
super.key,
this.blur = false,
});
final bool blur;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 40.0,
width: 40.0,
child: Stack(
fit: StackFit.expand,
children: [
if (blur)
ClipRect(
child: BackdropFilter(
filter: kBlurFilter,
child: const SizedBox(height: 40.0, width: 40.0),
),
),
DecoratedBox(
decoration: BoxDecoration(
border: Border.all(color: AppColors.grey1),
borderRadius: BorderRadius.circular(8.0),
color: Colors.transparent,
),
child: InkWell(
borderRadius: BorderRadius.circular(8.0),
onTap: () {
context.go(Routes.home);
},
child: Center(
child: Icon(
size: 24.0,
Icons.home_outlined,
color: Theme.of(context).colorScheme.onSurface,
),
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,17 @@
// 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/gestures.dart';
import 'package:flutter/material.dart';
/// Custom scroll behavior to allow dragging with mouse.
/// Necessary to allow dragging with mouse on Continents carousel.
class AppCustomScrollBehavior extends MaterialScrollBehavior {
@override
Set<PointerDeviceKind> get dragDevices => {
PointerDeviceKind.touch,
// Allow to drag with mouse on Regions carousel
PointerDeviceKind.mouse,
};
}

View File

@@ -0,0 +1,111 @@
// 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 '../../../domain/models/itinerary_config/itinerary_config.dart';
import '../localization/applocalization.dart';
import '../themes/colors.dart';
import '../themes/dimens.dart';
import 'date_format_start_end.dart';
import 'home_button.dart';
/// Application top search bar.
///
/// 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,
});
final ItineraryConfig? config;
final GestureTapCallback? onTap;
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: InkWell(
borderRadius: BorderRadius.circular(16.0),
onTap: onTap,
child: Container(
height: 64,
decoration: BoxDecoration(
border: Border.all(color: AppColors.grey1),
borderRadius: BorderRadius.circular(16.0),
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: Dimens.paddingHorizontal,
),
child: Align(
alignment: AlignmentDirectional.centerStart,
child: _QueryText(config: config),
),
),
),
),
),
const SizedBox(width: 10),
const HomeButton(),
],
);
}
}
class _QueryText extends StatelessWidget {
const _QueryText({
required this.config,
});
final ItineraryConfig? config;
@override
Widget build(BuildContext context) {
if (config == null) {
return const _EmptySearch();
}
final ItineraryConfig(:continent, :startDate, :endDate, :guests) = config!;
if (startDate == null ||
endDate == null ||
guests == null ||
continent == null) {
return const _EmptySearch();
}
return Text(
'$continent - ${dateFormatStartEnd(DateTimeRange(start: startDate, end: endDate))} - Guests: $guests',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyLarge,
);
}
}
class _EmptySearch extends StatelessWidget {
const _EmptySearch();
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.start,
children: [
const Icon(Icons.search),
const SizedBox(width: 12),
Expanded(
child: Text(
AppLocalization.of(context).searchDestination,
textAlign: TextAlign.start,
style: Theme.of(context).inputDecorationTheme.hintStyle,
),
),
],
);
}
}

View File

@@ -0,0 +1,144 @@
// 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 'dart:ui';
import '../themes/colors.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class TagChip extends StatelessWidget {
const TagChip({
super.key,
required this.tag,
this.fontSize = 10,
this.height = 20,
this.chipColor,
this.onChipColor,
});
final String tag;
final double fontSize;
final double height;
final Color? chipColor;
final Color? onChipColor;
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(height / 2),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 3, sigmaY: 3),
child: DecoratedBox(
decoration: BoxDecoration(
color: chipColor ??
Theme.of(context).extension<TagChipTheme>()?.chipColor ??
AppColors.whiteTransparent,
),
child: SizedBox(
height: height,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_iconFrom(tag),
color: onChipColor ??
Theme.of(context)
.extension<TagChipTheme>()
?.onChipColor ??
Colors.white,
size: fontSize,
),
const SizedBox(width: 4),
Text(
tag,
textAlign: TextAlign.center,
style: _textStyle(context),
),
],
),
),
),
),
),
);
}
IconData? _iconFrom(String tag) {
return switch (tag) {
'Adventure sports' => Icons.kayaking_outlined,
'Beach' => Icons.beach_access_outlined,
'City' => Icons.location_city_outlined,
'Cultural experiences' => Icons.museum_outlined,
'Foodie' || 'Food tours' => Icons.restaurant,
'Hiking' => Icons.hiking,
'Historic' => Icons.menu_book_outlined,
'Island' || 'Coastal' || 'Lake' || 'River' => Icons.water,
'Luxury' => Icons.attach_money_outlined,
'Mountain' || 'Wildlife watching' => Icons.landscape_outlined,
'Nightlife' => Icons.local_bar_outlined,
'Off-the-beaten-path' => Icons.do_not_step_outlined,
'Romantic' => Icons.favorite_border_outlined,
'Rural' => Icons.agriculture_outlined,
'Secluded' => Icons.church_outlined,
'Sightseeing' => Icons.attractions_outlined,
'Skiing' => Icons.downhill_skiing_outlined,
'Wine tasting' => Icons.wine_bar_outlined,
'Winter destination' => Icons.ac_unit,
_ => Icons.label_outlined,
};
}
// Note: original Figma file uses Google Sans
// which is not available on GoogleFonts
_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,
),
);
}
class TagChipTheme extends ThemeExtension<TagChipTheme> {
final Color chipColor;
final Color onChipColor;
TagChipTheme({
required this.chipColor,
required this.onChipColor,
});
@override
ThemeExtension<TagChipTheme> copyWith({
Color? chipColor,
Color? onChipColor,
}) {
return TagChipTheme(
chipColor: chipColor ?? this.chipColor,
onChipColor: onChipColor ?? this.onChipColor,
);
}
@override
ThemeExtension<TagChipTheme> lerp(
covariant ThemeExtension<TagChipTheme> other,
double t,
) {
if (other is! TagChipTheme) {
return this;
}
return TagChipTheme(
chipColor: Color.lerp(chipColor, other.chipColor, t) ?? chipColor,
onChipColor: Color.lerp(onChipColor, other.onChipColor, t) ?? onChipColor,
);
}
}