mirror of
https://github.com/vodemn/m3_lightmeter.git
synced 2025-08-05 04:36:41 +00:00
implemented LogbookScreen
This commit is contained in:
parent
73c1c0d66e
commit
a517a28daf
10 changed files with 160 additions and 24 deletions
|
@ -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<LogbookPhotosProvider> {
|
|||
widget.onInitialized?.call();
|
||||
}
|
||||
|
||||
Future<void> addPhoto(LogbookPhoto photo) async {
|
||||
await widget.storageService.addPhoto(photo);
|
||||
_photos[photo.id] = photo;
|
||||
setState(() {});
|
||||
Future<void> 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<void> updateProfile(LogbookPhoto photo) async {
|
||||
|
@ -73,6 +96,7 @@ class LogbookPhotosProviderState extends State<LogbookPhotosProvider> {
|
|||
Future<void> deleteProfile(LogbookPhoto photo) async {
|
||||
await widget.storageService.deletePhoto(photo.id);
|
||||
_photos.remove(photo.id);
|
||||
Directory(photo.name).deleteSync(recursive: true);
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
|
75
lib/screens/logbook/screen_logbook.dart
Normal file
75
lib/screens/logbook/screen_logbook.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<MeteringEvent, MeteringState> {
|
|||
final MeteringInteractor _meteringInteractor;
|
||||
final VolumeKeysNotifier _volumeKeysNotifier;
|
||||
final MeteringCommunicationBloc _communicationBloc;
|
||||
final LogbookPhotosProviderState _logbookPhotosProvider;
|
||||
late final StreamSubscription<communication_states.ScreenState> _communicationSubscription;
|
||||
|
||||
MeteringBloc(
|
||||
this._meteringInteractor,
|
||||
this._volumeKeysNotifier,
|
||||
this._communicationBloc,
|
||||
this._logbookPhotosProvider,
|
||||
) : super(
|
||||
MeteringDataState(
|
||||
ev100: null,
|
||||
|
@ -74,9 +77,14 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
|
|||
@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<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) {
|
||||
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,
|
||||
|
|
|
@ -10,7 +10,7 @@ class MeteringCommunicationBloc extends Bloc<MeteringCommunicationEvent, Meterin
|
|||
on<MeasureEvent>((_, emit) => emit(MeasureState()));
|
||||
on<EquipmentProfileChangedEvent>((event, emit) => emit(EquipmentProfileChangedState(event.profile)));
|
||||
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<ScreenOnTopClosedEvent>((_, emit) => emit(const SettingsClosedState()));
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<CameraContainerEvent, CameraC
|
|||
switch (communicationState) {
|
||||
case communication_states.MeasureState():
|
||||
if (_canTakePhoto) {
|
||||
_takePhoto().then((ev100Raw) {
|
||||
if (ev100Raw != null) {
|
||||
_ev100 = ev100Raw + _meteringInteractor.cameraEvCalibration;
|
||||
communicationBloc.add(communication_event.MeteringEndedEvent(_ev100));
|
||||
_takePhoto().then((photo) {
|
||||
if (photo != null) {
|
||||
_ev100 = photo.ev + _meteringInteractor.cameraEvCalibration;
|
||||
communicationBloc.add(communication_event.MeteringEndedEvent(_ev100, photoPath: photo.path));
|
||||
} else {
|
||||
_ev100 = null;
|
||||
communicationBloc.add(const communication_event.MeteringEndedEvent(null));
|
||||
|
@ -244,13 +243,12 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
|
|||
!_cameraController!.value.isInitialized ||
|
||||
_cameraController!.value.isTakingPicture);
|
||||
|
||||
Future<double?> _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;
|
||||
|
|
|
@ -69,11 +69,11 @@ class MockCameraContainerBloc extends CameraContainerBloc {
|
|||
bool get _canTakePhoto => PlatformConfig.cameraStubImage.isNotEmpty;
|
||||
|
||||
@override
|
||||
Future<double?> _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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<MeteringFlow> {
|
|||
MeteringInteractorProvider.of(context),
|
||||
VolumeKeysNotifier(ServicesProvider.of(context).volumeEventsService),
|
||||
context.read<MeteringCommunicationBloc>(),
|
||||
LogbookPhotosProvider.of(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
Loading…
Reference in a new issue