feat(image_picker): Add image picker both from gallery and camera

This commit is contained in:
BisratHailu 2026-01-23 23:38:50 +03:00
parent 8f329f774d
commit 4ef204f31b
58 changed files with 2281 additions and 72 deletions

View File

@ -1,4 +1,11 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<application
android:label="yimaru_app"
android:name="${applicationName}"

View File

@ -31,6 +31,7 @@ import 'package:yimaru_app/ui/views/welcome/welcome_view.dart';
import 'package:yimaru_app/ui/views/assessment/assessment_view.dart';
import 'package:yimaru_app/ui/views/learn_lesson/learn_lesson_view.dart';
import 'package:yimaru_app/ui/views/failure/failure_view.dart';
import 'package:yimaru_app/services/permission_handler_service.dart';
import 'package:yimaru_app/services/image_picker_service.dart';
// @stacked-import
@ -71,6 +72,7 @@ import 'package:yimaru_app/services/image_picker_service.dart';
LazySingleton(classType: SecureStorageService),
LazySingleton(classType: DioService),
LazySingleton(classType: StatusCheckerService),
LazySingleton(classType: PermissionHandlerService),
LazySingleton(classType: ImagePickerService),
// @stacked-service
],

View File

@ -15,6 +15,7 @@ import '../services/api_service.dart';
import '../services/authentication_service.dart';
import '../services/dio_service.dart';
import '../services/image_picker_service.dart';
import '../services/permission_handler_service.dart';
import '../services/secure_storage_service.dart';
import '../services/status_checker_service.dart';
@ -37,5 +38,6 @@ Future<void> setupLocator({
locator.registerLazySingleton(() => SecureStorageService());
locator.registerLazySingleton(() => DioService());
locator.registerLazySingleton(() => StatusCheckerService());
locator.registerLazySingleton(() => PermissionHandlerService());
locator.registerLazySingleton(() => ImagePickerService());
}

View File

@ -1,10 +1,15 @@
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/app/app.locator.dart';
import 'package:yimaru_app/models/user_model.dart';
import 'package:yimaru_app/services/secure_storage_service.dart';
class AuthenticationService {
class AuthenticationService with ListenableServiceMixin {
final _secureService = locator<SecureStorageService>();
AuthenticationService() {
listenToReactiveValues([_user]);
}
UserModel? _user;
UserModel? get user => _user;
@ -35,11 +40,12 @@ class AuthenticationService {
Future<void> saveUserName(Map<String, dynamic> data) async {
await _secureService.setString('firstName', data['firstName']);
_user = UserModel(
firstName: await _secureService.getString('firstName'),
userId: _user?.userId,
accessToken: _user?.accessToken,
refreshToken: _user?.refreshToken,
profileCompleted: _user?.profileCompleted);
userId: _user?.userId,
accessToken: _user?.accessToken,
refreshToken: _user?.refreshToken,
profileCompleted: _user?.profileCompleted,
firstName: await _secureService.getString('firstName'),
);
}
Future<void> saveBasicUserData(Map<String, dynamic> data) async {
@ -69,7 +75,19 @@ class AuthenticationService {
profileCompleted: await _secureService.getBool('profileCompleted'));
}
Future<void> saveProfileImage() async {}
Future<void> saveProfileImage(String image) async {
await _secureService.setString('profileImage', image);
_user = UserModel(
userId: _user?.userId,
firstName: _user?.firstName,
accessToken: _user?.accessToken,
refreshToken: _user?.refreshToken,
profileCompleted: _user?.profileCompleted,
profileImage: await _secureService.getString('profileImage'),
);
notifyListeners();
}
Future<void> saveFullName(Map<String, dynamic> data) async {
await _secureService.setBool('profileCompleted', true);

View File

@ -139,14 +139,6 @@ class DioService {
final response = await _refreshDio.post(
'$baseUrl/$kRefreshTokenUrl',
data: data,
options: Options(
followRedirects: false,
validateStatus: (status) => true,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
),
);
await _authenticationService.saveTokens(
@ -156,8 +148,9 @@ class DioService {
return true;
} catch (e) {
await _authenticationService.logOut();
await _navigationService.replaceWithLoginView();
print('Token refresh exception ${e.toString()}');
// await _authenticationService.logOut();
// await _navigationService.replaceWithLoginView();
return false;
}
}

View File

@ -1 +1,56 @@
class ImagePickerService {}
import 'package:image_picker/image_picker.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:yimaru_app/services/permission_handler_service.dart';
import '../app/app.locator.dart';
import '../ui/common/ui_helpers.dart';
class ImagePickerService {
final _permissionHandler = locator<PermissionHandlerService>();
final ImagePicker _picker = ImagePicker();
Future<String?> gallery() async {
try {
PermissionStatus status =
await _permissionHandler.requestPermission(Permission.mediaLibrary);
if (status == PermissionStatus.granted) {
final XFile? pickedFile = await _picker.pickImage(
source: ImageSource.gallery, maxWidth: 600, maxHeight: 600);
if (pickedFile == null) {
showErrorToast('Please select a picture');
return null;
} else {
return pickedFile.path;
}
}
return null;
} catch (e) {
return null;
}
}
Future<String?> camera() async {
try {
PermissionStatus status =
await _permissionHandler.requestPermission(Permission.camera);
if (status == PermissionStatus.granted) {
final XFile? pickedFile = await _picker.pickImage(
source: ImageSource.camera, maxWidth: 600, maxHeight: 600);
if (pickedFile == null) {
showErrorToast('Please take a picture');
return null;
} else {
return pickedFile.path;
}
}
return null;
} catch (e) {
return null;
}
}
}

View File

@ -0,0 +1,31 @@
import 'package:permission_handler/permission_handler.dart';
import '../ui/common/ui_helpers.dart';
class PermissionHandlerService {
Future<PermissionStatus> requestPermission(
Permission requestedPermission) async {
if (requestedPermission == Permission.camera) {
return await request(Permission.camera);
}
if (requestedPermission == Permission.storage) {
return await request(Permission.storage);
}
if (requestedPermission == Permission.mediaLibrary) {
return await request(Permission.mediaLibrary);
}
return PermissionStatus.denied;
}
Future<PermissionStatus> request(Permission permission) async {
if (await permission.isDenied) {
final PermissionStatus status = await permission.request();
if (status.isDenied || status.isPermanentlyDenied) {
showErrorToast('Permission Denied');
}
return status;
}
return PermissionStatus.granted;
}
}

View File

@ -8,3 +8,6 @@ enum ProgressStatuses { pending, started, completed }
// Levels
enum ProficiencyLevels { a1, a2, b1, b2, none }
// State object
enum StateObjects{profileImage}

View File

@ -177,6 +177,12 @@ TextStyle style18P600 = const TextStyle(
fontWeight: FontWeight.w600,
);
TextStyle style18W600 = const TextStyle(
fontSize: 18,
color: kcWhite,
fontWeight: FontWeight.w600,
);
TextStyle style12R700 = const TextStyle(
fontSize: 12,
color: Colors.red,

View File

@ -171,11 +171,6 @@ class AssessmentViewModel extends BaseViewModel {
// Complete profile
Future<void> saveProfileCompleted() async {
Map<String, dynamic> data = {'firstName': _userData['firstName']};
await _authenticationService.saveFullName(data);
}
Future<void> completeProfile() async =>
await runBusyFuture<Map<String, dynamic>>(_completeProfile());
@ -185,7 +180,6 @@ class AssessmentViewModel extends BaseViewModel {
await _apiService.updateProfile(data: _userData, user: user);
if (response['status'] == ResponseStatus.success) {
showSuccessToast(response['message']);
await saveProfileCompleted();
await replaceWithHome();
} else {
showErrorToast(response['message']);

View File

@ -27,9 +27,7 @@ class HomeView extends StackedView<HomeViewModel> {
_buildScaffoldWrapper(viewModel);
Widget _buildScaffoldWrapper(HomeViewModel viewModel) => viewModel.isBusy
? const StartupView(
label: 'Checking user info',
)
? const StartupView(label: 'Checking user info')
: _buildScaffold(viewModel);
Widget _buildScaffold(HomeViewModel viewModel) => Scaffold(

View File

@ -93,18 +93,10 @@ class HomeViewModel extends BaseViewModel {
return response;
}
Future<void> getProfileStatus() async {
Map<String, dynamic> response =
await runBusyFuture<Map<String, dynamic>>(_getProfileStatus());
if (response['status'] == ResponseStatus.success && !response['data']) {
await replaceWithOnboarding();
} else if (response['status'] == ResponseStatus.success &&
response['data']) {
await saveProfileStatus(response['data']);
}
}
Future<void> getProfileStatus() async =>
await runBusyFuture(_getProfileStatus());
Future<Map<String, dynamic>> _getProfileStatus() async {
Future<void> _getProfileStatus() async {
Map<String, dynamic> response = {};
UserModel? user = await _authenticationService.getUser();
@ -118,6 +110,11 @@ class HomeViewModel extends BaseViewModel {
response = {'data': true, 'status': ResponseStatus.success};
}
return response;
if (response['status'] == ResponseStatus.success && !response['data']) {
await replaceWithOnboarding();
} else if (response['status'] == ResponseStatus.success &&
response['data']) {
await saveProfileStatus(response['data']);
}
}
}

View File

@ -7,13 +7,16 @@ import 'package:yimaru_app/ui/common/enmus.dart';
import '../../../app/app.locator.dart';
class LearnViewModel extends BaseViewModel {
class LearnViewModel extends ReactiveViewModel {
final _navigationService = locator<NavigationService>();
final _authenticationService = locator<AuthenticationService>();
late final UserModel? _user = _authenticationService.user;
@override
List<ListenableServiceMixin> get listenableServices =>
[_authenticationService];
UserModel? get user => _user;
// Current user
UserModel? get user => _authenticationService.user;
final List<Map<String, dynamic>> _learnLevels = [
{

View File

@ -1,17 +1,37 @@
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
import 'package:yimaru_app/ui/widgets/custom_circular_progress_indicator.dart';
import 'package:yimaru_app/ui/widgets/profile_card.dart';
import 'package:yimaru_app/ui/widgets/profile_image.dart';
import 'package:yimaru_app/ui/widgets/view_profile_button.dart';
import '../../widgets/custom_elevated_button.dart';
import '../../widgets/image_picker_option.dart';
import 'profile_viewmodel.dart';
class ProfileView extends StackedView<ProfileViewModel> {
const ProfileView({Key? key}) : super(key: key);
Future<void> _showImagePicker(
{required BuildContext context,
required ProfileViewModel viewModel}) async =>
await showDialog(
context: context,
builder: (context) =>
_showImagePickerDialog(context: context, viewModel: viewModel),
);
AlertDialog _showImagePickerDialog(
{required BuildContext context,
required ProfileViewModel viewModel}) =>
AlertDialog(
backgroundColor: Colors.transparent,
content: _buildImagePicker(context: context, viewModel: viewModel),
);
@override
ProfileViewModel viewModelBuilder(
BuildContext context,
@ -24,30 +44,45 @@ class ProfileView extends StackedView<ProfileViewModel> {
ProfileViewModel viewModel,
Widget? child,
) =>
_buildScaffoldWrapper(viewModel);
_buildScaffoldWrapper(context: context, viewModel: viewModel);
Widget _buildScaffoldWrapper(ProfileViewModel viewModel) => Scaffold(
Widget _buildScaffoldWrapper(
{required BuildContext context,
required ProfileViewModel viewModel}) =>
Scaffold(
backgroundColor: kcBackgroundColor,
body: _buildScaffold(viewModel),
body: _buildScaffold(context: context, viewModel: viewModel),
);
Widget _buildScaffold(ProfileViewModel viewModel) =>
SafeArea(child: _buildBodyWrapper(viewModel));
Widget _buildScaffold(
{required BuildContext context,
required ProfileViewModel viewModel}) =>
SafeArea(
child: _buildBodyWrapper(context: context, viewModel: viewModel));
Widget _buildBodyWrapper(ProfileViewModel viewModel) => SingleChildScrollView(
child: _buildBody(viewModel),
Widget _buildBodyWrapper(
{required BuildContext context,
required ProfileViewModel viewModel}) =>
SingleChildScrollView(
child: _buildBody(context: context, viewModel: viewModel),
);
Widget _buildBody(ProfileViewModel viewModel) => Padding(
Widget _buildBody(
{required BuildContext context,
required ProfileViewModel viewModel}) =>
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: _buildColumn(viewModel),
child: _buildColumn(context: context, viewModel: viewModel),
);
Widget _buildColumn(ProfileViewModel viewModel) => Column(
Widget _buildColumn(
{required BuildContext context,
required ProfileViewModel viewModel}) =>
Column(
children: [
verticalSpaceMedium,
_buildNotificationIconWrapper(),
_buildProfileSection(viewModel),
_buildProfileSection(context: context, viewModel: viewModel),
verticalSpaceSmall,
_buildViewProfileButton(viewModel),
verticalSpaceLarge,
@ -66,20 +101,42 @@ class ProfileView extends StackedView<ProfileViewModel> {
color: kcDarkGrey,
);
Widget _buildProfileSection(ProfileViewModel viewModel) => Column(
Widget _buildProfileSection(
{required BuildContext context,
required ProfileViewModel viewModel}) =>
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: _buildProfileSectionChildren(viewModel),
children: _buildProfileSectionChildren(
context: context, viewModel: viewModel),
);
List<Widget> _buildProfileSectionChildren(ProfileViewModel viewModel) => [
_buildProfileImage(viewModel),
List<Widget> _buildProfileSectionChildren(
{required BuildContext context,
required ProfileViewModel viewModel}) =>
[
_buildProfileImage(context: context, viewModel: viewModel),
verticalSpaceSmall,
_buildProfileName(viewModel),
];
Widget _buildProfileImage(ProfileViewModel viewModel) => ProfileImage(
Widget _buildProfileImage(
{required BuildContext context,
required ProfileViewModel viewModel}) =>
ProfileImage(
profileImage: viewModel.user?.profileImage,
loading: viewModel.busy(StateObjects.profileImage) ? true:false,
onTap: () async =>
await _showImagePicker(context: context, viewModel: viewModel),
);
Widget _buildImagePicker(
{required BuildContext context,
required ProfileViewModel viewModel}) =>
ImagePickerOption(
onCameraTap: () async => await viewModel.openCamera(),
onGalleryTap: () async => await viewModel.openGallery(),
);
Widget _buildProfileName(ProfileViewModel viewModel) => Text(

View File

@ -1,25 +1,58 @@
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/app/app.router.dart';
import 'package:yimaru_app/services/image_picker_service.dart';
import 'package:yimaru_app/ui/common/enmus.dart';
import '../../../app/app.locator.dart';
import '../../../models/user_model.dart';
import '../../../services/authentication_service.dart';
class ProfileViewModel extends BaseViewModel {
class ProfileViewModel extends ReactiveViewModel {
final _navigationService = locator<NavigationService>();
final _imagePickerService = locator<ImagePickerService>();
final _authenticationService = locator<AuthenticationService>();
late final UserModel? _user = _authenticationService.user;
@override
List<ListenableServiceMixin> get listenableServices =>
[_authenticationService];
UserModel? get user => _user;
// Current user
UserModel? get user => _authenticationService.user;
// Image picker
Future<void> openCamera() async => runBusyFuture(_openCamera(),busyObject: StateObjects.profileImage);
Future<void> _openCamera()async{
String? image = await _imagePickerService.camera();
if (image != null) {
await _authenticationService.saveProfileImage(image);
}
pop();
}
Future<void> openGallery() async => runBusyFuture(_openGallery(),busyObject: StateObjects.profileImage);
Future<void> _openGallery() async {
String? image = await _imagePickerService.gallery();
if (image != null) {
await _authenticationService.saveProfileImage(image);
}
pop();
}
// Logout
Future<void> logOut() async {
await _authenticationService.logOut();
await _navigationService.replaceWithLoginView();
}
// Navigation
void pop() => _navigationService.back();
Future<void> navigateToProfileDetail() async =>
await _navigationService.navigateToProfileDetailView();

View File

@ -82,5 +82,5 @@ class CustomLargeRadioButton extends StatelessWidget {
Widget _buildSelectedCheckBox() => Checkbox(
value: selected,
activeColor: kcPrimaryColor,
onChanged: (value) => onTap);
onChanged: onTap != null ? (value) => onTap!() : null);
}

View File

@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import 'package:yimaru_app/ui/common/app_colors.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
class ImagePickerOption extends StatelessWidget {
final GestureTapCallback? onCameraTap;
final GestureTapCallback? onGalleryTap;
const ImagePickerOption({super.key, this.onCameraTap, this.onGalleryTap});
@override
Widget build(BuildContext context) => _buildContainer();
Widget _buildContainer() => Container(
height: 200,
decoration: const BoxDecoration(
color: kcBackgroundColor,
shape: BoxShape.rectangle,
borderRadius: BorderRadius.all(Radius.circular(32.0)),
),
child: _buildCameraOptionWrapper(),
);
Widget _buildCameraOptionWrapper() => Center(
child: _buildCameraOption(),
);
Widget _buildCameraOption() => Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: _buildCameraOptionChildren(),
);
List<Widget> _buildCameraOptionChildren() =>
[_buildCameraButton(), _buildGalleryButton()];
Widget _buildCameraButton() => GestureDetector(
onTap: onCameraTap,
child: _buildCamera(),
);
Widget _buildCamera() => Column(
mainAxisSize: MainAxisSize.min,
children: _buildCameraChildren(),
);
List<Widget> _buildCameraChildren() =>
[_buildCameraIcon(), verticalSpaceTiny, _buildCameraTitle()];
Widget _buildCameraIcon() => const Icon(
Icons.camera_alt_rounded,
size: 60,
color: kcPrimaryColor,
);
Widget _buildCameraTitle() => Text(
'Camera',
style: style18P600,
);
Widget _buildGalleryButton() => GestureDetector(
onTap: onGalleryTap,
child: _buildGallery(),
);
Widget _buildGallery() => Column(
mainAxisSize: MainAxisSize.min,
children: _buildGalleryChildren(),
);
Widget _buildGalleryIcon() => const Icon(
Icons.photo,
size: 60,
color: kcPrimaryColor,
);
Widget _buildGalleryText() => Text(
'Gallery',
style: style18P600,
);
List<Widget> _buildGalleryChildren() =>
[_buildGalleryIcon(), verticalSpaceTiny, _buildGalleryText()];
}

View File

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:yimaru_app/ui/common/ui_helpers.dart';
@ -39,7 +41,9 @@ class LearnAppBar extends StatelessWidget {
radius: 25,
backgroundColor: kcPrimaryColor,
backgroundImage: profileImage != null
? CachedNetworkImageProvider(profileImage!)
? FileImage(
File(profileImage!),
)
: null,
child: _buildImageBuilder(),
);

View File

@ -1,17 +1,26 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:yimaru_app/ui/common/app_colors.dart';
class ProfileImage extends StatelessWidget {
final bool loading;
final String? profileImage;
const ProfileImage({super.key, required this.profileImage});
final GestureTapCallback? onTap;
const ProfileImage(
{super.key,
this.onTap,
this.loading = false,
required this.profileImage});
@override
Widget build(BuildContext context) => _buildSizedBox();
Widget _buildSizedBox() => SizedBox(
height: 125,
width: 125,
height: 125,
child: _buildStack(),
);
@ -27,14 +36,21 @@ class ProfileImage extends StatelessWidget {
Widget _buildProfileImage() => CircleAvatar(
radius: 50,
backgroundColor: kcPrimaryColor,
backgroundImage: profileImage != null
? CachedNetworkImageProvider(profileImage!)
: null,
backgroundImage: loading
? null
: profileImage != null
? FileImage(
File(profileImage!),
)
: null,
child: _buildImageBuilder(),
);
Widget? _buildImageBuilder() =>
profileImage == null ? _buildPersonIcon() : null;
Widget? _buildImageBuilder() => loading
? null
: profileImage == null
? _buildPersonIcon()
: null;
Widget _buildPersonIcon() => const Icon(
Icons.person,
@ -44,6 +60,11 @@ class ProfileImage extends StatelessWidget {
Widget _buildCameraButtonWrapper() => Align(
alignment: Alignment.bottomCenter,
child: _buildCameraTapDetector(),
);
Widget _buildCameraTapDetector() => GestureDetector(
onTap: onTap,
child: _buildCameraButton(),
);

View File

@ -6,9 +6,13 @@
#include "generated_plugin_registrant.h"
#include <file_selector_linux/file_selector_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);

View File

@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux
flutter_secure_storage_linux
)

View File

@ -7,12 +7,14 @@ import Foundation
import battery_plus
import connectivity_plus
import file_selector_macos
import flutter_secure_storage_darwin
import sqflite_darwin
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
BatteryPlusMacosPlugin.register(with: registry.registrar(forPlugin: "BatteryPlusMacosPlugin"))
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
}

View File

@ -225,6 +225,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.2"
cross_file:
dependency: transitive
description:
name: cross_file
sha256: "701dcfc06da0882883a2657c445103380e53e647060ad8d9dfb710c100996608"
url: "https://pub.dev"
source: hosted
version: "0.3.5+1"
crypto:
dependency: transitive
description:
@ -321,6 +329,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.1"
file_selector_linux:
dependency: transitive
description:
name: file_selector_linux
sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
url: "https://pub.dev"
source: hosted
version: "0.9.4"
file_selector_macos:
dependency: transitive
description:
name: file_selector_macos
sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
url: "https://pub.dev"
source: hosted
version: "0.9.5"
file_selector_platform_interface:
dependency: transitive
description:
name: file_selector_platform_interface
sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85"
url: "https://pub.dev"
source: hosted
version: "2.7.0"
file_selector_windows:
dependency: transitive
description:
name: file_selector_windows
sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
url: "https://pub.dev"
source: hosted
version: "0.9.3+5"
fixnum:
dependency: transitive
description:
@ -366,6 +406,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.3"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1
url: "https://pub.dev"
source: hosted
version: "2.0.33"
flutter_secure_storage:
dependency: "direct main"
description:
@ -552,6 +600,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.1"
image_picker:
dependency: "direct main"
description:
name: image_picker
sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
image_picker_android:
dependency: transitive
description:
name: image_picker_android
sha256: "297e42bd236c4ac4b091d4277292159b3280545e030cae2be3d503f9ecf7e6a1"
url: "https://pub.dev"
source: hosted
version: "0.8.13+12"
image_picker_for_web:
dependency: transitive
description:
name: image_picker_for_web
sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
image_picker_ios:
dependency: transitive
description:
name: image_picker_ios
sha256: "956c16a42c0c708f914021666ffcd8265dde36e673c9fa68c81f7d085d9774ad"
url: "https://pub.dev"
source: hosted
version: "0.8.13+3"
image_picker_linux:
dependency: transitive
description:
name: image_picker_linux
sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4"
url: "https://pub.dev"
source: hosted
version: "0.2.2"
image_picker_macos:
dependency: transitive
description:
name: image_picker_macos
sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91"
url: "https://pub.dev"
source: hosted
version: "0.2.2+1"
image_picker_platform_interface:
dependency: transitive
description:
name: image_picker_platform_interface
sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c"
url: "https://pub.dev"
source: hosted
version: "2.11.1"
image_picker_windows:
dependency: transitive
description:
name: image_picker_windows
sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae
url: "https://pub.dev"
source: hosted
version: "0.2.2"
in_app_update:
dependency: "direct main"
description:
@ -832,6 +944,54 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.0+3"
permission_handler:
dependency: "direct main"
description:
name: permission_handler
sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1
url: "https://pub.dev"
source: hosted
version: "12.0.1"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6"
url: "https://pub.dev"
source: hosted
version: "13.0.1"
permission_handler_apple:
dependency: transitive
description:
name: permission_handler_apple
sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023
url: "https://pub.dev"
source: hosted
version: "9.4.7"
permission_handler_html:
dependency: transitive
description:
name: permission_handler_html
sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
url: "https://pub.dev"
source: hosted
version: "0.1.3+5"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878
url: "https://pub.dev"
source: hosted
version: "4.3.0"
permission_handler_windows:
dependency: transitive
description:
name: permission_handler_windows
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
url: "https://pub.dev"
source: hosted
version: "0.2.1"
petitparser:
dependency: transitive
description:

View File

@ -16,6 +16,7 @@ dependencies:
iconsax: ^0.0.8
flutter_svg: ^2.2.3
stacked_shared: any
image_picker: ^1.2.1
battery_plus: ^7.0.0
storage_info: ^1.0.0
flutter_html: ^3.0.0
@ -27,6 +28,7 @@ dependencies:
stacked_services: ^1.1.0
omni_datetime_picker: any
json_serializable: ^6.8.0
permission_handler: ^12.0.1
cached_network_image: ^3.4.1
flutter_secure_storage: ^10.0.0
flutter_timer_countdown: ^1.0.7

View File

@ -0,0 +1,155 @@
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:yimaru_app/app/app.locator.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:yimaru_app/services/authentication_service.dart';
import 'package:yimaru_app/services/api_service.dart';
import 'package:yimaru_app/services/secure_storage_service.dart';
import 'package:yimaru_app/services/dio_service.dart';
import 'package:yimaru_app/services/status_checker_service.dart';
import 'package:yimaru_app/services/permission_handler_service.dart';
import 'package:yimaru_app/services/image_picker_service.dart';
// @stacked-import
import 'test_helpers.mocks.dart';
@GenerateMocks(
[],
customMocks: [
MockSpec<NavigationService>(onMissingStub: OnMissingStub.returnDefault),
MockSpec<BottomSheetService>(onMissingStub: OnMissingStub.returnDefault),
MockSpec<DialogService>(onMissingStub: OnMissingStub.returnDefault),
MockSpec<AuthenticationService>(onMissingStub: OnMissingStub.returnDefault),
MockSpec<ApiService>(onMissingStub: OnMissingStub.returnDefault),
MockSpec<SecureStorageService>(onMissingStub: OnMissingStub.returnDefault),
MockSpec<DioService>(onMissingStub: OnMissingStub.returnDefault),
MockSpec<StatusCheckerService>(onMissingStub: OnMissingStub.returnDefault),
MockSpec<PermissionHandlerService>(
onMissingStub: OnMissingStub.returnDefault),
MockSpec<ImagePickerService>(onMissingStub: OnMissingStub.returnDefault),
// @stacked-mock-spec
],
)
void registerServices() {
getAndRegisterNavigationService();
getAndRegisterBottomSheetService();
getAndRegisterDialogService();
getAndRegisterAuthenticationService();
getAndRegisterApiService();
getAndRegisterSecureStorageService();
getAndRegisterDioService();
getAndRegisterStatusCheckerService();
getAndRegisterPermissionHandlerService();
getAndRegisterImagePickerService();
// @stacked-mock-register
}
MockNavigationService getAndRegisterNavigationService() {
_removeRegistrationIfExists<NavigationService>();
final service = MockNavigationService();
locator.registerSingleton<NavigationService>(service);
return service;
}
MockBottomSheetService getAndRegisterBottomSheetService<T>({
SheetResponse<T>? showCustomSheetResponse,
}) {
_removeRegistrationIfExists<BottomSheetService>();
final service = MockBottomSheetService();
when(
service.showCustomSheet<T, T>(
enableDrag: anyNamed('enableDrag'),
enterBottomSheetDuration: anyNamed('enterBottomSheetDuration'),
exitBottomSheetDuration: anyNamed('exitBottomSheetDuration'),
ignoreSafeArea: anyNamed('ignoreSafeArea'),
isScrollControlled: anyNamed('isScrollControlled'),
barrierDismissible: anyNamed('barrierDismissible'),
additionalButtonTitle: anyNamed('additionalButtonTitle'),
variant: anyNamed('variant'),
title: anyNamed('title'),
hasImage: anyNamed('hasImage'),
imageUrl: anyNamed('imageUrl'),
showIconInMainButton: anyNamed('showIconInMainButton'),
mainButtonTitle: anyNamed('mainButtonTitle'),
showIconInSecondaryButton: anyNamed('showIconInSecondaryButton'),
secondaryButtonTitle: anyNamed('secondaryButtonTitle'),
showIconInAdditionalButton: anyNamed('showIconInAdditionalButton'),
takesInput: anyNamed('takesInput'),
barrierColor: anyNamed('barrierColor'),
barrierLabel: anyNamed('barrierLabel'),
customData: anyNamed('customData'),
data: anyNamed('data'),
description: anyNamed('description'),
),
).thenAnswer(
(realInvocation) =>
Future.value(showCustomSheetResponse ?? SheetResponse<T>()),
);
locator.registerSingleton<BottomSheetService>(service);
return service;
}
MockDialogService getAndRegisterDialogService() {
_removeRegistrationIfExists<DialogService>();
final service = MockDialogService();
locator.registerSingleton<DialogService>(service);
return service;
}
MockAuthenticationService getAndRegisterAuthenticationService() {
_removeRegistrationIfExists<AuthenticationService>();
final service = MockAuthenticationService();
locator.registerSingleton<AuthenticationService>(service);
return service;
}
MockApiService getAndRegisterApiService() {
_removeRegistrationIfExists<ApiService>();
final service = MockApiService();
locator.registerSingleton<ApiService>(service);
return service;
}
MockSecureStorageService getAndRegisterSecureStorageService() {
_removeRegistrationIfExists<SecureStorageService>();
final service = MockSecureStorageService();
locator.registerSingleton<SecureStorageService>(service);
return service;
}
MockDioService getAndRegisterDioService() {
_removeRegistrationIfExists<DioService>();
final service = MockDioService();
locator.registerSingleton<DioService>(service);
return service;
}
MockStatusCheckerService getAndRegisterStatusCheckerService() {
_removeRegistrationIfExists<StatusCheckerService>();
final service = MockStatusCheckerService();
locator.registerSingleton<StatusCheckerService>(service);
return service;
}
MockPermissionHandlerService getAndRegisterPermissionHandlerService() {
_removeRegistrationIfExists<PermissionHandlerService>();
final service = MockPermissionHandlerService();
locator.registerSingleton<PermissionHandlerService>(service);
return service;
}
MockImagePickerService getAndRegisterImagePickerService() {
_removeRegistrationIfExists<ImagePickerService>();
final service = MockImagePickerService();
locator.registerSingleton<ImagePickerService>(service);
return service;
}
// @stacked-mock-create
void _removeRegistrationIfExists<T extends Object>() {
if (locator.isRegistered<T>()) {
locator.unregister<T>();
}
}

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@ import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('FailureViewModel Tests -', () {
group('ApiServiceTest -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});

View File

@ -4,7 +4,7 @@ import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('LearnLessonViewModel Tests -', () {
group('AuthenticationServiceTest -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});

View File

@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('DioServiceTest -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}

View File

@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('PermissionHandlerServiceTest -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}

View File

@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('SecureStorageServiceTest -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}

View File

@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('StatusCheckerServiceTest -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}

View File

@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('AccountPrivacyViewModel Tests -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}

View File

@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('AssessmentViewModel Tests -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}

View File

@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('CallSupportViewModel Tests -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}

View File

@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('DownloadsViewModel Tests -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}

View File

@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('FullNameViewViewModel Tests -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}

View File

@ -0,0 +1,36 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:yimaru_app/app/app.bottomsheets.dart';
import 'package:yimaru_app/app/app.locator.dart';
import 'package:yimaru_app/ui/common/app_strings.dart';
import 'package:yimaru_app/ui/views/home/home_viewmodel.dart';
import '../helpers/test_helpers.dart';
void main() {
HomeViewModel getModel() => HomeViewModel();
group('HomeViewmodelTest -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
group('showBottomSheet -', () {
test(
'When called, should show custom bottom sheet using notice variant',
() {
final bottomSheetService = getAndRegisterBottomSheetService();
final model = getModel();
model.showBottomSheet();
verify(
bottomSheetService.showCustomSheet(
variant: BottomSheetType.notice,
title: ksHomeBottomSheetTitle,
description: ksHomeBottomSheetDescription,
),
);
},
);
});
});
}

View File

@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('LanguageViewModel Tests -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}

View File

@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('LearnLevelViewModel Tests -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}

View File

@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('LearnModuleViewModel Tests -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}

View File

@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('LearnViewModel Tests -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}

View File

@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('LoginViewModel Tests -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}

View File

@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('OnboardingViewModel Tests -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}

View File

@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('OngoingProgressViewModel Tests -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}

View File

@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('PrivacyPolicyViewModel Tests -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}

View File

@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('ProfileDetailViewModel Tests -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}

View File

@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('ProfileViewModel Tests -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}

View File

@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('ProgressViewModel Tests -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}

View File

@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('RegisterViewModel Tests -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}

View File

@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('SettingsViewModel Tests -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}

View File

@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('StartupViewModel Tests -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}

View File

@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('SupportViewModel Tests -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}

View File

@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('TelegramSupportViewModel Tests -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}

View File

@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('TermsAndConditionsViewModel Tests -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}

View File

@ -0,0 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yimaru_app/app/app.locator.dart';
import '../helpers/test_helpers.dart';
void main() {
group('WelcomeViewModel Tests -', () {
setUp(() => registerServices());
tearDown(() => locator.reset());
});
}

View File

@ -8,13 +8,19 @@
#include <battery_plus/battery_plus_windows_plugin.h>
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
#include <file_selector_windows/file_selector_windows.h>
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
BatteryPlusWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("BatteryPlusWindowsPlugin"));
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
}

View File

@ -5,7 +5,9 @@
list(APPEND FLUTTER_PLUGIN_LIST
battery_plus
connectivity_plus
file_selector_windows
flutter_secure_storage_windows
permission_handler_windows
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST