Fixed design issues, added updated read me file

This commit is contained in:
Abdullah Alassaf
2024-08-01 12:57:13 +03:00
parent 296726ac82
commit 8d6da30d09
6 changed files with 297 additions and 214 deletions

View File

@ -14,3 +14,85 @@ A few resources to get you started if this is your first Flutter project:
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
## Development Process
1- You'll receive a task assignment in Jira that's been assigned to you.
2- In Jira, change the status of the task to "in progress".
3- Create a new branch for the task using the command "git checkout -b task_number".
4- Make your changes and commit them using the command "git commit -m 'Add my changes'".
5- Push your changes to the task branch using the command "git push origin task-branch".
Open a pull request on the DEV branch and add a reviewer to it.
6- Once the reviewer approves your pull request, merge your changes into the DEV branch.
7- Use the command "git checkout DEV" to switch to the DEV branch.
8- Upload apk file and ipa file to Firebase distribution (you can find the steps to upload your app to Firebase Distribution for both Android and iOS platforms in the Deployment section).
9- Update the task status in Jira to "QA".
## Deployment
### Android
**- Firebase Distribution:**
To test the app, we use Firebase Distribution to send testing builds of the app to the testers.
- Create an Android build for testing with the command: `flutter build apk --dart-define FLAVOR=staging --build-name={build_version}-qa --build-number={build_number}`.
- Upload the apk file to Firebase distribution
**- Google Play Store:**
1- To create an APK file from your Flutter project, you can use the command: `flutter build apk --release`
2- Upload your APK file to the Google Play Console and provide all necessary information about the release, such as release notes and version number.
3- Submit your app for review, which can take several days to complete. Once your app is approved, it will be available for download on the Google Play Store.
### iOS
**- Firebase Distribution:**
To test the app, we use Firebase Distribution to send testing builds of the app to the testers.
1- Create an iOS for testing with the command: `flutter build ios --dart-define FLAVOR=staging --build-name=1.0.0-qa --build-number=1`.
2- Create an archive of your app: Open Xcode and go to the "Product" menu, then select "Archive" to create an archive of your app. Make sure to select the "Generic iOS Device" as the build destination.
3- Once the archive is complete, go to the "Organizer" window and select the archive you just created and click on the "Distribute App" button.
4- Choose "Ad Hoc" as the distribution method and click "Next".
5- Choose the appropriate signing identity and click "Export".
6- Choose a location to save the exported app and click "Save".
7- Your HOC build is now ready to be signed and uploaded to Firebase Distribution.
8- Upload the ipa file to Firebase distribution
You can also create an archive through these commands lines:
1- Create an iOS for testing with the command: `flutter build ios --dart-define FLAVOR=staging --build-name={build_version}-qa --build-number={build_number}`.
2- Create an archive with this command: `xcodebuild -workspace Runner.xcworkspace -scheme Runner -archivePath "build/Runner.xcarchive" archive`.
3- Export the ipa file with this command: `-exportArchive -archivePath "build/Runner.xcarchive" -exportPath "build/exported_ipas" -exportOptionsPlist "build/ExportOptions.plist"`
**- Apple Store**
1- Build your app: Use Flutter to build your app for release. To build your app for iOS, use the command: `flutter build ios --release --no-codesign`
Note: The --no-codesign flag will tell Flutter not to sign your app, since you'll be doing that later with Xcode.
2- Create an archive of your app: Open Xcode and go to the "Product" menu, then select "Archive" to create an archive of your app. Make sure to select the "Generic iOS Device" as the build destination.
3- Validate and upload your app: Once the archive is created, Xcode will automatically open the Organizer window. Select the archive you just created and click the "Validate App" button to validate your app. Once the validation process is complete, click the "Distribute App" button to upload your app to the App Store Connect.
4- Submit your app for review: After uploading your app, you'll need to submit it for review by Apple. This process can take several days, and you'll be notified by email once your app is approved or rejected.

View File

@ -23,13 +23,12 @@ class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
String timeZoneSelected = '';
String regionSelected = '';
final TextEditingController searchController = TextEditingController();
final TextEditingController nameController = TextEditingController(text: '${HomeCubit.user!.firstName} ${HomeCubit.user!.lastName}');
final TextEditingController nameController =
TextEditingController(text: '${HomeCubit.user!.firstName} ${HomeCubit.user!.lastName}');
List<RegionModel> allRegions = [];
List<TimeZone> allTimeZone = [];
ProfileBloc() : super(InitialState()) {
on<InitialProfileEvent>(_fetchUserInfo);
on<TimeZoneInitialEvent>(_fetchTimeZone);
@ -53,7 +52,7 @@ class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
final nameParts = fullName.split(' ');
final firstName = nameParts[0];
final lastName = nameParts.length > 1 ? nameParts[1] : '';
var response = await ProfileApi.saveName(firstName: firstName, lastName: lastName);
await ProfileApi.saveName(firstName: firstName, lastName: lastName);
add(InitialProfileEvent());
await HomeCubit.getInstance().fetchUserInfo();
CustomSnackBar.displaySnackBar('Save Successfully');
@ -72,7 +71,7 @@ class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
Future.delayed(const Duration(milliseconds: 500), () {
focusNode.requestFocus();
});
}else {
} else {
focusNode.unfocus();
}
emit(NameEditingState(editName: editName));
@ -116,7 +115,7 @@ class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
Future selectRegion(SelectRegionEvent event, Emitter<ProfileState> emit) async {
try {
emit(LoadingInitialState());
await ProfileApi.saveRegion(regionUuid:event.val );
await ProfileApi.saveRegion(regionUuid: event.val);
CustomSnackBar.displaySnackBar('Save Successfully');
emit(SaveState());
} catch (e) {
@ -125,7 +124,6 @@ class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
}
}
Future<void> searchRegion(SearchRegionEvent event, Emitter<ProfileState> emit) async {
emit(LoadingInitialState());
final query = event.query.toLowerCase();
@ -158,7 +156,6 @@ class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
}
}
void _fetchRegion(RegionInitialEvent event, Emitter<ProfileState> emit) async {
try {
emit(LoadingInitialState());
@ -170,36 +167,37 @@ class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
}
Future<void> _selectImage(SelectImageEvent event, Emitter<ProfileState> emit) async {
if (await _requestPermission()) {
emit(ChangeImageState());
final pickedFile = await _picker.pickImage(source: ImageSource.gallery);
if (pickedFile != null) {
image = File(pickedFile.path);
final bytes = image!.readAsBytesSync().lengthInBytes;
final kb = bytes / 1024;
final mb = kb / 1024;
if(mb>1){
image=null;
CustomSnackBar.displaySnackBar('Image size must be 1 MB or less');
}else{
await _saveImage();
try {
if (await _requestPermission()) {
emit(ChangeImageState());
final pickedFile = await _picker.pickImage(source: ImageSource.gallery);
if (pickedFile != null) {
image = File(pickedFile.path);
final bytes = image!.readAsBytesSync().lengthInBytes;
final kb = bytes / 1024;
final mb = kb / 1024;
if (mb > 1) {
image = null;
CustomSnackBar.displaySnackBar('Image size must be 1 MB or less');
} else {
emit(LoadingInitialState());
await _saveImage();
emit(ImageSelectedState());
}
}
emit(ImageSelectedState());
} else {
print('No image selected.');
_showPermissionDeniedDialog(event.context);
}
emit(ImageSelectedState());
} else {
_showPermissionDeniedDialog(event.context);
} catch (_) {
emit(const FailedState(errorMessage: 'Something went wrong'));
}
}
Future<void> _saveImage() async {
emit(LoadingInitialState());
List<int> imageBytes = image!.readAsBytesSync();
String base64Image = base64Encode(imageBytes);
print(base64Image);
var response = await ProfileApi.saveImage(base64Image);
emit(ImageSelectedState());
await ProfileApi.saveImage(base64Image);
}
void _showPermissionDeniedDialog(BuildContext context) {
@ -229,9 +227,6 @@ class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
);
}
bool _validateInputs() {
final nameError = fullNameValidator(nameController.text);
if (nameError != null) {
@ -241,7 +236,6 @@ class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
return false;
}
String? fullNameValidator(String? value) {
if (value == null) return 'Full name is required';
final withoutExtraSpaces = value.replaceAll(RegExp(r"\s+"), ' ').trim();
@ -261,13 +255,12 @@ class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
return null;
}
Future<bool> _requestPermission() async {
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
if (Platform.isAndroid ) {
if (Platform.isAndroid) {
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
PermissionStatus status = await Permission.photos.status;
if(androidInfo.version.sdkInt<33){
if (androidInfo.version.sdkInt < 33) {
if (status.isDenied) {
PermissionStatus status = await Permission.storage.request();
if (status.isGranted) {
@ -276,7 +269,7 @@ class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
return false;
}
}
}else{
} else {
if (status.isGranted) {
return true;
} else if (status.isDenied) {
@ -301,5 +294,4 @@ class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
}
}
}
}

View File

@ -21,12 +21,11 @@ class MenuView extends StatelessWidget {
builder: (context, state) {
return BlocBuilder<AuthCubit, AuthState>(
builder: (context, state) {
final profileBloc = BlocProvider.of<MenuCubit>(context);
return SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Column(
children: [
ProfileTab(),
const ProfileTab(),
for (var section in menuSections)
MenuList(
section: section,

View File

@ -7,20 +7,23 @@ import 'package:syncrow_app/features/shared_widgets/text_widgets/body_medium.dar
import 'package:syncrow_app/features/shared_widgets/text_widgets/body_small.dart';
class ProfileTab extends StatelessWidget {
const ProfileTab({super.key,});
const ProfileTab({
super.key,
});
@override
Widget build(BuildContext context) {
return BlocBuilder<HomeCubit, HomeState>(
builder: (context, state) {
return _buildProfileContent(context );
return _buildProfileContent(context);
},
);
}
Widget _buildProfileContent(BuildContext context) {
final homeCubit = context.read<HomeCubit>();
return Padding(
padding: const EdgeInsets.symmetric(vertical: 10,),
padding: const EdgeInsets.symmetric(
vertical: 10,
),
child: InkWell(
onTap: () {
Navigator.of(context)
@ -28,7 +31,8 @@ class ProfileTab extends StatelessWidget {
MaterialPageRoute(
builder: (context) => const ProfileView(),
),
).then((result) {
)
.then((result) {
context.read<HomeCubit>().fetchUserInfo();
});
},
@ -47,7 +51,7 @@ class ProfileTab extends StatelessWidget {
Row(
children: [
BodyMedium(
text: '${HomeCubit.user!.firstName ?? ''} ',
text: '${HomeCubit.user?.firstName ?? ''} ',
fontWeight: FontWeight.bold,
),
BodyMedium(
@ -56,7 +60,9 @@ class ProfileTab extends StatelessWidget {
),
],
),
SizedBox(height: 5,),
const SizedBox(
height: 5,
),
const BodySmall(text: "Syncrow Account"),
],
),
@ -76,12 +82,12 @@ class ProfileTab extends StatelessWidget {
child: ClipOval(
child: HomeCubit.user?.profilePicture != null
? Image.memory(
HomeCubit.user!.profilePicture!,
fit: BoxFit.cover,
width: 110,
height: 110,
)
: Icon(Icons.person, size: 70), // Fallback if no image
HomeCubit.user!.profilePicture!,
fit: BoxFit.cover,
width: 110,
height: 110,
)
: const Icon(Icons.person, size: 70), // Fallback if no image
),
),
),

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_app/features/app_layout/bloc/home_cubit.dart';
import 'package:syncrow_app/features/menu/bloc/profile_bloc/profile_bloc.dart';
@ -25,175 +24,180 @@ class ProfileView extends StatelessWidget {
final profileBloc = BlocProvider.of<ProfileBloc>(context);
return DefaultScaffold(
title: 'Syncrow Account',
child:
state is LoadingInitialState
? const Center(child: CircularProgressIndicator()):
Column(
children: [
SizedBox(
height: MediaQuery.of(context).size.height * 0.05,
),
InkWell(
onTap: () {
profileBloc.add(SelectImageEvent(context: context, isSelected: false));
},
child: SizedBox.square(
dimension: 125,
child: CircleAvatar(
backgroundColor: Colors.white,
child: SizedBox.square(
dimension: 120,
child: CircleAvatar(
backgroundColor: Colors.white,
backgroundImage: profileBloc.image == null
? null
: FileImage(profileBloc.image!),
child: profileBloc.image != null
? null
:HomeCubit.user!.profilePicture != null
? ClipOval(
child: Image.memory(
HomeCubit.user!.profilePicture!,
fit: BoxFit.cover,
width: 120,
height: 120,
),
)
: null,
child: state is LoadingInitialState
? const Center(child: CircularProgressIndicator())
: SizedBox(
height: MediaQuery.sizeOf(context).height,
child: ListView(
children: [
SizedBox(
height: MediaQuery.of(context).size.height * 0.05,
),
),
),
),
),
const SizedBox(height: 20),
SizedBox(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
IntrinsicWidth(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 200),
child: TextFormField(
maxLength: 30,
style: const TextStyle(
color: Colors.black,
),
textAlign: TextAlign.center,
focusNode: profileBloc.focusNode,
controller: profileBloc.nameController,
enabled: profileBloc.editName,
onEditingComplete: () {
profileBloc.add(SaveNameEvent(context: context));
},
decoration: const InputDecoration(
hintText: "Your Name",
border: InputBorder.none,
fillColor: Colors.white10,
counterText: '',
InkWell(
onTap: () {
profileBloc.add(SelectImageEvent(context: context, isSelected: false));
},
child: SizedBox.square(
dimension: 125,
child: CircleAvatar(
backgroundColor: Colors.white,
child: SizedBox.square(
dimension: 120,
child: CircleAvatar(
backgroundColor: Colors.white,
backgroundImage: profileBloc.image == null
? null
: FileImage(profileBloc.image!),
child: profileBloc.image != null
? null
: HomeCubit.user!.profilePicture != null
? ClipOval(
child: Image.memory(
HomeCubit.user!.profilePicture!,
fit: BoxFit.cover,
width: 120,
height: 120,
),
)
: null,
),
),
),
),
),
),
const SizedBox(width: 5),
InkWell(
onTap: () {
profileBloc.add(const ChangeNameEvent(value: true));
},
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 10),
child: Icon(
Icons.edit_outlined,
size: 20,
color: ColorsManager.textPrimaryColor,
),
),
),
],
),
),
const SizedBox(height: 10),
DefaultContainer(
padding: const EdgeInsets.symmetric(
horizontal: 25,
vertical: 5,
),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(top: 20, bottom: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const BodyMedium(text: 'email '),
Flexible(child: BodyMedium(text: HomeCubit.user!.email ?? 'No Email')),
],
),
),
Container(
height: 1,
color: ColorsManager.greyColor,
),
InkWell(
onTap: () {
profileBloc.add(const ChangeNameEvent(value: false));
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const RegionPage(),
),
).then((result) {
profileBloc.add(InitialProfileEvent());
});
},
child: Padding(
padding: const EdgeInsets.only(top: 20, bottom: 20),
const SizedBox(height: 20),
SizedBox(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const BodyMedium(text: 'Region '),
Flexible(child: BodyMedium(text: HomeCubit.user!.regionName ?? 'No Region')),
],
),
),
),
Container(
height: 1,
color: ColorsManager.greyColor,
),
InkWell(
onTap: () {
profileBloc.add(const ChangeNameEvent(value: false));
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const TimeZoneScreenPage(),
),
).then((result) {
profileBloc.add(InitialProfileEvent());
});
},
child: Padding(
padding: const EdgeInsets.only(top: 15, bottom: 15),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const BodyMedium(text: 'Time Zone '),
Flexible(
child: BodyMedium(text: HomeCubit.user!.timeZone ?? "No Time Zone"),
IntrinsicWidth(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 200),
child: TextFormField(
maxLength: 30,
style: const TextStyle(
color: Colors.black,
),
textAlign: TextAlign.center,
focusNode: profileBloc.focusNode,
controller: profileBloc.nameController,
enabled: profileBloc.editName,
onEditingComplete: () {
profileBloc.add(SaveNameEvent(context: context));
},
decoration: const InputDecoration(
hintText: "Your Name",
border: InputBorder.none,
fillColor: Colors.white10,
counterText: '',
),
),
),
),
const SizedBox(width: 5),
InkWell(
onTap: () {
profileBloc.add(const ChangeNameEvent(value: true));
},
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 10),
child: Icon(
Icons.edit_outlined,
size: 20,
color: ColorsManager.textPrimaryColor,
),
),
),
],
),
),
),
],
)
),
],
),
const SizedBox(height: 10),
DefaultContainer(
padding: const EdgeInsets.symmetric(
horizontal: 25,
vertical: 5,
),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(top: 20, bottom: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const BodyMedium(text: 'email '),
Flexible(
child: BodyMedium(
text: HomeCubit.user!.email ?? 'No Email')),
],
),
),
Container(
height: 1,
color: ColorsManager.greyColor,
),
InkWell(
onTap: () {
profileBloc.add(const ChangeNameEvent(value: false));
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const RegionPage(),
),
).then((result) {
profileBloc.add(InitialProfileEvent());
});
},
child: Padding(
padding: const EdgeInsets.only(top: 20, bottom: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const BodyMedium(text: 'Region '),
Flexible(
child: BodyMedium(
text: HomeCubit.user!.regionName ?? 'No Region')),
],
),
),
),
Container(
height: 1,
color: ColorsManager.greyColor,
),
InkWell(
onTap: () {
profileBloc.add(const ChangeNameEvent(value: false));
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const TimeZoneScreenPage(),
),
).then((result) {
profileBloc.add(InitialProfileEvent());
});
},
child: Padding(
padding: const EdgeInsets.only(top: 15, bottom: 15),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const BodyMedium(text: 'Time Zone '),
Flexible(
child: BodyMedium(
text: HomeCubit.user!.timeZone ?? "No Time Zone"),
),
],
),
),
),
],
)),
],
),
),
);
},
),

View File

@ -5,7 +5,7 @@ description: This is the mobile application project, developed with Flutter for
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: "none" # Remove this line if you wish to publish to pub.dev
version: 1.0.2+15
version: 1.0.2+16
environment:
sdk: ">=3.0.6 <4.0.0"