1
0
mirror of https://github.com/flutter/samples.git synced 2025-11-08 13:58:47 +00:00

Compass app (#2446)

This commit is contained in:
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,34 @@
// 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:logging/logging.dart';
import '../../../../data/repositories/auth/auth_repository.dart';
import '../../../../utils/command.dart';
import '../../../../utils/result.dart';
class LoginViewModel {
LoginViewModel({
required AuthRepository authRepository,
}) : _authRepository = authRepository {
login = Command1<void, (String email, String password)>(_login);
}
final AuthRepository _authRepository;
final _log = Logger('LoginViewModel');
late Command1 login;
Future<Result<void>> _login((String, String) credentials) async {
final (email, password) = credentials;
final result = await _authRepository.login(
email: email,
password: password,
);
if (result is Error<void>) {
_log.warning('Login failed! ${result.error}');
}
return result;
}
}

View File

@@ -0,0 +1,113 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../../../../routing/routes.dart';
import '../../../core/localization/applocalization.dart';
import '../../../core/themes/dimens.dart';
import '../view_models/login_viewmodel.dart';
import 'tilted_cards.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({
super.key,
required this.viewModel,
});
final LoginViewModel viewModel;
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final TextEditingController _email =
TextEditingController(text: 'email@example.com');
final TextEditingController _password =
TextEditingController(text: 'password');
@override
void initState() {
super.initState();
widget.viewModel.login.addListener(_onResult);
}
@override
void didUpdateWidget(covariant LoginScreen oldWidget) {
super.didUpdateWidget(oldWidget);
oldWidget.viewModel.login.removeListener(_onResult);
widget.viewModel.login.addListener(_onResult);
}
@override
void dispose() {
widget.viewModel.login.removeListener(_onResult);
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
const TiltedCards(),
Padding(
padding: Dimens.of(context).edgeInsetsScreenSymmetric,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
controller: _email,
),
const SizedBox(height: Dimens.paddingVertical),
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));
},
child: Text(AppLocalization.of(context).login),
);
},
),
],
),
),
],
),
);
}
void _onResult() {
if (widget.viewModel.login.completed) {
widget.viewModel.login.clearResult();
context.go(Routes.home);
}
if (widget.viewModel.login.error) {
widget.viewModel.login.clearResult();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalization.of(context).errorWhileLogin),
action: SnackBarAction(
label: AppLocalization.of(context).tryAgain,
onPressed: () => widget.viewModel.login
.execute((_email.value.text, _password.value.text)),
),
),
);
}
}
}

View File

@@ -0,0 +1,97 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import '../../../../utils/image_error_listener.dart';
class TiltedCards extends StatelessWidget {
const TiltedCards({super.key});
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 300),
child: const AspectRatio(
aspectRatio: 1,
child: Stack(
alignment: Alignment.center,
children: [
Positioned(
left: 0,
child: _Card(
imageUrl: 'https://rstr.in/google/tripedia/g2i0BsYPKW-',
width: 200,
height: 273,
tilt: -3.83 / 360,
),
),
Positioned(
right: 0,
child: _Card(
imageUrl: 'https://rstr.in/google/tripedia/980sqNgaDRK',
width: 180,
height: 230,
tilt: 3.46 / 360,
),
),
_Card(
imageUrl: 'https://rstr.in/google/tripedia/pHfPmf3o5NU',
width: 225,
height: 322,
tilt: 0,
showTitle: true,
),
],
),
),
);
}
}
class _Card extends StatelessWidget {
const _Card({
required this.imageUrl,
required this.width,
required this.height,
required this.tilt,
this.showTitle = false,
});
final double tilt;
final double width;
final double height;
final String imageUrl;
final bool showTitle;
@override
Widget build(BuildContext context) {
return RotationTransition(
turns: AlwaysStoppedAnimation(tilt),
child: SizedBox(
width: width,
height: height,
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Stack(
fit: StackFit.expand,
alignment: Alignment.center,
children: [
CachedNetworkImage(
imageUrl: imageUrl,
fit: BoxFit.cover,
color: showTitle ? Colors.black.withOpacity(0.5) : null,
colorBlendMode: showTitle ? BlendMode.darken : null,
errorListener: imageErrorListener,
),
if (showTitle) Center(child: SvgPicture.asset('assets/logo.svg')),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,34 @@
// 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 '../../../../data/repositories/auth/auth_repository.dart';
import '../../../../data/repositories/itinerary_config/itinerary_config_repository.dart';
import '../../../../domain/models/itinerary_config/itinerary_config.dart';
import '../../../../utils/command.dart';
import '../../../../utils/result.dart';
class LogoutViewModel {
LogoutViewModel({
required AuthRepository authRepository,
required ItineraryConfigRepository itineraryConfigRepository,
}) : _authLogoutRepository = authRepository,
_itineraryConfigRepository = itineraryConfigRepository {
logout = Command0(_logout);
}
final AuthRepository _authLogoutRepository;
final ItineraryConfigRepository _itineraryConfigRepository;
late Command0 logout;
Future<Result> _logout() async {
var result = await _authLogoutRepository.logout();
switch (result) {
case Ok<void>():
// clear stored itinerary config
return _itineraryConfigRepository
.setItineraryConfig(const ItineraryConfig());
case Error<void>():
return result;
}
}
}

View File

@@ -0,0 +1,88 @@
// 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 '../../../core/localization/applocalization.dart';
import '../../../core/themes/colors.dart';
import '../view_models/logout_viewmodel.dart';
class LogoutButton extends StatefulWidget {
const LogoutButton({
super.key,
required this.viewModel,
});
final LogoutViewModel viewModel;
@override
State<LogoutButton> createState() => _LogoutButtonState();
}
class _LogoutButtonState extends State<LogoutButton> {
@override
void initState() {
super.initState();
widget.viewModel.logout.addListener(_onResult);
}
@override
void didUpdateWidget(covariant LogoutButton oldWidget) {
super.didUpdateWidget(oldWidget);
oldWidget.viewModel.logout.removeListener(_onResult);
widget.viewModel.logout.addListener(_onResult);
}
@override
void dispose() {
widget.viewModel.logout.removeListener(_onResult);
super.dispose();
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: 40.0,
width: 40.0,
child: DecoratedBox(
decoration: BoxDecoration(
border: Border.all(color: AppColors.grey1),
borderRadius: BorderRadius.circular(8.0),
color: Colors.transparent,
),
child: InkResponse(
borderRadius: BorderRadius.circular(8.0),
onTap: () {
widget.viewModel.logout.execute();
},
child: Center(
child: Icon(
size: 24.0,
Icons.logout,
color: Theme.of(context).colorScheme.onSurface,
),
),
),
),
);
}
void _onResult() {
// We do not need to navigate to `/login` on logout,
// it is done automatically by GoRouter.
if (widget.viewModel.logout.error) {
widget.viewModel.logout.clearResult();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalization.of(context).errorWhileLogout),
action: SnackBarAction(
label: AppLocalization.of(context).tryAgain,
onPressed: widget.viewModel.logout.execute,
),
),
);
}
}
}