From a517a28daf41f62405658b60d1cf01309528a95e Mon Sep 17 00:00:00 2001 From: Vadim <44135514+vodemn@users.noreply.github.com> Date: Wed, 9 Jul 2025 21:48:24 +0200 Subject: [PATCH] implemented `LogbookScreen` --- lib/providers/logbook_photos_provider.dart | 32 +++++++- lib/screens/logbook/screen_logbook.dart | 75 +++++++++++++++++++ lib/screens/metering/bloc_metering.dart | 26 ++++++- .../bloc_communication_metering.dart | 2 +- .../event_communication_metering.dart | 11 ++- .../state_communication_metering.dart | 11 ++- .../bloc_container_camera.dart | 14 ++-- .../mock_bloc_container_camera.part.dart | 4 +- lib/screens/metering/event_metering.dart | 7 +- lib/screens/metering/flow_metering.dart | 2 + 10 files changed, 160 insertions(+), 24 deletions(-) create mode 100644 lib/screens/logbook/screen_logbook.dart diff --git a/lib/providers/logbook_photos_provider.dart b/lib/providers/logbook_photos_provider.dart index 4d1890f..f8c35a2 100644 --- a/lib/providers/logbook_photos_provider.dart +++ b/lib/providers/logbook_photos_provider.dart @@ -1,8 +1,12 @@ +import 'dart:io'; + import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; +import 'package:lightmeter/providers/films_provider.dart'; import 'package:lightmeter/utils/context_utils.dart'; import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; +import 'package:uuid/v8.dart'; class LogbookPhotosProvider extends StatefulWidget { final LogbookPhotosStorageService storageService; @@ -52,10 +56,29 @@ class LogbookPhotosProviderState extends State { widget.onInitialized?.call(); } - Future addPhoto(LogbookPhoto photo) async { - await widget.storageService.addPhoto(photo); - _photos[photo.id] = photo; - setState(() {}); + Future addPhotoIfPossible( + String path, { + required double ev100, + required int iso, + required int nd, + }) async { + if (context.isPro) { + final photo = LogbookPhoto( + id: const UuidV8().generate(), + name: path, + timestamp: DateTime.timestamp(), + ev: ev100, + iso: iso, + nd: nd, + film: Films.selectedOf(context), + coordinates: null, // TODO + ); + //await widget.storageService.addPhoto(photo); + _photos[photo.id] = photo; + setState(() {}); + } else { + Directory(path).deleteSync(recursive: true); + } } Future updateProfile(LogbookPhoto photo) async { @@ -73,6 +96,7 @@ class LogbookPhotosProviderState extends State { Future deleteProfile(LogbookPhoto photo) async { await widget.storageService.deletePhoto(photo.id); _photos.remove(photo.id); + Directory(photo.name).deleteSync(recursive: true); setState(() {}); } } diff --git a/lib/screens/logbook/screen_logbook.dart b/lib/screens/logbook/screen_logbook.dart new file mode 100644 index 0000000..399542a --- /dev/null +++ b/lib/screens/logbook/screen_logbook.dart @@ -0,0 +1,75 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/navigation/routes.dart'; +import 'package:lightmeter/platform_config.dart'; +import 'package:lightmeter/providers/equipment_profile_provider.dart'; +import 'package:lightmeter/providers/logbook_photos_provider.dart'; +import 'package:lightmeter/res/dimens.dart'; +import 'package:lightmeter/screens/equipment_profile_edit/flow_equipment_profile_edit.dart'; +import 'package:lightmeter/screens/shared/sliver_placeholder/widget_sliver_placeholder.dart'; +import 'package:lightmeter/screens/shared/sliver_screen/screen_sliver.dart'; +import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; + +class LogbookScreen extends StatefulWidget { + const LogbookScreen({super.key}); + + @override + State createState() => _LogbookScreenState(); +} + +class _LogbookScreenState extends State with SingleTickerProviderStateMixin { + @override + Widget build(BuildContext context) { + return SliverScreen( + title: Text("Logbook"), + slivers: [ + _PicturesGridBuilder( + values: LogbookPhotos.of(context), + onEdit: _editProfile, + ), + SliverToBoxAdapter( + child: SizedBox(height: MediaQuery.paddingOf(context).bottom), + ), + ], + ); + } + + void _editProfile(LogbookPhoto photo) {} +} + +class _PicturesGridBuilder extends StatelessWidget { + final List values; + final void Function(LogbookPhoto photo) onEdit; + + static const int _crossAxisCount = 3; + + const _PicturesGridBuilder({ + required this.values, + required this.onEdit, + }); + + @override + Widget build(BuildContext context) { + return SliverGrid( + gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: + (MediaQuery.sizeOf(context).width - Dimens.paddingS * (_crossAxisCount - 1)) / _crossAxisCount, + mainAxisSpacing: Dimens.paddingS, + crossAxisSpacing: Dimens.paddingS, + childAspectRatio: PlatformConfig.cameraPreviewAspectRatio, + ), + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + return Container( + alignment: Alignment.center, + color: Colors.teal[100 * (index % 9)], + child: Image.file(File(values[index].name)), + ); + }, + childCount: values.length, + ), + ); + } +} diff --git a/lib/screens/metering/bloc_metering.dart b/lib/screens/metering/bloc_metering.dart index 6c3ab15..b72b487 100644 --- a/lib/screens/metering/bloc_metering.dart +++ b/lib/screens/metering/bloc_metering.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:lightmeter/data/models/volume_action.dart'; import 'package:lightmeter/interactors/metering_interactor.dart'; +import 'package:lightmeter/providers/logbook_photos_provider.dart'; import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart'; import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart' as communication_events; import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart' as communication_states; @@ -17,12 +18,14 @@ class MeteringBloc extends Bloc { final MeteringInteractor _meteringInteractor; final VolumeKeysNotifier _volumeKeysNotifier; final MeteringCommunicationBloc _communicationBloc; + final LogbookPhotosProviderState _logbookPhotosProvider; late final StreamSubscription _communicationSubscription; MeteringBloc( this._meteringInteractor, this._volumeKeysNotifier, this._communicationBloc, + this._logbookPhotosProvider, ) : super( MeteringDataState( ev100: null, @@ -74,9 +77,14 @@ class MeteringBloc extends Bloc { @visibleForTesting void onCommunicationState(communication_states.ScreenState communicationState) { if (communicationState is communication_states.MeasuredState) { + String? photoPath; + if (communicationState case final communication_states.MeteringEndedState state) { + photoPath = state.photoPath; + } _handleEv100( communicationState.ev100, isMetering: communicationState is communication_states.MeteringInProgressState, + photoPath: photoPath, ); } } @@ -152,15 +160,29 @@ class MeteringBloc extends Bloc { ); } - void _handleEv100(double? ev100, {required bool isMetering}) { + void _handleEv100(double? ev100, {required bool isMetering, String? photoPath}) { if (ev100 == null || ev100.isNaN || ev100.isInfinite) { add(MeasureErrorEvent(isMetering: isMetering)); } else { - add(MeasuredEvent(ev100, isMetering: isMetering)); + add( + MeasuredEvent( + ev100, + isMetering: isMetering, + photoPath: photoPath, + ), + ); } } void _onMeasured(MeasuredEvent event, Emitter emit) { + if (event.photoPath case final path?) { + _logbookPhotosProvider.addPhotoIfPossible( + path, + ev100: event.ev100, + iso: state.iso.value, + nd: state.nd.value, + ); + } emit( MeteringDataState( ev100: event.ev100, diff --git a/lib/screens/metering/communication/bloc_communication_metering.dart b/lib/screens/metering/communication/bloc_communication_metering.dart index 40dcd73..3b7b9a5 100644 --- a/lib/screens/metering/communication/bloc_communication_metering.dart +++ b/lib/screens/metering/communication/bloc_communication_metering.dart @@ -10,7 +10,7 @@ class MeteringCommunicationBloc extends Bloc((_, emit) => emit(MeasureState())); on((event, emit) => emit(EquipmentProfileChangedState(event.profile))); on((event, emit) => emit(MeteringInProgressState(event.ev100))); - on((event, emit) => emit(MeteringEndedState(event.ev100))); + on((event, emit) => emit(MeteringEndedState(event.ev100, photoPath: event.photoPath))); on((_, emit) => emit(const SettingsOpenedState())); on((_, emit) => emit(const SettingsClosedState())); } diff --git a/lib/screens/metering/communication/event_communication_metering.dart b/lib/screens/metering/communication/event_communication_metering.dart index 8c12656..439bb13 100644 --- a/lib/screens/metering/communication/event_communication_metering.dart +++ b/lib/screens/metering/communication/event_communication_metering.dart @@ -45,17 +45,22 @@ class MeteringInProgressEvent extends MeasuredEvent { } class MeteringEndedEvent extends MeasuredEvent { - const MeteringEndedEvent(super.ev100); + const MeteringEndedEvent( + super.ev100, { + this.photoPath, + }); + + final String? photoPath; @override bool operator ==(Object other) { if (identical(this, other)) return true; if (other.runtimeType != runtimeType) return false; - return other is MeteringEndedEvent && other.ev100 == ev100; + return other is MeteringEndedEvent && other.ev100 == ev100 && other.photoPath == photoPath; } @override - int get hashCode => Object.hash(ev100, runtimeType); + int get hashCode => Object.hash(runtimeType, ev100, photoPath); } class ScreenOnTopOpenedEvent extends ScreenEvent { diff --git a/lib/screens/metering/communication/state_communication_metering.dart b/lib/screens/metering/communication/state_communication_metering.dart index 1090bdc..f208a2c 100644 --- a/lib/screens/metering/communication/state_communication_metering.dart +++ b/lib/screens/metering/communication/state_communication_metering.dart @@ -47,17 +47,22 @@ class MeteringInProgressState extends MeasuredState { } class MeteringEndedState extends MeasuredState { - const MeteringEndedState(super.ev100); + const MeteringEndedState( + super.ev100, { + this.photoPath, + }); + + final String? photoPath; @override bool operator ==(Object other) { if (identical(this, other)) return true; if (other.runtimeType != runtimeType) return false; - return other is MeteringEndedState && other.ev100 == ev100; + return other is MeteringEndedState && other.ev100 == ev100 && other.photoPath == photoPath; } @override - int get hashCode => Object.hash(ev100, runtimeType); + int get hashCode => Object.hash(runtimeType, ev100, photoPath); } class SettingsOpenedState extends SourceState { diff --git a/lib/screens/metering/components/camera_container/bloc_container_camera.dart b/lib/screens/metering/components/camera_container/bloc_container_camera.dart index a5da36b..7e8897d 100644 --- a/lib/screens/metering/components/camera_container/bloc_container_camera.dart +++ b/lib/screens/metering/components/camera_container/bloc_container_camera.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:developer'; -import 'dart:io'; import 'dart:math' as math; import 'package:camera/camera.dart'; @@ -77,10 +76,10 @@ class CameraContainerBloc extends EvSourceBlocBase _takePhoto() async { + Future<({double ev, String path})?> _takePhoto() async { try { final file = await _cameraController!.takePicture(); final bytes = await file.readAsBytes(); - Directory(file.path).deleteSync(recursive: true); final tags = await readExifFromBytes(bytes); - return evFromTags(tags); + return (ev: evFromTags(tags), path: file.path); } catch (e, stackTrace) { _analytics.logCrash(e, stackTrace); return null; diff --git a/lib/screens/metering/components/camera_container/mock_bloc_container_camera.part.dart b/lib/screens/metering/components/camera_container/mock_bloc_container_camera.part.dart index ff908cb..183877e 100644 --- a/lib/screens/metering/components/camera_container/mock_bloc_container_camera.part.dart +++ b/lib/screens/metering/components/camera_container/mock_bloc_container_camera.part.dart @@ -69,11 +69,11 @@ class MockCameraContainerBloc extends CameraContainerBloc { bool get _canTakePhoto => PlatformConfig.cameraStubImage.isNotEmpty; @override - Future _takePhoto() async { + Future<({double ev, String path})?> _takePhoto() async { try { final bytes = (await rootBundle.load(PlatformConfig.cameraStubImage)).buffer.asUint8List(); final tags = await readExifFromBytes(bytes); - return evFromTags(tags); + return (ev: evFromTags(tags), path: PlatformConfig.cameraStubImage); } catch (e, stackTrace) { log(e.toString(), stackTrace: stackTrace); return null; diff --git a/lib/screens/metering/event_metering.dart b/lib/screens/metering/event_metering.dart index 107eac2..f6f8120 100644 --- a/lib/screens/metering/event_metering.dart +++ b/lib/screens/metering/event_metering.dart @@ -29,8 +29,13 @@ class MeasureEvent extends MeteringEvent { class MeasuredEvent extends MeteringEvent { final double ev100; final bool isMetering; + final String? photoPath; - const MeasuredEvent(this.ev100, {required this.isMetering}); + const MeasuredEvent( + this.ev100, { + required this.isMetering, + this.photoPath, + }); } class MeasureErrorEvent extends MeteringEvent { diff --git a/lib/screens/metering/flow_metering.dart b/lib/screens/metering/flow_metering.dart index f78482d..1e0a252 100644 --- a/lib/screens/metering/flow_metering.dart +++ b/lib/screens/metering/flow_metering.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:lightmeter/interactors/metering_interactor.dart'; +import 'package:lightmeter/providers/logbook_photos_provider.dart'; import 'package:lightmeter/providers/services_provider.dart'; import 'package:lightmeter/screens/metering/bloc_metering.dart'; import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart'; @@ -34,6 +35,7 @@ class _MeteringFlowState extends State { MeteringInteractorProvider.of(context), VolumeKeysNotifier(ServicesProvider.of(context).volumeEventsService), context.read(), + LogbookPhotosProvider.of(context), ), ), ],