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.settingsScreen.name: (_) => const SettingsFlow(),
NavigationRoutes.filmsListScreen.name: (_) => const FilmsScreen(),
NavigationRoutes.filmAddScreen.name: (_) => const FilmEditFlow(args: FilmEditArgs()),
NavigationRoutes.filmEditScreen.name: (context) => FilmEditFlow(args: context.routeArgs<FilmEditArgs>()),
NavigationRoutes.proFeaturesScreen.name: (_) => LightmeterProScreen(),
NavigationRoutes.timerScreen.name: (context) => TimerFlow(args: context.routeArgs<TimerFlowArgs>()),

View file

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

View file

@ -1,6 +1,5 @@
import 'package:flutter/material.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_resources/m3_lightmeter_resources.dart';
@ -25,13 +24,23 @@ class FilmsProvider extends StatefulWidget {
}
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;
@override
void initState() {
super.initState();
_filmsInUse = widget.storageService.filmsInUse;
_selected = widget.storageService.selectedFilm;
_discardSelectedIfNotIncluded();
}
@ -39,20 +48,34 @@ class FilmsProviderState extends State<FilmsProvider> {
@override
Widget build(BuildContext context) {
return Films(
values: [
const FilmStub(),
...widget.availableFilms ?? films,
],
filmsInUse: [
const FilmStub(),
if (context.isPro) ..._filmsInUse,
],
predefinedFilms: predefinedFilms,
customFilms: customFilms,
selected: context.isPro ? _selected : const FilmStub(),
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) {
_selected = film;
widget.storageService.selectedFilm = film;
@ -60,47 +83,99 @@ class FilmsProviderState extends State<FilmsProvider> {
}
}
void saveFilms(List<Film> films) {
_filmsInUse = films;
widget.storageService.filmsInUse = films;
/* Custom 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();
setState(() {});
}
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();
widget.storageService.selectedFilm = const FilmStub();
}
}
}
class Films extends SelectableInheritedModel<Film> {
final List<Film> filmsInUse;
typedef _SelectableFilm<T extends Film> = ({T film, bool selected});
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({
super.key,
required super.values,
required this.filmsInUse,
required super.selected,
required this.predefinedFilms,
required this.customFilms,
required this.selected,
required super.child,
});
/// [FilmStub()] + all the custom fields with actual reciprocity formulas
static List<Film> of(BuildContext context) {
return InheritedModel.inheritFrom<Films>(context)!.values;
static List<Film> predefinedFilmsOf<T>(BuildContext context) {
return InheritedModel.inheritFrom<Films>(context, aspect: _FilmsModelAspect.predefinedFilmsList)!
.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
static List<Film> inUseOf<T>(BuildContext context) {
return InheritedModel.inheritFrom<Films>(
context,
aspect: SelectableAspect.list,
)!
.filmsInUse;
final model = InheritedModel.inheritFrom<Films>(context, aspect: _FilmsModelAspect.filmsInUse)!;
return [
const FilmStub(),
...model.customFilms.values.where((e) => e.selected).map((e) => e.film),
...model.predefinedFilms.values.where((e) => e.selected).map((e) => e.film),
];
}
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:lightmeter/providers/films_provider.dart';
import 'package:lightmeter/screens/film_edit/event_film_edit.dart';
import 'package:lightmeter/screens/film_edit/state_film_edit.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
import 'package:uuid/uuid.dart';
class FilmEditBloc extends Bloc<FilmEditEvent, FilmEditState> {
static const _defaultFilm = FilmExponential(name: '', iso: 100, exponent: 1.3);
final FilmsProviderState filmsProvider;
final FilmExponential _originalFilm;
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)
: _originalFilm = film,
FilmEditBloc._(
this.filmsProvider,
FilmExponential film,
this._isEdit,
) : _originalFilm = film,
_newFilm = film,
super(
FilmEditState(
@ -19,7 +42,6 @@ class FilmEditBloc extends Bloc<FilmEditEvent, FilmEditState> {
isoValue: IsoValue.values.firstWhere((element) => element.value == film.iso),
exponent: film.exponent,
canSave: false,
isEdit: film != _defaultFilm,
),
) {
on<FilmEditEvent>(
@ -33,6 +55,8 @@ class FilmEditBloc extends Bloc<FilmEditEvent, FilmEditState> {
_onExpChanged(e, emit);
case FilmEditSaveEvent():
_onSave(event, emit);
case FilmEditDeleteEvent():
_onDelete(event, emit);
}
},
);
@ -46,7 +70,6 @@ class FilmEditBloc extends Bloc<FilmEditEvent, FilmEditState> {
isoValue: state.isoValue,
exponent: state.exponent,
canSave: _canSave(event.name, state.exponent),
isEdit: state.isEdit,
),
);
}
@ -59,7 +82,6 @@ class FilmEditBloc extends Bloc<FilmEditEvent, FilmEditState> {
isoValue: event.iso,
exponent: state.exponent,
canSave: _canSave(state.name, state.exponent),
isEdit: state.isEdit,
),
);
}
@ -74,12 +96,35 @@ class FilmEditBloc extends Bloc<FilmEditEvent, FilmEditState> {
isoValue: state.isoValue,
exponent: 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) {
return name.isNotEmpty && exponent != null && _newFilm != _originalFilm;

View file

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

View file

@ -1,13 +1,14 @@
import 'package:flutter/material.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/screen_film_edit.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class FilmEditArgs {
final FilmExponential film;
final FilmExponential? film;
const FilmEditArgs({required this.film});
const FilmEditArgs({this.film});
}
class FilmEditFlow extends StatelessWidget {
@ -18,8 +19,12 @@ class FilmEditFlow extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => FilmEditBloc(args.film),
child: const FilmEditScreen(),
create: (_) => FilmEditBloc(
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';
class FilmEditScreen extends StatefulWidget {
const FilmEditScreen({super.key});
final bool isEdit;
const FilmEditScreen({
required this.isEdit,
super.key,
});
@override
State<FilmEditScreen> createState() => _FilmEditScreenState();
@ -21,18 +26,28 @@ class _FilmEditScreenState extends State<FilmEditScreen> {
@override
Widget build(BuildContext context) {
return SliverScreen(
title: BlocBuilder<FilmEditBloc, FilmEditState>(
buildWhen: (previous, current) => false,
builder: (context, state) => Text(state.isEdit ? S.of(context).editFilmTitle : S.of(context).addFilmTitle),
),
title: Text(widget.isEdit ? S.of(context).editFilmTitle : S.of(context).addFilmTitle),
appBarActions: [
BlocBuilder<FilmEditBloc, FilmEditState>(
buildWhen: (previous, current) => previous.canSave != current.canSave,
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),
),
),
if (widget.isEdit)
IconButton(
onPressed: () {
context.read<FilmEditBloc>().add(const FilmEditDeleteEvent());
Navigator.of(context).pop();
},
icon: const Icon(Icons.delete),
),
],
slivers: [
SliverToBoxAdapter(

View file

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

View file

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

View file

@ -19,7 +19,7 @@ class FilmPicker extends StatelessWidget {
selectedValue: Films.selectedOf(context),
values: Films.inUseOf(context),
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(
value: ReadingValue(
label: _label(context),

View file

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

View file

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