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:
@@ -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;
|
||||
}
|
||||
}
|
||||
113
compass_app/app/lib/ui/auth/login/widgets/login_screen.dart
Normal file
113
compass_app/app/lib/ui/auth/login/widgets/login_screen.dart
Normal 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)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
97
compass_app/app/lib/ui/auth/login/widgets/tilted_cards.dart
Normal file
97
compass_app/app/lib/ui/auth/login/widgets/tilted_cards.dart
Normal 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')),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user