mirror of
https://github.com/vodemn/m3_lightmeter.git
synced 2024-11-21 15:00:40 +00:00
implemented CRUD for custom films
This commit is contained in:
parent
9884a6147f
commit
68cecf5391
12 changed files with 207 additions and 59 deletions
|
@ -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>()),
|
||||
|
|
|
@ -2,6 +2,7 @@ enum NavigationRoutes {
|
|||
meteringScreen,
|
||||
settingsScreen,
|
||||
filmsListScreen,
|
||||
filmAddScreen,
|
||||
filmEditScreen,
|
||||
proFeaturesScreen,
|
||||
timerScreen,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -25,3 +25,7 @@ class FilmEditExpChangedEvent extends FilmEditEvent {
|
|||
class FilmEditSaveEvent extends FilmEditEvent {
|
||||
const FilmEditSaveEvent();
|
||||
}
|
||||
|
||||
class FilmEditDeleteEvent extends FilmEditEvent {
|
||||
const FilmEditDeleteEvent();
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
|
|
|
@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
|
|||
|
||||
enum SelectableAspect { list, selected }
|
||||
|
||||
|
||||
|
||||
class SelectableInheritedModel<T> extends InheritedModel<SelectableAspect> {
|
||||
const SelectableInheritedModel({
|
||||
super.key,
|
||||
|
|
Loading…
Reference in a new issue