feat: legacy mobile apps created
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
const String getStaffLivePhotoSchema = '''
|
||||
query GetLivePhoto {
|
||||
me {
|
||||
id
|
||||
live_photo
|
||||
live_photo_obj {
|
||||
status
|
||||
url
|
||||
uploaded_at
|
||||
}
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String uploadStaffLivePhotoSchema = '''
|
||||
mutation UploadStaffLivePhoto(\$file: Upload!) {
|
||||
upload_staff_live_photo(file: \$file) {
|
||||
id
|
||||
live_photo
|
||||
live_photo_obj {
|
||||
status
|
||||
url
|
||||
uploaded_at
|
||||
}
|
||||
}
|
||||
}
|
||||
''';
|
||||
@@ -0,0 +1,66 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
@immutable
|
||||
class LivePhotoData {
|
||||
const LivePhotoData({
|
||||
required this.id,
|
||||
required this.imageUrl,
|
||||
required this.imagePath,
|
||||
required this.status,
|
||||
required this.timestamp,
|
||||
});
|
||||
|
||||
factory LivePhotoData.fromJson(Map<String, dynamic> json) {
|
||||
// TODO: For now live_photo_obj returns a placeholder from the backend.
|
||||
final livePhotoObj = json['live_photo_obj'] as Map<String, dynamic>;
|
||||
|
||||
return LivePhotoData(
|
||||
id: json['id'] as String? ?? '',
|
||||
imageUrl: json['live_photo'] as String? ?? '',
|
||||
imagePath: null,
|
||||
status: LivePhotoStatus.fromString(
|
||||
livePhotoObj['status'] as String? ?? '',
|
||||
),
|
||||
timestamp: DateTime.tryParse(
|
||||
livePhotoObj['uploaded_at'] as String? ?? '',
|
||||
) ??
|
||||
DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
final String? id;
|
||||
final String? imageUrl;
|
||||
final String? imagePath;
|
||||
final LivePhotoStatus status;
|
||||
final DateTime timestamp;
|
||||
|
||||
LivePhotoData copyWith({
|
||||
String? id,
|
||||
String? imageUrl,
|
||||
String? imagePath,
|
||||
LivePhotoStatus? status,
|
||||
DateTime? timestamp,
|
||||
}) {
|
||||
return LivePhotoData(
|
||||
id: id ?? this.id,
|
||||
imageUrl: imageUrl ?? this.imageUrl,
|
||||
imagePath: imagePath ?? this.imagePath,
|
||||
status: status ?? this.status,
|
||||
timestamp: timestamp ?? this.timestamp,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum LivePhotoStatus {
|
||||
pending,
|
||||
verified,
|
||||
declined;
|
||||
|
||||
static fromString(String value) {
|
||||
return switch (value) {
|
||||
'pending' => pending,
|
||||
'verified' => verified,
|
||||
_ => declined,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:http/http.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:krow/core/application/clients/api/api_client.dart';
|
||||
import 'package:krow/features/profile/live_photo/data/gql_schemas.dart';
|
||||
import 'package:krow/features/profile/live_photo/data/models/live_photo_data.dart';
|
||||
|
||||
@injectable
|
||||
class StaffLivePhotoApiProvider {
|
||||
StaffLivePhotoApiProvider(this._client);
|
||||
|
||||
final ApiClient _client;
|
||||
|
||||
Stream<LivePhotoData> getStaffLivePhotoWithCache() async* {
|
||||
await for (var response in _client.queryWithCache(
|
||||
schema: getStaffLivePhotoSchema,
|
||||
)) {
|
||||
if (response == null || response.data == null) continue;
|
||||
|
||||
if (response.hasException) {
|
||||
throw Exception(response.exception.toString());
|
||||
}
|
||||
|
||||
try {
|
||||
yield LivePhotoData.fromJson(
|
||||
response.data?['me'] as Map<String, dynamic>,
|
||||
);
|
||||
} catch (except) {
|
||||
log(
|
||||
'Exception in StaffInclusivityApiProvider '
|
||||
'on getStaffLivePhotoWithCache()',
|
||||
error: except,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<LivePhotoData> uploadStaffLivePhoto(LivePhotoData data) async {
|
||||
var result = await _client.mutate(
|
||||
schema: uploadStaffLivePhotoSchema,
|
||||
body: {
|
||||
'file': await MultipartFile.fromPath(
|
||||
'file',
|
||||
data.imagePath ?? '',
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
if (result.hasException) {
|
||||
throw Exception(result.exception.toString());
|
||||
}
|
||||
|
||||
if (result.data == null || result.data!.isEmpty) {
|
||||
throw Exception('Missing Live photo response from server');
|
||||
}
|
||||
|
||||
return LivePhotoData.fromJson(
|
||||
result.data?['upload_staff_live_photo'] as Map<String, dynamic>,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:krow/features/profile/live_photo/data/models/live_photo_data.dart';
|
||||
import 'package:krow/features/profile/live_photo/data/staff_live_photo_api_provider.dart';
|
||||
import 'package:krow/features/profile/live_photo/domain/staff_live_photo_repository.dart';
|
||||
|
||||
@Injectable(as: StaffLivePhotoRepository)
|
||||
class StaffLivePhotoRepositoryImpl implements StaffLivePhotoRepository {
|
||||
StaffLivePhotoRepositoryImpl({
|
||||
required StaffLivePhotoApiProvider apiProvider,
|
||||
}) : _apiProvider = apiProvider;
|
||||
|
||||
final StaffLivePhotoApiProvider _apiProvider;
|
||||
|
||||
@override
|
||||
Stream<LivePhotoData> getStaffLivePhotoWithCache() {
|
||||
return _apiProvider.getStaffLivePhotoWithCache();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<LivePhotoData> uploadStaffLivePhoto(LivePhotoData data) {
|
||||
return _apiProvider.uploadStaffLivePhoto(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow/core/application/di/injectable.dart';
|
||||
import 'package:krow/core/data/enums/state_status.dart';
|
||||
import 'package:krow/features/profile/live_photo/data/models/live_photo_data.dart';
|
||||
import 'package:krow/features/profile/live_photo/domain/staff_live_photo_repository.dart';
|
||||
|
||||
part 'live_photo_event.dart';
|
||||
|
||||
part 'live_photo_state.dart';
|
||||
|
||||
class LivePhotoBloc extends Bloc<LivePhotoEvent, LivePhotoState> {
|
||||
LivePhotoBloc() : super(const LivePhotoState()) {
|
||||
on<InitLivePhotoBloc>((event, emit) async {
|
||||
emit(state.copyWith(status: StateStatus.loading));
|
||||
|
||||
try {
|
||||
await for (final photoData
|
||||
in _repository.getStaffLivePhotoWithCache()) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: StateStatus.idle,
|
||||
photoData: photoData,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (except) {
|
||||
log(
|
||||
except.toString(),
|
||||
error: except,
|
||||
);
|
||||
}
|
||||
|
||||
emit(state.copyWith(status: StateStatus.idle));
|
||||
});
|
||||
|
||||
on<AddNewPhotoEvent>((event, emit) async {
|
||||
final photoData = LivePhotoData(
|
||||
id: null,
|
||||
imageUrl: null,
|
||||
imagePath: event.newImagePath,
|
||||
status: LivePhotoStatus.pending,
|
||||
timestamp: DateTime.now(),
|
||||
);
|
||||
|
||||
emit(state.copyWith(photoData: photoData));
|
||||
|
||||
try {
|
||||
await _repository.uploadStaffLivePhoto(photoData);
|
||||
} catch (except) {
|
||||
log(
|
||||
except.toString(),
|
||||
error: except,
|
||||
);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
photoData: state.photoData?.copyWith(
|
||||
status: LivePhotoStatus.declined,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
on<DeleteCurrentPhotoEvent>((event, emit) async {
|
||||
emit(state.removeCurrentPhoto(status: StateStatus.loading));
|
||||
|
||||
//TODO: Add photo deletion once backend implements the mutation
|
||||
await Future<void>.delayed(const Duration(seconds: 1));
|
||||
|
||||
emit(state.copyWith(status: StateStatus.idle));
|
||||
});
|
||||
}
|
||||
|
||||
final StaffLivePhotoRepository _repository =
|
||||
getIt<StaffLivePhotoRepository>();
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
part of 'live_photo_bloc.dart';
|
||||
|
||||
@immutable
|
||||
sealed class LivePhotoEvent {
|
||||
const LivePhotoEvent();
|
||||
}
|
||||
|
||||
class InitLivePhotoBloc extends LivePhotoEvent {
|
||||
const InitLivePhotoBloc();
|
||||
}
|
||||
|
||||
class AddNewPhotoEvent extends LivePhotoEvent {
|
||||
const AddNewPhotoEvent({required this.newImagePath});
|
||||
|
||||
final String newImagePath;
|
||||
}
|
||||
|
||||
class DeleteCurrentPhotoEvent extends LivePhotoEvent {
|
||||
const DeleteCurrentPhotoEvent();
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
part of 'live_photo_bloc.dart';
|
||||
|
||||
@immutable
|
||||
class LivePhotoState {
|
||||
const LivePhotoState({
|
||||
this.status = StateStatus.idle,
|
||||
this.photoData,
|
||||
});
|
||||
|
||||
final StateStatus status;
|
||||
final LivePhotoData? photoData;
|
||||
|
||||
LivePhotoState copyWith({
|
||||
StateStatus? status,
|
||||
LivePhotoData? photoData,
|
||||
}) {
|
||||
return LivePhotoState(
|
||||
status: status ?? this.status,
|
||||
photoData: photoData ?? this.photoData,
|
||||
);
|
||||
}
|
||||
|
||||
LivePhotoState removeCurrentPhoto({
|
||||
StateStatus? status,
|
||||
}) {
|
||||
return LivePhotoState(
|
||||
status: status ?? this.status,
|
||||
photoData: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import 'package:krow/features/profile/live_photo/data/models/live_photo_data.dart';
|
||||
|
||||
abstract class StaffLivePhotoRepository {
|
||||
Stream<LivePhotoData> getStaffLivePhotoWithCache();
|
||||
|
||||
Future<LivePhotoData> uploadStaffLivePhoto(LivePhotoData data);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/data/enums/state_status.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_app_bar.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_loading_overlay.dart';
|
||||
import 'package:krow/features/profile/live_photo/domain/bloc/live_photo_bloc.dart';
|
||||
import 'package:krow/features/profile/live_photo/presentation/widgets/live_photo_display_card.dart';
|
||||
import 'package:krow/features/profile/live_photo/presentation/widgets/photo_requirements_card.dart';
|
||||
|
||||
@RoutePage()
|
||||
class LivePhotoScreen extends StatelessWidget implements AutoRouteWrapper {
|
||||
const LivePhotoScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget wrappedRoute(BuildContext context) {
|
||||
return BlocProvider<LivePhotoBloc>(
|
||||
create: (context) => LivePhotoBloc()..add(const InitLivePhotoBloc()),
|
||||
child: this,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: KwAppBar(
|
||||
titleText: 'live_photo'.tr(),
|
||||
showNotification: true,
|
||||
),
|
||||
body: ListView(
|
||||
primary: false,
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
BlocBuilder<LivePhotoBloc, LivePhotoState>(
|
||||
buildWhen: (previous, current) => previous.status != current.status,
|
||||
builder: (context, state) {
|
||||
return KwLoadingOverlay(
|
||||
shouldShowLoading: state.status == StateStatus.loading,
|
||||
child: const PhotoRequirementsCard(),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Gap(24),
|
||||
BlocBuilder<LivePhotoBloc, LivePhotoState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.photoData != current.photoData,
|
||||
builder: (context, state) {
|
||||
final photoData = state.photoData;
|
||||
return AnimatedSwitcher(
|
||||
duration: Durations.short4,
|
||||
child: photoData == null
|
||||
? const SizedBox(height: 56)
|
||||
: LivePhotoDisplayCard(photoData: photoData),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Gap(90),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:krow/core/application/common/date_time_extension.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/uploud_image_card.dart';
|
||||
import 'package:krow/features/profile/live_photo/domain/bloc/live_photo_bloc.dart';
|
||||
import 'package:krow/features/profile/live_photo/data/models/live_photo_data.dart';
|
||||
|
||||
class LivePhotoDisplayCard extends StatelessWidget {
|
||||
const LivePhotoDisplayCard({
|
||||
super.key,
|
||||
required this.photoData,
|
||||
});
|
||||
|
||||
final LivePhotoData photoData;
|
||||
|
||||
String _getPhotoStatus(LivePhotoStatus status) {
|
||||
return switch (status) {
|
||||
LivePhotoStatus.pending => 'Pending'.tr(),
|
||||
LivePhotoStatus.verified => 'Verified'.tr(),
|
||||
LivePhotoStatus.declined => 'Declined'.tr(),
|
||||
};
|
||||
}
|
||||
|
||||
Color _getPhotoStatusColor(LivePhotoStatus status) {
|
||||
return switch (status) {
|
||||
LivePhotoStatus.pending => AppColors.primaryBlue,
|
||||
LivePhotoStatus.verified => AppColors.statusSuccess,
|
||||
LivePhotoStatus.declined => AppColors.statusError,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UploadImageCard(
|
||||
title: '${'Photo'.tr()} ${photoData.timestamp.toDayMonthYearString()}',
|
||||
imageUrl: photoData.imageUrl,
|
||||
localImagePath: photoData.imagePath,
|
||||
message: _getPhotoStatus(photoData.status),
|
||||
statusColor: _getPhotoStatusColor(photoData.status),
|
||||
onDeleteTap: () {
|
||||
context.read<LivePhotoBloc>().add(const DeleteCurrentPhotoEvent());
|
||||
},
|
||||
onSelectImage: () {
|
||||
ImagePicker()
|
||||
.pickImage(
|
||||
source: ImageSource.camera,
|
||||
preferredCameraDevice: CameraDevice.front,
|
||||
)
|
||||
.then(
|
||||
(photo) {
|
||||
if (photo == null || !context.mounted) return;
|
||||
|
||||
context.read<LivePhotoBloc>().add(
|
||||
AddNewPhotoEvent(newImagePath: photo.path),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
onTap: () {},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
|
||||
import 'package:krow/features/profile/live_photo/domain/bloc/live_photo_bloc.dart';
|
||||
|
||||
class PhotoRequirementsCard extends StatelessWidget {
|
||||
const PhotoRequirementsCard({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.grayWhite,
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'ensure_meet_requirements'.tr(),
|
||||
style: AppTextStyles.bodyMediumMed,
|
||||
),
|
||||
const Gap(12),
|
||||
Column(
|
||||
spacing: 8,
|
||||
children: [
|
||||
for (int i = 0; i < requirementsData.length; i++)
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
height: 20,
|
||||
width: 20,
|
||||
alignment: Alignment.center,
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: AppColors.bgColorDark,
|
||||
),
|
||||
child: Text(
|
||||
'${i + 1}',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Poppins',
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 8,
|
||||
color: AppColors.grayWhite,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
requirementsData[i].tr(),
|
||||
style: AppTextStyles.bodySmallReg,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(16),
|
||||
BlocSelector<LivePhotoBloc, LivePhotoState, bool>(
|
||||
selector: (state) => state.photoData != null,
|
||||
builder: (context, isPhotoTaken) {
|
||||
return KwButton.primary(
|
||||
label: isPhotoTaken?'take_new_photo'.tr():'take_photo'.tr(),
|
||||
onPressed: () {
|
||||
ImagePicker()
|
||||
.pickImage(
|
||||
source: ImageSource.camera,
|
||||
preferredCameraDevice: CameraDevice.front,
|
||||
)
|
||||
.then(
|
||||
(photo) {
|
||||
if (photo == null || !context.mounted) return;
|
||||
|
||||
context.read<LivePhotoBloc>().add(
|
||||
AddNewPhotoEvent(newImagePath: photo.path),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const requirementsData = [
|
||||
'stand_in_well_lit_area',
|
||||
'ensure_face_visible',
|
||||
'avoid_filters_obstructions',
|
||||
];
|
||||
Reference in New Issue
Block a user