feat: Enable users to upload attire photos via camera or gallery.
This commit is contained in:
@@ -12,6 +12,7 @@ import 'package:staff_authentication/staff_authentication.dart'
|
|||||||
import 'package:staff_main/staff_main.dart' as staff_main;
|
import 'package:staff_main/staff_main.dart' as staff_main;
|
||||||
import 'package:krow_core/core.dart';
|
import 'package:krow_core/core.dart';
|
||||||
|
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'src/widgets/session_listener.dart';
|
import 'src/widgets/session_listener.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
@@ -26,7 +27,10 @@ void main() async {
|
|||||||
|
|
||||||
// Initialize session listener for Firebase Auth state changes
|
// Initialize session listener for Firebase Auth state changes
|
||||||
DataConnectService.instance.initializeAuthListener(
|
DataConnectService.instance.initializeAuthListener(
|
||||||
allowedRoles: <String>['STAFF', 'BOTH'], // Only allow users with STAFF or BOTH roles
|
allowedRoles: <String>[
|
||||||
|
'STAFF',
|
||||||
|
'BOTH',
|
||||||
|
], // Only allow users with STAFF or BOTH roles
|
||||||
);
|
);
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
@@ -40,11 +44,22 @@ void main() async {
|
|||||||
/// The main application module.
|
/// The main application module.
|
||||||
class AppModule extends Module {
|
class AppModule extends Module {
|
||||||
@override
|
@override
|
||||||
List<Module> get imports =>
|
void binds(Injector i) {
|
||||||
<Module>[
|
i.addLazySingleton<ImagePicker>(ImagePicker.new);
|
||||||
core_localization.LocalizationModule(),
|
i.addLazySingleton<CameraService>(
|
||||||
staff_authentication.StaffAuthenticationModule(),
|
() => CameraService(i.get<ImagePicker>()),
|
||||||
];
|
);
|
||||||
|
i.addLazySingleton<GalleryService>(
|
||||||
|
() => GalleryService(i.get<ImagePicker>()),
|
||||||
|
);
|
||||||
|
i.addLazySingleton<FilePickerService>(FilePickerService.new);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Module> get imports => <Module>[
|
||||||
|
core_localization.LocalizationModule(),
|
||||||
|
staff_authentication.StaffAuthenticationModule(),
|
||||||
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void routes(RouteManager r) {
|
void routes(RouteManager r) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'package:krow_domain/krow_domain.dart';
|
|||||||
/// Service for capturing photos and videos using the device camera.
|
/// Service for capturing photos and videos using the device camera.
|
||||||
class CameraService extends BaseDeviceService {
|
class CameraService extends BaseDeviceService {
|
||||||
/// Creates a [CameraService].
|
/// Creates a [CameraService].
|
||||||
CameraService(this._picker);
|
CameraService(ImagePicker picker) : _picker = picker;
|
||||||
|
|
||||||
final ImagePicker _picker;
|
final ImagePicker _picker;
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class AttireRepositoryImpl implements AttireRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String> uploadPhoto(String itemId) async {
|
Future<String> uploadPhoto(String itemId, String filePath) async {
|
||||||
// In a real app, this would upload to Firebase Storage first.
|
// In a real app, this would upload to Firebase Storage first.
|
||||||
// Since the prototype returns a mock URL, we'll use that to upsert our record.
|
// Since the prototype returns a mock URL, we'll use that to upsert our record.
|
||||||
final String mockUrl = 'mock_url_for_$itemId';
|
final String mockUrl = 'mock_url_for_$itemId';
|
||||||
|
|||||||
@@ -7,10 +7,17 @@ class UploadAttirePhotoArguments extends UseCaseArgument {
|
|||||||
// We'll stick to that signature for now to "preserve behavior".
|
// We'll stick to that signature for now to "preserve behavior".
|
||||||
|
|
||||||
/// Creates a [UploadAttirePhotoArguments].
|
/// Creates a [UploadAttirePhotoArguments].
|
||||||
const UploadAttirePhotoArguments({required this.itemId});
|
const UploadAttirePhotoArguments({
|
||||||
|
required this.itemId,
|
||||||
|
required this.filePath,
|
||||||
|
});
|
||||||
|
|
||||||
/// The ID of the attire item being uploaded.
|
/// The ID of the attire item being uploaded.
|
||||||
final String itemId;
|
final String itemId;
|
||||||
|
|
||||||
|
/// The local path to the photo file.
|
||||||
|
final String filePath;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => <Object?>[itemId];
|
List<Object?> get props => <Object?>[itemId, filePath];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ abstract interface class AttireRepository {
|
|||||||
/// Fetches the list of available attire options.
|
/// Fetches the list of available attire options.
|
||||||
Future<List<AttireItem>> getAttireOptions();
|
Future<List<AttireItem>> getAttireOptions();
|
||||||
|
|
||||||
/// Simulates uploading a photo for a specific attire item.
|
/// Uploads a photo for a specific attire item.
|
||||||
Future<String> uploadPhoto(String itemId);
|
Future<String> uploadPhoto(String itemId, String filePath);
|
||||||
|
|
||||||
/// Saves the user's attire selection and attestations.
|
/// Saves the user's attire selection and attestations.
|
||||||
Future<void> saveAttire({
|
Future<void> saveAttire({
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ import '../arguments/upload_attire_photo_arguments.dart';
|
|||||||
import '../repositories/attire_repository.dart';
|
import '../repositories/attire_repository.dart';
|
||||||
|
|
||||||
/// Use case to upload a photo for an attire item.
|
/// Use case to upload a photo for an attire item.
|
||||||
class UploadAttirePhotoUseCase extends UseCase<UploadAttirePhotoArguments, String> {
|
class UploadAttirePhotoUseCase
|
||||||
|
extends UseCase<UploadAttirePhotoArguments, String> {
|
||||||
/// Creates a [UploadAttirePhotoUseCase].
|
/// Creates a [UploadAttirePhotoUseCase].
|
||||||
UploadAttirePhotoUseCase(this._repository);
|
UploadAttirePhotoUseCase(this._repository);
|
||||||
final AttireRepository _repository;
|
final AttireRepository _repository;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String> call(UploadAttirePhotoArguments arguments) {
|
Future<String> call(UploadAttirePhotoArguments arguments) {
|
||||||
return _repository.uploadPhoto(arguments.itemId);
|
return _repository.uploadPhoto(arguments.itemId, arguments.filePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,14 +16,14 @@ class AttireCaptureCubit extends Cubit<AttireCaptureState>
|
|||||||
emit(state.copyWith(isAttested: value));
|
emit(state.copyWith(isAttested: value));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> uploadPhoto(String itemId) async {
|
Future<void> uploadPhoto(String itemId, String filePath) async {
|
||||||
emit(state.copyWith(status: AttireCaptureStatus.uploading));
|
emit(state.copyWith(status: AttireCaptureStatus.uploading));
|
||||||
|
|
||||||
await handleError(
|
await handleError(
|
||||||
emit: emit,
|
emit: emit,
|
||||||
action: () async {
|
action: () async {
|
||||||
final String url = await _uploadAttirePhotoUseCase(
|
final String url = await _uploadAttirePhotoUseCase(
|
||||||
UploadAttirePhotoArguments(itemId: itemId),
|
UploadAttirePhotoArguments(itemId: itemId, filePath: filePath),
|
||||||
);
|
);
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:design_system/design_system.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
import 'package:staff_attire/src/presentation/blocs/attire_capture/attire_capture_cubit.dart';
|
import 'package:staff_attire/src/presentation/blocs/attire_capture/attire_capture_cubit.dart';
|
||||||
import 'package:staff_attire/src/presentation/blocs/attire_capture/attire_capture_state.dart';
|
import 'package:staff_attire/src/presentation/blocs/attire_capture/attire_capture_state.dart';
|
||||||
@@ -27,21 +28,71 @@ class AttireCapturePage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _AttireCapturePageState extends State<AttireCapturePage> {
|
class _AttireCapturePageState extends State<AttireCapturePage> {
|
||||||
void _onUpload(BuildContext context) {
|
/// On gallery button press
|
||||||
|
Future<void> _onGallery(BuildContext context) async {
|
||||||
final AttireCaptureCubit cubit = BlocProvider.of<AttireCaptureCubit>(
|
final AttireCaptureCubit cubit = BlocProvider.of<AttireCaptureCubit>(
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!cubit.state.isAttested) {
|
if (!cubit.state.isAttested) {
|
||||||
UiSnackbar.show(
|
_showAttestationWarning(context);
|
||||||
context,
|
|
||||||
message: 'Please attest that you own this item.',
|
|
||||||
type: UiSnackbarType.error,
|
|
||||||
margin: const EdgeInsets.all(UiConstants.space4),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Call the upload via cubit
|
|
||||||
cubit.uploadPhoto(widget.item.id);
|
try {
|
||||||
|
final GalleryService service = Modular.get<GalleryService>();
|
||||||
|
final String? path = await service.pickImage();
|
||||||
|
if (path != null && context.mounted) {
|
||||||
|
await cubit.uploadPhoto(widget.item.id, path);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (context.mounted) {
|
||||||
|
_showError(context, 'Could not access gallery: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// On camera button press
|
||||||
|
Future<void> _onCamera(BuildContext context) async {
|
||||||
|
final AttireCaptureCubit cubit = BlocProvider.of<AttireCaptureCubit>(
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!cubit.state.isAttested) {
|
||||||
|
_showAttestationWarning(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final CameraService service = Modular.get<CameraService>();
|
||||||
|
final String? path = await service.takePhoto();
|
||||||
|
if (path != null && context.mounted) {
|
||||||
|
await cubit.uploadPhoto(widget.item.id, path);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (context.mounted) {
|
||||||
|
_showError(context, 'Could not access camera: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showAttestationWarning(BuildContext context) {
|
||||||
|
UiSnackbar.show(
|
||||||
|
context,
|
||||||
|
message: 'Please attest that you own this item.',
|
||||||
|
type: UiSnackbarType.error,
|
||||||
|
margin: const EdgeInsets.all(UiConstants.space4),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showError(BuildContext context, String message) {
|
||||||
|
debugPrint(message);
|
||||||
|
UiSnackbar.show(
|
||||||
|
context,
|
||||||
|
message: 'Could not access camera or gallery. Please try again.',
|
||||||
|
type: UiSnackbarType.error,
|
||||||
|
margin: const EdgeInsets.all(UiConstants.space4),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -174,7 +225,10 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
AttireUploadButtons(onUpload: _onUpload),
|
AttireUploadButtons(
|
||||||
|
onGallery: () => _onGallery(context),
|
||||||
|
onCamera: () => _onCamera(context),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -2,9 +2,14 @@ import 'package:design_system/design_system.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class AttireUploadButtons extends StatelessWidget {
|
class AttireUploadButtons extends StatelessWidget {
|
||||||
const AttireUploadButtons({super.key, required this.onUpload});
|
const AttireUploadButtons({
|
||||||
|
super.key,
|
||||||
|
required this.onGallery,
|
||||||
|
required this.onCamera,
|
||||||
|
});
|
||||||
|
|
||||||
final void Function(BuildContext) onUpload;
|
final VoidCallback onGallery;
|
||||||
|
final VoidCallback onCamera;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -14,7 +19,7 @@ class AttireUploadButtons extends StatelessWidget {
|
|||||||
child: UiButton.secondary(
|
child: UiButton.secondary(
|
||||||
leadingIcon: UiIcons.gallery,
|
leadingIcon: UiIcons.gallery,
|
||||||
text: 'Gallery',
|
text: 'Gallery',
|
||||||
onPressed: () => onUpload(context),
|
onPressed: onGallery,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: UiConstants.space4),
|
const SizedBox(width: UiConstants.space4),
|
||||||
@@ -22,7 +27,7 @@ class AttireUploadButtons extends StatelessWidget {
|
|||||||
child: UiButton.primary(
|
child: UiButton.primary(
|
||||||
leadingIcon: UiIcons.camera,
|
leadingIcon: UiIcons.camera,
|
||||||
text: 'Camera',
|
text: 'Camera',
|
||||||
onPressed: () => onUpload(context),
|
onPressed: onCamera,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user