implemented LogbookScreen

This commit is contained in:
Vadim 2025-07-09 21:48:24 +02:00
parent 73c1c0d66e
commit a517a28daf
10 changed files with 160 additions and 24 deletions

View file

@ -1,8 +1,12 @@
import 'dart:io';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lightmeter/providers/films_provider.dart';
import 'package:lightmeter/utils/context_utils.dart'; import 'package:lightmeter/utils/context_utils.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
import 'package:uuid/v8.dart';
class LogbookPhotosProvider extends StatefulWidget { class LogbookPhotosProvider extends StatefulWidget {
final LogbookPhotosStorageService storageService; final LogbookPhotosStorageService storageService;
@ -52,10 +56,29 @@ class LogbookPhotosProviderState extends State<LogbookPhotosProvider> {
widget.onInitialized?.call(); widget.onInitialized?.call();
} }
Future<void> addPhoto(LogbookPhoto photo) async { Future<void> addPhotoIfPossible(
await widget.storageService.addPhoto(photo); String path, {
_photos[photo.id] = photo; required double ev100,
setState(() {}); 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<void> updateProfile(LogbookPhoto photo) async { Future<void> updateProfile(LogbookPhoto photo) async {
@ -73,6 +96,7 @@ class LogbookPhotosProviderState extends State<LogbookPhotosProvider> {
Future<void> deleteProfile(LogbookPhoto photo) async { Future<void> deleteProfile(LogbookPhoto photo) async {
await widget.storageService.deletePhoto(photo.id); await widget.storageService.deletePhoto(photo.id);
_photos.remove(photo.id); _photos.remove(photo.id);
Directory(photo.name).deleteSync(recursive: true);
setState(() {}); setState(() {});
} }
} }

View file

@ -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<LogbookScreen> createState() => _LogbookScreenState();
}
class _LogbookScreenState extends State<LogbookScreen> 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<LogbookPhoto> 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,
),
);
}
}

View file

@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/data/models/volume_action.dart'; import 'package:lightmeter/data/models/volume_action.dart';
import 'package:lightmeter/interactors/metering_interactor.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/bloc_communication_metering.dart';
import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart' as communication_events; 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; import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart' as communication_states;
@ -17,12 +18,14 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
final MeteringInteractor _meteringInteractor; final MeteringInteractor _meteringInteractor;
final VolumeKeysNotifier _volumeKeysNotifier; final VolumeKeysNotifier _volumeKeysNotifier;
final MeteringCommunicationBloc _communicationBloc; final MeteringCommunicationBloc _communicationBloc;
final LogbookPhotosProviderState _logbookPhotosProvider;
late final StreamSubscription<communication_states.ScreenState> _communicationSubscription; late final StreamSubscription<communication_states.ScreenState> _communicationSubscription;
MeteringBloc( MeteringBloc(
this._meteringInteractor, this._meteringInteractor,
this._volumeKeysNotifier, this._volumeKeysNotifier,
this._communicationBloc, this._communicationBloc,
this._logbookPhotosProvider,
) : super( ) : super(
MeteringDataState( MeteringDataState(
ev100: null, ev100: null,
@ -74,9 +77,14 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
@visibleForTesting @visibleForTesting
void onCommunicationState(communication_states.ScreenState communicationState) { void onCommunicationState(communication_states.ScreenState communicationState) {
if (communicationState is communication_states.MeasuredState) { if (communicationState is communication_states.MeasuredState) {
String? photoPath;
if (communicationState case final communication_states.MeteringEndedState state) {
photoPath = state.photoPath;
}
_handleEv100( _handleEv100(
communicationState.ev100, communicationState.ev100,
isMetering: communicationState is communication_states.MeteringInProgressState, isMetering: communicationState is communication_states.MeteringInProgressState,
photoPath: photoPath,
); );
} }
} }
@ -152,15 +160,29 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
); );
} }
void _handleEv100(double? ev100, {required bool isMetering}) { void _handleEv100(double? ev100, {required bool isMetering, String? photoPath}) {
if (ev100 == null || ev100.isNaN || ev100.isInfinite) { if (ev100 == null || ev100.isNaN || ev100.isInfinite) {
add(MeasureErrorEvent(isMetering: isMetering)); add(MeasureErrorEvent(isMetering: isMetering));
} else { } else {
add(MeasuredEvent(ev100, isMetering: isMetering)); add(
MeasuredEvent(
ev100,
isMetering: isMetering,
photoPath: photoPath,
),
);
} }
} }
void _onMeasured(MeasuredEvent event, Emitter emit) { 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( emit(
MeteringDataState( MeteringDataState(
ev100: event.ev100, ev100: event.ev100,

View file

@ -10,7 +10,7 @@ class MeteringCommunicationBloc extends Bloc<MeteringCommunicationEvent, Meterin
on<MeasureEvent>((_, emit) => emit(MeasureState())); on<MeasureEvent>((_, emit) => emit(MeasureState()));
on<EquipmentProfileChangedEvent>((event, emit) => emit(EquipmentProfileChangedState(event.profile))); on<EquipmentProfileChangedEvent>((event, emit) => emit(EquipmentProfileChangedState(event.profile)));
on<MeteringInProgressEvent>((event, emit) => emit(MeteringInProgressState(event.ev100))); on<MeteringInProgressEvent>((event, emit) => emit(MeteringInProgressState(event.ev100)));
on<MeteringEndedEvent>((event, emit) => emit(MeteringEndedState(event.ev100))); on<MeteringEndedEvent>((event, emit) => emit(MeteringEndedState(event.ev100, photoPath: event.photoPath)));
on<ScreenOnTopOpenedEvent>((_, emit) => emit(const SettingsOpenedState())); on<ScreenOnTopOpenedEvent>((_, emit) => emit(const SettingsOpenedState()));
on<ScreenOnTopClosedEvent>((_, emit) => emit(const SettingsClosedState())); on<ScreenOnTopClosedEvent>((_, emit) => emit(const SettingsClosedState()));
} }

View file

@ -45,17 +45,22 @@ class MeteringInProgressEvent extends MeasuredEvent {
} }
class MeteringEndedEvent extends MeasuredEvent { class MeteringEndedEvent extends MeasuredEvent {
const MeteringEndedEvent(super.ev100); const MeteringEndedEvent(
super.ev100, {
this.photoPath,
});
final String? photoPath;
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
if (identical(this, other)) return true; if (identical(this, other)) return true;
if (other.runtimeType != runtimeType) return false; if (other.runtimeType != runtimeType) return false;
return other is MeteringEndedEvent && other.ev100 == ev100; return other is MeteringEndedEvent && other.ev100 == ev100 && other.photoPath == photoPath;
} }
@override @override
int get hashCode => Object.hash(ev100, runtimeType); int get hashCode => Object.hash(runtimeType, ev100, photoPath);
} }
class ScreenOnTopOpenedEvent extends ScreenEvent { class ScreenOnTopOpenedEvent extends ScreenEvent {

View file

@ -47,17 +47,22 @@ class MeteringInProgressState extends MeasuredState {
} }
class MeteringEndedState extends MeasuredState { class MeteringEndedState extends MeasuredState {
const MeteringEndedState(super.ev100); const MeteringEndedState(
super.ev100, {
this.photoPath,
});
final String? photoPath;
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
if (identical(this, other)) return true; if (identical(this, other)) return true;
if (other.runtimeType != runtimeType) return false; if (other.runtimeType != runtimeType) return false;
return other is MeteringEndedState && other.ev100 == ev100; return other is MeteringEndedState && other.ev100 == ev100 && other.photoPath == photoPath;
} }
@override @override
int get hashCode => Object.hash(ev100, runtimeType); int get hashCode => Object.hash(runtimeType, ev100, photoPath);
} }
class SettingsOpenedState extends SourceState { class SettingsOpenedState extends SourceState {

View file

@ -1,6 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:developer'; import 'dart:developer';
import 'dart:io';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:camera/camera.dart'; import 'package:camera/camera.dart';
@ -77,10 +76,10 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
switch (communicationState) { switch (communicationState) {
case communication_states.MeasureState(): case communication_states.MeasureState():
if (_canTakePhoto) { if (_canTakePhoto) {
_takePhoto().then((ev100Raw) { _takePhoto().then((photo) {
if (ev100Raw != null) { if (photo != null) {
_ev100 = ev100Raw + _meteringInteractor.cameraEvCalibration; _ev100 = photo.ev + _meteringInteractor.cameraEvCalibration;
communicationBloc.add(communication_event.MeteringEndedEvent(_ev100)); communicationBloc.add(communication_event.MeteringEndedEvent(_ev100, photoPath: photo.path));
} else { } else {
_ev100 = null; _ev100 = null;
communicationBloc.add(const communication_event.MeteringEndedEvent(null)); communicationBloc.add(const communication_event.MeteringEndedEvent(null));
@ -244,13 +243,12 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
!_cameraController!.value.isInitialized || !_cameraController!.value.isInitialized ||
_cameraController!.value.isTakingPicture); _cameraController!.value.isTakingPicture);
Future<double?> _takePhoto() async { Future<({double ev, String path})?> _takePhoto() async {
try { try {
final file = await _cameraController!.takePicture(); final file = await _cameraController!.takePicture();
final bytes = await file.readAsBytes(); final bytes = await file.readAsBytes();
Directory(file.path).deleteSync(recursive: true);
final tags = await readExifFromBytes(bytes); final tags = await readExifFromBytes(bytes);
return evFromTags(tags); return (ev: evFromTags(tags), path: file.path);
} catch (e, stackTrace) { } catch (e, stackTrace) {
_analytics.logCrash(e, stackTrace); _analytics.logCrash(e, stackTrace);
return null; return null;

View file

@ -69,11 +69,11 @@ class MockCameraContainerBloc extends CameraContainerBloc {
bool get _canTakePhoto => PlatformConfig.cameraStubImage.isNotEmpty; bool get _canTakePhoto => PlatformConfig.cameraStubImage.isNotEmpty;
@override @override
Future<double?> _takePhoto() async { Future<({double ev, String path})?> _takePhoto() async {
try { try {
final bytes = (await rootBundle.load(PlatformConfig.cameraStubImage)).buffer.asUint8List(); final bytes = (await rootBundle.load(PlatformConfig.cameraStubImage)).buffer.asUint8List();
final tags = await readExifFromBytes(bytes); final tags = await readExifFromBytes(bytes);
return evFromTags(tags); return (ev: evFromTags(tags), path: PlatformConfig.cameraStubImage);
} catch (e, stackTrace) { } catch (e, stackTrace) {
log(e.toString(), stackTrace: stackTrace); log(e.toString(), stackTrace: stackTrace);
return null; return null;

View file

@ -29,8 +29,13 @@ class MeasureEvent extends MeteringEvent {
class MeasuredEvent extends MeteringEvent { class MeasuredEvent extends MeteringEvent {
final double ev100; final double ev100;
final bool isMetering; 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 { class MeasureErrorEvent extends MeteringEvent {

View file

@ -1,6 +1,7 @@
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:lightmeter/interactors/metering_interactor.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/providers/services_provider.dart';
import 'package:lightmeter/screens/metering/bloc_metering.dart'; import 'package:lightmeter/screens/metering/bloc_metering.dart';
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart'; import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
@ -34,6 +35,7 @@ class _MeteringFlowState extends State<MeteringFlow> {
MeteringInteractorProvider.of(context), MeteringInteractorProvider.of(context),
VolumeKeysNotifier(ServicesProvider.of(context).volumeEventsService), VolumeKeysNotifier(ServicesProvider.of(context).volumeEventsService),
context.read<MeteringCommunicationBloc>(), context.read<MeteringCommunicationBloc>(),
LogbookPhotosProvider.of(context),
), ),
), ),
], ],