feat: Enable users to upload attire photos via camera or gallery.
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user