implemented CRUD for custom films

This commit is contained in:
Vadim 2024-10-25 13:01:37 +02:00
parent 9884a6147f
commit 68cecf5391
12 changed files with 207 additions and 59 deletions

View file

@ -50,6 +50,7 @@ class Application extends StatelessWidget {
NavigationRoutes.meteringScreen.name: (_) => const ReleaseNotesFlow(child: MeteringFlow()), NavigationRoutes.meteringScreen.name: (_) => const ReleaseNotesFlow(child: MeteringFlow()),
NavigationRoutes.settingsScreen.name: (_) => const SettingsFlow(), NavigationRoutes.settingsScreen.name: (_) => const SettingsFlow(),
NavigationRoutes.filmsListScreen.name: (_) => const FilmsScreen(), NavigationRoutes.filmsListScreen.name: (_) => const FilmsScreen(),
NavigationRoutes.filmAddScreen.name: (_) => const FilmEditFlow(args: FilmEditArgs()),
NavigationRoutes.filmEditScreen.name: (context) => FilmEditFlow(args: context.routeArgs<FilmEditArgs>()), NavigationRoutes.filmEditScreen.name: (context) => FilmEditFlow(args: context.routeArgs<FilmEditArgs>()),
NavigationRoutes.proFeaturesScreen.name: (_) => LightmeterProScreen(), NavigationRoutes.proFeaturesScreen.name: (_) => LightmeterProScreen(),
NavigationRoutes.timerScreen.name: (context) => TimerFlow(args: context.routeArgs<TimerFlowArgs>()), NavigationRoutes.timerScreen.name: (context) => TimerFlow(args: context.routeArgs<TimerFlowArgs>()),

View file

@ -2,6 +2,7 @@ enum NavigationRoutes {
meteringScreen, meteringScreen,
settingsScreen, settingsScreen,
filmsListScreen, filmsListScreen,
filmAddScreen,
filmEditScreen, filmEditScreen,
proFeaturesScreen, proFeaturesScreen,
timerScreen, timerScreen,

View file

@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lightmeter/utils/context_utils.dart'; import 'package:lightmeter/utils/context_utils.dart';
import 'package:lightmeter/utils/selectable_provider.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';
@ -25,13 +24,23 @@ class FilmsProvider extends StatefulWidget {
} }
class FilmsProviderState extends State<FilmsProvider> { class FilmsProviderState extends State<FilmsProvider> {
late List<Film> _filmsInUse; late final Map<String, _SelectableFilm<Film>> predefinedFilms = Map.fromEntries(
(widget.availableFilms ?? films).map(
(film) => MapEntry(
film.id,
(
film: film,
selected: widget.storageService.filmsInUse.contains(film),
),
),
),
);
final Map<String, _SelectableFilm<FilmExponential>> customFilms = {};
late Film _selected; late Film _selected;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_filmsInUse = widget.storageService.filmsInUse;
_selected = widget.storageService.selectedFilm; _selected = widget.storageService.selectedFilm;
_discardSelectedIfNotIncluded(); _discardSelectedIfNotIncluded();
} }
@ -39,20 +48,34 @@ class FilmsProviderState extends State<FilmsProvider> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Films( return Films(
values: [ predefinedFilms: predefinedFilms,
const FilmStub(), customFilms: customFilms,
...widget.availableFilms ?? films,
],
filmsInUse: [
const FilmStub(),
if (context.isPro) ..._filmsInUse,
],
selected: context.isPro ? _selected : const FilmStub(), selected: context.isPro ? _selected : const FilmStub(),
child: widget.child, child: widget.child,
); );
} }
void setFilm(Film film) { /* Both type of films **/
void toggleFilm(Film film, bool enabled) {
Film? targetFilm = predefinedFilms[film.id]?.film;
if (targetFilm != null) {
predefinedFilms[film.id] = (film: film, selected: enabled);
_discardSelectedIfNotIncluded();
setState(() {});
return;
}
targetFilm = customFilms[film.id]?.film;
if (targetFilm != null) {
customFilms[film.id] = (film: film as FilmExponential, selected: enabled);
_discardSelectedIfNotIncluded();
setState(() {});
return;
}
}
void selectFilm(Film film) {
if (_selected != film) { if (_selected != film) {
_selected = film; _selected = film;
widget.storageService.selectedFilm = film; widget.storageService.selectedFilm = film;
@ -60,47 +83,99 @@ class FilmsProviderState extends State<FilmsProvider> {
} }
} }
void saveFilms(List<Film> films) { /* Custom films **/
_filmsInUse = films;
widget.storageService.filmsInUse = films; void addCustomFilm(FilmExponential film) {
customFilms[film.id] = (film: film, selected: false);
setState(() {});
}
void updateCustomFilm(FilmExponential film) {
customFilms[film.id] = (film: film, selected: customFilms[film.id]!.selected);
setState(() {});
}
// TODO: add delete button to UI
void deleteCustomFilm(FilmExponential film) {
customFilms.remove(film.id);
_discardSelectedIfNotIncluded(); _discardSelectedIfNotIncluded();
setState(() {}); setState(() {});
} }
void _discardSelectedIfNotIncluded() { void _discardSelectedIfNotIncluded() {
if (_selected != const FilmStub() && !_filmsInUse.contains(_selected)) { if (_selected != const FilmStub() &&
!predefinedFilms.values.any((e) => e.film == _selected) &&
!customFilms.values.any((e) => e.film == _selected)) {
_selected = const FilmStub(); _selected = const FilmStub();
widget.storageService.selectedFilm = const FilmStub(); widget.storageService.selectedFilm = const FilmStub();
} }
} }
} }
class Films extends SelectableInheritedModel<Film> { typedef _SelectableFilm<T extends Film> = ({T film, bool selected});
final List<Film> filmsInUse;
enum _FilmsModelAspect {
customFilmsList,
predefinedFilmsList,
filmsInUse,
selected,
}
class Films extends InheritedModel<_FilmsModelAspect> {
final Map<String, _SelectableFilm<Film>> predefinedFilms;
@protected
final Map<String, _SelectableFilm<FilmExponential>> customFilms;
final Film selected;
const Films({ const Films({
super.key, required this.predefinedFilms,
required super.values, required this.customFilms,
required this.filmsInUse, required this.selected,
required super.selected,
required super.child, required super.child,
}); });
/// [FilmStub()] + all the custom fields with actual reciprocity formulas static List<Film> predefinedFilmsOf<T>(BuildContext context) {
static List<Film> of(BuildContext context) { return InheritedModel.inheritFrom<Films>(context, aspect: _FilmsModelAspect.predefinedFilmsList)!
return InheritedModel.inheritFrom<Films>(context)!.values; .predefinedFilms
.values
.map((value) => value.film)
.toList();
}
static List<FilmExponential> customFilmsOf<T>(BuildContext context) {
return InheritedModel.inheritFrom<Films>(context, aspect: _FilmsModelAspect.customFilmsList)!
.customFilms
.values
.map((value) => value.film)
.toList();
} }
/// [FilmStub()] + films in use selected by user /// [FilmStub()] + films in use selected by user
static List<Film> inUseOf<T>(BuildContext context) { static List<Film> inUseOf<T>(BuildContext context) {
return InheritedModel.inheritFrom<Films>( final model = InheritedModel.inheritFrom<Films>(context, aspect: _FilmsModelAspect.filmsInUse)!;
context, return [
aspect: SelectableAspect.list, const FilmStub(),
)! ...model.customFilms.values.where((e) => e.selected).map((e) => e.film),
.filmsInUse; ...model.predefinedFilms.values.where((e) => e.selected).map((e) => e.film),
];
} }
static Film selectedOf(BuildContext context) { static Film selectedOf(BuildContext context) {
return InheritedModel.inheritFrom<Films>(context, aspect: SelectableAspect.selected)!.selected; return InheritedModel.inheritFrom<Films>(context, aspect: _FilmsModelAspect.selected)!.selected;
}
@override
bool updateShouldNotify(Films _) => true;
@override
bool updateShouldNotifyDependent(Films oldWidget, Set<_FilmsModelAspect> dependencies) {
if (dependencies.contains(_FilmsModelAspect.customFilmsList)) {}
if (dependencies.contains(_FilmsModelAspect.selected)) {
return selected != oldWidget.selected;
} else {
// TODO: reduce unnecessary notifications
return true;
}
} }
} }

View file

@ -1,17 +1,40 @@
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/providers/films_provider.dart';
import 'package:lightmeter/screens/film_edit/event_film_edit.dart'; import 'package:lightmeter/screens/film_edit/event_film_edit.dart';
import 'package:lightmeter/screens/film_edit/state_film_edit.dart'; import 'package:lightmeter/screens/film_edit/state_film_edit.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
import 'package:uuid/uuid.dart';
class FilmEditBloc extends Bloc<FilmEditEvent, FilmEditState> { class FilmEditBloc extends Bloc<FilmEditEvent, FilmEditState> {
static const _defaultFilm = FilmExponential(name: '', iso: 100, exponent: 1.3); static const _defaultFilm = FilmExponential(name: '', iso: 100, exponent: 1.3);
final FilmsProviderState filmsProvider;
final FilmExponential _originalFilm; final FilmExponential _originalFilm;
FilmExponential _newFilm; FilmExponential _newFilm;
final bool _isEdit;
factory FilmEditBloc(FilmExponential? film) => film != null ? FilmEditBloc._(film) : FilmEditBloc._(_defaultFilm); factory FilmEditBloc(
FilmsProviderState filmsProvider, {
required FilmExponential? film,
required bool isEdit,
}) =>
film != null
? FilmEditBloc._(
filmsProvider,
film,
isEdit,
)
: FilmEditBloc._(
filmsProvider,
_defaultFilm,
isEdit,
);
FilmEditBloc._(FilmExponential film) FilmEditBloc._(
: _originalFilm = film, this.filmsProvider,
FilmExponential film,
this._isEdit,
) : _originalFilm = film,
_newFilm = film, _newFilm = film,
super( super(
FilmEditState( FilmEditState(
@ -19,7 +42,6 @@ class FilmEditBloc extends Bloc<FilmEditEvent, FilmEditState> {
isoValue: IsoValue.values.firstWhere((element) => element.value == film.iso), isoValue: IsoValue.values.firstWhere((element) => element.value == film.iso),
exponent: film.exponent, exponent: film.exponent,
canSave: false, canSave: false,
isEdit: film != _defaultFilm,
), ),
) { ) {
on<FilmEditEvent>( on<FilmEditEvent>(
@ -33,6 +55,8 @@ class FilmEditBloc extends Bloc<FilmEditEvent, FilmEditState> {
_onExpChanged(e, emit); _onExpChanged(e, emit);
case FilmEditSaveEvent(): case FilmEditSaveEvent():
_onSave(event, emit); _onSave(event, emit);
case FilmEditDeleteEvent():
_onDelete(event, emit);
} }
}, },
); );
@ -46,7 +70,6 @@ class FilmEditBloc extends Bloc<FilmEditEvent, FilmEditState> {
isoValue: state.isoValue, isoValue: state.isoValue,
exponent: state.exponent, exponent: state.exponent,
canSave: _canSave(event.name, state.exponent), canSave: _canSave(event.name, state.exponent),
isEdit: state.isEdit,
), ),
); );
} }
@ -59,7 +82,6 @@ class FilmEditBloc extends Bloc<FilmEditEvent, FilmEditState> {
isoValue: event.iso, isoValue: event.iso,
exponent: state.exponent, exponent: state.exponent,
canSave: _canSave(state.name, state.exponent), canSave: _canSave(state.name, state.exponent),
isEdit: state.isEdit,
), ),
); );
} }
@ -74,12 +96,35 @@ class FilmEditBloc extends Bloc<FilmEditEvent, FilmEditState> {
isoValue: state.isoValue, isoValue: state.isoValue,
exponent: event.exponent, exponent: event.exponent,
canSave: _canSave(state.name, event.exponent), canSave: _canSave(state.name, event.exponent),
isEdit: state.isEdit,
), ),
); );
} }
Future<void> _onSave(FilmEditSaveEvent _, Emitter emit) async {} Future<void> _onSave(FilmEditSaveEvent _, Emitter emit) async {
if (_isEdit) {
filmsProvider.updateCustomFilm(
FilmExponential(
id: _originalFilm.id,
name: state.name,
iso: state.isoValue.value,
exponent: state.exponent!,
),
);
} else {
filmsProvider.addCustomFilm(
FilmExponential(
id: const Uuid().v1(),
name: state.name,
iso: state.isoValue.value,
exponent: state.exponent!,
),
);
}
}
Future<void> _onDelete(FilmEditDeleteEvent _, Emitter emit) async {
filmsProvider.deleteCustomFilm(_originalFilm);
}
bool _canSave(String name, double? exponent) { bool _canSave(String name, double? exponent) {
return name.isNotEmpty && exponent != null && _newFilm != _originalFilm; return name.isNotEmpty && exponent != null && _newFilm != _originalFilm;

View file

@ -25,3 +25,7 @@ class FilmEditExpChangedEvent extends FilmEditEvent {
class FilmEditSaveEvent extends FilmEditEvent { class FilmEditSaveEvent extends FilmEditEvent {
const FilmEditSaveEvent(); const FilmEditSaveEvent();
} }
class FilmEditDeleteEvent extends FilmEditEvent {
const FilmEditDeleteEvent();
}

View file

@ -1,13 +1,14 @@
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/providers/films_provider.dart';
import 'package:lightmeter/screens/film_edit/bloc_film_edit.dart'; import 'package:lightmeter/screens/film_edit/bloc_film_edit.dart';
import 'package:lightmeter/screens/film_edit/screen_film_edit.dart'; import 'package:lightmeter/screens/film_edit/screen_film_edit.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class FilmEditArgs { class FilmEditArgs {
final FilmExponential film; final FilmExponential? film;
const FilmEditArgs({required this.film}); const FilmEditArgs({this.film});
} }
class FilmEditFlow extends StatelessWidget { class FilmEditFlow extends StatelessWidget {
@ -18,8 +19,12 @@ class FilmEditFlow extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (_) => FilmEditBloc(args.film), create: (_) => FilmEditBloc(
child: const FilmEditScreen(), FilmsProvider.of(context),
film: args.film,
isEdit: args.film != null,
),
child: FilmEditScreen(isEdit: args.film != null),
); );
} }
} }

View file

@ -11,7 +11,12 @@ import 'package:lightmeter/screens/film_edit/state_film_edit.dart';
import 'package:lightmeter/screens/shared/sliver_screen/screen_sliver.dart'; import 'package:lightmeter/screens/shared/sliver_screen/screen_sliver.dart';
class FilmEditScreen extends StatefulWidget { class FilmEditScreen extends StatefulWidget {
const FilmEditScreen({super.key}); final bool isEdit;
const FilmEditScreen({
required this.isEdit,
super.key,
});
@override @override
State<FilmEditScreen> createState() => _FilmEditScreenState(); State<FilmEditScreen> createState() => _FilmEditScreenState();
@ -21,18 +26,28 @@ class _FilmEditScreenState extends State<FilmEditScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SliverScreen( return SliverScreen(
title: BlocBuilder<FilmEditBloc, FilmEditState>( title: Text(widget.isEdit ? S.of(context).editFilmTitle : S.of(context).addFilmTitle),
buildWhen: (previous, current) => false,
builder: (context, state) => Text(state.isEdit ? S.of(context).editFilmTitle : S.of(context).addFilmTitle),
),
appBarActions: [ appBarActions: [
BlocBuilder<FilmEditBloc, FilmEditState>( BlocBuilder<FilmEditBloc, FilmEditState>(
buildWhen: (previous, current) => previous.canSave != current.canSave, buildWhen: (previous, current) => previous.canSave != current.canSave,
builder: (context, state) => IconButton( builder: (context, state) => IconButton(
onPressed: state.canSave ? () {} : null, onPressed: state.canSave
? () {
context.read<FilmEditBloc>().add(const FilmEditSaveEvent());
Navigator.of(context).pop();
}
: null,
icon: const Icon(Icons.save), icon: const Icon(Icons.save),
), ),
), ),
if (widget.isEdit)
IconButton(
onPressed: () {
context.read<FilmEditBloc>().add(const FilmEditDeleteEvent());
Navigator.of(context).pop();
},
icon: const Icon(Icons.delete),
),
], ],
slivers: [ slivers: [
SliverToBoxAdapter( SliverToBoxAdapter(

View file

@ -5,13 +5,13 @@ class FilmEditState {
final IsoValue isoValue; final IsoValue isoValue;
final double? exponent; final double? exponent;
final bool canSave; final bool canSave;
final bool isEdit; final FilmExponential? filmToSave;
const FilmEditState({ const FilmEditState({
required this.name, required this.name,
required this.isoValue, required this.isoValue,
required this.exponent, required this.exponent,
required this.canSave, required this.canSave,
required this.isEdit, this.filmToSave,
}); });
} }

View file

@ -55,12 +55,12 @@ class _FilmsScreenState extends State<FilmsScreen> with SingleTickerProviderStat
controller: tabController, controller: tabController,
children: [ children: [
_FilmsListBuilder( _FilmsListBuilder(
films: Films.of(context).skip(1).toList(), films: Films.predefinedFilmsOf(context).toList(),
onFilmSelected: (film, value) {}, onFilmSelected: FilmsProvider.of(context).toggleFilm,
), ),
_FilmsListBuilder<FilmExponential>( _FilmsListBuilder<FilmExponential>(
films: Films.of(context).skip(1).whereType<FilmExponential>().toList(), films: Films.customFilmsOf(context).toList(),
onFilmSelected: (film, value) {}, onFilmSelected: FilmsProvider.of(context).toggleFilm,
onFilmEdit: _editFilm, onFilmEdit: _editFilm,
), ),
], ],
@ -72,8 +72,8 @@ class _FilmsScreenState extends State<FilmsScreen> with SingleTickerProviderStat
void _addFilm() { void _addFilm() {
Navigator.of(context).pushNamed( Navigator.of(context).pushNamed(
NavigationRoutes.filmEditScreen.name, NavigationRoutes.filmAddScreen.name,
arguments: const FilmEditArgs(film: FilmExponential(name: '', iso: 100, exponent: 1.3)), arguments: const FilmEditArgs(),
); );
} }

View file

@ -19,7 +19,7 @@ class FilmPicker extends StatelessWidget {
selectedValue: Films.selectedOf(context), selectedValue: Films.selectedOf(context),
values: Films.inUseOf(context), values: Films.inUseOf(context),
itemTitleBuilder: (_, value) => Text(value.name.isEmpty ? S.of(context).none : value.name), itemTitleBuilder: (_, value) => Text(value.name.isEmpty ? S.of(context).none : value.name),
onChanged: FilmsProvider.of(context).setFilm, onChanged: FilmsProvider.of(context).selectFilm,
closedChild: ReadingValueContainer.singleValue( closedChild: ReadingValueContainer.singleValue(
value: ReadingValue( value: ReadingValue(
label: _label(context), label: _label(context),

View file

@ -39,7 +39,7 @@ class MeteringScreenLayoutListTile extends StatelessWidget {
EquipmentProfileProvider.of(context).setProfile(EquipmentProfiles.of(context).first); EquipmentProfileProvider.of(context).setProfile(EquipmentProfiles.of(context).first);
} }
if (!value[MeteringScreenLayoutFeature.filmPicker]!) { if (!value[MeteringScreenLayoutFeature.filmPicker]!) {
FilmsProvider.of(context).setFilm(const FilmStub()); FilmsProvider.of(context).selectFilm(const FilmStub());
} }
UserPreferencesProvider.of(context).setMeteringScreenLayout(value); UserPreferencesProvider.of(context).setMeteringScreenLayout(value);
}, },

View file

@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
enum SelectableAspect { list, selected } enum SelectableAspect { list, selected }
class SelectableInheritedModel<T> extends InheritedModel<SelectableAspect> { class SelectableInheritedModel<T> extends InheritedModel<SelectableAspect> {
const SelectableInheritedModel({ const SelectableInheritedModel({
super.key, super.key,