diff --git a/lib/application.dart b/lib/application.dart index 24efde5..87cdfe3 100644 --- a/lib/application.dart +++ b/lib/application.dart @@ -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()), NavigationRoutes.proFeaturesScreen.name: (_) => LightmeterProScreen(), NavigationRoutes.timerScreen.name: (context) => TimerFlow(args: context.routeArgs()), diff --git a/lib/navigation/routes.dart b/lib/navigation/routes.dart index 7f8ac24..4b496f0 100644 --- a/lib/navigation/routes.dart +++ b/lib/navigation/routes.dart @@ -2,6 +2,7 @@ enum NavigationRoutes { meteringScreen, settingsScreen, filmsListScreen, + filmAddScreen, filmEditScreen, proFeaturesScreen, timerScreen, diff --git a/lib/providers/films_provider.dart b/lib/providers/films_provider.dart index d378ee6..83e6872 100644 --- a/lib/providers/films_provider.dart +++ b/lib/providers/films_provider.dart @@ -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 { - late List _filmsInUse; + late final Map> predefinedFilms = Map.fromEntries( + (widget.availableFilms ?? films).map( + (film) => MapEntry( + film.id, + ( + film: film, + selected: widget.storageService.filmsInUse.contains(film), + ), + ), + ), + ); + final Map> 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 { @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 { } } - void saveFilms(List 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 { - final List filmsInUse; +typedef _SelectableFilm = ({T film, bool selected}); + +enum _FilmsModelAspect { + customFilmsList, + predefinedFilmsList, + filmsInUse, + selected, +} + +class Films extends InheritedModel<_FilmsModelAspect> { + final Map> predefinedFilms; + + @protected + final Map> 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 of(BuildContext context) { - return InheritedModel.inheritFrom(context)!.values; + static List predefinedFilmsOf(BuildContext context) { + return InheritedModel.inheritFrom(context, aspect: _FilmsModelAspect.predefinedFilmsList)! + .predefinedFilms + .values + .map((value) => value.film) + .toList(); + } + + static List customFilmsOf(BuildContext context) { + return InheritedModel.inheritFrom(context, aspect: _FilmsModelAspect.customFilmsList)! + .customFilms + .values + .map((value) => value.film) + .toList(); } /// [FilmStub()] + films in use selected by user static List inUseOf(BuildContext context) { - return InheritedModel.inheritFrom( - context, - aspect: SelectableAspect.list, - )! - .filmsInUse; + final model = InheritedModel.inheritFrom(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(context, aspect: SelectableAspect.selected)!.selected; + return InheritedModel.inheritFrom(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; + } } } diff --git a/lib/screens/film_edit/bloc_film_edit.dart b/lib/screens/film_edit/bloc_film_edit.dart index 6fea9d0..f956e71 100644 --- a/lib/screens/film_edit/bloc_film_edit.dart +++ b/lib/screens/film_edit/bloc_film_edit.dart @@ -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 { 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 { isoValue: IsoValue.values.firstWhere((element) => element.value == film.iso), exponent: film.exponent, canSave: false, - isEdit: film != _defaultFilm, ), ) { on( @@ -33,6 +55,8 @@ class FilmEditBloc extends Bloc { _onExpChanged(e, emit); case FilmEditSaveEvent(): _onSave(event, emit); + case FilmEditDeleteEvent(): + _onDelete(event, emit); } }, ); @@ -46,7 +70,6 @@ class FilmEditBloc extends Bloc { isoValue: state.isoValue, exponent: state.exponent, canSave: _canSave(event.name, state.exponent), - isEdit: state.isEdit, ), ); } @@ -59,7 +82,6 @@ class FilmEditBloc extends Bloc { isoValue: event.iso, exponent: state.exponent, canSave: _canSave(state.name, state.exponent), - isEdit: state.isEdit, ), ); } @@ -74,12 +96,35 @@ class FilmEditBloc extends Bloc { isoValue: state.isoValue, exponent: event.exponent, canSave: _canSave(state.name, event.exponent), - isEdit: state.isEdit, ), ); } - Future _onSave(FilmEditSaveEvent _, Emitter emit) async {} + Future _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 _onDelete(FilmEditDeleteEvent _, Emitter emit) async { + filmsProvider.deleteCustomFilm(_originalFilm); + } bool _canSave(String name, double? exponent) { return name.isNotEmpty && exponent != null && _newFilm != _originalFilm; diff --git a/lib/screens/film_edit/event_film_edit.dart b/lib/screens/film_edit/event_film_edit.dart index 1b8980d..405168c 100644 --- a/lib/screens/film_edit/event_film_edit.dart +++ b/lib/screens/film_edit/event_film_edit.dart @@ -25,3 +25,7 @@ class FilmEditExpChangedEvent extends FilmEditEvent { class FilmEditSaveEvent extends FilmEditEvent { const FilmEditSaveEvent(); } + +class FilmEditDeleteEvent extends FilmEditEvent { + const FilmEditDeleteEvent(); +} diff --git a/lib/screens/film_edit/flow_film_edit.dart b/lib/screens/film_edit/flow_film_edit.dart index 45dc616..556e770 100644 --- a/lib/screens/film_edit/flow_film_edit.dart +++ b/lib/screens/film_edit/flow_film_edit.dart @@ -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), ); } } diff --git a/lib/screens/film_edit/screen_film_edit.dart b/lib/screens/film_edit/screen_film_edit.dart index c8b7e24..7692f9c 100644 --- a/lib/screens/film_edit/screen_film_edit.dart +++ b/lib/screens/film_edit/screen_film_edit.dart @@ -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 createState() => _FilmEditScreenState(); @@ -21,18 +26,28 @@ class _FilmEditScreenState extends State { @override Widget build(BuildContext context) { return SliverScreen( - title: BlocBuilder( - 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( buildWhen: (previous, current) => previous.canSave != current.canSave, builder: (context, state) => IconButton( - onPressed: state.canSave ? () {} : null, + onPressed: state.canSave + ? () { + context.read().add(const FilmEditSaveEvent()); + Navigator.of(context).pop(); + } + : null, icon: const Icon(Icons.save), ), ), + if (widget.isEdit) + IconButton( + onPressed: () { + context.read().add(const FilmEditDeleteEvent()); + Navigator.of(context).pop(); + }, + icon: const Icon(Icons.delete), + ), ], slivers: [ SliverToBoxAdapter( diff --git a/lib/screens/film_edit/state_film_edit.dart b/lib/screens/film_edit/state_film_edit.dart index 2db7b79..caa25c0 100644 --- a/lib/screens/film_edit/state_film_edit.dart +++ b/lib/screens/film_edit/state_film_edit.dart @@ -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, }); } diff --git a/lib/screens/films/screen_films.dart b/lib/screens/films/screen_films.dart index 62f9a24..2072eed 100644 --- a/lib/screens/films/screen_films.dart +++ b/lib/screens/films/screen_films.dart @@ -55,12 +55,12 @@ class _FilmsScreenState extends State 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( - films: Films.of(context).skip(1).whereType().toList(), - onFilmSelected: (film, value) {}, + films: Films.customFilmsOf(context).toList(), + onFilmSelected: FilmsProvider.of(context).toggleFilm, onFilmEdit: _editFilm, ), ], @@ -72,8 +72,8 @@ class _FilmsScreenState extends State 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(), ); } diff --git a/lib/screens/metering/components/shared/readings_container/components/film_picker/widget_picker_film.dart b/lib/screens/metering/components/shared/readings_container/components/film_picker/widget_picker_film.dart index b4af640..e46893f 100644 --- a/lib/screens/metering/components/shared/readings_container/components/film_picker/widget_picker_film.dart +++ b/lib/screens/metering/components/shared/readings_container/components/film_picker/widget_picker_film.dart @@ -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), diff --git a/lib/screens/settings/components/metering/components/metering_screen_layout/widget_list_tile_metering_screen_layout.dart b/lib/screens/settings/components/metering/components/metering_screen_layout/widget_list_tile_metering_screen_layout.dart index 242b5e4..f7c412b 100644 --- a/lib/screens/settings/components/metering/components/metering_screen_layout/widget_list_tile_metering_screen_layout.dart +++ b/lib/screens/settings/components/metering/components/metering_screen_layout/widget_list_tile_metering_screen_layout.dart @@ -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); }, diff --git a/lib/utils/selectable_provider.dart b/lib/utils/selectable_provider.dart index c18f998..c7a8aa8 100644 --- a/lib/utils/selectable_provider.dart +++ b/lib/utils/selectable_provider.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; enum SelectableAspect { list, selected } + + class SelectableInheritedModel extends InheritedModel { const SelectableInheritedModel({ super.key,