feat: Enable users to upload attire photos via camera or gallery.

This commit is contained in:
Achintha Isuru
2026-02-25 12:58:30 -05:00
parent 19b82ff73a
commit ed2b4f0563
9 changed files with 112 additions and 31 deletions

View File

@@ -4,7 +4,7 @@ import 'package:krow_domain/krow_domain.dart';
/// Service for capturing photos and videos using the device camera.
class CameraService extends BaseDeviceService {
/// Creates a [CameraService].
CameraService(this._picker);
CameraService(ImagePicker picker) : _picker = picker;
final ImagePicker _picker;

View File

@@ -31,7 +31,7 @@ class AttireRepositoryImpl implements AttireRepository {
}
@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.
// Since the prototype returns a mock URL, we'll use that to upsert our record.
final String mockUrl = 'mock_url_for_$itemId';

View File

@@ -7,10 +7,17 @@ class UploadAttirePhotoArguments extends UseCaseArgument {
// We'll stick to that signature for now to "preserve behavior".
/// Creates a [UploadAttirePhotoArguments].
const UploadAttirePhotoArguments({required this.itemId});
const UploadAttirePhotoArguments({
required this.itemId,
required this.filePath,
});
/// The ID of the attire item being uploaded.
final String itemId;
/// The local path to the photo file.
final String filePath;
@override
List<Object?> get props => <Object?>[itemId];
List<Object?> get props => <Object?>[itemId, filePath];
}

View File

@@ -4,8 +4,8 @@ abstract interface class AttireRepository {
/// Fetches the list of available attire options.
Future<List<AttireItem>> getAttireOptions();
/// Simulates uploading a photo for a specific attire item.
Future<String> uploadPhoto(String itemId);
/// Uploads a photo for a specific attire item.
Future<String> uploadPhoto(String itemId, String filePath);
/// Saves the user's attire selection and attestations.
Future<void> saveAttire({

View File

@@ -3,14 +3,14 @@ import '../arguments/upload_attire_photo_arguments.dart';
import '../repositories/attire_repository.dart';
/// 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].
UploadAttirePhotoUseCase(this._repository);
final AttireRepository _repository;
@override
Future<String> call(UploadAttirePhotoArguments arguments) {
return _repository.uploadPhoto(arguments.itemId);
return _repository.uploadPhoto(arguments.itemId, arguments.filePath);
}
}

View File

@@ -16,14 +16,14 @@ class AttireCaptureCubit extends Cubit<AttireCaptureState>
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));
await handleError(
emit: emit,
action: () async {
final String url = await _uploadAttirePhotoUseCase(
UploadAttirePhotoArguments(itemId: itemId),
UploadAttirePhotoArguments(itemId: itemId, filePath: filePath),
);
emit(

View File

@@ -3,6 +3,7 @@ import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.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_state.dart';
@@ -27,21 +28,71 @@ class AttireCapturePage extends StatefulWidget {
}
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>(
context,
);
if (!cubit.state.isAttested) {
UiSnackbar.show(
context,
message: 'Please attest that you own this item.',
type: UiSnackbarType.error,
margin: const EdgeInsets.all(UiConstants.space4),
);
_showAttestationWarning(context);
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
@@ -174,7 +225,10 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
),
)
else
AttireUploadButtons(onUpload: _onUpload),
AttireUploadButtons(
onGallery: () => _onGallery(context),
onCamera: () => _onCamera(context),
),
],
),
),

View File

@@ -2,9 +2,14 @@ import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
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
Widget build(BuildContext context) {
@@ -14,7 +19,7 @@ class AttireUploadButtons extends StatelessWidget {
child: UiButton.secondary(
leadingIcon: UiIcons.gallery,
text: 'Gallery',
onPressed: () => onUpload(context),
onPressed: onGallery,
),
),
const SizedBox(width: UiConstants.space4),
@@ -22,7 +27,7 @@ class AttireUploadButtons extends StatelessWidget {
child: UiButton.primary(
leadingIcon: UiIcons.camera,
text: 'Camera',
onPressed: () => onUpload(context),
onPressed: onCamera,
),
),
],